Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
100% found this document useful (1 vote)
2K views

Enhancing Visual Basic .NET Beyond The Scope of Visual Basic 6.0

Updated and Expanded. Taking what one learned under VB6 and turbo charge it with VB.NET savvy. This document reviews the fundamental differences between Win32 VB6 and VB.NET, and how to deal with and exploit those changes. You will learn how Classes really work, why they make applications powerful, how classes are abstracted to define Structures, how to clone a class or structure, how a sort or binary search works, how to declare .NET Native Fixed Strings, explain why .NET's Managed Space gives VB unbridled power, how Delegate Classes work, the cornerstone of every single method defined in .NET, how to invoke Delegate constructors, why Delegates are a powerful feature we should embrace, not ignorantly scorn, to explain the difference between persistent and non-persistent control images and why this distinction exists, etc. You know – all the really COOL stuff! A plethora of solutions are featured, along with generous code examples that solve real-world problems, not being just the typical academic kinder-care snippets, showing you how to supercharge VB.NET code to perform virtual coding miracles, though often using very simple code, and even how to do what even Microsoft says is impossible (some of these coding roadblocks have been declared impossible since the 1960s). A generous cornucopia of articles are also included, demonstrating often simple coding techniques to accomplish invredible coding feats for what too many self-described online coding "gurus" have claimed was impossible. I will also share lots of amazing trivia, a truckload of VB.NET SECRET coding techniques, anecdotes, snappy “old-guy” euphemisms, and memories from ancient times as a young developer when I could only write code between dinosaur migrations.

Uploaded by

David Ross Goben
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as PDF, TXT or read online on Scribd
100% found this document useful (1 vote)
2K views

Enhancing Visual Basic .NET Beyond The Scope of Visual Basic 6.0

Updated and Expanded. Taking what one learned under VB6 and turbo charge it with VB.NET savvy. This document reviews the fundamental differences between Win32 VB6 and VB.NET, and how to deal with and exploit those changes. You will learn how Classes really work, why they make applications powerful, how classes are abstracted to define Structures, how to clone a class or structure, how a sort or binary search works, how to declare .NET Native Fixed Strings, explain why .NET's Managed Space gives VB unbridled power, how Delegate Classes work, the cornerstone of every single method defined in .NET, how to invoke Delegate constructors, why Delegates are a powerful feature we should embrace, not ignorantly scorn, to explain the difference between persistent and non-persistent control images and why this distinction exists, etc. You know – all the really COOL stuff! A plethora of solutions are featured, along with generous code examples that solve real-world problems, not being just the typical academic kinder-care snippets, showing you how to supercharge VB.NET code to perform virtual coding miracles, though often using very simple code, and even how to do what even Microsoft says is impossible (some of these coding roadblocks have been declared impossible since the 1960s). A generous cornucopia of articles are also included, demonstrating often simple coding techniques to accomplish invredible coding feats for what too many self-described online coding "gurus" have claimed was impossible. I will also share lots of amazing trivia, a truckload of VB.NET SECRET coding techniques, anecdotes, snappy “old-guy” euphemisms, and memories from ancient times as a young developer when I could only write code between dinosaur migrations.

Uploaded by

David Ross Goben
Copyright
© Attribution Non-Commercial (BY-NC)
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 774

th

11 Edition

Enhancing Visual Basic .NET


Applications Far Beyond the
Scope of Visual Basic 6.0
“How to do the things that you used to do under VB6 even better under VB.NET,
and how to enhance VB.NET applications to accomplish so very much more.”

David Ross Goben


(The above two logos. Microsoft Visual Studio®, and Microsoft Windows®
are registered trademarks of Microsoft Corporation)
 Page 2

Enhancing Visual Basic .NET


Applications Far Beyond the
Scope of Visual Basic 6.0
11th Edition
“How to do the things that you used to do under VB6 even better under VB.NET,
and how to enhance VB.NET applications to accomplish so very much more.”

David Ross Goben

Copyright © 2010-2021 by David Ross Goben. All rights reserved.


Reproduction or uses of this manual without express written permission from David Ross Goben is prohibited, except for personal use and
to publicly share it, which means you can post it, upload it, and share it freely as long as there is no pay wall people might have to buy their
way through to access it freely, or by charging them an "access fee". You think you deserve compensation for what little work you did just
to hit a button to upload it to the web or copy it to a disc? What about me? If I can distribute it at no charge, even after working on this
tome for over a decade, then so can you. While reasonable effort has been taken in the preparation of this document to make it as complete
and as accurate as possible, no warranty or fitness is implied. The information is herein provided on an “as is” basis, and the author and any
authorized distributor shall have neither liability nor responsibility to any person or entity with respect to any damages or loss of revenues,
time, money or properties in any form arising from the information contained within this document, nor for any errors or omissions.
Besides, if you cannot take personal responsibility for testing and verifying your for-pay work that you base on mine, then why do you even
have a job? Who are you? Obama? Note finally I reserve the right to update this document at will, because I take enough responsibility for
finding and correcting errors and omissions I happen upon herein, and I reserve the right to re-posting them without notice as time permits.
(Last updated September 21, 2021)
This title was originally distributed as “Visual Basic .NET Compared to Visual Basic 6.0”, first published July, 2010.
It now also incorporates my book, “Navigating Your Way Through Visual Basic 6.0 to Visual Basic.NET Application Upgrades”, published July, 2010.
NOTE: You can download the latest update of this PDF document for free at Google Docs:
https://drive.google.com/file/d/1Iti2vRdoeywVeLb9INE-TTh6qVWyLdlg
NOTE: You can download this document’s source code listings for the code consisting of more than just a few
lines, unbroken by page breaks, and with spacing fully intact, all wrapped within a massive RichText file named
“Code Excerpts from Enhancing Visual Basic .NET.rtf” from the cloud in my public Google Drive folder:
https://drive.google.com/file/d/1_9-FnqGRe8qpcSsHxc-WzQCrQkG0hk-h/view?usp=sharing

IMPORTANT NOTICE
The opinions expressed herein, to include the jokes and ribbing, are those of the author and are not
opinions advocated or sanctioned (or even appreciated) by any other individual or body. These are the
personal views of an independent software engineer, a solution, operating system, and software
language designer, and all criticisms, praises, endorsements, to include disapprovals or even snide
remarks, are entirely his own. He is also probably the only one from among a multitude of C++
developers who will admit, even in public, to truly loving to develop applications under VB.NET. The
rest are apparently waiting for an anonymous support group to be formed.
I LOVE helping developers and hobbyists, though I rib them — take it as a love tap. However, those
writing code in ways troglidytes would never be caught extinct doing, yet boast themselves advanced, or
even expert developers (experts being those already having made all the mistakes), I do not think I
possess sufficient faculty to adequately describe my reaction. Maybe they took those pills made from
jellyfish that are supposed to improve memory and intelligence. But, for the life of me, I cannot recall
ever hearing of any jellyfish being cognizant enough to ace scholastic exams, let alone preschool, or
having an IQ higher than a turnip. Besides, when you tell someone they have the brains of a jellyfish,
they seem not to take it as a complement, saying such things as "Oh shucks, that's the nicest thing you've
ever said to me!", but instead tend to react with decidedly gutteral oaths, followed by a swift right cross.
 Page 3

Table of Contents.
Principle Table of Content Sections.
Foreword and Introduction.....................................................................................................................................................3
VB.NET Versus VB6 Platform Differences............................................................................................................................3
Featured Articles....................................................................................................................................................................6
Additional "Black Book" Tips..................................................................................................................................................9
Free Online PDF Documents Available by David Ross Goben...........................................................................................11
Foreword and Introduction. 12
th
Foreword to the 11 Edition......................................................................................................................................................13
Why I Share This Book for Free....................................................................................................................................................15
Running VB6 on Recent Windows Operating Systems................................................................................................................16
Introduction................................................................................................................................................................................17
Why Is VB.NET Not 100% Compatible to VB6?...........................................................................................................................20
Why .NET – An Outsider's Perspective........................................................................................................................................22
What is .NET?................................................................................................................................................................................................... 22
What is the .NET Framework?........................................................................................................................................................................... 23
What is a Namespace?..................................................................................................................................................................................... 24
What is an Application Domain?........................................................................................................................................................................ 25
What is a Solution/Assembly?........................................................................................................................................................................... 25
What is a Project?............................................................................................................................................................................................. 26
A Crash Course on the Heap and the Stack.............................................................................................................................26
Object-Oriented Programming – A Quick-Start Guide............................................................................................................28
Classes and Objects.......................................................................................................................................................................................... 28
Classes and Structures – A Closer Look............................................................................................................................................................ 30
Fields, Properties, Methods, and Events........................................................................................................................................................... 31
Encapsulation, Inheritance, and Polymorphism................................................................................................................................................. 32
Overloading, Overriding, and Shadowing.......................................................................................................................................................... 33
Scoping Rules................................................................................................................................................................................................... 33
My Four Basic Guidelines for VB.NET Development..............................................................................................................35
Closing Introductory Remarks..................................................................................................................................................36
VB.NET Versus VB6 Platform Differences. 39
Noteworthy VB.NET Features That Differ From, or Are New Since VB6..............................................................................40
Notes on Using the VB6 Compatibility Library..............................................................................................................................41
Notes on Useful Format and ToString Date/Time Shortcuts.........................................................................................................42
Notes on Passing Structures to Win32 Pinvokes..........................................................................................................................42
Notes on Parameterized and Non-Parameterized Arrays.............................................................................................................43
Notes on Mixing Fixed Strings, Fixed Arrays and Structures........................................................................................................45
A Note on Passing Parameters ByVal...........................................................................................................................................50
A Note on Returning Value Data via Return..................................................................................................................................51
A Note on Multi-Variable Declarations..........................................................................................................................................52
Notes on Collection Data Now Being Objects, Not Just Strings...................................................................................................52
A Note on VB.NET Default Properties Not Being Parameterless.................................................................................................54
A Note on VB.NET Having Dropped the 'Let' Keyword.................................................................................................................54
A Note on VB6 Versus VB.NET Variable Definitions.....................................................................................................................54
A Note on Edit and Continue........................................................................................................................................................55
A Note on Losing the VB6 BackStyle Property.............................................................................................................................55
A Note on Recovering VarPtr, ObjPtr, StrPtr, VarPtrArray, and StrPtrArray...................................................................................56
A Note on RichTextBox Property Renaming During Upgrades.....................................................................................................56
Notes on Recovering the LenB Function......................................................................................................................................57
Notes on Passing Parameters “As Any” to Pinvokes....................................................................................................................58
A Note on Common Dir Function Warnings..................................................................................................................................59
Notes on Fixing Item Issues and Enhancing Collections..............................................................................................................59
Notes on Avoiding Late-Bound Object References.......................................................................................................................60
A Note on VB6 Parameterless Defaults During Upgrades ...........................................................................................................61
A Note About VB6 Null Propagation..............................................................................................................................................61
A Note on Referencing Objects Before They Are Initialized..........................................................................................................62
A Short Note on Dealing with Renamed Properties......................................................................................................................62
 Page 4

A Note on Finding the ListCount property.....................................................................................................................................62


Notes on Changes in the Mouse Cursor Property........................................................................................................................62
A Note on Changes to the VB6 RECT Structure...........................................................................................................................64
Notes on Initialized and Terminate Event Upgrades.....................................................................................................................64
A Note on Changes to FormWindowState Enumeration Conflicts................................................................................................65
Notes on VB6 Namespace Twips Upgrade Conversions..............................................................................................................65
A Note on Fixing User-Defined Twips Constants..........................................................................................................................66
A Note on Speeding Returned VB6 Namespace List Item Values................................................................................................66
A Note on VB6 Date Value Conversions.......................................................................................................................................66
A Note on Speeding Format Command Use in VB.NET...............................................................................................................67
A Note on Accessing Screen Properties.......................................................................................................................................67
Notes on On Iexpr GOTO and How to Sort Without GOTO..........................................................................................................68
A Note on On Iexpr GoSub...........................................................................................................................................................71
Notes on Updating VB6 Error Trapping E-Z and Painlessly..........................................................................................................71
Notes on Changes to Common Dialogs and E-Z Upgrade Recovery...........................................................................................73
Notes on VB6.CopyArray and How to Improve It..........................................................................................................................75
Notes on the E-Z Recovery of the ItemData List Object Property.................................................................................................77
A Short Note on VB6’s Automatic Boolean Conversions...............................................................................................................78
Notes on Option Strict On and Why We Should Use It.................................................................................................................78
Notes on Accessing the Picture Property in Older Components...................................................................................................79
Notes on the Loss of VB6 Control Arrays and How to Quickly Create Them................................................................................80
Notes on the Upgraded MouseMove Parameter List....................................................................................................................84
Notes on Getting MouseMove Event Information.........................................................................................................................84
A Note on Changes to remotely firing Button Clicks.....................................................................................................................86
Notes on No AVI Animation Control in VB.NET and How to Add a Free One...............................................................................86
Notes on Changes to Resources Management............................................................................................................................88
A Note on Improved Collection and List Clearing..........................................................................................................................91
Notes on Adding Auxiliary Files to ClickOnce Deployment (Publish)............................................................................................92
A Short Note on Changes to VB6 TextBox SelStart and SelLen Properties..................................................................................93
Notes on Welcomed Changes to ToolTips....................................................................................................................................93
A Note on Changes to ListView.....................................................................................................................................................94
Notes on Handling Toolbar Button and Button Menu Clicks..........................................................................................................94
A Note on the Unload Form Command.........................................................................................................................................96
A Note on Finding the Lost VB6 NewIndex Property....................................................................................................................97
Notes on Process Handling in the KeyPress, KeyDown, and KeyUp Events...............................................................................97
Notes on Invoking Handled Events Under VB.NET....................................................................................................................100
A Short Note on TextBox Locked Property Changes..................................................................................................................102
A Short Note on changes to the Tag Property.............................................................................................................................102
Notes on Changes to the GotFocus and LostFocus Events.......................................................................................................102
A Short Note on Changes to the VB6 SetFocus Command........................................................................................................103
A Short Note on Long-Pathing Through Namespaces................................................................................................................103
Notes on Changes to Multiple Document Interfaces...................................................................................................................103
A Note on Changes to a Button’s Cancel and Default Properties...............................................................................................104
A Short Note on Changes to CheckBoxes..................................................................................................................................104
A Note on Property Naming Conflicts With VB Commands........................................................................................................104
A Note on Using Icons for Menu Images Under VB.NET............................................................................................................104
Notes on VB6 FSO Text Streaming and VB.NET File Streaming................................................................................................105
A Note on Changes to Counting CheckBoxed ListBoxes............................................................................................................105
Notes on Changes to Setting Mouse Cursor Icons.....................................................................................................................106
Notes on Making VB.NET Drag and Drop E-Z............................................................................................................................106
Notes on the Loss of MAPI Controls (and getting them back!)...................................................................................................109
A Note on Displaying a Checkbox as a Button............................................................................................................................110
Notes on some Click Events Changing to SelectedIndexChanged Events.................................................................................110
Notes on VB6-style Printing Under VB.NET Made E-Z...............................................................................................................111
Notes on the VB.NET GET and SET Property Format................................................................................................................112
A Note on the Support of Using...End Using...............................................................................................................................113
A Short Note on the New 'IsNot' Keyword...................................................................................................................................113
A Short Note on the New 'AndAlso' Keyword and Why We Should Use It..................................................................................113
 Page 5

A Short Note on the New 'OrElse' Keyword and Why We Should Use It....................................................................................113
A Note on Overloading, and Why We Should Welcome It..........................................................................................................113
A Short Note on New Variable Types SByte, UShort, UInteger, ULong, and UIntPtr..................................................................114
A Note on Partial Classes and Structures...................................................................................................................................114
A Note on VB.NET Form-Linked Controls...................................................................................................................................115
A Short Note on Snippets............................................................................................................................................................ 115
A Short Note on the Immediate Window.....................................................................................................................................115
A Short Note on the Continue Statement....................................................................................................................................115
A Note on Structured Exception Handling...................................................................................................................................115
A Note on Type Inference............................................................................................................................................................115
A Note on Anonymous Typing..................................................................................................................................................... 116
A Note on Enumerators............................................................................................................................................................... 116
A Note on Optional Event Parameters........................................................................................................................................116
Notes on the MY Namespace..................................................................................................................................................... 116
Notes on the Return of the Forms Collection..............................................................................................................................117
Notes on Class Construction and Destruction – a Detailed Look...............................................................................................117
Notes on New Style Pinvoke Signature – a Detailed Look.........................................................................................................120
A Note on the VB.NET Form Event Firing Sequence.................................................................................................................124
Notes on Form Command Changes – a Detailed Look..............................................................................................................124
A Note on Type Object as the New Universal Data Type............................................................................................................127
A Note on Finding the Instance Handle......................................................................................................................................127
A Note on Checking For a Previous Application Instance...........................................................................................................128
A Note on Getting the App Title...................................................................................................................................................129
A Note on Getting the App Path and the App EXE Name.................................................................................................................... 128
A Note on Collections Enhancements........................................................................................................................................129
Notes on Registry I/O................................................................................................................................................................. 129
A Note on Adjusting Form Opacity (Transparency).....................................................................................................................129
A Note on Literal Type Characters..............................................................................................................................................130
Notes on Loading Images and Creating a LoadPicture Function...............................................................................................130
Notes on Loading Icons.............................................................................................................................................................. 131
Notes on Embedding Classes Within Classes............................................................................................................................132
Notes on the Checked State of a CheckedListBox.....................................................................................................................132
Notes on New String Manipulation Features..............................................................................................................................134
Notes on Implicit Declaration of VB6 Object Arrays....................................................................................................................134
An Exhaustive List of VB6 Commands Compared to VB.NET............................................................................................136
VB.NET Behavior Changes Deemed Important by VB6 Aficionados..................................................................................149
Higher Math Functions.................................................................................................................................................................................................................................................................. 150
# – Various Compiler Directives................................................................................................................................................................................................................................................... 150
AddressOf...................................................................................................................................................................................................................................................................................... 150
AppActivate.................................................................................................................................................................................................................................................................................... 151
Array............................................................................................................................................................................................................................................................................................... 151
AscB, ChrB, InputB, InstrB, LeftB, LenB, MidB, RightB............................................................................................................................................................................................................. 151
Calendar......................................................................................................................................................................................................................................................................................... 151
CDate, IsDate.................................................................................................................................................................................................................................................................................. 151
Close#............................................................................................................................................................................................................................................................................................. 152
Const............................................................................................................................................................................................................................................................................................... 152
CStr, Str.......................................................................................................................................................................................................................................................................................... 152
CVar................................................................................................................................................................................................................................................................................................. 152
CVErr............................................................................................................................................................................................................................................................................................... 152
Date, Date$..................................................................................................................................................................................................................................................................................... 152
Declare............................................................................................................................................................................................................................................................................................ 152
Dim.................................................................................................................................................................................................................................................................................................. 153
Dim (arrays)..................................................................................................................................................................................................................................................................................... 153
Dir.................................................................................................................................................................................................................................................................................................... 153
DoEvents........................................................................................................................................................................................................................................................................................ 153
End.................................................................................................................................................................................................................................................................................................. 154
EndIf................................................................................................................................................................................................................................................................................................ 154
Enum............................................................................................................................................................................................................................................................................................... 154
EOF#............................................................................................................................................................................................................................................................................................... 154
Eqv.................................................................................................................................................................................................................................................................................................. 154
Erase .............................................................................................................................................................................................................................................................................................. 154
Error................................................................................................................................................................................................................................................................................................ 155
FileAttr............................................................................................................................................................................................................................................................................................ 155
FileDateTime................................................................................................................................................................................................................................................................................... 155
Get#, Put#....................................................................................................................................................................................................................................................................................... 155
GoSub............................................................................................................................................................................................................................................................................................. 155
ImeStatus........................................................................................................................................................................................................................................................................................ 155
Imp.................................................................................................................................................................................................................................................................................................. 155
Implements..................................................................................................................................................................................................................................................................................... 155
 Page 6

Input#.............................................................................................................................................................................................................................................................................................. 156
InputBox......................................................................................................................................................................................................................................................................................... 156
IsEmpty........................................................................................................................................................................................................................................................................................... 156
IsMissing........................................................................................................................................................................................................................................................................................ 156
IsNull............................................................................................................................................................................................................................................................................................... 156
IsObject........................................................................................................................................................................................................................................................................................... 156
Left, Right....................................................................................................................................................................................................................................................................................... 156
Len.................................................................................................................................................................................................................................................................................................. 157
Let, Set............................................................................................................................................................................................................................................................................................ 157
Line Input#...................................................................................................................................................................................................................................................................................... 158
Load................................................................................................................................................................................................................................................................................................ 158
LoadPicture.................................................................................................................................................................................................................................................................................... 158
LoadResBitmap, LoadResData, LoadResString......................................................................................................................................................................................................................... 158
LOC#............................................................................................................................................................................................................................................................................................... 158
Lock#.............................................................................................................................................................................................................................................................................................. 158
LOF#................................................................................................................................................................................................................................................................................................ 158
LSet, Rset....................................................................................................................................................................................................................................................................................... 158
Mod................................................................................................................................................................................................................................................................................................. 159
MsgBox........................................................................................................................................................................................................................................................................................... 159
Name............................................................................................................................................................................................................................................................................................... 159
Next................................................................................................................................................................................................................................................................................................. 159
ObjPtr, VarPtr, VarPtrAry, StrPtr, StrPtrAry.................................................................................................................................................................................................................................. 159
On...GoSub..................................................................................................................................................................................................................................................................................... 160
On...Goto........................................................................................................................................................................................................................................................................................ 160
Open#.............................................................................................................................................................................................................................................................................................. 160
Option Base.................................................................................................................................................................................................................................................................................... 160
Option Explicit............................................................................................................................................................................................................................................................................... 160
Option Private................................................................................................................................................................................................................................................................................ 160
Print, Print#.................................................................................................................................................................................................................................................................................... 160
Property Get, Property Let, Property Set..................................................................................................................................................................................................................................... 160
ReDim............................................................................................................................................................................................................................................................................................. 161
Rem................................................................................................................................................................................................................................................................................................. 161
Return............................................................................................................................................................................................................................................................................................. 162
SavePicture.................................................................................................................................................................................................................................................................................... 162
Seek#.............................................................................................................................................................................................................................................................................................. 162
Spc.................................................................................................................................................................................................................................................................................................. 162
Split ................................................................................................................................................................................................................................................................................................ 162
Static............................................................................................................................................................................................................................................................................................... 162
Stop................................................................................................................................................................................................................................................................................................. 163
Str.................................................................................................................................................................................................................................................................................................... 163
StrConv........................................................................................................................................................................................................................................................................................... 163
String, String$................................................................................................................................................................................................................................................................................ 164
Tab................................................................................................................................................................................................................................................................................................... 164
Time, Time$.................................................................................................................................................................................................................................................................................... 164
Timer............................................................................................................................................................................................................................................................................................... 164
To..................................................................................................................................................................................................................................................................................................... 164
Type...End Type.............................................................................................................................................................................................................................................................................. 164
TypeName....................................................................................................................................................................................................................................................................................... 165
TypeOf............................................................................................................................................................................................................................................................................................. 165
Unload............................................................................................................................................................................................................................................................................................. 166
Unlock#........................................................................................................................................................................................................................................................................................... 166
UserControl.................................................................................................................................................................................................................................................................................... 166
VarType........................................................................................................................................................................................................................................................................................... 166
Wend............................................................................................................................................................................................................................................................................................... 167
Width#............................................................................................................................................................................................................................................................................................. 167
Write#.............................................................................................................................................................................................................................................................................................. 167

Featured Articles. 168


VB6 Aficionado Complaint Department.................................................................................................................................169
String Format Changes Between VB6 and VB.NET..............................................................................................................197
Adapting Win32 String Data Types for VB.NET Interop.......................................................................................................199
Fix-Length Strings.................................................................................................................................................................................................... 199
Strings Used in Interfaces, such as Structures......................................................................................................................................................... 200
Strings Used in Platform Invocations........................................................................................................................................................................ 200
Strings Used in Structures........................................................................................................................................................................................ 201
Fixed-Length String Buffers...................................................................................................................................................................................... 202
Dealing with "As Any" in Pinvoke Signatures............................................................................................................................................................ 202
Using the AsAny Marshalling Parameter................................................................................................................................................................... 203
List of All .NET UnmanagedType Members.............................................................................................................................................................. 204
VB6 User-Defined Types Compared to VB.NET Structures.................................................................................................206
Proving VB6 User Defined Type Members are Properties, Not Variable Fields...................................................................................210
Passing Data Between Protected and Unprotected Memory Under VB.NET.....................................................................212
Comparing VB System I/O Commands; Dir Vs. FSO Vs. FileStreams................................................................................216
Releasing COM Objects from Memory Under VB.NET.........................................................................................................218
Tools for REALLY Looking Under the Hood of Visual Basic .NET......................................................................................219
 Page 7

Upgrading Administrative Rights Checks.............................................................................................................................221


Running applications that require Administrative Privileges................................................................................................................. 221
Check for the User having Administrative Rights................................................................................................................................. 222
Running Applications Requiring Administrative Rights without Being Prompted..................................................................................222
Disabling User Account Control Through the User Account Settings..................................................................................................................................223
Disabling User Account Control through the Control Panel.................................................................................................................................................223
Disabling User Account Control through MSCONFIG..........................................................................................................................................................223
Disabling User Account Control through Editing the Registry..............................................................................................................................................223
Cloning Structures Containing Object References Made E-Z.................................................................................................224
The Secret Truth Behind Accessing Private Class Members.......................................................................................................227
Restoring the VB6 APP Command For Use Under VB.NET.....................................................................................................229
VB.NET Generics Collection Classes....................................................................................................................................231
Generic Collections.............................................................................................................................................................................. 231
Generics.............................................................................................................................................................................................. 231
Generics Collection Classes................................................................................................................................................................ 233
How to Perform E-Z Font Manipulation at Runtime Under VB.NET....................................................................................234
Easily Change Font Sizes at Runtime................................................................................................................................................. 234
Easily Change Font Styles at Runtime (Bold, Italic, Underline, and Strikethrough).............................................................................234
Easily Change Font Names at Runtime............................................................................................................................................... 235
Building a Suite of Runtime Font Modification Tools............................................................................................................................ 236
Drawing VB.NET Labels with Transparency Made E-Z........................................................................................................239
Rendering Transparency Labels Over Controls................................................................................................................................... 239
Rendering Text Directly onto Controls.................................................................................................................................................. 241
Rendering Text Across Multiple Controls............................................................................................................................................. 242
Emulating VB6 Image Control Features Under VB.NET.......................................................................................................243
Painting Images on a Form.................................................................................................................................................................. 243
Image with Transparency Rendering over Multiple Controls................................................................................................................ 250
Emulating VB6 Image Controls Using a PictureBox............................................................................................................................ 252
Emulating VB6 Image Controls Using a Resource Icon....................................................................................................................... 253
Emulating VB6 Image Controls Using a Borderless Form................................................................................................................... 254
Room for Improvement......................................................................................................................................................................... 257
Super-Fast No-Holds-Barred VB6 Image Emulation............................................................................................................................ 262
Emulating Mouse Interaction on Drawn Images.................................................................................................................................. 263
Conclusion........................................................................................................................................................................................... 264
Implementing E-Z Unions Under VB.NET..............................................................................................................................265
Structures that use Sub-Structures and Union Sub-Structures............................................................................................................ 267
Understanding VB.NET Imports.............................................................................................................................................270
Namespaces, Modules, and Non-Inheritable Classes: A Rose by Any Other Name............................................................................270
Using Imports Aliases.......................................................................................................................................................................... 271
Referencing Class Code without Importing or Instantiation................................................................................................................. 271
Understanding VB.NET Delegates.........................................................................................................................................272
Delegates in Events............................................................................................................................................................................. 272
Delegates and AddressOf.................................................................................................................................................................... 273
Delegates and Callbacks..................................................................................................................................................................... 274
Invoking Methods through Delegates.................................................................................................................................................. 275
Using a Delegate CallBack to Perform Shell-Metzner Sorts on ANY 1D Array....................................................................................276
Closing Notes on Delegates................................................................................................................................................................ 280
Adding Run-Time Custom Menus and Cloning ToolStripMenuItems Under VB.NET.......................................................282
Understanding Menus by Converting a MenuStrip to a MainMenu...................................................................................................... 282
Optimizing MainMenu Design Code.................................................................................................................................................... 287
Adding a MainMenu and ContextMenu Control to Your VB.NET Toolbox............................................................................................290
Building ContextMenuStrip PopUp Menus on the Fly.......................................................................................................................... 290
Building MenuStrips on the Fly............................................................................................................................................................ 291
Cloning MenuStrip Items with a Cloneable ToolStripMenuItem Class.................................................................................................. 292
Using cToolStripMenuItem.CloneMenu to Create New MergeMenuStrip Methods..............................................................................295
Conclusion........................................................................................................................................................................................... 296
Adding Window and File Lists to VB.NET Menus.................................................................................................................297
Maintaining an Active Child Windows List...................................................................................................................................297
Adding an Open Window List to a VB.NET MenuStrip Menu........................................................................................................................... 298
Maintaining a Most Recently Used File List................................................................................................................................299
 Page 8

Flicker-Free Home-Brew Form Resizing (an E-Z Intro to VB.NET SubClassing)...............................................................305


The Minimalist Solution for Form Resizing Flicker......................................................................................................................306
Adding a Little Pizzazz to the Minimalist Solution.......................................................................................................................307
Adding More Pizzazz to Build a More Stable Solution................................................................................................................307
Adding Alotta Pizzazz: Professional Results by Intercepting the Windows Message Queue.....................................................308
Subclassing Under VB6 (an exercise in patience).................................................................................................................................309
Subclassing Under VB.NET Made E-Z..................................................................................................................................................312
Conclusion................................................................................................................................................................................................... 315
Centering System Dialogs Over VB.NET Forms Using the Thread Message Queue........................................................316
The Simple Explanation of Centering a Message Box on a Form..............................................................................................316
A More Detailed Explanation of Centering a Message Box on a Form.......................................................................................316
The Detailed Explanation of Centering a Message Box on a Form............................................................................................317
Putting It All Together.................................................................................................................................................................. 321
Running Our Own VB.NET Free-Threading Background Tasks..........................................................................................324
E-Z Ways to Draw Lines and Shapes, and to Paint Under VB.NET.....................................................................................325
First, News of Free VB.NET Line and Shape Controls........................................................................................................................ 325
Drawing Lines from the Paint Event..................................................................................................................................................... 325
Drawing Rectangles from the Paint Event........................................................................................................................................... 327
Drawing Ellipses (Circular Shapes) from the Paint Event.................................................................................................................... 329
Drawing Rounded Rectangles from the Paint Event............................................................................................................................ 329
Drawing Other Shapes from the Paint Event....................................................................................................................................... 334
Painting Backgrounds with HatchBrush Patterns from the Paint Event............................................................................................... 335
Painting Flood Fills from the Paint Event............................................................................................................................................. 336
GDI FloodFill................................................................................................................................................................................................... 336
GDI+ FloodFill................................................................................................................................................................................................. 339
Drawing Shapes Outside of the Paint Event........................................................................................................................................ 343
Conclusion........................................................................................................................................................................................... 344
Restoring Raster Graphics and Rubber Bands to .NET.......................................................................................................345
Getting an Image Graphics Object Without Going Through the Paint Event..............................................................................346
Restoring Raster Operations to .NET.........................................................................................................................................347
Getting One's Hands Dirty................................................................................................................................................................................ 347
The GDI32 Class.............................................................................................................................................................................................. 349
Emulating a Selection Rubber Band Under GDI.........................................................................................................................357
Emulating a Selection Rubber Band Under GDI+.......................................................................................................................359
Using the GDI+ DrawReversibleFrame Method................................................................................................................................................ 359
Using the GDI+ DrawRectangle Method and a Translucent Brush.................................................................................................................... 361
Extending VB.NET Controls Functionality............................................................................................................................363
Demonstrating Control Extension Using the VB.NET ComboBox (and ListBox).........................................................................363
Win32 ComboBox Messages........................................................................................................................................................................... 364
The CB_DIR Message (and how to make it work under VB.NET).................................................................................................................... 365
The CB_SETTOPINDEX and CB_GETTOPINDEX Messages......................................................................................................................... 367
The CB_SETEXTENDEDUI and CB_GETEXTENDEDUI Messages................................................................................................................ 369
The CB_SETCURSEL Message...................................................................................................................................................................... 370
Adding a CheckBox to the Edit Field of a ComboBox....................................................................................................................................... 371
Sizing the ComboBox DropDown List Width..................................................................................................................................................... 373
Sizing the ComboBox DropDown List Height.................................................................................................................................................... 374
Adding the DriveListBox, DirListBox, and FileListBox Controls to VB.NET...........................................................................................375
Adding Images to ListBoxes and ComboBoxes with Ease Under VB.NET........................................................................377
Principle features of an Owner-Drawn ListBox (and ComboBox)...............................................................................................377
Using A Generic List Collection and an ImageListControl...........................................................................................................378
Using A Flag at the Start of the List Item Text.............................................................................................................................379
Using a Custom Class as a List Item to Index Graphics.............................................................................................................380
 Page 9

Send (SMTP) and Retrieve (POP3) Email with Ease Under VB.NET...................................................................................382
Adding the VB6 MAPISession and MAPIMessage Controls to VB.NET...............................................................................382
PART ONE -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Sending Email Under VB.NET Using Native Methods....................................................................................................383
Quick and Dirty Email Senders............................................................................................................................................................... 383
TCP Ports, SSL Authentication, and Creating Credentials...................................................................................................................... 385
An Email Sender with a Lot of Muscle.................................................................................................................................................... 386
Sending Email Messages as HTML ...........................................................................................................................................................................389
Sending Alternate Message Views..............................................................................................................................................................................390
Sending a Plain Text Message Body and a RichText AlternateView..........................................................................................................................393
Sending Alternate Message Views with Different Context Types and Transfer Encoding.........................................................................................394
Typical Email Server Specifications....................................................................................................................................................... 398
PART TWO -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Encoding and Decoding Email Data................................................................................................................................399
Allowing Users to Specify Content-Type and Content-Transfer-Encoding Options.................................................................................. 400
Determining if Text can be Sent Encoded As Quoted-Printable, Base64, or 7Bit..................................................................................... 401
Converting 8-Bit HTML Data to 7-Bit for Sending without Loss of Integrity.............................................................................................. 402
Converting 8-Bit Text Data to 7-Bit for Sending without Data Loss.......................................................................................................... 402
Decoding Quoted-Printable Text............................................................................................................................................................. 404
Translating Base64 Data Back to Its Original Format.............................................................................................................................. 404
Translating BinHex Data Back to its Original Format............................................................................................................................... 407
PART THREE ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------\
Receiving Email Under VB.NET Using Native Methods.................................................................................................408
Connecting to a POP3 Server................................................................................................................................................................. 411
Checking for a POP3 Server Response.................................................................................................................................................. 413
Checking for Being Connected to a POP3 Server................................................................................................................................... 413
Getting a Response from the POP3 Server............................................................................................................................................ 413
Submitting a Request to the POP3 Server.............................................................................................................................................. 415
Disconnecting from the POP3 Server..................................................................................................................................................... 415
Getting Email Statistics from the POP3 Server....................................................................................................................................... 416
Getting an Email Reference List from the POP3 Server.......................................................................................................................... 416
Get an Email Header from the POP3 Server........................................................................................................................................... 417
Retrieve an Email from the POP3 Server................................................................................................................................................ 418
Deleting an Email from the POP3 Server................................................................................................................................................ 419
Reset (Undo) All Deletes from the POP3 Server..................................................................................................................................... 419
Send a 'Keep-Alive' NOOP Command to the POP3 Server..................................................................................................................... 420
Disposing of Resources.......................................................................................................................................................................... 420
Using the Completed POP3 Class.......................................................................................................................................................... 420
PART FOUR -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Email Data Blocks Made Easy..........................................................................................................................................422
Easily Extracting the Component Parts from an Email File...................................................................................................................... 425
Compiling Everything into an Email Class Library........................................................................................................428
Building the VBNetMail Class Library...................................................................................................................................................... 430
Accessing your New VBNetEmail Class Library DLL From Another Project............................................................................................. 430
The Complete SMTP Class File........................................................................................................................................431
The Complete POP3 Class File........................................................................................................................................433
The Complete Utilities Class File.....................................................................................................................................439
Conclusion................................................................................................................................................................................ 447
Black Book Tips. 448
Additional Black Book Tips.............................................................................................................................................................................. 449
Black Book Tip # 1: Bypassing Click Events When a Usual Context Menu is Not to be Displayed on a Right-Click................................................450
Bonus Tip................................................................................................................................................................................ 451
Black Book Tip # 2: Creating an Association Between a Unique File Extension and Our Application................................................452
Black Book Tip # 3: Get the Linked File Path From a Shortcut File................................................................................................... 455
Bonus Tip.................................................................................................................................................................................................... 456
Black Book Tip # 4: Create a Shortcut File Within Our Code............................................................................................................. 457
Bonus Tip.................................................................................................................................................................................................... 458
Black Book Tip # 5: Adding a File to the Recent Documents Folder.................................................................................................. 459
Black Book Tip # 6: Sorting Any Column in a ListView in Ascending or Descending Order, Even by Date........................................460
Bonus Tip.................................................................................................................................................................................................... 463
Black Book Tip # 7: Sizing a Label or TextBox to Fully Contain a String for Display..........................................................................465
Black Book Tip # 8: Set a New SelectedIndex for a ListBox or ComboBox Without Firing a Click Event..........................................468
 Page 10

Black Book Tip # 9: Display TextBox Text Format-Justified at Runtime............................................................................................. 470


Bonus Tip 1................................................................................................................................................................................................. 475
Bonus Tip 2................................................................................................................................................................................................. 476
Bonus Tip 3................................................................................................................................................................................................. 476
Black Book Tip # 10: Get the ListIndex of a ListBox Row Under the Cursor Without a Mouse Click.................................................477
Black Book Tip # 11: Open File Explorer With a Target File Selected................................................................................................ 478
Black Book Tip # 12: Determining If an Array is Dimensioned........................................................................................................... 479
Black Book Tip # 13: Customizing the Display of TabControl Tabs.................................................................................................... 480
Bonus Tip.................................................................................................................................................................................................... 484
Black Book Tip # 14: Detecting a TabControl Tab on a Right MouseDown Event..............................................................................485
Black Book Tip # 15: Prevent the User From Selecting a TabControl Tab......................................................................................... 486
Black Book Tip # 16: Hiding Tab Pages without Destroying the Tab Pages or their Resources.........................................................487
Black Book Tip # 17: Passing a Parameter ByVal When the Invoking Function Specifies a ByRef Parameter........................................491
Black Book Tip # 18: Show and Hide Additional Information at the Bottom of the Form....................................................................492
NOTES ON IMAGES.................................................................................................................................................................................. 497
Black Book Tip # 19: Easily Recovering Crashed Menus and Toolbars Under VB.NET....................................................................498
Black Book Tip # 20: Tracking ComboBox Items Under the Cursor................................................................................................... 505
Black Book Tip # 21: Demystifying Pinvoke Definitions of VB6 POINTAPI and RECT Structures.....................................................508
Black Book Tip # 22: Easily Sorting Strongly-Typed Generic Collections Lists..................................................................................514
Black Book Tip # 23: Dithering a Form's Background..............................................................................................................518
Black Book Tip # 24: Designing Intelligent Context Menus for TextBox and RichTextBox Controls..............................................521
Black Book Tip # 25: Setting Icon Image Transparency on On-the-Fly Context Menus at Runtime..................................................527
Bonus Tip # 1.............................................................................................................................................................................................. 530
Bonus Tip # 2.............................................................................................................................................................................................. 530
Black Book Tip # 26: Adding Background Transparency to Menu and Toolstrip Images.............................................................532
Black Book Tip # 27: Easily Do a Redim Preserve With Leftward Bounds of Multi-dimensioned Arrays.......................................536
Black Book Tip # 28: Implementing a C-Style Bit Field Class in VB.NET..................................................................................540
Black Book Tip # 29: Taking Advantage of, and Extending Application Events...........................................................................552
Black Book Tip # 30: Enable Built-In Justify-Alignment in a RichTextBox From VB.NET...................................................................555
Black Book Tip # 31: Easily Replace Power Pack Shape Controls With Faster Paint Event Code....................................................559
Black Book Tip # 32: Dealing with the Form Cursor Not Displaying Over Form Controls..................................................................568
Black Book Tip # 33: Passing Scalar Values and Strings To and From a Byte Array.........................................................................570
Black Book Tip # 34: Display Buttons Labels Wider Than the Button Normally Allows......................................................................580
Black Book Tip # 35: Performing Selections or Inserting RichText Data Off-screen Without Scrolling...............................................582
Black Book Tip # 36: Opening an Associated Application Without a File........................................................................................... 584
Black Book Tip # 37: Reliably Comparing Color Values, Even If They Are System Colors................................................................589
Black Book Tip # 38: Use [Enum].GetValues to Parse Enumeration Declarations as a List..............................................................590
Black Book Tip # 39: Reliably Detecting Transparency Colors.......................................................................................................... 592
Black Book Tip # 40: Turbo-charge Directory Tree Displays in a TreeView.......................................................................................593
Black Book Tip # 41: Sorting TreeView Directory Trees in an Orderly Folder and File Fashion.........................................................597
Black Book Tip # 42: Taking Advantage of VB.NET Drag and Drop Features and Making it E-Z.......................................................602
Bonus Tip.................................................................................................................................................................................................... 606
Black Book Tip # 43: Taking Advantage of the DateTimePicker Control............................................................................................ 607
Black Book Tip # 44: Padding Strings and Filling Separator Lines in Proportionally-spaced Text.....................................................610
Black Book Tip # 45: Making Sense of Form Default Buttons and Their DialogResult Properties.....................................................613
Black Book Tip # 46: Quick and Easy Text-Justification for Text Boxes, Labels, and Dialog Boxes...................................................614
Black Book Tip # 47: Owner-Drawn Controls Made E-Z.................................................................................................................... 620
Black Book Tip # 48: Owner-Drawn Directory TreeViews Using OwnerDrawAll Made E-Z...............................................................624
Black Book Tip # 49: Embedding Images Within Your Source Code................................................................................................. 628
Converting Between an Image and a Base64 String................................................................................................................................... 628
Formatting Base64 Strings for Embedding Within Source Code.................................................................................................................. 629
ImageLists Versus Resources..................................................................................................................................................................... 632
Creating BuildImageListCode, an ImageList Initialization Source Code Generator...................................................................................... 632
Creating Single Base64 Image Data........................................................................................................................................................... 639
Creating Single Base64 Icon Data.............................................................................................................................................................. 640
Black Book Tip # 50: Replace the BrowseFolderDialog Control With a Custom BrowserDialog Form..............................................642
Black Book Tip # 51: Make Using, Creating, and Embedding ImageStrips E-Z.................................................................................659
Loading an ImageStrip................................................................................................................................................................................ 660
Creating Your Own ImageStrip.................................................................................................................................................................... 660
Splitting an ImageStrip................................................................................................................................................................................ 661
Updating Black Book Tip # 49 to Use ImageStrips...................................................................................................................................... 661
Black Book Tip # 52: Extracting Icon Images from Files and Displaying Them in a Directory TreeView............................................665
 Page 11

Black Book Tip # 53: Determining If a Computer Has an Internet Connection...................................................................................697


Black Book Tip # 54: Manipulating Color Value Members With Ease................................................................................................ 702
Black Book Tip # 55: Converting Byte Sizes to Formatted Byte, KB, MB, GB, or TB Strings............................................................706
Black Book Tip # 56: Getting, Enumerating, and Changing Screen Settings.....................................................................................708
Black Book Tip # 57: Printing Plain, Formatted and WYSIWYG Documents Made E-Z....................................................................716
Using VB6 Printer Functionality Under VB.NET.......................................................................................................................................... 716
Printing Simple Text Under VB.NET............................................................................................................................................................ 716
Drawing Simple Text a Full Page at a Time.................................................................................................................................... 719
Drawing Simple Text on a Page One Line at a Time...................................................................................................................... 721
Selecting a Printer....................................................................................................................................................................................... 724
Selecting Page Setup Options.................................................................................................................................................................... 724
Selecting a Printer Font and Font Color...................................................................................................................................................... 725
Performing a Print Preview.......................................................................................................................................................................... 726
Supporting the Help Buttons on Dialogs...................................................................................................................................................... 727
Creating a Print Document Class................................................................................................................................................................ 728
Printing WYSIWYG RichTextBox Text......................................................................................................................................................... 732
Black Book Tip # 58: Enhancing ProgressBar Marquees and Adding Bling to ProgressBars Made E-Z...........................................741
Enhancing a Custom ProgressBar Marquee by Tiling It.............................................................................................................................. 744
Enhancing a ProgressBar Marquee by Emulating Aero Glass.................................................................................................................... 745
Drawing Custom ProgressBars of User-Defined Heights............................................................................................................................. 748
Adding Bling to the Standard ProgressBar Using Images............................................................................................................................ 749
Adding Super Bling to Your ProgressBars Using Complex Images.............................................................................................................. 753
Black Book Tip # 59: Adding a Horizontal ScrollBar to a ComboBox DropDown List........................................................................757
Black Book Tip # 60: Restoring 3D Curved-Surface Appearances for Buttons and Tabs...................................................................760
Black Book Tip # 61: Allowing a Form’s BackColor Property to Accept Transparency Colors...........................................................763
Closing Remarks......................................................................................................................................................................765
About the Author......................................................................................................................................................................768

Free Online PDF Documents Available by David Ross Goben.................................................................................................... 769


Open Letters Sent to Advocates for the Electric Universe and Expansion Tectonics Theories............................................................769
Enhancing Visual Basic .NET Applications Far Beyond the Scope of Visual Basic 6.0.......................................................................770
Doom 3 Walkthrough and Strategy Guide........................................................................................................................................... 771
Getting Fit After 40 – A Practical No-Nonsense Guide........................................................................................................................ 772
Also Available from the Author............................................................................................................................................................ 773
A Gnostic Cycle: Exploring the Origin of Christianity............................................................................................................................ 773
Recent Important Free PDF Public Posts on my Google Drive......................................................................774
 Page 12

Foreword and
Introduction.
Getting to Know VB.NET and What Object-Oriented Programming Is All About.
 Page 13

Foreword to the 11th Edition.


You are not going to find herein any of the “traditional” document introductory sections, sporting titles
like Who Should Read This Book (try anyone living on a shoestring, but still needing real solutions, not
sleepy-eyed academic white noise and their usual preschool code examples), Who This Book is For (how
hard is that to figure out?), Why Does This Book Exist? (it is my own personal reference, and it provides
pat answers to questions incessantly asked of me), How to Read This Book (try using eyes or a text to
speech reader). They also all begin with about 70 or so pages of useless droll that is less read than an
instruction manual. I should have started it on page 71 and pretend we had simply flipped past them.
The seeds of this tome took root in December of 1999. What started out as jotted personal cuff notes
have since then continuously evolved as I refined them so to pass them on to my students. Be aware,
however, that the VB6 Upgrade Wizard I sporadically mention herein had last shipped with VB2008,
though it was not really needed thereafter due to newer VB.NET editions being much more VB6-
compatible, even though we might still find VB2008 Express Edition or even fee-based VB6 upgrade
services out on the web, if we think we need them. Even so, once we are savvy in VB.NET, it is much
easier to simply rewrite the VB6 code as a new VB.NET app, copying and editing code a block at a
time, testing each block before advancing, and avoiding any enhancements until the app is proven. At
first I imagined this would be a difficult task, but by writing fresh code in the more versatile language of
VB.NET, I realized it was even easier to later amp up that code. I now strongly recommend this strategy.
Though VB2017 was their best edition (VB2019 was another paradigm shift that, although excellent, is
trying to make VB more akin to Managed C++), I believe VB2008 was their most significant release,
where their VB.NET vision was realized. Although VB2005 was a major paradigm shift from the rather
crappy VB2002 and VB2003 editions, restoring VB to its high standing as a Rapid Application
Development (RAD) platform, I saw it as more of a VB.NET “Release Candidate”, because using the
upgrade wizard to convert the linear, procedural language of VB6 to the object-oriented language of
VB2005 was, even with all its incredible enhancements, all greeted with loud and heavy sighs of relief,
still an often unpolished, sometimes arduous and frustrating effort, seldom worth all the time-consuming
labor required to transition VB6 code, other than for simple or small applications. Besides, what was the
typical result of this? It was, essentially, just a VB6 program that was able to run under VB.NET.
VB2002 and VB2003 were convoluted implementations that too often required tortuous, impossible to
remember hacks to deliver sometimes quite benign VB6 features. For example, VB6 could acquire an
application’s Major Version using App.Major. With VB.NET, we had to slog through typing an imposing
System.Diagnostics.FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly.Location).FileMajorPart
(fortunately, the compiled code more directly accessed FileMajorPart). With the introduction of the My
namespace in VB2005, this was simplified to a more logical My.Application.Info.Version.Major (see
page 229 to see code to recover the much simpler APP commands under VB.NET, enabling us to again
use App.Major and most other VB6 APP functions). I feel Microsoft should have never released pre-
VB2005 editions, save for Beta code to those who would diligently run it through the wringer, report
bugs, and suggest enhancements, though we must fully appreciate that Microsoft needed to offset the
enormously expensive development assets they had to invest in this long range game-changing venture.
Indeed, the only reason VB2002 was not Beta was because the VB6 Upgrade Wizard was finally ready.
I submit that VB6 should have been more strongly advocated through the release cycle of VB2008, not
with just the marginal nodding support it received, even though most objective observers recognized by
1999 that VB6 was desperately floundering in the wake of developing technology, becoming more of an
anchor chain around the neck of Visual Studio development, being one major reason C++ programmers
derided VB6 with merciless condescension and ridicule. Until VB.NET was finally reliably stable with
the release of VB2010, Microsoft should have aggressively supported VB6 extensions to access .NET
forms, workspaces and code. As it was, though they actually developed such extensions, which eased a
transition from a VB6 procedural platform to the more powerful object-oriented .NET environment, few
in the VB6 Community, outside of paying MSDN subscribers, ever seemed to be aware of them.
 Page 14

Contrary to the arm-flailing alarms sounded by “Classic VB” critics of VB.NET, many naively believing they
had the numbers or some sort of authority to force VB.NET back into just a VB6+1, anything in VB6 can be
easily duplicated as of VB2008, no longer requiring sometimes deep research just to deliver compatible
alternative workarounds for often quite simple VB6 features, especially with pre-VB2005 editions. Though
object-based encapsulation makes applications incredibly easy to debug, many VB developers may never
need an object-oriented language, where VB6 can still fill the VB Community's procedural language niche,
though I would love for it to have an updated IDE and its Integers to default to 32-bits (its 16-bit Integers,
being the same size as text characters, was plainly ridiculous on large memory model computer platforms).
VB2010 added many welcome language features and made it even more compatible to VB6, yet when
VB2015 was released, it blew all previous editions of the IDE out of the water! Its source code editing
features alone were worth the upgrade. Also, from VB2010 forward, it did not require a separate upgrade
process. Further, its Visual Studio 2015 Community Edition was nowhere near as limited as previous
“Express” editions, With it, we finally had a full .NET development environment, save for an MSDN
subscription with its tech support and privileges. Otherwise, we had the full implementation of Microsoft’s
Programming Language Suite, which featured multi-project solutions in Visual Basic. Remember, however,
when installing, we should select ALL the features we require, such as Desktop, Web, Phone, Storefront, or
whatever. By default, the installation configuration, based upon how we reach the download point, might
solely be for Web, Storefront, etc, so please take the time to actually read the option prompts and not simply
sleepily drone “Yeah, yeah. Whatever...” before we half-consciously click the button labeled “Next”.
Having said that, am I a VB6 fan? Yes! But will I continue to use it? I doubt it, except for maintenance
purposes, to support my customer base who have invested heavily in VB6 software and either have no
desire to upgrade, or do not wish to incur the expense or can afford the time of an upgrade. But I like too
much the colossal power and the magnificent freedom I enjoy while using VB.NET to want to turn back
to VB6. In fact, I have become so comfortable in VB.NET that I may now have trouble writing original
code in VB6 again, mainly because of the conflicting syntax between controls and commands that VB6
permitted or even required, being far more confusing than the smoothly consistent syntax of VB.NET.
I have always stressed the point that, because VB6 was a serious RAD platform, a developer can write in
hours what would have taken weeks to match in equitable robustness using C++. I know. I was a C++
professional developer. I still stand firm on this declaration, and I think it is even more true with
VB.NET (this explains why I have finally made a full migration from C++). By the time VB2005 came
out, VB.NET was clearly coming into its own as the new RAD platform of choice. The fact that its free
version, VB2005 Express, was a full VB.NET compiler, with its only limitations being that it did not
support direct interoperability with other .NET languages (most developers did not bother with this
anyway), it was missing a few professional-level templates, and it lacked support of a number of other
features made available only to the full Visual Studio environment, like the free Code Rush! Express
add-in editor (www.devexpress.com/Products/Visual_Studio_Add-in/CodeRushX/). It also featured much stronger
compatibility to VB6 code than VB2003 offered. VB2008 and later editions exhibit far greater
compatibility to VB6, and they are in fact the minimal VB.NET editions that I would consider
recommending to any VB6 user who has yet to migrate to the .NET platform. Just like VB2005, free
VB2008 and VB2010 Express editions are, or were available. Further, Microsoft has also provided a
number of excellent, previously pricey books regarding VB6 to VB.NET migration as free PDF
downloads (see my Closing Remarks on page 765 for web links to a sampling of these books).
It is traditional in a Preface of this type of book to list topic highlights, which typically repeat selections
from the Table of Contents. I suppose that means the topics they did not bother listing are really the less
important filler crap, added to make the book look more imposing, and hence, appear authoritative,
though such books often end up being largely unread, and typically used as windy day door stops, Were
I to list highlights, I would end up repeating the entire Table of Contents, which you just passed. Reread
it if you need to. Every point herein were solutions to real-world problems. They are not invented
kinder-care snippets. I documented them as I discovered them. I hope they will help you too!
 Page 15

Why I Share This Book for Free.


I am amazed the .NET coders I meet claiming to be Advanced, yet are clueless how Classes work and
why they make applications so powerful, how classes are abstracted to form Structures, how to clone a
class or structure, how to write a sort or binary search, how to declare .NET Native Fixed Strings, to
explain why .NET's Managed Space gives VB unbridled power, how Delegate Classes work, how to
invoke Delegate constructors, why Delegates are a powerful .NET feature that we should embrace, not
ignorantly scorn, to explain the difference between persistent and non-persistent control images and why
this distinction exists, etc. You know – all the really COOL stuff! We will learn these things herein!
This is the reason I share this, my personal reference book – to help new VB.NET programmers hone
themselves into knowledgeable developers, and to give grounded VB.NET developers an edge.
I will also share lots of trivia, VB.NET secrets, anecdotes, snappy “old-guy” euphemisms, and memories
from ancient times as a young developer when I could only write code between dinosaur migrations.
For example, I constantly hear from VB6 users how their code compiles to “native”. I suppose that is
sort-of true, if we squint at it; if we consider the intermediate code ActiveX runs under, called VBC (VB
pseudo-Code), as “native(-ish)”, thanks to marketing hype! The “native” part is actually the admittedly
extremely fast VB6 runtime p-code parser, VbRun.dll and the small EXE shell the VBC program is
wrapped within, and its incessant rapid-fire invocation of native DLL code. Still, VB6 runs almost as
fast as C++, though it is only a bit slower due to its innumerable, but optional error-trapping protections.
NOTE: The older VB1-VB6 IDE-native VB-Token code differs from ActiveX VBC P-code. VB-Tokens were like DOS BASIC
tokenized code in that all its keywords were compressed into tokens, though it was still interpreted, but ran very fast.
Conversely, VBC tokenizes its keywords and all other instructions, and so most VBC tokens trigger DLL functions in rapid-
fire succession, so it is little interpreted, but still parsed, though this makes it run as fast as compiled code. Because of this
incessant hammering of native DLL functions, marketing departments figured VB6 had earned the “native” title.
NOTE: If you prefer, use the included ngen.exe utility to precompile .NET programs into platform-specific native code.
What of the C++ users who incessantly belittle VB.NET as a Tinkertoy language? Yet, VB.NET is not
the same VB they had for so long loved to hate. Here is a secret – VB.NET is actually Managed C++; a
superior and highly modernized implementation of the Industry Standard C++95. Managed C++ is
specifically designed to take full advantage of emerging technology and the system architecture it is
operating under, but using VB syntax, plus a number of optional protections, which can be turned off,
that makes coding safer and easier than the drudgery C++ developers have to trudge through. The
advantage of these protections is phenomenal, transforming VB.NET into a RAD language. How rapid?
Say it takes us 2 weeks to develop some code in C++. Under VB.NET, a competent developer can write
code with equal, but likely with superior robustness in about 2 hours! That is 168 to 1! Do not shrink
from their ridicule. Forgive them; they bark from ignorance. A growing number are now barking from
insecurity as the news spreads of how rapidly robust VB code can be developed. They are fearful for
their careers, and many of them are now secretly learning VB.NET so to secure future employment!
NOTE: Even at my age, Tinkertoys are fun to play with. But, if we can round up a few stray kids to play with us, adults in
the vicinity then think it is cute, instead of wondering where they last put that darned number for the guys in the white coats.
We are going to have a lot of fun in this book, and just for kicks, we are going to do a number of things
that are supposedly impossible. As one small example, I never realized doing a Redim Preserve on
leftward bounds of multi-dimensional arrays was supposedly impossible, so back in '84 when I designed
Redim Preserve in my Basic Interpreter and in '95 in my C Compiler, I naively added full bounds
support. It was not until much later that someone told me this was impossible. I was stunned. The odd
thing was, to do this only took a few extra lines of code. Turns out this problem had been plaguing
developers since the 1960s. I guess I am some savant, but I keep reminding myself that when I had
returned home after living abroad for over 5 years, I was stuck in the men's room at JFK International
Airport because I was trying to figure out how to open a door with a knob, which I had not seen in years,
being conditioned to European door handles. It finally dawned on me that if I turn it one way and it did
not open, I could try turning it the other way. Some genius. I will laugh at myself a lot within this book.
 Page 16

Running VB6 on Recent Windows Operating Systems.


Many developers, like myself, must still support legacy VB6 and VBA code. This is not really so
surprising to those of us who have been in the business for any appreciable amount of time, considering
that there are still masses of developers supporting legacy code for even the 24- and 36-bit DEC PDP-10
and PDP-11 Mini-computer dinosaurs from the 1960s. As such, we still need to be able to run Visual
Studio 6 and especially Visual Basic 6 from our development platforms.
It is said, usually by self-titled online “gurus” who presume themselves to be an authority (that moniker,
when certain among them title themselves as such, makes me almost pee myself) that the VB6 IDE will
not run on Windows Vista through Windows 10, or any operating system that packs a 64-bit architecture.
However, the VB6 IDE, a 32-bit app (actually, an extended 16-bit architecture retooled for 32-bit, if we
really want to get technical), will run reliably on these systems if we simply Run as Administrator; the
64-bit systems will easily support it through WOW64; Windows 32-bit On Windows 64-bit.
Installation Setup for Visual Studio 6, or just VB6, should also be installed by running as an administrator.
Still, be advised that the installation will report a single error if we are installing on an operating system later
than Windows 2000, though it will not report any specific details on what that error was, though it will if we
install it in Safe Mode. But other than that, there is no differences or any real need for using Safe Mode . Even
so, we can safely ignore this error because it was actually caused by Setup not being able to over-write a
critical system file, OLEAUT32.DLL, a shared file installed by the operating system as a service provider for
32-bit Object Linking and Embedding Automation, with a much older version that was actually designed to
operate under Windows 95 through Windows 2000. But that, if it had actually succeeded in over-writing it,
would have been a very bad thing for our system. Fortunately, operating systems nowadays are more picky
about what it allows to be upgraded in its kernel (core system).
Microsoft currently maintains a updated VB6 Support Statement titled “Support Statement for Visual Basic
6.0 on Windows Vista, Windows Server 2008, Windows 7, Windows 8 and Windows 8.1, Windows Server
2012, and Windows 10”, which we can find at https://msdn.microsoft.com/en-us/vstudio/ms788708.aspx. In its Executive
Summary, they state “The Visual Basic team is committed to “It Just Works” compatibility for Visual Basic
6.0 applications on the following supported Windows operating systems: Windows Vista, Windows Server
2008 including R2, Windows 7, Windows 8 and Windows 8.1, Windows Server 2012 including R2, and
Windows 10.”
Microsoft also provides support summaries for each operating system. For Windows 10, they state:
Since the initial release of this support statement, the Windows 10 operating system has been announced. This document
has been updated to clarify Microsoft’s support for VB6 on Windows 10.
VB6 runtime will ship and will be supported in Windows 10 for the lifetime of the OS. Visual Basic 6.0 runtime files
continue to be 32-bit only and all components must be hosted in 32-bit application processes. Developers can think of the
support story for Windows 10 being the same as it is for Windows 8.1.

David Ross Goben


 Page 17

Introduction.
Upgrading from Visual Basic 6.0 to Visual Basic .NET, at first glance, might appear to be
an intimidating endeavor. Some users were moved so far outside their VB6 comfort zone
they had chided VB.NET as VB.NOT or even Visual Fred, implying that it was no
longer VB. After all, most everyone had read through copious magazines and blogs about supposedly
massive differences between these platforms. Granted, some of those differences were real, but most of
them were just imagined, spawned by nothing beyond prejudicial assumptions based on unsubstantiated
intuitive skepticism, sometimes simply because language syntax differed or commands were renamed or
moved. Of the real platform deviations, most were due to having to be expressed differently, plainly
because VB.NET more diligently followed a more stringent language syntax, something VB6 was not
always very good at. Hence, often a language feature might have to be implemented under VB.NET
using radically different syntax rules than the sometimes cryptic or archaic way it was incorporated in
VB6. Other disparities, some hailed as more profound, mostly ended up being features VB.NET did in
fact support, but again, due to more uniform .NET architectural specifications, VB.NET could not
support them in a VB6-style manner, and so was forced to implement non-VB6-style invocation rules.
Nevertheless, by employing some simple user-defined VB6-style helper functions, like those provided
throughout this tome, we can easily emulate those “lost” VB6 commands, or, in most cases, make
existing functionality accessible through VB6-style syntax. Regardless, we will find, overall, VB.NET
already supports most supposedly “lost” features, both major and minor, in but different forms, and in all
cases it also implements more robust methods toward applying their functionality. Indeed. many of the
supposed “major” differences bemoaned by myriad “VB6 purists” no longer exist, having existed only in
Beta releases of VB.NET, but being addressed by the time of the initial product launch, or, in more
severe cases, in later editions. See the article VB6 Aficionado Complaint Department on page 169 to
examine 116 of the most critical complaints VB6 devotees leveled against VB.NET, and how all but a
pittance of them are groundless. Yet, of those pitiful few that are not groundless, we will build solutions
so even they can be quite easily resolved, rendering as petty whatever meager complaints might remain.
NOTE: Robust implies code being bullet-proof (bug-proof). This is usually described as sturdy code that is written to avoid
program operation errors using strong types and error-trapping. Technically, Robust is a marketing department's term for
Idiot Proof, describing a process of preventing some idiot from screwing up the operation of an application.
NOTE: I find the term VB6 Purists to be laughable, because VB6 was no pure language by any means, being rather a
mongrel mix of really cool features and very forgiving coding syntax, added to by shortcuts and hacks, starting with VB1,
shoehorned with brilliance, tricks and duct-tape to provide a Graphical User Interface and windowed forms to what was
otherwise DOS-level QuickBasic. Even so, VB6 was for years a personal favorite. But the point remains that its language is
syntactically convoluted, often clumsy, and its overall design precluded much more modern programming concepts.
When a VB6 application is upgraded to VB.NET to at least VB2008, using the Visual Basic Upgrade
Wizard (you might still find VB2008 Express to get this wizard), chances are, once the upgraded
application comes up in Visual Studio or Visual Basic Express, its Task List will present us with quite a
number of alerts, upgrade issues, to-dos, warnings, notes, global warnings, run-time warnings, and
design issues. DO NOT PANIC! It is not as bad as we might at first think it is. Most of them are notices,
and are just that: Notices. And virtually all of these added tagged comments can be safely reviewed and
deleted after little to no editing. Of those that actually require more attention, most can be quickly solved
using a simple modification, or by instead invoking easy-to-write helper functions. The noted solutions
provided herein can help solve most upgrade woes, each of which are generally repetitiously
encountered in code, making subsequent identical fixes faster. These solutions will work when we are
upgrading to either the full Visual Studio .NET editions, or to the Visual Basic .NET Express Editions.
Instead of Microsoft just adding some nifty .NET features to VB6 and calling it VB7, after long debate
during the early stages of .NET development in 1999, Microsoft finally chose the more difficult task of
reengineering VB from the ground up, making it fully .NET and OOP compliant, meanwhile bracing for
the expected backlash from a largely non-professional user base who were devoted, but also rabidly
resistant to change.
 Page 18

Considering the monumental work required to accomplish such a task (ponder the syntactical transition
from VB.NET Beta1 to VB2008), this was not an easy decision. For example, VB.NET now sports two
powerful forms packages called Windows Forms and Web Forms; a new and more powerful version of
ADO to easily access disconnected data sources; a more logical, much more powerful and feature-rich
language that also removes or replaces many legacy or ‘hacked’ commands that no longer had meaning,
or had complex or convoluted formats; uniform property naming; enhanced type safety; exposing low-
level access that advanced developers require; and made it easier to write distributed applications.
This bounty of new VB.NET features opened all the doors that had previously been slammed shut in
VB6 developer's faces. With Web Forms and ADO.NET, one can quickly develop scalable Web sites.
With inheritance, the language now fully supports object-oriented programming. Windows Forms fully
support accessibility and visual inheritance; and deploying applications is now as easy as copying our
executables and components from directory to directory, often referred to as ‘X-COPY deployment’, so
we no longer need to worry about DLL versioning issues. With VB.NET, this potential versioning issue
and the resulting intense aggravation, often referred to as “DLL Hell”, is now a thing of the past.
NOTE: DLL Hell describes when an older DLL version overwrites a newer DLL, and then previous working apps start
blowing up due to no longer finding DLL entry points that had existed in the newer DLL. This also includes newer DLLs that
dropped older entry points, which clearly should be a developmental cardinal sin in professional-grade applications.
As importantly, VB.NET fully integrates with the other Microsoft Visual Studio .NET languages. We can
even develop application components in different programming languages, and our classes can now
inherit from classes written in them, and visa versa, using cross-language inheritance. With a unified
debugger, we can now debug multiple language applications, irrespective of whether they are running
locally or on remote computers. Whatever .NET language we use, the .NET Framework provides a rich
set of Application Programming Interfaces (APIs) for Microsoft Windows that are, in the .NET world,
referred herein as Pinvokes or P/Invokes, shortcut terms for system kernel Platform Invocations.
NOTE: P/Invoke or Pinvoke terms are used for two reasons: 1) because API is common to the reference “API Calls”, and
2) it refers to invoking the system platform code, which implies invoking something from outside the .NET managed space,
which is exactly what it is. Further, these invocation processes are likewise referred to as Signatures, replacing the API
Declaration term; tying in with Method Signatures, which are like C/C#/C++ Prototype Declarations.
No matter if code is written in VB.NET, C# (C-Sharp), C++, Delphi.NET, Google Chrome, or MSIL
(Microsoft Intermediate Language, a brilliant cross-platform assembly language), their syntax must
always sync up perfectly. In fact, code written in one language should be instantly translatable into one
of the other languages based solely upon the compiled MSIL instructions and its metadata from any of
those languages, unless their developer applied a code obfuscator to protect their data source code..
For example, suppose we wrote the following VB.NET code within a Form so we can emulate the old
VB6 command “ZOrder(Me, 0)”, to bring that form to the front of the system's window display stack:
Public Sub ZOrder(ByVal frm As Form, ByVal Position As Integer)
If (Position = 0) Then
frm.BringToFront() 'use VB.NET function to bring the form to the top of the z-order.
Else
frm.SendToBack() 'use VB.NET function to send the form to the back of the z-order.
End If
End Sub

This code is compiled into Microsoft Intermediate Language (MSIL) as the following:
method public instance void ZOrder(class [System.Windows.Forms]System.Windows.Forms.Form frm, int32 Position) cil managed
{
.maxstack 8
L_0000: ldarg.2
L_0001: ldc.i4.0
L_0002: bne.un.s L_000c
L_0004: ldarg.1
L_0005: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::BringToFront()
L_000a: br.s L_0012
L_000c: ldarg.1
L_000d: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::SendToBack()
L_0012: ret
}
 Page 19

The above MSIL code can then be immediately translated into C#:
public void ZOrder(Form frm, int Position)
{
if (Position == 0)
{
frm.BringToFront();
}
else
{
frm.SendToBack();
}
}

Or into C++:
public: void __gc* ZOrder(Form __gc* frm, Int32 __gc* Position)
{
if (Position == 0)
{
frm->BringToFront();
}
else
{
frm->SendToBack();
}
}

Or into Delphi.NET:
procedure Form1.ZOrder(frm: Form; Position: Integer);
begin
if (Position = 0) then
frm.BringToFront
else
frm.SendToBack
end;

Or into Google Chrome:


method Form1.ZOrder(frm: Form; Position: Int32);
begin
if (Position = 0) then
frm.BringToFront
else
frm.SendToBack
end;

Or into any other .NET-compliant language.


To achieve this fluid transition, one cannot hide a lot of code. so much of the code previously hidden
from VB6 had to be brought out into the light. The fact that it was hidden at all was meant to simplify
the language to fully empower it as a RAD platform, which it certainly did, but it was also a reason
many of my fellow C++ developers looked down their noses at VB6, chiding it as a tinker-toy language,
because the total breadth, the total power, and the total unreserved system control that VB developers
could otherwise wholly exploit was withheld from their grasp, which I think severely handicapped VB
developmental capabilities, limiting them far beyond that which most might venture to imagine, and, as
Microsoft later agreed, this tactic also severely inhibited VB’s true RAD potential.
The collected notes bound herein will make most common upgrade issues and programming obstacles a
snap to surmount. Not only will they help us solve most issues, but we will also get a glimpse into
powerful “outside the box” approaches to application development, and we will learn many secrets that
too few have knowledge of. I hope to also convey my excitement for this implementation of Visual
Basic that is now a true force to be reckoned with, having capabilities that so far still exceeds most
everyone's imagination. It is more powerful, it can be developed much faster, and can be executed just as
fast as other contenders in software development, even C++. The reason for this, and why VB6 and VB.NET
differ so much is that, to repeat, VB.NET is not actually VB, but Managed C++; a far more powerful and
modernized implementation of Industry Standard C++95 that has been modeled to look and act like Visual Basic .
So, embrace Visual Fred like a badge of honor, and get ready to get your Geek on.
 Page 20

Why Is VB.NET Not 100% Compatible to VB6?


VB6 users have endlessly complained that VB.NET is not 100% compatible to VB6. This is a rather strange
charge considering that even VB6 was likewise not 100% compatible to VB1 through VB5, often needing
anywhere from minor tweaks or simple recompiles to sometimes quite extensive redesign for more complex code.
To use many of the much faster VB6 features, like regular expressions, scripting and file system objects, some
VB1-5 restructuring was not worth the trouble. But, the primary reason VB6 did not cleanly upgrade was because
it flatly lacked Object-Oriented support. Further, many of the features VB6 users bewail the loss of 1) were
unreliable or even unsafe code, so safer means were implemented, 2) supported a dated, limited architecture,
prohibiting adoption of emerging technologies, 3) were not applicable to an object-based paradigm, so object-
friendly implementations were introduced, or 4) invited the development of less than robust, bug-prone code.
NOTE: Paradigm infers an organized method of performing a task. It does not describe two Roosevelt 10-cent coins.
VB1 through VB6 were procedural programming languages, which execute linearly, featuring simple
procedural calls to functions and subroutines. On the other hand, VB.NET is a fully Object-Oriented
Programming Language (OOPL), which is a layered construct of objects that interact with one-another,
each encapsulating their own methods and data. These paradigms, procedural and object-oriented, are
vastly different. I find it amazing VB.NET can even use as much original VB6 code as it does. Previous
to VB.NET, an object-oriented language often required colossal source code re-tinkering to upgrade a
procedural version of its language. A case in point is the procedural language C versus the object-
oriented language C++. Granted, we can compile and execute straight C code, for the most part, using a
C++ compiler and successfully execute it, but it was still a procedural program. It did not magically
become an OOPL. To convert VB6 to use objects as VB.NET requires, entailing form objects, control
objects and so on, necessitates often exceptional code restructuring, but the VB6 Upgrade Wizard,
available only through VB2008, did a good job, though manually translating source code is easy enough.
There were two options Microsoft had to consider when designing VB.NET: they could simply retrofit
the existing code base to run on top of the .NET Framework, but it would still be stuck with being a
procedural language, even though it would run VB6 code right off the bat, or they could rebuild VB
from the ground up, just to satisfy the overwhelming customer demand that they not take any shortcuts,
not sacrifice a single .NET feature, and take absolute advantage of a fully object-oriented platform. To
deliver the endless laundry list of features most customers demanded, including inheritance, threading,
providing full and uninhibited access to the platform and to other platform languages, and to ensure VB
moved forward into the next generation of Web applications, the right, though more difficult customer-
approved decision was to rebuild VB from scratch. And no, it is not modified C# – both were built on
top of a Managed C++ chassis. Yet, since late 1999, .NET development, along with other members of
the .NET Suite, like F#, were all developed through a Co-Evolution process, adopting features added to
any one language to all the other languages in the suite, making language interoperability a snap.
For example, many, but not all the new features found in the Windows Forms package could have been
added to the VB6 code base as new controls or as additional properties. However, this would have been
to tremendous cost to all the other great features that customers demanded, such as security and visual
inheritance, not to mention that VB.NET would have lost the powerful Web Forms package entirely,
which allows our users to access and run code behind our forms directly from the web without needing
to first install driver code onto their machines to execute its ActiveX P-code controls, like VB6 required.
Once on this tract, one of Microsoft’s primary goals was to ensure VB.NET could fully interoperate with
classes written in other languages, such as C# or C++, without caveats or implementing hidden helper
code. Another goal was to enable VB developers to harness the full power of the .NET Framework with
absolutely no impediments, and all without requiring internal support workarounds that had been
essential, such as to enable Win32 APIs to actually work under VB6. This coding band-aide was plugged
in to work around the VB6 inability to employ method overloading, also known as function overloading,
by introducing the messy and potentially dangerous “As Any” functionality to VB6 API Declarations.
 Page 21

Method overloading, an essential feature common to Object-Oriented Programming, allows different


methods to employ the very same method name, but each would utilize different parameter counts
and/or types. As a result, when we invoke the method name, the compiler in turn inspects the Delegate
Classes assigned to the suite of methods by that name, in turn delivering executable code that invokes
the appropriate method based on the provided parameters through a process called Signature
Verification. This process totally eliminated any need for a dangerous duct-taped Any Type. Method
overloading is easy and will be demonstrated in plentiful examples throughout this tome.
VB.NET now has the same variable types, arrays, user-defined types, enumerations, classes, and
interfaces as does Managed C++ and any other language that targets the .NET CLR (Common Language
Runtime). As a result, they also had to redact features from the language that did not play well with
the .NET paradigm, such as VB6-style fixed strings and non-zero-based arrays, even though this
document will show detailed examples for how to still implement them (both of these features can be
enabled or emulated using E-Z coding techniques or by very small user-defined classes).
But let us get down to the brass tacks. Let us examine exactly why VB.NET is the presumed twisted
monster VB6 critics rage with cacophonous saliva-sprayed shouts, ignoring for the briefest of moments
the fact that VB.NET has become, hands down, the single most powerful, most accessible Rapid
Application Development programming language on the planet, engendering more write-n-run
applications than all other developmental platforms combined, and consider the following:
Most VB6 users demanded the ability to build code in a common language IDE, have 100% unfettered
cross-language interoperability, and with puffed up chests expected and would accept no less than 100%
unrestricted object-oriented programming capabilities, most failed to realize the earth-shattering impact
these impassioned but likely unconsidered demands forcibly wrought on their beloved VB's design, just
to satisfy all their new expectations. A great many of them, being mostly devoted hobbyists and
amateurs, naively assumed that after such a monumental undertaking they could then continue on their
merry little way, writing VB6-style code just as they did before, using the same often non-uniform, and
often convoluted syntax they used before, and, oh yes, they expected there would be a few commands
here and there to address class inheritance and seamless access to methods whose source was written in
some other language. They did not grok the fact that in order to provide them with exactly what they
were unyieldingly expecting would also clearly necessitate colossal changes to VB so that it would be a
fully integrated, object oriented environment that would interoperate with and act exactly like the
other .NET-compliant languages. This requires perfect synchronicity between them; that each of them
could clearly understand and use objects from each other without the slightest misconstruction.
VB.NET is now precisely what all those angry, shouting VB6 Aficionados most vehemently demanded
Microsoft to transform VB into, getting virtually every little feature they could dare muster, having
barked their demands off like a chorus of auctioneers. Some hobbyists were so taken by surprise that
Microsoft was actually taking them seriously, many suddenly went berserk, like a shopaholic stumbling
into a 99% off clearance sale, their minds almost snapping as their caffeine-fueled brains melted into
oozing globs as they struggled to cognize virtually every conceivable enhancement they could envision,
some needing soothsayers to enter altered states of consciousness so to divine tenuous meaning as they
mumbled almost incoherently. It was like handing a teenager the keys to a souped-up '69 Mustang
Convertible. Microsoft, like some genie in a bottle, was earnestly endeavoring to grant even the most
exotic wish for functionality. These VB6 users cum arm-chair banana republic despots never realizing
that they were, in the end, going to get exactly what they were demanding.
And a great many of them hated the spawn of their own over-indulgent collective genius.
But that is OK. The rest of us are grateful beyond words.
Be careful what you wish for...
 Page 22

Why .NET – An Outsider's Perspective.


Why .NET? Why NOT? Though the .NET Common Language Runtime, or CLR, operates on Microsoft
Windows and Linux, this can expand. For some time, development of CLRs for other platforms, such as
iMac and UNIX, which Linux and the iMac OsX are modeled after, are either in the works, or being
conceptualized (considering .NET’s F# already runs on iMac, it might already have a working CLR).
The result of this will be that we can write code in .NET and run that code on multiple platforms and
CPUs without changing the code or even needing to recompile. This is the core purpose behind .NET; to
be a multi-platform development environment that can compile on one platform, but execute on many.
This is much like the Java scripting language and its philosophy of “write once, run anywhere.”
NOTE: The iMac’s OsX is a Unix-based operating system modeled after Carnegie Mellon University’s Mach microkernel,
which was in turn an upgrade/replacement for BSD’s FreeBSD and NETBSD implementations of UNIX.
NOTE: Using system-specific Pinvokes, such as invoking Win32 DLL methods, hinders platform interoperability because
iMac or Linux do not support Win32 Pinvokes. However, most Win32 Pinvokes do have CLR-compliant methods within .NET.
Unlike previous Win32 Component Object Model-based Visual Studio editions, .NET compiles code
into a machine independent language called Microsoft Intermediate Language, or MSIL (or just IL).
MSIL was carefully modeled around advanced Central Processing Unit architecture, crafted to work
tightly with the instruction sets of any 32- or 64-bit CPU. MSIL is technically an actual machine
language, but one for a hypothetical, yet ideal CPU, and which can be almost instantly converted into
native machine code on the CPU and Operating System it is running on, using its metadata by a Just-In-
Time Compiler. Hence, if a platform has a .NET CLR, the code can run on that platform without change!
Froth-mouthed VB6 critics of VB.NET claim this makes VB.NET a tinker-toy p-code language, but it
most definitely is not, because its code is executed. not interpreted. This is an odd charge, considering
VB6 is a fast ActiveX p-Code app, wrapped in an EXE installer and a native runtime support suite, and
then called native(-ish) by sneaky marketing departments, because it incessantly invokes DLL code.
NOTE: P-code, or Pseudo-code, is an intermediate version of a program, popular with many in-house development
languages, which also runs much faster than interpreting text source code because it is already in an intermediate state
between the source code and ready-to-run code. Microsoft DOS BASIC was such a tokenized language. Microsoft BASIC80
in fact ran scores of times faster than non-tokenized versions. As a result it became an instant monster hit among users.
What are the real advantages to compiling .NET to MSIL instead of native code? As MSIL. when it is
Just-In-Time compiled to native code at runtime, it is also optimized to the specific processor and
operating system it is running on. Thus, as an MSIL, it can optimize itself to run as a 32-bit app on 32-
bit editions of Windows, Linux, or OsX, or optimized and run as a 64-bit app on 64-bit editions of
Windows, Linux, OsX, or Windows Server. It is even possible for new processors and operating systems
to create .NET run-time engines as they are released. This means an MSIL app can optimize and run on
new hardware or operating systems without a recompile or upgrade, as long as it also has a
compatible .NET CLR installed, making it platform-independent.
Because the MSIL contains metadata on its classes, developers writing that code do not need to create
include files, Type libraries, Interface Definition Language code, or other files to use these classes in
other apps. An app can just automatically extract this data. This makes sharing and using it easier.
NOTE: To repeat a much earlier point, if you would rather compile your .NET code directly into native code, you can use
the included ngen.exe utility, or set compiler options, to precompile .NET programs into platform-specific native code.

What is .NET?
The .NET environment is described using technical jargon that will likely be unfamiliar to new .NET
programmers. Understanding this terminology and what they imply in the scheme of things will go a
long way toward making sense of it, and turn people into .NET developers. Rest assured that we do not
need to fully understand most of it when we first get started, and we can most probably get along by
knowing virtually nothing about it, to start. The great thing about it is that we can start developing code
without having to understand every little stinking thing before we can write a program. Obviously, a
fuller understanding of it is immensely helpful, but we can safely relegate such comprehension to
 Page 23

periods when we have time to do so. Picking this stuff up will also present us with some really cool
tricks that we can use to spiff up our code. But trying to absorb everything all at once will only result in
us forgetting most of it, and causing us anxiety due to information overload.
Basically, the entire .NET environment is wrapped within a thing called the .NET Framework. This
framework must, like any technology, be installed on a system before one can use it. Before anyone
complains, this approach is no different – though nowhere nearly as complicated – as requiring someone
to install a Computer Operating System prior to them being able to run applications designed for a
particular system environment. If we have been using computers long enough, we might remember
saving our pennies to get the Level II BASIC upgrade for the TRS-80 Model 1 or Apple ][ computer so
we could write “real” programs beyond their default and primitive Tiny BASIC. Presently, all recent
versions of Windows have come with the latest .NET Framework pre-installed, or we can download
them free of charge, just as it is standard for computers to come with an Operating System installed.

What is the .NET Framework?


The .NET Framework is a complex and very powerful development environment. At its root is a support
system called the CLR that runs and manages our applications, though it can appear to us mere mortals
as a tree-like structure filled with classes. Many of these are non-inheritable classes, also referred to as
Namespaces, acting like a tree branch, where each branch is populated by classes and other namespaces,
and many of these namespaces are in turn populated by even more classes and namespaces, ad nauseum.
At the very core of this massive edifice is a single root class named Object. From this primordial class,
all other classes in .NET are derived, even our own, though we might not ever notice any sort of
difference at all. Each more complex class object often inherits a simpler base class as its foundation.
This progression builds until we have truly complex classes that bear little resemblance to their ancestral
roots, but nonetheless share, as a part of its default bag of tricks, the interfaces of all its ancestral classes.
Normally, however, constructing VB.NET classes seems absolutely no different from constructing any
other class, being not noticeably different at all from doing so in VB6, C#, or C++.
Apart from its CLR, the .NET Framework is filled with nothing but classes. This is the first thing we
should know about .NET – everything in its environment, every one of its forms, modules, structures
and enumerations is actually a class or an abstraction of one. Variables are even class objects, albeit a
variation of it known as abstracted classes, Type Definitions, or Structures, being much like, but far
more powerful than the User Defined Types of VB6, but nonetheless feature extensive built-in property
and method support. Even VB Module files are classes, where all exposed members are treated as
Shared, and the module is internally imported automatically so we do not need to fully qualify a method
or property within it by referencing the module class name to access it, though we certainly can if we so
desire. Still sound strange or confusing? No worries! We will clarify these points soon enough.
NOTE: The main difference between a Concrete and Abstract class is: Though associated variables to both do not contain
their data, but point to it, Concrete classes must be instantiated using the New keyword and are more loosely connected to
that data, so during assignment they will copy just a pointer to their data to another variable. Conversely, Abstract classes
are technically instantiated, but each points to its own unique copy of data and are more tightly bound to it. and they will
automatically clone that data (make a new copy) when assigning themselves to a different variable. This makes them a bit
messy, making copies of their data all over the place, but they instantly clean up when they go out of scope (accessibility).
I said the structure of the .NET Framework was like a tree, thus implying branches and leaves. It is
formatted much like the directory structure of a disc drive, where each broad branch, usually defined as
a special non-inheritable class, is referred to as a Namespace. It in turn envelopes a collection of related
classes, where certain of these classes comprise additional Namespace branches, but containing their
own embedded child classes, all layered within this onionskin-like framework. This is on top of the fact
that any class in this framework can also become a branch of its tree. Indeed, .NET Namespaces simply
organize classes into groups of subclasses that are usually expected to work together.
The structure of the .NET Framework is comprised of terms like Namespaces, Application Domains,
Solutions, Assemblies, and Projects. Let us take a brief look at these from an outsider’s perspective.
 Page 24

What is a Namespace?
A Namespace is a non-inheritable static class containing classes, to include structures, enumerations,
interfaces, and even other Namespaces. In fact, everything within the .NET Framework, to include any
program we develop, is enveloped in a Namespace, though those new to .NET might not even be aware
of it. Indeed, when we create a new project or suite of projects, one of their properties is a Namespace.
By default, the name given the namespace is the same as the first, and often the only project. However,
if we have multiple projects that are related to each other, we might choose to change the default
namespace for each of them to be identical, especially if they will be working together as a suite.
A Namespace is actually a non-inheritable, single- and self-instantiating class enveloping other classes.
When a namespace is imported, its non-private members, whether they be classes, structures, methods,
enumerations, or properties are defined as Shared, declared as static and available to code that is granted
access to them. Through general project properties they can be referenced by the entire application, or, if
we wish, just by selected source code files if that namespace had been specifically imported into them.
Yet, it will be inaccessible to external applications, if, for example, our project is a DLL. Again, a
Namespace is non-inheritable, and so we are therefore prevented from instantiating a namespace as an
object. But this would be pointless, anyway, because a non-inheritable class can have only one instance,
which is instantiated automatically when it is imported. In the most simple of terms: adding a namespace
is the exact same thing as importing a non-visible VB Module into our application.
Namespaces do not follow stringent organizational rules regarding classes that can be declared in them.
It is simply a convenience for the developer that related classes can be grouped within them, just like a
disc folder. Sure, we can cram anything into them, but it makes more sense to group things that have
something to do with each other, or is considered a part of a suite of solutions for something. Hence,
grouping of classes within a namespace is a bit of an esoteric art. But keeping related classes together is
just common sense. It is like defining properties and methods for a class – we normally define only
properties and methods for it that actually have something to do with the class. Anything else is useless
and poor design. Likewise, a namespace would hold classes that have something to do with each other.
For example, there is a namespace named Microsoft.VisualBasic. The VisualBasic namespace is a child
namespace of the Microsoft namespace. Within the VisualBasic namespace we will find most classes,
methods, constants, properties, and enumerations relating to VB.NET, such as the Constants and
ControlChars enumerations. As such, we will therefore not find language-specific syntax keywords
there, such as If, Then, Else, etc., being part of the VB.NET Language Implementation Specification.
NOTE: There are two root namespaces in the .NET Framework – System and Microsoft – and all else branches from them
as sub-classes. Yes, we can also declare classes within other classes under .NET, a useful and very powerful feature.
We can bring up the Object Browser from the View menu and explore these namespaces and classes for
ourselves. A great hint for VB2008 users is to have the Dynamic Help tab open and in view (found in the
VB2008 Help menu, but was sadly gone as of VB2010 because it conflicted with its new background
compiler, even though it was helpful beyond words), so as we click items in the Object Browser, we can
use the Dynamic Help to explore them in exhaustive detail. Dynamic Help should be one of our new best
friends for VB2008, and can help us find answers to anything that we find of interest. Unlike Visual
Studio 6, the .NET Help System is not mostly directed at C++, but is directed at all CLR languages, of
which VB.NET is a full member, and includes VB examples for all related features.
A cool trick, if we are writing code in C# or Managed C++, is that we can actually access the Visual
Basic classes and invoke their functionality. This is really handy when there is a VB-only method that
we want to take advantage of from non-VB language code. Conversely, if our VB.NET code needs to
use something like pointer manipulation, where we would change data through pointers (pointers are
often used to target physical memory locations), then we can write such code in C# or C++ and invoke it
from VB.NET, even though we can in fact do such from VB.NET by using deeper .NET features and
platform invocations, as will also be demonstrated soon enough within this document when we add a
.NET version of the VB6 VarPtr function to directly access memory addresses of variables and objects.
 Page 25

We will briefly discuss how to access namespaces when we take a look at the structure of code. Suffice
it to say for now that there are four types of class or class abstractions that can be created at the
namespace level: Classes, Modules, Structures, and Enumerations. Each can be declared with a scope
access of Private, Public, Friend, or Protected. Private members are visible only to the code block they
are declared within. Public members can be accessed from outside the class, including other applications
that can access the program, such as a DLL. Friend members are treated as Public within the project, but
are treated as Private to all outside applications that could otherwise access the program. Protected
members are accessible only from within its own class, or by a derived class using its MyBase qualifier.
Enumerations should already be familiar, but a much-welcomed enhancement in .NET is the ability to
declare them with a storage class (storage type). Instead of their values being generic, slow, Variant-type
expressions, we can declare the values of an Enum to provide Singles, Doubles, Integers, and such.
NOTE: Variants were 128-bit, 16-byte containers that stored the actual scalar value or object pointer, its Type Flag (storage
class), and sundry bit-flags. What made them so slow was they were constantly being interrogated by outside methods, which
meant they had to perpetually reference its stored value, Type Flag and bit flags. Objects point to the data (its class data is
just an IntPtr), and its protected class methods can instantly access that pointed object, making Objects extremely fast.
Structures, unlike the more primitive User-Defined Types of VB6, can also include methods and
properties. For certain, structures are classes, except that they are what is called a value-type object (an
abstract class, in Object-Oriented lingo), as opposed to reference-type objects (a concrete class) that
most classes resolve to. We will be looking deeper into these two types a little later.
VB.NET Modules, by appearance, are treated just like VB6 Modules. However, there is an important
internal difference that perhaps we need not worry ourselves about, but is a very cool bit of trivia:
VB.NET modules are actually Namespaces. The key distinction of a module is that it, unlike a regular
class, cannot be instantiated as objects simply because a module is auto-imported into a project, being
treated as a non-inheritable class. As such, non-private methods and variables in the module are handled
within the application as Shared. These treatments were done so that VB6 developers migrating over
to .NET would not suffer a grand mal seizure because they no longer had Modules available. I have
heard a number of C# developers complaining that they cannot likewise define C# modules. Well,
technically they can. All they have to do is to declare a class as non-inheritable, declare all of the non-
private variables and methods that they want to be accessible to the application as Shared, and to import
this class so that they do not have to declare the module name when they wish to access a variable or
method within the module, though this can never hurt in-as-far as self-documentation is concerned.

What is an Application Domain?


An Application Domain is a solution or group of solutions that work together, sharing memory, but are
completely separate from each other in the CLR's managed space. This process separation guarantees
shared memory between multiple executables will not conflict, which had previously been a nightmare.
Granted, most of us will never need to worry about this, focusing instead on single executables, but
those of us who do write such applications, the headaches have been marginalized, especially the pain of
trying to quash circular references in released shared objects that somehow kept resurrecting themselves,
as had been an unwelcome “bug-as-feature” under VB6. Again, this is a concept that, in the end, is quite
helpful, though we really do not have to bother our heads with even knowing that it exists. The fact is
that most of us could have gone through the rest of our careers without thinking about it even once.

What is a Solution/Assembly?
Solutions are how we author, organize, and package our software project needs. A Solution is much like
a packing list that describes what our projects require in order to run. An Assembly is like a warehouse
manager who takes that packing list and collects together the needed resources, such as binary and text-
based resource files, configuration files, internal and external classes, plus anything else needed to
enable our projects to actually operate. I tend to put Solutions and Assemblies together, because from the
outside, they seem to be two sides of the very same coin, and to most of us lowly mortals the distinction
 Page 26

between these terms might appear to be a bit on the gray side. Instead of pulling out dusty charts and
cracking aged thick. well-thumbed tomes to explain their mechanics, suffice it to say that when we talk
about a Solution or an Assembly, we are pretty much talking about the same general thing. A Solution is
the Foreman; the wrapper that can hold one or more projects, to direct the construction of their EXE or
DLL files, and an Assembly puts together how that Solution can be realized as actual EXE or DLL files;
a tangible operating unit, actualizing the nuts and bolts of each project's framework.
NOTE: Older VB.NET Solutions could only create one EXE, and the other optional projects could produce only non-
EXE files, such as controls and DLLs. Later VB.NET releases and the command-line compiler can compile solutions
with more than one EXE, though there is generally little need for it. By default, the name of the solution will be the
same as the name that we provide for the first, and usually the only project we created.
Generally, the only time we really need to be concerned with an Assembly is when we want to edit the
application description, version number, title, and such. In this case we can simply open the
AssemblyInfo.vb file, found in the Solution Explorer, or open the project properties and select the
Assembly Information button to edit this file through a much safer dialog interface, and modify the
entries (if you do not understand what I mean, simply access it and look for yourself – it will then
become quite obvious). Personally, I think of the whole thing as a Solution, and I associate the idea of an
Assembly as this single AssemblyInfo file, and forget about their actual technical definitions.

What is a Project?
A Project defines the executable/control/DLL we are creating. Each project compiles just that one object.
When we create a new VB.NET project, it also slyly creates a wrapper around that Project called a
Solution, taking on the same name as our project (this is simply an organizer for the project or even a
group of projects we want to be related – see the paragraph below), as well as a Namespace that wraps
around one or more Solutions (also see the paragraph below) with the same name as our new project.
Indeed, when we go about creating new programs, .NET will be prompting us for a new project, which
will also create a like-named Solution and Namespace. We typically set these in the creation dialog if we
will in fact require them to be different from the default, but we can just as easily change them later.
It might seem logical to first create a Namespace, then create a solution within it, and finally to create
our project within the Solution, and you would be correct, but most user programs never need to work
so auspiciously, or even to ever worry about Namespaces or Solutions at all, and so this proper approach
to defining our project would therefore be nothing more than a peevish headache than a help in most
write-n-run programs. This somewhat cut-to-the-chase inverted approach is a well-received feature by
most programmers. Besides, the Solution and Namespace can be redefined later with absolute ease.

A Crash Course on the Heap and the Stack.


Before discussing Object-Oriented Programming, it is important we better understand how computer
memory is used. We will not concern ourselves with the amount allocated for system support services,
drivers, and core-level operating system components (its Kernel), but we will examine the domain
partitions that are assigned to each application of the remaining memory that are each shared by two
areas called the Heap and the Stack. The Heap starts at a partition's maximum address and works its
way down toward its base as protected objects are added to it. Conversely, the Stack starts at the
partition's base address and works its way up toward its maximum as transient objects are added to it.
By understanding how the Heap and the Stack are used by the operating system, we can better grasp the
similarities and differences between the types of objects that are stored within each, the advantages of
using either of these spaces, why they differ in usage, and how we can best exploit them.
The Heap refers to the memory area set aside by the operating system for use by protected application
objects. Its size depends not only on how much memory is installed on a computer, but also how much
of it is minimally needed by the Stack, how much of what is left can be made available to users, and how
much memory is required by operating system kernel processes and user applications.
 Page 27

It is called the Heap because programs and objects are stored within it in an organized cluster, like piling
them into a neatly stacked heap. Objects are added by layering them in sequential order, starting at the
top of the Heap and working downward toward its base, much like an inverted stack of blocks, diligently
mapping and linking each and every object appended to it. The Heap is also Protected by the fact the
operating system maintains rigid control over the allocation of its space to objects, programs, and
scratchpad memory. Nothing goes into the Heap without the operating system's explicit permission.
To store objects on the Heap, an operating system request must be made to set aside a specific amount of
memory for each object. The system replies with a flag and a pointer. If the flag indicates the request is
granted, the pointer holds the Heap's base address, after it had been updated by subtracting the requested
size from it. The invoking process is expected to copy the object to this freshly allocated space. The
process must also maintain references to all its objects so they can later be accessed and deallocated.
Maintaining references also helps keep them “alive”, especially in Managed Memory applications that
provide features to automatically deallocate “dead” (unreferenced) objects. One can manually release an
object back to the Heap by sending the system a deallocation request, along with a pointer to the object.
Under .NET, thanks to the CLR's Managed Memory model. if we set Nothing to all references to an
object, the CLR's Garbage Collector walks the Heap and Stack, finds unreferenced objects, and handles
disposal for us, if we had failed to deallocate them, but only if we had that memory set aside through the
managed methods of the .NET CLR (typically, by having declared the objects As New). If we allocate
any memory outside the CLR through directly invoking the operating system, or via processes not under
CLR control, we must explicitly write code to free it through a Dispose or a Finalize class subroutine. If
not, it can remain in memory after an application ends, which is called a Memory Leak. If enough such
leaks accumulate to the point of filling the Heap, the system can no longer allocate space. As a result,
items added to the Stack, found at the base of the Heap. might also encroach the Heap, corrupting it.
Such issues can result in the computer requiring a reboot in order to correct the Heap and the Stack.
The Stack is another set-aside memory area that is part of the Heap partition, but located at its base. It is
called the Stack because the Central Processing Unit labels it as such, and because data is stacked on top
of it, like a reverse version of the Heap. Basically, data and references are pushed onto it, and later
popped off for retrieval. For example, when a method is invoked, the address to return to is pushed onto
the Stack. Once that method has finished, it returns to its invoker by popping the address off the Stack
and placing it into the Instruction Pointer of the CPU, and then resumes execution from there.
The Stack is transient in nature by the system not keeping strict control of its space allocation, as it does
the Heap. Generally, the system references the Stack through the Stack Pointer register of the computer's
CPU. The way programs store additional Stack data is to maintain a secondary offset pointer to track
where on the Stack we can append that data. When a method is invoked, the return address is pushed on
the Stack, and the current Stack Pointer is recorded and used as a working base offset by internal
program references. Because invoked methods and program blocks can be stacked and unstacked in an
exacting sequential order, like pushing plates into a spring-loaded dispenser; the last item pushed onto
the Stack is always the very first item popped back off.
Parameters passed in with a method are stored at the offset, adjusting it higher in memory, above the
data just copied onto it. Likewise, local variables declared within the method, or even an assigned blocks
of program code, such as within an If...End If block, are placed on the Stack at the offset, and then
adjusting the offset to point above it so subsequent objects can afterward be added to the Stack.
The beauty of the Stack is that because all items stored on it is transient, having no static references to
its members, when a method or a program block ends, the tracked offset is reset to its lower memory
address, such as the address where a method's return address is found, and all the data stored above it is
instantly dissolved, having gone out of Scope, or accessibility. Therefore, everything stored above the
base address assigned to that block is now free to be assigned to subsequent Stack objects.
NOTE: The purpose of the New verb is to inform the compiler to store an instantiated class object on the protected Heap.
Structures also instantiate objects, but these more ethereal objects are stored on the unprotected, more transient Stack.
 Page 28

Object-Oriented Programming – A Quick-Start Guide.


VB.NET is a full-fledged Object-Oriented Programming Language, or OOPL. Everything in VB.NET,
to include simple variables, are derived by instantiating a class. People new to OOPL might not at first
imagine much use for it, preferring instead linear, procedural languages like VB6, even a basic
introduction can help clarify the tremendous advantages an OOPL offers over procedural design, which
had engendered exasperatingly confusing “spaghetti” code, where process control often shot about in
chaotic directions, like rats in a maze, due to unrestricted control transfers, unstructured variable and
function organization, and undisciplined scoping rules that would allow access to out of bounds objects.
The introduction of structured programming by languages like Pascal, ADA, Algol, and C tried to
promote structured code, but failed because it was too easy to work around using undisciplined design.
Classes, however, enforce encapsulation, where each is a self-contained building block, acting like a
black box, allowing applications to be built like stacking bricks through a process of Modularization,
and linked together by a central controlling class, such as a startup form. Anyone involved in large
projects understands breaking projects up into smaller self-contained modules isolates its code and data,
making bug tracking simpler, and lends itself ideally to team environments, where each team member
works on a block of classes, individually testing and debugging them before plugging them into the
main application, even if they must be developed in stages, graduating the functionality of each stage
from simple to complex as the project evolves until it achieves its final design goal.
NOTE: Black Boxes refer to predictable widgets. Though their inner workings may be unseen or not known except by its
developer, when known data is input, predictable known output results are guaranteed, allowing their working content to be
ignored by all, save for those whose task it is to develop, provide, and service those widgets.
The typical flow of an OOPL project is to break each task down into more primitive classes to
consolidate common features, which, when grouped together, will define the more complex outer class
that a primary program thread manages. Each subclass has a particular set of tasks to perform. Each
module can be further segmented into simpler classes that perform even simpler tasks. Again, these
smaller classes are self-contained and can be individually tested. Even if we are working on a project by
ourselves, building programs in modular segments makes its development faster and easier to maintain.
The VB2008 edition of VB.NET worked around most incompatibilities between an OOPL like VB.NET
and a procedural language like VB6, to include making the new Integrated Development Environment
reasonably familiar and compatible to that of VB6. An example of this, as was earlier detailed, are VB
Modules, which are files whose public contents were accessible from other files as though that code was
written within the invoker’s code. In VB.NET, Modules are classes that are automatically imported into
the IDE, allowing developers to use these module classes just as they did under VB6. Other examples
are control and form containers that help developers to easily access the control components of their
program, just as if they were collections, which many developers had been used to using under VB6.
If you are new to Object-Oriented Programming, the following terms and concepts will help you get a
head start in gaining a basic understanding of the Object-Oriented Programming Language paradigm.
Just keep reminding yourself that it is a whole lot easier to do than it is to explain.

Classes and Objects.


It is critically important that we at least understand the concept of objects, the classes managing them,
and how they are incorporated under .NET. If we have authored numerous classes, this is old news, but I
keep meeting self-described advanced developers who have authored only a few classes, and some none
at all, save for Form and Module classes, or were not sure how class objects were handled internally. An
understanding of classes and objects instantiated from them is critical to mastering .NET.
A Class is program code that manages Objects, like field variables, forms, and the controls dropped on the
forms. A class contains the properties, methods and events that object instances of the class are managed and
protected by. An Object is a collection of Fields declared in a class, bound like a list of variables, which the
class designed to create and support its objects must protect and control access to them by outside processes.
 Page 29

Each Object is individually stored in Heap memory, yet all such class instances share access to the same
class code maintained in the general program pool. The collective field declarations in a class comprise
its Object definition, where its field order, count and types establish its Data Footprint, or Signature.
Further, objects can even be managed by other classes designed to share identical object Signatures,
such as Derived classes through Inheritance, like Bitmap and Image classes being able to share objects.
But the truth is, most of this will usually be invisible to us as we go about creating forms and add
properties, controls, events, and methods in the same old way we had done so in languages like VB6.
Classes bind fields and methods together, called encapsulation. A class containing only methods that
manage the fields of its objects is much easier to debug. Objects and their class definitions are therefore
developer-friendly. Ultimately, an object is a collection of fields, and its class wrapper should always
protect those fields. The properties, methods, and events declared within a class definition should be the
only conduits through which consumers (users) of objects access them.
Novices to OOP design often have trouble initially making sense of the modular encapsulation of objects
and methods. How can we make real programs with that crap? Classes can be seen as linking a block of
fields, defining the type of Object it can work with, with methods that support them, collectively called
its members. We access these members using a Reference Variable, which points to an object, but is
declared as a particular class. We access the object fields and class methods by typing a Dot after the
variable, and in .NET a list of its methods and associated object fields pops up in a selection list. This
makes using objects a whole lot easier than using traditional procedural language variables and methods.
The hardest part of OOP is understanding Classes. Yet, it is not the big deal some think it is. Truthfully,
writing VB.NET classes, like form and module classes. appears no different from writing them in
procedural languages. VB.NET form classes are little different from VB6 forms, and VB.NET field
variables are little different from VB6 field variables. We even invoke methods in forms, modules and
classes the same way as before. However, once we become comfortable with classes, we can begin to
take full advantage of their class design, and then begin to write coding miracles, as we will find herein!
In simpler terms, an Object is the Form comprising the user-interface of a VB program. The buttons and
other controls on the form are also objects having their own hidden class code that the form manages
interaction with through events and properties, which we normally do not worry too much about, just as
we did not under VB6. Their Object is actually a grouping of its procedure-level Fields. Other aspects of
the program, like modules and outside references to support our application comprise other objects.
An Object is something we interact with. We interact with objects all the time: televisions, cars, phones,
fluffy puppies, etc. In computer terms, an object is a tangible aspect as opposed to the activity we do
with it, where the things we do to interact with the object is the Class code. What is important here is
that a class does not just reference objects through Properties, what an OOP calls Attributes, it also
references the programmed intelligence of its methods to invoke actions, such as a Bill object being able
to print itself if its class incorporates a Print method (subroutine/function) that can be invoked (called).
An Object is an instance of a Class's data definition; its Field Members declared at the method level
within the class, causing this group of declared fields to be brought out from concept into manipulative
tangibility by storing a copy of this group of fields (the Object), to consume space in the protected
memory of the Heap. Each object instance of a class is managed by just a single copy of its class code.
By accessing an object through a reference variable that is declared as a class type that can be associated
with the object, and the variable points to the object instance, that reference variable will act as if its
object was the individual field group that had been declared within the variable's class type.
Classes are important to object-oriented programming because they allow us to group (wrap) related
items as a single unit, enabling us to fully control their visibility and accessibility to other procedures or
objects. Classes can also inherit and reuse code defined in other classes, except if they are specifically
declared to be non-inheritable, meaning they can only be imported, and act therefore exactly like a VB
Module, making them static, single-instance classes that auto-instantiate when they are imported.
 Page 30

We have learned that in OOP, all objects are instantiated from class declarations and that a class supports
its objects with properties and methods. Classes are clearly the heart of Object-Oriented Programming.
There are two types of class objects in OOP, being Reference Type Objects and Value Type Objects.
Reference Type Objects are considered regular, classical, or typical classes, and are also referred to as
Concrete Classes. These types must be instantiated using the New keyword because space must be
tracked and set aside for them in protected Heap memory, either directly by the application or indirectly
by invoked methods or external processes. Variables assigned to their objects do not contain the object
data, but are instead treated as a reference pointer into the object's Heap memory address.
Value Type Objects are structures having a uniform size, able to stack neatly upon each other, making
them scalar. Numeric variables, like Double, Char, Long or Integer are Value Type Objects. For
example, when we declare a variable of type Integer, we do not instantiate an integer object as we would
a class; we do not need to use the New keyword to create it. Value types are typically called Structures,
because their makeup is actually a VB.NET Structure (a VB6 User-Defined Type). In OOP, such
structures are called Abstract Classes. Although internally constructed exactly like classes, and their data
is also pointed to by their variables, unlike classes, structures are abstracted by enabling them to readily
clone and generously share their data. Unlike classes that store their objects in the protected Heap,
abstract classes instead store them in the more transient space of the system Stack, and so do not need to
be released to the Garbage Collector. Also unlike classes, their data is automatically released when they
go out of scope (program accessibility) because they cannot be referenced by static or outer-level
(external) variables. Even though they are messy, dribbling copies of themselves everywhere they go to
anything glancing their way, they also clean up quickly and easily by their simply going out of scope.
Though VB.NET Structures are similar to VB6 Type blocks, under .NET, they follow OOP rules, and
being Abstracted Classes, they are like concrete classes in that they can encapsulate properties and
methods. They can even be initialized using the New keyword. With structures, the reserved
parameterless New constructor is used to initialize all fields of the structure during its creation, not to
instantiate a protected Heap-stored object. We can, however, define parameterized constructors, along
with properties and methods. This will later be discussed, which should relieve any lingering confusion.
Classes and Structures – A Closer Look.
Classes, Structures, and Enumerations are formatted in a similar manner in that structures and
enumerations are abstracted from the Class model, though I see enumerations more so as categorized
constants, being limited to simple constant fields that share an identical storage class. Although they
follow a field layout like classes, they are treated like Constants in that they dissolve once a program
compiles, being accessed only during compilation, because each of their field values are incorporated
into the compiled code and are thus no longer required for reference during an application run, unlike
memory-consuming class and structure objects, which are not instantiated until the program is running..
Classes and structures are internally frameworked in an identical manner, and even mirror object storage
and addressing schemes. Yet unlike classes, structures cannot inherit from other classes or implement
interfaces. They also do not have to so diligently manage their memory allocation and their data is
always stored on the Stack, not on the Heap as class objects are stored..
All procedure-level Field Variables in a class or structure are clumped together as a stacked collection of
fields, defining an Object, being stored separately from the source code of the class or structure. The
fields in each object is assigned an index into a Field Offset Table that tracks the start address of each
defined field, so they can be addressed identically in a program as class-defined variables through
internally computed offsets. By default, the start of each field offset is aligned on a multiple of its size,
allowing for faster memory access, This is why it is very important not to just assign member fields in
an arbitrary order, so faster access results, though the fastest access occurs on fields aligned on multiples
of the computer's CPU architecture. Further, by aligning fields to minimize intervening gaps between
them, chances of data corruption is reduced if such gaps are not anticipated during Win32 interop.
 Page 31

For classes, objects are stored in the protected memory of the system Heap. A class object is considered
to be Instantiated when it is placed on the Heap because system memory has to be allocated and set
aside through operating system invocations for storage, and must be specifically released to deallocate
that space or else memory leaks will result, thus remaining in memory even after the program ends.
Though structures also contain objects, they are stored in the more transient memory of the system
Stack. This space for the object is not allocated like memory from the system Heap, but is assigned from
a computed Stack offset address, such as above a method return address or a offset assigned to a code
block, like an If...End If. The object is copied to that space at an offset from the base address assigned
to the block, this offset is then updated to point beyond the copied object, ready for the next object to be
copied. The advantage here is that stored Stack objects dissolve easily and quickly when they go out of
scope. For example, when a method or program block ends, any structures stored above the block's
assigned base address or above a method's return address becomes inaccessible, and so go out of scope
and instantly dissolve. Because structures can be referenced only by variables local to their definition
block or inner blocks, they dissolve cleanly with no worry of external references still existing.
Reference variables for classes and structures both point to the base address of an object assigned to
them, and internal referencing their fields from the Field Offset Table allows program code to address
each member in the pointed-to object as if the class or structure fields alone held that object's data.
Variables pointing to class objects are called Reference Variables. Copying them to a another variable of
the same type copies only the object's address. If we want to copy an object to a like-typed reference
variable, we must Clone the source object, which instantiates a brand new object on the Heap that is an
exact copy of the source, and assigns its Heap address to the destination reference variable.
However, structure variables, though they are technically also reference variables, are not likewise
referred because each such variable, unlike for classes, points to a single object and is tightly bound to
that object, so much so that the object is essentially invisible, being seen on the surface as if it were
actually the field variables declared within the structure. Also, if we were to point the variable to
another structure object, the old one becomes inaccessible and dissolves when it goes out of scope. Also
unlike classes, copying a structure variable to another of the same type copies its object, not its address.
NOTE: Some have pointed out to me that Static variables declared within methods do not dissolve when the
method or block goes out of scope. That is true, but Static variables are not stored on the Stack, but rather they
are maintained on the Heap. The Static and Shared keywords inform the compiler that these fields must be
retained, so they are instead stored on the Heap as object instances, and a special linking reference that binds
them to their method, class, or structure program code is internally retained during the run of the application .

Fields, Properties, Methods, and Events.


Classes and structures consist of Fields, Properties, Methods, and Events, and they are often also
referred to as Members of a class or structure.
 Fields are data the object contains. Fields are like variables because they can be read or set directly,
and are in fact declared exactly like VB variables. For example, if we have an object named "Car",
we could store its color in a public field named "Color" (for example, Public carColor As Color.
Normally, one would store fields as Private or Protected, and actually access this field through
Public or Friend Properties.
 Properties are methods that access and set information about an object as though they were fields or
variables, and implemented through property Get and Set attributes. These attributes provide greater
validation control over how field values are stored or returned. This layer of indirection between a
class field and the procedures users employ to access it helps protect that data and lets us validate
values before they are assigned or retrieved. To the consumer of the object, accessing an object
property member acts, or feels as if the consumer were actually given direct access to a publicly
declared field or variable.
 Page 32

 Methods come in two flavors; Subroutines and Functions. We may have referred to executing these
as calling them under VB6 (and Microsoft still does), but it is preferred in OOP to say that they are
being invoked. The reason behind this is that to invoke something is to imply that it is something
called out from something else. Methods are essential because, after all, most objects, such as a
form, needs to do something. Methods therefore represent actions that an object can perform on or
with its data. For example, a "Car" object could have "StartEngine," "Drive," and "Stop" methods.
We define methods by adding subroutines or functions to our class, so we, our users, or an event can
invoke them.
 Events are special subroutines that are triggered by notifications an object receives from other
objects, applications or system components, such as the keyboard or mouse. Events enable objects to
perform actions whenever a specific incident is triggered. An example of an event for the "Car" class
would be a "Check_Engine" event. Because Microsoft Windows is an event-driven operating system,
events can come from other objects, applications, or from an input device, such as a mouse, a
keyboard, or from a joystick. As under VB6, clicking a form button executes code defined within a
Click event method.

Encapsulation, Inheritance, and Polymorphism.


Fields (variables declared within a class but at the method level, which comprises the object’s data),
properties (methods providing information about an object), methods (subroutines and functions that
provide actions to perform on an object), and events (subroutines designed to react to event triggers, such as
a mouse click or keyboard action, or raise them, such as throwing an error event to issue an alert ) are only
one part of the object-oriented programming equation. True object-oriented programming also requires the
language to support three qualities: Encapsulation, Inheritance, and Polymorphism.
 Encapsulation means a group of related properties, methods, and other members are treated as a
single unit. In simple terms, this is just like creating a Form, adding Controls to it, and writing
events, subroutines, and functions to support them. Classes can control how objects can be changed
and methods are invoked. For example, a class can validate values before enabling object changes.
Encapsulation makes it easier to change our implementation later on by letting us hide
implementation details of our objects; a practice called data hiding. Consumers of a class object
need not be concerned with the inner working of a class; they simply consume them, like not
worrying about the inner support code for a button control dropped on a form.
 Inheritance describes the ability to create new classes based on an existing class. The new class
inherits all the properties and methods and events of the base class, and can be customized,
enhanced, or extended by applying additional properties and methods, to include overriding
(superseding) methods existing in the base class with newer methods that exhibit different
functionality. For example, we can create a new class named "Truck" based upon a "Car" class. The
"Truck" class inherits the "Color" property from the "Car" class, but can also add its own additional
properties such as "FourWheelDrive." Inheriting a class is as easy as tripping over invisible logs in
public; just include Inherits myBaseClass under the heading of the new class, where myBaseClass is
the class, preexisting or a creature of our own creative genius, we want to inherit from.
 Polymorphism allows us to have multiple classes that can be used interchangeably, even though each
class implements the same properties or methods differently. polymorphism is important to object-
oriented programming because it lets us use objects with the same name, regardless of which object
is in use at the moment. For example, given a base class of "Automobile", polymorphism allows a
programmer to define different "StartEngine" methods for any number of classes implementing it.
The "StartEngine" method of derived class "DieselCar" can be completely different from that
method used n other implementing classes. VB6 faked polymorphism using class shells as duct-tape,
but VB.NET implements polymorphism properly.
 Page 33

Overloading, Overriding, and Shadowing.


Overloading, overriding, and shadowing are similar OOP concepts that can be easily confused. Although
all three techniques enable us to create members with the same name, there are important differences:
 Overloaded members are used to provide different versions of a property or method that have the
same name, but accept a different number of parameters, or parameters with different data types. For
example, a series of functions all named Cinteger that return type Integer, but each exhibits a
different type parameter; one is a string, another is a short integer, and another is double precision.
 Overridden properties and methods are used to replace an inherited property or method that is not
appropriate in a derived class. Overridden members, however, must accept the same data type and
number of arguments. Derived classes will inherit these overridden members.
 Shadowed members are used to locally replace a member of broader scope (visibility) with a member
of a narrower scope. Any type can shadow any other type. For example, we can declare a property
that shadows an inherited method declared as Public with a method of the same name that is
declared as Friend, Private, or Protected. Shadowed members, however, cannot be inherited.

Scoping Rules.
The visibility or accessibility of an object is handled by what are known as Scoping Rules. Most of us
are aware that if a method, which is a self-contained block of code, is declared Public, it can be accessed
by everything in and outside its code block, and Private methods are accessible locally to the file or
method or block within which they are declared.
NOTE: VB.NET rightly allows only the Dim keyword to declare private variables within methods and properties, which is
used to dimension additional space on the program Stack, used for local variable storage.
A VB.NET program is composed of code blocks, such as subroutines and functions, and often contain
blocks within blocks, where each block is a grouping of encapsulated code and data, much like the code
and data declarations within an If-Then-Else body. A Class block is defined from the declaration of the
class to the matching End Class declaration, an If block ends with End If, a Select block ends with a
matching End Select, and so on, to include inner blocks. A local variable has viability, or a Lifetime,
from the point of its declaration to the end of the block that it was declared within. At the end of that
block it falls out of scope and is dissolved. If an inner block declares a local variable with the same
name as a local variable or field declared in an outer block, then the inner variable takes precedence at
its level and inward during referencing, unless a deeper, like-named variable is declared. Further, an
inner block can access local variables and fields declared in outward scope where naming collisions are
not encountered. Scoping rules do, however, prevent code in one block from directly accessing a
variable in deeper, inner blocks, or in blocks where its own scope cannot reach, such as a block that has
been declared at the very same level within an encapsulating block, or in separate inner blocks declared
at outer levels., Even so, code in inner blocks can access variables declared in outer blocks, even if those
variables are declared Private, because inner blocks are included as a part of an outer block's definition.
From that gibberish we can surmise that outer variables are visible to inner
variables, but not visa versa. Also, scope visibility works in related blocks, where
we can see each block being declared in concentric class circles from outer to
inner. Consider the illustration. Variables declared in class “A” are visible to all
other classes, unless an identical variable is declared locally, or within an
intermediate block from the location trying to access the class “A” level variable.
Conversely, code in “A” cannot directly access variables declared in classes “B”
through “F”. Code in “F” can directly access variables declared in “A”, “E” and
“F”, but not directly in or within “B”. Code in “E” can directly access variables in “A” and “E”, but not
directly in “F”, or directly in or within “B”.
 Page 34

This accessibility to inner blocks changes, however, if we refer to the data as members of the classes.
So, if classes “C” and “D” are declared Public or Friend, and a string variable named “myName” is
declared Public in class “D”, then code declared within classes “A”, “E”, and “F” can access that string
by using the reference “B.C.D.myName”, or even “A.B.C.D.myName”.
On the other hand, if class “C” were declared Private, but class “D” were declared Public, no objects but
those declared within classes “B” and “C” could access “myName”, because class “C”, being declared
Private, has no visibility to outside objects except to those declared within itself or within class “B”
(objects in “B” are declared at the same level as class “C”; being siblings of “C” within class “B”).
NOTE: A lot of people have argued with me about objects in class “B” being able to access D.myName, but really think
about that one. Can we not still access it from “B” using C.D.myName? Even if “C” is Private, objects within “B” can still
“see” class “C” because they are siblings to it. And even so, that does not mean that its child members are suddenly invisible
to objects in “B”. Playing these mind games is fun. I still clearly remember first figuring this ancient illustration out.
NOTE: A better illustration is a baby who procured Mom's scissors. The child has full access to Mom, Dad and the scissors,
but Mom and Dad cannot access the scissors by the baby's evasive moves; this from a being with an IQ of 3 above a rock.
When the Dim keyword is used to declare a Field variable in the body of a Class, this is the same as
declaring it Friend. Personally, I have always preferred to declare variables defined outside a method
body or outside code blocks within methods specifically as Public, Private, Friend, or Protected, and
reserve using the Dim keyword exclusively within method code blocks for declaring local, temporary
variables, which have lifetimes only as long as control is within their declaration code's block, primarily
because they can only be of private scope. Although it is legal to use Dim instead of Friend at procedural
levels in a class, it is so simply for backward compatibility with older versions of BASIC, though this
should have been dropped for reasons of code clarity. Of course, technically, a lot of other VB6
functionality should also be dropped, such as using AND and OR for both logical and binary operators.
While on this subject, it is worth mentioning that if we declare a subroutine or function without Public,
Private, Friend, or Protected in the declaration, it does not, as most people assume, default to Public; it
actually defaults to Friend, which is Public in its scope, but Private to all external processes. By the
same token, Classes and Structures declared outside class bodies as Dim are treated as Friend. However,
to throw a little refined and oxygenated volatile petroleum products on this Dim fire, Fields and Methods
declared Dim within Classes and Structures are treated as Public (who thought these things up, and are
they still institutionalized? Actually, they were forced to do it this way because in VB6 we could not
declare field visibility within Structures except by using Dim or by an implied Dim, due to their much
more primitive and limited architecture).
This stuff can really make our brain itch until we can wrap our minds around it. So go through it a block
at a time, and try to explain to ourselves what each thing means (pretending to be explaining it to some
other idiot really helps). Once we can clearly explain it, it will all suddenly make absolutely perfect
sense (clinically, this is called Gestalt Awakening, an Epiphany, or Eureka). We all go from “What is
this damn fool babbling about?” to “Even a damn fool would understand it!” Of course, none of us ever
say that last part until the idiot part of our brains gets out of the way of our wiser, inner Self.
GENERAL NOTE: When I mention Microsoft adding features to VB6 like they were duct-taped in, I am in no way being
critical, What they did was ABSOLUTE GENIOUS, on the brilliant inventive level of Tim Allen and his “backyard
engineering”. I use duct-tape for everything, Got a ratty-looking wallet? An old baseball cap that got chewed up in the
washer? A Bible whose cover has come loose? The ladies will go out and waste money on new things. Guys go for the duct-
tape. The above 3 examples are real. And yes I did. We do not worry about appearance; we worry about functionality. By the
way, has anyone ever noticed how damaging a woman saying “That's so cute!” is to our wallet? I think the battle damage my
wallet suffered was from paying for “cute” items, the resounding bank impacts on it being why it started looking so ratty.
 Page 35

My Four Basic Guidelines for VB.NET Development.


Everyone has a set of rules that they have grown into or developed over time. Following are four
fundamental rules that have helped me to stay out of a lot of trouble while using VB.NET:
1. Always set Option Explicit and Option Strict options to On. Option Explicit On requires that all
variables be declared before they are referenced. Option Strict On disallows type conversions
that would result in data loss. This might sound a bit harsh, but I think anyone who leaves these
options Off is an amateur who does not feel responsible enough to ensure that their code is
reliable. Option Strict On forces strict type checking, not allowing automatic variable type
casting, which can result in sneaky data-loss bugs in code that are quite often very difficult to
track down,. I feel that anyone who cottons to the idea of automatic type casting, that data loss is
not important is nothing but a hack, not a serious developer, or a true developer at all. When one
needs to convert the value of a Long variable to a Single variable, I would prefer to see the code
perform an explicit CSng function. This does not add additional code to the compiled program
because it represents code that would otherwise be automatically added by the compiler.
However, a person reviewing such code will know the author intentionally performed this task. It
also goes a long way toward self-documenting that code and eliminates hard to find bugs that
might cast values to a “mystery” type that might cause issues when used later on. This is
especially frustrating when a block of code works sometimes, but not at others.
2. The only default property that we should ever consider is indexed items in lists. For example,
being able to specify myObject.Items(10) instead of myObject.Items.Item(10). Anything else should
normally be taboo. Why? For reasons of self-documenting code. In VB6, what makes more sense
to us as someone who did not write the following code: myData = "Hello" or myData.Text =
"Hello"? What does the second example show us that the first one does not? Anyone who
complains about the loss of parameterless default properties needs to learn how to develop real
code, not romper-room stacked blocks. The more obvious one makes the code, the easier it is for
someone else to read. And anyone who thinks that implicit type casting is great is someone who
would be a big mistake to hire – their code will likely contain potential casting anomalies. In
other words, avoid at all costs taking advantage of non-self-documenting default behaviors,
except where they make sense, such as indexed lists.
Also, consider that all default properties are resolved only during a Late-Binding process, which
resolve each time they are encountered during runtime, slowing data type resolution time down
by up to 5 times, especially for implicit typing. Default properties slow processing time another 5
times, resulting in late-bound code running up to 10 times slower than early-bound code, where
types are resolved during runtime. Do you still like default properties and implicit typing?
3. Use the Dim statement to declare only variables within methods (subroutines and functions),
properties or events. Use Private, Protected, Public,or Friend declarations on methods,
properties, or fields declared outside of these methods. These are much more self-documenting
than using any default behavior. We all should know that variables declared with Dim inside a
method is private to the code block within which it is declared, and to sub-blocks. Not every new
developer can answer the type of a method-level variable declared with Dim. It is not Public.
4. Please do not depend on default scoping rules. Defaults sometimes change. Explicitly declaring
classes and methods as Public, Private, Friend, or Protected goes a long way toward telling
someone who lost a coin toss and is stuck reviewing our code of what our intentions actually are.
Half the people I talk to think that declaring classes without such scoping keywords defaults to
Public, when in fact in VB6 and VB.NET it defaults to Friend. When people examining our code
make the wrong assumptions about it, it makes it much more difficult for them to locate the
errors that a second set of eyes is normally capable of finding. Self-documenting code Rules.
 Page 36

Closing Introductory Remarks.


One of the biggest complaints I hear from VB6 users is that VB.NET will not cleanly upgrade (using the
Upgrade Wizard) from a previous version of VB, typically requiring numerous additional after-upgrade
edits, which are clearly flagged in special, traceable comments within the upgraded code, awaiting
attention. After all, this was not usually as extensive as for VB1 through VB5, which might just require a
recompile, and maybe some minor, or sometimes some very extensive tweaks, depending on the features
being upgraded; but even this is a luxury that is amazingly unique to VB for computer languages, on top
of the fact that they are moving from a Procedural Programming Language to an Object-Oriented
Programming Language, which are, programmatically, worlds apart.
Hence, VB6 users seem to feel cheated, and might even declare with inner-conviction that VB is now
broken. Honestly, if anything was broken, it was VB6. It failed time and again to follow syntax rules
when it realized new features, as though they had been simply plugged in from other specifications. The
end result was that the VB1 through VB6 family had finally digitally painted itself into a corner, and
expansion could now only be incorporated through hacks and duct-tape. Plus, technology had changed
profoundly since its introduction in 1991, and in fact had long ago passed it up. VB’s roots began in
1988, when Alan Cooper demonstrated his original visual concepts to Bill Gates. Project Tripod was
later purchased, enhanced, and merged with QuickBASIC to become Project Ruby, and released as
Project Thunder; VB1. At that time, systems standards were 16-bit, and device communications were
hand-coded (search for Alan Cooper’s online article, Why I am called “the Father of Visual Basic”).
VB6, or a VB6+1, was what Microsoft first intended to develop, but in late 1999, they concluded that
such a version would be too constricted in the rich .NET environment and could never survive or even
compete in a 64-bit world. It was having enough trouble coping under 32-bit architecture, where its base
integer type did not come close to the platform’s integer type. VB6 used 16-bit integers, whereas a 64-bit
platform uses 64-bit integers. An integer type should comply with the architecture it operates on, or at
least close, so using 32-bit integers on a 64-bit system makes more sense than using 16-bits, especially if
we think about the VB6 String format being Unicode, where a single character is also 16-bits wide.
Another complaint, probably shouted the loudest, was that VB keywords and syntax have changed, and
dinosaur technology like Direct Data Exchange (DDE) was no longer supported (well, actually it is –
through COM references). This is the price we must pay to progress forward.
Welcome to the REAL world of software engineering. I have been a software engineer since 1978. In that
time, individual languages have changed significantly. And every time, the upgrade process from one
level of a language to the other has always been frustrating, and clenched fists have always been shaken
at the dark sky, as our forlorn, sleep-deprived howls of aggravation echo unrequited and muffled in the
fog-shrouded hollows of the night. It all comes down to this: We can be a victim of change, or a
participant in it.
While most VB6 users feel they are alone in this dilemma and are the sole victim in this language
upgrade predicament, they most certainly are not. They have it easy compared to some language
upgrades. Try translating between C (K&R C) and C99, or even to C++85 or ISO C++95, or better,
between upgrades of the very first high level language, FORTRAN (Formula Translation), such as
between FORTRAN57 and FORTRAN77, or to FORTRAN95. Upgrades bite everyone, sometimes
severely. Amateurs whine and complain. Professionals sigh, and then hit the books. It is quite often not
an easy pill to swallow, the thought of cycling from coveted ‘guru’ to rank novice due to such changes. It
has happened to me more than I care to say. Every professional in the business long enough has gone
through these cycles enough times to now just roll their eyes each time it happens.
I, myself, was apprehensive about the upgrade from VB6 to VB.NET. Prior to VB2005, it was a severely
rocky path, and perhaps the whining and complaining I heard were to a great extent justified. But
everyone should have expected that such a necessary, monumental paradigm shift should take so much
time. But since VB2008, upgrades have become easy, and syntax is so-o-o-o much friendlier to VB6.
 Page 37

If there is fault to cast on Microsoft, it would have to be that they should have continued to fully support
and fully maintain VB6 with undiminished dedication until the release of VB2008. Microsoft did claim
to provide “extended” (actually, I would call it “nodding”) support for VB6 until March 31, 2008.
VB2008 was unveiled on November 19, 2007. However, this “nodding” support was in no way FULL
support, which requires a staff giving it FULL, proactive attention, but I will grant that they did develop
some excellent VB6 to .NET leveraging tools. The pre-VB2005 .NET Framework requirements for
sometimes long and convoluted command paths in order to just support some rather simple and
minuscule VB6 commands should never have been deemed acceptable at Microsoft. The “My”
namespace should have been introduced much earlier than VB2005 to address that situation.
Advancements in high-level languages might require changes in coding habits, but they are supposed to
make software development easier with each successive release of the development environment, not
more complicated. In my mind, VB.NET was Beta until VB2005 was released. Further, I considered
VB2005 to be a Release Candidate until VB2008 was released. I thought VB2008 was a more valid
development environment release than the sad, rocky predicaments VB2002 and VB2003 placed us in.
When VB2008 was released, I believe that Microsoft finally got Object-Oriented Visual Basic right.
Upgrades from VB6 to VB.NET have finally become easy, and by my determination, native VB.NET
development has finally regained its highly coveted status as a Rapid Application Development
platform, and later editions have only made this progression even better.
Goodbye, VB6...
Now, as we prepare to peruse this thick tome, we are going to find some real gems. We are going to
discover things some thought were impossible. After all, it had actually been my job to do just that – to
find solutions for everything most solution designers and software engineers deemed impossible, or too
difficult to do. We will even find things that Microsoft said was not possible. The paradox here is that
many of these solutions are so bloody simple, using just a few lines of code, that I am flabbergasted that
no one else ever thought of them, All it requires is stepping “outside the box” for some fresh air, and
approaching the problem from a brand new angle. For example, one technique I discovered several years
ago is featured in Black Book Tip # 61 on page 763. It explains a brain-dead simple backyard
engineered solution to adding transparency colors to a form's BackColor property (this otherwise results
in an Exception Error) that it seems no one ever thought of, though it is unbelievably easy to implement.
But why stop just there? So I fill that section out by sharing properties that too few people know about
that can really amp up the power and capabilities of our forms and custom controls.
The engineering “herd” is always trying to solve problems using “tried and true” techniques that their
mentors, their instructors, and even those they actually do like, grilled into them because that is how
those mentors and instructors learned to solve those problems, and they had often even advised us to
“never deviate from these techniques and you will never go wrong.”
Taking that advice to heart is like trudging muddy trenches in France on the Western Front during World
War 1; we are stooped over and cannot see the great expanse beyond its lip, fearing the bullets of
criticism from others.
Indeed, I almost lost my job working for a former dyed-in-the-wool IBM engineer because my
approaches were so radically divergent from the norm. Literally one day before he was going to “let me
go” on the traditional “let 'em go” Friday, we had a problem for a critically important client (read this as
well-monied), who he promised “our team would resolve the problem before close of business, or there
was no charge.”
...Until he and my fellow engineers realized that the problem was impossible to fix. Of course, they
rattled the “usual” ideas and techniques off, like first-graders reciting the Alphabet, from 10 AM to 2:30
AM the next morning (as long as we were working the problem, it was not close of business). All to utter
failure. I just leaned back to hold up a file cabinet and analyzed this circus, because no one dared let
 Page 38

Daffy Dave touch this critical computer/software problem, rejecting the solution I had offered at 1 PM as
utterly ludicrous. Finally, when they were totally spent, and he and they were resorting to desperately
devising a believable excuse to deliver to the client, having reached the point where even letting me
touch it could do no harm, I pushed them aside and in less than 3 minutes of pecking madly on the
keyboard of the client's computer, entering the rejected solution I had offered over 13 hours earlier, I
managed to completely solve the problem, save company face, and yield it a fat payday from the client.
Within a year I had a salary larger than the rest of our sizable development team combined (and my
annual bonus was larger than my salary), producing more bug-free code each day than all the other
developers put together (these are not exaggerations). Why? Because I could do what I said and firmly
believed I could do. Afterward, it was nothing for my boss to give a request for something he thought
was impossible, but he wanted me to take a look at it anyway, and later that day or the next morning
when he came into the office I would hand him fully functional code that did exactly what he requested.
My boss often asked me how I managed to solve so many impossible problems as fast and as easily as I
did. I simply replied that desperation is the mother of invention, and when we are in it over our head, we
quickly figure out how to swim.
So be brave, be resourceful, be inventive. Look at everything from outside the box.
Above my desk is a banner the company made for me that reads:

I can program anything; but if it is impossible, it will take me a couple more hours.
–– David, the Great and Humble.

Make that your banner too.


 Page 39

VB.NET Versus VB6


Platform Differences.
Exploring the Changes in the VB Language, and VB.NET's Advantages Over VB6.

Most Common Issues Reported by VB6 Upgrade Wizard % of Reports


Could not resolve default property of object “<objectname>” 52%
Property/method was not upgraded 13%
Property/method/event is upgraded but has a different behavior 12%
COM expression not supported 7%
Use of Null/IsNull() detected 4%
Sum of All the Above ▬▬▬▬▬▬▬▬▬▬▬▬▬▬► 84%
 Page 40

Noteworthy VB.NET Features That Differ From, or Are New Since VB6.
The copious notes posted herein were initially transcribed from sporadic cuff notes I scribbled down as I
migrated from VB6 to VB.NET. Like any scientist, I endeavor to document everything I learn, and I do
not consider it learned until I can also adequately describe it. These notes, indeed this entire tome, began
as but a 6-page stack of madly jotted personal reminders, some of which I could afterward even read
without putting the local state university's linguistics department to its ultimate test. This stack continued
to grow as I learned or discovered, mostly by accident, newer and better means with which to adapt VB6
program code to the more well-defined syntax of VB.NET. First, to Beta1 and Beta2, an experience so
aggravatingly painful that some of us still bear its deep and disfiguring scars. We all should have been
awarded medals of valor and been atta-boyed by the President in the Oval Office after that traumatic
experience. Even so, many of us were prescient enough to recognize the potential for what it would
become as the .NET Development Team desperately scrambled to add functionality of truly legendary
proportions, the kind we spark kid's imaginations with around crackling campfires. So, we plodded on
through the painful experience of VB2002. VB2003 was a little less painful, if the winds were right.
VB2005 was a bit more pleasant upgrade. VB2008 was finally a rather easy upgrade. And so on.
Prior to the release of VB2005, transitioning VB6 code to VB.NET was in most cases, save for the
simplest of applications, a dreadfully trying path, pregnant with exasperating pitfalls that often required
major redesign of otherwise rather simplistic VB6 code segments. The only reason we stuck to it was to
build VB.NET savvy as we waited for its design to stabilize. When VB2005 was released, I noticed that
dozens of my stack of upgrade notes were no longer required as VB.NET compatibility to VB6 had been
dramatically tightened, and so those many pages were gleefully discarded from my personal tome.
When VB2008 was released, greater quantities of my notes no longer had relevance, and my new notes
then began to focus more on extending VB.NET itself, not just upgrading code. Indeed, VB2008 was the
very first VB.NET edition that I ever recommended to anyone just considering a transition from VB6.
VB2005 and VB2008 were strongly focused on language and VB6 compatibility. VB2010, though it
dramatically enhanced VB6 compatibility, it also began gearing itself more toward ADO.NET, business
transactions and web development. Yet, the Integrated Development Environment, or IDE, of VB2015
and 2017 made any migration worth it, bringing most needed development tools to within easy reach.
With a little more tweaking, I think the upgrade portions of my notes will start to shrink back to their
original 6 pages, if not eliminate their need for such requisite upgrade notes entirely. I can only wish.
Finally, be aware that there will be some slight repetition or similarity between a rather small number of
the notes in this section. You are not experiencing deja vu. This tome being a solution book for both
those saddled with upgrading their own, or worse, someone else's VB6 code to VB.NET, and those
veteran VB.NET developers who simply want to move far beyond the limiting solutions VB6 had drilled
into us as almost unbreakable rote, I sometimes had to approach the same problem from perspectives,
where one view makes more sense to someone upgrading VB6 code, and another view that makes more
sense to veteran VB.NET developers. I will also be repeating, a lot, advice that we can pass strings
ByVal to Pinvokes, because I am asked constantly how to pass mutable strings more times than I can
possibly count. This is one place Microsoft really screwed the pooch on, confusing the issue really badly
by not getting out in front of it, but instead trying to obfuscate and divert the blame, all in an effort to
avoid admitting a serious mistake. Even so, their intention had actually been to save the less disciplined
among us from ourselves, though their aversion to admitting guilt did nothing but piss many of us off.
NOTE: Solutions using instructions to the .NET Cross-Language Compiler, which are embraced within angle brackets,
“<>”, to flag special compiler Attributes, such as GCHandles, Marshaling instructions, or anything else thus embraced,
like <VbFixedString(xxx)>, will assume we have also placed the following line in the heading of any source code file they
are used in, below any Option instructions,but before its Class or Module declaration, in case I ever forget to mention it:
Imports System.Runtime.InteropServices 'This is our portal to INCREDIBLE programming power!
 Page 41

● Notes on Using the VB6 Compatibility Library.


If we came from VB6, until we are comfortable in VB.NET, we may want to
go to the References Tab on an application’s Project Properties and add a
reference to Microsoft.VisualBasic.Compatability. This allows us to import
this namespace into each class or module file needing it, or, in the Imported
Namespaces panel, ensure that the Microsoft.VisualBasic.Compatability
namespace is checked for an application-wide import of it. Either way, this
allows us to specify VB6-compatible features using its VB6 class. But, as I
will demonstrate throughout this tome, I prefer to take exclusive advantage of VB.NET’s far more
powerful and extendable capabilities and bypass any restrictive VB6 syntax altogether.
Microsoft says that when a VB6 application is upgraded to VB.NET, the conversion of some VB6
code is impossible due to syntactical or architectural differences. For this reason functions in the
VB6 Compatibility Library are used to allow code to run in VB.NET without the need for extensive
modifications. The functions provided in the VB6 compatibility namespace mimic VB6 behavior
while remaining compliant to the .NET CLS (Common Language Specification). We should search
the VB.NET help using “vb6 compatibility library” for more details.
To my mind, that is a rather fast and loose use of the conversion of some VB6 code is impossible
phrase, for conversion of VB6 code to VB.NET is incredibly easy, as we will learn in this tome.
Call me bull-headed, or full of the other end of the bull, but I do not recommend this. It has been my
own experience thus far that I can easily duplicate all the “altered” VB6 functionality, and quickly,
and with but little editing, and I have yet to be unable to provide truly superior alternatives by
employing the tremendous power offered me by the .NET Framework. Indeed, by not trying to
confine myself within the restrictive rote of VB6 code specifications, we can truly spread our wings
and make our code shine brightly by embracing the liberating environs of the .NET Framework.
For example, consider this VB6 statement using the Format command, “myStr = Format(TotalFolders
- FolderCnt, "#,##0")”, is upgraded by the Upgrade Wizard (sadly lacking as of VB2010) to “myStr =
VB6.Format(TotalFolders - FolderCnt, "#,##0") ”. First, I find this a bit of bother, because VB2008
fully supported the original VB6 statement (and this is probably one of the main reasons why the
upgrade wizard was dropped after VB2008). But regardless, we can speed this code up even more by
instead using “myStr = (TotalFolders – FolderCnt).ToString("#,##0")”. Because expressions are also
value-type objects, they behave exactly like object variables or fields, and so expressions, even
strings, values or methods, provide an exhaustive repertoire of method and property members.
Another example regarding the Format statement is in expressing dates and times as text. Many have
complained that although VB.NET supports a wide range of Date and Time formatting tags, it does
not recognize some “classic” VB6 shortcuts, such as "Short Date" and "Long Time". As of VB2008,
this is no longer true. But even so, that does not mean that their functionality was not already built
into even older editions of VB.NET. For example, the VB.NET Upgrade Wizard will take the VB6
statement “S = Format(Now, "Short Date")” (which is supported as of VB2008) and converts it to “S =
VB6.Format(Now, "Short Date")”. However, it would actually be better to take advantage of VB.NET’s
more memory-efficient and much faster built-in functionality by instead upgrading it to “S =
Now.ToShortDateString”. But this is not our only option. Another slick means of expressing dates and
times is by taking advantage of a Date object’s ToString method, where we can optionally specify
date and time formats with very little typing. For example, we could change the above “ S =
Now.ToShortDateString” assignment to “S = Now.ToString("d")”. The “d” format tag, a shortcut tag
representing the short date string “M/d/yyyy”, which we can also enter, is just one among many tags
that VB.NET reserves to express dates and times in just about any format we wish, to include custom
formats (see the next Note). Still another option is to use the VB.NET FormatDateTime function and
its DateFormat enumeration, where we can specify short or long date and time formats. Search
the .NET help, or do web searches for “DateTimeFormatInfo” and “DateTime.ToString Method”.
 Page 42

● Notes on Useful Format and ToString Date/Time Shortcuts.


Following is a brutally truncated table of just a few shortcut tags available for use under VB.NET:
Format Comment (long form definition) Result for January 22, 2010
"d" or "Short Date" M/d/yyyy (Short Date Pattern) 1/22/2010
"D" or "Long Date" dddd, MMMM dd, yyyy (Long Date Pattern) Friday, January 22, 2010
"t" or "Short Time" h:mm tt (Short Time Pattern) 2:22 PM
"T" or "Long Time" h:mm:ss tt (Long Time Pattern) 2:22:48 PM
"f" dddd, MMMM dd, yyyy h:mm tt (Full Date Short Time Pattern) Friday, January 22, 2010 9:53 AM
"F" dddd, MMMM dd, yyyy h:mm:ss tt (Full Date Time Pattern) Friday, January 22, 2010 9:53:21 AM
"g" (General Date Short Time Pattern) 1/22/2010 9:53 AM
"G" (General Date Long Time Pattern) (default) 1/22/2010 9:53:21 AM
"M" MMMM dd (Month Day Pattern) January 22
"R" ddd, dd MMM yyyy HH':'mm':'ss 'GMT' (RFC 1123 Pattern) Fri, 22 Jan 2010 09:53:21 GMT
"s" yyyy'-'MM'-'dd'T'HH':'mm':'ss (Sortable Date Time Pattern) 2010-01-22T09:53:21
"u" yyyy'-'MM'-'dd HH':'mm':'ss'Z' (Universal sortable) (invariant) 2010-01-22 09:53:21Z
"U" Universal sortable Friday, January 22, 2010 2:53:21 PM
"Y" MMMM, yyyy (Year Month Pattern) January, 2010
"o" Roundtrip, local (useful for sorting) 2010-01-22T09:53:21.2512235-05:00
"o" Roundtrip, UTC (useful for sorting) 2006-01-22T09:53:21.2512235Z
“d MMM yyyy” (User-customized format) 22 Jan 2010
“dd MMM HH:mm:ss” (User-customized format) 22 Jan 09:53:21
“\Mon\t\h\: M” (User-customized format) Month: 1
“h:mm:ss.ff tt” (Customized format) 9:53:21.00 AM

● Notes on Passing Structures to Win32 Pinvokes.


At times we need to send a Structure (a VB6 User-Defined Type, or UDT) from the .NET Managed
Space environment to interoperate with the unmanaged 32-bit Windows Operating System, called
Win32 Interop, using a Platform Invocation, or Pinvoke (also called an Application Programming
Interface, or API), in a manner similar to how VB6 did it. Yet, for Win32 interop under .NET, we
must additionally provide its cross-language cross-platform compiler with marshaling (management)
instructions to ensure the structure's Member Fields are sequenced in memory in the same order as
they were declared, just as all Win32 Interop methods expect, even if doing so makes it less
memory-efficient and slower, and, if needed, string data is formatted as the Pinvoke requires.
NOTE: A perk of the .NET Managed Space is it auto-deallocates all unreferenced Objects instantiated by its own methods.
NOTE: I should be using the terms Cpu32 or Cpu64 in place of Win32 to address cross-platform code development.
Also, using Win32 Interop methods inhibits .NET from non-Windows platform interoperability. Non-windows platforms
cannot be expected to support them, though .NET does provide its own methods to mirror most Win32 interop features.
For good reason VB.NET does not always store member fields in their declared order, though this
reordering is 100% predictable, to reduce possible memory alignment gaps between fields. Access is
up to 500% faster when fields align on boundaries that are multiples of the CPU's 32- or 64-bit
architecture, called Architectural Alignment. By default, .NET uses multiples of the next field's unit
size, called Natural Alignment, which provides 200% to 500% faster access. Though a bit slower
than architectural alignment, its biggest advantage is being less likely to yield field gaps, thus saving
memory, especially when fields are reordered to further minimize gaps. See VB6 User-Defined
Types Compared to VB.NET Structures on page 206 for more details on avoiding memory gaps.
Marshaling to support Win32 interop sounds complicated, but with .NET’s Runtime Interoperability
Services it is easy. Just add “Imports System.Runtime.InteropServices” in the file header. Fortunately,
most of this typing can be automatic, thanks to IntelliSense and Auto-complete. We can then precede
any structure we need to marshal for Win32 interop with “<StructLayout(LayoutKind.Sequential)>”, to
include any other special marshaling instructions to tailor-fit the data to suit the requirements of the
invoked method. Again, when we start typing, IntelliSense will lend a hand. For example:
<StructLayout(LayoutKind.Sequential, Pack:=1)> 'NOTE: ':=' allows using parameters out of order. No need for blank commas!
Private Structure BrowseInfo 'See the description below regarding the additional PACK parameter.
Dim OwnerID As Byte 'Owner ID tag (VB.NET might store these fields in a different, more efficient order).
Dim hwndOwner As UInt32 'Handle to the owner (without Pack:=1, this might be aligned 4 bytes higher than OwnerID).
End Structure 'NOTE: Faster access is possible if hwndOwner was ordered before OwnerID. Figure out why.

NOTE: The parameter assignment tag, “:=”, is actually a shortcut to allow specifying only certain parameter fields in
perhaps a long list of optional parameters, even out of order, without needing to add intervening empty commas.
 Page 43

The “Pack:=1” marshal parameter forces class and structure fields to use byte-packed spacing, where
each consecutive field starts on the byte directly following the previous field. Though slower than
the default natural alignment, it is far more memory efficient, This C++-style byte-packing presents
issues due VB6's inability to support it. However, most VB6-approved APIs are sequenced to avoid
this issue by adding special fields labeled Undefined to class and structure declarations so to fill out
any unavoidable natural alignment field gaps. If such gaps are not accounted for, they can cause data
corruption if non-CLR Win32 processes do not anticipate possible architectural gaps between fields.
NOTE: Each member field is aligned in memory equal to a multiple of its size for faster processing,
although aligning fields on the CPU's Integer size, 32- or 64-bits, processes them a whole lot faster!
NOTE: Using LayoutKind.Sequential simply tells the compiler to line the fields up in an internal offset table in the same
order they were declared, regardless if this is less efficient. Also. Pack:=1 tells the compiler to point each member in
sequential offsets based on their actual lengths. At its core, much of the magic .NET performs is really quite elementary.
A famous exception is the GetComboBoxInfo Pinvoke and its infamous COMBOBOXINFO structure. which
few coders resolve properly, most employing an easy hack by plugging the expected structure length,
64, into its cbSize field. However, we will explain and resolve it properly by demonstrating two MS-
approved techniques that can also be employed if this issue rears its head elsewhere. It is covered in
Black Book Tip # 59: Adding a Horizontal ScrollBar to a ComboBox DropDown List on page 757.
We might also need to consider ensuring that any string members, which are by default 16-bit
Unicode, are passed instead as 8-bit ANSI strings, if needs require, to a system Pinvoke that expects
ANSI character strings instead, which can be achieved by adding “CharSet:=Charset.Ansi” in the
StructLayout parameter list shown above, or by specifying that its string data will be handled as some
other expected Unmanaged Type, because system strings actually have numerous formats beyond
just Unicode, like LPSTR, LPWSTR, ByValTStr, BSTR, or whatever else is required. Also refer to the article,
Adapting Win32 String Data Types for VB.NET Interop on page 199 for more detailed information.
NOTE: The use of Unmanaged Type can be confusing. It just refers to various Win32 string types that are not directly
managed by the CLR, though most are CLR-compatible, like LPSTR and LPWSTR for ANSI and Unicode CLR formats.
NOTE: Although, as a “power user”, I tend to specify the actual string data type, like Ansi, in structures and Pinvoke
declarations, we can actually get by just using the Auto verb instead, letting .NET figure out appropriate formats for us.
It does this by examining both the user declaration and by examining the specified DLL's internal reference table.
NOTE: See Notes on Mixing Fixed Strings, Fixed Arrays and Structures on page 45 to see how to assign fixed strings and arrays to
VB.NET Structures if an application requires a structure to be of a uniform, fixed size, such as for use in random access processing.
NOTE: VB.NET value types are system-defined Structures. They are handled internally like classes, but with exceptions
(abstractions). In short, they do not auto-initialize, their object data is stored on the system Stack so they can clean up
automatically when they go out of scope so allocation tables do not need updating, their variables are tightly bound to
them, their references cannot be shared, and unlike a class that hides its data, structures freely share their data.
● Notes on Parameterized and Non-Parameterized Arrays.
VB6 Strings were Value Types, or Structures. Conversely, VB.NET Strings are handled as Arrays of
type Char, being therefore Reference Types because all VB.NET arrays are likewise Reference Types.
Variables for Objects are called Reference Pointers because they are assigned the reference address
of an Object that is stored away in protected memory. To create a new object and also assign it to a
reference variable requires that the object be declared using the New verb, either directly or through
an invoked method, like Space(), because we are instantiating new object space within the system's
protected memory Heap, and its reference address is then assigned to the variable. Conversely, the
addresses of pre-existing objects, like constants, reference variables and string literals are assigned to
variables without the New verb because those objects already exist elsewhere before assigning them.
Using Dim S As New String(ChrW(0), 64) requires the New verb because the statement is creating a
brand new Heap-stored string object. Yet, assigning a string literal, like Dim S As String = "Test" ,
does not call for the New verb, nor does an assignment from a string variable, like Dim S As String =
stVar, where stVar points to a string object. This is because we are not assigning newly instantiated
data, but that data already exists, either in the source code or through reference indirection.
 Page 44

Notice the above "Test" string literal is in fact a reference type, being an array of type Char. But,
assigning pre-existing VB.NET strings seems to act just like VB6 value type strings, which copied
one variable's data to another using a process called a ByVal Copy, because the destination variable
receives a cloned copy of the source data (a ByRef Copy copies just a reference pointer to the data).
In the above literal and variable examples, new clones of the pre-existing data are in fact instantiated
in the Heap and assigned to variable S, because VB.NET is performing automatic cloning of pre-
existing “simple strings”, which I call Non-parameterized Arrays, internally using an array’s Clone
method. It does this for us from “behind the green curtain”, for greater compatibility to VB6, but
most especially to maintain VB’s famous Rapid Application Development functionality. As such,
even though strict OOPL guidelines specify an OOPL target string variable should be assigned the
address of the string literal or given a copy of the address held by a source variable or constant, as C,
C#, and C++ do, these guidelines also allow for Implementation-defined Extensions, which VB.NET
takes full advantage of, because doing otherwise would severely inhibit its RAD functionality.
To keep the idea of VB.NET simple strings simple, I refer to those declared like Dim myStr As String
as Non-parameterized Arrays, being arrays of type Char that lack indexing, so using myStr(2) on
them will be flagged as an invalid reference. However, with explicitly declared Array references, or
Parameterized Arrays, being arrays with indexing, like Src(2), when we copy one array reference to
another, it will not copy the array’s data (a ByVal Copy), but rather just the reference address of the
array (a ByRef Copy). Hence, the destination array variable will point to the exact same array data as
the source array variable. This is in strict compliance to OOPL rules. In order to actually copy a
parameterized array's data, we must use the source array’s Clone method. For example, if we had a
string array variable named Src() that references string array data, and another string array variable
named Dst(), we assign Dst the array address Src points to by using the assignment Dst = Src. Yet, if
we instead need to copy the actual array’s data, giving Dst a separate copy of the array, not just a
copy of the array's address, we must implement the assignment statement Dst = Src.Clone.
NOTE: We can also copy non-parameterized strings the exact same way (this is precisely what VB.NET does internally
when we copy one string variable to another), Knowing this, we can pass an immutable string to a method that normally
alters a string by sending it as DataMangler(myString.Clone), or, if we have Option Strict On, which I hope we do, along
with Option Implicit On, because the Clone method returns the result as an Object array. we pass it as
DataMangler(DirectCast(myString.Clone, String)) or DataMangler(myString.Clone.ToString) , but the DirectCast
compiler directive uses no extra code, and so we end up using the same compiled code as DataMangler(myString.Clone)!
A case in point: the debug output from the following code sample will yield “Zero”, not “First”:
Dim Src() As String = {"First", "Second", "Third"} 'init 1st array. Keep in mind Src, like Dst, is just a POINTER.
Dim Dst() As String = Src 'Declare 2nd string array and assign the Src's address to it.
Src(0) = "Zero" 'Change the content of a Src array data element.
Debug.Print(Dst(0)) 'Now see if Dst reflects the change at the corresponding index.

As stated, to give Dst a copy of the array, change Dst = Src to Dst = Src.Clone. Again, if we have
Option Strict On, we must cast it to a string array because the Clone method returns the result as an
Object array. For example, Dst = DirectCast(Src.Clone, String()). We use DirectCast here because
the return type is known, and so we end up using the exact same compiled code as Dst = Src.Clone.
Although simple strings are arrays of Char, they have been handled as a special RAD shortcut ever
since VB2002 because they are typed as non-parameterized arrays. For these strings, unlike with
C/C++, VB.NET will automatically apply any needed cloning for us, just like VB6 actually did, but
fully behind the scenes (under VB6, copying arrays were also automatically cloned for us). This
way, most string processing under VB.NET will not be more complicated than it really needs to be.
Just keep in mind that only copying parameterized arrays requires we use the Clone method. For
example, consider this unnecessary, but working VB.NET code using non-parameterized strings:
Dim Src As String = "123" 'Initialize simple source string (this is technically an array of type Char).
Dim Dst As String = DirectCast(Src.Clone, String) 'Apply copy to Dst (more work than required. Just use 'Dim Dst As String = Src').
Src = "ABC" 'Change source data.
Debug.Print(Dst) 'Check result. It will report "123", even if we had used 'Dim Dst As String = Src'.
 Page 45

Because VB.NET auto-handles cloning of non-parameterized arrays, the declaration of Dst can
simply be “Dim Dst As String = Src”, just as it was under VB6. Also, the Clone method greatly
simplifies the more complex process of copying strings and arrays as required by C/C++. The Clone
method is now in the open for reasons of cross-language interoperability, but the process is still
simple enough to not tarnish its Rapid Application Development functionality. Copying strings under
C/C++ can be a bother, and more tedious with complex arrays. VB.NET still has it way too easy.

● Notes on Mixing Fixed Strings, Fixed Arrays and Structures.


Many VB6 classes, structures, APIs, and file I/O used fixed-length strings. To convert fixed-size
fields or string variables to VB.NET, like Dim Str As String * 128, we are typically recommended to
just dynamically create strings of the needed length. As such, to emulate this under VB.NET, we can
use Dim Str As String = Space(128), allowing 256 bytes; 127 characters plus a null terminator, letting
Space instantiate the new string object for us, and then use LSet or PadRight during assignments to
maintain its full length size. We are also often advised to employ VB6.FixLengthString, a member
of the Microsoft.VisualBasic.Compatability namespace, utilized in the form Public Str As New
VB6.FixedLengthString(128) to create a string of a size that can be treated like a VB6 fixed string.
NOTE: I prefer to avoid this last suggestion and sever explicit VB6 namespace connections, though I will be the first to
admit that it also makes VB6 to VB.NET upgrades a whole lot easier and smoother in the short run, But, we can also try
using a more powerful StringBuilder class, found in the System.Text namespace, which can also be set to a fixed length.
I recommend that for a VB6 fixed string declared as Dim Str As String * 128, to replace it in VB.NET with
<VBFixedString(256)> Dim Str As String (see the note below for why I used 256 (128 x 2, not 128). Even better,
if we need a fixed array of 128 Unicide Chars, but handled as a String, define it as “<VBFixedString(256)>
Dim Str As Char()” or better as <VBFixedArray(127)> Dim Str As Char(). Notice here we used Dim Str As
Char() instead of Dim Str() As Char (Strings are zenomorphs!). Note these marshaling commands actually
embed their strings and arrays within the structure or class object! Note finally <VBFixedArray(xxx)> specifies
the Dimension Upper Bounds of a Unicode array, not its byte length (0 to 127 = 128 Unicode Chars).
Also, declaring <VBFixedArray(127)> Dim Str As Char() makes Str a 128 Character VB.NET Fixed
String, though the docs seem not to indicate that. If we were to declare it as LPStr to define a fixed 8-
bit ANSI String using <MarshalAs(UnmanagedType.LPStr, SizeConst:=128)> Dim Str As String, keep in
mind the compiler might do lots of hidden background Unicode/ANSI conversion to copy data
assignments back and forth. Also bear in mind that we might need to ensure full field filling if we
will not use null-terminated strings, so we will need to get used to using LSet or PadRight a lot.
With “CharSet:=CharSet.Ansi” in a marshaling tag, the compiler treats string members as 8-bit ANSI
strings. We will cover mixed types in Adapting Win32 String Data Types for VB.NET Interop on page 199.
'---------------------------------------------------------------
' Structure for system version information (for use by thw Win32 GetVersionExA Pinvoke)
' Note that we can MANUALLY fill this structure by reading My.Computer.Info.OSVersion and My.Computer.Info.OSPlatform.
'---------------------------------------------------------------
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi)>
Private Structure OSVERSIONINFO 'force what was done for VB6 APP command "behind the green curtain".
Dim dwOSVersionInfoSize As Integer ' Also, by specifying the CharSet as ANSI, we are informing the
Dim dwMajorVersion As Integer ' compiler that we want it to treat ALL string declarations in
Dim dwMinorVersion As Integer ' this structure or class as 8-bit ANSI strings. If you need to
Dim dwBuildNumber As Integer ' mix string types, this will be addressed later.
Dim dwPlatformId As Integer 'dwPlatformId: Win32_311=0, Win32_9X=1, Win32_Win=2, Win32_CE=3.
<VBFixedString(128)> Dim szCSDVersion As String 'Fixed maintenance string (128 8-bit ANSI characters).
End Structure 'NOTE: The above Charset marshaling command informs the compiler of the default format of structure strings.

NOTE: The VBFixedString parameter value is a byte count, not a character count. If a Pinvoke requires a structure
with 8-bit ANSI strings, be sure to match the byte count to the number of 8-bit ANSI characters in its string. Conversely,
if the Pinvoke requires 16-bit Unicode strings, as it is in most-all native VB.NET applications, we should double its
character count to provide for 16-bit Unicode strings, as we did previously by replacing the 128 character count with a
256 byte count. Do not get nervous! With this book, one could very well understand Strings better than most VB gurus!
The VBFixedString documentation states: “The VBFixedStringAttribute is informational and cannot be used to convert a
variable length string to a fixed string. The purpose of this attribute is to modify how strings in structures and non-local variables are
used by methods or API calls that recognize the VBFixedString Attribute. Keep in mind that this attribute does not change the actual
length of the string itself. The VBFixedString Attribute specifies the length of a string in bytes, not characters.”
 Page 46

What this geek speak means is that VBFixedString is 1) Informational, actually being formatting
instructions to the compiler, and 2) It does not allocate Heap space. Developers might wonder if they
must therefore also declare a separate string object when we define such interop structures or classes?
The answer is No, except when we will also need to pass data to a method meant to receive it, rather than
it just providing us with data. We will address pre-initializing member data in numbered points below.
The phrase “does not change the actual length of the string” regards the null space set aside within the
structure's object, where a null anywhere within a string's space defines its length. .NET keeps a Field Offset
Table that tracks the start offset of each field (we will modify this table on page 265 to create E-Z Union
structures), and so the next field offset will be indexed beyond the space where the string is to be located. It
would have been better to clarify the documentation by stating that it will be calculated within a structure's
Field Offset Table, and actual space is not allocated on the Heap but is allocated as actual uninitialized space
within the structure's object. VB.NET has all it needs to safely resolve all these things, making additional
allocation unnecessary in most cases, allowing us to process it just as it was done under VB6.
If we are providing data to an interop Pinvoke, or just want to initialize that space. what follows are four
solutions (refer also to Cloning Structures Containing Object References Made E-Z on page 224):
1. The quickest solution is to assign a structure using the New keyword, such as “Dim Struct1 As New mySTRUCT”.
Although a structure is not a Concrete Class (no Heap space is allocated), it is an Abstracted Class, which acts just
like scalar variables (after all, scalar variables are structures). But, like concrete classes, it also features a non-
parameterized New constructor, albeit pre-defined. This constructor forces all scalar members (numeric fields) to
initialize to zero, and all objects, like strings, to Nothing. It also supports us writing embedded methods, properties,
and parameterized constructors for it! We could go at that like a moth to a lightbulb. This will also be demonstrated!
NOTE: Structures are assigned, not instantiated, so its members are not auto-initialized like classes, The non-
parameterized New constructor can be used to initialize them by invoking the same method classes use by default!
2. The next solution is to assign the structure to a variable and then initialize its members. For example, were string
variable szText a member of a structure named mySTRUCT that specifies it contains 8-bit ANSI strings (see its
declaration heading below point 4), then we could perform our declaration and initialization in one or two lines:
Dim Struct1 As New mySTRUCT 'Declare structure variable. Use AS NEW to init members to default values.
' VB.NET uses the structure's Field Offset Table to null all Field space.
'NOTE: This line NOT needed if the string is EMBEDDED, not an object var (see # 4), so As New will have cleared it!
Struct1.szText = New String(Chr(0), 128) 'Init new blank buffer (16-bit Chars-->8-bit ANSI due to struct marshaling).

3. Alternatively, if we want to initialize multiple object members after assigning a reference variable to our structure,
we should consider writing a method that we would be able to pass the structure variable we just declared to, and
initialize it that way. Pass it the structure ByRef so that the members of the structure can actually be accessed and
modified. Otherwise, if the structure is passed ByVal, only a copy of the structure will be passed, due to it being an
Abstract Class, thus leaving the original structure unchanged. Consider the following subroutine:
'initialize fixed strings in mySTRUCT structure after the mySTRUCT reference is assigned.
Private Sub InitmySTRUCT(ByRef stVar As mySTRUCT)
With stVar 'You can also instantiate class objects or LSet string text.
.szText = Space(128) 'The structure is declared AS NEW to init other members so the
'place other initializations here... ' compiler will not nag us about using uninitialized space.
End With 'Although it appears we are defining a Unicode string of 256 bytes,
End Sub ' our structure marshal tag tells VB.NET to STORE it as 8-bit ANSI.

NOTE: We might assume passing the Structure ByVal would still alter the string, being an Object type, but we would be wrong!
Declaring Strings using marshaling, like VbFixedString, embeds the string in the structure object, just like it does scalar fields!
4. Lastly, we should consider a solution that merges the above idea with the one Microsoft recommends when the
VBFixedArray attribute is used in structures (discussed shortly), and that is to take advantage of the fact that a
VB.NET Structure, in OOP terms, is an Abstract Class. An Abstract Class is technically a transient form of a class.
This is significant, because as a class type, it can contain methods and properties in addition to data fields.
What all of this boils down to is that after all our member variables are declared in the structure; we
can also adjoin structure-embedded methods and properties. I typically name all embedded setup
helper methods as Initialize, for application-wide consistency. Consider the following example:
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi)>
Public Structure mySTRUCT 'A'vast ye swabs! Thar be secret treasure lurkin' 'twixt me riggin', matey!
Dim dwInfoSize As Integer 'This scalar field is embedded within the structure object on the Stack.
Dim dwEntryId As Integer 'This scalar field is embedded within the structure object on the Stack.
<VBFixedString(128)> Dim szText As String 'Enbed this Fixed ANSI String of 128 bytes within the structure object.
'The above String is NOT a Heap-stored object! IT IS ACTUALLY EMBEDDED WITHIN THE STACK-STORED STRUCTURE OBJECT!
 Page 47

' Pre-initialize the 'fixed-length' string to the required filler space.


Public Sub Initialize() 'try changing this to: Public Sub New(ByVal InitStr As String) 'define data upon create.
szText = Space(128) ' and change this to : szText = LSet(InitStr, 128) 'fill full string space.
'Or, add a New method with a DUMMY parameter to init a structure to defaults in 1 line:
End Sub ' Public Sub New(ByVal Init As Boolean), and invoke it using: Dim X As New mySTRUCT(True)
End Structure ' Or try adding ALL these methods... There are ENDLESS possibilities!

With the above, we can declare and initialize a new copy of the structure with these two lines:
Dim Struct1 As New mySTRUCT 'The New keyword also makes sure that its data members are in default null states,
Struct1.Initialize() ' meaning scalars are set to 0, and all else, such as objects, are set to Nothing.

If we are new to OOP, do not panic if we need to pass this structure to a Pinvoke. When a Structure is
passed to a Pinvoke (or to one’s own methods), the Pinvoke process will pass a pointer to its data in the
same old way it had done so reliably under VB6. Further, internally, just like classes, data and program
code are also stored in entirely separate system memory locations, so there is no chance of this program
code being included as a part of the structure’s data block or size. Though this is obvious to experienced
developers, I am always being asked this question by those new to computer programming.
To set minds at ease, as an aside, if we were to take a look at the length of the structure declared in
the above solution, such as by using “Len(Struct1).ToString”, we will find that the returned length
would report 136 bytes. This is the size of 2 Integer variables (32-bit/4-byte), allocating 8 bytes, plus
the length of the fixed-length ANSI string, which is 128 bytes long, yielding a total of 136 bytes.
Notice I stated the string length was 128 bytes. This is important to keep in mind, because many
Win32 Pinvokes use 8-bit ANSI characters instead of VB’s 16-bit Unicode characters. Even more,
the GetVersionEx Pinvoke documentation that inspired these examples was the GetVersionExA
(ANSI) version (as declared with the trailing “A” in its Alias modifier). Further, if we re-read the
previously-stated documentation, the very last line states: “ The VBFixedStringAttribute specifies the length
of a string in bytes, not characters. ” Had I been using the Unicode (Wide) version of the Kernel32.DLL
function, GetVersionExW, I would have had to have assigned 256 as the designated byte length of
the VBFixedString attribute in order to fully accommodate 128 16-bit Unicode characters.
Even though the Space method in fact returns a string of 128 Unicode Chars (256 bytes), here is a
secret – First, sized strings are actually embedded within a structure's object data space, not as
separate objects, here as 8-bit ANSI (or any other data type, to include Unicode). Second, the
CharSet parameter also instructs the compiler to automatically provide effortless intermediate
conversion between another data type and the structure-declared type, if it is needed, “behind the
green curtain” whenever we copy data to or from it, eliminating any need for us to think about it.
NOTE: The most confusing thing about this is poor documentation, which was divined by committee by non-engineer
types (adding another layer of confusion) from what was “engineer speak” (Geek Speak), a language harder to learn
for English-speakers than Russian or Chinese, unless we are absolutely fascinated by the myriad confusion of countless
conjugated persons, tenses, tones, context, etc. Basically, it sounds like they are talking around a Hot Pocket and a fistful
of M&Ms in their mouths. The problem is that most engineers assume everyone around them already understands their
jargon, because, outside of a comic book store, they only associate with other engineers, so they come off sounding like
Sheldon in the TV series, “Big Bang Theory”. BTW, its original premise was inspired by computer geeks, not physicists.
The trick to getting past the confusion is to think about how we want the strings in our structure or
class to be handled. If they are all going to be handled exactly the same way, as 16-bit Unicode or 8-
bit ANSI, then it can be simplified by our being able to specify that as the CharSet parameter in the
marshaling instruction we add to the layout header tag, which we should look at as specific
instructions to the compiler, not to the Visual Basic language itself. By default, it will be handled as
16-bit Unicode unless we choose otherwise, but it never hurts to document it in our source code.
If we specify Auto, which I often suggest we do, then we should also be aware of what hoops the
compiler has to jump through to tango with our data, which can result in extra “behind the scenes”
bit-fiddling, at a cost to a little slower operation. Although using Auto is hands down the least
confusing of the lot, Power Users will want to optimize declarations to minimize back and forth
translation. Following is a breakdown of what can be happening to our string data during Interop.
 Page 48

1. By default, VB.NET uses VB-native 16-bit Unicode strings. The compiler endeavors to keep them in that 16-bit
format, unless we throw obstacles in its path, like a lunchroom bully, to force handling of different string types.
2. If a Unicode string is sent to a Pinvoke that is declared with the Ansi verb, behind the scenes it will convert it to
a new temporary ANSI string (see the note on the VB6 instruction “StrConv” on page 163 to see how we can
do this in VB.NET), passes it to the Pinvoke, and then converts the returned ANSI string back to Unicode.
3. When we pass a Unicode string to a Pinvoke declared using the Auto verb, most times the Unicode version of
the Pinvoke will be used, unless the Pinvoke handles only ANSI text, whereupon it will be converted to ANSI if
the source is not already ANSI, and then converts it back to Unicode upon return, or if our source string is
already stored in ANSI format, whereupon it will try to use the ANSI version, unless... (see the next 3 points).
4. Be careful of Pinvokes that do not process by data type. RtlMoveMemory is like this (internally it expects data
to be Byte()). If we specify type Auto and give it a string, it does not know what to convert it to and so by default
passes it unconverted. See below for why some uses of RtlMoveMemory seem not to work because of this.
5. If we declare a Pinvoke that includes the Alias verb to specify the actual method in the DLL to invoke, it will
convert, if necessary, the string to the appropriate format for transport, and then back, if needed, upon return.
6. If our structure or class is marshaled as ANSI and we invoke a Pinvoke that uses Unicode strings, it will convert
the structure/class-resident ANSI string to Unicode, pass it to the Pinvoke, and then convert the returned string
back to ANSI for storage in the structure/class object's string field.
Notice further the generic “<VBFixedString(xxx)>” marshaling tag can also specify the specific type it is
to be stored as, no matter the marshaling format declared in the structure or class header, such as
“<MarshalAs(UnmanagedType.LPWStr, SizeConst:=xxx)> ” (see the example below), which is the VB#native
Unicode 16-bit wide string format, or we can specify another type we need, such as LPStr for ANSI, or
even the VB6-beloved ActiveX BStr format. We will cover them in more detail much later on page
199 when we deal with passing “non-normal” COM and WIN32 string types to Pinvokes.
The single-most confusing Pinvoke programmers use in VB.NET is RtlMoveMemory, though it is
much more famously known as simply CopyMemory. This wolf in sheep drawers is declared here:
Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (
<MarshalAs(UnmanagedType.AsAny)> ByVal Destination As Object,
<MarshalAs(UnmanagedType.AsAny)> ByVal Source As Object,
ByVal Length As Integer) 'I HATE the dangerous AS ANY operator with a PASSION, but this example is quick and dirty.

It is often declared “Public Declare Auto Sub CopyMemory...” because users might pass it strings. If we
use Auto, the compiler gets confused how to convert data. This is because the function does not
declare data types in its internal reference table, so it assumes Byte(). Auto, however, forces it to
depend on the reference table, where it only finds an IntPtr data type, so it ends up doing no string
conversion! We can use a specific type, like Ansi, to make it work, but it is safer just not using Auto.
Data is stored in memory in left-to-right order, from its lowest binary bit rightward to its highest. In
the text “Ace”, the 16-bit Unicode letter “A”, written in hexadecimal as &H0041, actually stores its
lower &H41 byte before its higher &H00. Parsing this from a binary stream in 8-bit ANSI, not Unicode,
this means “A” is followed by a null terminator. Thus, the code processes only the first character in
the Pinvoke! Besides, a rule of thumb is that when we declare Pinvokes including an Alias verb, we
should never use the generic Auto string-marshaling tag, because this only confuses the compiler!
NOTE: For more information on the differences between string handling by VB6 and VB.NET, see “ String
Format Changes Between VB6 and VB.NET” on page 197. To see the wide variety of string handling marshaling
instructions to the compiler, which can be important if we are working with various WIN32 and COM string
formats, or mixed formats, see Adapting Win32 String Data Types for VB.NET Interop on page 199.
NOTE: More RtlMoveMemory issues crop up if we try to copy data to or from a class object. .NET prevents this due to
CLR Managed Memory controls, but we can use .NET's StrucuretToPtr or PtrToStructure methods to do this instead.
This material appeared complicated the first time I tried using it, but once I researched it, it became obvious.
My own personal recommendation is to only apply special compiler attributes to the body of structures when
I will be performing Win32 interop processes with them, which is to say that I will be invoking methods
declared in unmanaged or non-CLR space. Also, I prepend any member strings that must be of a fixed size
with “<VBFixedString(xxx)>”, specifying the number of bytes the string will occupy, or doubling it if I am using
Unicode, and I typically declare structures using the New verb so I will always know that all of its data fields
are being pre-initialized to Null (classes do this automatically simply because we use the New keyword).
 Page 49

If we declare fixed dynamic arrays in a structure, though VB.NET does not allow declaring pre-
initialized dimensioned arrays, we can apply a compiler directive like “ <VBFixedArray(1023)> Dim I()
As Integer”, where the VBFixedArray parameter specifies the index of the array's upper bounds.

However, in the above cases, after assigning the structure to a reference variable, we need to also
programmatically dimension its array fields because the bracketed compiler instructions cannot, and
should not do that for us – it just informs the compiler how much object pointer space to set aside for
the embedded array table. The compiler cannot automatically pre-dimension it before the structure is
instantiated. The VBFixedArray docs state “The VBFixedArray Attribute is informational and does
not allocate any storage. The purpose of this attribute is to modify how arrays in structures and non-
local variables are used by methods or API calls that recognize the VBFixedArray Attribute. Keep
in mind that this attribute does not convert a variable length array to a fixed array and that we must
still allocate array storage using Dim or ReDim statements.” Remember, structures are assigned, not
instantiated. Unlike classes, structures do not auto-initialize. Further, VBFixedArray is not part of the
VB language but a compiler directive. As with VBFixedString, VBFixedArray does not allocate its
array pointer tables on the Heap, but it does embed them within the structure or class object.
To show what all this blather really means, consider a structure supporting the above guidelines:
<StructLayout(LayoutKind.Sequential)>
Structure BOXSTRUCT 'Note: DIM handles variables as PUBLIC in Structure, Enumeration and Class object data.
Dim boxHeight As Integer ' Within Class and Structure bodies, outside Methods, DIM is handled as FRIEND.
Dim boxWidth As Integer ' Within Methods, DIM variables are handled as PRIVATE. This all due to archaic design.
<VBFixedArray(5)> Dim boxFaceImgIdx() As Short 'set aside space for 6 (5+1) short integers.
'-----------------------
'safe, in-structure method used to handle initializing embedded objects and object references.
Public Sub Initialize()
ReDim boxFaceImgIdx(5) 'note that the value in this parameter MUST match the VBFixedArray parameter, above.
End Sub
End Structure

NOTE: For those still concerned adding methods to structures might affect its overall size, be at ease. Structures are
treated just like standard classes in that they separately store the program code from data. The structure/class program
code is stored in common application program code space, and abstract class data is stored in the program Stack space
(data for concrete classes are also stored separately, but in protected Heap memory). As such, the length of the structure
will not report any change in length at all if we add a method, or any number of methods or properties to it. For a
variable assigned to the above structure, say myBox, the result of the expression “ Len(myBox)”, will yield 20, which is two
32-bit integers, plus 5+1 16-bit integers, or 8 + 12, or 20.
We can then realize and easily initialize a copy of the above structure this way:
Dim myBox As New BOXSTRUCT 'note that using NEW just initializes all members of a Structure to their default null values.
myBox.Initialize() 'initialize sizing of the yet-undimensioned boxName array using the Initialize method.

NOTE: If we require multiple dimensions, such as 32x32, adjust the pertinent lines in the above structure to:
<VBFixedArray(32,32)> Dim boxName(,) As Short 'space for 33 rows of 33 (32+1) – note the Ranking (the comma) in boxName(,).
...
ReDim boxName(32,32) 'note that these parameters MUST match the VBFixedArray parameters, above.

NOTE: We can specify multiple dimensions, like “ <VBFixedArray(1023, 63)> Dim I(,) As Integer ”. We must also
match the VBFixedArray values in the ReDim statement. Note the dimension Rank must be included in the declaration
variable (the commas). For example, a 3-dimensional array like “ Dim I(20, 40, 128) As Integer” would be declared
as “<VBFixedArray(20, 40, 128)> Dim I(,,) As Integer ”. Notice here that the rank for “I()” is changed to 2 commas,
which indicates 3 dimensions. Notice finally we must use ReDim because the array is initially a declared but blank array.
NOTE: Typically, when passing strings directly to Pinvokes as parameters, if our Pinvoke is expecting 8-bit ANSI strings, we should
also be sure to include the ‘Ansi’ verb directly after the ‘ Declare’ instruction in a Pinvoke declaration, because VB.NET strings are
defined as 16-bit Unicode character arrays. This verb will force automatic type conversion to 8-bit ANSI arrays, if required. A great
many Pinvokes, especially those of 8-bit and 16-bit vintage, typically expected those strings to be 8-bit ANSI. This will be more
thoroughly demonstrated later. A more in-depth look at Win32 Pinvoke declarations will also be covered within this document.
NOTE: In most cases we should consider including the ‘ Ansi’ verb in Win32 Pinvoke declarations when strings are used
in a Pinvoke that assumes 8-bit ANSI characters, except, as will be later covered, when the Pinvoke uses 16-bit Unicode
strings (no conversion needed) or if we specify an explicit method Alias. When an alias is used in a Pinvoke declaration,
we are informing the system of the specific DLL method to invoke, such as an ANSI method tagged with an “A”, like
“SendMessageA”. Regardless, VB.NET will extract any information it needs to validate our declaration from the DLL's
internal tables during compilation and will automatically adjust data types to suit our specifications. So, we will only
 Page 50

confuse it if we simultaneously include a conversion verb in a declaration that also includes the Alias verb. And that, in
actual practice, is not as confusing as it sounds. I repeat this ad nauseum because I am asked about it all the time.
Private Declare Function GetTempPath Lib "kernel32" Alias "GetTempPathA"
(ByVal nBufferLength As Integer, ByVal lpBuffer As String) As Integer 'Valid using Alias.

The following alternative declaration uses the Auto verb, but no Alias, is also valid (always fall back on Auto if we are
not sure, and trust VB.NET to figure it out for us (99.99% of the time it will reason it out correctly).
Private Declare Auto Function GetTempPath Lib "kernel32"
(ByVal nBufferLength As Integer, ByVal lpBuffer As String) As Integer 'Valid using Auto (or Unicode/ANSI).

But this declaration, specifying both Auto and Alias, is not, and will return non-recognizable text:
Private Declare Auto Function GetTempPath Lib "kernel32" Alias "GetTempPathA"
(ByVal nBufferLength As Integer,
ByVal lpBuffer As String) As Integer 'This will generate invalid text data.

NOTE: The execution of this erroneous code will not blow up or crash the computer, but our text will look as though we
had sent it only the first character of the string, and “former” friends will laugh. Also, we can more easily get the Temp
folder directory from VB.NET using “Dim Temp As String = Environment.GetEnvironmentVariable("Temp") ”.

● A Note on Passing Parameters ByVal.


VB.NET, by default, passes all parameters by value (ByVal), complying with tight OOPL guidelines.
This is in opposition to by reference (ByRef) that VB6 used on intrinsic types (see the note below).
To pass parameters ByVal means the passed variable cannot be altered by the invoked routine, but
rather a copy (a clone) of it is passed, which can be altered without changing the original. If we wish
to alter the passed variable, then pass it ByRef. In the spirit of code documentation best practices, it is
strongly suggested that we explicitly reference each parameter ByVal or ByRef. This makes it easier
for someone else stuck with reviewing our code to get a clearer understanding of its intent, and can
significantly help later in tracking down any bugs. For example, if a routine is supposed to modify
an integer variable, but we see that the variable is passed in ByVal, it would be obvious why the
variable is not changing when the method returns because we might know that the routine is in fact
altering data. Finally, keeping public fields (variables) to a minimum and keeping data protected as
much as possible enhances the robustness of code and minimizes the risk of corrupting data.
NOTE: VB6 used ByRef ONLY for Intrinsic types, such as numeric scalars and strings, but it actually defaulted to
ByVal for non-intrinsics. The VB6 documentation states, “By default, Visual Basic 6.0 uses the ByRef method when the
parameter’s data type is an intrinsic one such as Integer, Long, Boolean, String, and so on. When a parameter uses a
non-intrinsic data type, Visual Basic 6.0 passes it by value by default.” It amazes me so many people have missed this all
these years, though I should not be surprised, because amateur coders almost famously do not read documentation.
VB.NET passing Strings ByVal as parameters to methods as Immutable strings in native VB.NET
code is non-compliant to strict OOPL guidelines, but it is an allowed implementation extension
(under OOPL rules, passing a copy of a reference would still point to the same data), This is useful
to prevent encountering endless VB.NET bugs (amid endless calls to Tech Support). After all, this
was also the source of endless VB6 bug reports, trumped only by default properties being improperly
resolved due to ambiguous use within expressions. Under VB.NET, passing data ByRef passes the
address of the data to the method, and passing data ByVal passed the address of a copy of the data.
For scalar types, such as numeric variables and Structures/UDTs, passing them ByVal passed an
immutable copy as a parameter to the method in that the copy is altered by the method if it
manipulated the parameter in any way, though the original was left untouched because it was not
passed to the method. Upon return, the possibly altered copy of the value is simply discarded.
For OOPL object references, like class instances, Strings, Arrays, etc, passing them ByVal passes a
copy of the data reference, where the reference copy still points to the same object data. Again,
VB.NET instead passes clones of string data to native methods as an allowed language extension.
For VB.NET, Microsoft took the OOPL-compliant path and passed a copy of reference variables
when passed ByVal, but Strings were passed throw-away clones, as if they were scalar. For passing
objects as parameters to Interop methods, they did the same thing. This was the correct step to ensure
 Page 51

data safety and operational consistency. But, passing Interop strings ByVal as immutable had totally
confused VB6 coders, because sending VB6 strings ByVal to interop had been their “trick” to pass
them as mutable, taking advantage of a little-known VB6 feature that passed methods non-intrinsic
objects ByVal by default, and Microsoft extended this to pass interop mutable ByVal strings.
Passing ByVal Strings to interop as immutable would have actually been acceptable if Microsoft had
also allowed passing mutable strings ByRef to interop methods, just as they did for native methods,
but they managed to totally screw the pooch by also passing immutable ByRef strings during interop.
Some bonehead at Microsoft limited their thinking only to managed code safety, but not of the long
range consequences of doing so, especially if we peek at the inner workings of the CLR, which
incessantly invokes interop methods and passes mutable string references back and forth constantly.
Even when Microsoft addressed the ByRef/ByVal issue, they stepped into a fresh steaming pile that had
ejected from the south side of a northbound bull. First, they again allowed mutable Strings to be passed
ByVal to interop, just to lower the volume on the wailing and gnashing of teeth of VB6 crybabies over
having lost this much-beloved duct-taped hack (being a duct-tape hack, I would not have reinstated this
cheat, even though technically following OOP rules, it actually violates its own VB.NET implementation-
defined extension to the rule). Second, they screwed the poor pooch yet again by requiring an added
vbByRefStr Marshaling command so to pass mutable strings ByRef, which did nothing but throw a large
speed bump in the way of its RAD operation, adding a burdensome sand trap in front of user goals,
instead of passing strings ByRef to Interop as it allowed within native VB.NET code. Granted, they were
thinking of data safety and keeping the managed environment of the .NET Framework protected, but
clearly someone there in a decision-making capacity lacked the foresight to see the consequences of such
a decision down the road. After all, if data safety was of such paramount concern, why did they allow
Unsafe Blocks and Unsafe Pointers in C#, let alone reintroduce the potentially dangerous As Any duct-
tape hack from VB6 that had only been added because VB6 was not capable of Method Overloading?
Someone there was clearly dipping a little too much into the "medical" (read this as recreational)
marijuana. Their best solution would have been to drop the vbByRefStr Marshaling requirement early on,
along with the Interop ByVal String hack in a service pack to VB2005, amid profuse apologies. and
simply accept the vbByRefStr tag if users still employed it. However, now that so many years have
passed, they can no longer try to align Interop ByVal Strings with native VB.NET ByVal String handling.
Microsoft's best bet now is to allow both unhindered Interop ByRef and ByVal mutable string
passing. After all, this strategy would actually be in total compliance to OOPL specifications.

● A Note on Returning Value Data via Return.


VB.NET functions are able to return results more directly than was offered by VB6. Under VB6, we
had to assign a result to the function name and then use the Exit Function command if it were not at
the function’s end, or use Exit Sub from a subroutine. Under VB.NET, we can still do it that way, or
we can use the Return statement to both specify a return expression and also to return immediately
from the function or subroutine. For example, “Return iVal * 2” to return a value of twice the value
of iVal. Default return values, if Return is not used, are still allowed, though it is considered a “Best
Practice” to always ensure that all return avenues are clearly addressed. Value functions return a
default value of zero, and types String and Object functions return an object set to Nothing. Under
VB.NET, the following two function definitions will provide identical results.
Old VB6 Way (this technique is still supported by VB.NET):
Public Function GetReciprocalValue(ByVal Reciprocal As Double) As Double
GetReciprocalValue = 0.0# 'init default (optional, but recommended).
If Reciprocal <> 0.0# Then
GetReciprocalValue = 1.0# / Reciprocal 'assign result to the method name (Idea borrowed from FORTRAN).
End If
End Function

New VB.NET Way:


Public Function GetRecipricalValue(ByVal Reciprocal As Double) As Double
If Reciprocal <> 0.0# Then
 Page 52

Return 1.0# / Reciprocal 'directly return the result (Idea borrowed from OOP and C++).
End If
Return 0.0# 'return default (optional if Option Strict is Off, though recommended).
End Function 'Note that a default value will still be returned, 0.0# in this case, as it was under VB6.

NOTE: Because we should always ensure that all return avenues are covered (protected), and even though default
return values will be set, just like in VB6, it is a best practice to always specify a return value if the test fails.

● A Note on Multi-Variable Declarations.


VB.NET can declare multiple variables with a single type. Such as “ Dim X, Y, Z As Integer” to set
X, Y, and Z to Integer. With VB6, we must separately declare like-typed variables, even on one line,
like “Dim X As Integer, Y As Integer, Z As Integer ”, but using “Dim X, Y, Z As Integer” declares X
and Y as Variant, unless an explicit Defxxx declares a default type. During VB6 to VB.NET upgrades,
the X and Y were assigned type Object, unless there was an explicit Defxxx. We cannot expect VB6
code that just looks like VB.NET syntax to be left untranslated during an upgrade, ignoring the VB6
rules the upgrade must fully respect in order to make any sense of a translation.
NOTE: Specifically, under VB6, unspecified variable type declarations were normally expected to be defined to the
now-defunct DefInt, DefSng, etc declarations, or to type Variant if these default types were unspecified.

● Notes on Collection Data Now Being Objects, Not Just Strings.


Collections, ComboBoxes, and ListBoxes store objects as data instead of the text strings that were
the only type used by VB6. This change makes collections much more powerful, even though we can
still continue to use just text strings as data if we want to, which are objects, after all. Therefore, a
collection under VB.NET now must utilize the object’s ToString method in order to obtain the text
data that was, under VB6, provided by default to the collection by its text member; the data that
would be visibly displayed within a ComboBox or ListBox, as well as the data that would be used
during comparisons, such as when we set the Sort property of these collection objects to True.
SECRET NOTE: Obviously, we can also use scalars, which are Abstract Classes. Internally, VB.NET will wrap each of
them in an object shell and unwrap them when they are released – an Object-Oriented process called Boxing. This
boxing process is transparent to us and its design is very simple, but be aware it has a minuscule cost in time, but which
can add up for considerably substantial member counts. A Box object is actually nothing more than a simple IntPtr
wrapped in a tiny class that provides management of its abstract class object via Property indirection. This also explains
why using structures normally does not always work with sorts, unless we design them to access a value directly, like
scalars. We can also write a boxing class to handle our Structures, providing a ToString method at the very least.
Keep in mind that for some objects, specifically custom classes, their default ToString method, when
not explicitly declared, will simply return the class name of the object, which is typically useless. So,
depending on the class we use, we may need to override its default ToString method, declared at the
very least within its base class, thus providing our own ToString method that will supply the
invoker, such as a ListBox, with the meaningful text such as had been provided to it under VB6 with
strings. This is an awfully diminutive price to pay for tremendously more powerful functionality.
To override the ToString method of a base class (even a new, empty VB.NET class we write inherits
the base Object class, which includes numerous methods, such as ToString), we must include a
method declared as: “Public Overrides Function ToString() As String”.
Consider the following sample class designed to be a minimal data storage class for collections:
'----------------------------------------------------------------------------------------
' SIMPLE COLLECTION CLASS OBJECT FEATURING A TEXT STRING AND AN INTEGER KEY FIELD.
' This can be used as a storage class in general Collections, ListBoxes, and ComboBoxes.
'----------------------------------------------------------------------------------------
' Add an entry like this: Me.ListBox1.Items.Add(New clsData("Harry Potter", 132))
' Read Index like this : With Me.ListBox1.Items
' Me.Label1.Text = "Current Index = " & DirectCast(.Item(.Count - 1), clsData).Index.ToString
' End With
' NOTE: A ListBox/ComboBox will invoke our ToString method. (Try disabling the ToString() method and see what happens.)
'
'-------------------------------------------------------
Public Class clsData
' Declare Field members (Fields are declared like variables, but Fields are persistent, like VB6 Static Variables).
 Page 53

Public Text As String 'default text of entry (Public so it can be directly edited by outside code).
'NOTE: We could also have specified a reference to a Structure here.
Private mKey As Integer 'Key value (Private so it is protected from direct access from the outside).
' Additional special private index that is shared by ALL clsData instances follows.
' The following declaration was added to this project simply as an example of a user-defined added feature:
Protected Shared MstrIndex As Integer = 0 'pre-init to zero by first instantiation (and shared by ALL instantiations).
' 'NOTE: VB.NET Shared = VB6 Static.
'-------------------------------------------------------
' Access readonly Key value (this, and the above integer, could also be changed to a string, if you require a text key)..
Public ReadOnly Property Key() As Integer
Get
Return mKey
End Get
End Property
'-------------------------------------------------------
' Access readonly accumulating Index value.
' This will return the number of clsData objects that have so far been instantiated.
Public ReadOnly Property Index() As Integer
Get
Return MstrIndex
End Get
End Property
'-------------------------------------------------------
' Allow New object with no data provided
Public Sub New() 'See notes below to see how to not need to declare this.
Me.Text = String.Empty 'initialize blank data to "".
Me.mKey = 0 'initialize null key.
MstrIndex += 1 'increment shared master index.
End Sub
'-------------------------------------------------------
' Instantiate a New object with just the text property specified (function overload).
Public Sub New(ByVal Txt As String) 'See notes below to see how to not need to declare this.
Me.Text = Txt 'save text data.
Me.mKey = 0 'initialize null key.
MstrIndex += 1 'increment shared master index.
End Sub
'-------------------------------------------------------
' Instantiate a New object with both a text property and a key property (function overload).
Public Sub New(ByVal Txt As String, ByVal Key As Integer) 'You could declare Key as Optional ByVal Key As Integer = 0.
Me.Text = Txt 'save text data. ' This way you would not need to declare the New(Txt) method.
Me.mKey = Key 'save key value. 'If you also declared Txt as Optional ByVal Txt As String = "",
MstrIndex += 1 'increment shared master index. ' you would only need to declare this single method.
End Sub
'-------------------------------------------------------
' Used by many invokers, such as ListBoxes and Collections to retrieve displayable data.
Public Overrides Function ToString() As String
Return Me.Text
End Function

End Class

NOTE: What we choose to return in our custom ToString override method does not have to be so basic as just returning
a string property value. We can render strings from numeric values, combines string values, or format the returned
string however we require for our application. For example, in returning an email address for display, we may want to
display both its list-selectable text and the raw email address, such as “<Dim Witt> dw12ax@cox.com”.
Create a new Windows Application project. Put a ListBox and a Label somewhere on the form. Add
the above class to the project. Next, add the following Form Load event code:
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
With Me.ListBox1.Items
.Add(New clsData("Harry Potter", 123))
.Add(New clsData("Sally Rae", 124))
.Add(New clsData("George Burns", 125))
.SelectedIndex = .Add(New clsData("Gracie Allen", 126)) 'ADD() returns VB6 NewIndex value.
Me.Label1.Text = "Current Index = " & DirectCast(.Item(.Count - 1), clsData).Index.ToString
End With
End Sub

Execute the code. Notice ListBox1 will automatically use the class’s ToString method
to display the text we had added for each entry, either invoking its default ToString
method, or our override method. Try disabling our overridong ToString method in the
clsData class by commenting it out and see what happens. We will instead get four
entries of something like this, “WindowsApplication1.clsData”, which is what the base
class ToString method (MyBase.ToString) will return, this result being in fact the name
of our data class object.
 Page 54

● A Note on VB.NET Default Properties Not Being Parameterless.


VB.NET supports default properties, but not with the
drunken delirium of VB6, which resulted in not just a
lot of Late-binding, but sometimes Late-binding issues.
This is mainly because VB.NET does not support default properties lacking parameters. We will be
presented with lots of error reports to this effect in upgraded VB6 code if we were prone to using them.
Parameterless default properties make code too indefinite, giving us no direct clue in many cases to what the
code’s actual intent is, as opposed to explicitly spelling out all properties and methods. Usually, the only good
place for us to define a default is in the Item property of an Items collection, where we can replace typing
MyCol.Items.Item(3) with MyCol.Items(3), which still makes perfect sense to someone stuck with having to
review or, worse, debug our code. Besides, in a Microsoft study. 52% of all errors reported by the VB6
Upgrade Wizard were due to: Could not resolve default property of object “<objectname> ”. Even so,
keep in mind that using any default property always results in slower operating code due to its values
needing to be resolved during Late-binding, because default property types cannot be solved until then.
What should be done in the following case if Text were the default property of class object Item:
Dim obj As Object = Item

Are we clear in an instant what is actually passed into “obj”? Is it the Item object or is it its Text
data, its default property, both of which “obj” can accommodate? It is the default Text property. But
then, what do we do if we actually want to pass the Item object to such a variable? Under VB6, we
would need to specify Set or Let to clarify our intent to pass data as we mean to pass it. This is yet
another extremely good reason why we should not allow parameterless properties.

● A Note on VB.NET Having Dropped the ‘Let’ Keyword.


VB.NET classes dropped the Let property. Because all VB.NET variables are abstract or concrete
classes, Let no longer has relevance, so Get and Set are all that is left. Thus, setting a local variable
to an object does not need a totally redundant Set modifier when assigning it, because that is the
only way by which the assignment can be made. Hence, the consequentially redundant Set and Get
verbs are always assumed and therefore they are never needed or required under VB.NET (if we do
type them in. The VB.NET IDE will immediately auto-redact them). The following VB6 code:
Dim MyCar As CarClass 'declare the object.
Set MyCar = New CarClass 'instantiate the object.

This could be implemented under VB.NET as the following code:


Dim MyCar As CarClass 'declare the object reference variable.
MyCar = New CarClass 'instantiate an object (If you pre-pend 'Set' to the line, VB.NET will auto-remove it).

NOTE: We can also instantiate upon declaration under VB.NET, like this: “Dim MyCar As CarClass = New CarClass ”, or
abbreviate it even further by using the short-form: “Dim MyCar As New CarClass”.

● A Note on VB6 Versus VB.NET Variable Definitions.


VB.NET variables have taken on broader definitions to both modernize the language and align itself
with current technology. Refer to the following table for converting VB6 variable types to VB.NET
(VB.NET has more types, such as unsigned integers, but VB6 does not support them).
VB6 Name VB6 Size in Bits/Bytes VB.NET Names VB.NET Size In Bits/Bytes
Byte 8 bits / 1 byte, unsigned Byte, UInt8 8 bits / 1 byte, unsigned
Integer 16 bits / 2 bytes, signed Short, Int16 16 bits / 2 bytes, signed
Long 32 bits / 4 bytes, signed Integer, Int32 32 bits / 4 bytes, signed
Single 32 bits / 4 bytes Single 32 bits / 4 bytes
Double 64 bits / 8 bytes Double 64 bits / 8 bytes
Currency 64 bits / 8 bytes Replaced by Decimal 128 bits / 16 bytes
Variant 128 bits / 16 bytes Object 32 bits / 4 bytes
 Page 55

● A Note on Edit and Continue.


VB.NET projects, as of VB2005, support Edit-and-Continue debugging; a feature dearly missed by
many former VB6 developers in earlier releases of VB.NET. This feature allows us to add/edit/delete
lines of code during an interactive debugging session, just as we could do under VB6. This is a great
feature for fixing bugs or correcting/testing variable values on the spot. Note though, that presently
Edit and Continue is only supported in 32-bit applications, but 64-bit is forthcoming.
Also be aware that there was an issue when running code in debug on a 64-bit system under
VB2012-VB2015, although Update 1 for VB2015 from Microsoft has in fact fixed it. It had to do
with a crash reporting that “Microsoft.VsHub.Server.HttpHostx64.exe has stopped working. ”
Thomas Barthelemy addressed this issue at http://thomas-barthelemy.github.io/2015/05/01/visual-studio-vshub/ in an
article named “How to fix: HttpHostx64.exe has stopped working”. He reported that "With the help
of the Microsoft Connect team we were able to locate the issue on System32/ASProxy64.dll which
belongs to the Astrill VPN software." If we have this issue, please be sure to check for updates or
else just uninstall and then reinstall Visual Studio 2015.

● A Note on Losing the VB6 BackStyle Property.


One upgrade warning that may frustrate more than a few users is reported when a control’s
Backstyle is being modified. Consider the following upgraded VB6 code:
frmMain.lblLoc.BackColor = Color.White 'set label background color to White.
'UPGRADE_ISSUE: Label property lblLoc.BackStyle is not supported at runtime. Click for more: BLAH-BLAH-BLAH.
frmMain.lblLoc.BackStyle = 1 'set label BackStyle to opaque in order to show the color White.

Under VB6, the BackStyle was by default set to 0 (Transparent). If set to 1, its background was set to
Opaque, which allowed the color set in the control’s BackColor property to be displayed. Under
VB.NET, these two properties were merged; it was such a waste of resources to have a BackColor
property that was in most cases unused. This added to an overall heavier code overhead. Therefore, the
BackColor property, when set to a color, determines the background color of the object, and logically
forced the back style to be opaque. By setting the BackColor to the new color value Color.Transparent,
the background and back style would be logically rendered transparent.
To correct the above code, we simply delete the line setting BackStyle, because by setting the
BackColor property under VB.NET to a non-Transparent color, we have in fact set it to opaque. If
we want to set it to transparent, use something like “Me.lblLoc.BackColor = Color.Transparent”,
though we would still delete the BackStyle setting line: “Me.lblLoc.BackStyle = 0”.
NOTE: Even with the BackColor set to Transparent, its background assumes the color of its parent; the form (its
default parent). To make it transparent to a control behind it, at runtime we must set the object’s Parent to the control it
is positioned in front of, and adjust its Left and Top properties to those relative to its new parent. For example, on a form
we have a label (Label1) and a PictureBox (PictureBox1). To display the label over the PictureBox with the background
transparent on the image, within the Form_Load event, enter this:
'Set Label1 over Picture1, relative to their current positioning.
With Me.Label1
.Parent = Me.PictureBox1 'set Label1 parent to PictureBox1.
.BackColor = Color.Transparent 'set Label1 background color to transparent.
.Left = .Left - .Parent.Left 'adjust Label1’s location to be relative to its new parent.
.Top = .Top - .Parent.Top
End With
 Page 56

● A Note on Recovering VarPtr, ObjPtr, StrPtr, VarPtrArray, and StrPtrArray.


If our upgraded VB6 code requires the “undocumented” VB6 functions VarPtr, ObjPtr, StrPtr,
VarPtrArray, or StrPtrArray, use these VB.NET functions to replace and duplicate them:
Imports System.Runtime.InteropServices
'******************************************************************************
' This Module Provides the addresses of Numeric Variables, Objects, and Strings.
'******************************************************************************
Module modVarPtr
'VB.NET version of VB6 VarPtr 'also works for VarPtrArray.
Public Function VarPtr(ByVal o As Object) As Integer 'use Object as a 'catch all' universal data type.
Dim GC As GCHandle = GCHandle.Alloc(o, GCHandleType.Pinned) 'get a trackable handle and pin the object address.
VarPtr = GC.AddrOfPinnedObject.ToInt32 'get (& return) the memory address of pinned object.
GC.Free() 'free the allocated space used.
End Function

'VB.NET version of VB6 ObjPtr


Public Function ObjPtr(ByVal o As Object) As Integer 'note these methods actually use VarPtr().
Return VarPtr(o) 'use VarPtr -- it will return address of the object.
End Function

'VB.NET version of VB6 StrPtr 'also works for StrPtrArray.


Public Function StrPtr(ByVal o As String) As Integer
Return VarPtr(o) 'use VarPtr -- it will return address of the string.
End Function
End Module

I am amaze that so many VB6 users absolutely demanded a full VB OOPL environment with
absolutely no sacrifice of OOPL functionality or data safety in the then-proposed VB.NET, with fist-
shaking threats of abandoning the VB platform if their demands were not fully met. Yet, when they
got exactly that, they had the gall to whine about the loss of things, like the VarPtr or Let keywords,
which did not jive at all with OOPL data safety, but threatened OOPL specifications. What did they
do? They threatened to abandon the VB platform. I have a really funny anecdote to describe such
people, but I do not think their senses of humor extend to things that strike too close to home. Me? I
love making fun of myself. I am a geek, and my jokes about geeks are merciless!

● A Note on RichTextBox Property Renaming During Upgrades.


For some dumb reason during an upgrade, when Rich Text format data is being copied from one
RichTextBox to another through their RichText property fields, the assigned property will change.
First, under VB.NET, the VB6 “TextRTF” property is simply named “Rtf”. This typically upgrades
OK, except during an assignment; a target “TextRTF” property will be incorrectly upgraded to “ Text”
rather than properly to “Rtf”. For example, consider the following original VB6 code:
With Me.rtbInfo
.TextRTF = Me.rtbSearch.TextRTF 'now copy data to help display form.
...
End With

When it is upgraded to VB.NET, this code results in:


With Me.rtbInfo
'UPGRADE_WARNING: TextRTF was upgraded to Text and has a new behavior. Click for more: BLAH-BLAH-BLAH
.Text = Me.rtbSearch.Rtf 'now copy data to help display form.
...
End With

Notice that the source “TextRTF” property has been properly upgraded to “ Rtf”, but the destination
“TextRTF” was ‘upgraded’ instead to “Text”. We will have to fix this statement by further changing
the “Text” property to correctly reflect “Rtf”. Some upgrade…
 Page 57

● Notes on Recovering the LenB Function.


A VB6 program would use LenB to obtain the actual byte length of a string or structure, usually to
insert the length of a structure into its first member (some Win32 Pinvokes require variable-sized
structures). However, during an upgrade to VB.NET, code using LenB is always flagged as not being
supported by VB.NET. Unfortunately, Microsoft’s help is a bit lacking in providing a clear resolution
to this important issue. The clarifications below should resolve this quandary.
Online “gurus” often declare that VB.NET's Len function does not support Structures (but that was
true only for Beta releases; do they not stay updated?) and bellow advice that we should use a
Marshal Class (part of the System.Runtime.InteropServices namespace), which has a SizeOf method
that will accept objects, to include Structures (Abstract Classes), and it will return its size in bytes.
At first glance, this seems to be exactly what we need!
In actual testing, however, I found that the result does not accommodate the actual allocated sizes of
fixed-string members within those structures, except as 4-byte IntPtr types (Integer Pointers), which they
technically are, but Pinvokes expect fixed-length string members to be embedded, except when we are in
fact passing a pointer to a string. Using that class, the MYSTRUCT structure described below, will return
a value of 12, which accounts for 2 Integers, plus 1 IntPtr referencing its string data ((2+1) x 4):
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi)>
Public Structure MYSTRUCT
Dim dwInfoSize As Integer '4 byte footprint.
Dim dwEntryId As Integer '4 byte footprint.
<VBFixedString(128)> Dim szText As String 'An 8-bit ANSI string with a declared footprint size of 128 bytes.

Public Sub Initialize() 'Initialize the 'fixed-length' string to the proper size.
szText = New String(Chr(0), 128) 'This method occupies no space in regard to the size of the structure.
End Sub '(Here I am beating that poor exhausted horse again. Someone call the ASPCA!)
End Structure

On the other hand, VB.NET’s Len function now fully supports Structures and will return its full
allocated size, to include the allocated lengths of any VBFixString-declared string members.
NOTE: Allocated lengths of String members in structures refers to an actual declared size. For normal string
declarations, this would be a value of 4 (for an IntPtr). But if the string is declared with a fixed string prefix, such as
“<VBFixedString(128)>”, it is assumed to be embedded in the Structure's object and this will alter the object's size.
For individual Unicode Strings, such as “Dim S As String = "abc" : Dim I As Integer = Len(S) ” we
can either double the value of the string’s length, or we can use the GetByteCount property of the
System.Text namespace’s Unicode.Encoding class.
Consider the following upgraded VB6 code:
'UPGRADE_ISSUE: LenB function is not supported. 'Click for more: BLAH-BLAH-BLAH
CopyMemory(MnMxInfo, lParam, LenB(MnMxInfo)) 'get structure byte size.
'UPGRADE_ISSUE: LenB function is not supported. 'Click for more: BLAH-BLAH-BLAH
Dim iLen As Integer = LenB(myString) 'get string byte size.

We can manually fix the above two lines by updating them to this bit of bother:
CopyMemory(MnMxInfo, lParam, Len(MnMxInfo)) 'get structure byte size.
Dim iLen As Integer = System.Text.Encoding.Unicode.GetByteCount(myString) 'get string byte size.
'Or:
Dim iLen As Integer = 2 * Len(myString) 'get string byte size.

Better, these fixes can be eliminated and the original code will become once again viable simply by adding
our own overloaded LenB functionality. In a module, just add these two functions:
Module modLenB
'********************************************************
' Provide the VB6 LenB Functionality – Type-Safe approach.
' This will handle strings separately from other types.
'********************************************************
Public Function LenB(ByVal ObjStr As String) As Integer
'Note that ObjStr.Length will fail if ObjStr was set to Nothing, so use Len(), below .
If Len(ObjStr) = 0 Then Return 0
Return System.Text.Encoding.Unicode.GetByteCount(ObjStr)
End Function
 Page 58

Public Function LenB(ByVal Obj As Object) As Integer


If Obj Is Nothing Then Return 0
Try 'Structure.
Return Len(Obj)
Catch
Try 'Type-def objects.
Return System.Runtime.InteropServices.Marshal.SizeOf(Obj)
Catch
Return -1 'Allow user to check for <0 as an error.
End Try
End Try
End Function
End Module

● A Note on Passing Parameters “As Any” to Pinvokes.


If a Pinvoke has parameters typed As Any, we would be much better off using VB.NET type-safe
overloaded methods instead. With them, we can declare more than one method with the same name,
but using different parameters. This technique simplifies coding because we do not need to be
concerned about which method to invoke; the compiler will figure that out for us. This is a feature
VB6 users had long begged loudly for from Microsoft, but which VB6, as its platform had been
implemented, was sadly unable to deliver. As it was, VB6 was forced to jump through hoops to
provide users with Win32 API interop invocation support. Yet, function overloading was asking far
too much from a narrowly limited platform design already being stretched to the very limits of its
original 16-bit DOS QuickBASIC underpinning. The fact is, the Any type was added as a
workaround, otherwise it would have never existed in the first place.
Now, it seems, many VB programmers cannot seem to survive without the Any type, which was,
purely and simply, duct-tape to emulate an otherwise inaccessible feature, though one that was
absolutely essential for pre-VB.NET platforms. Without it, many Win32 API methods would have
been off limits for use in VB applications. Such a limitation would have certainly been unacceptable,
because it would have held VB program capabilities too far back, and would have been a nail in
VB’s coffin, not to mention giving its detractors even more ammunition to fire at it.
For example, consider the following typical API that has found a lot of use in VB6:
Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long,
ByVal lParam As Any) As Long

This method was often used to send string text assigned to the lParam value to another control in
another application running on the computer, or to a different process, or to a different control. If
there was no text, then a value of zero was usually sent as lParam. Using “As Any” allowed both text
and values to be passed by the very same parameter.
Under VB6, if we wanted to break this API up to support values and strings separately without
resorting to As Any, we did it by declaring two separate APIs and giving them separate names, such
as SendMessageByNum and SendMessageByStr, and manually invoking the appropriate method,
depending on what type of data we needed to send. Consider the following VB6 examples.
Declare Function SendMessageByNum Lib "user32" Alias "SendMessageA" (
ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long,
ByVal lParam As Long) As Long

Declare Function SendMessageByStr Lib "user32" Alias "SendMessageA" (


ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long,
ByVal lParam As String) As Long

With VB.NET, we are now able to declare two separate method bodies, but provide them with the
very same name. This way, we do not have to think about which method to use because they will be
the same, though the compiler will be able to tell by examining the parameters, and it would select
the proper method for us. Consider the following VB.NET declarations, both named SendMessage.
 Page 59

Declare Function SendMessage Lib "user32" Alias "SendMessageA" (


ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer,
ByVal lParam As Integer) As Integer

Declare Function SendMessage Lib "user32" Alias "SendMessageA" (


ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer,
ByVal lParam As String) As Integer

However, if we feel we cannot live without it, or simply feel that using it will get around having to
learn how to write overloaded methods, which are actually quite simple, as demonstrated above, we
can show the world we like writing potentially system-crashing code by modifying this VB6 code:
lpKeyName As Any 'not type-safe, so do not just throw any ol' thing into this stew pot.

Upgrade it to actually work As Any under VB.NET like this:


<MarshalAs(UnmanagedType.AsAny)> ByVal lpKeyName As Object 'not type-safe, but works just like VB6 As Any.

● A Note on Common Dir Function Warnings.


If we are warned the Dir function has changed behavior, this most-often concerns the sequencing of
folder shortcuts “.” (Current) and “..” (Parent) during a directory scan. In VB6 these two folders
were always the first two entries gathered. Some coders got goofy and just skipped checking the first
two entries based on this assumption. This is no longer true, nor should it have been assumed.
To ignore them, see if the left character of the file/folder name is not “.” ( If DirEntry.Substring(0, 1)
<> "." Then). As long as we are doing this, reports of changed behavior can be safely ignored.

However, we should consider abandoning the archaic and slow Dir function altogether and resort to
using the extremely fast and much more powerful My.Computer.FileSystem or System.IO classes
and methods, which work much faster than even the File System Object declared in the ActiveX
COM reference Windows Script Host Object (also known as Windows Script Host Object Model, or
WSHOM), embodied by wshom.ocx (this OCX in turn redirects to IWshRuntimeLibrary.DLL) or
scrrun.dll, if we reference Microsoft Scripting Runtime instead.

● Notes on Fixing Item Issues and Enhancing Collections.


If we use Collection-type objects, such as Collections, ListBoxes, or ComboBoxes, we often get a
warning that a default property for Item in a Collection cannot be determined. This is because
Collection Items in VB.NET do not default to a text property as they did under VB6. The Item
property of a VB6 Collection was of type String, but under VB.NET it is now a more versatile type
Object, but with the proviso the provided Object also features a ToString method, which Strings do.
We could resolve this by ignoring the warning, because we have in fact already been assigning a
String Object to the Item property, then this will be corrected during the Late-Binding process. Yet, if
we want to avoid this supported Late-Binding bit-fiddling, which is much slower, we can greatly
speed it up by employing the collection Item object’s ToString method (for example.,
myListBox.Items.Item(Index).ToString()) to resolve it during the much faster Early-Binding stage.
SECRET NOTE: Doing Late-Binding on the Item property of a collection list is on an order of about 5 times slower
during averaged benchmark testing, and is also average for any Late-Binding process. Further, using the Default
property of a collection list, where we would use myList.Items(3) instead of myList.Items.Item(3), is also as slow,
meaning that if we are using both in Late-Binding, it will execute about 10 times slower!
If we are using an actual Collection, consider changing the collection object itself, such as into one
of the much faster System.Collections.Generic strongly-typed 0-based collection classes. If we are
working with text data, then define it as a List, or, if we also require an associated Key, try using a
Dictionary, or a SortedDictionary if we would like to auto-sort the dictionary based on the Key.
For example, we could change our VB6 declaration from something like this:
Dim myCol As New Collection
 Page 60

To something more robust and strongly typed, like this VB.NET declaration:
Dim myCol As New Collections.Generic.List(Of String)

We can of course separate the definition from the instantiation of the collection. We can also use a
structure or class instead of String as the type of object to strongly tie this collection to. Or, we can
simplify editing even more by redefining the Collection control in our app in just 3 lines of code:
' Replace 1-based Collection with 0-based string-typed collection.
' Make this OF a custom Class or Structure if you want to add more features, such as keys.
Public Class Collection
Inherits System.Collections.ObjectModel.Collection(Of String)
End Class 'or you can use the KeyedCollection or ReadOnlyCollection classes of ObjectModel.

NOTE: Because a generic collection’s Remove method will expect an object (used like a Key) to identify what object to
delete, use instead the new RemoveAt method, which will allow us to specify an index, as we did under VB6.
NOTE: Remember, the warning about an Item’s default property will probably be the most frequently encountered
warning in an upgrade if the VB6 project did much of anything with collections. Keep in mind that using the Item
object’s ToString property will be the safest, easiest solution, especially if we are instead using a ListBox or a
ComboBox, but which can also use custom classes for data instead of just strings (as it had been for VB6). However, be
mindful that these new collections are zero-based, not 1-based as the default ‘classic’ Collection. If we need it to be 1-
based, stuff a dummy entry as the first item, but remember that the Count property will always be one higher.
NOTE: If you have never tried using Inheritance because you thought the process was arcane and you feared you might
accidentally summon malevolent preternatural demons out of the dark bowels of your computer's CPU, then consider
that the above local scoping override to the Collection Class as your first E-Z step. We can also add our own methods
and properties to that tiny class! An E-Z intro can be found in Black Book Tip # 21: Demystifying Pinvoke Definitions of
VB6 POINTAPI and RECT Structures on page 508, The first time we do this on our own, our neighbors will be
awakened in the wee hours by hearing who they think might be Victor Frankenstein – “It's alive! It's ali-i-i-i-ve!!!”

● A Note on Avoiding Late-Bound Object References.


We may surmise from the last point that using Late-Binding (supposedly a bonus feature of Visual
Basic) can in turn be confusing to the Upgrade Wizard. This is because sometimes it cannot be sure
what kind of objects it is working with. As a rule, we should only take advantage of Late-Binding as
a last resort; for it can take a heavy toll in time during execution, because internally, the processing
engine must scan the object for supported functionality, which is stored internally not as fast indexes,
but as text data that must be parsed against internal tables also stored within the DLL (OCX –
ActiveX – files simply redirect us to a DLL). Once the entry is found (if it exists), then an invocation
address and a delegate (prototype) of the method must be matched to any parameters. Using Late-
Binding is very much like using slow interpreted DOS BATCH programs (to be fair, I should have
said the faster VBScript or JavaScript). It should only be used when we have absolutely no other
choice, because it is slow and cumbersome.
Consider the following simple VB6 code segment:
Dim o As Object
o = Me.Label1
o.Caption = "SomeText"

When this is upgraded to VB.NET, the wizard will not know, when processing the last line, that the
object “o” is associated with a Label control, because its scope of knowledge of its surroundings is
confined strictly to each statement as it is being upgraded. As such, even though in the second line a
Label control is assigned to “o”, the Upgrade Wizard has discarded that knowledge as it rolls up its
sleeves and clears the table to begin work on the next line. The only thing it knows for sure that is
that “o” is declared as type Object. Hence, during the upgrade, it will not upgrade Caption to the
new and now-uniform property Text in VB.NET. There is no sure way that it can assume that the
actual object stored within “o” is a Label control, because it might be a user-defined class that will in
fact continue to use a Caption property. These will have to be manually, though easily repaired.
 Page 61

● Notes on VB6 Parameterless Defaults During Upgrades.


We will quickly discover that VB.NET does not support parameterless default properties. A lot of
warnings will be issued in our upgraded code if we were one prone to using them. Indeed, 52% of
the errors reported by the VB6 Upgrade Wizard are due it being unable to resolve default properties.
The decision to not support parameterless default properties was not an easy one at Microsoft, but I
wholly agree with their decision. Parameterless default properties make code too indefinite, giving
us no direct clue in many cases to what the code’s actual intent is. Having said that, it would have
been nice if a Text property or at least the ToString function, if they exist, were to be fallen back
upon as a default, but this would not have worked seamlessly in all cases because there is an
unavoidable level of uncertainty when using parameterless default properties regarding whether one
is actually wishing to pass on an object or its default property. For example, what should be done in
the following case, if the Text property of the Item object were a default property:
Dim obj As Object = Item

Are we clear as to what is actually passed into “obj”? Is it the Item object itself, which “obj” will
accommodate, or is it its Text data, its default property, to which “obj” is equally accommodating?
Perhaps the most commonly-used default property is the Item list when referring to a member of a
Collection. Item requires an index or key property, which fully qualifies it to be declared a default
property. We can specify “Items.Item(Index)” or “Items(Index)”, but its intent is still clear.
But having said all that, keep in mind that default properties are processed late-bound, meaning that
processing them and determining them is a process-slowing, time-consuming practice. For the
fastest possible program execution, always early bind code as much as possible. Also, as is probably
made evident in the previous example, we should avoid using default properties with the Object and
Variant data types in our VB6 code, because these can be difficult to resolve during an upgrade to
VB.NET, and we will likely have to further modify the code ourself after the upgrade.

● A Note About VB6 Null Propagation.


One obscure area that may run us into trouble is in regard to Null Propagation. In VB6 and previous,
Null propagation supported the premise that wherever Null is used in an expression, the result of the
expression will itself be Null. Consider the following VB6 code:
Dim V
V = 1 + Null
V = Null + Right$("SomeText", 1)
V = Right("SomeText", 0)

Each of the above expressions yields a NULL result under VB6. However, under VB.NET, the
statement 1+Null generates a type mismatch. Also, where VB6 had two versions of the Right
function — Right$ returning a string, and Right returns a variant that could be Null — VB.NET
only has one version, Right, that always returns a string. Further, V is declared as a variant by default
under VB6 (we could also use “As Variant”). The Upgrade Wizard will change this to be cast as
type Object. Also, Null, a variant type, is not supported by VB.NET, though a database-oriented
System.DBNull is.
And speaking of databases, Null propagation is commonly used in database applications, where we
need to check if a database field resolves to Null. In these cases we should check results against
System.DBNull, or by using the function IsNull and performing the appropriate action based upon
the result of the test, because Null propagation is still supported in Databases.
 Page 62

● A Note on Referencing Objects Before They Are Initialized.


Sometimes we may receive a warning that a String variable or a structure field was being altered
before it had been initialized. Unlike normal scalar variables, strings and structure fields are not
auto-initialized to Nothing or zero, and so start up in an undefined state. However, these warnings
are very easy to address. One choice is to ignore the warning; the code will still work properly,
though we should be aware that this will also force it to be resolved during a much slower Late-
Binding process. The best choice is to change a variable’s declaration so that it is defined as having
an initial value. For example, if a string variable is in question and it is declared as something like
Dim S As String, then change it to Dim S As String = Nothing, Dim S As String = "", or Dim S
As String = String.Empty . If the variable is numeric, declare it with an initial value of zero.
However, if we are told a structure or one of its members is being updated before it has been
initialized, simply change the declaration of the structure to be as New, such as Dim newStruct As
New myStruct. For structures, this forces initialization of its entire object space to be filled with
nulls, effectively setting all its numeric field members to zero and its object fields to Nothing.

● A Short Note on Dealing with Renamed Properties.


Often we will find that form controls such as Listboxes and ComboBoxes now have renamed
members in VB.NET from what they were in VB6. Most of the time it is because we may have
accessed the object’s “Hwnd” (Windows Handle) property. For reasons of cross-language uniformity,
VB.NET changed all Window Handle properties to “Handle”. Simply edit the properties from hwnd
to Handle and they will work correctly. Why do upgrades not automatically fix this?

● A Note on Finding the ListCount Property.


If form controls contain lists of any sort, then they likely implement Collections.IList. As such,
upgraded VB6 properties that generate warning errors such as an unsupported ListCount property
can be fixed by addressing the Items collection within the control (all collection-type lists now use
the Items collection, and all with a Default Property of Item – there is no longer a List collection or
ListItem property). For example, if a ListBox control named LstBox used the now-unrecognized
ListCount property, edit it to instead use LstBox.Items.Count. Why do upgrades not fix this?
NOTE: See "Notes on E-Z Recovery of the ItemData List Object Property" on page 77 to see how to easily add our
own Custom ListItem class with an ItemData property to VB.NET Collections, ListBoxes, and ComboBoxes.

● Notes on Changes in the Mouse Cursor Property.


We may encounter warnings that the Screen.MousePointer property has a new behavior. It has been
my experience that in almost all cases we can safely ignore them, except in situations where we will
be saving/loading the current cursor. Under VB6, this was typically to an integer value. This is now a
Cursor object. For example, the following upgraded VB6 code:
Dim OldPointer As Integer
'UPGRADE_WARNING: Screen property Screen.MousePointer has a new behavior. Click for more: BLAH-BLAH-BLAH
OldPointer = System.Windows.Forms.Cursor.Current 'this generates a type-mismatch error if it is not fixed.

This can be corrected to (note that in a form, the System.Windows.Forms path is already loaded):
Dim OldPointer As Cursor 'Fix it by changing the old VB6 Integer into a Cursor object reference variable.
OldPointer = Cursor.Current

Setting the Mouse Cursor has changed a bit, but I think for the better; it has been compacted and is
now much easier to use. For the most part, it has been upgraded to use a more convenient
enumerator, Cursors (technically, System.Windows.Forms.Cursors), from which the old VB6 standbys
can be set, such as Cursors.Default, Cursors.Arrow, Cursors.Cross, and so on. Note further that a
new cursor, Cursors.AppStarting, being the Aero Circle, has been available since Windows XP.
 Page 63

The most significant change that we will notice is how we assign standard or custom cursors to an
object, such as to the form – we now set all cursors, both custom and system-provided, to a single
and more logical Cursor property, and the old MouseIcon and MousePointer properties have
disappeared, being moot. Now to set the form cursor to the Hourglass, we would submit “Me.Cursor
= Cursors.WaitCursor” (note the new name) rather than the VB6 “ Me.MousePointer = vbHourglass”.
Moreover, thanks to method overloading, we will also load it (set it) with either an existing custom
Cursor object, such as “Me.Cursor = OldPointer” (declared above), or create a new Cursor object for
it. What this means is that when we load it with a cursor object, VB.NET knows that we are setting it
to a Custom cursor, and when we set it to one of the standbys, we are setting it to an enumerator
value. Previously in VB6 we would load a custom cursor to the current form like this:
Me.MouseIcon = LoadPicture("c:\MyCursors\EW_06.CUR") 'use an external file resource.
Me.MousePointer = vbCustom 'use the new custom cursor object.

But now, under VB.NET, we would instead use this single line:
Me.Cursor = New Cursor("c:\MyCursors\EW_06.CUR") 'use an external file resource.

NOTE: Even though we can assign enumerator values to the Cursor property, we should still obtain the cursor object
through the Cursor.Current property. This is because even if we are using standard enumerators, such as
Cursor.WaitCursor, and we can also test for it using something like“ If Me.Cursor = Cursors.WaitCursor Then”, and
although it is possible to assign the Cursor property to an Integer variable (it is returned as an IntPtr), the value will
have little apparent meaning, regardless of some texts reporting that their VB6 and VB.NET values are equivalent.

Loading Cursors from an Embedded Resource.


If we would rather load a cursor that is an embedded resource (compiled within our program
executable, but not in the typical Resources location) rather than use an external cursor file, this is
quite easy to do, but we will first have to be sure to store a copy of the cursor file locally.
To add a local copy of the cursor to our project (if it is not added to our project already), in the
Solution Explorer, right-click where we want to store the resource (I create folders named Classes,
Forms, Modules, and Resources – using the AddAdd New Folder option – to keep my projects
tidy), or the project name itself if we simply want to store it in in the local pool of files. Select
AddAdd Existing Item, and browse to and select our cursor file (even if it is stored locally). It will
be added to our project and we will see it listed in the target storage list.
Next, we need to ensure that it is actually embedded within our application executable. To do that,
right-click the cursor item in the Solution Explorer list, and then in its Properties window, ensure
that the Build Action property is set to “Embedded Resource”.
Next, to use our embedded cursor within our application, assuming that the cursor file is named
Dilbert.cur (and note that this name is case sensitive), load it using a command like the following:
Cursor = New Cursor(Me.GetType(), "Dilbert.cur") 'use an embedded cursor resource.

Loading Cursors from the Application Resources.


If we would rather store the cursor in the Application Resources, select ProjectPropertiesResources,
Select either Files or Other (it does not matter which) from the first dropdown, then Add Existing File from
the Add Resource dropdown, then select our cursor.
Next, in order to load our Cursor resource, we will need to read it as a stream. However, because it is actually
stored in the resources as a 1-dimensional array of Bytes, we cannot cast it directly into a stream. However,
what we can do is first read these bytes into a memory stream (rather than writing it first to a file). A memory
stream exists wholly in-memory and we can dispose of it when we are finished with it, or we can let the
Garbage Collector handle that for us when it drops out of scope and all references are released.
Thus, once we have the item stored, we can use a command sequence like the following to get it:
Cursor = New Cursor(New System.IO.MemoryStream(My.Resources.Dilbert)) 'read cursor resource as a stream.
 Page 64

● A Note on Changes to the VB6 RECT Structure.


I did a lot of VB6 programming with the Rect structure, typically defined as the following:
Type RECT
Left As Long 'The Upgrade Wizard claims Left and Right conflict with the Form's Left and Right properties,
Top As Long ' but they do not, because they have to be referenced as RECT.Left and RECT.Right. They are
Right As Long ' not like VB6 Enumerations, which could be cited without their Enum Type. Apparently someone
Bottom As Long ' on the Upgrade Wizard Team was sampling the Italian Vino a few too many times...
End Type

This presents a problem in VB.NET forms, because the Upgrade Wizard claims the Left and Right
members conflict with the parent Form, but they do not! Unlike VB6 Enumerations, these members
must be cited as RECT.Left and RECT.Right. Regardless, the Upgrade Wizard “resolves” this faux issue
by renaming these two members Left_Renamed and Right_Renamed. Because yelling at the
computer does nothing, even when I enunciate my shouts distinctly into its stupid microphone, I
have gotten in the habit of defining my RECT structures like this in VB6 code that will be upgraded:
Type RECT 'This Structure is handled as Friend.
iLeft As Long
iTop As Long 'All these members default to Public.
iRight As Long
iBottom As Long
End Type

This change to VB6 code makes so much difference, because a RECT UDT upgrades cleanly to:
Structure RECT 'This Structure is handled as Friend.
Dim iLeft As Integer
Dim iTop As Integer 'All these members declared Dim are handled as Public.
Dim iRight As Integer
Dim iBottom As Integer
End Structure

Otherwise, we may need to burrow through our code and correct these afterward, unless we do not mind
having items that have been tagged “_Renamed”. Note further that VB.NET also provides a superior
Rectangle structure that is 100% compatible to our VB6 RECT UDT, so just replace them in VB.NET code.

● Notes on Initialized and Terminate Event Upgrades.


If we have classes containing Initialized and/or Terminate events, then after they are upgraded we
will find that the Initialized event is renamed Initialized_Renamed, and the Terminate event is
renamed Terminate_Renamed. We will also notice that a New (constructor), and a Finalize
(destructor) have been added to our class code. The New event will in turn invoke the
Initialized_Renamed code, and the Finalize event will invoke the Terminate_Renamed code.
Regardless of the fact that this is a workable strategy for class code upgrades, to me it looks a bit
goofy, and is too much work than just leaving the original function names alone and invoking them
from New and Finalize. As a bit of extra self-imposed work, I simply cut the code from within the
body of the Initialized_Renamed code and paste it over the top of its invocation line (replacing it)
for Initialized_Renamed within the Sub New code. I do likewise for Terminate_Renamed and Sub
Finalize. I then of course delete the empty bodies Initialize_Renamed and Terminate_Renamed.
For Example, the following VB6 Initialized method:
Public Sub Initialized()
LnkPrev = Nothing 'init links to nothing for now.
LnkNum = 0 'assume base of new array list (at least for now).
Init() 'reset potential variable data.
End Sub

It is upgraded to VB.NET to:


Public Sub New()
MyBase.New()
Initialized_Renamed() 'NOTE: VB.NET declares its own Initiate() and Terminate()
End Sub ' events, and so MUST rename the VB6 methods.
Public Sub Initialized_Renamed()
LnkPrev = Nothing 'init links to nothing for now.
LnkNum = 0 'assume base of new array list (at least for now).
Init() 'reset potential variable data.
End Sub
 Page 65

But I further combine these two methods by changing it to:


Public Sub New()
MyBase.New()
LnkPrev = Nothing 'init links to nothing for now.
LnkNum = 0 'assume base of new array list (at least for now).
Init() 'reset potential variable data.
End Sub

NOTE: See "Notes on Class Construction and Destruction – a Detailed Look" on page 117 for more details on New,
Finalize, and Dispose.

● A Note on Changes to FormWindowState Enumeration Conflicts.


Various states are often checked against constants. In VB6 we check the form’s WindowState property for the
constants vbNormal, vbMaximized, or vbMinimized. Although an upgrade sets these constants to
FormWindowState.Normal, FormWindowState.Maximized, and FormWindowState.Minimized, it may have
trouble determining from the program context what property the term should be upgraded to. After all,
VB.NET also recognizes vbNormal to be a property constant for file attributes (a set of constants,
interestingly enough, that VB6 users themselves whined Microsoft into "restoring" – now they complain
about the conflicts, which is why Microsoft had dropped them in the first place. Go figure...). As such, we
can usually simply glance at the code in a wider context and determine what property should be checked, such
as WindowState, FileAttributes, Constants, or other constant enumerators.

● Notes on VB6 Namespace Twips Upgrade Conversions.


Nearly all individual command upgrades thankfully do not warrant warnings. Most-times these silent
fixes will efficiently redress code, but once in a while these fixes will leave something to be desired
in-as-far as efficiency is concerned. It is not that they do not operate correctly; logically, they work
perfectly, but the code they generate can sometimes be a bit of over-kill. Most times this over-kill
involves twips. Twips (1440 dots per logical inch) were the default unit of graphical measurement in
VB6 and previous, allowing fast integer conversions between pixels (96 dots per logical inch) and
points (72 points per logical inch), but it made the code incompatible as-is with API functions,
which only operated in terms of pixels. Now that VB.NET has eliminated this confusion and by
default uses display/printer-compatible and universally-accepted pixel measurements, we now have
to suffer with VB6 upgrades that still need to deal with translating VB6 twips, mostly due to so many
apps using hard-coded twip values or offsets (I am as guilty of this as anyone).
Consider this VB6 line of code from one of my apps:
Me.Height = (Me.Height - Me.ScaleHeight) + Me.Animation1.Top + 60 'set visible height of the form.

The above code will compute the container height of the form ( Me.Height - Me.ScaleHeight), add the
top location of the Animation1 control to set the client area height, and then add 60 twips as a buffer.
When the code is upgraded to VB.NET, it becomes the following confusing line of code:
Me.Height = VB6.TwipsToPixelsY((VB6.PixelsToTwipsY(Me.Height) - VB6.PixelsToTwipsY(Me.ClientRectangle.Height)) +
VB6.PixelsToTwipsY(Me.Animation1.Top) + 60) 'set visible height of form.

This code uses the Microsoft.Windows.Compatability.VB6 namespace. Although we could leave this
code as-is, save for the 60 value, all this conversion back and forth between twips and pixels eats
precious loads of time. Consider modifying it to use just pixels. First some simple math: 60 twips /
15 twips per pixel = 4 pixels. Following is our pixel-only conversion:
Me.Height = (Me.Height - Me.ClientRectangle.Height) + Me.Animation1.Top + 4 'set visible height of form.

Notice that the only real differences from the original VB6 statement is that we added a 4-pixel
buffer instead of an exactly equivalent 60 twips, and since the ScaleHeight property does not exist
(the old and really confusing name given to the VB6 Client space height), we now read the Height
property from the more logical ClientRectangle structure (or even better, from its ClientSize).
 Page 66

● A Note on Fixing User-defined Twips Constants.


In light of the above mention of the confusion regarding twips and why the upgrade jumps through
so many hoops to upgrade the code, but at the same time not breaking it, it is strongly recommended
that any time we specify a lot of offsets, to instead resort to constants, which can usually be centrally
located in a module somewhere. By using constants, we can update a lot of code spread throughout
our application, but all from one place. For example, if I were to be offsetting screen coordinates by
60 twips, I might define a constant named PIXELS4 or TWIPS60. For example:
Public Const TWIPS60 As Long = 60 'set twip offset for 4 pixels.

This would be upgraded to VB.NET to:


Public Const TWIPS60 As Integer = 60 'set twip offset for 4 pixels.

But, knowing my code now operates with pixels (which I prefer, and not only because of my C++
development work or from constantly interfacing from VB6 to the Win32 API), I would divide this by
15 (15 twips per pixel), and change this line to:
Public Const TWIPS60 As Integer = 4 'set twip offset for 4 pixels.

● A Note on Speeding Returned VB6 Namespace List Item Values.


Keep in mind, as stated before, that ListBoxes and ComboBoxes in VB.NET accept Objects for their
Item collections, not VB6 strings. As such, VB.NET will upgrade the following VB6 command line:
SaveSetting App.Title, "Settings", "History" & CStr(Idx), Me.cboRecent.List(Idx) 'assumes List Item is string.

To the following:
SaveSetting(My.Application.Info.Title, "Settings", "History" & CStr(Idx), VB6.GetItemString(Me.cboRecent, Idx))

Apart from the requisite parentheses that must surround all .NET method parameters, notice that the
“App.Title” property was upgraded to use “ My.Application.Info.Title”, but more important to our
point, the “Me.cboRecent.List(Idx)” was upgraded to “VB6.GetItemString(Me.cboRecent, Idx)”.
This last fix ends up fully functional, but it is more work than we require. Remember that Strings are
also Objects, and in VB6 we had always supplied String text to the collection in Listboxes and
ComboBoxes. We can therefore simplify the last change to “Me.cboRecent.Items(Idx).ToString”.
NOTE: We may have noticed that ListBox and ComboBox controls are now syntactically aligned with Collections, all
implementing System.Collections.IList. As such, all collection-type controls have an Items array that accepts data of
generic type Object, and have a Default property of Item. Hence, we no longer have collection-type controls that have
an Items collection in one control and a List collection in another.

● A Note on VB6 Date Value Conversions.


An issue concerning dates was of VB6 storing Dates not only in Date-type variables, but also in
Doubles. This was because VB6 and earlier used a Double as the storage format for its Date type. I
must admit I took advantage of it in VB6 because it was so easy; it was just begging me to do it. In
VB.NET, they are no longer stored as 64-bit Doubles, but as 128-bit Date objects, and with loads of
extended functionality, making them far more powerful. However, if we have upgraded VB6 code
that manipulates dates as Double, use the Date function ToOADate to get a Double from a VB.NET
Date, and FromOADouble to get a Date from Double.
NOTE: VB6 used Doubles for dates because it was part of the OLE2 Specification, which VB6 was built around, even
using its ActiveX intermediate code (p-code), called VBC, as its native(-ish) compiled code, and then called executable
native by marketing departments because the runtime wrapper, execution parser, and support DLLs were CPU-native.
Often a double was used to strip the time (or date) from a date value, which stored the date as a
whole number part of a Double, and time as its fractional part:
 Page 67

Dim netDate As Date = Now 'get date and time of day.


Dim dblDate As Double = Fix(netDate.ToOADate()) 'get date without time of day.
netDate = netDate.Subtract(netDate.TimeOfDay) 'or, simply remove time factor from self.

The first two lines emulate the VB6 method. The third emulates this in VB.NET without any helper
functions (I have also seen this same solution written in some on-line VB.NET code as “ netDate =
Date.FromOADate(Fix(netDate.ToOADate()))”, which accomplishes the same task, but eats more time).

● A Note on Speeding Format Command Use in VB.NET.


Statements using VB6’s Format command, like “myStr = Format(TotalFolders - FolderCnt, "#,##0") ”
will be upgraded in VB.NET to “myStr = VB6.Format(TotalFolders - FolderCnt, "#,##0") ”. However, we
can speed up this code by instead using “ myStr = (TotalFolders – FolderCnt).ToString("#,##0") ”. The
integer result is a value-type, and as such it acts exactly like a variable or field; hence, embraced
(encapsulated) expressions, even for strings or function results, have method and property members.
By the way, VB.NET also fully supports the original statement, “ myStr = Format(TotalFolders -
FolderCnt, "#,##0")”, so we could simply remove the “ VB6.” that the upgrade prepends to the Format
statement, and it will continue to be operational, even though I prefer the isolation the ToString
method provides the result’s format, on top of it being a more elegant solution.

● A Note on Accessing Screen Properties.


In VB6, we could access the Me.Screen object and obtain its dimension properties, such as Left,
Right, Top, Bottom, Width, and Height. The new VB.NET Screen class is now loaded with
functionality, to include enabling us to easily access multiple screens at the same time, where one
screen displays source code, and another the run-time application we are debugging. And so for us to
access the Screen’s Left, Right, Top, Bottom, Width, and Height properties, because of multi-screen
support, we have to drill a little deeper into the Screen objec going to its PrimaryScreen.Bounds
structure, which exposes the desired properties. For example, if our VB6 code contained:
Dim ScrHeight As Double
ScrHeight = Me.Screen.Height

It will be upgraded to VB.NET like this:


Dim ScrHeight As Double
ScrHeight = VB6.PixelsToTwipsY(System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height)

However, we can simplify this code in 3 ways: 1) if we are in a form, then the
System.Windows.Forms namespace is already loaded, so we can cut that from the code. 2) We are
now working in Pixels, so we can remove all Twip conversions. 3) In VB.NET we can combine
declaration with assignment. All these options give us this much shorter and simpler code:
Dim ScrHeight As Double = Screen.PrimaryScreen.Bounds.Height 'you may want to instead set this to an Integer.

NOTE: By the way, adding or removing “System.Windows.Forms” does not alter the compiled code. This framework
mapping allows the compiler to zero in on target methods. Once a target is determined, the compiled code does not need
to calculate the address of a method or class or enumerator each time it is accessed; it is already known, so when the
preprocessing is finished and the actual output code is generated, absolute addressing has already been established.
 Page 68

● Notes on On Iexpr GOTO and How to Sort Without GOTO.


To handle upgrading “On Iexpr GOTO n1, n2, n3,...” one first cannot help but wonder why such code
still existed in VB6 to begin with. But no matter. The best resolution and fastest means of adapting
this code to operate best in VB.NET is to convert it to a Select block. For example:
Select Case IExpr
Case n1
'do code associated with n1
Case n2
'do code associated with n2
Case n3
'do code associated with n3
...
End Select

By moving the scattered code within a block, we contain it. Also, when each routine completes its tasks
under its Case heading, it immediately transfers control, not falling into the next Case block as it does in
C/C++, but directly to the End Select, where program flow continues (VB code contains an invisible
embedded C/C++ Break command at the end of each case block, ultimately translating to either a hidden
Goto or jump statement to the end of the block).

If we cannot easily adapt our code to this format, then we are guilty of
writing the infamous “spaghetti code”, which was an old software
engineering term used to describe procedural programs whose logic went all
over the place, like spaghetti on a plate. The advent of languages like ADA,
C, and Pascal were meant to provide developers with means to avoid writing
spaghetti code. It was not until .NET Framework’s introduction of VB.NET
and C# where everything is virtually forced to be encapsulated and
modularized (managed), and likewise (hopefully) forced developers to write
modular, logical code for them to even work on these platforms (I say
virtually, as we will very soon understand).
Many programmers have often made the claim to me that it is impossible for
some spaghetti code to be written any other way. It has been my long experience
that their excuse is a load of horse pucks, and that by rewriting the code in a
modular fashion it will also make debugging that code easier. Therefore, if we
do not want to re-write the code cleanly, then we should consider leaving it in
VB6. A good example that is frequently cited to me is the Shell-Metzner Sort
algorithm. I am shown code, almost to an instruction, to that which I had once
found in a Creative Computing Magazine in the 1970s, back in the days when
TRS-80 was King and a deep-freezer-sized 36-bit PDP-10 “mini-computer” was the envy of every
university computer science department geek. It was described as shown above.
The program written to support it was like the following, though here is code that, believe it or not,
even VB.NET will accept and execute flawlessly, and all without a single complaint (provided that
Option Strict is turned off, Option Explicit is turned off, and Option Infer is turned on):
 Page 69

This subroutine is simply the previous program flowchart translated to old DOS BASIC as much as
possible. Back then, we could specify variables at a whim, never having to DIM them or define their
type (the ominous origin of the famously slow Variants in VB1-VB6), as we have emulated with
Option Infer On. Lines beginning with “L” create labels for what would have been line numbers.
Now we might appreciate why flowcharts were so popular back then, because the actual program
spaghetti code is convoluted and hard to follow without doing what I did, which was to take a pencil
and draw dividing lines, as well as line arrows from the GOTO locations to their destinations.
I have been told repeatedly this routine cannot be structured because GOTO instructions go in all
directions and cannot be logically blocked. But when I look at the flow chart, I am seeing loops and
blocks and IF…ELSE blocks of code. I first wrote the following structured translation long ago in
FORTRAN, then in C++, then in QuickC and QuickBASIC (predecessor to VB1), then in VB6, and
finally in VB.NET. Following is a segment of a string sort, comparing the original with the current
(some variables have already been declared by this time, which we will ignore for now):
' sort initialization Original Algorythm (1-Based)
' ----------------------------
NumberofItems = m_MyCount 'get number of items to sort. N=(Number of Items)
HalfDown = NumberofItems 'number of items to sort. M=N
'
' perform the sort.
'
Do While CBool(HalfDown \ 2) 'while counter can be halved. A: IF(M\2)=0 THEN STOP
HalfDown = HalfDown \ 2 'back down by 1/2. M=M\2
HalfUp = NumberofItems - HalfDown 'look in upper half K=N-M
IncIndex = 0 'init index to start of array. J=1
Do While IncIndex < HalfUp 'do while we can index range.
IndexLo = IncIndex 'set base. B: I=J
Do
IndexHi = IndexLo + HalfDown 'if (IndexLo) > (IndexHi), then swap. C: L=I+M
If StrComp(StrAry(IndexLo), StrAry(IndexHi),
CompareMethod.Text) = CompFlag Then ' IF D(I)>D(L) THEN GOTO D
Tmp = StrAry(IndexLo) 'swap string items. T=D(I)
StrAry(IndexLo) = StrAry(IndexHi) ' D(I)=D(L)
StrAry(IndexHi) = Tmp ' D(L)=T
IndexLo = IndexLo - HalfDown 'back up index. I=I-M
Else ' IF I>=1 THEN GOTO C
IncIndex += 1 'else bump counter. D: J=J+1
Exit Do ' IF J>K THEN GOTO A
End If ' GOTO B
Loop While IndexLo >= 0 'while more things to check.
Loop
Loop

For completeness, following is my module to sort string arrays, ascending or descending:


Module modSortStringArray

'Sort a String Array Alphabetically.


'*******************************************************************************
' modSortStringArray - Sort a string array in Ascending or Descending order using
' the Shell-Metzner Sort algorythm. This sort is extremely
' fast. Though mot much longer than QuickSort, it sorts MUCH
' FASTER with much fewer replacements.
'EXAMPLE:
' Dim Test(3) As String 'or Dim Test() As String = {"Bob", "Zed", "Allen", "Rick"}
' Test(0) = "Bob"
' Test(1) = "Zed"
' Test(2) = "Allan"
' Test(3) = "Rick"
' If SortStringArray(Test) Then
' Dim S As String = "The array is sorted:" & vbCrLf
' For Index As Long = 0 To 3
' S = S & " " & Test(Index) & vbCrLf
' Next Index
' MsgBox S
' Else
' MsgBox "The array was not sorted. The string was not an array"
' End If
'*******************************************************************************

Public Function SortStringArray(ByRef StrArray() As String, Optional ByVal SortDescending As Boolean = False) As Boolean
'
' get number of elements to do. Exit if this is not an array.
'
Dim NumberofItems As Integer
 Page 70

Try
NumberofItems = UBound(StrArray) + 1 'number of strings to do.
Catch
Return False 'Array not dimensioned, so error.
End Try
'
' determine if we are sorting in Ascending or Descending order.
'
Dim AscDecFlag As Integer = 1 'default to ascending.
If SortDescending Then
AscDecFlag = -1 'we will be doing descending.
End If
'
' now perform the sort.
'
Dim HalfDown As Integer = NumberofItems 'number of items to sort.
Dim HalfUp, IndexLo, IndexHi, IncIndex As Integer

Do While CBool(HalfDown \ 2) 'while counter can be halved.


HalfDown \= 2 'back down by 1/2.
HalfUp = NumberofItems - HalfDown 'look in upper half.
IncIndex = 0 'init index to start of array.
Do While IncIndex < HalfUp 'do while we can index range.
IndexLo = IncIndex 'set base.
Do
IndexHi = IndexLo + HalfDown
If StrComp(StrArray(IndexLo), StrArray(IndexHi), CompareMethod.Binary) = AscDecFlag Then 'check strings.
Dim Tmp As String = StrArray(IndexLo) 'swap strings
StrArray(IndexLo) = StrArray(IndexHi)
StrArray(IndexHi) = Tmp
IndexLo = IndexLo - HalfDown 'back up index.
Else
IncIndex += 1 'else bump counter.
Exit Do
End If
Loop While IndexLo >= 0 'while more things to check.
Loop
Loop
End Function
End Module

NOTE: Most VB.NET lists and arrays already have a built-in sort method that employs the QuickSort algorythm that
we can invoke by selecting Array.Sort(strArray), for example. The advantage here is that a sort method is already
present and easy to access. However, the above Shell-Metzner sort is much faster than QuickSort, significantly so in
large lists, and uses fewer replacements. The above version can also sort in descending order, if we wish it.
For amusement, here, though incomplete, is the guts of the QuickSort used by .NET:
Friend Sub QuickSort(ByVal left As Integer, ByVal right As Integer)
Do
Dim low As Integer = left
Dim hi As Integer = right
Dim median As Integer = Array.GetMedian(low, hi)
Me.SwapIfGreaterWithItems(low, median)
Me.SwapIfGreaterWithItems(low, hi)
Me.SwapIfGreaterWithItems(median, hi)
Dim y As Object = Me.keys.GetValue(median)
Do
Try
Do While (Me.comparer.Compare(Me.keys.GetValue(low), y) < 0)
low += 1
Loop
Do While (Me.comparer.Compare(y, Me.keys.GetValue(hi)) < 0)
hi -= 1
Loop
Catch exception1 As IndexOutOfRangeException
Throw New ArgumentException(Environment.GetResourceString("Arg_BogusIComparer",
New Object() {y, y.GetType.Name, Me.comparer}))
Catch ex As Exception
Throw New InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), ex)
Catch obj1 As Object
Throw New InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"))
End Try
If (low > hi) Then
Exit Do
End If
If (low < hi) Then
Dim obj3 As Object = Me.keys.GetValue(low)
Me.keys.SetValue(Me.keys.GetValue(hi), low)
Me.keys.SetValue(obj3, hi)
If (Not Me.items Is Nothing) Then
Dim obj4 As Object = Me.items.GetValue(low)
Me.items.SetValue(Me.items.GetValue(hi), low)
 Page 71

Me.items.SetValue(obj4, hi)
End If
End If
If (low <> &H7FFFFFFF) Then
low += 1
End If
If (hi <> -2147483648) Then
hi -= 1
End If
Loop While (low <= hi)
If ((hi - left) <= (right - low)) Then
If (left < hi) Then
Me.QuickSort(left, hi)
End If
left = low
Else
If (low < right) Then
Me.QuickSort(low, right)
End If
right = hi
End If
Loop While (left < right)
End Sub

● A Note on On Iexpr GoSub.


To handle upgrading “On Iexpr GOSUB n1, n2, n3, ...” one once again cannot help but wonder why
such code still existed in VB6 to begin with. The best resolution and fastest means of adapting this
code to operate in VB.NET is to convert this to a Select block, much as outlined in the previous
point, but here it is more ideally suited to address the issue, breaking it down to this:
Select Case IExpr
Case n1
'Invoke method associated with n1.
Case n2
'Invoke method associated with n2.
Case n3
'Invoke method associated with n3.
...
End Select

As we can see, invoking a selection of subroutines is a natural choice for a Select…Case block.
When a subroutine returns, its control will not fall into the next Case block, but will go directly to
the End Select, where program flow will continue.

● Notes on Updating VB6 Error Trapping E-Z and Painlessly.


Although VB.NET still supports “On Error Resume Next” and “On Error Goto 0” unstructured exception
handling to support VB6-style error trapping, we really should consider upgrading it to the more
controlled (encapsulated) and E-Z to use Try…End Try structured exception handling statement.
Consider the following VB6-style error trapping implemented (and still supported) in VB.NET:
Private Function ReadFile(ByVal FilePath As String) As String()
Dim fso As New FileSystemObject 'using COM object IWshRuntimeLibrary
Dim ts As TextStream

On Error Resume Next 'Resume on errors.


ts = fso.OpenTextFile(FilePath, IOMode.ForReading, False) 'open file.
If CBool(Err.Number) Then 'if error generated...
MsgBox("Cannot open " & FilePath & ". It does not exist",
MsgBoxStyle.OkOnly,
"File Open Error")
Return Nothing 'nothing for invoker to process.
End If
On Error GoTo 0 'turn off error Trapping.
Dim TxtLines() As String = Split(ts.ReadAll(), vbCrLf) 'place each line in an array element.
ts.Close() 'close text stream.
Return TxtLines 'return array of text lines.
End Function

The preceding can easily be adapted to the following Try…End Try block:
 Page 72

Private Function ReadFile(ByVal FilePath As String) As String()


Dim fso As New FileSystemObject 'use COM object IWshRuntimeLibrary. We should upgrade this to a StreamReader.
Dim ts As TextStream 'We REALLY should upgrade all this to a faster System.IO.StreamReader.

Try 'try the following...


ts = fso.OpenTextFile(FilePath, IOMode.ForReading, False) 'open file.
Catch ex As Exception 'catch errors (you can STILL check Err.Number).
MsgBox("Cannot open " & FilePath & ". It does not exist",
MsgBoxStyle.OkOnly,
"File Open Error")
Return Nothing 'nothing for invoker to process.
End Try 'end of error trapping.
Dim TxtLines() As String = Split(ts.ReadAll(), vbCrLf) 'place each line in an array element.
ts.Close() 'close text stream.
Return TxtLines 'return array of text lines.
End Function

NOTES: We cannot mix VB6-style error trapping and Try…End Try error trapping within the same block of code (one
type in one place, and the other in another place). Choose one or the other for the block. Also, the optional Finally block
segment can precede the End Try statement, but following the last, or only Catch block, holding code that will follow
Try and Catch, regardless of there being errors or not.
There is a lot more to the Catch statement than meets the eye. We can also catch multiple exceptions
by applying multiple Catch phrases, each Catch phrase encapsulating its own type of error. For
example, by adding a ‘When’ clause to a Catch phrase we can narrow down error checks. This way
we could specifically trap “File Not Found” errors if we wanted to, and trap all other errors in
another generic block. With that in mind, we can replace the above Catch block with:
Catch When Err.Number = 53 'catch <File Not Found>.
MsgBox("Cannot open " & FilePath & ". It does not exist",
MsgBoxStyle.OkOnly,
"File Open Error")
Return Nothing 'nothing for invoker to process.
Catch ex As Exception 'general error trap.
MsgBox("Error with " & FilePath & "." & vbCrLf &
ex.Message,
MsgBoxStyle.OkOnly,
"File I/O Error")
Return Nothing 'nothing for invoker to process.

Each Catch phrase should have its own unique Catch exception filter variable (use a blank Catch
line –a Catch phrase without an exception parameter– if it should catch everything, but we will not
need to process an exception variable). Also, always place the generic “catch-all” trap as the last in
the list, otherwise it might execute before any narrower traps that might also be present are checked.
We can also add an optional Finally block to the bottom of the Try block (before “End Try”). A Finally
block is always processed when execution leaves any part of the Try statement, regardless if there were
errors or not. Although in my examples I used “ Return Nothing” to exit from the traps, I did this because
there was nothing else to do. However, a trapped error in no way whatsoever means that continued
processing is not possible.
We can also have Try statements that have a blank Catch block, because it might not matter if errors
were generated or not. However, remember that the Try statement must contain at least one Catch block,
even if the Catch block is empty.
For example, we could have something like this:
Try 'This layout emulates VB6, where Try duplicates: On Error Resume Next
'Insert commands you do not care what happens in here.

Catch 'blank Catch block, which is immediately transferred to during any exception error in Try.
End Try 'The end of the Try Block is like VB6: On Error Goto 0

This would be exactly like using “ On Error Resume Next” at the beginning of the Try block, and it is
exactly like ending the block at End Try with “On Error Goto 0” under VB6.
 Page 73

● Notes on Changes to Common Dialogs and E-Z Upgrade Recovery.


Common Dialogs have changed significantly, and in my view, for the better. Still, we are going to
find some interesting warnings that may at first confuse you, or worse, panic you. For example, on a
form I have a CommonDialog control named CommonDialog1. With it I am going to invoke a
subroutine to open a text file so that I can read it in. We will only look at the dialog interface for
opening the file in the original VB6 subroutine code, shown below.
With frmSpellCheck.CommonDialog1
.Flags = cdlOFNFileMustExist Or cdlOFNPathMustExist Or cdlOFNLongNames Or cdlOFNExplorer
.DefaultExt = "txt" 'default extension if the user does not supply one.
.FileName = vbNullString
.Filter = "Text File (*.txt)|*.txt" 'we will be looking for files with a .txt extension.
.Title = "Open an Existing Text File" 'add a header message to the dialog form.
.CancelError = True 'generate an error if the user hits CANCEL in the dialog.
On Error Resume Next 'Keep running if an error found (hitting CANCEL generates an error).
.ShowOpen 'display the Open dialog box.
If CBool(Err.Number) Then Exit Sub 'Assume user hit CANCEL.
On Error GoTo 0 'else clear error trapping.
TxtFile = Trim(.FileName) 'grab filename.
If Len(TxtFile) = 0 Then Exit Sub 'exit if no filepath was supplied.
End With

After an upgrade to VB.NET, our code block looks something like this presumed nightmare:
'UPGRADE_WARNING: CommonDialog variable was not upgraded. For more: BLAH-BLAH-BLAH
With frmSpellCheck.CommonDialog1
'UPGRADE_ISSUE: Constant cdlOFNLongNames was not upgraded. For more: BLAH-BLAH-BLAH
'UPGRADE_ISSUE: MSComDlg.CommonDialog property CommonDialog1.Flags was not upgraded. For more: BLAH-BLAH-BLAH
.Flags = MSComDlg.FileOpenConstants.cdlOFNLongNames
'UPGRADE_ISSUE: Constant cdlOFNExplorer was not upgraded. For more: BLAH-BLAH-BLAH
'UPGRADE_ISSUE: MSComDlg.CommonDialog property CommonDialog1.Flags was not upgraded. For more: BLAH-BLAH-BLAH
.Flags = MSComDlg.FileOpenConstants.cdlOFNExplorer
'UPGRADE_WARNING: MSComDlg.CommonDialog property frmSpellCheck.CommonDialog1.Flags was upgraded to
frmSpellCheck.CommonDialog1Open.CheckFileExists which has a new behavior. For more: BLAH-BLAH-BLAH
'UPGRADE_WARNING: MSComDlg.CommonDialog property frmSpellCheck.CommonDialog1.Flags was upgraded to
frmSpellCheck.CommonDialog1Open.CheckPathExists which has a new behavior. For more: BLAH-BLAH-BLAH
.CheckFileExists = True
.CheckPathExists = True 'MUST EXIST.
.DefaultExt = "txt"
.FileName = vbNullString
'UPGRADE_WARNING: Filter has a new behavior. For more: BLAH-BLAH-BLAH
.Filter = "Text File (*.txt)|*.txt"
.Title = "Open an Existing Text File"
'UPGRADE_WARNING: The CommonDialog CancelError property is not supported in Visual Basic .NET. For more: BLAH-BLAH-BLAH
.CancelError = True
On Error Resume Next
.ShowDialog()
If CBool(Err.Number) Then Exit Sub
On Error GoTo 0
TxtFile = Trim(.FileName)
If Len(TxtFile) = 0 Then Exit Sub
End With

Although this may look messy, it is actually quite easy and pain-free to clean up. First, the single
CommonDialog control available to VB6 users has (finally) been broken up into five separate
dialogs (which I think they should have been in the first place, considering that the system interface
had kept them separate since, if I recall, Windows 3.0), called OpenFileDialog, SaveFileDialog,
ColorDialog, FolderBrowserDialog, and FontDialog. By default, the Upgrade Wizard will add
“Open” to the end of the CommonDialog control’s name, and the CommonDialog control will be
upgraded to a FileOpenDialog type control. In my case, my CommonDialog1 control (now an
OpenFileDialog object) is now named CommonDialog1Open.
To fix the above problems, I first rename the erroneous CommonDialog1 as CommonDialog1Open.
The reason that this name was not automatically changed in the code to the new control by the
Upgrade Wizard was because the wizard was not sure if we actually wanted one of the other four
dialog controls (the upgrade wizard’s knowledge is usually confined to the singular line it is
currently working on, never taking an ‘overall’ view of the code). This way we will have to apply a
personal touch to the control before our application can actually run. By the way, if we do want to
save the file instead, here or elsewhere, we will need to add a SaveFileDialog control to our form.
 Page 74

If it is an upgraded VB6 project, we might consider some naming uniformity, so we would rename
the new SaveFileDialog1 to CommonDialog1Save, for instance. This also brings the dialog controls
up in sequential order on an Intellisense dropdown list.
The next thing we should be aware of is that the single Flags property is replaced by individual
Boolean properties; and each enumerator we had applied to it are now set on separate lines; the
actual Flags property replaced by the fitting property allied with the control it is being used with.
But if an applied flag is no longer recognized for that application of the control, then it is simply set
to the now-non-existing Flag property, plus an error warning is issued for each one. If a property is
still being assigned to Flags, it is a safe bet that we can just delete them. That clears another hurdle.
We are next told that the properties CheckFileExists and CheckPathExists have been upgraded to a
new name and a new behavior. The new names are obvious, but the only new behavior is that they
are now individual Boolean properties and they are set to True or False instead of having an
enumerated constant applied. We can ignore the warnings, so simply delete the warning comments.
The next thing we look at is that the filter has a new behavior. This one originally had me confused,
until I realized that VB6 allowed us to use a Null (Chr$(0)) as a field separator, not just the Pipe (“|”).
We now have to use the Pipe to separate fields. By the way, in case we are new to dialog filters, we
add filters in pairs, where the first half is descriptive; the part the user sees, having nothing to do
with actual filtering, like “Text File (*.txt)”, and the second part is the actual filter pattern, such as
“*.txt”. We combine these by separating them with a pipe “|” character, rendering “Text File
(*.txt)|*.txt”. We can also select alternative selection filters, which the user can select from a
dropdown list, by appending more Description | Filter pairs, separating them from the others with a
pipe character as well. We can even combine multiple filters into a single choice, such as “ Image
Files (BMP, JPG, PNG)|*.bmp;*.jpg;*.png”. We separate the patterns in the filter pattern portion with a
semicolon (the descriptive portion is as we choose, but a comma separator is traditional.
The last warning we run into tells us that the CancelError property is not supported in VB.NET. The
way we deal with it in VB6 is pretty much as shown in our VB6 example. I use the On Error
Resume Next, because when CancelError was set to True, if the user selected Cancel, a Cancel
Exception Error was thrown. The On Error Resume Next prevented the program from aborting on
this error, and then all we had to do is check if Err.Number was non-zero (under VB6, a non-zero
value would be automatically cast to a Boolean, eliminating the need for Cbool(Err.Number), but
that is actually a very bad habit to get into, and VB.NET especially does not like it with Option Strict
turned on). We clear the error trap with On Error Goto 0. Although this type of error trapping is still
accepted, one really should get into using VB.NET’s much superior Try…Catch…End Try format (I
have been using it since C++ in Visual Studio 6, and it is a treasure).
However, we can completely eliminate the error trapping or setting the now unsupported
CancelError flag. All we need to do is catch the returned value directly from the ShowDialog
function. If we were testing for multiple replies, such as checking for Abort, Cancel, Retry, Ignore,
Yes, No, OK, or None, I might consider placing it in a Select block, such as:
Select Case .ShowDialog()
Case Windows.Forms.DialogResult.Abort 'if we are doing this within a form, we can shorten this
'Do something here ' to DialogResult.Abort.
Case Windows.Forms.DialogResult.Retry 'can be shortened to DialogResult.Retry.
'Do another thing here
Case Windows.Forms.DialogResult.Cancel 'can be shortened to DialogResult.Cancel.
Exit Sub
End Select
 Page 75

But since we are simply checking for a user Cancel, we can replace all this VB6 code:
.CancelError = True
On Error Resume Next
.ShowDialog()
If CBool(Err.Number) Then Exit Sub
On Error GoTo 0

With this single VB.NET line of code:


If .ShowDialog() = DialogResult.Cancel Then Exit Sub

With all that quick work, which we will find easier and faster to deal with as we get a few under our
belt,, we will quickly end up with the following block of problem-free code:
With frmSpellCheck.CommonDialog1Open
.CheckFileExists = True
.CheckPathExists = True 'MUST EXIST
.DefaultExt = "txt"
.FileName = vbNullString 'in VB.NET, this is the same as "".
.Filter = "Text File (*.txt)|*.txt"
.Title = "Open an Existing Text File"
If .ShowDialog() = DialogResult.Cancel Then Exit Sub
TxtFile = Trim(.FileName)
If Len(TxtFile) = 0 Then Exit Sub
End With

● Notes on VB6.CopyArray and How to Improve It.


One upgrade that was initially a little confusing to me was a ‘quiet’ one (no upgrade warning issued)
that employs the VB6.CopyArray helper function (from the Microsoft.VisualBasic.Compatibility
namespace) that will convert an array of objects to a system array, happening when the VB6 code
passes any array to another variable. This is because VB6 passed a copy of the array, whereas
VB.NET passes a reference to the array, so the Clone method is needed to actually copy the array
(something VB6 did “behind the curtain”, which sometimes drove me mad, because I wanted to
copy a reference to it, being a C++ developer at my “day job” – which is where the undocumented
VB6 ObjPtr and StrPtrArray functions really come in handy...). However, if we were to set Option
Strict On, which I always do, then the return type from VB6.CopyArray generates an error, because it
returns type System.Array, so we must recast it.
Suppose we had a Class named dynNodes that has an array named Item of objects named dynNode,
and the dynNode object had a recursive function named GetAllMarked that returns an array of all
marked dynNode objects in dynNodes.
In VB6, suppose we had this block of code:
'*******************************************************************************
' Function Name : GetAllMarked
' Purpose : Get all marked items from this node down.
'*******************************************************************************
Friend Function GetAllMarked() As dynNode()
Dim Nds() As dynNode
Dim Cnt As Integer

If m_MyMarker Then 'is THIS node marked?


ReDim Nds(0) 'yes, so save it to the list.
Nds(0) = Me 'stuff a reference to self in it.
Cnt = 1 'count 1 gathered.
Else
Cnt = 0 'else list still empty.
End If
'
' now scan through child nodes and gather their lists.
'
With m_MyNodes
For Index As Integer = 1 To .Count 'process all child nodes.
Dim sNds() As dynNode = .Item(Index).GetAllMarked() 'recurse through each for all marked.
On Error Resume Next 'error trap.
Dim UB As Integer = UBound(sNds) 'get upper bounds.
If Err.Number = 0 Then 'we have a dimmed array if no error.
ReDim Preserve Nds(Cnt + UB) 'expand container to contain gathered items.
For I As Integer = 0 To UB
Nds(Cnt + I) = sNds(I) 'append new list to local.
 Page 76

Next I
Cnt = Cnt + UB + 1 'bump new count.
End If
Next Index 'do all child nodes.
If CBool(Cnt) Then
GetAllMarked = Nds 'return list.
Else
GetAllMarked = Nothing 'No array to return.
End If
End With
End Function

When this code is upgraded, 2 lines are changed (marked with a darker shading, above):
Dim sNds() As dynNode = VB6.CopyArray(.Item(Index).GetAllMarked()) 'recurse through each for all marked

Return VB6.CopyArray(Nds) 'return list

These work fine, until we set Option Strict On (I am a strict typing nut from my many years as a
FORTRAN/C/C++ software engineer). Afterward, they are flagged as errors, reporting “ Option Strict
On disallows implicit conversions from System-Array to '1-dimensional array of DynamicNodes.dynNode'”.
To correct it, we can optionally skip using VB6.CopyArray and instead directly use the Clone method
to the source array, and then, in either case, we must cast it to the proper type, being dynNode in this
case. For example, DirectCast(VB6.CopyArray(Nds), dynNode), or DirectCast(Nds.Clone, dynNode).
If we look at the disassembly of the VB6.CopyArray function (thanks to the excellent utility .NET
Reflector from RedGate Software, http://Reflector.Red-Gate.com), we find this simple code:
Public Shared Function CopyArray(ByVal SourceArray As Array) As Array
If (SourceArray Is Nothing) Then
Return Nothing
End If
Return DirectCast(SourceArray.Clone, Array)
End Function

The way I address this upgrade is simple. In cases where the code redundantly clones a freshly
created array, as we see in the above example, I edit it further by removing VB6.CopyArray from the
upgraded code, but retain its parameters: Dim sNds() As dynNode = .Item(Index).GetAllMarked() and
Return Nds. Although the above upgraded code actually works just fine and is safe to use, it is
wasteful program-slowing over-kill in our case because it is redundantly cloning a fresh array that
would afterward be tossed aside, instead of being used as my code intended. This particular
occurrence of redundant cloning is actually profusely common in VB6 and occurs far more often
than we might ever imagine. What this upgrade code using VB6.CopyArray in fact does is emulate
exactly what VB6 does all the time “behind the green curtain”, because VB6 not just auto-clones all
Strings when they are assigned to any variables, but VB6 also auto-clones any and all arrays during
assignments in the very same way. The fact is, VB6 strings were actually objects, but they were only
treated as scalar variables because of all the automatic cloning VB6 did on them.
Creating Strongly-Typed Clone Helper Functions
If our application is cloning a lot of arrays, we can write simple helper functions to make cloning
them much easier. For example, to clone a lot of 1-dimensional String arrays, we could create and
use a strongly-typed cloning function like this:
' Make copying 1-dimensional string arrays as E-Z as drinking beer at a barbecue.
Friend Function CloneSA(ByRef strAry() As String) As String()
If strAry Is Nothing Then 'if the array is not dimensioned...
Return Nothing 'return the same.
End If
Return DirectCast(strAry.Clone, String()) 'else clone the array and return the clone.
End Function

With this in place, we can clone string arrays as simply as Dim Dst() As String = CloneSA(Src).
 Page 77

● Notes on E-Z Recovery of the ItemData List Object Property.


VB.NET no longer supports the simple ItemData property of a ListBox or ComboBox. In VB6, the
ItemData property could be set at design time in the Properties window to associate an Integer with a
ListBox or ComboBox item, often used to associate an ID number with a text string. In VB.NET the
ItemData property no longer exists, but this is due to the more powerful functionality given to Listboxes
and Comboboxes, where list items are now able to be of any object, not just as simple text.
In the upgrade from VB6 to VB.NET, we may notice that the upgrade uses the VB6.SetItemData method
to initialize any design time ItemData information, usually invoked in the constructor of the parent form
(Public Sub New). To access the ItemData information, it uses the VB6.GetItemData method to emulate
the functionality of the lost VB6 ItemData property. For example, “Dim I As Integer =
VB6.GetItemData(List1, List1.SelectedIndex)”. To obtain the text from ItemData, it used
VB6.GetItemString, as in “Dim Result As String = VB6.GetItemString(List1, List1.SelectedIndex) ”.
Because I personally want to avoid using code-heavy helpers and get rid of the VB6 Compatibility
Library reference altogether (the reference to the Microsoft.VisualBasic.Compatibility namespace), and
I would simply rather write my applications in native VB.NET code, I would instead extract my string
using “Dim Result As String = List1.Items(List1.SelectedIndex).ToString() ”. However, it does not
emulate all that the VB6 helper did, which, if we looked at the VB6 class’s source code using Red-Gate
Software’s .NET Reflector, http://Reflector.Red-Gate.com, it is quite extensive, and eats up a lot of precious
time, though I will give it kudos for providing a solution to the issue.
So what I want to do, at the cost of only a little (re-usable) work, but with the benefit of involving
significantly less overall code and overhead, I would first create a simple ItemData class, such as the
following, which is based on the ListBoxItem class used within the VB6 helper:
Public Class ListItem
'-------------------------------
' Field Data - You can store more than just these fields here.
'-------------------------------
Public ItemString As String 'Text data for Item.
Public ItemData As Integer 'You can also declare this as String, if you would rather have a string key.
'-------------------------------
' Custom constructor; used to actually add data to a ListBox or ComboBox.
' You are not limited to adding just one or two items, or of just these types.
'-------------------------------
Public Sub New(ByVal ItemString As String, ByVal ItemData As Integer)
Me.ItemString = ItemString ' You may want to add more string parameters.
Me.ItemData = ItemData ' and then combine them with a space separator in ToString.
End Sub ' or even via a custom reporting method. Limitless possibilities!

'-------------------------------
' Constructor for assigning just text and no ItemData
'-------------------------------
Public Sub New(ByVal ItemString As String)
Me.New(ItemString, 0)
End Sub

'-------------------------------
' Provide a text data property to override the useless default in the Item() object.
' you can also use this to combine stored items when more than one text item is added.
'-------------------------------
Public Overrides Function ToString() As String
Return Me.ItemString
End Function
End Class

I would assign new text and indexes to my Listbox named ListBox1 using something like this:
Me.ListBox1.Items.Add(New ListItem("David", 50159))

Extracting the text data can be done easily enough:


Dim SName As String = Me.ListBox1.Items(Index).ToString ' you can also specify ItemString instead of ToString.

But extracting the index trades off with slightly more work, but it is still simple enough:
Dim Idx As Integer = DirectCast(Me.ListBox1.Items(Index), ListItem).ItemData
 Page 78

Because Items(Index) returns a generic object, we have to take the extra step of directly casting it to
the type that we know it contains, which is a ListItem object. But if we have a lot of these to punch
into the keyboard, this can result in a lot of typing. But even so, we can still employ less typing and
much less code overhead than using VB6.GetItemData and VB6.GetItemString simply by writing
our own little helper function, which we can add to a small module at the end of the class:
Public Function ExtLI(ByRef Obj As Object) As ListItem
If TypeOf Obj Is ListItem Then 'if the object is type ListItem.
Return DirectCast(Obj, ListItem) 'return the object as a ListItem.
End If
Return Nothing 'else return a null object.
End Function

Using the above helper function, we can now obtain the item data using the following:
Dim Idx As Integer = ExtLI(Me.ListBox1.Items(Index)).ItemData

And to get the item string, we can use either of the following two methods:
Dim SName1 As String = Me.ListBox1.Items(Index).ToString 'Method 1 example.
Dim SName2 As String = ExtLI(Me.ListBox1.Items(Index)).ItemString 'Method 2 example.

● A Short Note on VB6’s Automatic Boolean Conversions.


If we were prone to using values in integer variables and fields as Boolean flags in conditional
statements, such as using “If Idx Then” to test if the integer variable Idx contains a zero (False) or non-
zero (True) value, if we have Option Strict turned On (which I highly recommend), we will need to
properly cast them to Boolean, such as “If CBool(Idx) Then”.

● Notes on Option Strict On and Why We Should Use It.


One thing that will crop up, not as an upgrade warning, but as a program error during compiling if
we have Option Strict turned On (I always have this and Option Explicit turned On; more work for
us during development, but worth its weight in Hot Pockets if we want to have clear, concise code
that is always early-bound, and therefore runs much faster), is that if we work with Short integers
(former Integers in VB6) or Single precision floating point, we will often get compiler errors because
Short values are being promoted to Integer, and Singles are promoted to Doubles. This is especially
noticed when the receiving medium cannot accept these greater-precision values, such as method
parameters that are declared as Short or Single. Consider these examples:
Dim shValue As Short = 0
shValue += 1 'generates "Option Strict On disallows conversions from 'Integer' to 'Short'"
Dim sngValue As Single = 0.0
sngValue += 1.0 'generates "Option Strict On disallows conversions from 'Double' to 'Single'"

The reason the compiler errors occur is due to the fact that expressions cast their component parts to
their equal or most senior member. If we let the cursor hover over any of the literal values, we will
see why we run into problems. ‘0’ and ‘1’ are by default considered to be Integer values, and “0.0”
and “1.0” are by default considered Double values. The values seem to cast easily enough during
value-only assignment, but they run into problems when the result of an expression is assigned. We
will always run into this problem when working with these smaller types because the default types in
VB.NET are Integer for non-floating point values, and Double for floating point values.
The way around this is simple. Just append the offending integer literals with “S”, which will cast
them to type Short, and append “!” to the offending floating point literals to cast then to type Single.
More, we should also do the same during the assignments, even though there is no error reported,
because we are never really sure when the values are going to be demoted to the appropriate type. If
they are demoted during run-time, then that requires extra computer cycles to convert those values to
the proper type as a late-bound process. But even if the compiler automatically demoted them during
compilation so that their conversion was early-bound, it still does not clear up the problem for
someone reviewing our code and they see that the values are displayed in their promoted state.
 Page 79

Here are some more Literal Type Characters appending flags, so that we can ensure literal values are
what we would expect, or need them to be:
Value Type VB6 Symbol VB.NET Symbols VB.NET Examples
Char Not Available c String(" "c, 128)
Short Not Available S 123S
Integer % % or I 123% or 123I
Long & & or L 123& or 123L
Single ! ! or F (Float) 123.45! or 123.45F
Double # # or R (Real) 123.45# or 123.45R
Decimal Not Available @ or D 123.45@ or 123.45D

NOTE: It may seem redundant to use a ‘C’ tag because a single character of a string is a Char, it is actually an element
in an array of 1 of type Char. For example, “ Dim s As String = New String(" ", 128)” will generate the error
“Option Strict On disallows conversions from 'String' to 'Char'.” Use instead Chr(32) or ChrW(32) or CChar(" ") or " "c.
NOTE: If our Long, Integer, or Short values are Unsigned, then precede the character tag with a U, such as 123US for
Unsigned Short, 123UI for Unsigned Integer, or 123UL for Unsigned Long. Further, we can also cast values to their
unsigned version using CUShort, CUInt , and CULong. Also notice that a Byte is an unsigned value from 0-255. We can
cast a Signed Byte (SByte), storing values from -127 to +128 by using the CSByte casting function. Sadly, there is not a
Literal Type Character for Byte or Signed Byte, but this is normally not an issue of great concern.

● Notes on Accessing the Picture Property in Older Components.


Coping with not being able to copy a Picture property to an Image property is a big problem that has
cornered most developers migrating over to VB.NET. Sometimes an upgrade will involve using VB6
or VB5 COM components right within the VB.NET upgraded application, such as VB5’s Common
Control ComCtl32.dll, though the Upgrade Wizard will copy it locally and rename it
axInterop.ComCtlLib.dll (a lot of us used this library in VB6 because it allowed us to display our
windows using XP styles when we included a manifest file). There is nothing wrong with doing this,
and our application can even be stronger for it because additional resources, especially a huge
reserve like all the COM-based code generated from Visual Studio 6, equals superior strength.
However, I have also seen these controls used for importing image lists, and the problem with the VB5/6
ImageList control is that they only have a Picture property. That worked OK in VB5 and VB6, because a
PictureBox control has both an Image and a Picture property, and so we could assign the Imagelist’s
Picture property to the PictureBox’s matching Picture property like this: “ Me.PictureBox1.Picture =
Me.ImageList1.ListImages(4).Picture”. A VB.NET PictureBox control may have much more muscle, but
it is also leaner, having only an Image property (fixing the persistence problem eliminated the need for
two separate properties). And being that VB.NET does not support lightweight controls, the VB6 Image
control went away, but it had also become mostly dead weight now that a PictureBox was able to serve
both purposes. That is, except that an Image control could treat icon transparency colors as transparent.
PictureBoxes cannot (well, directly, anyway). I think Microsoft screwed up a little here. Still, there is a
way to get PictureBox controls to display Icons, complete with transparency fields: refer to the article,
”Emulating VB6 Image Control Features Under VB.NET” on page 243.
We now have an annoying problem: A VB.NET PictureBox lacks a Picture property. When we
upgrade our VB6 project, the line shown above is upgraded to “Me.PictureBox1.Image =
Me.ImageList1.ListImages(4).Picture”. It compiles fine. It might even get chatty and ask us to add
some references to the fore-mentioned ax-control, but at least it will do it for us automatically if we
authorize it. But when we try to run it, we are told “ Unable to cast COM object of type 'System.__ComObject'
to class type 'System.Drawing.Image'. Instances of types that represent COM components cannot be cast to types that do
not represent COM components; however they can be cast to interfaces as long as the underlying COM component
supports QueryInterface calls for the IID of the interface. ”
 Page 80

Yeah, yeah, whatever… I do happen to recall that a stdole.Picture is the same as a VB6 Picture, but
it was still COM. However, from my C++ days, I recalled that I converted images through their
Handle property. I threw the following module together to address situations when the VB5/6
ImageList contains Bitmaps and/or Icons, and we need to convert them to VB.NET-style Images.
Module modPicToImg
'Reference to .NET stdole required (If COM OLE Automation is referenced, stdole is ALREADY ref'ed at a deeper level)
'******************************************************************************************
' PicToImage: Convert VB6 Bitmap picture to VB.NET Image (Bitmap format)
'******************************************************************************************
Public Function PicToImage(ByVal picProperty As stdole.IPictureDisp) As System.Drawing.Image
Return System.Drawing.Image.FromHbitmap(CType(picProperty.Handle, IntPtr))
End Function

'******************************************************************************************
' IcnToImage: Convert VB6 Icon picture to VB.NET Image (Bitmap format)
'******************************************************************************************
Public Function IcnToImage(ByVal picProperty As stdole.IPictureDisp) As System.Drawing.Image
Return System.Drawing.Icon.FromHandle(CType(picProperty.Handle, IntPtr)).ToBitmap
End Function
End Module

NOTE: COM’s OLE Automation may have been auto-added during upgrades if our upgrade involved ImageList controls. Also,
stdole can be added to Visual Studio .NET through the free Visual Studio Tools for Office, available from Microsoft:
www.microsoft.com/downloads/details.aspx?familyid=54EB3A5A-0E52-40F9-A2D1-EECD7A092DCB&displaylang=en . However, before we add it, we might
check the project properties and see if stdole is already installed. Just go to the References, hit Add, and check under the .NET list for
stdole. It may have been installed through the Primary Interop Assemblies. We could even find 3 or 4 different instances of stdole
declared within the .NET Reference list. Do not worry about which one to pick – just choose one of them).

To employ these conversion functions, we can copy a bitmap/picture using:


Me.PictureBox1.Image = PicToImage(Me.AxImageList1.ListImages(1).Picture)

Or to copy an icon:
“Me.PictureBox1.Image = IcnToImage(Me.AxImageList1.ListImages(2).Picture)

This is fast, but if we need to process them even faster, we will have to work around this Picture
format conversion process by opening the VB5/6 ImageList control by selecting its form-top control
to reveal a little option expansion selector, then go into its ActiveX properties and make a note of the
image size on the General tab, and then on the Image tab, write down each image, index, key, and
tag, if any of the additional properties are set. Next, add a VB.NET ImageList to our form, and then
commence filling it with duplicate data. Mind you, the VB.NET ImageList is 0-based, as opposed to
the VB5/6 ImageList, which is 1-based, and so we will want to stuff a Dummy image into that zero
location so that our upgraded code will not have to otherwise be offset-adjusted. Afterward, delete
the now-unused VB5/6-control and rename our new ImageList control to that of the old one.

● Notes on the Loss of VB6 Control Arrays and How to Quickly Create Them.
If we are trying to get rid of VB6 Compatibility Library components so to reduce overall code size,
we may run into a few VB6 helper class control arrays, or we need to create new ones. For as much
as VB6 users wailed and mourned over the loss of Control Arrays when migrating to VB.NET, they
are sure E-Z to build from scratch. I will demonstrate two fast techniques to do so, giving us a
method and a function to make building the root framework of a Control List simple child's play,
using an Image Control List as an example.
When building control arrays under VB6, we could copy a control and paste numerous copies of it to
the form, giving them the same name, if we simply set the first form's Index to 0. The first had an
Index of 0, the next 1, the next 2, and so on. With them, we could use one event to service several
controls, access them as a collection in For...Each loops, and add new controls of its type at a whim.
When VB6 users upgraded to VB.NET, they discovered they could not use multiple controls with the
same name, because they are now class objects, which require unique encapsulated identities so to
guarantee their data safety, meaning that each control had to have different names and unique
 Page 81

instances of its objects. Though most users never bothered reading all that bothersome superfluous
stuff, like Microsoft upgrade reports, tips, recommendations, white papers, notification of significant
differences, and incompatibility alerts, all freely available from Microsoft, they still sat there in front
of their screens with jaws dropped so wide a flock of Canadian Geese could have nested there for the
winter, and then weakly, and later in angry wrath, seethed that “but...but, they didn't tell me!”
Even so, the upgraded objects are collected into a special VB6 Compatibility Library control array.
For example, suppose we have a form with lots of Image controls that shared the same name, and a
user made a selection by clicking one to activate an option. Suppose further that we have 18 such
images, lined up 6 by 3, all named Image1, and indexed from 0 to 17. When we bring the form up in
designer mode under VB.NET, we may notice, if we tinker around with the Properties list, we see
the Image1 controls are now a series of PictureBoxes named _Image1_0 through _Image1_17. We
will also notice a Image1 gearbox control below it on the form control’s ribbon.
But what if we want to create a Control Array from scratch, or get rid of that VB6 Upgrade-added
overhead and convert them to code we can at least understand?
You came to the right place. We are going to create a frameworked (meaning skeletal, but fully
functional) version of an Image Control Array using simple tech, fast operation, and almost no code.
The directions we take it after that are all up to us. We can also easily adapt this to other controls.
When first exploring solutions like this, we are advised to be tentative in our experiments, rather
than rush in with wild abandon, especially if we happen upon hesitant angels, who are milling
around, looking fearfully in the direction we are racing. Well, I make a backup and dive right in.
Sure, I screw up, or so it is rumored, because my trusty backup is always there to hide the evidence. I
have only 2 rules for software R & D: #1) Always backup code. #2) Do not forget to do #1.
If we look to the web, we will find many self-styled online “gurus” recommending methods that
actually end up being exercises in self-inflicted torture, which carry long prison sentences in some
states (I call them “goo-goos, though I am sure they are really decent, wonderful people, who we
might not even scramble desperately for our pepper spray if we met them in a dark alley ). And
hopefully because we still love our life, we were not desperate enough to do what they told us to do
in their blogs (and people accuse me of bloviating). The usual technique they proffer features a lot of
repetitive typing, like pages of it, to individually, manually, stuff an array with each PictureBox, and
then to individually, manually attach event handlers for each of them to event methods. Manually?
When I saw that, I had to check my calendar to see if it was April 1. It finally dawned on me that all
they were doing was just puppeting what they found in the code generated by the VB6 Upgrade
Wizard. Wow, I tremble in the presence of their genius.
Amateurs. This is not as complicated as these braniacs try making it out to be. They will eat our dust
as we rub on our geek and tech out, going for warp speed and, more importantly, much less typing.
The first thing we want to do, if we are using Upgraded VB6 code that is, is to make a note of all
events that associate with our Control Array. In our example, we have one named Image1, So scan
the code for events used by it. Searching for “Sub Image1_” or “Handles Image1.” works.
Once we have them noted, then delete the VB6 Compatibility Library’s Image1 control from the
form ribbon. Notice this will also remove the “ Handles Image1.Click” handler from the Image1_Click
event, and from any handlers associated with Image1, such as MouseMove. It will also remove a lot
of superfluous code from the application. I say good riddance; it was slowing us down.
For example, we may afterward find a “naked” Image1_Click event like this, or we can create it
from scratch if we just want to create a brand new Image Control Array:
Private Sub Image1_Click(ByVal sender As Object, ByVal e As EventArgs) 'change the name to suit your control.
 Page 82

Perfect! The same story will be for other events using Image1. Do not worry about the error we will
see noted at the start of each of them if we had upgraded code, beginning with “ Dim Index As Short.”
We will be fixing them faster than poop through a goose.
So, we go into our form’s code and create an array beneath the declaration of the form class, naming
it to match our previous Image Array name, which in our example's case is Image1:
Private Image1(0) As PictureBox 'Stock Car version of an Image Control Array. Form Load code below will resize this array.

In the Form_Load event, instead of adding pages of code, we are going to add just this, noting that
the highlighted portions indicate where we will need to change it to suit our control array:
For Each Ctl As Control In Me.Controls 'The highlights indicate changes for your controls.
If TypeOf Ctl Is PictureBox Then 'scan to search for members of our Image1 Control List.
Dim pic As PictureBox = DirectCast(Ctl, PictureBox) 'get a found PictureBox reference, to test it further.
If pic.Name.StartsWith("_Image1_") Then 'is it one of our image control images (test first 8)?
Dim Idx As Integer = pic.Name.SubString(8)) 'yes, so grab index value from the end of its name.
If Ubound(Image1) < Idx Then 'Do we need to resize our ImageList?
ReDim Preserve Image1(Idx) 'Yes, so make room for it.
End If
Image1(Idx) = pic 'Now save the new image to its reserved parking spot.
AddHandler pic.Click, AddressOf Image1_Click 'add a Click event handler as a welcome packet.
AddHandler pic.MouseMove, AddressOf Image1_MouseMove 'add a MouseMove event handler complements of management.
'...Other initialization code goes here...
End If
End If
Next

Finally, if we upgraded, at the very beginning of each event's code, we will notice some now-dead
(and error-tagged) code that was used to get an Index of type Short (though we can use Integer if
we wish) from the control: Dim Index As Short = Image1.GetIndex(sender). Simply replace this
error line with either of the following lines, copying it to all events that have this error:
Dim Index As Short = CShort(DirectCast(sender, PictureBox).Name.SubString(8)) 'Short Integer Version.
' Or...
Dim Index As Integer = CInt(DirectCast(sender, PictureBox).Name.SubString(8)) 'Integer Version.

This will grab the Index from the control name and assign it to Index, doing exactly what the
GetIndex method did. Indeed, we could write our own, like this Integer version:
Friend Function GetInage1Index(sender as Object) As Integer
If TypeOf sender Is PictureBox Then 'PictureBox?
With DirectCast(sender, PictureBox).Name 'do tests on name.
If .StartsWith("_Image1_") Then 'one of our image controls?
Return CInt(.SubString(8)) 'return its index if so.
End If
End If
Return -1 'otherwise report failure.
End Function

We now have a working frameworked Image Control Array!


Wrap it up with a dynamic PictureBox array, the above techniques, store a control container Name
for name testing, adding Add, Count, RemoveAt, and Item methods, add WithEvents linkage,
implement Dispose, and we would be on our way to creating our very own Control Array objects.
And that is all there is to it. This simple technique will solve most old or new control array issues
without a ton of extra code overhead. We can handle them just like the VB6 versions, whether
indexing or looping through them using For...Each. Use ReDim Preserve to expand the array to
instantiate additional members.
NOTES: We may instead prefer to add these controls to a strongly typed Generics List. We can define such a list as
.Private Image1 As New List(Of PictureBox) . We can then take advantage of all its properties and methods.
An alternative is to define a class that Implements Ienumerable that will allow us to declare the class as a Collection.
Thus, if the class was named clsImages, we could declare an instance of it as Public Image1 As New clsImages() .
Consider the following sample class. Feel free to add as many properties and methods as your application requires.
 Page 83

'******************************************************************
'clsImages Image Class for PictureBox collection.
' Create an instance like this:
' Public Image1 As New clsImages()
'******************************************************************
Class clsImages
Implements IEnumerable

'******************************************************************
'Image Storage for PictureBox Collection.
'******************************************************************
Private ImageList As New List(Of PictureBox)

'******************************************************************
' Add: Add a PictureBox to the Image List.
' Add any PictureBox event handlers before or after invoking this method.
'******************************************************************
Friend Sub Add(ByVal Image As PictureBox)
ImageList.Add(Image)
End Sub

'******************************************************************
' Count: Get the number of PictureBoxes added to the Image List.
'******************************************************************
Friend Function Count() As Integer
Return ImageList.Count
End Function

'******************************************************************
' Item: Get the index a PictureBox in the Image List by an index.
'******************************************************************
Friend Function Item(ByVal Index As Integer) As PictureBox
If Idx >= 0 AndAlso ImageList.Count > 0 AndAlso Idx < ImageList.Count Then
Return ImageList.Items(Index)
End If
Return Nothing
End Function

'******************************************************************
' RemoveAt: Remove a selected PictureBox image from the Image List by an Index.
'******************************************************************
Friend Function RemoveAt(ByVal Index As Integer) As Boolean
Try
If Idx >= 0 AndAlso ImageList.Count > 0 AndAlso Idx < ImageList.Count Then
ImageList.RemoveAt(Index)
Return True
End If
Catch
End Try
Return False
End Sub

'******************************************************************
' Clear: Clear the PictureBoxes from the Image List.
'******************************************************************
Friend Sub Clear()
ImageList.Clear
End Sub

'******************************************************************
'Support. Used by Intrinsic functions, like For...Each.
'******************************************************************
Overridable Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
Return ImageList.GetEnumerator
End Function

End Class
 Page 84

● Notes on the Upgraded MouseMove Parameter List.


During an upgrade to VB.NET, we will find the MouseMove event seems to be missing several very
convenient parameters that VB6 had provided. These parameters gave information on whether a
mouse button was pressed, if any of the Shift, Control, or Alt keys were pressed, and it also provided
cursor coordinates. However, if we were lucky enough to see an upgraded VB6 MouseMove event,
we will see that an upgrade provides this presumed missing information, even if we do not need
them, we will find them here. First, consider the following original VB6 event heading:
Private Sub Form1_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)

After this is upgraded to VB.NET, it will look similar to the following:


Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseMove
Dim Button As Short = e.Button \ &H100000
Dim Shift As Short = ModifierKeys \ &H10000 'NOTE: Pay particular attention to the event argument and ModifierKeys.
Dim X As Single = e.X
Dim Y As Single = e.Y

NOTE: The backslash operator performs integer division, discarding its Remainder, so no Float results.
Mind you, this is convenient, but most-times we will not need or use most of these provided values,
so we can just delete what is not required. However, they do reinforce to us how we can obtain them
in new VB.NET projects. As an aside, the expressions for Button and Shift should be cast to Short if
we have Option Strict set to On (CShort(e.Button \ &H100000) and CShort(ModifierKeys \ &H10000)).
Alternatively, the code runs even faster if we used type Integer instead, which would also eliminate
any need to recast them to anything.
Refer to the next noted topic for a VB.NET developer's perspective on using the MouseMove event.

● Notes on Getting MouseMove Event Information.


The parameters for mouse events, like MouseMove, have changed in VB.NET from what they were
in VB6. In VB6, we were provided with a somewhat excessively informative event header like this:
Private Sub Form1_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)

This header let us see if a mouse button is being held down by checking the Button parameter, if the
Shift, Ctrl, or Alt keys were being held down by checking the Shift parameter, and we could obtain
the local X and Y mouse coordinates from the like-named parameters.
But under VB.NET, the event header will look something like this:
Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) Handles MyBase.MouseMove

Many developers coming from VB6 will throw their hands up in frustration. However, this is where
we should really pay attention to the parameters we are provided. Typically, the event arguments are
just that, EventArgs, as in “ByVal e As EventArgs”. Yet, notice here that in this case the event arguments
are declared as “ByVal e As MouseEventArgs”. Like the Form Closing event, which has an Event
Argument defined as FormClosingEventArgs, allowing us to access special propertieslike the Cancel
and CloseReason properties, MouseEventArgs provides us with several useful properties: Button,
Clicks, Delta, Location, X, and Y. They are described in the following table (we do not need to
specify System.Windows.Forms with this because that namespace is already loaded in a form):
Property Description
Button Gets an enumerator of type System.Windows.Form.MouseButtons that indicates which mouse button was pressed.
Clicks Gets an integer value having the number of times the mouse button was pressed and released.
Delta Gets an integer value having a signed number of detents the mouse wheel has rotated (a detent is a single notch
“bumped” on the mouse wheel).
Location Returns a System.Drawing.Point structure containing the integer X and Y mouse location during the generating
mouse event, relative to the top-left corner of the control beneath it (the form in the above example’s case).
X Gets an integer pixel value having the x-coordinate of the mouse during the generating mouse event.
Y Gets an integer pixel value having the y-coordinate of the mouse during the generating mouse event.
 Page 85

The VB.NET MouseButtons enumeration is emulated below, compared to VB6 Button values:
Public Enum MouseButtons As Integer 'translation of VB6 Button parameter values.
None = 0 'VB6 value * &H100000.
Left = &H100000 'VB6 1.
Right = &H200000 'VB6 2.
Middle = &H400000 'VB6 4.
XButton1 = &H800000 'VB6 8. 'NOTE: These last 2 members specify special auxillary buttons that can be found
XButton2 = &H1000000 'VB6 16. ' on some Microsoft and LogiTech gaming mice and joysticks.
End Enum

Of course, we can cut to the chase and check the built-in enumerations, such as MouseButtons.Left
or MouseButtons.Right. But, if our pre-written re-usable code uses the old VB6 values of 0, 1, 2, 4,
8, and 16, we can insert the following line at the start of the event code to use them:
Dim Button As Integer = e.Button \ &H100000 'Note the Backslash for faster Integer Division.

The Location structure in the above table is unlike the simple POINTAPI structure we used under
VB6, containing only X and Y integer members, representing pixel coordinates, but is in fact an
abstracted class that also includes X and Y integer properties, comprising its entire data footprint.
We can do a lot with what looks to be just a simple VB.NET Point. What follows is a prototype list
of its many built-in properties and methods, though its data is identical to a VB6 POINTAPI:
Public Structure Point
Public Shared ReadOnly Empty As Point
Private x As Integer 'Note that these two fields are the only two fields defined in the Structure, which means you can
Private y As Integer 'actually use the simpler VB6 Point Structure or this one interchangably, even with Interop!!!
Public Sub New(ByVal x As Integer, ByVal y As Integer)
Public Sub New(ByVal sz As Size)
Public Sub New(ByVal dw As Integer)
<Browsable(False)> Public ReadOnly Property IsEmpty() As Boolean
Public Property X As Integer
Public Property Y As Integer
Public Shared Widening Operator CType(ByVal p As Point) As PointF
Public Shared Narrowing Operator CType(ByVal p As Point) As Size
Public Shared Operator +(ByVal pt As Point, ByVal sz As Size) As Point
Public Shared Operator -(ByVal pt As Point, ByVal sz As Size) As Point
Public Shared Operator =(ByVal left As Point, ByVal right As Point) As Boolean
Public Shared Operator <>(ByVal left As Point, ByVal right As Point) As Boolean
Public Shared Function Add(ByVal pt As Point, ByVal sz As Size) As Point
Public Shared Function Subtract(ByVal pt As Point, ByVal sz As Size) As Point
Public Shared Function Ceiling(ByVal value As PointF) As Point
Public Shared Function Truncate(ByVal value As PointF) As Point
Public Shared Function Round(ByVal value As PointF) As Point
Public Overrides Function Equals(ByVal obj As Object) As Boolean
Public Overrides Function GetHashCode() As Integer
Public Sub Offset(ByVal dx As Integer, ByVal dy As Integer)
Public Sub Offset(ByVal p As Point)
Public Overrides Function ToString() As String
Private Shared Function HIWORD(ByVal n As Integer) As Integer
Private Shared Function LOWORD(ByVal n As Integer) As Integer
Shared Sub New()
End Structure

The last piece of information we need to complete the features we expected from our VB6 event is
the old Shift parameter. There is no place for it in the MouseEventArgs class, nor should it really be
expected to be found there, because it has to do instead with the Keyboard. But it is still extremely
easy to gather. Simply add another line at the beginning of our event as follows:
Dim Shift As Integer = ModifierKeys \ &H10000

Consider the following subset of ModifierKeys values we can whip up:


Public Enum SpecialKeys As Integer
Shift = 1 '1 * &H10000
Control = 2 '2 * &H10000
Alt = 4 '4 * &H10000
End Enum
 Page 86

Of course, if we want to check for other keys, such as the function keys, Media buttons, Left Shift
Key (LShiftKey), Right Shift Key (RShiftKey), Caps, Caplock, etc., then we will want to examine the
ModifierKeys value unaltered. For example: “If ModifierKeys = Keys.BrowserHome OrElse ModifierKeys =
Keys.H Then”. Further, instead of worrying about defining a local Shift variable for VB6 compatibility,
we can instead simply test ModifierKeys against Keys.Shift, Keys.Control, or Keys.Alt.
Finally, be aware that we can alternatively check “My.Computer.Keyboard” for the Boolean result of its
properties “AltKeyDown”, “CtrlKeyDown”, “ShiftKeyDown”, “NumLock”, or “CapsLock”, as in:
If Not My.Computer.Keyboard.NumLock Then Debug.Print("NumLock key is NOT ON!")

● A Note on Changes to Remotely Firing Button Clicks.


The way that button clicks are forced has changed in VB.NET. Previously in VB6, we could force a
click event on a button or menu by setting its Value property to True. For example:
Me.cmdHelp.Value = True 'treat as forced button click.

The Value property no longer exists under VB.NET, nor does such a property have a logical place in
an object oriented world. With VB.NET, we employ the PerformClick method on the control:
Me.cmdHelp.PerformClick()'treat as forced button click.

● Notes on No AVI Animation Control in VB.NET and How to Add a Free One.
Many users complain that VB.NET does not have an animation control to play AVI files. Although
several online solutions exist, they normally require running the application with administrator
permissions because they involve adding OCX interfaces. However, there is an even easier way to
do this, and all without permission issues, or even running the AVI as a separate process. If we
presently have VB6 installed, or just the free VB6 SP6 Redistribution Pack (see below), this is very
easy to do (note that our application end-users will never need to go through this process at all).
First, if we have tried to install VB6 on a Windows system after Windows 2000, we will have been
told that there are known compatibility issues with this application. However, these issues are easy to
surmount. If we have not done so already, and we need it, go ahead and install VB6. Be sure to run
the installation as an Administrator (right-click the Setup and choose Run as Administrator). It will
report an error, but just ignore it (it could not register an older OLEAUT32.DLL, but that is a good
thing!), and even tell the operating system, if it asks us, that it installed OK. The trick to getting past
the incompatibilities issue is to right-click the installed VB6.EXE application, or any shortcut to it,
select Properties, select the Compatibility tab, and then place a checkmark in the “Run this program
as an administrator” checkbox. Afterward, any time we launch VB6, we will have to hit the “Allow”
option in a User Account Control dialog, but this is a small price to pay for unfettered, unrestricted
access to the VB6 IDE, especially if we support source code that compiles under it.
Some systems, such as those mentioned above, will prevent we from installing Service Pack 6 for
Visual Basic 6.0, http://www.microsoft.com/downloads/details.aspx?familyid=9EF9BF70-DFE1-42A1-A4C8-
39718C7E381D&displaylang=en, due to incompatibilities. Even if we do not install VB6, we should
still be sure to install the Runtime Distribution Pack for Service Pack 6 for Visual Basic 6.0,
http://www.microsoft.com/downloads/details.aspx?FamilyId=7B9BA261-7A9C-43E7-9117-F673077FFB3C&displaylang=en . We are allowed to
do this even if we no longer own VB6. If we do have VB6 installed and Visual Basic 6.0 Service
Pack 6 would not run or install, install the Microsoft Visual Basic 6.0 Service Pack 6 Cumulative
Update, http://www.microsoft.com/download/en/details.aspx?amp;displaylang=en&id=7030 ). This installation may still
inform us that it might not have installed correctly, even though it actually did (this alert is in fact
due to a problem it has trying to register OLEAUT32.DLL, which is already installed and
registered), so select the “This program installed correctly” option if this message prompt comes up.
If we are unsure about this, install VB6 in Safe Mode (Select Start / Run (or -R), then enter
msconfig, select the Boot tab, and select Safe Boot). We will have to undo this in Safe Mode to
 Page 87

reboot normally. However, it will still report an error, but this time it will clearly inform we that it
could not register OLEAUT32.DLL (it is because of heightened system protections that have been
added that prevent this DLL file from being tampered with, and especially by an older version of it,
which the VB6 version most certainly is).
What is important for our main point, which is installing an animation control, is that the COM
support library that provides this VB6 control is now loaded onto our computer and registered. But
once it is loaded into our system, we are able to add an animation control to our Toolbox.
To add the VB6 animation control to our Toolbox, and an optional new COM toolbox tab to contain
it, do the following:
1. With any, or a new VB.NET Window Application form up in the IDE, select the Toolbox tab.
2. Right-click one of the Toolbox tabs and choose “Add Tab”. A new Tab will be added to the Toolbox, and it will wait
for we to name it (this might not at first seem apparent, but notice a new blank tab with a cursor blinking in its
heading). Name it “COM”, or to our own personal preference. And lock in the change by hitting Enter.
3. Next, right-click the new COM Tab and select “Choose Items…” from the popup menu. After a rather long pause to
gather its massive reserve of available system resources, a “Choose Toolbox Items” dialog window appears.
4. Click the “COM Components” tab on the dialog window (there will be another delay to format its list).
5. Locate “Microsoft Animation Control 6.0 (SP6)” in the displayed list and place a check in its checkbox.
6. Select OK to apply the selection and close the dialog window.
Notice that we now have an animation control in our new COM Toolbox Tab, which we can use just
as we had done with VB6. Notice further that when we place an animation control on a form, the
properties for the control report that it is of type AxMSComCtl2.AxAnimation. (Ax represents
ActiveX) We will also notice that once we add an animation control to a form, that two new
references are added to our project properties, both for “Microsoft Windows Common Controls-2
6.0 (SP6)”; one of them for Interop.MSComCtl2.dll, and another for AxInterop.MSComCtl2.dll,
which are required to support the animation control. Notice further that their “Copy Local”
properties are both set to True, meaning that these will be, and must be loaded into our program
directory, being non-registration versions of the ActiveX MSComCtl2 registered controls (the system
will copy them automatically for us).
Developing an easy solution was like a mission for me. I first tried to design an easy code-only
solution, but I quickly came to the conclusion that adding it programmatically was a wrong direction
because there were too many steps involved, and it still depended on a DLL file being installed and
accessible, Once we have the DLL, it might be simple enough to do it in-code,, but it is also not
something that is going to be sitting at the top of my head every time I need to use it. On the other
hand, a toolbox control is both easy to find and even easier to apply to a form than it is to go about
writing a bunch of set-up and placement code. So I say, let the background form designer do all of
that programming for us.
My only real question to Microsoft is why they could not have added a native animation control to
VB.NET long before now? I would think it essential, even for the release of VB2002. AVI animation
might be a COM process, but a .NET-compliant version should not be out of the question. After all,
most other VB.NET controls started out under VB6 as COM controls.
 Page 88

● Notes on Changes to Resources Management.


Hoards of VB6 users have grumbled loudly about the resource storage format used under .NET,
though those complaints are mainly for the fact that .NET resources are stored differently than they
were for VB6 (Win32). As a counterpoint, I firmly believe that they are stored better. What is more,
two very annoying things have also been totally eliminated under .NET that drove me to frothing
distraction under VB6: 1) With .NET, we no longer need to separately compile the resources (or
figure out what to type into the resource compiler source file) as we had to do under VB6, and 2)
.NET resources can be used immediately, even during development, such as sound, which did not
work from the IDE at all under VB6. Just load and go. We can also do a whole lot more with them,
particularly if we write multi-cultural apps, especially with the ResourceManager namespace.

Audio Resources.
Probably the biggest complaint I hear is that embedded resources are supposedly more difficult to
access under VB.NET. But from how I see it, I know that this process is a whole lot easier, though I
think it really has something to do with their belief that they are only able to play audio files through
the PlaySound Pinvoke in WINMM.DLL, which cannot access the .NET-embedded resources
(though its File I/O part will still work OK). My advice to those people is simple – abandon the
PlaySound Pinvoke entirely! Instead, use .NET’s My.Computer.Audio class methods, which are
much easier to use than a Pinvoke. And, considering how easy it is to now add and access those
embedded resources, I am glad for it, especially because I can now play those resources, extract
them, check them, check for their existence, etc., all without giving them a great deal of thought.
For example, what follows is typical VB6 Pinvoke code I had written for playing sounds:
Private Declare Function PlaySound Lib "winmm.dll" Alias "PlaySoundA" (ByVal lpszName As String,
ByVal hModule As Integer,
ByVal dwFlags As Integer) As Integer
Private Const SND_FILENAME As Integer = &H20000
Private Const SND_RESOURCE As Integer = &H40004
Private Const SND_SYNC As Integer = &H0
Private Const SND_ASYNC As Integer = &H1
Private Const SND_NODEFAULT As Integer = &H2
Private Const SND_LOOP As Integer = &H8
Private Const SND_NOWAIT As Integer = &H2000
'*******************************************************************************
' PlayWavFile(): Play sound from a file.
'*******************************************************************************
Public Function PlayWavFile(ByVal FileName As String, _
Optional ByVal PlayAsync As Boolean = False, _
Optional ByVal PlayLoop As Boolean = False, _
Optional ByVal NoWait As Boolean = False) As Integer
Dim flags As Integer

If PlayAsync Then
flags = SND_FILENAME Or SND_SYNC Or SND_NODEFAULT
Else
flags = SND_FILENAME Or SND_ASYNC Or SND_NODEFAULT
End If
If NoWait Then flags = flags Or SND_NOWAIT 'check for NoWait flag.
If PlayLoop Then flags = flags Or SND_LOOP 'check for continuous play.

PlayWavFile = Cint(PlaySound(FileName, 0, flags)) 'play file.


End Function
'*******************************************************************************
' PlayWavResource(): Play sound from a resource file.
'*******************************************************************************
Public Function PlayWavResource(ByVal SoundName As String, _
Optional ByVal PlayAsync As Boolean = False, _
Optional ByVal PlayLoop As Boolean = False, _
Optional ByVal NoWait As Boolean = False) As Integer
Dim flags As Integer = 0

If PlayAsync Then
flags = SND_RESOURCE Or SND_SYNC Or SND_NODEFAULT
Else
flags = SND_RESOURCE Or SND_ASYNC Or SND_NODEFAULT
End If
If NoWait Then flags = flags Or SND_NOWAIT 'check for NoWait flag.
If PlayLoop Then flags = flags Or SND_LOOP 'check for continuous play.
PlayWavResource = PlaySound(SoundName, GetHInstance(), flags)
End Function
 Page 89

'*******************************************************************************
' PlayWavStop(): stop playing any sounds that might be playing.
'*******************************************************************************
Public Sub PlayWavStop()
Call PlaySound(vbNullString, 0, 0)
End Sub

This was a tried and true module that served me well for about a decade, both in VB6 and in C++
(with my C++ version of it). However, with the introduction of the .NET Framework, the choice of
options available to us has expanded substantially, rendering API invocations totally unnecessary.
In VB.NET, from the My.Computer namespace, we now have a LOT of computer control only as far
away as our fingertips. From the My.Computer.Audio namespace, we can easily play WAV files,
stop them, or issue system sounds (the message beeps, such as alerts, warnings, etc.). From
My.Resources, we have full access to all of our embedded resources. From
My.Resources.ResourceManager, we can poke and prod and check our resource data to no end.
One loud complaint I have heard regarding this is that many programmers cannot seem to use a
string name to get access to an audio resource, so they have resorted to using a Select block to check
a text name against a list, then play the associated embedded WAV resource. Well, I can understand
that one. The very first day I started playing with VB.NET resources, I thought the same. But being a
software engineer, I knew that it could not be implemented so poorly. With all that raw power at my
fingertips, Microsoft was going to just settle on some weak solution like that? So I took a look at the
resource manager and between experiments (and stopping to actually read documentation), I quickly
figured it all out. By simply paying attention to the popup tooltips, I learned that by providing the
resource manager’s GetObject method with the text name of the desired resource, it would return an
object that I could cast into the appropriate type (byte array, stream, or string), which comprises my
audio data (byte array if from VB6, or stream if VB.NET-added). I could then take advantage of the
Play method in My.Computer.Audio and play that resource, or from a file path with equal ease.
Following is my new VB.NET version of the previous VB6 PlayWav routines:
'*******************************************************************************
' PlayWavFile(): VB.NET Play sound from a file.
'*******************************************************************************
Public Function PlayWavFile(ByVal FileName As String,
Optional ByVal PlayAsync As Boolean = False,
Optional ByVal PlayLoop As Boolean = False,
Optional ByVal NoWait As Boolean = False) As Boolean
' see if the audio file exists.
If Not System.IO.File.Exists(FileName) Then Return False 'nope; error.
' process our flags.
Dim flags As AudioPlayMode = AudioPlayMode.Background
If Not NoWait Then flags = AudioPlayMode.WaitToComplete
If PlayLoop Then flags = AudioPlayMode.BackgroundLoop

My.Computer.Audio.Play(FileName, flags) 'play audio file.


Return True 'success.
End Function

'*******************************************************************************
' PlayWavResource(): VB.NET Play sound from a resource file.
'*******************************************************************************
Public Function PlayWavResource(ByVal SoundName As String,
Optional ByVal PlayAsync As Boolean = False,
Optional ByVal PlayLoop As Boolean = False,
Optional ByVal NoWait As Boolean = False) As Boolean
' get data for sound from resource.
Dim obj As Object = My.Resources.ResourceManager.GetObject(SoundName)
If obj Is Nothing Then Return False 'bad name, so return error.
' process our flags.
Dim flags As AudioPlayMode = AudioPlayMode.Background
If Not NoWait Then flags = AudioPlayMode.WaitToComplete
If PlayLoop Then flags = AudioPlayMode.BackgroundLoop
' process file
If TypeOf obj Is System.Array Then 'byte array (usually upgraded VB6 files).
My.Computer.Audio.Play(DirectCast(obj, Byte()), flags)
Return True 'all is good, so return success.
End If
If TypeOf obj Is System.IO.Stream Then 'typical VB.NET resource file.
My.Computer.Audio.Play(DirectCast(obj, System.IO.Stream), flags)
Return True 'all is good, so return success.
 Page 90

End If
If TypeOf obj Is String Then 'typical external wave file.
Return PlayWavFile(DirectCast(obj, String), PlayAsync, PlayLoop, NoWait)
End If
Return False
End Function

'*******************************************************************************
' PlayWavStop(): VB.NET stop playing any sounds that might be playing.
'*******************************************************************************
Public Sub PlayWavStop()
My.Computer.Audio.Stop()
End Sub

Now this is definitely easy. True, I was initially disappointed that the PlaySound Pinvoke no longer
worked with .NET resource files, but what I got in trade has rendered the PlaySound Pinvoke moot.

AVI Resources.
AVI files are a source of frustration with VB6 users and VB.NET users alike. In VB6, it was a bit of
a bother. To store the file FileCopy.avi in a VB6 resource file we had to:
1. Add an entry, such as “101 CUSTOM "FileCopy.avi"” to a resource text file, such as myResources.rc.
2. Compile the resource from a DOS prompt, using something like “rc myResources”.
3. Finally, add the resulting myResources.RES file via Projects / Add File to our VB6 application.
Why many VB6 users claim that this resource-adding hack is easier than adding resources under
VB.NET can only be due to insanity. To add such a resource under VB.NET, all I need to do is go to
Project / Properties, select the Resources tab, set the resource type to Files, hit the Add Resource button,
and navigate the browser to and select my FileCopy.avi file, and it is added to my resources. Close the
project properties and it is automatically compiled and associated to my application.
Yet reading and playing the AVI resource file can be frustrating, because I have heard all sorts of tales of
woe about how someone cannot figure out how to play it, or how they cannot convert the resource to a
string so they can use a StreamWriter to save it as a file, and various variations of this dilemma.
Unlike Audio files, which can be played directly from the resources, an AVI must currently be played
from a file (actually, there is a way to play AVI files from a VB6 resource file by using something like
“Lresult = SendMessage(Animation1.hwnd, ACM_OPEN, ByVal App.hInstance, ByVal aviResourceID) ”, where
ACM_OPEN is declared as WM_USER (&H400) + 100&). I was hoping that the new .NET architecture
would enable the Animation control to read byte arrays (byte arrays exhibiting read/write/seek features,
which is a byte array wrapped within a Stream interface) or streams directly, but sadly has yet to happen.
Therefore, our first task is to copy the resource data to a file. I will assume that we have the
FileCopy.avi file loaded in our resources. When it is added, it will automatically be saved in the
resources by its file-defined name, such as FileCopy (we can alter this by renaming the resource). I
like to play it as a file from wherever the executable program is running from, so I will do that,
though we may require a different non-locked local resource, such as a temp folder on the user’s
system if the application runs from a CD or DVD or some other similar write-protected source.
Also, the properties box reports that FileCopy has a type of System.Byte[], which is saying that it is a
Byte Array (C uses square braces to denote arrays). As such, instead of trying to convert the
resource to a string, I will treat it as a byte array and use a FileStream object so that I can write a
binary file. With VB6, all we had to do was issue the command “LoadResource 101, AviFile”, where
AviFile is a string specifying the file path to save the ID 101 resource as a file; if we had ‘named’ our
AVI resource 101 (see the above 3-step VB6 resource compiling process). Now consider the
following VB.NET code:
 Page 91

' set up the local variable to hold the path to our FILECOPY.avi file.
Dim AviFile As String = Application.StartupPath & "\FILECOPY.avi"
' first see if it already exists. If so, then we will not have to create it...
' if the AVI file does not already exist...
If Not System.IO.File.Exists(AviFile) Then
' set up our FileStream object to create it.
Dim fs As New System.IO.FileStream(AviFile, System.IO.FileMode.Create)
' write byte array resource directly from resource to file from its beginning (0) for its length.
fs.Write(My.Resources.FILECOPY, 0, My.Resources.FILECOPY.Length)
' close the new local AVI file copy of the resource.
fs.Close() 'this automatically invokes fs.Dispose(True) to release all used resources.
End If

The only thing left for us to do is to play it in our Animation control:


With Me.Animation1 'start animation...
.AutoPlay = True 'turn animation on.
.Open(AviFile) 'load the animation file.
.Visible = True 'show the animation AVI.
Application.DoEvents() 'let display catch up.
'
' ... Do application processes, then finally...
'
.Close() 'close AVI animation.
.AutoPlay = False 'turn off AVI animation.
.Visible = False 'and hide it.
End With

A final thing we may want to do when we are leaving our application is to perhaps delete the AVI
file, although keeping it present will mean future runs will not have to start off saving another copy
to a file, but, because I like to clean house afterward, I normally do this in my application exit code:
System.IO.File.Delete(AviFile) 'delete the avi file resource.

Other standard resources we can store are Icons, Images, and String. Other than the auxiliary Files,
which I prefer to use, we may like to use Other, which is much the same as Files, but it is a way to
separately catalog our various data. We can even set the access modifiers of our resources as Public
or Friend (default).

● A Note on Improved Collection and List Clearing.


Be aware that with all the new features and muscle available to collections and lists, such as the
various new forms of the Collection object, ListBox, and ComboBox, that a new method, Clear, is
now available that makes clearing the list out a snap, and fast. Previously, a loop had to be employed
to clear out a Collection or ListBox or ComboBox, such as is shown in this VB6 sample:
With colHistory 'Quickly purge the History Collection...
Do While CBool(.Count) 'While entries exist in the collection.
.Remove 1 'remove the entries (standard collections use a 1-based list).
Loop 'loop until all entries are purged (Removing from 1 rather than .Count is =MUCH= faster).
End With

The above method is extremely fast; beat by only a few seconds in gigantic, memory-filling
collections by sending the handle of the Listbox the LB_DELETESTRING message (&H182), sent
via the SendMessage Pinvoke. This all can now be accomplished, even in gargantuan collections
with this simple and even faster command:
colHistory.Clear() 'More quickly purge the History Collection in VB.NET.

NOTE: If we use a Generics collection, which also features the Clear method, we can alternatively use the Remove
method to remove entries by a Key, but we will have to use the RemoveAt method to remove indexed entries.
 Page 92

● Notes on Adding Auxiliary Files to ClickOnce Deployment (Publish).


A lot of VB6 users liked to use the Package & Deployment Wizard to build simple installs for their
projects. However, a lot of people have complained that they cannot seem to add auxiliary files to a
ClickOnce Deployment (Publish) under VB.NET. For all the frustration and chatter I have seen on the
web regarding this, the solution is almost too easy. First, we must be sure that our auxiliary files are
actually included in our project. Second, we must be sure that their Build Action is set to other than
None. Third, we must be sure that our file will be written out to the installation location.
Just because we see our file in the Solution Explorer, this does not mean
that the file is also included in our project. It may simply be a
consequence of the file existing in our project folder, because maybe we
copied it to that location. If we notice that the icon for the needed file
seems a bit faded, or that a Build property is not shown in the file’s
attributes when we click on it in the Solution Explorer, or if when we
right-click it and we notice an “Include in project” option is enabled in
the pop-up menu, we can bet that the file is not a part of our build.
If it is not currently included in our project, simply right-click it and
select “Include in Project”. Or, if it is located elsewhere, select
Project from the menu, then select Add Existing Item.
The second step is to address the Build issue. One at a time, click on
a now-included file and be sure the Build Action property is set to
Content, meaning that the file will be part of the project’s content.

We may notice the build actions for other files. Files that are already
embedded in the final executable, such as resource objects, normally have a build action of None.
Modules, classes, and forms have build actions of Compile.

We will also want to check the “Copy to Output Directory”


property while we are here. If it reports “Do not copy”,
then it will not be sent to the destination folder. We may
want to change this to “Copy always” or “Copy if newer”
(copy if it is newer than an existing file of that name, or
copy it if it does not yet exist).

Another thing we may want to do is to determine how it


should be supplied. If we go to the project properties and
select the PUBLISH tab, we will notice a button named
“Application Files”. Select it and a list of application
files that will be included in the publication will be listed.

Depending on how we set the Target Framework version under the


Advanced Compile Options (see the Compile tab in the Project
Properties under the Advanced Compile Options button), we may want
to also check the Prerequisites list. For example, if we compiled our
application to use .NET Framework 4.6, we may want to be sure that the
person installing our application has the ability to load .NET Framework
4.6 if it is not already installed. In the Prerequisites list (see the
Prerequisites button on the Publish tab). We can have the prerequisites
loaded from the vendor website (Microsoft), or from the same location
as our application (the prerequisites will be bundled right within our
published installation), or we can specify the URL of a web address where they can be downloaded. If we are
 Page 93

publishing our application for web distribution, it is usually easier to have prerequisites downloaded from the
vendor. If we are building a CD/DVD installation, it is probably best to provide them ourselves on that media.

Finally, once we have gone through the Publishing Wizard at least once and made our few choices
(where to publish to, how the app will be installed, if updates can/should be checked, and from
where), we can usually process subsequent builds by simply choosing the Publisher’s Finish button.

● A Short Note on Changes to VB6 TextBox SelStart and SelLen Properties.


This is a very minor change. The VB6 SelStart and SelLen properties of TextBox and RichTextBox
controls has become SelectionStart and SelectionLength under VB.NET.

● Notes on Welcomed Changes to ToolTips.


Tooltips have changed between VB6 and VB.NET. The VB6 ToolTipText property is no longer
supported as it was. It is now handled by a much more memory-efficient ToolTip control. The
purpose of the control is to help speed the execution of code. Having this support code packed into
each and every control, even on controls we did not use tooltips on, ate a lot of time and memory
resources. Fortunately, it translates very well during an automatic upgrade from VB6 to VB.NET.
By simply dropping a ToolTip control onto a VB.NET form, tooltips are suddenly reflected to the
other controls on the form, exposing a ToolTip property on each. For example, if we drop a fresh
ToolTip control onto a form, it is named Tooltip1 by default, and a property that is not actually part
of a form's controls is exposed on each of them as “Tooltip on Tooltip1”. We can pre-fill the text of
these tips at design time through the property interface just as we had done it in VB6. The ToolTip
control accomplishes this through a process called Reflection, which is a fancy way of saying that it
has inserted itself into the message stream and monitors objects the cursor hovers over, and checks to
see if it is in its list of objects that have been attached to it. If so, it submits the assigned tooltip.
However, on top of it all, we now have a much more powerful control. We can even drop more than
one ToolTip control onto a form, which in turn exposes more than one ToolTip property. This may
seem like a silly thing to do, but due to our now being able to customize the presentation of our
tooltips, we can assign one set and style of ToolTips to certain controls, and assign another set and
style to others (we would not really want to add more than one tool tip text to any one control,
anyway). These customizations feature the ability to enable a balloon-type format, changing
foreground and background colors, modify display timing, to include being always on, setting
emboldened titles for the ToolTip windows, create owner-drawn ToolTips (user-drawing is enabled
through the ToolTip control’s Draw and PopUp Events), plus many other features.
By the way, with multiple ToolTip styles defined for a form, we would simply set ToolTip text to the
ToolTip control interface that we want to have displayed (ToolTip on ToolTip1, ToolTip on ToolTip2,
etc.), leaving any others blank (blank ToolTip text disables that ToolTip interface).
In VB6 program code, we would set a ToolTip through a control’s ToolTipText property, like so:
Me.cmdContinue.ToolTipText = "Continue processing" 'continue processing with this button.

With VB.NET, this command could be expressed as:


Me.ToolTip1.SetToolTip(Me.cmdContinue, "Continue processing") 'continue processing with this button.

To retrieve ToolTip text, in VB6 we would use a command such as this:


Dim ttText As String
ttText = Me.cmdContinue.ToolTipText

In VB.NET, the above could be implemented as:


Dim ttText As String = Me.ToolTip1.GetToolTip(Me.cmdContinue)
 Page 94

● A Note on Changes to ListView.


The VB6 ListView control was 1-based. In VB.NET, it is 0-based, and we must adjust indexing
accordingly (this includes icon indexes). Like with other list controls, the VB6 ListView’s ListItems
collection has become a uniform Items collection. Also be aware that the slow Clear method offered
in VB6 has been replaced in VB.NET with an updated and faster Clear method. Under VB6, it had
been several times faster to actually loop through the list and perform a Remove(1) statement as long
as the Count property was non-zero than it was to use its Clear method (in VB.NET, this would need
to be upgraded to a RemoveAt(0) statement). Also, the Item property of the Items list (formerly
ListItems in VB6) is no longer a String property, but is rather an Object. However, if we still store
string data in this list, then use the Item's ToString method to access it. If we store class objects
there, ensure the class features a ToString method so the ListView can display a text representation.

● Notes on Handling Toolbar Button and Button Menu Clicks.


When a program using the Toolbar control is upgraded from VB6 to VB.NET, a couple things
happen. First, ToolBars, though supported by VB.NET for backward compatibility, are automatically
converted during an upgrade to the newer and more capable ToolStrip controls. Second, both normal
and special dropdown button list click events must be handled differently with ToolStrips. These
issues are quite easy to adapt to and resolve programmatically, but we really should understand what
is going on by examining the differences between the aging ToolBar and the newer ToolStrip, which
are both featured in VB6 and VB.NET. Also, even though
continuing to implement the old ToolBar control might seem to be
a more attractive choice from the perspective of a person upgrading
from VB6 to VB.NET, we cannot just drop a ToolBar control onto
a form from a fresh VB.NET install. The VB.NET Toolbox initially
offers only a ToolStrip control. Even so, we are able to add a
ToolBar control to the Toolbox, if we cannot seem to live without
it, through its Choose Items option (right-click a desired Toolbox
group header to access this option, and then in the dialog box select the Toolbar entry declared in
the .NET tab’s Global Assembly Cache directory).
When a normal button is clicked on a ToolBar under VB6, its ButtonClick event will provide us with
a Button parameter that represents the clicked ToolBar button. If our VB6 processing code will
identify these buttons by the key we assigned to it during development, we access it through its Key
property, if we process them by their assigned toolbar index (VB6 indexes start at 1), we use the
Button object’s Index property, or finally, if we process them by their assigned text, we should use a
Button’s Caption property. For example:
Private Sub Toolbar1_ButtonClick(ByVal Button As MSComctlLib.Button)
Select Case Button.Key 'Process by Key (use Button.Index for the button's 1-based index).
Case "Exit" 'if Exit button, then simply unload the main form.
Unload(Me)
...
End Select
End Sub

When developing code for VB.NET’s ToolStrip buttons, this process is different. However, it is still
very easy to use. By default, VB.NET generates a separate click event for each button. For example:
Private Sub ToolStrip1_tsiExit_Click(ByVal sender As Object, ByVal e As EventArgs) Handles ToolStrip1_tsiExit.Click
Me.Close() 'if Exit button, then simply unload the form (replaces VB6 Unload(Me)).
Me.Dispose() 'add this line if the form was opened as a Dialog.
End Sub

However, we can consolidate all button code by creating event code named something like
ToolStrip1_ButtonClick and add a list of button event handlers at its end. Additionally, be sure to
also declare a Button object of type ToolStripItem in order to access each selected button, like so:
 Page 95

Private Sub ToolStrip1_ButtonClick(ByVal sender As Object, ByVal e As EventArgs)


Handles ToolStrip1_tsiOpen.Click,
ToolStrip1_tsiClose.Click,
ToolStrip1_tsiOptions.Click,
ToolStrip1_tsiExit.Click
' Define Button as a ToolStripItem object.
Dim Button As ToolStripItem = DirectCast(sender, ToolStripItem)

Select Case Button.Name 'Process by the button's Name (no Key available. Use Text if the
'ImageStyle does not display the Text field, otherwise try the Tag field).
Case "tsiExit" 'if Exit button, then simply unload the main form.
Me.Close() 'Close the form.
Me.Dispose() 'add this line if the form was opened as a Dialog.
'...other handling code goes here...
End Select
End Sub

The above example generally covers how a VB6 to VB.NET upgrade also handles it, excepting that
the original upgraded code will retain the VB6 “ Unload(Me)” command and mark it as unsupported
and request editing. I simply replace it with “Me.Close”, and then add “Me.Dispose” if the form was
handled as a dialog (though adding it even if it was not invoked as a dialog does no harm).
NOTE: During an upgrade from VB6, the upgrade automatically generates the code to declare the Button object.
However, it will replace the original “ Button.Key” with “Button.Name”, even though the VB6 button Name and Key
values were seldom, if ever identical. This can be a potential problem because it will not provide an upgrade note
informing us of this. So be aware that this will happen. I like to take the time to assign the upgraded button’s Tag
property to reflect the old VB6 Key property, or, as I have done in the above snippet, simply update the test for the button
name. Be aware that we could have also originally tested against the button’s Name property under VB6, in which case
the code would have upgraded more smoothly.
NOTE: See the next note,“A Note on the Unload Form Command” on page 96 for an explanation of why the VB6
Unload command is not removed during an upgrade,
However, when developing the code under VB.NET, we can alternatively collectively process these
clicks through the ToolStrip itself in a single method, and from its click event we can extract the
button information. Remember also that VB.NET button indexes begin at 0, not 1. For example:
Private Sub ToolStrip1_ItemClicked(ByVal sender As Object, ByVal e As ToolStripItemClickedEventArgs)
Handles ToolStrip1.ItemClicked
' The following is how you would get the clicked Button's Name property from this ToolStrip event:
Dim ButtonName As String = e.ClickedItem.Name 'notice the type assigned to 'e', above.
' The following is how you would get the Button's definition index (from zero):
Select Case ToolStrip1.Items.IndexOf(e.ClickedItem) 'get the button's index in the tool bar items list.
Case 3 'if Exit Button, then simply unload the form (4th button option, indexed 0-3).
Me.Close() 'Close the form
Me.Dispose() 'add this line if the form was opened as a Dialog.
'...other handling code goes here...
End Select
End Sub

Button Menus (VB6 ToolBar Button style 5; tbrDropdown) are menus that drop down from toolbar
buttons and offer a list of items to select from (we will notice a dropdown indicator on the right side
of the button (▼)). Under VB6, such selections could be handled through the ButtonMenuClick
event, and we could acquire the index of the item in the button’s dropdown list from the menu
button’s Index property, regardless of whether we predefined the dropdown button list or
dynamically added them within the application code. Consider the following VB6 example:
Private Sub Toolbar1_ButtonMenuClick(ByVal ButtonMenu As MSComctlLib.ButtonMenu)
Call ShowListItem(ButtonMenu.Index) 'process according to the index value of the button.
End Sub

But when we process a pre-defined dropdown button in VB.NET from a ToolStrip (style
ToolStripDropDownButton), event handling takes two different approaches. First, it can provide an
individual event code block for each dropdown button we care to declare code for, like this:
Private Sub Option1ToolStripMenuItem_Click(ByVal sender As Object, ByVal e As EventArgs)
Handles Option1ToolStripMenuItem.Click
'Handle code for predefined dropdown button with displayed text "Option 1".
End Sub
 Page 96

Of course, the above is only practical when we pre-define dropdown button values in VB.NET. If we
add them dynamically by adding entries to a DropDown button, we named, say, tsddbOptions,
through its DropDownItems collection, such as we might have done in the following example:
With Me.tsddbOptions.DropDownItems 'Add options to ToolStrip Drop-Down Button tsddbOptions.
.Clear 'clear the list if needed.
.Add("Option 1") 'index 0.
.Add("Option 2") 'index 1.
.Add("Option 3") 'index 2.
.Add("Option 4") 'index 3.
End With

We could handle selections to those dynamically added buttons, as well as through any that had
already been predefined at development time through the dropdown button’s
DropDownItemClicked event, much like the following:
Private Sub tsddbOptions_DropDownItemClicked(ByVal sender As Object, ByVal e As ToolStripItemClickedEventArgs)
Handles tsddbOptions.DropDownItemClicked
' The following is how you would acquire the dropdown Button's Text property (in lieu of a VB6 Key):
Dim KeyName As String = e.ClickedItem.Text 'this is typically practical, because the text data is defined.
' The following is how you would acquire the Button's index (from zero):
Select Case Me.tsddbOptions.DropDownItems.IndexOf(e.ClickedItem) 'grab the option index in the drop down list.
Case 3 'Option 4 (4th of 4 options).
'You MUST do the following command IF you are closing a form, otherwise an exception error will occur.
Me.tsddbOptions.HideDropDown() 'Close list, so exception will not fire thru the following Me.Close().
Me.Close() 'Close the form.
Me.Dispose() 'add this line if the form was opened as a Dialog.
Exit Sub
...
End Select
End Sub

NOTE: Please notice the “HideDropDown” invocation in the above code. If the dropdown is still open during a form
close, an exception will occur because the closing form will destroy form resources, such as the dropdown menu object,
which will still have code running as a background process, to handle collapsing the dropdown. Forcing the closing of
the button dropdown menu will detach that background process, which allows a clean removal of resources.
NOTE: By default, some of the above examples for the “e” parameter will have their types preceded by
“System.Windows.Forms.”, but this additional tagging is not really necessary, because that namespace has already
been loaded into the current Form’s code block, but it would be required if this code executes in an outside module.
To conclude, we can still add a ToolBar control to the Toolbox and process them much as we did
under VB6, but the newer MenuStrips are much more powerful and offer many new features that are
far more flexible and dynamic and in keeping with current software development needs, such as the
ability to dock and undock from a ToolStrip Container, and are easier to write customization code
for, if we wish the application’s user to be able to do so. ToolBars, though still supported in VB.NET
simply for reasons of backward compatibility, are clearly by now very dated technology that had
been the initial mainstay in Visual Basic since the days of VB1 and QuickC for Windows.

● A Note on the Unload Form Command.


When VB6 forms are unloaded, their resources are released, and so many developers tend to invoke
a lot of form unloading commands. However, during an upgrade, a command such as “Unload
frmHelp” is not automatically changed to an equivalent “ frmHelp.Close” command, as we might think
it should. Instead, an upgrade warning is issued. This is E-Z enough to deal with, but there is good
reason for why it did what it did without taking care of business for us.
First, fix it by changing the command to invoke the form’s Close method. The reason why this was
not automatically changed for us is that the Upgrade Wizard was not sure if it should also invoke the
form’s Dispose method. This non-change will force our attention to it, because it will force a
compile error, (Unload will not be recognized). Were it up to me, I would have had the upgrade
automatically apply Close and Dispose, regardless (Dispose methods are and should be designed to
be invoked multiple times, but they should execute their resource releasing functions only once). The
reason Dispose is not automatically added is due to how the window was processed. If it was open
due to its Show method, closing it will automatically invoke its Dispose method. However, if we
 Page 97

displayed the form using the ShowDialog command, it will not automatically invoke Dispose,
because a dialog is expected to return a result, which would be lost if it were disposed. We would
not want to release those resources before we can process the dialog result in its invoking code (i.e.,
Dim iResult As DialogResult = frmHelp.ShowDialog(Me) ). Also, MDI child windows should also have
their Dispose methods invoked after Close if they are not displayed. The Upgrade Wizard simply
cannot make these assumptions for us.

● A Note on Finding the Lost VB6 NewIndex Property.


When we added a new entry to a VB6 ListBox or ComboBox, the control had a property named
NewIndex that contained the index of the item just added. This was especially useful in sorted lists.
However, this property would as often get us into trouble if we had removed entries from the list
before reading it. As such, because this property is only guaranteed to contain a reliable value
immediately after a new item had been added to the control’s list, VB.NET was forced to eliminate
this less-than robust and sometimes problematic property. Instead, it provides this “NewIndex” value
as the return value of its Add method, which is the only place where this value can ever be guaranteed
to be 100% reliable. For example, this simple code returns the “lost” NewIndex value:
Dim NewIndex As Integer = Me.ListBox1.Items.Add("Some Data") 'The Add method returns the old VB6 NewIndex property value.

● Notes on Process Handling In the KeyPress, KeyDown, and KeyUp Events.


Minor changes that we may not at first notice is to the KeyPress, KeyDown, and KeyUp events.
Unlike VB6, KeyPress, KeyDown, and KeyUp, event handling under VB.NET gives us far more
control over what is done to key processing. A key down or up event is triggered when the user
pressed or releases a keyboard key. A key press event is triggered when the user simply types a key
on the keyboard. Typically, most developers concern themselves with just the KeyPress event.
Under VB6, the KeyDown event supplied us with an Integer Keycode parameter, which grabbed the
system code for the key typed, and it provided an Integer Shift property, which allowed us to check
for the Shift, Cntrl, or Alt keys being pressed. We could disable the KeyCode by setting it to 0, which
in turn caused the KeyPress event to be supressed. For example, consider this VB6 code:
'*******************************************************************************
' Subroutine Name : lstEnums_KeyDown (VB6)
' Purpose : Allow DEL key to select Delete button.
'*******************************************************************************
Private Sub lstEnums_KeyDown(KeyCode As Integer, Shift As Integer)
Select Case KeyCode
Case 46 'DEL key
If Me.cmdDelete.Enabled Then 'activate delete button if it is enabled.
Me.cmdDelete.Value = True 'click Delete button.
KeyCode = 0 'disable KeyCode.
End If
End Select
End Sub

Under VB.NET, the rules have changed a bit, but I think it is for the better. Consider the following
example of a VB.NET version that properly upgrades the above VB6 KeyDown event:
'*******************************************************************************
' Subroutine Name : txtName_KeyDown (VB.NET)
' Purpose : Allow DEL key to select Delete button.
'*******************************************************************************
Private Sub txtName_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs) Handles txtName.KeyDown
Select Case e.KeyValue
Case 46 'DEL key (we could have also checked ReadOnly e.KeyCode against Keys.Delete).
If Me.cmdDelete.Enabled Then 'command button enabled?
Me.cmdDelete.PerformClick() 'yes, press Delete key.
e.SuppressKeyPress = True 'disable further processing in KeyPress() event.
End If
End Select
End Sub
 Page 98

Notice that there are 3 key differences noted. First, notice that we obtained the KeyValue (the
VB.NET version of a VB6 KeyCode), from the “e” parameter, which is declared as a KeyEventsArg.
Second, we invoked the Delete key using its PerformClick method. Third, instead of setting
e.KeyValue to 0 to supress the KeyPress event, as we might do if we were following the VB6 model,
we instead supressed the KeyPress event (and further processing) by setting the “e” parameter’s
Boolean SuppressKeyPress property to True. If we had tried to change e.KeyValue to 0, as we did
under VB6, an error is generated, because under VB.NET, KeyValue is ReadOnly.
The VB6 KeyPress event was handled after the KeyDown event, but before the KeyUp event. The
KeyPress event provided a KeyAscii parameter that was the ASCII value of the key pressed (duh).
We could disable it by setting the KeyAscii code to 0, which cancelled further system processing of
the code. Usually, the developer wants to restrict what the user types into a TextBox. For example,
suppose we wanted to ensure that the user entered only alphabetic or numeric characters into a
TextBox, and further, the alphabetic characters should always be uppercase. Consider this VB6 code:
'*******************************************************************************
' Subroutine Name : txtNewName_KeyPress (VB6)
' Purpose : Filter keyboard so that invalid data cannot creep in.
'*******************************************************************************
Private Sub txtNewName_KeyPress(KeyAscii As Integer)
Select Case KeyAscii
Case 1 To 31 'allow control keys.
Case Else
Dim C As String = UCase$(Chr$(KeyAscii)) 'get uppercase character from code.
Select Case C 'check uppercase character code.
Case "A" To "Z", "0" To "9", "_" 'allow A-Z, 0-9, and "_".
KeyAscii = Asc(C)
Case Else
KeyAscii = 0 'disallow others.
End Select
End Select
End Sub

As an experunent, we will try to duplicate the above VB6 code in VB,NET as much as possible:
'*******************************************************************************
' Subroutine Name : txtName_KeyPress
' Purpose : Filter keyboard so that invalid data cannot creep in.
'*******************************************************************************
Private Sub txtName_KeyPress(ByVal eventSender As Object, ByVal e As KeyPressEventArgs) Handles txtName.KeyPress
Dim KeyAscii As Integer = Asc(e.KeyChar)
Select Case KeyAscii 'check the current key being entered into the txtName textbox.
Case 1 To 31 'ignore checking control keys (let them pass through).
Case Else
Dim C As String = UCase(Chr(KeyAscii)) 'get uppercase character from code.
Select Case C
Case "A" To "Z", "0" To "9", "_" 'check against the range of allowed characters.
e.KeyChar = CChr(C) 'all is well for these, so be sure code reflects uppercase.
Case Else
e.KeyChar = ChrW(0) 'out of range, so nullify it (technique borrowed from VB6).
End Select
End Select
End Sub

If we handle processing of a key, we want to prevent the system handlers from processing it further.
To do that in VB6, we set KeyChar to zero. Yet, if we do that in VB.NET, the system will still
process key code zero as a key code, because the system only knows that a key had been pressed. As
a result, the system will ring an alert bell because it determined that nil is not a valid key code.
Under VB.NET, there is a change to how the KeyPress event handles its code from the way VB6 did
it. For example, the “e” parameter is now a KeyPressEventArg, which provides not an Integer
KeyAscii code as VB6 did, but rather a KeyChar parameter that is of type Char. Also, to cancel
further processing from the KeyPress event and to prevent an invalid code from being sent to the
operating system for default processing, instead of setting the value to zero as we did under VB6, we
set the boolean e.Handled parameter to True, which tells the operating system that we have handled
the processing of the key and that no further handling is required.
 Page 99

Select Case C
Case "A" To "Z", "0" To "9", "_" 'range of allowed text.
e.KeyChar = CChr(C) 'all is well for these, so be sure code reflects uppercase.
Case Else
e.Handled = True 'indicate Keypress event was handled (no need for further system processing).
End Select

NOTE: We should not add “e.Handled = True” after we updated the KeyChar property to an uppercase state, because
it would then not find its way into the textbox at all. This flag tells it to ignore the key that was typed. preventing the ley
code processing in the system message queue from reaching the TextBox.
Consider the following rather involved, but educational syntax parsing example:
'*******************************************************************************
' Subroutine Name : txtConstValue_KeyPress (VB.NET)
' Purpose : Filter keyboard so that invalid data cannot creep in.
' : Process a command line such as "(BASE + 3) 'lower value".
'*******************************************************************************
Private Sub txtConstValue_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs) Handles txtValue.KeyPress
Dim KeyAscii As Integer = Asc(e.KeyChar) 'grab ASCII code of character.
Dim S As String = Trim(Me.txtConstValue.Text) 'get contents of assignment data (a Constant value assignment).
Dim J As Integer = -1 'init location index.
Dim I As Integer = InStrRev(S, "'") 'comment tag present?
If CBool(I) Then
J = Me.txtConstValue.SelectionStart + 1 'if so, check for selection point.
End If
Dim AllowLC As Boolean = J >= I 'set lowercase allowance flag if selection beyond comment tag.

Select Case KeyAscii


Case 1 To 31 'ignore control keys.
Case Else
Dim C As String = UCase(Chr(KeyAscii)) 'get text version of code.
If CBool(InStr(1, "ABCDEFGHIJKLMNOPQRSTUVWXYZ &1234567890()'", C)) Then
If Not AllowLC Then 'if not allowing lowercase data...
e.KeyChar = CType(C, Char) 'update e.KeyChar to uppercase.
End If
ElseIf CBool(InStr("+-", C)) Then 'if math involved, ensure enbraced by parentheses.
S = Trim(Me.ConstValue.Text) 'grab trimmed text.
If VB.Left(S, 1) <> "(" Then 'no paren?
S = "(" & S & ")" 'no, so enclose it.
With Me.ConstValue
.Text = S
.SelectionStart = Len(S) - 1 'set insert point immediately before ')'.
End With
End If
Else
e.Handled = True 'cancel KeyPress event, and prevent further OS handling of key.
End If
End Select
End Sub

A better alternative is to do validation tests in the KeyDown event (KeyDown is also handled before
KeyPress, which is in turn handled before KeyUp), and we can suppress further processing by KeyPress
if a key is invalid by setting e.SupressKeyPress to True, which also auto-sets e.Handled to True:
Private Sub txtName_KeyDown(ByVal sender As Object, ByVal e As KeyEventArgs) Handles txtName.KeyDown
Select Case e.KeyValue 'check the value of the ReadOnly key.
Case 1 To 31 'ignore control keys.
Case Else
Select Case UCase(Chr(e.KeyValue)) 'check character from code.
Case "A"c To "Z"c, "0"c To "9"c, "_"c 'check range of characters (note the c tag for chars, not str).
Case Else
e.SuppressKeyPress = True 'let no other keys reach KeyPress (this also sets Handled to True).
End Select
End Select
End Sub

With this, we can afterwards simplify our KeyPress event code to just this little block of code:
Private Sub txtName_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs) Handles txtName.KeyPress
e.KeyChar = UCase(e.KeyChar) 'assume all values are valid from KeyDown event (UCase will not affect non-letters).
End Sub 'this method not needed AT ALL if we do not need UpCase-only text in the textbox.
 Page 100

● Notes on Invoking Handled Events Under VB.NET.


Under VB6, we may have seen, or have even written code that invoked another event method. For
example, it was common to see code that forced a click event of an “ADD” button if the user typed
text into a TextBox, and then hit the Enter key. This was, and is, a handy little shortcut.
Suppose we have a TextBox named txtCommand, and a Button named btnAdd (with a caption of
“&Add”, – which displays as “Add”, so the user can also fire the Add button with Alt-A), which will
add the user text to a Collection named colList. We might see VB6 code like this:
'VB6 Code
Option Explicit
Dim colList As New Collection

'******************************************************************
' txtCommand_GotFocus
' When textbox gets focus (the user selected it, for example).
' then select entire contents.
'******************************************************************
Private Sub txtCommand_GotFocus()
With Me.txtCommand
.SelStart = 0 'set selection to start of text.
.SelLength = Len(.Text) 'select entire text.
End With
End Sub

'******************************************************************
' txtCommand_KeyPress
' When typing data into the textbox, if the user types ENTER,
' then automatically invoke the ADD button.
'******************************************************************
Private Sub txtCommand_KeyPress(KeyAscii As Integer)
Select Case KeyAscii
Case 13 'CR?
Call btnAdd_Click() 'Yes, so for button click (we could have also used 'btnAdd.Value = True').
KeyAscii = 0 'nullify code so it is not processed further.
End Select
End Sub

'******************************************************************
' btnAdd_Click
'******************************************************************
Private Sub btnAdd_Click()
colList.Add(Me.txtCommand.Text) 'add user command to our list.
Call txtCommand_GotFocus() 'reset full selection and focus to text box.
End Sub

In the KeyPress event, the (ByRef) KeyAscii variable is tested for being 13; a Carriage Return, via
the Enter key. If 13 is found, then the ADD button is invoked by calling the BtnAdd_Click event code,
and then nullifying the KeyAscii value to prevent further processing of it. The Click event for
BtnAdd then adds the contents of the textbox to the Collection colList, and then the GotFocus event
is manually invoked to ensure that the contents of the TextBox are now fully selected.
For those who must still do VB6 programming, be aware that we could have invoked the Click event
for BtnAdd by instead using the command “Me.btnAdd.Value = True” (which would upgrade to
VB.NET to a more logical “Me.btnAdd.PerformClick”), which is the official VB6 method for forcing
the invocation of a Button object's click event, but what I am interested in is how to deal with the
VB.NET side, because all events under VB.NET will require parameters to be passed. And besides,
such a change would not work for invoking the GotFocus event of txtCommand.
Most of we already know what I am driving at here. If we have seen a button click event under
VB.NET, it would be declared like this:
Private Sub btnAdd_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnAdd.Click

And the GotFocus event for txtCommand (which is an Enter event under VB.NET, just as a LostFocus
event becomes a Leave event under VB.NET), would be expressed as:
Private Sub txtCommand_Enter(ByVal sender As Object, ByVal e As EventArgs) Handles txtCommand.Enter
 Page 101

But now these events have parameters in VB.NET, many wonder how to invoke them? One can
force a button click by invoking its PerformClick method, but we would still be stuck with the
GotFocus event (now Enter) for txtCommand. Actually, the solution is very simple.

The first parameter of each event is the actual object being processed; btnAdd for the btnAdd_Click
event code, and txtCommand for the txtCommand_Enter event code. Both of these have a second
argument object of an Eventargs type. As such we can simply specify the second parameter as “ New
System.EventArgs”. We use “New” because we are instantiating an new object.

However, if the method we invoke will not modify or use the EnventArgs parameter, we could sneak
by cleanly by simply passing the EnventArgs parameter from the event we are invoking it from, if
we are indeed invoking it from within an event, but I am a bit squeamish about getting into the habit
of doing something like that.
Following is the VB.NET upgrade of the above code:
Option Strict Off
Option Explicit On
Friend Class Form1
Inherits System.Windows.Forms.Form
Dim colList As New Collection
'NOTE: I hate it that the Upgrade Wizard turns Option Strict Off (Grrr...)
'******************************************************************
' txtCommand_GotFocus (this becomes txtCommand_Enter in VB.NET -- txtCommand_LostFocus would become txtCommand_Leave)
' When textbox gets focus (the user selected it, for example),
' then select entire contents.
'******************************************************************
Private Sub txtCommand_Enter(ByVal sender As Object, ByVal e As EventArgs) Handles txtCommand.Enter
With Me.txtCommand
.SelectionStart = 0 'set selection to start of text.
.SelectionLength = Len(.Text) 'select entire text.
End With
End Sub

'******************************************************************
' txtCommand_KeyPress
' When typing data into the textbox, if the user types ENTER,
' then automatically invoke the ADD button.
'******************************************************************
Private Sub txtCommand_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs)
Handles txtCommand.KeyPress
Dim KeyAscii As Short = Asc(e.KeyChar)
Select Case KeyAscii
Case 13 'CR?
Call btnAdd_Click(btnAdd, New System.EventArgs())
KeyAscii = 0
End Select
e.KeyChar = Chr(KeyAscii)
If KeyAscii = 0 Then
e.Handled = True
End If
End Sub

'******************************************************************
' btnAdd_Click
'******************************************************************
Private Sub btnAdd_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnAdd.Click
colList.Add(Me.txtCommand.Text)
Call txtCommand_Enter(txtCommand, New System.EventArgs())
End Sub
End Class

And sure enough, the upgrade performs the conversions of these invocations just as I described. All
that is left here is to tighten up the code. For example, event txtCommand_KeyPress, although it works
perfectly, could be seriously tightened up for much faster operation:
Private Sub txtCommand_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs) Handles txtCommand.KeyPress
If e.KeyChar = vbCr Then 'CR?
btnAdd.PerformClick() 'yes, force adding the current data.
e.Handled = True 'tell system not to handle this key code further.
End If
End Sub
 Page 102

● A Short Note on TextBox Locked Property Changes.


The VB6 TextBox Locked property has become the VB.NET ReadOnly property, which makes more
sense. VB.NET still has a Locked property, but it is now used to prevent a control from being
resized. This is usually due to anchoring of a control on a form, which is affected when a form
resizes. For example, were a TextBox to be anchored to the top, bottom, left, and right of a form, then
as the form size changes, the TextBox would proportionally stretch. However, if the TextBox has its
Locked property set to True, then it will not resize with the form and maintain a fixed size.
● A Short Note on Changes to the Tag Property.
The Tag property on various VB6 controls is no longer limited to just String data, but is now an
Object under VB.NET. This makes it powerful because we can use it to reference whole classes or
structures, and even a list of items. But in most cases one will continue to use a control’s Tag
property with text data. As such, use it’s ToString method to easily read its data as string text.
● Notes on Changes to the GotFocus and LostFocus Events.
The VB6 upgrade will change a VB6 GotFocus event to an Enter event under VB.NET. However,
some may notice that the Enter event simply precedes the GotFocus event in sequence under
VB.NET. Likewise, a LostFocus event is upgraded to a Leave event, though under VB.NET, a
LostFocus event precedes a Leave event when we are changing focus using a Mouse, but the Leave
event precedes a LostFocus event when we change focus using the keyboard. Here is the
breakdown, according to Microsoft:
When we change the focus by using the keyboard (TAB, SHIFT+TAB, and so on), by calling the
Select or SelectNextControl methods, or by setting the ContainerControl.ActiveControl property to
the current form, focus events occur in the following order:
1. Enter.
2. GotFocus.
3. Leave.
4. Validating.
5. Validated.
6. LostFocus.

When we change the focus by using the mouse or by calling the Focus method, focus events occur
in the following order:
1. Enter.
2. GotFocus.
3. LostFocus.
4. Leave.
5. Validating.
6. Validated.

NOTE: If the CausesValidation property is set to False, the Validating and Validated events are suppressed.
The reason that the Enter and Leave events are used instead of the GotFocus and LostFocus events
is because under VB.NET, GotFocus and LostFocus have been changed to low-level focus events
that are tied directly to the WM_KILLFOCUS and WM_SETFOCUS Windows messages.
A LostFocus event corresponds to the WM_KILLFOCUS system message, which is sent
immediately before the focus is removed from the control. A GotFocus event corresponds to the
WM_SETFOCUS system message, which is sent when a control has gained keyboard focus. As
such, their definition has certainly changed between VB6 and VB.NET.
The VB.NET Enter and Leave events correspond more directly to the VB6 GotFocus and
LostFocus events. Hence, the Enter and Leave events should now be used for all controls except the
Form class, because the Enter and Leave events are suppressed by the Form class. The equivalent
events used by the Form class are instead the Activated and Deactivate events, which also
correspond more directly to the VB6 GotFocus and LostFocus events for Forms.
NOTE: Do not attempt to set focus from within the Enter, GotFocus, Leave, LostFocus, Validating, or Validated event
handlers. Doing so forces the thread to yield control and can cause the application to stop responding to messages.
 Page 103

● A Short Note on Changes to the VB6 SetFocus Command.


Under VB6, when we wanted to set focus to a control or form, we specified the control’s SetFocus
method, such as TextBox1.SetFocus. Under VB.NET, this has changed to the Focus method, as in
TextBox1.Focus.

● A Short Note on Long-Pathing Through Namespaces.


If we are apprehensive about long-pathing, and wonder if using MessageBoxButtons.AbortRetryIgnore
instead of System.Windows.Forms.MessageBoxButtons.AbortRetryIgnore is more efficient – do not be
concerned. The pathing, shortcutted or not, is only a map for the compiler to allow direct access to a
feature, such as the AbortRetryIgnore constant. When the program is compiled, it no longer bothers
with these long paths, because by then it already knows precisely where to access information,
because all target locations are by then pre-computed.

● Notes on Changes to Multiple Document Interfaces.


First, displaying a Window List for Multiple-Document Interfaces (MDIs)
has changed between MainMenu and ToolStripMenu implementations.
When using a MainMenu interface, which VB6 used, we set the MdiList
property of a MenuItem control, typically a Main Menu item named
&Window or &Windows. With the new VB.NET ToolStripMenu controls,
we should set the ToolStripMenu’s MdiWindowListItem property to the
menu item that will be used to display the window list under.
Second, under VB6, we added a MDI Parent window by selecting it from a
template, and we created an MDI Child menu by setting a standard window’s
MDIChild property to True. Also, the MDI Parent form’s menu was replaced
by the menu of any displayed Child form, or at least the one with focus.
Under VB.NET, we create an MDI Parent window by setting a standard
window’s IsMdiContainer property to True, and we created an MDI Child
window by setting its MdiParent property to the MDI Parent form at runtime,
usually when instantiating the child form. For example:
'*******************************************************************************
' Subroutine Name : LoadFile
' Purpose : Load a text file into a new MDI Child Form and display it.
'*******************************************************************************
Private Function LoadFile(ByVal Path As String) As Boolean
Static FileIndex As Integer = 0 'static refernce used for unique child form names.

If Not FileIO.FileSystem.FileExists(Path) Then 'if the file does not exist...


Return False 'report failure.
End If

Me.Cursor = Cursors.WaitCursor 'show that we are busy.

Dim frm As New mdiChild 'instantiate a new Child Form.


frm.MdiParent = Me 'make the form an MDI Child of this form.
frm.Tag = Path 'save the full path to the file.
FileIndex = FileIndex + 1 'bump the static file naming index.
frm.Text = "Window" & CStr(FileIndex) 'apply a new title to the Child Form.
frm.Show() 'display our child form (note that we MUST NOT specify Show(Me)).

Dim TS As New System.IO.StreamReader(Path) 'open the file


With frm.TextBox1
.Text = TS.ReadToEnd 'read its contents into a textbox.
.SelectionStart = 0 'show start of file.
.SelectionLength = 0 'make sure that nothing is selected (otherwise whole file may be).
End With
TS.Close() 'close the file.
TS.Dispose() 'dispose of resources.

Me.Cursor = Cursors.Default 'show that we are no longer busy.


Return True 'report success.
End Function
 Page 104

● A Note on Changes to a Button’s Cancel and Default Properties.


Under VB6, a button control had a Cancel and Default property. If Cancel was set to True, then
pressing the Escape key (ESC) automatically triggered this button. Likewise, if the Default property
were set to True, then pressing the Enter key, even within a TextBox, triggered that button. Only one
button on a form could have its Cancel and/or Default properties set to True (a single button could
have both set). The problem here is that each button on a form had these properties, and although the
system was nice enough to reset any other button that had one of these properties set, in case we
enabled it on another, I had always thought that it would have made more logical sense to place these
properties on the form, where we can only assign a single button to each property, saving resources.
Coincidentally, under VB.NET, this configuration has been simplified by placing these options on
the Form, through the form’s CancelButton and AcceptButton properties, which are assigned the
button control for which the Escape (Cancel) or Enter (Accept) keys will trigger.
NOTE: Refer also to Black Book Tip # 45: Making Sense of Form Default Buttons and Their DialogResult Properties on page 613.

● A Short Note on Changes to CheckBoxes.


Under VB6, when we clicked a CheckBox, it triggered a Clicked event for the checkbox, and it also
toggled its Integer Value property between 0 (Unchecked) and 1 (Checked). A third value, 2
(Grayed), indicated an Indeterminate state. Under VB.NET, this is upgraded to a CheckChanged
event, and we can check the checkbox’s boolean Checked property for a value of False
(Unchecked), or True (Checked). Note further that we can also check the CheckState property for a
value of CheckState.Checked, CheckState.Unchecked, and CheckState.Indeterminate (Grayed).

● A Note on Property Naming Conflicts With VB Commands.


We may notice VB.NET sometimes has naming conflicts (referred to as Pollution in OOP) between
different loaded enumerations. The most frequent clash is between a Form’s Left and Right
properties, and the VB string commands Left and Right. The easiest way around this conflict is to
place a special VB alias at the top of the form, declared as “ Imports VB = Microsoft.VisualBasic”. This
simple command can be used to ensure that when we want to grab the left or the right of a string
variable, we can simply specify VB.Left and VB.Right, as otherwise, if we simply specified Left or
Right, VB.NET would assume that we are referring to the Left or Right properties of the form.
This VB alias is also handy for helping us to remember a forgotten VB command. All we have to do
is type VB and a dot, and a dropdown list of VB commands is immediately presented. I use this
feature a lot, even on non-Form source files.

● A Note on Using Icons for Menu Images Under VB.NET.


VB.NET Menus allow us to specify bitmaps as menu images, which will be displayed on the left
side of a dropdown menu ribbon. However, we can use icons instead, which are more plentiful and
more available, by simply invoking its ToBitmap method. For example, if we were creating a new
ToolStripMenuItem, we could declare one like this:
' create a new menu item.
Dim mnuItem As New ToolStripMenuItem("&Open", My.Resources.icnOpen.ToBitmap, New EventHandler(AddressOf mnuFileOpen_Click))

Or modify the image of an existing menu item by declaring something like this:
Me.mnuFileOpen.Image = My.Resources.icnOpen.ToBitmap

Or even from a file, doing it something like this:


Me.mnuFileOpen.Image = New Icon("C:\MyHipBag\Icons\MyOpenIcon.ico").ToBitmap

Bear in mind, though, that whatever their actual Transparency color was will become opaque, so we
may need to also set the menu item's transparency color property to match the icon's background.
 Page 105

● Notes on VB6 FSO Text Streaming and VB.NET File Streaming.


Under VB6, the fastest File Input/Output processing possible was with a File System Object, available
by setting a reference to either Microsoft Scripting Runtime (Scrrun.dll) or to Windows Script Host
Object Model (wshom.ocx, which reroutes to IwshRuntimeLibrary.dll).
For example, under VB6 with an FSO, we could quickly read a text file into a TextBox like this:
Dim FSO As FileSystemObject 'file system object (reference to scrrun.dll or wshom.ocx needed).
Dim Ts As TextStream 'textstream object for file I/O.

Set FSO = New FileSystemObject 'instantiate our File System Object.


Set Ts = FSO.OpenTextFile(Path, ForReading, False) 'open the filepath for reading.
myForm.TextBox1.Text = Ts.ReadAll 'read its contents into a textbox.
Ts.Close() 'close the file.
Set Ts = Nothing 'dispose of the TextStream resource.
Set FSO = Nothing 'dispose of our file system object.

If we wanted to do the same FSO operation under VB.NET, we would have to add a COM reference
to Windows Script Host Object Model (wshom.ocx) and then add “Imports IWshRuntimeLibrary” to the
top of the file, or we would have to add a COM reference to Microsoft Scripting Runtime
(scrrun.dll) and then add “Imports Scripting” to the top of the file. We could then add the code
below to do the same thing:
Dim FSO As New FileSystemObject 'instantiate a File System Object for File I/O.
Dim Ts As TextStream = FSO.OpenTextFile(Path, ForReading, False) 'open the filepath for reading thru a textstream object.
' NOTE: If you use scrrun.dll instead of wshom.ocx, the above second parameter would be specified as 'IOMode.ForReading.
myForm.TextBox1.Text = Ts.ReadAll 'read its contents into a textbox.
Ts.Close() 'close the file.
Ts.Dispose() 'dispose of the TextStream resource.
FSO = Nothing 'dispose of our file system object.

As though an FSO TextStream did not speed File I/O up fast enough, VB.NET introduced
StreamReaders and StreamWriter objects that kick File I/O into turbo drive. Although they all do
data streaming, FSO is much slower due to its Win32 COM architecture that is designed to
communicate with invokers through an ActiveX VBC p-code interface. But even so, FSO
performance is spectacular, because its DLL performs its system operations in native code, but it still
runs only 20% as fast as .NET FileStreams. To perform the same task as shown above, but without
adding references or importing namespaces, and also do it a whole lot faster, do this instead:
Using New System.IO.StreamReader(Path) 'open a Stream reader to the provided text file path.
myForm.TextBox1.Text = .ReadToEnd 'read the entire file contents into a textbox.
.Close() 'close the file and auto-invoke the streamer's Dispose() method.
End Using 'End Using will also auto-Invoke Dispose() if we did not.

NOTE: See the article “Comparing VB System I/O Commands; Dir Vs. FSO Vs. FileStreams” on page 216 for a
practical demonstration, comparing directory parsing speeds. It will surprise you.

● A Note on Changes to Counting CheckBoxed ListBoxes.


Under VB6, counting checkboxes under a Checked ListBox (a ListBox with its Style property set to
CheckBox), was accomplished by examining its Integer SelCount property. Also, we could quickly
examine the list's selected (checked) items through its Selected array property.
Under VB.NET, these selected items are kept within a list collection as Objects. To get the number
of selected items, we would examine the ListBox.SelectedItems.Count property. To extract a selected
item from this list, we could specify something like ListBox.SelectedItems(Index). Bear in mind that
a selected item is returned as an Object, so we will also have to use CType or DirectCast in order to
recast it back to its original type. However, since most people simply store strings in a ListBox, we
can just specify Dim S As String = Me.ListBox1.SelectedItems(Index).ToStrin g, or better, instead use
Dim S As String = DirectCast(Me.ListBox1.SelectedItems(Index), String ).
 Page 106

● Notes on Changes to Setting Mouse Cursor Icons.


Under VB6, we could show the system was busy by specifying “Screen.MousePointer = vbHourglass”.
We might want to follow this with “ DoEvents” to ensure system paint messages are processed, so that
we will actually see the cursor become busy. And then, after we are finished performing a long task,
show that it is no longer busy by specifying “Screen.MousePointer = vbDefault”.
Under VB.NET, this has changed slightly, but the documentation does not help. The documentation
tells us to set “System.Windows.Forms.Cursor.Current” to the cursor we want displayed. For example:
Cursor.Current = Cursors.WaitCursor 'note that WaitCursor replaces the VB6 vbHourglass.

NOTE: This worked under early editions of VB.NET, but it was redacted because it affected all displayed forms, which
was a massive inconvenience to users who wanted to allow concurrent access to another displayed form.
Unfortunately, in newer editions of VB.NET this does absolutely nothing, which had been driving
me to distraction, being one who depended heavily on documentation. However, after some
experiments, I discovered we could do the following instead, and it worked every time:
Me.Cursor = Cursors.WaitCursor 'show that we are busy by assigning the wait cursor to the current form.

NOTE: In the above line of code, “Me” represents the current form. Also, if we were to add the command
“Application.DoEvents” afterward, which was the trick to get a new cursor to display under VB6, this might even
prevent the cursor from changing on the screen under VB.NET, so be sure to remove it if it is there.
To reset the cursor to the default, once a long task finishes, issue the following command:
Me.Cursor = Cursors.Default

NOTE: See "Black Book Tip # 32: Dealing with the Form Cursor not displaying over Form Controls" on page 568,
offering a better and more customizable solution than using My.Application.UseWaitCursor, to display any cursor over a
form and all its controls, not just a Wait Cursor.

● Notes on Making VB.NET Drag and Drop E-Z.


Drag and Drop has changed profoundly between VB6 and VB.NET. The changes are significant enough
that the Upgrade Wizard will make absolutely no attempt to upgrade the code at all, except to tell us in
an upgrade note that it failed to upgrade something. This sends many developers into a tail-spin panic.
On the cover of the book, The Hitchhikers Guide to the Galaxy, written by the late Douglas Adams,
there were two words printed in large bold friendly letters: DON’T PANIC. This is good advice to those
with a predisposition toward tail-spin panics, or jumping off cliffs when startled by a small mouse, who
had absolutely no intention of startling anybody, or for seeing them hurling to a gloomy end.
First, the changes to the VB.NET version were not meant to frustrate us, but rather to make Drag
and Drop much more powerful (and ironically, easier – actually it has, once we understand it).
Second, the reason that the VB6 code was not upgraded was because the Upgrade Wizard claimed
that there were too many subtle changes to keep track of in a reliable manner by an automated
processor like the Upgrade Wizard (or so they say). But that does not mean that the actual upgrading
of the code must be difficult or frustrating at all. In fact, resolving it is so easy that we may truly
wonder why the Upgrade Wizard did not bother with automatically upgrading them, thus avoiding
all the frustration, hair-pulling, the tossing of hands into the air, tail-spin panics, or hurling off cliffs.
If we do a search on the web or in the MSDN Help Library for “Drag and Drop Overview”, or just
go to http://msdn.microsoft.com/en-us/library/ms742859.aspx, we will find a great starting point for easily
resolving any problem that we might encounter while using Drag and Drop. I wish I had found this
resource before I had gone through all my heartaches; but I hope that this helps you. On top of that, I
should have known that all I would have to do is search for a topic with “Overview” in the title and I
would be where I should always want to start anyway.
 Page 107

When VB6 was the only game in town, developers kept complaining, “Why can’t Drag and Drop do
this,” or, “Why can’t Drag and Drop do that?” This list of complaints was long. Well, VB.NET’s
Drag and Drop now does all that. And that is why we now see all the differences between VB6’s and
VB.NET’s implementation of that technology. Also, VB6 used Object Linking and Embedding for
Drag and Drop support, but OLE is an aging paradigm that .NET, with its much faster messaging
technology that takes advantage of multi-processors, is trying to wean us away from.
When doing Drag and Drop under VB.NET, usually all we need to remember is that filenames are
returned as string arrays, that the Clipboard’s DataFormats object should become our new best
friend, and that we must always remember to now implement a DragEnter event on our target
objects, even if it is to simply issue “ e.Effect = DragDropEffects.All”, just so we can drop
something onto it (it is not enough to just enable EnableDrop on the control). These few pieces of
advice will help us out of most any trouble we may wander into when using Drag and Drop.
Imagine this scenario (an actual one I faced when I first met this implementation difference). Under
VB6, I want to drop one or more files that can be dragged from my File Browser onto a ListBox
named lstFiles in an application. The lstFiles object is set to accept Drag and Drop. I did this by
setting that object’s OLEDropMode property from None (0) to Manual (1). This property enables
drag recognition and will display a drag icon for the mouse cursor when it is dragged over it. I also
have an OLEDragDrop event that reacts to dropping objects onto lstFiles.
All that OLEDragDrop did was add the file or files to the bottom of the list. For example:
Private Sub lstFiles_OLEDragDrop(Data As Object, Effect As Integer, Button As Integer, _
Shift As Integer, X As Single, Y As Single)
Dim Idx As Integer
For Idx = 1 To Data.Files.Count 'parse through all possible files in the list.
Me.lstFiles.Items.Add(Data.Files(Idx)) 'append each to the end of the file list.
Next Idx
End Sub

As one can see, the Data object contains a 1-based Collection object that holds a list of one or more
file paths, which we can easily loop through to pick up all of its entries. This, and single items that
we could grab using the Drag Data object’s GetData method were all the capability VB6 Drag and
Drop had. Hmm. When we look at it that way, the VB6 interface was not very impressive.
However, VB.NET comes along and it seems like a monkey wrench was thrown into the works (a
monkey wrench, or spanner in Britain, is a large adjustable wrench with a long fulcrum arm that
was popular in the nineteenth century, though now typically used in only really tough, tight
situations, or plumbing, where we need to monkey around with a stuck pipe or large nut – in
a pinch it also made a good, but sloppy hammer).
When I upgraded my application, I discovered that the Drag and Drop code did not likewise
upgrade. So I tried to take a look at what I needed to do. I dutifully enabled the EnableDrop
property on my upgraded lstFiles object by setting that property to True.
I then added a DragDrop event to the lstFiles object, but I was not quite sure what to do with it. I did
notice that my “e” parameter was of type DragEventArgs, and that it had a Data object, but this
Data object did not have a Files collection. So I took a look at the documentation to see what I could
glean from that. What I gathered was that I needed to use the Data object’s GetData method to get
data (duh!). All I had to do was provide GetData with a Format string. All the examples I saw were
for basic types, like “String” and such. But I was not getting anywhere with that, so I thought I
would use the GetFormats method, which returned a string array, listing all acceptable formats, and
I put them into a message box. But I was still getting nowhere: the MsgBox did not pop up. Plus, the
mouse cursor looked like this when the mouse was dragged over my lstFiles control:
 Page 108

More reading revealed that I had to parse the data being dragged and to decide if it could be dropped
onto my target object, and to set the proper copy effect for that object, which was accomplished
through the object’s DragEnter event. Though I was so far getting nowhere on the format, I really
liked this new DragEnter data verification event idea a whole lot better than the rather limited
support we had in VB6. So I cheated, for the time being anyway, by simply setting my Effects
property to Copy, just so I could finally fiddle around in the DragDrop event:
Private Sub lstFiles_DragEnter(ByVal sender As Object, ByVal e As DragEventArgs) Handles lstFiles.DragEnter
e.Effect = DragDropEffects.Copy
End Sub

With that, my message box in my DragDrop event popped up with the list of
formats available. Well, the last three formats looked promising, especially
FileDrop and FileName. I recognized FileDrop from working with Clipboard I/O,
using the DataFormats class, which featured a number of string members
specifying, surprisingly enough, data formats. FileDrop was used in clipboard I/O
to handle copying one or more files. So, I assumed (rightly so, as it turned out) that
I could use Filename or FileDrop interchangeably, though I prefer FileDrop simply
because I can avoid misspellings by using the DataFormats enumeration.
So with that, I changed my above DragEnter event to:
Private Sub lstFiles_DragEnter(ByVal sender As Object, ByVal e As DragEventArgs) Handles lstFiles.DragEnter
If e.Data.GetDataPresent(DataFormats.FileDrop, True) Then 'Note that you can use "FileName" or even "FileDrop" in
e.Effect = DragDropEffects.Copy 'place of DataFormat.FileDrop. Param=TRUE allows compatibles.
Else 'formats to be converted to the specified target format.
e.Effect = DragDropEffects.None 'Put up a no-entry sign.
End If
End Sub

This worked for file dragging, but then I ran into a minor snag in my DragDrop event, but this ended
up only helping me to better understand the special handling that Drag and Drop affords files. As an
experiment, I placed the following debugging line into my DragDrop event:
MsgBox(e.Data.GetData(DataFormats.FileDrop).ToString) 'I used ToString because the returned data is of type Object.

The result, instead of a filename, was “System.String[]”. This did not do well as a
filename, but it tolf me the result was a string array (C#, C++, and Managed C++ uses
square brackets instead of VB’s parentheses to indicate arrays). So, as a further
experiment, I changed my test code to this:
Dim DataFiles() As String = e.Data.GetData(DataFormats.FileDrop, True) 'grab file list.
Dim Names As String = String.Empty 'init list accumulator.
For Each str As String In DataFiles 'loop through file list.
Names &= CStr(idx) & ": " & str & vbCrLf & vbCrLf 'append each file.
Next
MsgBox(Names) 'display result.

And that did the trick.


In reviewing all this, I have come to the conclusion that it
would have been possible for the Upgrade Wizard to have
upgraded the code. It would have had to add a DragEnter event,
but it should have simply set “e.Effect = DragDropEffects.All”,
but added an UPGRADE TO-DO notice that the developer
should specify the formats that they want to allow, and the type
of drop events they want to support. In upgrading the “Data.Files” VB6 collection, a generic “Dim
DataFiles() As String = e.Data.GetData("FileName", Yes)” is easily possible, and instead of looping
from 1 to the count of Collection items, we loop from 0 to the Ubound size of the DataFiles array.
So in review, the following VB6 code:
 Page 109

Private Sub lstFiles_OLEDragDrop(ByRef Data As Object, ByRef Effect As Integer, ByRef Button As Integer,
ByRef Shift As Integer, ByRef X As Single, ByRef Y As Single)
Dim Idx As Integer
For Idx = 1 To Data.Files.Count 'parse through all possible files in the list.
Me.lstFiles.Items.Add(Data.Files(Idx)) 'append each to the end of the file list.
Next Idx
End Sub

Can be upgraded to the following VB.NET Code:


Private Sub lstFiles_DragDrop(ByVal sender As Object, ByVal e As DragEventArgs) Handles lstFiles.DragDrop
Dim DataFiles() As String = e.Data.GetData(DataFormats.FileDrop, True) 'get file list.
For Each str As String In DataFiles 'grab each selected filepath...
Me.lstFiles.Items.Add(str) 'and add it to the ListBox.
Next
End Sub

NOTE: If we have set Option Strict On, as I do, then we must also cast the data returned by GetData, because it returns
a generic Object type. We know that this type is actually a String Array, so we can use the DirectCast command to cast it
to string: Dim DataFiles() As String = DirectCast(e.Data.GetData(DataFormats.FileDrop, True), String()).
Of course, we will also have to remember the DragEnter event, which will allow us to specify what
kinds of files our target control will allow, which is now handled during dragging, rather than
generically by the enablement of a control parameter, which had invited the undesired possibility of
invalidly formatted data being dropped under VB6:
Private Sub lstFiles_DragEnter(ByVal sender As Object, ByVal e As DragEventArgs) Handles lstFiles.DragEnter
If e.Data.GetDataPresent(DataFormats.FileDrop, True) Then 'Note that you can use "FileName" or even "FileDrop" in
e.Effect = DragDropEffects.Copy 'place of DataFormat.FileDrop. Param=TRUE allows compatibles.
Else 'formats to be converted to the specified target format.
e.Effect = DragDropEffects.None 'Put up a no-entry sign.
End If
End Sub

NOTE: Just a reminder: I use the optional parameter “True” to get the GetData and GetDataPresent methods to
allow compatible formats that might not be of that type to be converted to that type.
Overall, I am very impressed with VB.NET’s implementation of Drag and Drop. It is easy to use
and easy to upgrade, and offers me much tighter control over what can or cannot be dropped on a
control that accepts dropped items.

● Notes on the Loss of MAPI Controls (and getting them back!).


Plain and simple, MAPI (Messaging Application Program Interface) is not yet a .NET technology; it is
still COM technology (Common Object Model), used by ASP (Active Server Page), IIS (Internet
Information Server), and any other COM application that access email. Being COM-based, we should
not expect to see the VB.NET Toolbox sport controls such as VB6’s MAPISession or MAPMessage.
Under the VB6 implementation of MAPI, it used the MAPSession control to (what else?) manage a
MAPI session. The MAPIMessage control was used to process email messages, both incoming (POP3;
Post Office Protocol – Version 3) and outgoing (SMTP; Simple Mail Transfer Protocol).
When we upgrade a VB6 MAPI application to VB.NET, we will notice that the upgraded
application will still have the VB6 MAPISession and MAPIMessage controls on any form
that had them before. This is because their control sources have been copied locally and are
referenced internally. Under .NET, a copy of the COM-based MSMAPI32.DLL is converted
into a non-COM version (its DLLRegisterServer entry is disabled) and saved to a project-local file named
Interop.MSMAPI32.DLL. But, because both VB6 controls actually accessed this DLL provider through
the MSMAPI32.OCX ActiveX interface, another project-local non-COM DLL named
AxInterop.MSMAPI32.DLL is internally compiled by .NET that duplicates the ActiveX visual Interface
services for the controls, as well as the function mapping services to the new Interop.MSMAPI32.DLL.
Having found these controls on their upgraded applications, many developers also want to add them to
other VB.NET projects so they can take advantage of them there, but they cannot seem to find a way to
easily access the new DLLs from those new projects. It is doable, but it requires numerous coding hacks.
 Page 110

But relax. Why not just add these two VB6 controls to our VB.NET Toolbox and access them directly?
1. With any form up on the Visual Studio screen so that the IDE toolbox is active, right-click a toolbox category we
want to add the MAPI controls to (if we want to add them to their own category, such as to one named COM, right-
click any category and select the Add Tab option, then type the name of our category, such as COM, press ENTER,
then right-click that tab).
2. Select Choose Items…, and wait (a while) for the IDE to build a massive control reference list from the computer.
3. Once the Choose Toolbox Items dialog is finally displayed – select the COM Components tab.
4. Scroll down and put checkmarks in the check boxes for Microsoft MAPI Messages Control, Version 6.0, and
Microsoft MAPI Sessions Control, Version 6.0. (Both of these were actually linked to MSMAPI32.OCX, which in
turn drilled down to MAPI32.DLL, but they will now both link to a .NET-compiled axInterop.MSMAPI32.DLL and
drill down to Interop.MSMAPI32.DLL).
5. Click the OK button, and we will find these two controls now in our selected Toolbox category list, and we can
begin using these controls just exactly as we would had been using them under VB6.
NOTE: If we do not find these entries in the Choose Toolbox Items dialog box, then we may not or no longer have the
VB6 redistributables on our system, so we will have to minimally install the free Runtime Distribution Pack for Service
Pack 6 for Visual Basic 6.0, available from Microsoft, http://www.microsoft.com/downloads/details.aspx?FamilyId=7B9BA261-7A9C-
43E7-9117-F673077FFB3C&displaylang=en. We are allowed to do this even if we no longer own VB6. If we have VB6 installed,
we also want to install the Microsoft Visual Basic 6.0 Service Pack 6 Cumulative Update, available at
http://www.microsoft.com/download/en/details.aspx?amp;displaylang=en&id=7030.

● A Note on Displaying a Checkbox as a Button.


Be aware that under VB6, we could display a Checkbox as a button by setting its Style property from
Standard to Graphical. This was useful for buttons we wanted to display as toggling up or down on
each click, and we could easily check its pushed state through its Value property. VB.NET also
allows this, but by setting its Appearance property from Standard to Button. We check for it being
toggled down by checking its more standardized Checked property for being True.
● Notes on Some Click Events Changing to SelectedIndexChanged Events.
We may notice that on VB6 list control upgrades to VB.NET, such as for a ListBox or ComboBox,
that the VB6 Click event is upgraded to a VB.NET SelectedIndexChanged event. Most of the time,
this is a very good thing, because normally we want to address these lists because the user clicked on
a list and wants to work on the selected line, and VB6 did not have a SelectedIndexChanged event,
so the conversion of this event is very helpful. This can, however, bring about problems if our
original Click event code had manipulated the list’s SelectedIndex property. For me, for example, I
would have preferred that it remain a Click event, because if I want to deal with the selection index
changing, I would specifically address that in my code. This issue had me pulling what is left of my
hair out (I tend to do a lot of hair-pulling) because a particular program worked fine under VB6, but
under VB.NET it was issuing subsequent SelectedIndexChanged events because my Click event
needed to change the selected line based on certain criteria. This works fine under the Click event
because it does not respond to selection changes, assuming them already made.
Another issue that we will have to deal with for this upgrade is that a SelectedIndexChanged event
will fire before the main form is loaded if we initialize the SelectedIndex property to something other
than its default -1 value when our application is loading, before its Form Load event has finished.
Normally, I deal with these issues, which will also occur in any existing Resize and TextChanged
events as well, by defining a private Boolean mFormLoaded variable field (Private mFormLoaded As
Boolean = False), and set it to True when the form is finished loading. as outline in “A A Note on the
VB.NET Form Event Firing Sequence” on page 124. There, I explained that in these events,
making their first line “If Not mFormLoaded Then Return” will totally eliminate this issue.
In the final analysis, however, it would have been much better if the Click event had simply been left
alone and that an upgrade note was inserted above it, suggesting the change instead of making it, and
I likewise suggest we simply reconvert it back to a Click event, but only if our Click event will also
be modifying the control’s SelectedIndex property.
 Page 111

For example:
Private Sub lstSearchList_Click(sender As Object, e As EventArgs) Handles lstSearchList.Click
'NEW click body to house and replace SelectedIndexChanged code, shown below.
'Copy its body code to here and delete the SelectedIndexChanged framing.
'The ORIGINAL Click event had been upgraded to SelectedIndexChanged by the Upgrade Wizard.
End Sub

'*******************************************************************************
' Subroutine Name : lstSearchList_Click
' Purpose : React when an item in the list is clicked.
'*******************************************************************************
'UPGRADE_WARNING: Event List1.SelectedIndexChanged may fire when form is initialized. Click for more: Blah-Blah-Blah...
Private Sub lstSearchList_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs)
Handles lstSearchList.SelectedIndexChanged
PlayClick() 'play resource click.
'Me.cboSearchList.Text = VB6.GetItemString(Me.lstSearchList, Me.lstSearchList.SelectedIndex) 'get selection data.
Me.cboSearchList.Text = Me.lstSearchList.Items(Me.lstSearchList.SelectedIndex).ToString 'get selection data.
End Sub

NOTE: We should also be aware that the SelectedIndex value of a ComboBox will change as we move the cursor over
its lines, even though it will not fire a SelectedIndexChanged event (it is simply stubbed out for mouse movement within
its class definition). We can check this value during a MouseMove event to see which item the cursor is moving over. The
SelectedIndex value is only stable for code parsing outside ComboBox control events after its Click or
SelectedIndexChanged event fires as a result of the user actually clicking on an entry within the list.

● Notes on VB6-style Printing Under VB.NET Made E-Z.


I have heard an unending torrent of former VB6 coders raising the roof about not being able to use a
printer under VB.NET. Actually, that is a load of horse pucks. What they really want, however, is to
do it the same way as they did it under VB6. Conversely, I hated the way VB6 used the printer.
Even so, this VB6-style printing problem had actually been fully addressed as far back as VB2005
with the introduction of Microsoft’s free Visual Basic Power Packs, http://msdn.microsoft.com/en-
us/vbasic/bb735936.aspx). As of VB2010, these power packs come pre-installed. Once installed (if we
have to install them with VB.NET prior to VB2010), we will find a new group in our ToolBox named
Visual Basic PowerPacks. From this, we can drop a FormPrint tool on our form, so we can print
forms just like we did under VB6. We can also drop a LineShape, OvalShape, or RectangleShape
tool on our form to begin drawing lines, circles, and rectangles on our form. The thing that I really
like about these shape tools is that they offer a truckload of functionality, allowing us to do things we
may have only dreamed of doing easily under VB6. Another tool, a DataRepeater, is well worth
playing around with and taking full advantage of. The DataRepeater is a scrollable container for
controls that display repeated data, such as rows in a database table. It is useful as an alternative to a
DataGridView control when we require greater control over the data layout (see http://msdn.microsoft.com/en-
us/library/cc488279.aspx for details on how to take full advantage of it).

But to simply start printing to our printer using the power packs, go to the References tab of our
Project Properties and add a .NET reference to “Microsoft.VisualBasic.PowerPacks.Vs” (sort the
list by clicking the column heading to make this much easier to locate).
Next, at the top of our form, enter this Imports line:
Imports Microsoft.VisualBasic.PowerPacks.Printing.Compatibility.VB6

Now, all we need to do is to create a Printer reference within our code and we are ready to rock ‘n’
roll, which we can instantiate with “Dim Printer As New Printer”, and suddenly we can start
printing to our heart’s content, just like we did under VB6.
NOTE: For those of us who bemoan the lost glory days of troglodyte-level VB6 printing, we may be disappointed that
with this updated Printer object, we can do a whole lot more and have much greater control over our printing without
having to go through all the complicated gymnastics that we may have had to go through under VB6, even though that
older functionality is still there for backward compatibility to the VB6 Printer object. Microsoft apologizes for this
greater convenience in advance.
 Page 112

● Notes on the VB.NET GET and SET Property Format.


VB.NET Get and Set property members are embraced together within a single property declaration
(the VB6 Let member has no useful function in VB.NET), no longer separately declared as they were
in VB6, making property management easier. For as much as many VB6 users complained about
this, a great many more of them demanded VB.NET feature this functionality, eliminating property
declaration confusion. Even though this restricts us to using members with identical scope, it is easy
enough to separately declare Read-Only and Write-Only members with different scope, though they
sadly cannot share the same name due to internal Delegate Class naming collisions.
For example, if we type the following line and then press ENTER:
Public Property DoorCount() As Integer

Under editions of VB.NET prior to VB2010. the following code block was automatically generated:
Public Property DoorCount() As Integer
Get

End Get
Set(ByVal Value As Integer)

End Set
End Property

As of VB2010, we would have to additionally enter Get or Set after entering the property declaration
above in order to view this expanded coding. This is because more recent editions of VB.NET have
been enhanced to automatically generate a default ‘backing field’ variable for a property if we do not
declare one. What this means is that, internally, a hidden variable field will be automatically
generated by the compiler, complete with Get and Set property code support, unless we override it
with our own Get and Set property declarations. This is incredibly convenient, allowing us to very
quickly define various properties without needing to initially declare separate associated field
variables, along with Get and Set code, until later. We can even include the ReadOnly or WriteOnly
modifiers to the Property declaration, though in these cases we will also be required to write custom
retrieval or storage code so that such exclusive access would make any kind of sense in a class.
All we need to do is fill in the blank code lines within the Get and Set members. Again, if we want to
make the property read-only so that Get is the only member supported, we cannot simply delete the
Set block, because this would generate a syntax error. What we would need to do is explicitly tell the
program that the property is Get-only, so we would type the line with the ReadOnly modifier
(conversely, we could also use the WriteOnly modifier for a Set-only property). For example,
Public ReadOnly Property DoorCount() As Integer

And VB will generate the following block of code (less my bloviating comments):
Public ReadOnly Property DoorCount() As Integer 'Vb2010 and later will not auto-generate the Get/Set structure unless you type Set or Get
Get 'below it. Until then, it auto-reserves a variable that is named as the property, but with a
'underscore, such as Protected _DoorCount As Integer, unless you over-ride it by declaring an
End Get 'identical variable name, or use a different variable name. This way you can quickly set up
End Property 'a class initially w/o pounding away at a lot of property code to get to testing the class.

VB2010 also introduced shorthand code to automatically declare Set and Get Property members with
an initial assignment in one line. For example, enter the following line:
Property myProp As String = "Empty"

And the following code will be generated, replacing what we typed (with added comments by me):
Private _myProp2 As String = "Empty" ' ┌────────────────────────────────────────────────────────────────────────────────┐
Property myProp As String ' │ │
Get ' │ │
Return _myProp2 ' │ │
End Get ' -------------------------->│ WOW! LOOK AT ALL THE GREAT SPACE FOR DEVELOPER COMMENTS IN HERE! │
Set(ByVal value As String) ' │ │
_myProp2 = value ' │ │
End Set ' │ ECHO!... Echo... echo... (echo)... echo... │
End Property ' └────────────────────────────────────────────────────────────────────────────────┘
 Page 113

● A Note on the Support of Using...End Using.


VB.NET supports Using...End Using code blocks, which will instantiate a temporary object and
automatically disposes of it at End Using, regardless of how the code actually exits this block. It is
similar to With...End With, but it is initiated with the form “Using nm As New objClass”. For example:
Using SR As New System.IO.StreamReader(Path) 'use a temporary SreamReader object to open a text file.
Dim Ary() As String = Split(SR.ReadToEnd, vbCrLf) 'read file (specified by the Path string variable) into an array.
SR.Close() 'close file and auto-Dispose of thw stream reader object.
For Idx As Integer = 0 To UBound(Ary)
Debug.Print(Ary(Idx)) 'display file contents in the Immediate Window.
Next
End Using 'Auto-Dispose of instantiated object if not already disposed.

This is the same as implementing the following code:


Dim SR As New System.IO.StreamReader(Path) 'use a temporary SreamReader object to open a text file.
With SR
Dim Ary() As String = Split(SR.ReadToEnd, vbCrLf) 'read file (specified by the Path string variable) into an array.
SR.Close() 'close file and auto-Dispose.
For Idx As Integer = 0 To UBound(Ary)
Debug.Print(Ary(Idx)) 'display file contents in the Immediate Window.
Next
End Wit

NOTE: The object instantiated by Using...End Using is also required to implement System.IDisposable and expose a Public
Dispose method so that the End Using instruction can automatically destroy the object by invoking the object’s Dispose
method, otherwise the IDE will report an error. Built-in framework objects have this implementation, but user-defined objects
may need to update their class to support the Dispose method by implementing System.IDisposable, even if our
implementation does not do anything other than use the default code that is automatically inserted by our entering the
“Implements System.IDisposable” command line above the field zone within our class. The Dispose method is used to
release any possible unmanaged interop resources the class instantiated or reserved while being processed.

● A Short Note on the New ‘IsNot’ Keyword.


VB.NET now features an IsNot keyword, which avoids really weird syntax like “If Not (ObjItem Is
NewCLass) Then...”, instead allowing a more natural “If ObjItem IsNot NewClass Then... ”

● A Short Note on the New ‘AndAlso’ Keyword and Why We Should Use It.
VB.NET supports a logical AndAlso operator. It is similar to the logical And operator, except that if
a member on one side is found to be False, then any logical comparison processes end, being moot.
This significantly speeds program execution. Indeed, I recommend we forget all about using logical
And again and always use AndAlso. This is called Short-Circuit Evaluation. This works exactly like
the regular logical And operator “&&” under C++ and C#.

● A Short Note on the New ‘OrElse’ Keyword and Why We Should Use It.
VB.NET supports a logical OrElse operator. It is similar to the logical Or operator, except that if a
member on one side is found to be True, then any logical comparison processes ends, being moot.
This significantly speeds program execution. Indeed, I recommend we forget all about using logical
Or again and always use OrElse. This is called Short-Circuit Evaluation. This works exactly like the
regular logical Or operator“||” under C++ and C#.

● A Note on Overloading, and Why We Should Welcome It.


VB.NET supports Operator Overloading, allowing us to declare definitions for object operators
such as “+” or “–” or “=” for user-defined classes. Perhaps more importantly for most developers, it
also supports Method Overloading, allowing different parameter counts and types for same-named
methods. For instance, we can declare separate functions, all identically named MakeInt, that will all
return type Integer, but each of their parameter definitions will accept a different type value, such as
String, Long, Short, Double, or whatever. That way we will not need to stop and think about which
method to actually invoke; we simply use MakeInt and insert our parameter. The compiler will
figure out all the nitty-gritty details for us.
 Page 114

Consider these three overloads:


Public Function MakeInt(ByVal Value As Short) As Integer 'convert 16-bitvalue to 32-bit integer
Return CInt(Value)
End Function

Public Function MakeInt(ByVal Value As Single) As Integer 'convert 32-bit float to 32-bit integer
Return CInt(Value)
End Function

Public Function MakeInt(ByVal Value As Double) As Integer 'convert 64-bit float to 32-bit integer
Try
Return CInt(Value)
Catch ex As Exception 'usually an overflow or underflow error
MsgBox(ex.Message) 'report description of error
Return 0 'return an integer default of 0
End Try
End Function

● A Short Note on New Variable Types SByte, UShort, UInteger, ULong, and UintPtr.
VB.NET supports the new Integer value types SByte (Signed Byte), UShort (Unsigned Short;
UInt16), UInteger (Unsigned Integer; UInt32), ULong (Unsigned Long; UInt64), and UIntPtr
(Unsigned IntPtr (Integer Pointer)), all to enable signed bytes and unsigned integers.
NOTE: An IntPtr is a platform-specific type that can be used to represent a memory pointer or a window handle.

● A Note on Partial Classes and Structures.


A Partial class or structure is where a class or structure is defined with the new Partial verb,
allowing multiple source files to be used to define a single class or structure. Note that only one of
the separate declarations, usually that which is developer-modified, is allowed not to include the
Partial operator. This also allows CLR-generated code, such as for Form
classes, to be stored in a separate file, away from developer-generated Form
code. However, the developer is also free to exploit this Partial verb,
enabling them to break up their own complex class or structure bodies into
more than one file. For example, we can declare several separate class files
for “myClass”, specifying “Partial Class myClass” in all of them (but maybe
optionally declaring only “Class myClass” in the root class declaration).
If we select the Solution Explorer, “Show All Files” option, we will
see additional files listed under forms. For a form named
frmAPIViewer.vb, we will see FrmAPIViewer.resX, the form’s XML-based resource file, and also
frmAPIViewer.Designer.vb, which is the CLR-generated source code used for form suppor. If we
open the frmAPIViewer.Designer.vb file, we would see something similar to this in its heading:
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class frmAPIViewer
Inherits System.Windows.Forms.Form
<System.Diagnostics.DebuggerNonUserCode()> _
Public Sub New()
MyBase.New()
'This call is required by the Windows Form Designer.
InitializeComponent()
End Sub
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then
components.Dispose()
End If
Finally
MyBase.Dispose(disposing)
End Try
End Sub...

Although we, in our own partial class files, do not have to get so elaborate as to need to add special
compiler instructions (the “jargony” stuff embraced by angle brackets), we should certainly get the
idea. For example, if I do any form subclassing, which I do quite often, I like to place the
subclassing code within its own separate Partial Class form file.
 Page 115

● A Note on VB.NET Form-Linked Controls.


New VB.NET form-linked controls make developing form features easy, replacing old-style VB6
form-resident controls, such as this sample list of VB.NET controls:
o WebBrowser o OpenFileDialog (replaces CommonDialog)
o MaskedTextBox o SaveFileDialog (replaces CommonDialog)
o MenuStrip (replaces VB6 main menu) o ColorDialog (replaces CommonDialog)
o StatusStrip (replaces VB6 StatusBar) o FolderBrowserDialog (replaces CommonDialog)
o ToolStrip (replaces VB6 Toolbar) o FontDialog (replaces CommonDialog)
o Timers

NOTE: Most old VB6 controls and DLL services can still be implemented
and freely used within VB.NET projects. The means to being able to do this
is amazingly easy, and are discussed in detail in “Notes on No AVI
Animation Control in VB.NET and How to Add a Free One” on page 86,
where I show how to add (and get) a free Microsoft VB6 Animation control
to use in VB.NET.

● A Short Note on Snippets.


The user can access and import dozens of pre-written code snippets via the right-click popup menu.
Further, by adding a free Snippet Editor, we can add our own favorite code snippets to the collection
without the usual need to write it using copious angle brackets. See Microsoft’s CodePlex Open Source
Project Community site: http://snippeteditor.codeplex.com/, complete with source code. This version fully supports
VB.NET since VB2005, to include Express Editions.

● A Short Note on the Immediate Window.


VB.NET features an Immediate Window (opened with Ctrl-G), which was added in VB2005. It
provides for evaluating expressions while debugging, or changing variable values. It is the same as
that for VB6. Because the cross-language compiler is also continuously active in the background of
the IDE, we can even evaluate expressions while not debugging, just as we could under VB6.

● A Short Note on the Continue Statement.


The Continue statement was added in VB2005, allowing program control to immediately pass to the
loop control test of a Do, For, and While loop, duplicating the Continue statement in C#/C++. This
idea was one of my many modest contributions to the VB.NET language. I love this one.

● A Note on Structured Exception Handling.


VB2008 expanded the capabilities of the Try...End Try error trapping structure to feature Structured
Exception Handling. This expansion allows us to embed more than one Catch block to enable
detecting more than one type of error that might have occurred within the wider Try block, trapped
with the new “When” keyword. For example, a Catch block beginning with “Catch When Err.Number =
53”. Such special Catch blocks would precede more general ones, such as “ Catch ex As Exception” or
even simply “Catch”. This also includes the ability to embed deeper Try...End Try blocks within
Catch blocks. We can even ignore errors by implementing an empty Catch block, duplicating VB6's
much-loved On Error Resume Next command, but without also needing an On Error Goto 0.

● A Note on Type Inference.


Type inference, or implicit typing, refers to the ability to deduce automatically the type of a value in
a programming language. The ability to infer types automatically makes many programming tasks
easier, leaving the programmer free to omit type annotations while maintaining some level of type
safety. For example, “Dim Valu = 1234” will define Valu as a 32-bit Integer. Floats default to Double. I,
personally, cringe at this type of programming, because I always want to know my storage types at
all times, which makes for much easier debugging, in case my code forgot itself and had a bug in it.
 Page 116

● A Note on Anonymous Typing.


Anonymous types allow data types to encapsulate sets of properties into a single structure without
having to first explicitly define it, allowing us to build structures seemingly “on the fly”. This gives
programmers much of the convenience of a dynamic language, but with the type safety of a statically
typed language. For example, Dim person = New With {.FirstName = "John", .LastName = "Smith"} creates a
user-defined-type structure on-the-fly containing two members: FirstName and LastName. Note that we
can (and should) declare member types, such as “.FirstName As String = "John"”, otherwise they will
default to type Object and not be resolved until the much slower Late-Binding process.

● A Note on Enumerators.
Enumerator members no longer need explicit constant value assignment as of VB2005; they can now
auto-increment by an integer value of 1, just as had been done under VB6. Thus, the following
declaration is allowed (the first item, if not explicitly assigned a value, defaults to a value of 0):
Enum MyConstants As Integer 'declare an optional explicit storage class (I STRONGLY suggest this for faster-running code).
myColors 'this defaults to 0.
myShapes 'this defaults to 1 (auto-increment from previous).
myBorders = 4 'explicitly set a value of 4.
myCorners 'this defaults to 5 (auto-increment from previous).
End Enum

NOTE: VB.NET thankfully requires we qualify a member with its Enum Container name, such as MyConstants.myColors.
Some VB6 users whine about this, but if they want to use just the member names, then use Constants. If they only knew
the countless naming collision problems this has caused legions of VB6 users, they would not advertise their ignorance.
SECRET NOTE: Enumerators, and the reason they require specifying their members with their enumerator type, is
because they are abstracted classes, basically being like Structures modified with a lot of limitations.

● A Note on Optional Event Parameters.


The inclusion of the standard " sender As Object, e As EventArgs" in event handlers has become optional.
These parameters are not typically used in simple events that handle only one object, such as a single
button click. For example, the following VB.NET button click event:
Sub btnClose_Click(sender As Object, e As EventArgs) Handles btnClose.Click
Me.Close()
End Sub

Can now be manually edited to be declared more simply as this, and it will still run OK:
Sub btnClose_Click() Handles btnClose.Click
Me.Close()
End Sub

NOTE: If a button event handles multiple buttons (the Handles declaration can list multiple attached objects, for
example), then checking the “sender” argument is essential if we need to know exactly which button was actually
clicked. Further, removing these parameters also disables the IDE’s ability to take us straight to the default event source
code if we double-click a form object. It will instead simply take usstraight to the top of the source code file. This is
because the modified declaration and the auto-generated Delegate for the event, their signatures, will not match (a
Delegate is a class that just acts like a method prototype). As such, the system will associate a simpler Delegate to it.

● Notes on the MY Namespace.


VB2005 introduced new features for Rapid Application
Development that greatly improves productivity. One of these
features, the My namespace, enables quick access to frequently
used information that is normally buried deeply within the .NET
Framework. My also provides information and default object
instances that relate to the application and its run-time
environment. This information is organized in a format that is
accessible through IntelliSense and logically delineate according to use. See later in this document
for a more detailed look at this very powerful feature.
 Page 117

The My namespace acts as a wrapper to “speed dial” common tasks. It simplifies access to otherwise
difficult-to-obtain information, including Information about the program, the computer, the forms,
resource objects and strings, program settings, and the current user. For example, previously, to get
an app’s major version, we had to ‘jump through hoops’ and do it like this: Dim Major As Integer =
System.Diagnostics.FileVersionInfo.GetVersionInfo(System.Reflection.Assembly.GetExecutingAssembly.Location).Fi
leMajorPart. This is now just “Dim Major As Integer = My.Application.Info.Version.Major ”.

● Notes on the Return of the Forms Collection.


One common task in VB6 was showing an instance of a form through the application's Forms
Collection. As of VB2005, this much-missed collection has returned. We can once more invoke an
instantiated form like this: My.Forms.Form2.Show. The new auto-generated My.Forms collection exposes a
reference to each loaded form in the application. We can even abbreviate this further by leaving off
the “My.Forms” prefix and just use Form2.Show (this will not affect the size of the compiled code), giving
we the same syntax as VB6, although with a very different underlying implementation.
Prior to the release of VB2005, because forms in .NET are reference objects, Form2 in the above
example, being an instance of a form class occupying (consuming) memory space, had to be
carefully tracked by programmers, so when their applications ended, they could make sure to close
down and release all the resources their forms and controls consumed from the computer's memory,
so to avoid responsibility for the much-dreaded memory leak, Unchecked, enough of them can lock
up a computer's memory. Having a Forms Collection, just like a form (once again) having a Controls
Collection, allowed developers to very quickly verify what forms (or controls) had not yet been
uninstantiated, and so dispose of them. Indeed, with .NET, if a program forgot to close them, the
.NET framework can While loop through the Forms collection and release them (I wish they had a
reverse For...Each, like For Each frm As Form In -Forms ), just as each form itself can use its
Controls Collection to dispose of controls, and all containers.
Under VB6, we could also locally over-ride a class by declaring something like “Load Form2”, which
can be duplicated under VB.NET by using something like “Dim Form2 As New Form2”. Rest assured that
this does not over-write its base class definition; just the references local to the instance, thanks to
Scoping Rules. Also, the base class can be referenced outside this instance's scope just fine, or we
can access it locally in the form using either Me.BaseClass, or by embracing the name in square
brackets, like “[Form2]”, to inform the compiler we are locally referring to the base class Form2.

● Notes on Class Construction and Destruction – a Detailed Look.


The Sub New and Sub Finalize procedures in VB.NET initialize and destroy
objects; they replace the Class_Initialize and Class_Terminate methods used in
VB6 and earlier. Unlike Class_Initialize, the Sub New constructor can run only
once for each object as it is created. It cannot be invoked explicitly anywhere
other than in the first line of code of another constructor from either the same
class or from a derived class (a class that inherited the referenced class). Furthermore, the code in the
Sub New method always runs before any other code in a class. As of VB2005, VB implicitly creates a
New constructor at run time in our classes if we do not explicitly define a Sub New procedure in them.
NOTE: VB6 had a Static Sub New constructor that executed the first time a class was instantiated. This non-standard
feature is not needed in OOP due to being able to Share variables among instances. See the example on page 52.
Before releasing objects, the .NET CLR (Common Language Runtime) automatically invokes its
Finalize method for objects that have defined a Sub Finalize procedure. The Finalize method
should contain code that must execute before an object is to be destroyed, such as for closing files
and saving program state information. There is naturally a tiny performance penalty for executing
Sub Finalize, so we should define a Sub Finalize method only when we must explicitly release
unmanaged (non-CLR) resources, which the Garbage Collector lacks the knowledge to dispose of.
 Page 118

NOTE: The .NET garbage collector cannot dispose of unmanaged objects; objects that the operating system executes directly,
outside the CLR environment. This is because different unmanaged objects must be disposed of in different ways. That
information is not directly associated with the unmanaged object; it must be found in its documentation. A class that uses
unmanaged objects must dispose of them in its Finalize method. This is addressed in a later example for closing non-typical
MS Office COM objects (COM objects are called unmanaged types because they all operate outside .NET’s CLR).
The Finalize destructor is a protected method that can be invoked only from the class it belongs to,
or from derived classes' own Finalize method. The system invokes Finalize automatically when an
object is actually being destroyed, so we should never explicitly invoke a class’s Finalize method.
Unlike VB6’s Class_Terminate, which executed when an object is set to Nothing, there may be a short
delay from when an object on the Heap completely loses scope and when the CLR invokes the object’s
Finalize destructor, which happens only during the continuously-running garbage collection cycle.
VB2005 and later editions also allows a second type of destructer that is built into all current .NET
intrinsic objects, called Sub Dispose, which can be explicitly invoked at any time, and allows one to
immediately release resources, just as many developers were used to doing under VB6. In fact, the
Dispose method does what setting an object to Nothing did under VB6, but it will also do it better.
Class instances sometimes reference resources that are not managed by the CLR, such as non-CLR
device context handles, database connections, memory space allocated via Pinvokes, and so on.
Therefore, we should provide both an explicit and an implicit way to free those resources. We
provide implicit control by implementing the Finalize method on an object. The CLR's Garbage
Collector invokes this method when it finds that there are no longer active references to the object.
In special cases we may need to give developers the ability to explicitly release external resources
before the garbage collector frees their encapsulating object. If an external resource is scarce or
expensive, better performance can be attained if the developer explicitly releases resources when
they will no longer be used. To provide explicit control, we can add our own Dispose method in our
objects by implementing the IDisposable interface in our class. The consumer of the object should
then invoke this method when it finishes using the object.
NOTE: Dispose can be invoked even if other references to the object still exist. This is where VB6 objects often got into
a lot of trouble, because under VB6, if more than one reference to an object existed and one reference was set to
Nothing, obliterating the object's data, if we accessed one of the other still-pointing references by invoking a class
member method or accessed one of its properties, even if just to check if it was still active (not simply also setting it to
Nothing, which would just disable the connection to the target object), the object would suddenly be resurrected,
sometimes causing all sorts of headaches, to include applications that will not terminate when they are “supposed” to,
because a prerequisite for closure is the destruction of an object, which it keeps detecting as existing. Although it is
possible to still do this under VB.NET, it can only happen within the code block where it was declared, where it will still
exist until the block goes out of scope, and nowhere else, greatly minimizing this potential VB6-like resurrection.
Be aware that the Dispose method should be written in such a way that it can be invoked multiple
times, but it should actually deallocate resources only the first time through (as of VB2010, Dispose
now includes code to do this for us). This is possible because an object’s program code always exists
during the lifetime of an application, as it did under VB6, though not its data, which is treated as the
actual object. Further, if we also include a Finalize method in case the Dispose method is not
invoked, to ensure critical data space is released, normally allocated outside the CLR in unmanaged
memory, to avoid the Garbage Collector invoking Finalize after we released the data via Dispose,
we should close out the Dispose method by invoking GC.SuppressFinalize(Me) before exiting its
resource-releasing code.
Note that even when we provide explicit control using Dispose, we might still need to provide
implicit cleanup using a Finalize method. Finalize provides a backup to prevent any unmanaged
resources from permanently leaking whenever the programmer fails to invoke Dispose, which can be
quite often. The Finalize method could, ideally, simply invoke the Dispose method.
 Page 119

The following rules explain the usage guidelines for the Finalize method:
1. Implement Finalize only on objects requiring it. There is a slight performance cost to Finalize methods.
2. If we think we need a Finalize method, consider implementing IDisposable to allow users of our class to
avoid the cost of invoking the Finalize method, and enabling them to dispose of resources immediately
(note that the Dispose method includes commented-out prototype code for a Finalize method. Example
code will soon be provided).
3. Do not make the Finalize method more visible. It should be Protected, never Public.
4. An object's Finalize method should free any external resources (unmanaged space, such as COM objects)
that the object owns. Also, a Finalize method should release only the resources that its actual object has
held onto. The Finalize method should never reference any other objects that it does not manage..
5. Do not directly invoke a Finalize method on an object other than the object's base class.
6. Be sure to invoke the base class's Finalize method as the last line in our object's Finalize method.
7. A Finalize destructor should not throw exceptions. This is because such exceptions cannot be handled by
the application due to the object being destroyed, and therefore in extreme cases it can cause the
application to terminate, because an object exception must always reference an object (Duh!). This might
ultimately cause “memory leaks”, where allocated memory for the application is not released when the
application terminates abnormally, and clogging available resources until the system is rebooted.
The following rules explain the usage guidelines for the Dispose method:
1. Implement the dispose method on a class that encapsulates resources that explicitly need to be freed, such
as unmanaged Interop memory allocations, COM objects, or even large local resources, like memory-
hungry images and large tracts of text (simply add “Implements IDisposable” within the top bracing of our
class – see the example at the end of this list). Place our actual resource cleanup code in the “ Protected
Overridable Sub Dispose(ByVal disposing As Boolean) ” implemented method. Users can free these external
resources by invoking the public Dispose method, “Public Sub Dispose()”.
2. Implement the dispose method on a base class that commonly has derived classes that hold onto resources,
even if the base class does not. If the base class features a Close method, this often indicates the need to
implement Dispose. In such cases, do not implement a Finalize method on the base type. Finalize should be
implemented only in any derived types that actually introduce resources that require cleanup.
3. Free any disposable resources a class owns in its Dispose method.
4. After Dispose has been invoked on an instance, if a Finalize method also exists, prevent it from executing by
invoking GC.SuppressFinalize(Me) in the Dispose method. The exception to this is in the rare instance where
work must also be done in Finalize that is not covered by Dispose. In such cases, a flag should be added to
inform Finalize if the Dispose method had been invoked, to prevent it from trying to free resources that would
have already been freed by Dispose. As of VB2010, this is now provided by a disposedValue Boolean Field.
5. It is a best practice to always invoke the Dispose method of the base class if it implements IDisposable,
which will automatically write the skeletal base code for us when we implement IDisposable.
6. Do not simply assume Dispose will be invoked. Unmanaged resources owned by a type should also be
released in a Finalize method in the event that Dispose is not invoked. So be sure to implement rule 4 in
our Dispose method. The Dispose method now automatically does this as of VB2010!
7. Throw an ObjectDisposedException from instance methods on this class (other than Dispose) when
resources are already disposed. This rule does not apply to the Dispose method because it should be able
to be invoked multiple times without throwing an exception for the simple reason that Dispose can be
invoked even when multiple references to the object still exist (a situation that sometimes cannot be
avoided, and raised havoc with memory leaks in VB6). In reference to rule 7 of the usage rules for the
Finalize method, this exception, or any exception should not be thrown from the Finalize method either.
8. Propagate invokes to Dispose through the hierarchy of base types. The Dispose method should free all
resources held by this and any object owned by this object. For example, we can create an object such as
a TextReader that in turn internally instantiates a Stream and an Encoding object, both of which are
created by the TextReader without the user's knowledge. Furthermore, both the Stream and the
Encoding object can acquire external resources. When we invoke the Dispose method on the
TextReader, it will in turn invoke Dispose on both the Stream and the Encoding objects, causing them
in their turn to release their own external resources (its Close method auto-invokes its Dispose method).
 Page 120

9. Consider not allowing an object to be usable after its Dispose method has been invoked. Re-creating an
object that has already been disposed is a difficult pattern to implement, and not of any real sensible use,
anyway, though VB6 did it with crazy-eyed abandon, to the brutal consternation of VB6 developers.
10. Allow a Dispose method to be invoked multiple times without throwing exceptions. This allows multiple
references to free their connections to the object. Use a Boolean field in the class to indicate if the
Dispose method has been invoked. The Dispose method should simply do nothing after its first
invocation. As of VB2010, the Dispose method has this flag built right into its code.
What follows is an empty class with IDisposable implemented, so we can see its auto-created code:
Option Explicit On
Option Strict On

'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' Empty Class With Idisposable.
'
'Sample class to expose IDisposable implementation.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Public Class EmptyClassWithIDisposable
Implements IDisposable

#Region "IDisposable Support"


Private disposedValue As Boolean 'This flag was introduced in VB2010. It allows Dispose to run just once.

' IDisposable
Protected Overridable Sub Dispose(disposing As Boolean).
If Not Me.disposedValue Then 'if this object has not already been disposed... '
If disposing Then 'and if we are trying to dispose of it... '
' TODO: dispose managed state (managed objects). 'code to dispose of our MANAGED objects goes here. '
End If 'NOTE: Finalize must not handle Managed Objects. '
'code to dispose of any UNMANAGED objects goes here'
' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
' TODO: set large fields to null.
End If
Me.disposedValue = True 'indicate Dispose() has been run. '
End Sub

' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources.
'Protected Overrides Sub Finalize() 'NOTE: Notice this Finalize method is commented out. Uncomment if needed.
' ' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
' Dispose(False)
' MyBase.Finalize()
'End Sub

' This code added by Visual Basic to correctly implement the disposable pattern.
Public Sub Dispose() Implements IDisposable.Dispose
' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
Dispose(True)
GC.SuppressFinalize(Me) 'prevent any Finalize method from invoking us. '
End Sub
#End Region 'NOTE: Were you paying attention? This object has been Disposed, right? How do you figure that the
' disposedValue property still exists? An OOPL has to do a LOT of slight of hand tricks to
End Class ' keep control of object space. This is one of them... Read the SECRET NOTE below.

SECRET NOTE: If we have multiple static references to an object, consider adding a Friend ReadOnly Property to
the class, such as IsDisposed, that just returns the above Boolean disposedValue. This way we can detect if the object
has already been disposed of, and if so, we can just set a still-active reference to Nothing, thus allowing the Garbage
Collector to release the object's allocated space. Even though any memory-expensive allocations would have been
released by a orevious invocatiopn to Dispose, the base object will still exist, to include periphery flags and fields. until
there are no viable references to that object. What is important here is that the Garbage Collector cannot deallocate
ALL of an object's space from the System Heap until there are no longer any active references to that object's data
space, even if the object has actually been disposed of through a Dispose method invocation! The closest Microsoft
has come to actually revealing this was in Rule 7 of both the above Finalize and Dispose guidelines.

● Notes on New Style Pinvoke Signatures – a Detailed Look.


VB.NET has a new technique for declaring Pinvoke Signatures. Even though the VB6 method of
declaring Pinvoke Signatures (called API Functions or API Calls under VB6) is still fully supported
as legacy code in VB.NET, it is recommended by Microsoft that we adopt the .NET format for
current development projects (although I like it, most of the time using Declare is faster).
 Page 121

Rather than describe how to convert VB6-style Pinvoke Signatures to the new VB.NET format, the
following examples probably explains it much more clearly:
VB6 format examples that have been upgraded to VB.NET (which still work under VB.NET):
Public Declare Function SHBrowseForFolder Lib "shell32" (
ByRef lpbi As BROWSEINFO) As Integer

Public Declare Function SHGetPathFromIDList Lib "shell32" (


ByVal pidList As Integer,
ByVal lpBuffer As String) As Integer

Public Declare Function SHGetSpecialFolderLocation Lib "shell32" (


ByVal hWndOwner As Integer,
ByVal nFolder As Integer,
ByRef ListId As Integer) As Integer

New .NET format examples of the same Pinvoke Signatures:


Imports <System.Runtime.InteropServices

<DllImport("shell32.dll")>
Public Shared Function SHBrowseForFolder(
ByRef lpbi As BROWSEINFO) As Integer
End Function

<DllImport("shell32.dll")>
Public Shared Function SHGetPathFromIDList(
ByVal pidList As Integer,
ByVal lpBuffer As String) As Integer
End Function

<DllImport("shell32.dll")>
Public Shared Function SHGetSpecialFolderLocation(
ByVal hWndOwner As Integer,
ByVal nFolder As Integer,
ByRef ListId As Integer) As Integer
End Function

Further, if we want to define the function entry point (Alias), specify the character set of the string to
be sent, or provide other additional attributes in the declaration, we can do something like the
following (exclude the InteropServices parameters we do not need to specify):
<DllImport ("kernel32.dll", (the DLL containing the method we need to access)
EntryPoint:="MoveFileW", (the actual invoked method that MoveFile will be forwarded to, forcing Unicode)
SetLastError:=True, (True is the default)
CharSet:=CharSet.Unicode, (Unicode is the default, and would force the wide (MoveFileW) method invocation)
ExactSpelling:=True, (True is the default)
CallingConvention:=CallingConvention.StdCall)> (StdCall is the Default. Very FEW Pinvokes specify Cdecl)
Public Shared Function MoveFile(ByVal src As String, ByVal dst As String) As Boolean
' This function copies a file from the path src to the path dst. Leave this function body empty.
' The DLLImport attribute forces invocation to the alias moveFile to be forwarded to MoveFileW
' in KERNEL32.DLL. Note that the parameter specifier ':=' allows us to specify only used
' parameters, not all possible, or even apply them out of their originally declared order.
End Function

A different and legal way to declare this Pinvoke Signature using the VB.NET format is as follows:
<DllImport("kernel32")>
Public Shared Function MoveFile(
<[In](), MarshalAs(UnmanagedType.LPTStr)> ByVal lpExistingFileName As String,
<[In](), MarshalAs(UnmanagedType.LPTStr)> ByVal lpNewFileName As String) As Integer
End Function

NOTE: We are also optionally able to declare if a parameter is sent to a function as a constant [In], where the data
should be marshaled from the caller to the callee, but not back to the invoker, or if a parameter will receive a result
[Out], where the invoked function should marshal the data back to the invoker. Were we to wish to use these special tags,
which I have yet to find a situation that really required them, in most cases we will use [In]. They are also useful for self-
documenting the code. Were we to look at MSDN help for a Pinvoke, it details more options, which can help we to
determine how we should implement it. MSDN states that if an item is In (the function reads from the buffer; the invoker
provides the buffer and initializes it), Out (the function writes to the buffer; if used on the return value, the function
provides the buffer and initializes it, otherwise, the invoker provides the buffer and the function initializes it), InOut (the
function both reads from and writes to the buffer; the invoker provides the buffer and initializes it), or Opt (the
parameter can be NULL).
 Page 122

I must admit that I greatly prefer and use the VB6 format, not only because its definition is terser than the VB.NET
format, but also because the VB6 “Declare” format can also be defined without modification within Modules, whereas
DllImport format functions presently cannot, unless we remember to remove (or never add) the Shared declaration when
adding them to Modules (because they are automatically shared).
Under VB.NET, Module classes are automatically imported, and imported member functions cannot
also be declared as Shared because they are already shared publicly (the automatic importation of
them causes their Public/Friend members to be treated as Shared Public/Friend and as a non-
inheritable class). This should not be confused with the fact that the Shared keyword can also be
used to specify that a data member it may be associated with will be commonly shared by all
instances of the class that it is declared within. However, that Shared members cannot be shared with
other outside code that might try to access the shared item (directly, anyway) without first qualifying
it through an object that is in fact an instance of the encapsulating class (was that a mouthful, or
what?).
But in plainer language: to avoid syntax errors, do not bother tagging module members as Shared
unless we are explicitly creating a non-inheritable class, which will require it.
Though the Charset parameter makes the DllImport format valuable, we can also append “Auto” (or
“Ansi”, or “Unicode”) after the VB6-style Declare verb to duplicate that feature. Further, although
StdCall is the default calling convention to unmanaged methods with Platform Invoke (StdCall
indicates that Pinvokes will clear the parameters off the stack), which means that we do not really
need to explicitly specify it, we may wish to, simply for self-documentation purposes. Cdecl (the
invoker will clear the stack) is used for invoking functions written in C++ and C# (unless their
functions are explicitly declared using the StdCall verb, or the /Gz command line compiler
parameter is present, which means all methods, unless otherwise explicitly declared, are to be
considered StdCall, which is the usual case, as StdCall uses significantly less code).
The StdCall convention is used to summon Win32 Pinvokes; the callee cleans the stack. However,
variable count argument (ParamArray) methods must be declared Cdecl, because the callee cannot
predetermine argument counts. Methods using this calling convention require a function prototype.
The following list shows the implementation of the StdCall calling convention:
Element Implementation
Argument-passing order Right to left (last is first to be popped, first is last to be popped from the stack (parameters are pushed
onto the program stack in FILO – First-In/Last-Out – order)).
Argument-passing convention By value (ByVal), unless a pointer or reference type is passed (ByRef).
Stack-maintenance responsibility Called function pops its own arguments from the stack, flushing it of all arguments to the method.
Name-decoration convention An underscore (_) is prefixed to the method name. The name is followed by the at-sign (@) and then
followed by the number of bytes (in decimal) in the argument list. Therefore, a function declared as “int
func( int a, double b )” is decorated as follows: _func@12 (int=4 bytes + double=8; 12 bytes)
Case-translation convention None (hence, case sensitive).

The default calling convention for C++ and C# programs is Cdecl. Because the stack is cleaned up
by the caller, it can support variable count arguments methods (ParamArrays). The Cdecl calling
convention creates larger executables than StdCall because it requires each function to provide stack
cleanup code. The following list shows the implementation of the Cdecl calling convention:
Element Implementation
Argument-passing order Right to left (last is first to be popped, first is last to be popped from the stack (parameters are
pushed onto the program stack in FILO – First-In/Last-Out – order)). Same as StdCall.
Argument-passing convention By value (ByVal), unless a pointer or reference type is passed (ByRef). Same as StdCall.
Stack-maintenance responsibility Calling function pops the arguments from the stack, flushing it of all arguments to the method.
Name-decoration convention Underscore character (_) is prefixed to method names, except when exporting __cdecl functions that
use C linkage.
Case-translation convention None (hence, case sensitive). Same as StdCall.

NOTE: Even though the ByVal and ByRef references in the above tables may indicate to us that perhaps we should pass
strings to Pinvoke Signatures ByRef, note that in that case we must then marshal the string by preceding it with
“<MarshalAs(UnmanagedType.VBByRefStr)>” in the form “<MarshalAs(UnmanagedType.VBByRefStr)> ByRef myString As
String” (found in System.Runtime.InteropServices). HOWEVER, as of VB2005, to be fully OOP-compliant, we can
now pass strings (Array objects of type Char) ByVal, which will still send a reference pointer to the base of the string to
 Page 123

the unmanaged code. This is because any OOP object reference, sent ByVal or ByRef, it still sends a pointer to the base
address of the object, as is done ByRef, but ByVal would simply send a copy of that pointer, which will still point to the
base address of the string!. For managed memory safety, pre-2005 VB.NET had always sent managed strings as
unalterable Constants to Pinvokes, so even if they were sent ByRef, returned strings would still never be modified by
unmanaged code. At that time, the only way around this string parameter issue, in the very rare instances where the
Pinvoke would pass back a string via one of its parameters (the Pinvoke Signatures for GetWindowsDirectory,
GetSystemDirectory, and GetCurrentDirectory come to mind), was to use the managed modifier as shown above.
VB2005 introduced simpler OOP-compliant techniques, which will be discussed below, and more fully in a later article,
entitled “Upgrading Data Types for Win32 Pinvokes”, on page 199.
Consider the following:
Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" (
<MarshalAs(UnmanagedType.VBByRefStr)> ByRef lpBuffer As String,
ByVal nSize As Integer) As Integer
'you can replace the whole ByRef line with "ByVal lpBuffer As String" to get the EXACT SAME RESULT, as of VB2005.
Dim S As String = New String(Chr(0), 260) 'init receiving buffer. Also try: Dim S As String = Space(260).
Dim I As Integer = GetSystemDirectory(S, 260) 'now get system directory to String S and length to I.
S = S.Substring(0, I - 1) 'assign S to returned text, less also-counted Null terminator.

Above, we marshaled a ByRef string using the VBByRefStr modifier.


A StringBuilder variation of this, which many online gurus usually recommend, is shown below:
Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" (
ByVal lpBuffer As StringBuilder,
ByVal nSize As Integer) As Integer

Dim sb As New StringBuilder(260) 'use StringBuilder to get API return data.


GetSystemDirectory(sb, sb.Capacity) 'get system directory to StringBuilder and ignore Return length.
Dim S As String = sb.ToString 'aquire returned text as string. ToString reads up to Null.

For completeness, considering the subject we are now on, following are the new VB.NET formats for the above two
GetSystemDirectory Pinvoke Signature variations:
<DllImport("kernel32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>
Public Function GetSystemDirectory(
<MarshalAs(UnmanagedType.VBByRefStr)> ByRef S As String,
ByVal length As Integer) As Integer 'or 'ByVal S As String, ByVal length As Integer' if VB2005+
End Function

<DllImport("kernel32.dll", CharSet:=CharSet.Auto, SetLastError:=True)>


Public Function GetSystemDirectory(
ByVal sb As StringBuilder,
ByVal length As Integer) As Integer
End Function

NOTE: As of VB2005, we can bypass the whole ByRef string attribute hodgepodge and instead simply pass the string
ByVal to the Pinvoke, exactly as we would have done it under VB6, to pass a reference pointer to it. This actually makes
it comply with OOPL standards, because a string is in fact an object, and so it will by default pass a reference pointer to
the first character of its string (pass it a Clone if we want to ensure the original string will not be altered). Internally, if
necessary for ANSI conversion, .NET will copy the original string to an 8-bit array and pass a reference to it to the
Pinvoke, then translate it back to the native type upon return if the 8-bit array was modified. This eliminated one more
VB6 user complaint. Previously, passing a value ByVal would strictly not allow alteration of the string in Interop. This
means that the above ByRef string examples can now simply be passed ByVal, and the marshaling tags would no longer
be needed. This just goes to show that if us whiny, knuckle-dragging drool-monkey users scream loud enough... Granted,
Microsoft had been violating OOPL protocols, but their intention had been to protect our data.
 Page 124

● A Note on the VB.NET Form Event Firing Sequence.


The VB.NET event firing sequence, when a form is loading, has a different order than VB6. Under
VB6, we used the I Love (or Loathe) RAP slogan to remember the order: Initialize, Load, Resize,
Activate, and Paint. Under VB.NET, the Initialize process no longer fires an event (it is replaced by
an internal invocation to InitializeComponents, which at its end invokes PerformLayout, which in
turn will naturally invoke the main form's TextChanged and Resize events, if they are defined).
Indeed, the exact firing order of initial form events under VB.NET is now TRLAP; TextChanged,
Resize, Load, Activate, and Paint; Trouble Really Loves A Programmer?
I wish the Load event would fire first, and any declared TextChanged and Resize events would
simply be ignored until the form has been fully loaded, but this is impractical because the form can
be displayed by the user before it has finished running its Load event. But so I dream, because the
internal PerformLayout method would naturally invoke other events, such as TextChanged when
object text properties are assigned, and Resize events when form objects are redimensioned.
However, be it as it may be, if we have TextChanged and/or Resize event methods defined for a
form, we can be sure that those events WILL fire before the form is fully loaded, and even before the
form’s Load event is processed. This can result in some frustration on our part and errors on VB’s
part, because, if the form is not yet loaded, these events can trigger an exception error.
We can eradicate this frustration by declaring a Boolean flag at the procedure-level of any form,
named mFormLoaded (Private mFormLoaded As Boolean = False ). This should be set to True (mFormLoaded =
True) at the bottom of the form’s Load event code. Then, as the first statement in each existing
TextChanged and Resize event we have on a form, we can insert the following simple statement:
If Not mFormLoaded Then Return 'do not process this event until the form has been flagged as having been loaded.

● Notes on Form Command Changes – a Detailed Look.


Form commands have changed. We will quickly notice that some of the standard form processing
commands we may have used in VB6 no longer exist under VB.NET. Well, actually they still do
exist, but just in a form more open to our inspection than they were defined under VB6, where most
everything was hidden from us. Mind you, these changes were not put in place to frustrate us, though
maybe sometimes they do; they were put in place to enable us to have not partial, but complete
control over them, plus to also fully support cross-language interoperability.
When I started using VB.NET, I initially felt uncomfortable because so many familiar VB6 form-
processing commands I depended on seemed to be missing, such as the Load, Unload, and the
Query_Unload events. But as I found time to conduct experiments and research them in the neigh-
overwhelming tomes of fresh documentation, I found the way VB.NET handled these functions was
similar to how VB6 had previously done it for us, but from “behind the green curtain”.
Also, as stated in the previous note, I noticed that events I depended upon were now either gone, or
they fired in a different order. Under VB6, the form startup event sequence was Initialize, Load,
Resize, Activate, and Paint. Under VB.NET, Initialize was moved to the background, and now
TextChanged and Resize processes fire events as untouchable initialization processes cobble our
application together. Indeed, that order is now TextChanged, Resize, Load, Activate, and Paint.
The VB6 Load command was often used as just a way to instantiate objects without displaying them.
We often referenced controls by loading them into an indexed array. Forms were different in that we
could load new instances and assign them to a reference variable. Indeed, one thing we can still do
under VB.NET is simply load a form and address it through its object name. But, we could also
ignore the Load command in VB6 and go directly to Show. For example, if we had a form named
Form2, we could instantiate it to a reference variable thus:
 Page 125

Dim Form2 As Form2 'declare a new reference to type Form2 (not needed in VB6 if the Show command is used).
Set Form2 = New Form2 'declare new instance of form and assign to the reference (not really needed in VB6).
Form2.Show Me, vbModal 'show the new form as a dialog via the reference and make the current form its parent.
Unload Form2 'remove the current instance of the form.

The vbModal command allowed the new form to be displayed like a dialog box, where control of the
application cannot continue until the new form is closed. The vbModeless parameter told it to
display like a regular form, not hogging input focus.
VB.NET has two display methods: Show to show the form as an ordinary window (like
vbModeless), and ShowDialog to display it as a dialog, controlling focus until closed (like
vbModal). Close also removes the form resources by self-invoking its Dispose method, except if it is
displayed as a dialog, where we must invoke Dispose so we can first grab its DialogResult.
The above VB6 code would be written in VB.NET like this:
Dim Form2 As New Form2 'declare new instance of prototype form2.
Form2.ShowDialog(Me) 'show new form in modal/dialog format, make current form its parent.
Form2.Close() 'close form and release components (works like VB6 Unload command).
Form2.Dispose() 'mark form's components for deletion (expected if ShowDialog used; otherwise Close auto-disposes).

The VB.NET Close method acts very much like the VB6 Unload command. We can get by without
invoking the Dispose method if we use the Show method, because its Close method will
automatically invoke Dispose. However, Dispose must be explicitly invoked if the ShowDialog
method was used because in this case the Close method will not automatically invoke Dispose. The
reason for this is because we may want to obtain the form's DialogResult parameter value before
disposing of its resources, when this form result resource will be lost.
If we look to the documentation for the Close method (search Help for Form.Close), it states “When a
form is closed, all resources created within the object are closed and the form is disposed. You can prevent the closing of
a form at run time by handling the FormClosing event and setting the Cancel property of the CancelEventArgs passed
as a parameter to our event handler. If the form you are closing is the startup form of our application, our application
ends. The two conditions when a form is not disposed on Close is when 1) it is part of a multiple-document interface
(MDI) application, and the form is not visible; and 2) you have displayed the form using ShowDialog. In these cases,
you will need to call Dispose manually to mark all of the form's controls for garbage collection. ” Elsewhere, it states
the point more directly: “Dispose will be called automatically if the form is shown using the Show method. If
another method such as ShowDialog is used, or the form is never shown at all, you must call Dispose yourself within
your application.” Further, if we invoke Dispose before Close, the FormClosing event will not fire.
Alternatively, we are able to invoke Dispose from within the FormClosed event if we want to.
We can emulate the VB6 Show method easily, if we cannot live without it, using a simple module:
Option Explicit On
Option Strict On
Module modShowForm
Public Enum FormShowConstants As Integer
Modeless
Modal
End Enum
'*************************************************************************
' ShowForm emulates the VB6 method of displaying a form, specifying a
' parent, and selecting a display mode (Modal or Modeless).
'*************************************************************************
Public Function ShowForm(ByVal Form As Form,
Optional ByVal Modal As FormShowConstants = FormShowConstants.Modeless,
Optional ByVal OwnerForm As Form = Nothing) As DialogResult
If (OwnerForm IsNot Nothing) Then
Form.Owner = OwnerForm 'Init function result.
End If
ShowForm = Nothing 'Init DialogResult to Nothing. Always cover our bases.
If (Modal = FormShowConstants.Modeless) Then 'In this case, we can invoke ShowForm like a Subroutine.
Form.Show() 'Assume form closed by form itself. Dispose invoked by Form Close event.
Else
Form.ShowDialog() 'Assume form closed by form itself. We must invoke Dispose() to free resources.
ShowForm = Form.DialogResult 'set method result to form's DialogResult.
Form.Dispose() 'Dispose of dialog form's resources, now that we gathered DialogResult.
End If
End Function 'Return DialogResult, or Nothing if Modeless.
End Module
 Page 126

To use it, we provide it with our form, an optional modal setting (default Modeless), and an optional
owner form, such as Dim Result as DialogResult = ShowForm(Form1, FormShowConstants.Modal, Me).
The weird thing about the VB6 Load command was that it was not really necessary with forms,
unless we wanted to load a form without displaying it. This way we could load the form, initialize
some control fields, and then display the form once it is set up. The VB6 documentation indicated
that the Load command simply instantiated the form. Hence, the less obvious VB6 “ Load Form1”
statement was being used in place of the much clearer VB6 “Set Form1 = New Form1” statement.
This last point brings us to one of the issues I have with VB. During the Load event of a form, we could
initialize it, which is a handy place to do that sort of thing, but then we could also display it using the
Show. method while we are still executing the Load event. That never settled well with me, though I do
understand why it is done, especially when a whole lot of things that take time are rumbling around in
the Load event, though that would be better served by a background task, such as setting a timer with a
10 milisecond delay once the main form parts have loaded, which will then launch the burdensome stuff
while we typically sit there staring at the screen for a few seconds. It is still legal under VB.NET.
Everything must have an order to it. VB.NET should be more stringent in this issue. I just never saw the
logic in the ILRAP/TRLAP sequences being able to be processed out of their defined orders. It makes
sense to me in the new VB.NET TRLAP sequence that the TextChange events fires before the Resize
event because of the anchoring capabilities VB.NET forms have regarding form resizing. A dynamic
label or textbox therefore should certainly establish its data bounds before a form Resize event fires. And
Activate and finally Paint, in perfectly logical order, should not fire until the Load process (and the other
events) have finished doing their thing. The final thing to do is always painting. It is also in this last event
that we should do any special shaping commands, such as draw form-surface lines and circles and such.
But during the Load event, changes made to labels and fields can cause the TextChanged and Resize
events to fire repeatably. It would maybe be ideal if Microsoft had adopted an LTRAP sequence, which
would have resolved every sequencing frustration we have had with VB.NET form processing, but sadly,
the TextChanged and Resize events have little choice but to fire during the Initialization process, before
the Load event, unless there was flagging to inhibit them before a Load is processed.
NOTE: VB.NET initially lacked native shaping controls because VB.NET cannot work with windowless (lightweight)
controls, but it can work around this by employing Microsoft’s free Visual Basic Power Packs, http://msdn.microsoft.com/en-
us/vbasic/bb735936.aspx, which deliver new controls for our IDE Toolbox, featuring line and shape controls that can draw
lines, rectangles and ovals at design time, eliminating the need to draw shapes manually in the form’s Paint event (these
controls come pre-installed with VB2010 and later). The pack also includes a printer control and collection, emulated
printer I/O from VB6, a PrintForm component to allow we to print forms as we did in VB6, and a really neat data
repeater that allows we to display rows of data in a scrollable container.
One event I once thought lacking in VB.NET was the VB6 QueryUnload event. This event had fired
before the VB6 Unload event (the VB.NET FormClosed event). I often used it to test if the user had
chosen to close the form using either the window frame’s menu or had selected the “X” icon in the
upper right corner of the frame, or they had pressed ALT-F4. I simply had to check the UnloadMode
parameter for a value of vbFormControlMenu. If this test was true, then the user was blowing out.
We could also check in a Multi-Document Interface form for a value of vbFormMDIForm to see if a
child form was closing, or if the form owner was closing by testing for vbFormOwner. If I decided I
did not want the form to close, I would set the Cancel parameter to a non-zero value (usually 1).
Finally, I took a much closer look at the FormClosing event. There was the answer right in front of
me. Apart from the usual “ByVal sender As Object” parameter, the second parameter, e, was of type
FormClosingEventArgs. So in the body of the method I typed ‘e’ and then the dot. This presented me
with two properties: “Cancel”, a Boolean flag that I could use to check or set whether the form
should be closed, and “CloseReason”, which provides a value that indicates why the form is closing.
They are defined in the following enumeration within the System.Windows.Forms namespace:
 Page 127

Public Enum CloseReason As Integer


None 'the closing reason could not be defined.
WindowsShutDown 'the operating system is shutting down.
MdiFormClosing 'an MDI child form is closing.
UserClosing 'the user is closing the form.
TaskManagerClosing 'Windows Task Manager is closing the form.
FormOwnerClosing 'the form's owner is closing and is therefore closing this form.
ApplicationExitCall 'Application.Exit() method was invoked, closing all forms.
End Enum

Therefore, if I want to cancel the form closing because the user is trying to close the form (assuming
that some data-critical application is still running that must run to its end, as otherwise data could
be corrupted for example), I might write my FormClosing event like this:
Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles Me.FormClosing
If (e.CloseReason = CloseReason.UserClosing) AndAlso CriticalProcessesRunning = True Then
e.Cancel = True 'cancel form close if user-defined critical processes running flag is set.
End If
End Sub

● A Note on Type Object as the New Universal Data Type.


VB.NET uses type Object as its universal type because all objects and scalars inherit from it.
Because all variables are derived from classes or structures, defining a general type as Object is like
using Variant, but with the added benefit of being able to process them much faster. Granted,
VB.NET could have continued to use Variant for its universal data type, but Microsoft wisely chose
to adopt the naming convention of the CLR to avoid confusion for cross-language development,
which means that, if it had been retained, it would have rendered VB incompatible with the rest of
the .NET platform. The type system is also simplified by having only a single universal data type
declared, and by one that is compatible all across the .NET platform, and also faster.
In addition, VB.NET, as of VB2005, now provides the VarType function (similar to the VB6
VarType method that only worked with Variants). This returns an integer that will represent the data
type stored within an Object variable; a flag that is preset within the object. This can be used to very
quickly identify the type of object actually referenced there. This integer value corresponds to values
listed within the new VarientType enumeration, such as vbInteger, vbString, vbArray, etc.
Furthermore, if we were to use the ToString method of the VarType result (for example,
“Debug.Print(VarType(GenObj).ToString)”), the resulting text will be expressed as the text name of the
type, such as “Integer” or “Double”, for example. This is the same as using “GenObj.GetType.Name”.

● A Note on Finding the Instance Handle.


Another thing that VB6 developers moving to VB.NET miss is getting an Application’s Instance
Handle. Under VB6, we obtained it by simply specifying App.HInstance. Because of the managed
structure of VB.NET, it is not so direct, but with the addition of a rather simple function, we can
make it that way (note that the VB6 Compatibility Library does feature a VB6.GethInstance method).
What we need to do is obtain any instantiated class module in our assembly, and we can obtain the
application’s instance handle from that. For example, to get the module for a class named frmView
(OK, it is a form class), we might execute this line:
Return System.Runtime.InteropServices.Marshal.GetHINSTANCE(GetType(frmView).Module)

But this is much too restrictive, requiring us to provide it with an active class name. If we can
generalize it even more, I am all for it. As it happens, we can acquire an array of class modules in
our assembly by invoking System.Reflection.Assembly.GetExecutingAssembly.GetModules() (we can provide a
parameter of True if we want to gather the application resources as well). We can store it into an
array that has been declared like this: “ Dim m() As System.Reflection.Module ”. All that is left is to simply
take the first entry in the array ( m(0)) as the parameter to GetHINSTANCE, and we are good to go,
placing it all within the body of a couple lines of code:
 Page 128

Dim m() As System.Reflection.Module = System.Reflection.Assembly.GetExecutingAssembly.GetModules()


Return System.Runtime.InteropServices.Marshal.GetHINSTANCE(m(0))

But even this is far too complicated. We can actually reduce all this down to just a single line of
Visual Basic code, as the free Integrated Development Environment editor enhancer, Code Rush!
Express informed me (visit www.devexpress.com/Products/Visual_Studio_Add-in/CodeRushX/), by embedding the code
assigned to m() directly as the parameter for GetHINSTANCE. With this knowledge, I wrote the
following module and function for my re-use library:
Imports System.Runtime.InteropServices, System.Reflection

Module modHInstance
'*******************************************************************
' HInstance: Get the application instance handle.
'*******************************************************************
Public Function HInstance() As IntPtr
Return Marshal.GetHINSTANCE(Assembly.GetCallingAssembly.GetModules()(0))
End Function 'Note that the above (0) grabs the zeroeth member of the returned Module array.
End Module

● A Note on Checking For a Previous Application Instance.


Checking for the previous instance of an application has changed. In VB6, we simply needed to check the
Boolean value of App.PrevInstance to tell if a previous instance of an application is running, such as, to
activate a previous instance and close our instance. Also, if we simply want to ensure than only one instance
is running in VB.NET, we can go to the project properties, and on the Application page, make sure a check is
in the Enable application framework checkbox and in the Make single instance application checkbox.
However, sometimes we will allow multiple instances of an application to run, but we want to check
how many are running. First, we can obtain the current process name using this command line:
Dim pName As String = System.Diagnostics.Process.GetCurrentProcess().ProcessName

We can also gather an array of all like-named processes like this:


Dim pArray() As System.Diagnostics.Process = System.Diagnostics.Process.GetProcessesByName(pName)

All we need to do next is to simply check to see if the UBound of the array is zero (indicating a
single instance), or non-zero (indicating two or more instances, counting the current one):
If UBound(pArray) = 0 Then
Return False 'only one instance running (this one).
Else
Return True 'two or more instances running, including this one.
End If

We can put this all together into a single module and function like so:
Imports System.Diagnostics.Process
Module modHasPreviousInstance
'*******************************************************************
' HasPreviousInstance: Return True if a previous instance is running.
'*******************************************************************
Public Function HasPreviousInstance() As Boolean
Return (UBound(GetProcessesByName(GetCurrentProcess().ProcessName)) <> 0)
End Function
End Module

● A Note on Getting the App Path and App EXE Name.


To get the Application Path under VB6, we used App.Path. To get the Application Executable Name,
we used App.EXEName. And if we wanted to combine them, we checked to ensure that the
App.Path value had a trailing slash, and then appended App.EXEName to it.
With VB.NET, it is a little more involved, but it also entails providing we with many more options,
as well as many more directions in which we can go with these functions. However, we can easily
write them as simpler functions to operate much as VB6 did.
As a base, to obtain the full path, to include the executable, we can use the following statement:
Dim FullPath As String = My.Application.ExecutablePath
 Page 129

To get just the folder path to it, the App Path, we would use a statement like this:
Dim AppPath As String = My.Application.Info.DirectoryPath

To get just the executable’s filename with extension, we would use a statement like this:
Dim AppEXEName As String = GetFileName(My.Application.ExecutablePath)

● A Note on Getting the App Title.


If we used the VB6 App.Title command a lot (I constantly used it for registry value saving/loading in
tandem with the GetSetting and SaveSetting persistent storage registry functions), we can use
something like the following in VB.NET to obtain the same thing:
Dim AppTitle As String = My.Application.Info.Title

● A Note on Collections Enhancements.


VB.NET empowers Collections, ListBoxes, and ComboBoxes with the ability to use objects as data, as
stated in a previous note. VB6 was restricted to using only simple strings. Therefore, if we have a
ListBox named ListBox1 and we want to get the string we assigned at the index stored in the integer
variable Idx, in VB6 we would have used the command “ Me.ListBox1.Items(Idx)”. However, under
VB.NET with Option Strict turned on, this same statement may caution us with a warning that the default
value cannot be determined. Although the code will actually run, we can make it operate faster by
changing this command to Me.ListBox1.Items(Idx).ToString. We can make it run even faster by Early-
Binding it with Me.ListBox1.Items.Item(Idx).ToString. Also, notice that in an upgrade from VB6, this
line will be upgraded to VB6.GetItemString(Me.ListBox1, Idx). Although this works, it operates faster if
we simply used the original VB6 command in tandem with its new ToString property, as shown above. It
is also much easier to remember when we are writing native VB.NET code. Additionally, we can format
the text by applying an optional format parameter to the ToString method, such as
“Me.ListBox1.Items.Item(Idx).ToString("$#,##0.00")”.

● A Note on Adjusting Form Opacity (Transparency).


To adjust the Opacity (solidness) of a form (we might think of this feature inversely as more as a
Transparency feature) is as simple as “Me.Opacity = 0.5”. The range is from 0.0, or invisible, to
1.0, which is fully opaque (solid), representing 0 to 100 percent (divided by 100, that is). Default is
1.0, or 100 percent opaque (meaning fully non-transparent).

● Notes on Registry I/O.


The Registry can be accessed under VB.NET through the My.Computer.Registry class. This exposes
features that are more powerful than those offered by VB6. Under VB6, all keys were stored under the
“HKEY_CURRENT_USER\Software\VB and VBA Program Settings” hive key path (they still are if
we use the GetSetting and SaveSetting functions). Unlike VB6, where we had no direct access to the
Registry or to other hives except through the API, under VB.NET we can access the entire registry with
ease (try running the RegEdit utility to browse the registry if we have some basic understanding of it, but
do not alter anything unless we actually do know what we are doing – not just think we know).
For example, suppose we wanted to save our form’s WindowState to a key value “WindowState”
using a sub-key from the CurrentUser\Software hive path that specified our company name, “Acme
Inc”, and then a sub-key from that for the application, “MyOLAPpro”. This means that we will
require a hive path named “HKEY_CURRENT_USER\Software\Acme Inc\MyOLAPpro”.
Under VB6, we had to first test for the SubKeys “Acme Inc” and “MyOLAPpro”, in succession,
creating them if they do not exist, before we could write the “WindowState” value. However, this
pre-testing/pre-creation is no longer necessary under VB.NET. All we have to do is the following:
'save the WindowState key (SetValue automatically converts numeric keys to text by invoking their ToString method).
'(use Cint() to cast the WindowState to a numeric value; default is otherwise worded text, such as "Normal" or "Maximized")
My.Computer.Registry.SetValue("HKEY_CURRENT_USER\Software\Acme Inc\MyOLAPpro", "WindowState", Cint(Me.WindowState))
 Page 130

Under VB.NET, its registry SetValue method can be used to automatically build a full path,
including intermediate sub-keys, even if they do not already exist.
We can use the My.Computer.Registry.CurrentUser.DeleteSubKey method to do the opposite and delete a
Key. Notice further that by allowing we to use DeleteSubKey (and CreateSubKey) only from the Hive
Key members CurrentUser and LocalMachine, we cannot accidentally delete the primary hive
members HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE, which some have accidentally
done via Pinvoke access to the Registry, and always to disasterous results.
We can retrieve the above saved WindowState value like this:
Me.WindowState = DirectCast(My.Computer.Registry.GetValue("HKEY_CURRENT_USER\Software\Acme Inc\MyOLAPpro",
"WindowState", FormWindowState.Normal), FormWindowState)

Because the GetValue method always returns a generic Object, we can use DirectCast to cast it to
the type that we know that it is actually formatted to (Integer). If the value to retrieve must be of type
String, we can either use DirectCast or the ToString method to acquire a text value (DirectCast is
faster because it does not require helper methods, as CType does, but which may be necessary if the
data cannot directly cast to the target type (type Object stores the actual storage type internally)).

● A Note on Literal Type Characters.


One thing that bugs me is when I am using Short Integers and I add 1 to it, such as “Ofst += 1”: if I
have Option Strict turned On (which I always do), it warns me that Option Strict will not allow
casting from Integer to Short. The answer is to append “S” to the 1 digit, hence “Ofst += 1S”, so the
values will not promote to Integers. And how is that? Under VB6, we had a number of shortcut
symbols, called Literal Type Characters, that we could append to values to directly cast them to a
particular storage type. VB.NET also supports these, plus a few more. Consider the following Table:
Value Type VB6 Symbol VB.NET Symbols VB.NET Examples
Char Not Available c String(" "c, 128)
Short Not Available S 123S
Integer % % or I 123% or 123I
Long & & or L 123& or 123L
Single ! ! or F (Float) 123.45! or 123.45F
Double # # or R (Real) 123.45# or 123.45R
Decimal Not Available @ or D 123.45@ or 123.45D

NOTE: It may seem redundant to use a ‘C’ tag because a single character of a string is a Char, it is actually an element
in an array of 1 of type Char. For example, “ Dim s As String = New String(" ", 128) ” will generate the error “Option
Strict On disallows conversions from 'String' to 'Char'.” Use instead Chr(32), ChrW(32), CChar(" "), or " "c.
NOTE: If our Long, Integer, or Short values are Unsigned, then precede the Literal Type Character with a U, such as
123US for Unsigned Short, 123UL for Unsigned Long, or 123UI for Unsigned Integer.

● Notes on Loading Images and Creating a LoadPicture Function.


Loading images into PictureBox controls involves little work, but it can be
compressed into a simple function for compatibility to VB6. For example, under
VB6 we could load an image into a PictureBox using a statement such as this:
“Picture1.Picture = LoadPicture(PicPath)”. With VB.NET, we would do it like this:
“Picture1.Image = Image.FromFile(PicPath)” (under VB.NET, a PictureBox control no
longer supports both an Image and a Picture property; it now features only an
Image property, significantly reducing resources). Or, we can also define an
image object that would hold the graphic for later use, such as: “ Dim img As Image =
Image.FromFile(PicPath)” or “Dim img As Image = My.Resources.myImage ”, and then assign it to a PictureBox
when it is needed, like this: “Picture1.Image = img”. For compatibility, here is a simple LoadPicture
function for VB.NET that we can use just like the old VB6 function:
 Page 131

Public Function LoadPicture(ByVal PicturePath As String) As Image


Try 'init error trapping.
Return Image.FromFile(PicturePath) 'load a image file (bitmap, jpeg, png, Icon, etc.).
Catch 'catch ANY error.
End Try 'close error trapping.
Return Nothing 'return no object if the file format is unrecognized.
End Function

VB.NET does not support the VB6 Image control because it does not support windowless controls.
People used Image controls in VB6 for two reasons: 1) they used fewer resources (actually, this was only
true prior to Windows NT), and 2) they persisted their images (a PictureBox had to enable its
AutoRedraw property, which ate more resources). But the primary benefit of windowless controls had
been to reduce resource consumption. However, this was only true when we had a very large number of
controls on a form. Further, this benefit applied only to Windows 95 and Windows 98. Windows NT
through Windows 10 do not have these resource constraints, and so lightweight controls offered
absolutely no benefit for these more recent platforms. As far as the benefit of image persistence without
loss of resources, VB.NET has taken care of this in the PictureBox control, and so the advantages that the
VB6 Image control could offer to VB.NET are minimized (but not yet moot). Because of this, during
VB6 to VB.NET upgrades, Image controls are changed to PictureBox controls. See the later article,
Emulating VB6 Image Control Features Under VB.NET, on page 243 to see several ways of emulating
the features of the old VB6 Image controls under VB.NET, and enabling the one feature that Image
controls had that a PictureBox does not have: being able to display icons with transparent backgrounds.
That article also explains how to easily extend this transparent background capability to images as well.
One more thing: In case we did not catch it, the above VB.NET LoadPicture method will
also load Icon files, which, like VB6 PictureBox controls, will automatically convert the
Icon to Image (Bitmap) format by merging the PictureBox’s blank background color with
the icon, allowing the background color to replace the icon’s transparency color (we may
have noticed the reference to icon files in the above code’s comments).
● Notes on Loading Icons.
Loading Icon images has also changed slightly. Under VB6, we could load an icon file as an Icon
like this: “Me.Icon = LoadPicture(IconPath)”, where IconPath is the file path to the icon file. Under
VB.NET, this command has changed to “ Me.Icon = New Icon(IconPath)”, or “Me.Picture1.Image = New
Icon(IconPath).ToBitmap”, or even “Me.Picture1.Image = Image.FromFile(IconPath)”. Note the use of “ New” in
most of these statements, because the icon resource is newly instantiated, replacing the previous icon
or image, which will now go out of scope and will be picked up and its resources released during
garbage collection. Note further, as shown in the above examples, that when loading an icon file or
resource to an image object requires that it also be converted to Bitmap format, as indicated by the
appended “.ToBitmap” method invocation. This is because an Image object is actually being redefined
as a Bitmap object (an Image inherits from a Bitmap object, so their data formats are compatible).
NOTE: The FromFile Image method will internally instantiate a new bitmap object and return it; so it is not necessary
for us to declare it as a New Image. The IDE’s automatic Syntax Parser will help us keep these things straight, so there
is no real cause to be concerned about when to implement the New keyword, and when not to.
If the image is stored in the application resources, which I highly recommend for reasons of speed
(adding icons, images, audio files, etc. to an application’s resources is almost too easy under
VB.NET), then we can access it in one of two ways. If our Icon will be displayed in an Icon object,
such as the current form, we can load it using either of the following two methods:
Me.Icon = My.Resources.Shadow 'acquire a reference pointer to the icon stored in the resources, named Shadow.
Me.Icon = DirectCast(My.Resources.ResourceManager.GetObject("Shadow"), Icon) 'get the resource pointer via resource name.

NOTE: Just in case we forgot, all object variables are actually pointers to separate resources. In older days, a pointer
actually always pointed to an object. Now they are sometimes replaced by Handles, which are much like a Valet ticket in
that they are a chit that can be used to reference the actual object from a lookup table. This is used extensively under
.NET, because the Garbage Collector is constantly deallocating discarded objects from the sistem Heap.
NOTE: In case we want to load an image as an icon, use “Me.Icon = Icon.FromHandle(myPicImage.GetHIcon)”.
 Page 132

If our Icon will be stored in a 32x32 PictureBox named PictureBox1, and the 32x32 pixel icon’s
name is Shadow, we can load it from the resources using either of the following two methods:
PictureBox1.Image = My.Resources.Shadow.ToBitmap 'An Icon format object must be converted to BitMap/Image format.
PictureBox1.Image = DirectCast(My.Resources.ResourceManager.GetObject("Shadow"), Icon).ToBitmap.

NOTE: To add icons to the application’s resources, go to the project’s properties and select the Resources tab. Choose
Icons from the Resource menu, then from the Add Resource option select Add Existing File... This opens a browser.
Navigate to our icon storage location and select all the icons we want to load, and then select Open to load them. Select
File \ Save or hit Ctrl-S to save the resource file (Shift-Ctrl-S saves all files that have been modified), or just close the
project properties and we will be asked if we want to update the resources.

● Notes on Embedding Classes Within Classes.


I make the point in a couple of articles within this document that we are able to embed classes within
other classes. This is a very important feature that can often be worth its weight in gold.
Very often we need to access a class or two from within another class, but on numerous occasions
we do not need to access these other classes except from within the class employing them. If we find
ourselves in such a situation, where I find myself all the time, we can actually delare these
subordinate classes, modules, or a mix of them, directly within our main class or module. We can
declare them anywhere at the field and method level of the main class, at its top, at its bottom, or
right between method declarations. Even so, it is a best practice, for reasons of clarity, though it is
not a cut and dried rule under VB, that the subordinate class be declared before they are referenced.
Some are quite surprised to hear this, even though they seem to have absolutely no issues declaring
Structures in the same manner, which are, after all, just abstracted classes. We can even declare
classes within Modules, which are just non-inheritable static classes. These embedded classes are not
limited in scope, save for the fact they can only be directly accessed from within the class body they
are declared within, though through scoping rules we can still declare instances of such classes from
outside the encapsulating class or module by simply referencing the encapsulating class's or
module's name. Yet, if we need to do that, we should just consider creating their own class file.
In similar light of the above, were you aware that we can also declare more than one class or module
within the body of the same source file by just declaring yet another class or module after the closing
bracing of the previous class or module? I have been doing things like this since the days when I was
writing code under Pascal., though this technique is best used when such classes are tightly related.

● Notes on the Checked State of a CheckedListBox.


There seems to be a bit of hair-pulling out there when developers are trying to figure out how to
properly monitor the checked state of an item in a CheckedListBox control. Actually, there is little to
be confused about, except, it seems, when it comes to reading documentation. The frustration arises
when a user is trying to monitor the checked state of a just-selected item in the list using the
GetItemCheckState method during a ItemCheck event. For example: “Dim IsChecked As Boolean =
clbOptions.GetItemCheckState(clbOptions.SelectedIndex) = CheckState.Checked”. Normally,
this statement works as advertised, except from within an ItemCheck event.
Why? Because in this event, it is preparing to flip its check state. It is here the interface allows us to
verify the current state before flipping it, if Checked, Indeterminate, or Unchecked, by our setting the
desired state using the SetItemCheckState method. Actually. we should not be checking the state of
the check in this event, because it is meant for us to verify its current state before it is flipped, as a
part of a validation process, but that does not stop people from trying. So, if we want to use ir to see
if the SelectedIndex is checked, then we must flip its reported state, because the new state has not
yet been toggled, but it is only about to be. In this case. use “Dim IsChecked As Boolean =
clbOptions.GetItemCheckState(clbOptions.SelectedIndex) = CheckState.Unchecked”. Even better, do not
use this event at all when you are only wanting to check the state after an item has been clicked, but
use one of the control's other click monitoring events instead.
 Page 133
 Page 134

● Notes on New String Manipulation Features.


New string manipulation commands are exposed as of .NET Framework 4.0 (latest is 4.6) that make
working with strings easier. Although VB6 commands like Instr, Mid, InstrRev, and the like are still
supported as a legacy part of the language, much more efficient zero-indexed commands are now
available. I am constantly stumbling on some now-old features as enhancements I was not
previously aware of all the time. Sometimes, for fun, I will type a dot after a command or variable
and explore what pops up. For just a short list of amazing new features offered by VB.NET for string
manipulation, consider the following incomplete list of enhancements:
 myString.Contains("\"c) will return True if the string, myString, contains a backslash character. Note that the
sequence "\"c specified a single character, as opposed to a string, which would be "\", that can also be used.
 To look at the last character of a string, we can check myString.Last.
 To find the first occurrence of a character or string, we can use myString.IndexOf(":"c). We can find subsequent
occurences using myString.IndexOf(":"c, offset). If the returned value is -1, then there was no match. Suppose
we want to return the text preceding a backslash, we can use Dim Result As String = myString.SubString(0,
myString.IndexOf("\"c)).
 We can also search backward for a character or string using myString.LastIndexOf("\"c) or
myString.LastIndexOf("\"c, offset).
 We can grab the rest of the string starting at an offset using myString.SubString(offset), or specify an offset and
a character count using myString.SubString(offset, Count) to grab that count of characters.
 We can change the character case using myString.ToUpper or myString.ToLower.
 We can use myString.TrimStart, myString.TrimEnd, and myString.Trim to trim a string. We can also specify
character parameters in the trim operators, such as myString.Trim(" "c, "-"c) to trim dashes and spaces from
either side of the string.
 We can determine if a string starts with another string using myString.StartsWith(sndString).
 We can remove a select number of characters from an offset using myString.Remove(offset, Count).
 We can replace characters, such as remove all quotation marks using myString.Replace(""""c, Nothing).
More robust string processing tools are available, such as a StringBuilder defined in System.Text. It
is a powerful tool that allows us to manipulate, insert, delete and concatenate strings up to 200 times
faster than regular string manipulation features. What is also great about them is that we can pass
them to functions and even Pinvokes just like an ordinary string.

● Notes on Implicit Declaration of VB6 Object Arrays.


VB6 supported implicit object array declaration. This means it could declare an array concurrently
filled with instantiated objects of the specified type. Consider the following VB6 example:
Dim ObjAry(9) As New clsWidget 'create a static array that is filled with 10 (0-9) instantiated objects of Type clsWidget.

Afterward, ObjAry is declared as a 10-member static array of Type clsWidget, which contains a list
of 10 new members, where each of these members has also been instantiated As New clsWidget.
VB.NET does not support the above syntax. However, we can easily emulate it and, with far less
code overhead and make its intent much clearer, by replacing it with something like the following:
Dim ObjAry(9) As clsWidget 'Declare a dynamic array of the needed size and type (static array under VB6).
For Each obj As clsWidget In ObjAry 'Process all uninstantiated reference variable members of the array.
obj = New clsWidget 'Assign each member as a new instance of a clsWidget object.
Next 'Loop until all members are instantiated.

Although Microsoft could easily upgrade a future VB.NET edition to support the VB6-style syntax,
this night not be a good idea. It was certainly not good under VB6. The reason was that VB6 had to
devote both extra time and resources just to deliver this functionality, especially in a generic way,
and to support all possible object types, which is not easy in a non-object-oriented language. Indeed,
the above explicit VB.NET translation, even under VB6, generates less over-all code and it actually
runs\much faster than the bulkier generic runtime support that had to be built into the VB6 compiler.
VB6 had to internally deal with the rather complex task of casting and tracking object types in a
generic way, just to safely create new instanced object arrays on a non-object, COM-based platform.
 Page 135

Further, such generic code would also have to be resolved during a late-binding process, which
further slowed its execution speed even more, just to emulate the much faster above VB.NET
translation, even under VB6, complete with a final, properly typed array.
However, if you need to create several arrays of type clsWidget, you could try writing a fast-running
helper function to simplify this task, delivering easier development-time declarations. For example:
'*******************************************************************************
' Function Name : NewWidgetArray
' Purpose : Create a new instance of an array of clsWidget of a
' : specified UBound dimension. Hence, specifying a UBound
' : value of 9 renders 10 total objects, 0-9.
'*******************************************************************************
Friend Function NewWidgetArray(ByVal AryUBound As Integer) As clsWidget()
If ArySize >=0 Then 'Make sure we are not creating a negative-sized array.
Dim Ary(ArySize) As clsWidget 'Declare a dynamic array to hold clsWidget objects of the specified dimension.
For Each obj As clsWidget In Ary 'Process all uninstantiated reference variable members of the array.
obj = New clsWidget 'assign each member as a new instance of a clsWidget object.
Next 'Loop until all members are instantiated.
Return Ary 'Return the filled clsWidget array to the invoker.
End If
Return Nothing 'If AryUBound is negative, return a null object.
End Function

With this function, we can quickly declare an array named xWidgets of type clsWidget with 50
instantiated members, and another array named yWidgets of 10 clsWidget members, like so:
Friend xWidgets() As clsWidget = NewWidgetArray(49) 'create an array of 50 instantiated clsWidget objects.
Friend yWidgets() As clsWidget = NewWidgetArray(9) 'create an array of 10 instantiated clsWidget objects.

A more generic solution, which also more closely emulates the VB6 syntax, is as follows, noting that
the provided object class must implement a Clone method, which, if it does not, must be added.
NOTE: Creating Clone methods is easy. See Cloning Structures Containing Object References Made E-Z on page 224
to see how to add cloning to a structure or class. Also see Cloning MenuStrip Items with a Cloneable
ToolStripMenuItem Class on page 292 for how to add a clone method to a non-clonable object through a derived class.
'*******************************************************************************
' Function Name : OArray
' Purpose : Create a new instance of an array of a specified class of a specified Ubound dimension. So specifying
' : a UBound of a specified Ubound dimension. Specifying a UBound value of 9 renders 10 total objects, 0-9.
' NOTE: This function returns an array of Type Object. We must caste it to the required type after invoking it. Also, the
' returned array must be cast from Type Object() to the required type, which was submitted as the Cls parameter, below.
'*******************************************************************************
Friend Function OArray(ByRef Cls As Object, ByVal AryUBound As Integer) As Object()
If ArySize >=0 Then 'Make sure we are not creating a negative-sized array.
Dim Ary(ArySize) As Object 'Declare a dynamic array to hold objects of the specified type and dimension.
For Each obj As Object In Ary 'Process all uninstantiated reference variable members of the array.
obj = Cls.GetType.Clone 'Add a new object to the array using the object's Clone method.
Next 'Loop until all members are instantiated.
Return Ary 'Return the filled object array to the invoker as an Object() array.
End If
Return Nothing 'If AryUBound is out of bounds, then return a null object.
End Function

With the above function, we can easily declare an array named xWidgets of type clsWidget with 50
members (0-49), and another array named yWidgets of 10 clsWidget members (0-9), like so:
Friend xWidgets() As clsWidget = DirectCast(OArray(clsWidget, 49), clsWidget()) 'create array of 50 new clsWidget objects.
Friend yWidgets() As clsWidget = DirectCast(OArray(clsWidget, 9), clsWidget()) 'create array of 10 new clsWidget objects.

NOTE: Although we need to invoke the DirectCast compiler directive, it actually generates no additional program code.
One other thing that VB.NET does not support is VB6's much-beloved Implicit Object Creation,
meaning that objects, if they had previously been set to Nothing, would auto-reinstantiate upon a
subsequent first reference. Even further, VB6 would create static arrays, not dynamic arrays, if they
were dimensioned upon declaration, as VB.NET does. If we cleared a static array using Erase, this
set each array member to Nothing, but we could afterward reference a member to auto-reinstantiate
it. If VB6 users understood the developmental headaches this unaddressed bug caused, they would
never mourn its loss when they migrated to VB.NET, and they would have wished Microsoft had in
fact fixed it, rather than, as did a majority of them, threaten to riot if Microsoft dared. See VB6
Aficionado Complaint Department, point 34, on page 178 for more details on the issues it spawned.
 Page 136

An Exhaustive List of VB6 Commands Compared to VB.NET.


Based on various Microsoft sources.
VB.NET support for various language elements has changed since VB6, mostly for compliance and
interoperability with the Common Language Specification (CLS) in the .NET Common Language
Runtime (CLR). Many VB6 elements had to be renamed, reclassified, or combined with other elements
to assume a more logical and robust structure. Some elements are no longer supported because the CLR
provides functions that renders them moot, or provides more efficient means to achieve them. Refer to
the .NET on-line help on the Common Language Runtime.
The following list is the most complete table that we will find to date that lays out changed VB6
programming commands and their VB.NET replacements (most changes are typically unnoticed). We
are encouraged to explore these functions ourselves using the Object Browser (F2), the .NET Reflector,
MSDN Help, and the MSDN.com website. This might accidentally lead to superior knowledge.
NOTE: Some long paths are not required, such as System.Windows.Forms within Form code.
VB6 element VB.NET-equivalent Namespace, class, or run-time library
location
Abs function Math.Abs function. Math not needed if imported. System Namespace, Math Class
AddressOf VB.NET supports the AddressOf keyword, but only Visual Basic Run-Time Library
when the application defines a Delegate class that can Members,
point to the target method.
And (logical function) And (logical function), but it would be much faster to use System Namespace, Math Class
AndAlso. If the leading expression is false, then the
trailing expression will not need to be evaluated.
App statement See the App Class definition, described on page 229, Not Applicable
for more convenient VB6 App member declarations.
All can be emulated.
AppActivate The VB6 version of the AppActivate method takes a System.Windows.Forms namespace,
second (optional) wait argument; if this argument is Form class
True than the method waits until the external
application receives the input focus. The VB.NET
version of this method takes only one argument.
App.TaskVisible System.Windows.Forms.Form.ShowInTaskBar or System.Windows.Forms namespace,
Me.ShowInTaskBar Form class
Array The { } braces can be used in the declaration of a System namespace, Array class
standard array to pre-assign values to it.
Asc Asc, or Convert.ToInt32(char) Visual Basic Run-Time Library
Members, Strings module
AscB function Asc function Visual Basic Run-Time Library
Members, Strings module
As Any keyword phrase VB.NET supports overloading declares, so explicit data System.Runtime.InteropServices. See
types should be used. However, Marshalling does allow the earlier article, “Using the AsAny
an AsAny expression, but this is subject to the same Marshaling Parameter”
errors the VB6 form was subject to, such as passing
wrong-type parameters.
App.Path System.Reflection.Assembly.GetExecutingAssembly. System.Reflection.Assembly
Location or Application.StartupPath Namespace
Atn function Math.Atan function. Math not needed if imported. System Namespace, Math Class
Calendar property CultureInfo.CurrentCulture Property System.Globalization Namespace,
CultureInfo Class
 Page 137

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
CDate, IsDate VB.NET version of CDate and IsDate methods is less
forgiving than the corresponding VB6 method. Under Visual Basic Run-Time Library
VB6 these methods attempt to reverse the day-month Members, Strings module
numbers if they don’t make up a valid date
Chr Chr, ChrW, Convert.ToChar, or CChar Visual Basic Run-Time Library
Members, Strings module
Collection Collection, Collections.HashTable,
System Namespace
Collections.Dictionary
ChDir statement ChDir function, or the faster Visual Basic Run-Time Library
System.IO.Directory.SetCurrentDirectory Members, FileSystem module
ChDrive statement ChDrive function Visual Basic Run-Time Library
Members, FileSystem module
Chr$,ChrB, functions Chr function Visual Basic Run-Time Library
Members, Strings module
Circle function e.DrawEllipse function or System.Drawing.Graphics
System.Drawing.Graphics.DrawEllipse function namespace
Clipboard.Clear Clipboard.Clear or Clipboard.SetDataObject(New System.Windows.Forms.Clipboard
DataObject("")) namespace, SetDataObject class
Clipboard.GetFormat Clipboard.GetFormat or
System.Windows.Forms.Clipboard
Clipboard.GetDataObject.GetDataPresent
namespace, GetDataObject class
(DataFormat)
Clipboard.GetText Clipboard.GetText or System.Windows.Forms.Clipboard
Clipboard.GetDataObject.GetData (DataFormat, True) namespace, GetDataObject class
Clipboard.SetData Clipboard.SetData or Clipboard.SetDataObject(New System.Windows.Forms.Clipboard
DataObject(textobject)) namespace, SetDataObject class
Clipboard.SetText Clipboard.SetText or Clipboard.SetDataObject(New System.Windows.Forms.Clipboard
DataObject(DataFormats.Text, "Hello")) namespace, DataObject class
Close statement FileClose Function Visual Basic Run-Time Library
Members, FileSystem module
Const VB6 supports implicit conversion between string and Visual Basic Run-Time Library
numeric values, VB.NET does not. Members.
Cos function Math.Cos function System Namespace, Math Class
CreateObject The CreateObject method defined in VB.NET fails to
recognize classes that have been converted to
Visual Basic Run-Time Library
VB.NET, created by VB6 CreateObject simply
Members.
because VB6 created ActiveX COM objects, which
are stored in unmanaged space.
CStr, Str Str and CStr methods support date arguments under Visual Basic Run-Time Library
VB6, but not under VB.NET. Members, Strings module
CurDir function CurDir, or the much faster Visual Basic Run-Time Library
System.IO.Directory.GetCurrentDirectory function Members, FileSystem module
Currency data type Replaced by the more accurate Decimal data type.
The Decimal type’s ToString(“c”) method can be Visual Basic Run-Time Library
used to display Decimal values as Currency, according Members, VariantType Enumeration
to the current culture settings.
CVar VB.NET does not support the Variant data type,
consequently the CVar function isn’t supported and Not Applicable
should be rendered as CObj.
CVDate function DateValue function Visual Basic Run-Time Library
Members, DateAndTime module
CVErr function Error Statement Does not apply
Date function, Date Now, or Today Property. Still supported as a data
statement type but no longer a function returning a 8-byte Visual Basic Run-Time Library
double precision value. Use the Today property to Members, DateAndTime module
return the day in the 8-byte CLR format.
Date$ function DateString Property Visual Basic Run-Time Library
Members, DateAndTime module
 Page 138

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
DateAdd DateAdd, or DateObject.Add Visual Basic Run-Time Library
Members, DateAndTime module
DateDiff DateDiff, or DateObject.Subtract Visual Basic Run-Time Library
Members, DateAndTime module
Debug.Assert method Debug.Assert, Debug.Fail functions System.Diagnostics Namespace,
Debug Class
Debug.Print method Debug.Print, Debug.Write, Debug.WriteIf, System.Diagnostics Namespace,
Debug.WriteLine, and Debug.WriteLineIf methods Debug Class
Declare VB.NET fully supports Declare, though unsafe “As
Any” parameters must be Marshalled. Also, a minor
Visual Basic Run-Time Library
problem is that VB6 allows Declare statements
Members.
containing two or more parameters with the same
name, but they are illegal under VB.NET.
Def<Type> statements Not supported in VB.NET, nor should it be. See Data
Type Changes for VB6 Users in Help. All variables Does not apply
must be explicitly declared.
DeleteSetting statement DeleteSetting function Visual Basic Run-Time Library
Members, Interaction module
Dim VB6 allows defining variables without the As
clause, in which case the variable type is affected
by the DefXxx directive, which VB.NET does not
support (if no DefXxx, Variant is default). VB.NET Visual Basic Run-Time Library
allows Dim without the As clause only if Option Strict Off, Members
then the variable type is Object. Also, if Dim is inside an If,
For, For Each, Do, While, or Select block, VB.NET limits
the variable scope to the block, whereas VB6 makes the
variable visible to the entire method.
Dir Dir, or System.IO.GetFiles, System.IO.GetDirectories, Visual Basic Run-Time Library
System.IO.File.Exists, System.IO.Directory.Exists Members, FileSystem module
DoEvents function Appication.DoEvents function System.Windows.Forms Namespace,
Application Class
Empty keyword Handled by Nothing keyword. Does not apply
End keyword End, but it is highly recommended that we invoke System.Windows.Forms Namespace,
Application.Exit instead of, or prior to End. Application Class
EndIf VB6 supports the obsolete spelling “EndIf”; the VB6
code editor expands it to “End If”. VB.NET does not Does not apply
support the “EndIf” spelling.
EOF EOF, or the faster test: System.IO.StreamReader.Peek Visual Basic Run-Time Library
= -1 Members, FileSystem module
Eqv operator Use the equals (=) operator, in a form such as result
= (CBool(op1) = Cbool(op2)). See Boolean Does not apply
Operator for VB6 Users in Help.
Erase keyword Erase. The biggest difference is that VB6 also
supported static arrays, and erased their data but not
their memory allocation. VB.NET uses only Dynamic Visual Basic Run-Time Library
Arrays, which erases all references. Use Members
System.Array.Clear if we want to erase only the array
cell contents but not the array cells themselves.
Error keyword Err.Raise. VB.NET does not support the Error Visual Basic Run-Time Library
statement, which had been used to raise an error. Members
Exp function Math.Exp function System Namespace, Math Class
 Page 139

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
FileAttr VB6 supports FileAttr with two arguments. If the
second argument is 1 or omitted the function returns
the mode used to open the file; if the second argument
Visual Basic Run-Time Library
is 2, then the function raises an Error 5 under VB6 (it
Members
used to return the operating system file handle under
16-bit version of Visual Basic.) The VB.NET version
of this method supports only one argument.
FileCopy statement FileCopy function, or System.IO.File.Copy Visual Basic Run-Time Library
Members, FileSystem module
FileDateTime FileDateTime, or
Visual Basic Run-Time Library
System.IO.File.GetLastAccessTime,
Members, FileSystem module
System.IO.GetCreationTime
FileLen FileLen, or StreamObject.Length Visual Basic Run-Time Library
Members, FileSystem module
Filter Array.BinarySearch System Namespace, Array Class
Fix Fix, or Math.Floor (for negative numbers), or
System Namespace, Math Class
Math.Celing (for positive number)
Format function Format or AnyObject.ToString(“Format String”). The
VB.NET version differs from VB6 in several ways,
The VB.NET version does not support:
Formatting of string values using the old @, &, <, and >
placeholders. It uses newer methods to implement these Does not apply
features.
A few rare date/time formats, though they can be easily
emulated with custom definitions.
FirstDayOfWeek or FirstWeekOfYear optional arguments.
FormatCurrency ScalarObject.ToString(“c”) Visual Basic Run-Time Library
Members, Strings module
FormatDateTime DateObject.ToString(“Format String”), Visual Basic Run-Time Library
DateObject.ToLongDateString, etc. Members, Strings module
FormatNumber NumberExpr.ToString(“Format String”) Visual Basic Run-Time Library
Members, Strings module
FormatPercent NumberExpr.ToString(“p”) Visual Basic Run-Time Library
Members, Strings module
Forms collection My.Application.OpenForms My.Application namespace,
OpenForms collection
Get # statement FileGet function. FileGet does not work the same with
nonscalar values, so we cannot swap data with VB6 apps
IF the file has Variant values. If a file has dynamic arrays
or structures, we must marshal them. When FileGet gets
a dynamic array in Random Access files, a two-byte Visual Basic Run-Time Library
length value is expected prior to each array, making a Members, FileSystem module,
slightly longer file. To force the same results as VB6, use System.Runtime.InteropServices
the ArrayIsDynamic argument with FileGet to prevent
length descriptors from being expected or checked. For
structures, also lead them with:
<StructLayout(LayoutKind.Sequential, Pack:=1)>
GetSetting GetSetting function Microsoft.VisualBasic Namespace
GoSub statement No longer supported. Remove all in original code
before upgrading and replace with method
Does not apply
invocations. Use the Return Statement. See Control
Statement for VB6 Users in Help.
Hex Hex, IntegerObject.ToString(“x”), Visual Basic Run-Time Library
Convert.Tostring(Integer, 16) Members, Strings module
ImeStatus VB.NET does not support ImeStatus.
Does not apply
 Page 140

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
Imp operator Not supported in VB.NET. Use the equals (=) operator in
conjunction with Not and Or. i.e,, replace “A Imp B” with
“Not A Or B”, or, if A and B are more complex, “((Not Does not apply
A) Or B)”. See Not and Or operators, and Boolean
Operator for VB6 Users.
Implements VB.NET supports the Implements keyword, but its
argument must be an Interface type, not a class as in
VB6 (VB6 did not support interface types, but used
Does not apply
class skeletons in their place). Further, the Implements
keyword is also used to qualify method and property
declaration that implement a member of the interface.
Initialize event Not supported in VB.NET; use Sub New. See
Class_Initialize Changes for VB6 Users and Using Does not apply
Constructors and Destructors
Input #, Input$ Input, InputString function
statements, Input$, Visual Basic Run-Time Library
InputB, InputB$ Members, FileSystem module
functions
InputBox function The VB.NET version of the InputBox function works
just like the VB6 version, except for:
VB6 accepts vbCR characters as line separators in the
message, but pre-VB2008 requires vbCRLF.
VB6 takes an optional pair of coordinates defined in twips, Visual Basic Run-Time Library
whereas VB.NET uses pixels.
Members
VB.NET fires a Deactivated event for the current
form, and then an Activated event when the input box
is closed and the form regains the input focus. Does
not apply
Instancing property Not supported in VB.NET. For class-level declarations,
see Private (Visual Basic) and Public (Visual Basic).
Does not apply
For procedure-level declarations, see Shared (Visual
Basic).
Instr function Instr function, but it would be much faster to use the Visual Basic Run-Time Library
IndexOf method of the string to check. Members, Strings module
InStrB function InStr function Visual Basic Run-Time Library
Members, Strings module
InStrRev InstrRev function, but it would be much faster to use Visual Basic Run-Time Library
the LastIndexOf method of the string to check. Members, Strings module
Int Int (for 16-bit Short Integers) Visual Basic Run-Time Library
Members, Math module
IsEmpty function IsNothing function Visual Basic Run-Time Library
Members, Information module
IsMissing function Not supported in VB.NET. Handled by Nothing
Does not apply
keyword. See Procedure Declaration for VB6 Users.
IsNull function Handled by Nothing keyword. Database nulls can be
Visual Basic Run-Time Library
tested for using the IsDBNull function or against the
Members, Information module
System.DBNull.Value property.
IsObject function IsReference function Visual Basic Run-Time Library
Members, Information module
Kill statement Kill function Visual Basic Run-Time Library
Members, FileSystem module
LBound Lbound, or ArrayObject.GetLowerBound
Lcase$ function Lcase function, or StringObject.ToLower Visual Basic Run-Time Library
Members, Strings module
 Page 141

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
Left$, LeftB, LeftB$ VB.NET supports the Left string function. But, if the
functions code runs in a form or user control, VB.NET
interprets it as a reference to the Left property of the
Form and UserControl object, forcing a compile error.
To avoid this, reference the Microsoft.VisualBasic Visual Basic Run-Time Library
namespace using “Imports VB = Members, Strings module
Microsoft.VisualBasic”, and specify it as in “Return
VB.Left(arg, 1)”, or use the Substring method
exposed by the System.SubString class, such as
“Return arg.SubString(0, 1)”
Len function The VB6 method works with both strings and User-
Defined Types. With UDTs, it returns the number of
bytes taken when the block is written to disk or passed
to a Windows API method. The VB.NET method
Visual Basic Run-Time Library
works with Strings (returns character count),
Members, Strings module
Structures (returns full byte size), and Objects
(returns full byte size). Also, if we simply want the
character count of a string, consider the faster Length
method of the target string.
LenB function The VB6 method works with strings and UDTs,
returning the number of bytes. VB.NET recommends
Len instead, because LenB is unsupported. VB.NET’s
Len function now works with the functionality of both Visual Basic Run-Time Library
VB6’s Len and LenB functions. It returns a character Members, Strings module,
count (½ the actual byte size) of Strings, and for System.Text namespace, Unicode
Objects and Structures it returns their full byte sizes class
(in earlier versions, Structures were not supported).
Also use System.Text.Unicode.GetByteCount to get
the full byte sizes of strings.
Let, Set assignment Not supported in VB.NET; the new Set statement is
statements unrelated to the older one. See Default Property Does not apply
Changes for VB6 Users.
Line function DrawLine function System.Drawing.Graphics
Line Input # statement LineInput function Visual Basic Run-Time Library
Members, FileSystem module
Load statement Use the New keyword to create a new instance of a Visual Basic Run-Time Library
form, and the Show function to display it (Load was a Members, System.Windows.Forms
shorthand substitute for New). namespace
LoadPicture VB.NET does not support LoadPicture, using instead
the Image.FromFile method. There are a few
differences, because the Image.FromFile method
does not support the optional size, colorDepth, x, and Does not apply
y. Also, when an empty string is passed in the first
argument, the LoadPicture VB6 method returns a null
image.
LoadResBitmap, VB.NET does not support LoadResString,
LoadResData, LoadResBitmap, and LoadResData methods. Does not apply
LoadResString
LOC # statement LOC statement, or System.IO.File.Position Visual Basic Run-Time Library
Members, FileSystem module
LoadResBitmap, VB.NET does not support LoadResString,
LoadResData, LoadResBitmap, and LoadResData methods because Does not apply
LoadResString VB6 style resource files have been superceded..
LOC # statement LOC statement, or System.IO.File.Position Visual Basic Run-Time Library
Members, FileSystem module
Lock # statement Lock, Unlock functions Visual Basic Run-Time Library
Members, FileSystem module
 Page 142

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
LOF # statement LOF statement, or System.IO.File.Length Visual Basic Run-Time Library
Members, FileSystem module
Log function Math.Log method System Namespace, Math Class
LSet, RSet statements Lset, Rset. Also try PadRight and PadLeft methods of
the System.String class. Note that LSet no longer
copies user-defined types. VB.NET uses structures
that can be copied directly using “=”. Members can Visual Basic Run-Time Library
also be copied one by one. See Data Type Changes Members, Strings module; and
for VB6 Users. Also see the article, Passing Data System Namespace, String Class
Between Protected and Unprotected Memory Under
VB.NET” to see how to move a structure as a block of
memory.
Ltrim$ function Ltrim function, or the much faster Visual Basic Run-Time Library
StringObject.TrimStart Members, Strings module
Mid function Mid function, but it would be much faster to use the Visual Basic Run-Time Library
SubString method of the string to check. Members, Strings module
MidB function Mid function Visual Basic Run-Time Library
Members, Strings module
MidB statement Mid Statement Visual Basic Run-Time Library
Members, Strings module
MkDir statement MkDir function, or Visual Basic Run-Time Library
System.IO.Directory.CreateDirectory Members, FileSystem module
MOD function MOD function. The VB6 Mod operator converts its
operands to integers and then returns the remainder of
the division. The VB.NET Mod operator does not
perform integer conversion, but retains the operands in
their original type. If the operands are floating-point Visual Basic Run-Time Library
numbers, the result is the remainder of the floating-point Members
division. If our code requires the same VB6 functionality
under VB.NET, the operands of the Mod operator should
be explicitly converted to 32-bit integers before using the
Mod operator: “result = Cint(op1) Mod Cint(op2)”.
MsgBox function The VB.NET version of the MsgBox function works
just like the VB6 version, except for:
The VB6 version accepts individual CR characters as line
separators in the message, but pre-VB2008 requires a CR-
LF pair (vbCRLF).
The VB6 version takes an optional pair of coordinates Does not apply
defined in twips, whereas the VB.NET version uses pixels.
The VB.NET version fires a Deactivated event in the current
form, and then an Activated event when the input box is
closed and the form regains the input focus.
Name statement Rename function Visual Basic Run-Time Library
Members, FileSystem module
Next statement Next statement. However, under VB6 a single Next
keyword could terminate two or more For loops. For
example: For I = 1 To 10
For j = 1 To 10
... Does not apply
Next j, i
VB.NET does not support this syntax and requires, for
structural integrity, that each For loop be explicitly
terminated by a distinct Next keyword.
Now function Now Property Visual Basic Run-Time Library
Members, DateAndTime module
Null keyword Handled by Nothing keyword. Does not apply
ObjPtr function Not supported. However, the VarPtr function
demonstrated on page 56 of this document can be Does not apply
used to return a valid ObjPtr address.
 Page 143

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
Oct$ function Oct function, or Convert.ToString(Integer, 8) Visual Basic Run-Time Library
Members, Conversion module
On ... GoSub Not supported in VB.NET; use Select...Case
Does not apply
construction Statement. See Control Statement for VB6 Users.
On ... GoTo construction Not supported in VB.NET; use Select...Case
Does not apply
Statement. See Control Statement for VB6 Users.
Open # statement FileOpen function Visual Basic Run-Time Library
Members, FileSystem module
Option Base statement Not supported in VB.NET. See Array Bounds for VB6
Users. Zero-based arrays only. See point 7 on page Does not apply
171 for a simple class solution to this issue.
Option Explicit Option Explicit On Does not apply
Option PrivateModule Private Module declaration. The Option Private
statement directive is used only in Access VBA and had no effect
in VB6. VB.NET does not support this directive. Use Does not apply
the Module Statement in VB.NET. Any module can be
marked as private using the Private keyword.
Or (logical function) Or (logical function), but it would be much faster to
Visual Basic Run-Time Library
use OrElse. If the leading expression is true, then the
Members
trailing expression will not need to be evaluated.
Print # statement Print, PrintLine function Visual Basic Run-Time Library
Members, FileSystem module
Property Get, Property Not supported in VB.NET. See Property Procedure
Let, Property Set Changes for VB6 Users. Does not apply
statements
Pset, Scale No longer supported, yet equivalent functionality
N/A
exists in the System.Drawing namespace.
Put # statement FileGet function. FileGet does not work the same with
nonscalar values, so we cannot swap data with VB6 apps IF
the file has Variant values. If a file has dynamic arrays or
structures, we must marshal them. When FileGet gets a
dynamic array in Random Access files, a two-byte length Visual Basic Run-Time Library
value is expected prior to each array, making a slightly Members, FileSystem module,
longer file. To force the same results as VB6, use the System.Runtime.InteropServices
ArrayIsDynamic argument with FileGet to prevent length
descriptors from being expected or checked. For structures,
also lead them with: <StructLayout(LayoutKind.Sequential,
Pack:=1)>
Randomize System.Random object is already randomized declared System Namespace, Random Class
ReDim In VB6 the ReDim keyword can both declare an array
and create its elements; in VB.NET we need two
separate statements: the Dim keyword declares the Visual Basic Run-Time Library
array (and optionally creates its elements), but the Members, Array module
ReDim keyword can only create (or recreate) array
elements.
Rem VB6 supported multi-line remarks. For example: VB.NET doesn’t support this. We
‘ first line _
second line _
must add an apostrophe at the
third line beginning of each line.
Replace StringObject.Replace method Does not apply
Reset statement Reset function Visual Basic Run-Time Library
Members, FileSystem module
Return VB.NET does not support the GoSub keyword,
therefore it does not need to support the Return
Visual Basic Run-Time Library
keyword. However, VB.NET uses the Return keyword
Members
to return a value from a Function or from the Get
block of a Property block.
 Page 144

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
RGB RGB function, useful for API, but .NET uses ARGB
System.Drawing Namespace, Color
color values. For these, use the
Class
System.Drawing.Color.FromArgb function.
Right$, RightB functions VB.NET supports the Right string function. But, if the code
runs in a form or user control, VB.NET interprets it as a
reference to the Right property of the Form and UserControl
object, Forcing a compile error. To avoid this, reference the
Microsoft.VisualBasic namespace using “Imports VB =
Visual Basic Run-Time Library
Microsoft.VisualBasic”, and specify it as in “Return Members, Strings module
VB.Right(arg, 1)”, or use the Substring method
exposed by the System.SubString class, such as “Return
arg.SubString(arg.Length-1, 1)”
RmDir statement RmDir function, or Visual Basic Run-Time Library
System.IO.Directory.RemoveDirectory Members, FileSystem module
Rnd Rnd, or System.RandomObject.Next System Namespace, Random Class
Round function Math.Round function System Namespace, Math Class
Rtrim$ function Rtrim function, or the much faster Visual Basic Run-Time Library
StringObject.TrimEnd Members, Strings module
SavePicture method Image.Save method Visual Basic Run-Time Library
Members
SaveSetting statement SaveSetting function Visual Basic Run-Time Library
Members, Interaction module
Scale method Not supported in VB.NET. Use Anchor property. Does not apply
Screen.ActiveControl System.Windows.Forms.Form.ActiveForm. System.Windows.Forms namespace,
ActiveControl or Me.ActiveControl Form class
Screen.ActiveForm System.Windows.Forms.Form.ActiveForm System.Windows.Forms namespace,
Form class
Screen.FontCount System.Drawing.FontFamily.Families.Length System.Drawing namespace,
FontFamily class
Screen.Fonts collection System.Drawing.FontFamily.Families System.Drawing namespace,
FontFamily class
Screen.Height System.Windows.Forms.Screen.PrimaryScreen.Bou System.Windows.Forms namespace,
nds.Height Screen class
Screen.MouseIcon Me.Cursor = New
Not applicable
Cursor(“C:\WINDOWS\Cursors\newcur.cur”)
Screen.MousePointer System.Windows.Forms.Form.ActiveForm. System.Windows.Forms namespace,
Cursor.Current Forms class. NOTE: This no longer
works. See Black Book Tip # 32 on Page
568 for a working solution.
Screen.TwipsPerPixelX VB.NET does not operate with Twips. Not Applicable
Screen.TwipsPerPixelY VB.NET does not operate with Twips. Not Applicable
Screen.Width System.Windows.Forms.Screen.PrimaryScreen.Bou System.Windows.Forms namespace,
nds.Width Screen class
SendKeys SendKeys.Send method Visual Basic Run-Time Library
Members
Seek# Both the Seek# command and the Seek function map
Visual Basic Run-Time Library
to the Seek method in VB.NET. The file number
Members
cannot be preceded by #.
Set, Let assignment Not supported in VB.NET; the new Set statement is
statements unrelated to the older one. See Default Property Does not apply
Changes for VB6 Users.
SetAttr statement SetAttr function, System.IO.File.SetAttributes, or Visual Basic Run-Time Library
System.IO.Directory.SetAttributes Members, FileSystem module
Shell Process.Start function
Sgn function Math.Sign function System Namespace, Math Class
Sin function Math.Sin Method System Namespace, Math Class
 Page 145

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
Spc function SPC function. The Spc keyword was used in VB6 to
insert spaces in a Print, Print#, and Debug.Print
Does not apply
method. The VB.NET SPC function is used only
within Write, WriteLine, and Debug.Print statements.
Split function Split, or StringObject.Split function, with only one
minor difference from the VB6 function: when its first
argument is an empty string, the VB6 version returns a
non-initialized dynamic string array (the array has Does not apply
Lbound=0 and Ubound=-1.) However, the VB.NET
version more safely returns an array with one element
at zero index set equal to vbNullString (“”).
Sqr function Math.Sqrt Function System Namespace, Math Class
Static keyword Static keyword. VB6 supported the Static keyword at
both the variable-declaration level, preserving the
value of the variable during the lifetime of the running
application, and at the method-declaration level,
resulting in all variables within the method being
treated as static variables. VB.NET supports the Static
keyword only at the variable-declaration level, Does not apply
meaning that all pertinent variables within a method
should be declared Static if we wish to emulate the
VB6 functionality, though this is not memory-
efficient, nor is it a very practical solution. Extremely
few applications or methods would ever benefit from
such VB6 functionality.
Stop keyword Stop keyword, however, we should replace this Debug
keyword with Debugger.Break, because the
System.Diagnostics Namespace,
Debugger.Break keyword will be ignored when the
Debug Class
project is compiled, but the Stop keyword will cause
the application to crash.
StrConv function StrConv function. However, the VB6 version would
take both strings and Byte array as its first argument.
It could also convert from/to ANSI and Unicode. The
VB.NET version works only with strings, and though
more powerful, it cannot take Byte arrays, nor can it
convert to/from Unicode. However, such conversion
functionality has been marshaled to the System.Text.Encoding namespace
System.Text.Encoding namespace. For example:
‘To convert a String to a Byte Array:
Dim MyString As String = “Hello”
Dim Bytes() As Byte = _
System.Text.Encoding.UTF8.GetBytes(MyString)
‘To convert the Byte Array back to a String:
Dim strText As String = _
System.Text.Encoding.UTF8.GetChars(Bytes)
String function VB functions that returned Strings by appending a
dollar sign ($) are replaced by overloaded methods.
System Namespace, String Class
Their counterparts that return Variants have been
replaced with overloaded methods that return Objects.
String[$] functions Not supported in VB.NET. See String[$] Function for
Does not apply
Visual Basic 6.0 Users.
StrPtr function Not supported. However, the VarPtr function
demonstrated on page 56 of this document can be Does not apply
used to return a valid StrPtr address.
Tab function Tab function. The Tab keyword was used in VB6 to
insert spaces in a Print, Print#, and Debug.Print
Does not apply
method. The VB.NET Tab function is used only
within Write, WriteLine, and Debug.Print statements.
Tan Math.Tan System Namespace, Math Class
 Page 146

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
Terminate event Not supported in VB.NET; use Sub Dispose and/or
Sub Finalize. See Using Constructors and Does not apply
Destructors.
Time function, Time Replaced by the TimeOfDay property of the
statement System.DateTime structure. Note that the Date data
Visual Basic Run-Time Library
type is no longer represented as a Double; instead, it is
Members, DateAndTime module
represented as a DateTime structure. See DateTime
Structure, Date Data Type.
Time$ function TimeString Property Visual Basic Run-Time Library
Members, DateAndTime module
Timer function Timer Property. The VB6 function returned a Single
Visual Basic Run-Time Library
value; the VB.NET Timer function returns a Double
Members, DateAndTime module
value.
To keyword The To keyword is supported inside Dim and ReDim
statements; however, under VB.NET the lower indices
of the array can only be zero. Therefore, in practice, Does not apply
this keyword can be removed with no difference in
program operation.
Trim$ function Trim or the much faster StringObject.Trim function. Visual Basic Run-Time Library
Members, Strings module
Type statement Not supported in VB.NET; use the Structure
Does not apply
Statement. See Structure Declaration for VB6 Users.
Typename function Typename function or Object.GetType.TopString.
However, there is one important detail that we should
verify when migrating VB6 code over to VB.NET and
that is that in checking for upgraded variable type
naming. Normally, this is confined to VB6 Integer
(16-bit) and Long integer (32-bit) variables. In
VB.NET an Integer is 32-bits, and a Long is 64-bit. A
VB6 Integer is in VB.NET as 16-bit Short, and a VB6 Does not apply
Long is a 32-bit Integer in VB.NET. For example, if a
variable named “MyValue” was a VB6 Integer, and
we were upgrading the following statement to
VB.NET:
If TypeName(MyValue) = “Integer” Then

The text in the expression cannot be upgraded. The


text must be manually changed to “Short”.
TypeOf function TypeOf function, However, in many cases the VB6
version does not exactly corresponds to the VB.NET
version. For example, in VB6, the following test
always succeeds in VB.NET, because all data types
inherit from System.Object:
If TypeOf value Is Object Then Does not apply
If the test for Object is meant to check that a value
isn’t scalar, we will instead need to use code much
like the following:
If TypeOf value IsNot String AndAlso _
Not value.GetType().IsValueType Then
Ubound Ubound, or ArrayObject.GetUpperBound
UCase$ function UCase function, or the much faster Visual Basic Run-Time Library
StringObject.ToUpper method. Members, Strings module
 Page 147

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
Unload statement We cannot unload a form in VB.NET as we did in
VB6. Invoking the form’s Close method, in the form
“Me.Close” has the same effect. However, if the
application is to close itself when the primary form is
closed, and we will close the form while it is
executing the form’s Load event, the form and
application will close, but with a system error. To
avoid this, if we must re-close a form during its Load
event, activate a timer at the end of the Load event for
a short period (10 milliseconds is more than enough Does not apply
time in most cases), and let it close the form outside of
the Load event. The in-load closing will cause an error
because there are other processes that must execute
during the form’s instantiation, before it can properly
close.
Note: On forms that we have displayed as a dialog
(non-modal), Me.Close is not enough, but we must
also invoke the form’s Dispose method if its resources
are a great concern.
Unlock statement Lock, Unlock functions Visual Basic Run-Time Library
Members, FileSystem module
Usercontrol keyword In VB6 we could use the UserControl keyword within a
user control class to reference the current user control.
However, because VB.NET is uniformly configured, we
Does not apply
would instead use the keyword Me.
Variant data type Object Data Type; see Universal Data Type Changes
Does not apply
for VB6 Users.
VarPtr function The VB6 VarPtr function returned the address of a
variable’s data as an integer that could in turn be
passed to Pinvokes that take addresses. However, so
does sending a variable ByRef, even under VB6. As
such, there is little practical use to this functionality, System.RunTime.InteropServices
except for passing the address of a Structure to a namespace
Pinvoke. Refer to the very first point of the article,
“VB6 AFICIONADO COMPLAINT DEPARTMENT”, to
see how to easily implement this lost functionality
back into VB.NET, or look to page 215.
VarType Although it still exists, it should be replaced by the
GetTypeCode method of the specific data types, or use
GetType, which returns an object of class Type that
has properties to get information. N/A

Vartype as of VB2005 allows we to inspect the actual


type of an Object.
vbCFBitmap DataFormats.Bitmap System.Windows.Forms namespace,
enumeration DataFormats enumeration
vbCFDIB enumeration DataFormats.DIB System.Windows.Forms namespace,
DataFormats enumeration
vbCFEMetafile DataFormats.EnhancedMetafile System.Windows.Forms namespace,
enumeration DataFormats enumeration
vbCFFiles enumeration DataFormats.FileDrop System.Windows.Forms namespace,
DataFormats enumeration
vbCFMetafile DataFormats.MetafilePict System.Windows.Forms namespace,
enumeration DataFormats enumeration
vbCFPalette DataFormats.Palette System.Windows.Forms namespace,
enumeration DataFormats enumeration
vbCFRTF enumeration DataFormats.Rtf System.Windows.Forms namespace,
DataFormats enumeration
 Page 148

VB6 element VB.NET-equivalent Namespace, class, or run-time library


location
vbCFText enumeration DataFormats.Text System.Windows.Forms namespace,
DataFormats enumeration
Wend keyword While loops under VB.NET end with the End While
statement instead of Wend, which complies with
Does not apply
uniform blocking declaration in VB.NET; see Control
Statement for VB6 Users.
Width # statement FileWidth function Visual Basic Run-Time Library
Members, FileSystem module
Write # statement Write, WriteLine functions Visual Basic Run-Time Library
Members, FileSystem module
 Page 149

VB.NET Behavior Changes Deemed Important by VB6 Aficionados.


Adapted from Microsoft and VB Migration Partner, available freely from these sources.
We took a much broader look at VB.NET differences with VB6 language features in An Exhaustive List
of VB6 Commands Compared to VB.NET on page 136, Listed here we will take a more detailed look at
what users have considered to be the more important ones. Note, however, that much of the whining,
err... distant forlorn echoing laments, chest-thumping, and the dragging of chains through lonely
forbidding deeply shadowed halls often involve nothing more than a simple name-, or even to a single
letter-change, such as Print # to Print. For example, consider the following short sample list of common
VB6 commands and their corresponding VB.NET counterparts:
VB6 Statement VB.NET Corresponding Statement
ChDir ChDir
ChDrive ChDrive
Close FileClose
FileCopy FileCopy
Get FileGet
Input # Input
Kill Kill
Line Input # LineInput
Lock Lock
MkDir MkDir
Open FileOpen
Print # Print, PrintLine
Put FilePut
Reset Reset
RmDir RmDir
SetAttr SetAttr
Unlock Unlock
Width # FileWidth
Write # Write, WriteLine

As you can see, some name changes were made simply to enforce language command name uniformity, to
simplify our remembering of them. For example, the VB6 commands Open, Close, and FileCopy are now
FileOpen, FileClose and FileCopy under VB.NET.
Various other important VB6 features, as incorporated into VB.NET, work differently; some unnoticeably,
some slightly, some profoundly, and some do not work at all between the two. This should be no surprise and
should have been fully expected when one is moving from a procedural language that was rooted in a small
architecture universe to an object-oriented language designed to breach beyond 32-bit technologies, and most
especially from a language that had been fitted so inexorably to a 16-bit edifice that its trying to compete in a
32-bit, and now a 64-bit platform was becoming almost laughable.
Sometimes I think VB.NET would have been more readily accepted by died-in-the-wool VB6
Aficionados if VB.NET and Visual Studio .NET were simply introduced as a new and separate platform,
that the old Visual Studio 6-style platform was dropped as a suite, save that VB6 would be maintained
and proactively advocated as a wholly stand-alone legacy platform. Although those of us not confined
 Page 150

by the argument, “I know how to program in VB6 and I don't want to learn anything more” knew the end
was coming. The best part is, these same people who refused to budge also kept running into the brick
walls that VB.NET would have easily freed them from, but instead kept stubbornly demanding that
Microsoft add another function or provide a DLL to extend the language so they could also do some of
the cool things they saw other kids on the playground doing, even though VB6 had been “rode hard and
put away wet” so many times and had been patched, shoehorned, and duct-taped so much that by 1999
all objective observers clearly knew that VB6 had painted itself into a wanna-be-16-bit environment that
spoke DOOM (add a basso voice with reverb for effect – and wearing a long black hooded shroud
helps) in the advent of emerging and far more extensible technologies. Having said that, if VB6 had
been wholly re-written from the ground up using current technology, update its forms package to rub
shoulders with .NET, extend UDTs to support .NET Structure extensions, extend scalar functionality.
and so on, to include options to auto-promote types to a VB.NET environment, a natural. Comfortable,
and indeed enthusiastic migration to VB.NET would have been inevitable. I was actually working on
doing exactly that when my eyesight began to fail. But now that it is slowly returning...
That, or introduce them to the easier version of VB.NET that Microsoft supports called Small Basic, a
free, easy to use pre-Visual Basic.NET development environment and compiler supported by Microsoft
at http://msdn.microsoft.com/en-us/beginner/ff384126.aspx. I am not joking. This thing is fun!
Under normal circumstances these language changes have little to no effect on program execution.
However, it is important that we be aware of those differences, especially those whose functionality had
been dropped due to ancient technology, unsafe usage, non-use (except by ol' Bob, and what's-his-name,
who lives over on the flood plain in the next county), or non-standard syntax, and we should check our
code to ensure usage of these differences are addressed and made compatible.

Higher Math Functions


Higher math functions, like Sin, Cos, Tan, Cot, Atn (Atan), Sqr (Sqrt), Sgn (Sign), and many other
functions that we petty mortals have little use of when balancing our checkbooks, have thankfully been
moved out of the core language and into the Math namespace, thus "result = Atn(value)" has become
"result = Math.ATan(value)". Or, if we import the namespace in the heading of our file, such as " Imports
System.Math", we can use these functions as we did before. This approach allows extending higher math
functions later with more extensive functions without having to change the core language.

# – Various Compiler Directives


VB6 uses compiler directives, logic primitives, like #Const, #If, #Then, #Else, and #End If to tell the
compiler how to build the executable. A #Const directive can be assigned a symbolic name and given a
value, such as #Const InHouse = 1. Later in the code, a #If directive can evaluate InHouse and if it
results in a true value (non-zero), the code following the #Then directive is compiled, otherwise the code
following the optional #Else, which defines code to compile instead, is processed, until a #End If
directive is encountered, whereupon normal compiling resumes.
These Compiler Directives are also supported under VB.NET, but be aware that the use of some VB6
expression evaluators, such as Abs, Sgn, and LenB, are not supported and warnings will be issued. This is
because LenB support has been dropped (see below) and the math functions have been moved out of the
core language of VB.NET into the System.Math namespace. But really, how hard is it to type “Math.”?

AddressOf
VB.NET supports AddressOf. Unlike the VarPtr method we created and describe elsewhere on pages 56,
159, and 169 to retrieve the address of variable or object data, AddressOf is used instead to acquire the
address of a selected block of program code. However. To access the address of a method's code, we
must declare a Delegate that will be associated with that method, which can in turn be used to retrieve
its address. This ensures the type-safe passing of the target method and any parameters, especially when
we need to access methods that have been defined in unmanaged system space, such as in Win32 DLLs.
 Page 151

Although I do not cover this in my article on Delegates, Understanding VB.NET Delegates on page 272,
because I thought it detracted from the fastest way to alleviate user frustration with them, a Delegate is
actually a Class declaration. It even has a parameterized New constructor. We can also expose it after
constructing our Delegate. Say we created the Delegate WndProcDelegate to stop the compiler whining
about us using AddressOf and not declaring a Delegate for the third parameter of the SetWindowLomg
Pinvoke, "oldProc = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf WndProc)". Although the compiler can
automate the re-evaluation of the AddressOf invocation of the WndProc Delegate for us, we can still use
"oldProc = SetWindowLong(hWnd, GWL_WNDPROC, New SetWindowLongDelegate(AddressOf WndProc))", if we are
keen to performing lots of additional, but unnecessary typing, to expose what is being done “behind the
curtain”, as well as self-document the explicit use of a Delegate in our code.

AppActivate
The VB6 AppActivate method provides for an optional second Boolean parameter that, if set to True
(default is False), tells the method to wait until the external application receives input focus. The
VB.NET version does not accept this seldom-used, cobwebbed second parameter.
Array Function
The VB6 Array statement returned a Variant array, allowing us to assign values to that array in a single
line of code. Because Variants are not supported under .NET, the VB6-type Array function is not
supported (this differs from the unrelated, but similar Array Class that VB.NET does support, which
stores members as an array of Objects). When a VB6 project is upgraded to VB.NET, Variant variables
are converted to the Object data type, so the upgraded code will likewise return an array of type Object.
What to do next: Review the code. One may prefer to use a value-type array in place of the object array,
and assign the old Array values like: “Dim i() As Integer = {1, 2, 3}”. Though using Objects is several
times faster than using variants, using specific types are even faster than using generic Objects.
Note that VB Migration Partner offers a replacement method named Array6 that emulates the VB6
Array functionality. Following is its complete source code:
Public Function Array6(ByVal ParamArray args() As Object) As Object
Return args
End Function

AscB, ChrB, InputB, InstrB, LeftB, LenB, MidB, RightB


VB.NET does not support byte-oriented string methods, and have marked them as obsolete. After all,
VB has been using 16-bit strings, not 8-bit, since VB4 (well, VB4 came in both a 16-bit version that did
8-bit processing, and a 32-bit version that did 16-bit processing, but VB5 left 8-bit behind).

Calendar
VB.NET does not support the VB6 Calendar property. Instead, VB.NET provides many more powerful
dating function support in its Namespace libraries that made an intrinsic Calendar property unnecessary.

CDate, IsDate
VB.NET versions of CDate and IsDate are less forgiving than the VB6 versions. Under VB6, these
methods attempt to reverse the day-month numbers if they do not make up a valid date. For example,
both of the following statements under VB6 assign a date constant of #11/23/2008# to a variable:
dat = CDate("11/23/2008") ' dd/mm/yyyy format is assumed.
dat = CDate("23/11/2008") ' mm/dd/yyyy format is assumed.

To VB.NET, one of these assignments fails (which fails all depends on our locale settings). The reason
is that .NET pays closer attention to the Locale, and must assume we are operating with them in mind.
 Page 152

Close#
The VB6 Close# upgrades to the VB.NET FileClose method. The file number cannot be led by #.

Const
VB6 supports implicit conversion between string and numeric constant values, but VB.NET does not.
The following code is legal under VB6, but not under VB.NET:
Const ONE As String = "1"
Const TWO As Integer = ONE * 2 ' this is equal to 2 (numeric)

CStr, Str
Str and CStr support date arguments under VB6, but not under VB.NET. Use format commands under
the object's ToString property, or the Format command instead.

CVar
VB.NET does not support Variants, so CVar is not supported, and we should use Cobj instead.

CVErr
VB.NET does not support CVErr. The easiest way to transport error information is to Throw an
Exception Error: Throw New Exception("Stanley, this is another fine mess you've gotten us into!").

Date, Date$
VB6 overloads the Date keyword, being both the name of the Date type and the name of the function
that returns today’s date. The value of today’s date in VB.NET can be gathered by invoking the Date
class's Today property. Also, references to the Date$ should be upgraded to the DateString property.

Declare
VB.NET fully supports Declare, except the ability to use unmanaged “As Any” parameters (AsAny
under VB.NET using the UnmanagedType compiler marshaling command). Also, watch for the
conversion of VB6 Integers to VB.NET Shorts. and VB6 Lomgs to VB.NET Integers, along with string
conversions between 16-bit Unicode and 8-bit ANSI, which some Pinvokes require. Another thing is that
VB.NET does not support 32-bit integers used as callback addresses, as happens with a few Windows
methods where the use of AddressOf is specified, unless we also declare a Delegate class with prototype
syntax, and then change the parameter type so that it uses that delegate. Finally, a minor problem is that
VB6 allows Declare statements that contain two or more parameters with the same name, which is
illegal under VB.NET. Of course, few VB6 users were even aware of that.
DefBool, DefByte, DefCur, DefDate, DefDbl, DefInt, DefLng, DefObj, DefSng, DefStr, DefVar
These compiler directives, which go all the way back to the days of Tiny Basic in the early 1970s, were
essential to primitive computers (and big expensive ones occupying whole floors of a university's
Computer Science Department) with limited memory space. Back then, it was nothing for a computer
(which we had to solder together from parts and mask our own printed circuit boards) to sport just 4K
(4096 bytes) of memory, and we were limited to variable names of just 1 or 2 characters (my first
computer had 4K, untill I upgraded to an 8K TRS-80, but boy was I loaded for bear when I upgraded it
to 16K and modified it to support lowercase characters). These then-essential commands, borrowed
from the original FORTRAN language, have no place in languages sporting long variable names, where
using these keywords would severely limit us giving meaningful names to our variables.
 Page 153

Dim
VB6 allowed us to define variables without the “As” clause, in which case the type of the variable is
affected by the DefXxx directive whose range corresponds to the first character of the variable’s name. If
no DefXxx directive was present, the Variant type was used by default. VB.NET allows Dim keywords
without the As clause only if Option Strict Off is used; where it will then use the type Object as the
default. A different rule applies if the variable is part of a list, like in this statement:
Dim x, y, z As Double

To VB6, the z variable is of type Double, whereas x and y are affected by the current DefXxx directive
(or are Variants if no directive is found). Conversely, to VB.NET, all three variables are of type Double.
Another difference; if Dim appears inside a code block, then the VB.NET limits the scope of the variable
to the block itself, but VB6 ignored scoping rules and made the variable visible to the entire method.
Also, VB6 supported Auto-instancing Arrays, like “Dim ary(10) As Widget”, where an array is declared
as a class, and each element is instantiated as a new class member. VB.NET will declare the array to that
type, but its elements are uninstantiated (set to Nothing). Afterward, For...Each the array to address that.

Dim (arrays)
VB.NET does not support non-zero-based arrays; arrays whose lower index is not zero (but see point 7
on page 171 to see how to still do that under VB.NET). VB.NET also requires that the rank – the number
of dimensions of the array – be specified if the array is declared but not initialized. See page 171 for a
simple class to support arrays with a non-zero lower bounds.
Dim arr(,) As Integer ' a VB.NET two-dimensional array of integers.

Another difference is that VB6 supported Implicit Declaration opf Object Arrays, so under VB6, if you
wanted to create an array og instantiated objects, you could do so like this:
Dim Ary(5) As New clsObj 'create an array of 6 (0-5) instantiated objects of type clsObj.

VB.NET does not support this slow and cumbersome process, vecause it can be better expressed, and a
whole lot faster, in a small custom loop with explicit declarations. See Notes on Implicit Declaration of
VB6 Object Arrays on page 134 for complete details and for a sample VB.NET implementation.

Dir
The VB6 Dir function was used to return the name of a file or directory. The returned "." and ".."
strings were used in the path argument list to represent the current and the parent directory, respectively.
The VB.NET Dir function is the same, but the use of "." and ".." syntax has different behavior.
What to do next: VB6 Code that parsed directories often assumed "." and ".." were the first two entries,
and so skipped them. But this is no longer true under VB.NET. Because of this minor difference, such
upgraded code will skip the first two files or directories. A best practice is that we should simply check
to see if the left-most character of a directory name starts with ".", and ignore those that do, as in “ If
Left(dirValue, 1) <> "." Then”. Even better, consider using the much faster System.IO namespace.

DoEvents
The DoEvents method ensures that all pending Windows messages are processed, then it returns the
number of open forms. For VB.NET, it is invoked using Application.DoEvents. Like VB6, it ensures
pending messages are processed, yet unlike VB6, it returns no value. Very few seemed to be aware that
the VB6 version returned a value, anyway. The number of developers who did know about it could have
all fit inside a clown car, and most of them worked on the VB6 Support Team at Microsoft.
 Page 154

End
VB.NET supports End, but it is best that we invoke Application.Exit instead of, or before using the
End. This is the VB.NET code that VB Migration Partner used in place of End:
Application.Exit(): End

EndIf
VB6 supports the spelling of End If as EndIf. The VB6 editor automatically expands this word into End
If, but this obsolete keyword might be encountered in applications whose source code is automatically
generated. VB.NET does not support the EndIf spelling.

Enum
VB6 allows us to use almost any character in an Enum member name. If it is not a valid VB identifier,
we just need to enclose it in square brackets:
Public Enum Test
[two words] ' embedded space; for VB.NET, we can replace this with: two_words.
[dollar$ symbol] ' symbol and space; for VB.NET, we can replace this with: dollar__symbol.
[3D] ' leading digit; for VB.NET, we can replace this with: _3D.
[New] ' language keyword; for VB.NET, this is acceptable; square brackets around VB Keyword.
End Enum

VB.NET forbids Enum member names with starting digits, or contain spaces or symbols, except the
underscore. It does support square brackets in Enum names, but they only allow defining names
matching a VB language keyword.
Also, under VB6, the name of an Enum Member is considered global, and so it can be referenced
without the Enum name. Under VB.NET, the name of the Enum cannot be omitted.

EOF#
The EOF# keyword is used by VB.NET as EOF. The file number cannot be led by #.
Eqv
VB.NET does not support Eqv, but it is easy enough to duplicate using this equivalent expression:
result = (CBool(expr1) = CBool(expr2))

Erase
A VB6 array can be declared as static or dynamic. A VB6 static array is declared in a Dim statement
with an indexing bounds specified, such as Dim Ary1(10) As String. A VB6 dynamic array is declared
when the array is first declared using no indexing bounds, such as Dim Ary2() As String, and later
instantiated by using ReDim, such as ReDim Ary2(10) These two VB6 array types use different memory
allocation mechanisms. Memory for static arrays are allocated at compile time, whereas dynamic arrays
are always allocated at runtime. To us mere mortals, we do not seem to see any difference between the
two, except for those of us who notice something peculiar going on with the Erase command.
When Erase is used on a static array, the memory allocated to the array is simply cleared; all its array elements
are reset to their default value, which is zero for scalars and Nothing for strings, but the array bounds are not
removed and we can afterward assign values to its elements. On the other hand, when a dynamic array is erased,
its memory allocation is released and any attempt to reference an element within it fires a runtime exception.
All arrays under VB.NET are dynamic. and Erase behaves exactly like all VB6 dynamic arrays. This
minor difference can cause a problem when migrating VB6 code that took advantage of static arrays and
erases a static array, and then afterward referenced one of its elements:
' this code works with VB6 but fails under VB.NET, EXCEPT as noted, below.
Dim Ary1(10) As Double ' a static array.
' ...
Erase Ary1 ' Under VB.NET, use System.Array.Clear(Ary1) to duplicate this VB6 behavior!
Ary1(0) = 123 ' exception under VB.NET, unless we used System.Array.Clear(Ary1).
 Page 155

Error
VB.NET does not support Error. We should instead invoke the Err.Raise method, or Throw an
Exception Error, such as: Throw New Exception("Alert! My punch bowl is out of M&Ms!").

FileAttr
VB6 supports FileAttr that features two parameters. If the optional second parameter is missing or set to 1.
the function returns the mode used to open the file. If that parameter is set to 2, the function raises Error 5; it
had once returned the operating system's handle for the file under 16-bit Visual Basic. VB.NET supports
only one parameter, because the second parameter last worked correctly in the 16-bit version of VB4.

FileDateTime
The VB6 FileDateTime method worked with both files and directories. FileDateTime under VB.NET
works only with directories.

Get#, Put#
The VB6 Get# and Put# functions were used to read and write data to a file.
Under VB.NET, these are upgraded to the FileGet and FilePut functions, and The file number cannot be
led by a #. When these functions are used to pass dynamic arrays or strings to a file opened in Random
mode, a two-byte length descriptor is by default added prior to each array/string entry so to specify its
data length, resulting in a different and longer file length. When reading, these values are used to tell
VB.NET the length of the array/string to be read.
What to do next:
1. If we want the same results for strings as under VB6, then, when opening the file in Random mode, be sure to set the
StringIsFixedLength argument to True, indicating that a length descriptor is not to be used, and that the passed strings are already set
to their required size (i.e., Dim myStr As New String(ChrW(0),32)).
2. If we want the same results for arrays as under VB6, also be sure to set the optional ArrayIsDynamic argument to False with the
FileGet and FilePut functions, preventing length descriptors from being read or written that would specify the number of dimensions
and their lengths, which means the arrays being passed predetermines these parameters.

GoSub
VB.NET does not support GoSub. We should move the code in the GoSub block to a distinct method,
defining and passing as arguments the local variables that the GoSub block uses. Besides, in benchmark
testing, it was found that a GoSub invocation takes about 5 times longer to execute than invoking a
normal subroutine method.

ImeStatus
VB.NET does not support the ImeStatus method. No one seemed to know it was there.

Imp
The Imp operator is not supported by VB.NET, but it is very easy to duplicate with either of these
equivalent expressions:
result = Not op1 Or op2 'suggested implementation.
result = Not (op1) Or (op2) 'more robust implementation od the above line.

Implements
VB.NET supports the Implements keyword, but its argument must be of an Interface type, not a class,
as VB6 required. The Implements keyword is also used by VB.NET to qualify methods and properties
that implement a member of that interface, to help document which components are provided by the
implementation, and which are developer-defined. Here is a method provided by an implementation:
Private Sub IAddin_Connect(ByVal root As Object) Implements Iaddin.Connect
'...
End Sub
 Page 156

Input#
The VB6 Input# keyword is used as Input by VB.NET. The file number cannot be led by #.

InputBox
Under VB.NET, the InputBox function works just like it did under VB6, save for three minor variations that
few VB users are even aware of. First, VB6 and VB.NET, as of VB2008, accept either a single Carriage
Return (vbCr; ChrW(13)) or a Carriage Return-Linefeed pair (vbCrLf; ChrW(13) & ChrW(10)) as a message text
line separator, but pre-2008 VB.NET editions allowed only a CR-LF pair. Second, VB6 accepted an optional
pair of coordinates in twips (15 twips = 1 pixel), but VB.NET accepts only pixels as optional coordinates.
Third, VB.NET throws a Deactivated Event in the invoking form before the InputBox is displayed, and
throws an Activated Event when the InputBox is closed and the invoking form again regains focus.

IsEmpty
VB.NET does not support the Variant value Empty because Variants are not supported. Thus, the
IsEmpty function is likewise unsupported. This is because VB6 had allowed unassigned variables as
function parameters, so VB6 could declare a variable without first assigning it a value before passing it
to a method, This makes data safety unreliable, and so OOPL rules do not allow VB.NET to support it.

IsMissing
VB6 supported Optional Parameters that lack explicit Default values, plus the IsMissing function. Variant-
only Optional Parameters and IsMissing were introduced in VB4, and Typed Defaults were introduced in
VB5. VB.NET, however, requires all Optional Parameters have explicitly declared Default values, which
resolve much faster. To emulate this dated VB6 functionality, use ignorable optional parameters, such as
dates outside expected bounds, say #1/1/1000#, or strings explicitly set to Nothing. Consider this VB6 code:
Sub Execute(Optional ByVal shipDate As Variant) ' in VB.NET: Sub Execute(Optional ByVal shipDate As Date = #1/1/1000#).
If IsMissing(shipDate) Then ' in VB.NET: If shipDate = #1/1/1000# Then ...
shipDate = Now
'...code to process shipDate goes here...
End If
End Sub

IsNull
VB.NET does not support the Null Variant value, and so the IsNull function is also not supported,
because VB.NET does not support Variants. In most cases Null can be declared as either Nothing or
DBNull, so we could use the following test: If value Is Nothing OrElse TypeOf value is DBNull Then ...

IsObject
The VB6 IsObject function was used to determine if a Variant variable was of type vbObject. If the
variable was empty, the IsObject function returned True for an Object, but False for a Variant. Likewise,
if the variable contained a string, it returned False.
When a VB6 project is upgraded to VB.NET, any Variant variables are converted to the Object data
type. Also, the VB.NET IsReference function should be used instead to determine if a variable contains
an Object. If an Object is set to Nothing, the function still returns True, simply because it is an Object
reference. Further, VB.NET strings are also objects, as are arrays of any type, and all will return True.
What to do next: Examine the variables being evaluated by the IsReference function and make sure the
logic is still valid. A test using the Vartype method may be required to differentiate strings and arrays..

Left, Right
VB.NET supports Left and Right string functions. Yet, if it is within form or user control code, VB.NET
interprets these names as references to its Left and Right properties, due to Scoping Rules, which will in
turn throw a compilation error. We can avoid this issue in two easy ways. First, we can explicitly
reference the Microsoft.VisualBasic namespace, preferably with an Imports alias at the top of the file.
 Page 157

Imports VB = Microsoft.VisualBasic
...
Function GetFirstLastChar(ByVal arg As String) As String
Return VB.Left(arg, 1) & VB.Right(arg, 1)
End Function

Second, we can alternatively employ methods exposed by the System.String namespace. For example:
Function GetFirstLastChar(ByVal arg As String) As String
Return arg.SubString(0, 1) & arg.Last(1) 'these indexes are 0-offset. Last(1) returns the Rightmost 1 character of the text.
End Function

Len
The VB6 Len function operates with strings and User Defined Types. With UDTs, it returns the number
of bytes of all its members, both the space taken by scalars and the Char count of any scalar strings and
arrays. VB.NET's Len function works with strings and Structures, but we must take care with strings
and arrays declared within Structures. Unlike VB6 strings and arrays being scalar variables so they are
embedded in UDTs, they are Objects under VB.NET, meaning all that is stored of them in a Structure is a
4-byte IntPtr for each object, not their string or array data. Thus, the Len function will faithfully return
the length of the Structure, but it will only count the length of each member string or array as 4 bytes.
Microsoft tells us to use a Marshal Class (part of System.Runtime.InteropServices), which has a SizeOf
function that accepts objects and abstract classes, like Structures, and will return its size in bytes. In
actual fact, however, I found that it does not accommodate the actual allocated sizes of string or array
members in Structures, except as 4-byte IntPtr types, just as the VB.NET Len function does, although
its greatest advantage over Len is that SizeOf does allow for, and properly accounts for any possible
alignment gaps that might exist between object member fields declared within the structure..
We can get around this by leading each string field with <VBFixedString(xxx)>, to tell VB.NET the
number of Bytes, not Chars, to allocate for it in the Structure, like <VBFixedString(128)> Public sVar
As String, working just like VB6 Public sVar As String * 64 (64 Unicode Chars or 128 bytes, or
better as <VBFixedArray(63)> Dim sVar As Char() ), and precede every array we want embedded in the
structure with <VBFixedArray(xxx)> to define its upper bounds index, like <VBFixedArray(5)> Dim boxID()
As Short for a 6 Short Integer array. We may also need to initialize these members (see Notes on Mixing Fixed
Strings, Fixed Arrays and Structures on page 45 for more details on Structure fixed strings and arrays). Doing
that, the VB.NET Len and SizeOf will work like LenB did under VB6, because VB.NET will then know
how much space to set aside for each member string or array, because with these marshaling commands
the tagged strings and arrays will actually be embedded within the structure or class object. Again.
SizeOf results are more reliable because, unlike Len, SizeOf will account for possible alignment gaps.
NOTE: Remember that these marshaling commands embed string and array data within a structure or class object!
NOTE: VB6 auto-converted most 16-bit Unicode strings to 8-bit ANSI during Win32 Interop Pinvokes for APIs declared as
ANSI “behind the green curtain”, except when the API used Unicode strings.
NOTE: Refer to Notes on Recovering the LenB Function on page 57 for a robust solution to this Structure length issue.

Let, Set
Let and Set assignment verbs are not used by VB.NET. As such, object assignment does not require a
Set verb because everything in VB.NET are concrete or abstract classes, so using Set would be
redundant. Also, its lack of support for parameterless default members ensures that there is no ambiguity
for something like this:
Dim tb As TextBox, txt As String
tb = TextBox1 ' assign an object reference (VB6 would expect SET to be used here to avoid default property assignment).
txt = TextBox1.Text ' (explicitly) assign the default member (VB6 would need a LET if .Text was missing).

NOTE: Be sure to avoid using Default members to avoid Late-Binding, causing significantly slower program execution.
NOTE: Default Properties and Late-Binding together slows total value resolution by as much as 10 times (5 times apiece).
 Page 158

Line Input#
The VB6 Line Input# is used by VB.NET as LineInput and that instead of taking a variable as an
argument, it returns the value read from the file. The file number cannot be led by #.
result = LineInput(1)

Load
VB.NET does not recognize the VB6 Load command. However, creating an instance of a form, such as
Dim Frm As New Form1 does the exact same thing and is used to replace the old Load command.

LoadPicture
VB.NET does not support the VB6 LoadPicture method, but can emulate it using the Image.FromFile
function. However. Image.FromFile does not support the optional size, colorDepth, x, or y arguments,
which few users ever used, though they can be easily emulated with just a little bit of very simple code
in a helper function if these auxiliary parameters are in fact needed. Further, the VB6 LoadPicture
method returned a null image when an empty string was passed to it, which is the same as assigning an
Image the value of Nothing. VB.NET will instead, and properly, report an error. After all, if the user
failed to provide a file, this sounds like an error to me.
NOTE: See Notes on Loading Images and Creating a LoadPicture Function on page 130.

LoadResBitmap, LoadResData, LoadResString


VB.NET does not support LoadResString, LoadResBitmap, and LoadResData. Data stored in resources
no longer requires separate, tedious, and often frustrating compilation outside the IDE in a DOS prompt,
nor does it need the above-mentioned language-intrinsic functions to read from the compiled data. It is
now compiled for us automatically as soon as we leave the IDE's resource dialog. We can also instead
use methods found in the My.Resources namespace to easily extract any of these resources from our code.

LOC#
VB6 LOC# is used by VB.NET as LOC. The file number cannot be led by #.

Lock#
VB6 Lock# is used by VB.NET as Lock, and the To keyword is not recognized. The file number cannot
be led by #.

LOF#
VB6 LOF# is used by VB.NET as LOF. The file number cannot be led by #.

LSet, RSet
It is often said VB.NET does not support LSet or RSet, but they were restored in VB2003, due to loud
uninformed user demand, replacing the more meaningful PadRight and PadLeft methods Microsoft had
offered. LSet had been used under VB6 to copy structures. However, using this command was not a safe
method for copying UDTs, especially if it contained object references. This very same thing can be done
more easily under VB.NET by just assigning structures using the equal sign ( Struct2 = Struct1), because
structures are value types (abstract classes), not reference types (concrete classes), so each variable
points to its own copy of a structure. Even so, the LSet and RSet functions are still available to support
their original intended implementations, which was to place text left- or right-aligned in a fixed-sized
field, padded with spaces as needed on its right side (LSet or PadRight) or left side (RSet or PadLeft).
NOTE: By the way. If a Structure contains instantiated objects, simply copying them using the Equals operator will copy a
reference to the object, not the object itself. So how do we easily get around that? Did you know we can easily add a Clone
method to our structures? It will copy the structure, yet clone any object members. See page 224 for details.
 Page 159

Mod
The VB6 Mod operator accepted any numeric expression and its result always returned as an integer.
The Mod operator under VB.NET no longer accepts Variants, and if either operand is a floating-point
number, the result will be a floating-point number (the remainder value).
What to do next:
1. If a Variant has been converted to an object during upgrade, manually convert it to the appropriate numeric data type.
2. If either of the operands is a floating-point number, modify the data type of the result, or modify the code to convert the
operands using the CShort, CInt, or CLong function before using the Mod operator.

MsgBox
Under VB.NET, the MsgBox function works just like it did under VB6, save for two minor variations that
few VB users are even aware of. First, VB6 and VB.NET, as of VB2008, accept either a single Carriage
Return (vbCr; ChrW(13)) or a Carriage Return-Linefeed pair (vbCrLf; ChrW(13) & ChrW(10)) as a message text
line separator, but pre-2008 VB.NET editions allowed only a CR-LF pair. Second, VB.NET throws a
Deactivated Event in the invoking form before the MsgBox is displayed, and throws an Activated Event
when the MsgBox is closed and the invoking form again regains focus.

Name
The VB6 Name method, used to rename a directory or a directory file, is instead called by VB.NET by
the more logical Rename keyword.

Next
Under VB6, a single Next could be used to terminate two or more For loops. Did a developer at
Microsoft spill a can of Jolt on their keyboard late one night and accidentally allow this functionality to
be perpetuated in VB's otherwise very cleanly formatted source code structure? Consider the following
VB6 example:
For i = 0 To 10 'start outer loop.
For j = 0 To 20 'start inner loop.
... 'process inner koop code.
Next j, i 'loop through inner then outer loop. Note the unclosed indent for the outer loop.

VB.NET does not support this structure-fumbling syntax and requires each For loop to be rightly
terminated by an explicit Next. This might have been OK under DOS BASIC, where we were unable to
indent lines for structure, but that is where that format should have been left behind. Anyone who waxes
nostalgic and bemoans the loss of such structure-hacking functionality should be tested for opiates.
ObjPtr, VarPtr, VarPtrAry, StrPtr, StrPtrAry
VB6 included five widely publicized "undocumented" functions that QuickBasic, QBasic, and Visual
Basic brought over from DOS BASIC: ObjPtr, VarPtr, VarPtrAry, StrPtr, and StrPtrAry. These methods
have no equivalent under the VB.NET intrinsic language, and cannot (and will not) be translated by
Microsoft. Even so, here is code that WILL do exactly that, all accessible from a single VarPtr function:
Imports System.Runtime.InteropServices
'VB.NET version of VB6 VarPtr, ObjPtr, StrPtr, etc. (ALL of them are now supported by this one function).
Public Function VarPtr(ByVal o As Object) As Integer 'use Object as 'catch all' data type (can also be 'As IntPtr').
Dim GC As GCHandle = GCHandle.Alloc(o, GCHandleType.Pinned) 'get a trackable handle and pin the obj address.
VarPtr = GC.AddrOfPinnedObject.ToInt32 'get address of pinned object (the variable's data addr).
GC.Free() 'free the allocated space used.
End Function

ObjPtr was really useful; it pointed to object data as a contiguous block of fields. I used it to turbo-charge
Random Access file I/O, but be sure we have <StructLayout(LayoutKind.Sequential)> set on the class or structure.
 Page 160

On...GoSub
VB.NET thankfully does not support calculated DOS-BASIC On...GoSub statements. We should either
move the VB6 code in the invoked GoSub blocks to distinct methods, defining and passing as arguments
the local variables that each GoSub block uses, or embed them within a Select block. Note that VB
Migration Partner converts this keyword but delivers code that cannott be easily maintained, therefore
it’s recommended that we get rid of On...GoSub statements before migrating the project.
On...Goto
VB.NET thankfully does not support the DOS-BASIC calculated On...GoTo statements. We can replace
it with a Select block, whose Case blocks contain a GoTo statement, which increases the number of
GoTos in the application and makes control flow very hard to follow. It is recommended that original
VB6 code be revised to get rid of On...GoTo statements before migrating the project, or GoTo a garage
sale and buy an old computer that still sports DOS BASIC. On Error GoTo 0.
Open#
The VB6 Open# is used by VB.NET as Open, which now has a standardized object-oriented syntax.
keywords normally used in an Open# statement, such as Input, Output, Random, Binary, Access,
Shared, Read, Write, and Lock, are implemented in a cleaner way, although VB2005 reintroduced the
VB6 nomination methods because Microsoft noticed a throng of hobbyists gathered around their campus
armed with pitchforks, torches, and hanging effigies from trees. The file number cannot be led by #.
Option Base
VB.NET does not support the DOS BASIC Option Base because all arrays must have a zero lower
index. Although this can be emulated in a class, is it really worth all that effort? This command was
borrowed from FORTRAN to help those who could not count from zero. However, refer to Point 7 on
page 171 for a small example class that can be used to emulate this functionality if you really require it.
Option Explicit
VB.NET supports the Option Explicit directive, but VB.NET additionally requires that it includes an
explicit On or Off argument to enable or disable the compiler option. For example: Option Explicit On.
Option Private
VB.NET does not support the Option Private directive. This compiler instruction is used only by Access
VBA and actually has no effect under VB6. The last I checked, we were using Visual Basic, not Access.
Print, Print#
VB.NET does not support the Print method that printed text to a form, to a user control, or to a
PictureBox’s surface. Use the much more robust DrawText methods from their Graphics objects instead.
VB.NET also partially supports the Print# method that outputs data to a file, which it provides through
its PrintLine command, but the bytes actually emitted are not always the same as in the original VB6
code, therefore a converted VB.NET application might not be able to exchange data with existing VB6
applications, although it will be just fine between VB.NET implementations of those applications.
Property Get, Property Let, Property Set
VB.NET uses a different syntax for properties. The VB6 Property Get method maps to the Get block in
VB.NET; and the Property Let and Property Set methods map to the Set block. Note also that VB6
required that each property member be defined in separate method blocks, whereas VB.NET embraces
the GET and SET within the Property block. Consider this VB.NET example:
Public Property Name() As String
Get
Return m_Name
End Get
Set(ByVal value As String)
m_Name = value
End Set
End Property
 Page 161

Another difference due to this encapsulation of property members is that we cannot declare one member
as Public and another as Friend. This way a user of the class in a DLL could read from a property, but
not write to it. For all the chest-thumping and gnashing of teeth, this can be easily resolved by writing
another private property that allows DLL-resident code to write to it. For VB.NET, the way to make a
property only to allow a Get was to declare it ReadOnly, and to allow it to only be Set as WriteOnly.
For example:
Protected m_Name As String

'Public property available to external processes.


Public ReadOnly Property Name() As String
Get
Return m_Name
End Get
End Property

'Property available only to internal operation and unavailable to external processes.


Friend WriteOnly Property Name_InHouse()
Set(ByVal value As String)
m_Name = value
End Set
End Property

Also, VB6 supports ByRef parameters for the Property Let and Property Set blocks, but VB.NET does
not. The reason for this is that an object's properties should NOT point to outside scalar variables. being
very data-unsafe, though it can (and very often must) reference other objects, which are able to pass a
pointer to their object data ByVal (just as ByRef), which is for the same reason we can pass a string to a
Pinvoke ByVal. This is simply because an immutable ByVal copy of an object reference variable
(technically, a pointer to the actual object) still contains the very same pointer address!

ReDim
The VB6 ReDim command can (sadly) both declare an array and create its elements (what part of RE-
Dim is not understood?); in VB.NET, we must use two separate statements: Dim is used to declare the
array (and optionally also creates its elements), but the ReDim keyword can only instantiate (or
reinstantiate) array elements. For example, the following VB6 statement:
ReDim Ary(10) As Integer ' declare and instantiate a VB6 dynamic array.
' It must be converted to the following VB.NET sequence:
Dim Ary() As Integer ' Define the array. Use Dim Ary(10) As Integer to skip the next step.
...
ReDim Ary(10) ' Instantiates its elements.

The Redim hack was used in VB6 as a shortcut to declare and allocate Dynamic Arrays, as otherwise
two separate statements were required to declare and then dimension the array. The use of Dim to both
declare and allocate it was to define Static Arrays. I have also noticed that many VB.NET detractors are
not quick to admit they were previously whining about these static arrays not being deallocated when
the Erase command was used on them, which was actually part of their documented design.
VB.NET does not support the As clause in ReDim; the type of array elements is defined in the Dim
statement. Like Dim, ReDim does not support lower indices other than zero.
NOTE: See Point 7 on page 171 to see how to define a dynamic class to allow non-zero lower array bounds.

Rem
VB6 supports multiline remarks, as in this example:
' first line _
second line _
third line

VB.NET does not support this syntax. We must add an apostrophe to the start of each comment line:
' first line
' second line
' third line
 Page 162

Return
The Return statement under VB6 was used to branch back to the code following a GoSub statement.
Under VB.NET, the GoSub statement is not supported. The Return statement is used to return control to
invoking code from a Function or Sub method, emulating its functionality in C#/C++.
What to do next: Review any lines of code that use Return in conjunction with a GoSub statement.

SavePicture
The SavePicture statement under VB6 was used to save a graphic from the Picture or Image property of an
object to a file. If the format of the graphic for the Picture property was either GIF or JPEG (JPG), it was
saved as a bitmap file (BMP). Graphics for the Image property were always saved as bitmaps regardless.
Under VB.NET, the default behavior of the Image.Save method saves the files in different formats. Save
takes an ImageFormat enumeration as an optional parameter that allows us to specify the format to use, and
the default output format is now “PNG” (Portable Network Graphics); a much more efficient format than the
dated, but easier to manipulate “BMP” format. BMP is still used in-process in .NET, such as in Image
objects, which is why we can DirectCast them between each other, simply because they are so easy to draw
to, but PNG takes up much less disk space.
What to do next: Modify the upgraded code to use the Save method, as in the following example:
' Default, will actually save the image as a PNG (Portable Network Graphics) file, regardless of the file extension.
Picture1.Image.Save("MyPicture.bmp")
' Using this optional parameter instead, it will save as a Bitmap, as was expected by the VB6 code.
Picture1.Image.Save("MyPicture.bmp", System.Drawing.Imaging.ImageFormat.Bmp).

Seek#
The VB6 Seek# command and the Seek function are both supported in VB.NET as Seek. For it, the
command with one argument is the function, and when used with two arguments it is the command. The
file number cannot be led by #.

Spc
The VB6 Spc command was used to insert spaces in a Print, Print#, and Debug.Print method. It is
supported by VB.NET, but only within Write and WriteLine commands, which replace VB6 Print#.

Split
When its first argument is an empty string, the VB6 Split command returns an empty string array; an
array having no elements (its LBound=0 and Ubound=-1). VB.NET's Split command returns an array
with a single element (zero indexed) set to Nothing. This change in VB.NET was to address uncounted
VB6 program crashes because programmers tried afterward to work with an undimensioned array. I
would have thrown an exception error, or else issued the SystemSounds.ManiacalLaughter audio.
By the way, to check for an undimensioned array under VB6, we had to do something like this:
'VB6 String Array Dimensioning check.
Public Function IsStrDimmed(vArray() As String) As Boolean
On Error Resume Next
IsDimmed = IsNumeric(UBound(vArray))
End Function

Under VB.NET, all we have to do is act like a good ol' boy and test the array for being not Nothin': If vArray
IsNot Nothing Then...
 Page 163

Static
VB6 supports Static both at the variable-declaration level, where a variable is declared as static and
preserves its value between calls to the method, and at the method-declaration level, in which case all
variables declared within the method are treated as Static variables. VB.NET supports Static, but only at
the variable-declaration level. Static at the method-declaration level did not have much use, where every
single declared variable within a method had their values preserved. These are best served by declaring
only the variables we actually need preserved as Static. For example, the following VB6 code:
Static Sub Test()
Dim x As Integer, y As Long

End Sub

The above must be converted to VB.NET like this:


Sub Test()
Static x As Short
Static y As Integer
...
End Sub

Stop
VB.NET supports Stop, but a Programming Best Practice is to invoke Debugger.Break instead. The
reason for this is that if a Stop remains in product code, it will crash the application once it is compiled
into an EXE (not exactly the best selling pitch for our product to potential customers). Conversely,
Debugger.Break will be ignored if the project is compiled to Release code. Stop was an OLD debugging
command that came about when dinosaurs still walked the Earth. What it did was stop the program run
and wait on that program line in the code editor interface. Many programs on older computers used it
because the code editor had been the DOS Prompt, like the TRS-80, the Apple ][, and the original PC,
going all the way back to the days even before display monitors, keyboards, or even printer interfaces,
when quasi-human-looking troglodytes, sporting mystery-stained lab coats, shaggy oily hair, unkempt
beards, and smoking pipes (and had not gone home, or even bathed in weeks) stared like zombies at a
row of lights on a console and mentally converted each binary readout to eight 3-bit octal values in their
heads, further translating that to ASCII or EBCID and instantly knowing exactly which text letter or
computer instruction code it stood for. And college kids today think they have it tough in CompSci...

Str
The VB6 Str function takes a Long data type containing any valid numeric expression as an argument.
Date variables or string literals were also treated as valid numeric expressions.
The VB.NET Str function takes an Object containing any valid numeric expression. Date variables and
string literals are no longer treated as valid numeric expressions because they are potentially unsafe.
What to do next: If the Str function uses a date variable or date literal as an argument, use the Format or
the object's ToString function (x.ToString("<format>")), which is faster but with the same result as
Format, to format it. For more information, see Format and ToString Function in the MSDN Help.

StrConv
The VB6 StrConv takes both strings and Byte arrays in its first argument, and can convert from ASCII
to Unicode and back. The VB.NET StrConv works only with strings and greatly extended its
functionality to broad support of international language interpolation. and therefore can no longer
convert to or from Unicode and Binary, because binary conversion would be better served by lower-
level system services in the System.Text.Encoding Namespace. For E-Z examples, consider these:
 Page 164

' To convert ANSI byte array of 8-bit text to 16-bit Unicode text.
Dim strText As String = System.Text.Encoding.ASCII.GetChars(Bytes)

' To convert 16-bit Unicode text to ANSI byte array of 8-bit text.
Dim Bytes() As Byte = System.Text.Encoding.ASCII.GetBytes(StrText)

' To convert 2 consecutive byte array elements to Unicode Chars.


Dim strText As String = System.Text.Encoding.Unicode.GetChars(Bytes)

' To convert a Unicode String to an equivalent byte array (2 bytes per char).
Dim Bytes() As Byte = System.Text.Encoding.Unicode.GetBytes(strText)

String, String$
VB6 and VB.NET both overload the String keyword, being both the name of the String type and the
name of a method. In VB.NET the VB6 String and String$ methods are both String.

Tab
The VB6 Tab command was used to insert spaces in a Print, Print#, and Debug.Print method. It is
supported by VB.NET, but only within Write and WriteLine commands, which replace VB6 Print#.

Time, Time$
The VB6 Time command is used in VB.NET as the TimeOfDay method. The VB6 Time$ command is
used as the TimeString method.

Timer
The VB6 and VB.NET Timer function work the same, excet that VB6 returns a Single value and
VB.NET returns a Double value.

To
The VB6 To keyword can be used inside Dim and ReDim statements and specify a lower indices that
can be non-zero. VB.NET can use the To keyword inside Dim and ReDim statements, but its lower
indices can only be zero, so in practice this keyword and the lower indices can be removed.

Type...End Type
VB6 Type...End Type blocks, or User Defined Types (UDTs), are upgraded to VB.NET Structures.
However, if the VB6 UDT contains initialized arrays, fixed-length strings, or auto-instancing (As New)
object variables, the Structure is required to be initialized. This is easy to do. Consider the following
workable VB.NET Structure that addresses all these issues, and note the embedded Initialize method:
Friend Structure CustData
Public ID As Integer ' No updating needed.
Public Name As String ' this was VB6 Public Name As String * 30.
Public Notes() As String ' this was VB6 Public Notes(10) As String.
Public Address As Location ' this was VB6 Public Address As New Location.

Public Sub Initialize() ' user-defined method to properly initialize this structure.
Name = Space(30) ' address the sizing of Name that had been declared As String * 30.
ReDim Notes(10) ' address the sizing of Notes() that had been declared Notes(10) As String.
Address = New Location ' address the sizing of Address that had been declared As New Location.
End Sub
End Structure

On top of upgrading a VB6 Type block to a Structure, when we assign this Structure to a variable, we
must afterward invoke its Initialize method (what we actually call it is up to us). Indeed, it is also
strongly suggested that we assign it using its default constructor (As New) as well. For example:
Dim CurrentRecord As New CustData 'New initializes structure variables to their default values.
CurrentRecord.Initialize() 'size and dimension structure data as required.
 Page 165

TypeName Function
The TypeName function was used by VB6 to return a string specifying the actual data type of a variable.
The data type text returned by the TypeName function may have different values under VB.NET. For
example, any Variant variables are converted to Objects, strings are of type Object in VB.NET, and some
controls return a different type. For more information, see the VB TypeName Function. One thing to
watch out for when migrating code from VB6 is the naming of its Integer and Long types, because a
VB6 Long is a VB.NET Integer, and a VB6 Integer is a VB.NET Short.
What to do next: Compare the result of the TypeName function with the result in the VB6 program to
determine if they are the same, and modify our VB.NET code if necessary.
NOTE: We may also choose to use the variable’s own GetType.Name method instead.

TypeOf Function
The TypeOf function under VB6 was used in an If...EndIf statement to determine whether an object
reference is of a specified object type. In this context, a user-defined type is considered an Object type.
Under VB.NET, user-defined types (now called Structures) are not Object types and cannot be evaluated
by the TypeOf function as they were under VB6, though class objects work just as they did before.
The following example demonstrates the use of TypeOf with a user-defined type:
'--------------------------------------------------------------------------------------
' VB6 version
'--------------------------------------------------------------------------------------
Private Type MyType
a As Integer
End Type

Private Sub Form_Load()


Dim m As MyType
If TypeOf m Is MyType Then
MsgBox("MyType")
End If
End Sub

'--------------------------------------------------------------------------------------
' After upgrade to VB.NET...
'--------------------------------------------------------------------------------------
Private Structure MyType
Dim a As Short
End Structure

Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load


Dim m As MyType
' UPGRADE_WARNING: TypeOf has a new behavior. For more: Blah-Blah-Blah
If TypeOf m Is MyType Then
MsgBox("MyType")
End If
End Sub

What to do next: Modify the code to do a type comparison with GetType, not TypeOf, instead:
' Modified code
Dim m As MyType
Dim mTest As MyType
If m.GetType Is mTest.GetType Then...
 Page 166

Unload
Under VB.NET, we cannot use the VB6 Unload command to unload a form. This is because VB.NET
forms are objects that have been instantiated and have to be disposed, not simply unloaded. As such, to
"unload" an instantiated form, invoke its Close method. A Form's Close method in turn automatically
invokes its Dispose method, releasing all of its resources to the Garbage Collector, except if the form
had been displayed using its ShowDialog method, or it is an MDI Child form that had not been
displayed, in which case we must invoke its Dispose method after Close ourselves. This is important in
dialog forms because we will likely want to gather its DialogResult property prior to disposing of its
resources, because once its objects are disposed of, we cannot access its DialogResult property.

Unlock#
The VB6 Unlock# command is used by VB.NET as Unlock. The file number cannot be led by # and the
To keyword is not recognized.

UserControl
VB6 allows us to use the UserControl keyword within a user control class to reference the current user
control, like so: UserControl.BackColor = vbRed. VB.NET does not recognize UserControl as a keyword.
We must replace the UserControl reference with Me, as in: Me.BackColor = Color.Red.

VarType
The VarType function under VB6 was used to return an enumeration representing the underlying data
type of a variable declared as a Variant.
When a VB6 project is upgraded to VB.NET, Variant variables are converted to the Object data type. In
VB6, an unassigned variant returned 0 (Empty); in VB.NET, an unassigned object returns 9 (Object).
What to do next: Determine if the VarType function is returning 9 and, if so, modify the code to correctly
interpret the new result. Refer to the following VB.NET constant table:
Constant Value Description

vbEmpty 0 Uninitialized (default).

vbNull 1 Contains no valid data.

vbInteger 2 Integer subtype.

vbLong 3 Long subtype.

vbSingle 4 Single subtype.

vbDouble 5 Double subtype.

vbCurrency 6 Currency subtype.

vbDate 7 Date subtype.

vbString 8 String subtype.

vbObject 9 Object.

vbError 10 Error subtype.

vbBoolean 11 Boolean subtype.

vbVariant 12 Variant (used only for arrays of variants).

vbDataObject 13 Data access object.

vbDecimal 14 Decimal subtype.

vbByte 17 Byte subtype.

vbArray 8192 Array.


 Page 167

Wend
VB6 supports While…Wend loops, but VB.NET supports While…End While loops. To work with
VB.NET, the Wend must be changed to End While.
For some laughs, check out old Microsoft Support Forum postings. We would be checking out the window to
see if the world was coming to an end. How could they dare cast their beloved Wend to the side of the road
like a dead raccoon? The Wend keyword was borrowed from the Pascal programming language. Pascal was
invented to teach students fundamentals of high-level programming in textbook-form. It was fans who
actually turned it into a tangible programming language, However, many students at Dartmouth College
complained that Pascal was still too difficult, so in 1964 their Computer Science Department designed an
“Idiots Guide to Programming” language, based on FORTRAN, called Beginners All-purpose Symbolic
Instruction Code, or BASIC. And again, fans turned it into a tangible programming language. Because Bill
Gates and Phil Allen loved BASIC so much, Microsoft built it into one of the most powerful programming
languages on the planet (after all, BASIC was what catapulted Microsoft to the top of the software heap). As
a side note, Dartmouth later tried to reestablish itself as the “final authority” on the language by releasing
Real BASIC. It was a monumental flop. This is what happens when academics try to “muscle in” on people
who work in the Real World, and who have real muscle, not esoteric book theory. Once people who truly
loved BASIC got their hands on it, Dartmouth BASIC was just an obstacle in the road to dodge around.

Width#
The VB6 Width# command is used by VB.NET as FileWidth. The file number cannot be led by #.

Write#
VB.NET partially supports the VB6 Write# method, which writes data to a file, but it uses the Write or
WriteLine commands and the file number cannot be led by a #, However, the bytes actually emitted are
not necessarily the same as in the original VB6 code, therefore the converted VB.NET application might
not be able to exchange data with existing VB6 applications, though this will not be an issue between
.NET implementations of those applications.
 Page 168

Featured Articles.
Articles Featuring VB6/VB.NET Differences and VB.NET Enhancements..
 Page 169

VB6 Aficionado Complaint Department.


116 Famous VB6 User Complaints against VB.NET Answered.
Here we will examine 116 internet-famous complaints, once prominently featured at Karl E. Petersen’s Classic VB website
(http://vb.mvps.org/), under the title of “Visual Fred”, which froth-mouthed and die-hard VB6 fanatics railed against VB.NET,
which they referred to as VB.NOT or Visual Fred (implying, of course, that it was no longer Visual Basic, which is utterly
ridiculous). Granted, many things do have to be worked around in order to deliver identical functionality to VB6, but this
does not mean that Visual Basic is now broken (the opposite can as easily be said); it is just that a fully object-oriented
interoperable development language like VB.NET is restricted to following stringent hierarchical rules that cannot be as bent
or twisted or hacked as severely as VB6 had famously bent and twisted and hacked them, and so it must apply different
solutions to many issues that diverge from how VB6 had shoehorned them. The original text (complaints) will be underlined
at the beginning of each numbered point, and each will feature my personal explanation of each issue, as well as my own
means for implementation/emulation to resolve them. Complaints with lines through them have been resolved by Microsoft.
1. VarPtr is not available. The VB6 VarPtr function returned the address of a variable’s data as an
integer that could be passed to functions taking pointer addresses, like the RtlCopyMemory Pinvoke
(use PtrToStructure or StructureToPtr instead!). In fact, even under VB6 the VarPtr function was
famously ‘undocumented’, though through the fact that Microsoft had stated as much, explaining
that it was retained for backward compatibility to older code, as far back as QuickBasic and even
DOS Basic, it also included complete documentation on how to use it. However, they were very
clear regarding the fact that commands such as VarPtr, VarPtrAry, StrPtr, StrPtrArray, and ObjPtr
are not and will not be supported by contacting Microsoft Technical Support (see the complete
Microsoft Knowledge Base article Q199824 at http://support.microsoft.com/kb/199824 for full VB6
implementation details). However, by sending a variable ByRef to a Pinvoke (String as ByVal), even
under VB6, we accomplish much the same thing. As such, there was little practical use for this
functionality, except for the rare instances where one needed to pass a fixed address to a Pinvoke.
Under VB.NET, the address of such a data item can still be retrieved, though not through VarPtr, but
instead through invocations into the managed space of the CLR (Common Language Runtime) using
GCHandles (Garbage Collector Handles). Because the CLR must be free to move data around in
memory so it can support the Garbage Collector, it has to know when it can and cannot move data,
such as when that data is in use or not. Allowing data to be moved by the CLR dramatically
improves performance. The reason why this protected and normally inaccessible feature is not
directly supported by VB.NET is because it accesses unhandled (unmanaged) memory space. Even
so, to in fact add this functionality to VB.NET, all we need to do is place the line “Imports
System.Runtime.InteropServices” at the top of the file before the declaration of our class or
module and then add the following tiny method to it, which supports all those “missing” methods:
'VB.NET version of VB6 VarPtr, ObjPtr, StrPtr, etc. (ALL of them are now supported by this one function)
Public Function VarPtr(ByVal o As Object) As Integer 'Object as a 'catch all' (Return can also be 'As IntPtr').
Dim GC As GCHandle = GCHandle.Alloc(o, GCHandleType.Pinned) 'get a trackable handle and pin the object address.
VarPtr = GC.AddrOfPinnedObject.ToInt32 'get an IntPtr to the pinned object (var's data addr).
GC.Free() 'free the allocated space used.
End Function 'NOTE: This method also exposes C#-style Unsafe Pointers for use by VB.NET!

2. StrPtr is not available. The VarPtr method in #1 gets a string address. StrPtr was unsupported for
VB6 by Microsoft. The VB6 VarPtr function on a String variable returned the address of an ActiveX
(OLE2) BSTR (see page 197 for BSTR info); a pointer that points to a Unicode character array. To get
the actual address of the string data itself, we had to use the StrPtr function. The StrPtr function
returned the address of the first character of the string (we must also take into account, if we try to
manipulate the string at that point, that VB6 Strings, just like VB.NET strings, are stored as 16-bit-
wide Unicode characters). The returned address could in turn be passed to Pinvokes that expect
addresses, like the RtlCopyMemory Pinvoke. The StrPtr function is seldom required under VB.NET.
There, we can supply strings directly to functions as ByVal strings and this will provide the
beginning address of the string text, just as the VB6 StrPtr function had done. Even though VB.NET
strings are composed of a string of 16-bit (wide) Unicode characters, if they are passed to or from an
ANSI Pinvoke to our VB.NET string, the CLR can automatically convert them to and from 8-bit
 Page 170

ANSI, if needed. just by changing the “Declare” to “Declare Auto” in the Pinvoke Signature, telling
the CLR to convert it to the required type if it is needed by the Pinvoke. Yet, in the few situations
where we do need the string’s address, the VB.NET VarPtr method in #1 will do the job.

3. ObjPtr is not available. Use the VarPtr function provided in #1 to obtain the address of an object's
data, though this is not recommended for new programmers, and should only be used by experts.
Me, I like pointing to objects for super-duper hyperdrive object data block transfers and cloning!

4. As Any is not supported for API Declares. Overloading under VB.NET eliminated the need for As
Any. VB6 was unable to support overloading, which is why the As Any feature was added to VB6 in
the first place. This keyword was a cheat to get around the non-support of Overloading under VB6 in
order for it to be able to in turn address overloaded Pinvokes (APIs). However, as of VB2005, there
is in fact support for passing parameters As Any by using the System.Runtime.InteropServices
namespace. For example, what follows is code that will provide “As Any” support for the
CopyMemory (actually RtlMoveMemory) Pinvoke (be sure to also place the instruction “Imports
System.Runtime.InteropServices” at the top of the file body):
Imports System.Runtime.InteropServices

Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (


<MarshalAs(UnmanagedType.AsAny)> ByVal Destination As Object,
<MarshalAs(UnmanagedType.AsAny)> ByVal Source As Object,
ByVal Length As Integer)

5. Use of ByVal/ByRef directly within API calls is not supported. This was addressed in VB2003.

6. Private class variables are not private to the class instance. What is meant is an instance of a class is
able to access private or protected fields of a sibling instance, which normally happens in Linked
Lists. Fields or methods in objects are private to that instance if declared Protected or Private, which
technically they still are, if we are knowledgeable to understand what is really happening, because
then we would see that one object is not accessing another's data members, but it is really accessing
itself by assuming the identity, and therefore becoming that other object! Also, the class must be
designed specifically to do even that! For instance, when we must parse sibling members in a linked
list to find its branch root or terminus (see Notes on the Secret Behind Accessing Private Members
of Sibling Objects on page 227 for a detailed explanation of what is really going on). Other class
objects still cannot see these members. My question is, why is someone even ranting over this? I am
also very doubtful any instance is going to get all depressed and suicidal, or turn to the Dark Side.
Consider a method in a linked list, where members must swap protected reference pointers:
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Protected NextDataCell As DataCell 'next data cell in this data cell's chain.
Protected PrevDataCell As DataCell 'previous data cell in this data cell's chain.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
'*********************************************************************************
' Method : UnhookFromChain
' Purpose : remove self from a linked list so we can be moved or (sniff!) discarded.
' NOTE : Take advantage of the unique fact that an instance can access private members of itself to access
' : them in sibling objects. Because this class has specific knowledge of itself, it is able to do
' : this, otherwise this capability would be impossible, and objects could not communicate or
' : associate with each other, oe anything else. Because VB6 also does this, why is anyone complaining?
'*********************************************************************************
Friend Sub UnhookFromChain()
If Me.PrevDataCell IsNot Nothing Then
Me.PrevDataCell.NextDataCell = Me.NextDataCell 'point previous sibling to our next sibling. Examine this carefully!
End If 'Note that THIS instance is NOT accessing siblings, but those siblings are actually exchanging linkage!
If Me.NwxtDataCell IsNot Nothing Then
Me.NwxtDataCell.PrevDataCell = Me.PrevDataCell 'point next sibling to our previous sibling. Examine this carefully!
End If
Me.NextDataCell = Nothing 'disavow knowledge of Mr. Phelps... Siblings are setting THEIR members to Nothing.
Me.NextDataCell = Nothing 'orphan ourselves from money-grubbing siblings.
End Sub
 Page 171

In the above example, we want to enable the ability for an object to remove itself from a linked list,
where one member of type DataCell is accessing another DataCell object. If the class were ignorant
of itself, it would not be able to do the above, which is use protected NextDataCell and
PrevDataCell members of siblings. Without self-class knowledge, to perform the above, we would
have to expose the NextDataCell and PrevDataCell members as Friend or Public, which defeats the
object’s ability to protect its data from intrusion by non-sibling entities, but, what do those methods
do? Think hard. Granted, we might think two cells of the same class being able to access Private or
Protected members defeats this, but it does not, because when they are “referenced”, that code
assumes their identities and are therefore accessing themselves! Also, a class that inherits from this
class lacks the ability to access protected members of its base class. so those members are in fact
hidden. If we try to write “outside” code to access these protected fields, we will fail. I used to see
VB6 classes demonstrated on web forums showing off how to do this in VB6 linked lists all the time.
But because VB.NET can also do the exact same thing, this suddenly becomes taboo? Id'juts!

7. Arrays may not have a lower bound other than zero. The array lower bound is forced to zero, to be
fully compliant with the OOPL paradigm. However, if such a thing is really needed, it can be easily
emulated through a very small helper class (this is similar to how VB6 did it). For example:
' simple class (no error-trapping) to define a 1-dimensional array with a non-zero lower bounds.
' We are creating a strongly-typed Generics class so we will not have to redefine "itemType" in-code. Generics Rule!
Public Class VB6Array(Of itemType) 'itemType is a placeholder for the actual instantiated array type.
Private _lbound As Integer 'save lower bounds.
Private _vbArray() As itemType 'array definition.

'define lower and upper bounds of array. LBound = Lower bounds, UBound = upper bounds.
Public Sub New(ByVal LBound As Integer, ByVal UBound As Integer).
Me.ReDim(LBound, UBound)
End Sub

'redimension the array (NOTE: the square brackets [] below allows an intrinsic name to be used).
Public Sub [ReDim](ByVal LBound As Integer, ByVal UBound As Integer)
_lbound = LBound
ReDim Preserve _vbArray(UBound - LBound + 1) 'redimension, but retain previous data.
End Sub

'Default Property. Obtain and assign indexed item. Set as Default so you do not need to specify .Item().
Default Public Property Item(ByVal Index As Integer) As itemType
Get
Return _vbArray(Index - _lbound)
End Get

Set(ByVal Value As itemType)


_vbArray(Index - _lbound) = Value
End Set
End Property
End Class

This type of class allows us to define not only the lower and upper bounds of an array, but the type
of array, whether it be a Structure, a Class, a String, a value type like an Integer, or any other type.
If we want to emplement it so we can upgrade the following VB6 definition:
Dim a(1 To 10) As String 'define string array a() to have bounds 1 through 10.

Use this VB.NET definition with the VB6Array class:


Dim a As New VB6Array(Of String)(1, 10) 'define string array a() to have bounds 1 through 10.

8. Dynamic arrays are not allowed within Structures (UDTs). Because VB.NET structures are abstract
classes, they are not auto-initialized like VB6 UDTs were, but are simply assigned, and so dynamic
arrays cannot be declared with a preset dimensional size. However, we can easily work around this
issue by initializing such members after the structure has been assigned to a variable.
Consider the following example.
Structure myStruct
Dim boxHeight As Integer
Dim boxWidth As Integer
VBFixedArray(32)> Dim boxStart() As Point 'set aside space fora fixed-size 32+1 byte array member.
 Page 172

Dim boxID() As Integer 'set aside a dynamic array for Box ID's.

'method that can be used to resize the object members of the structure once it has been instantiated.
Public Sub Initialize()
ReDim boxStaert(32) 'note that this parameter value MUST match the VBFixedArray parameter, above.
ReDim boxID(32) 'size dynamic array to 32+1 elements.
End Sub
End Structure

We can now instantiate and initialize an instance of the above structure in the following way:
Dim myBox As New myStruct 'the optional NEW initializes all member fields of a Structure to their default values.
myBox.Initialize() 'initialize any dynamic members of the structure using this developer-built method.

9. Arrays are not declared using the upper bound. Addressed in Beta2. Previously, the value used to
dimension an array specified the number of items to allocate, like C and C++. Hence, “ Dim Test(40)
As Integer” had an array with elements indexed 0 to 39. Conversely, VB6 would create an array
with elements indexed 0 to 40, allocating 41 elements, because it could not count. VB.NET now
does this. When BASIC was developed, it diverged from FORTRAN, which it was loosely based on,
to simplify dimensioning by making the upper bounds of arrays less confusing to users, such as
hobbyists, who might not understand zero-based definitions, where the zeroth element is considered
part of the count. Professional developers actually prefer zero-based indices, as is evident by
examining other professional high-level languages, where zero-based arrays are fully understood.

10. Option Base is not supported. Everything in the .NET CLR (Common Language Runtime) is zero-
based. However, if we really do need to use an array with a non-zero base, see the response for # 7.

11. Variants are not supported. Programs that use Variants are slow because they require a lot of
interpretation at runtime. VB.NET uses the Object data type as its universal data type because all
class objects and value types inherit from it. Because all variables are in fact objects (technically,
value types or abstract classes), defining a general type as Object is just like using Variant, but with
the added benefit of them also being processed much faster. Granted, VB.NET could have continued
to use Variant for its universal data type, but Microsoft chose to adopt the naming convention of the
CLR to avoid confusion and to avoid the much higher code overhead required for cross-language
development. VB.NET’s type system is also simplified by having only one universal data type.
Indeed, keeping type Variant would have made VB.NET incompatible with other .NET languages.
12. Currency is not supported. VB6 supported a Currency data type, but could not declare a variable to
be of type Decimal, although variants could have a sub-type of Decimal. VB.NET supports Decimal
but not Currency. Even so, a serious issue with the Currency data type was it not being sufficiently
accurate to avoid frequent rounding errors, which was the biggest complaint for this type, especially
among those developing scientific and financial apps. VB.NET defines Decimal as its own data type
and used it to completely replace the error-prone Currency data type. Use decimalVar.ToString(“c”)
to display the Currency data format using Decimal's much more accurate precision.
Currency variables were stored as 64-bit numbers in integer format, scaled by 10,000 to give a fixed-
point number with 15 digits to the left of the decimal point and 4 digits to the right. This
representation provided a range of -922,337,203,685,477.5808 to 922,337,203,685,477.5807.
Decimal variables are stored as 96-bit signed integers, scaled by a variable power of 10. The power-
of-10 scaling factor specifies the number of digits to the right of the decimal point, ranging from 0 to
28. With a scale of 0, the largest possible value is +/-79,228,162,514,264,337,593,543,950,335. With
28 decimal places, the largest fractional value is +/-7.9228162514264337593543950335, and the smallest
non-zero value is +/-0.0000000000000000000000000001.
NOTE: To display the Currency format of a Decimal value, we can specify the Decimal’s ToString(“c”) method,
according to current culture settings. For example, “CDec(123).ToString("c")” yields “$123.00” in Region US-En).
 Page 173

NOTE: The first thing that spelled DOOM for the Currency format was its limited range, being less than 1 Quadrillion,
considering that the US alone has over $5.4 Quadrillion in gold and silver reserves (please get the US Dollar off this
faux fiat currency the FED illegally spews out like toilet paper, and back onto the Green Back and gold – the true subtext
beneath the Wizard of Oz and its Silver Slippers, the Yellow Brick Road, and the Emerald City, which came out prior to
Jekyll Island and the clandestine founding of the FED). The second thing was the concrete galoshes of its very poor
accuracy, being just 4 decimal places, which is nowhere near good enough for bean counters, and was subject to
constant over- and under-flow errors. Indeed, if the VB6 critics knew how inaccurate it was, they would not say a word.

13. Dates are not stored internally as Double values. Under VB6, a Date variable was stored internally in a
Double format and could be manipulated as a Double. Date variables were stored as IEEE 64-bit
floating-point numbers that represent dates ranging from 1 January 100 to 31 December 9999; times
were from 0:00:00 to 23:59:59. Any recognizable literal date values can be assigned to Date variables.
When other numeric types are converted to Date, values to the left of the decimal represented date
information, while values to the right of the decimal represent time as a fraction of 1, representing part of
a 24-hour day in seconds (24*60*60, or 86,400 seconds). Therefore, midnight was 0 and midday was
0.5. Negative whole numbers represent dates before 30 December 1899.
Under VB.NET, Date variables hold IEEE 128-bit integer values that represent dates ranging from
January 1 of the year 0001 through December 31 of the year 9999, and times from 12:00:00 AM
(midnight) through 11:59:59.9999999 PM. Each increment represents 100 nanoseconds of elapsed
time since the beginning of January 1 of the year 1 by the Gregorian calendar (though this calendar
was not adopted until 1582 in Europe, 1752 in America, 1923 in Greece, and finally 1999 in Russia).
The maximum value represents 100 nanoseconds before the beginning of January 1 of the year
10000. Due to this finer tuning, they can no longer be manipulated directly as a 64-bit Double.
NOTE: A nanosecond is one billionth of a second. To non-US standards, a US Billion is a thousand million, which is
also called a Milliard, and also a MegaMillion. Regardless, a nanosecond is a 0.0000000001 fraction of a second, and
100 nanoseconds is a 0.00000001 fraction of a second. And to think Puritans took the minute hand off their clocks.
However, the only reason that VB6 used a Double to store a date/time value was because it actually
used the OLE Automation date/time storage format, which conveniently used the Double format.
Further, we can still use it if we need to by converting between the VB6-compliant Double and the
VB.NET-compliant Date (Date and Time) using a variable’s ToOADate and FromOADate methods
provided by the .NET Framework (the OA in the functions represents OLE Automation). By
representing dates as integers, this simplified and significantly sped up the manipulation of dates,
because 64-bit integer manipulation is many times faster than manipulating Doubles. For example:
Dim DtNET, DtNET2 As Date
Dim DtVB6 As Double

DtNET = Now 'get present date and time.


DtVB6 = DtNET.ToOADate() 'Convert VB.NET date/time format to VB6 format.
Debug.Print("VB6 Format Date: {0}", DtVB6) 'display result of it.
DtNET2 = Date.FromOADate(DtVB6) 'convert VB6 format date/time to VB.NET format.
Debug.Print("VB.NET Format Date: {0}", DtNET2) 'display result of it.

14. Longs are not 32-bits; they are 64-bits. True. But this is really a good thing, especially for
professional developers who require cross-language compatibility and need to develop code for
modern PC platforms, and add to that the fact that some of the .NET Framework technologies are
designed around modern 32-bit and 64-bit technologies. Using OOPL standards, such as they are
implemented in C++, C# and VB.NET, the size of an Integer should be the word-size of the
platform. On a 32-bit platform, that means that Integers should be 32-bit wide, not 16-bits (the 16-
bit integer size in VB6 was a carry-over from earlier versions of VB that ran on older systems that
had a 16-bit architecture). It therefore makes a lot of sense to update the data sizes to the new
technology. By strict software engineering standards, a Long is supposed to be a longer version of
the standard word size (imagine the fracas from VB6 programmers when the new 64-bit word-sized
systems become de facto). Regardless, Integers should always be the word size of their platform.
This results in much faster program execution. Use Integer or Int32 if we want 32-bit integers.
Unsigned versions of these types are available by pre-pending “U” to the type.
 Page 174

15. Integers are not 16-bits; they are 32-bits. See note for # 14. Use Short or Int16 if we want 16-bit
integers. Unsigned versions of these types are available by pre-pending “U” to the type. But think
about it, do we really want an integer that has the same value range as an unsigned Char?
16. True, coerced to an Integer, is not -1, but is 1 instead. Addressed in Beta2. DOH! Mongo strikes
again! I think that it should have remained 1 for C/C++ compatibility. I guess too many people
checked for <0 (less than zero) rather than correctly for <>0 (not zero), or better, used CBool.
17. The Imp and Eqv operators are not supported. True. Though very little used, they are almost too E-Z
to implement. Eqv is replaced by the “=” operator in the form “result = (CBool(op1) = CBool(op2))”.
Imp can be implemented using “result = Not A Or B”, or, if A and B are more complex, use “result =
Not (A) Or (B)”. From this, we can easily define tiny custom functions to support them.

18. Fixed-length Strings are not supported. Fixed-length strings were not supported in the first version
of the CLR. Support was later added through the VB6 Compatibility Library once the VB Upgrade
facility was implemented. So, yes they are. We can also prepend “<VBFixString(bytecount)>” to the
string declaration, or, by adding a reference to. and importing Microsoft.VisualBasic.Compatability,
we can then declare a variable or field “As New VB6.FixedLengthString(charcount)”. Personally, I
would rather simply dynamically declare a string of a specific length, such as “ Dim myStr As New
String(ChrW(0), 64)”. However, we might instead opt for a StringBuilder (declared in the
System.Text namespace), which features string manipulation that is 200 times faster than with
standard strings, such as Dim myStr As New StringBuilder(64). StringBuilders are definitely worth
exploring in the .NET documentation. However, if we really do want to declare an actual VB.NET
fixed string, use <VbFixedArray(vUb)> Dim fxdStr As Char(), where vUb is the array UBound value!
19. DefInt, DefLong, et al., are not supported. Readability and robustness of code is greatly improved by
avoiding the use of implicit type declarations. These old-school-with-whiskers declarations were
incorporated for setting default types for variables starting with certain letters back in the days when
BASIC was very basic and only supported variable names of 1- or 2-characters. This feature was a
carryover from concepts for FORTRAN 57, the world’s first high-level programming language, on
which BASIC was loosely based. In an OOPL world, that ancient approach makes very little sense.
20. Dim may not always create procedure-level variables. By this I assume they mean a Field that is a
sibling of procedures (methods), and the biggest complaint I hear on this is that Dim will not give the
item Private scope. Dim used at the procedure level will always declare variables/methods as
Friend. Use Private for fields and methods at this level and they will be private. I suggest we only
use Dim for declaring local variables within methods, where they cannot accept the Private keyword.
Another difference with Dim that many developers may not at first be aware of is that, unlike VB6,
VB.NET cannot declare a simple un-initialized dynamic array, such as “ Dim tmpArray() As Integer”, and
then redimension it multidimensionally, such as ReDim tmpArry(10, 10)”, or visa versa. For example:
Dim tmpArray() As Integer 'VB6 allows a simple declaration form, even for multi-dimensional arrays.
ReDim tmpArray(10, 10) 'VB6 works fine here, but VB.NET will barf through its nose due to rank variations.

Under VB.NET, it also requires that the rank – the number of dimensions of the array (indicated by
commas) – be specified if the array is declared but not initialized. For example:
Dim tmpArray(,) As Integer 'the VB.NET form requires the dimension rank, indicated by commas, to be specified.
ReDim tmpArray(10, 10)

21. ReDim will not create arrays not already declared. What part of “re” is not understood? This hack
functionality was originally meant to work with Variants, and then later as a shortcut for declaring
dimensioned Dynamic Arrays, not Static Arrays. Standard variable type arrays in VB6 should have
been explicitly declared before being RE-dimensioned. Enabling ReDim to also declare standard
variables in VB6 was a hack, and using it this way is also very bad programming practice.
 Page 175

VB6 had a distinction between fixed- and variable-size arrays. Fixed-size arrays were declared with
the Dim statement, which includes the bounds of the array within this declaration. Dynamic arrays
were declared in Dim statements by not specifying bounds information; the dynamic array then
needed to be redimensioned using the ReDim statement before it could be used. Unfortunately, the
VB6 ReDim statement also provided a shorthand way to both declare and allocate space for a
dynamic array within a single statement. The VB6 ReDim statement happens to also be the only
statement that can be used both to declare and to initialize a variable.
Under VB.NET all arrays are dynamic, and a Dim statement can be used in VB.NET both to declare
and to initialize a dynamic array. The ReDim statement is, therefore, used only to allocate or
reallocate (redimension) the space for an existing dynamic array, as it should.
Because all variable declarations can both declare and specify an initial value for variables, the use
of ReDim to declare and initialize variables becomes redundant and unnecessary. To require only the
Dim statement to be used to declare variables keeps the language simpler and more consistent.
22. Local variables are not necessarily visible (in scope) throughout a procedure. But they are in block-
local scope within the block level they were declared within, from their declaration forward to the
block’s end, as software engineering standards declare that a well-designed programming language
should behave, and by sub-blocks, if those sub-blocks did not over-ride them with like-named local
variables declared within those deeper, inner blocks. This is all in accordance to strict OOP rules.
Sadly, under VB6 they were recognized from their declaration forward within the body of a method
or class, regardless of their block-local scope level, such as a variable in an inner block that can be
used by an outer block, which is actually poor design and Microsoft should have known better than
to allow this in a structured design. Only undisciplined VB6 programmers took advantage of this
anyway, because this practice was not in compliance to general scoping rules, which confines scope
only to the zone of the block range they are declared within. This VB6 variable leakage problem was
actually a pain in the butt for OOP developers, who depended upon strict scoping rules to govern the
visibility of local variables. It also caused a significant number of bugs for those who made the silly
assumption of believing that VB6 might actually respect block-local scoping rules. For example:
Sub Test(ByVal x As Integer)
If x < 0 Then 'y declared below will be recognized at this level.
Dim y As Integer = -x 'declare y at an inner block level. Sloppy! (But... but... they LET me!"
'... 'If Johnny jumped off a bridge...
Else
'... 'y would also be recognized here. S-L-O-P-P-Y! Amateur.
End If
MsgBox("x is " & Cstr(x)) '...and y would be recognized here as well.
End Sub

Under VB.NET, the variable "y" in the example above is available only within the block in which it
is declared. Specifically, it is available only from its declaration, down to the Else statement. If the
variable needs to be available to the entire procedure, then it must be declared outside of the
If/Else/End If control structure. VB6 allowed “y” to be used anywhere after its declaration.
NOTE: I suppose we should actually cut Microsoft a lot of slack for implementing this poor scoping design, because not
only their BASIC-80 (later known as MS-BASIC), which was the most popular version of DOS BASIC, had not only
supported variables to be recognized anywhere after their declaration simply because scoping rules did not yet exist
within the language, but the Dartmouth BASIC upon which BASIC-80 was based had supported it, where the language
was unstructured and so bounds were much more clouded than with structured languages like VB. This is how this
functionality had crept into Microsoft QuickBasic and its free version that shipped with MS-DOS, Microsoft QBasic,
where structured programming was an optional design (a user could choose between calling Subroutines or use a
GOSUB), and was the basis upon which VB1 was later developed, which had been blended with the graphical user
interface of Microsoft QuickC for Windows.
Strict scoping rules are common to structured languages. Just as procedure locals support structured
programming by allowing variable definitions that are private to a procedure, block-level variables
support structured decomposition by allowing variable definitions that are private to its block.
 Page 176

Consider the following poorly-designed, though working VB6 code:


If x=1 Then
Dim y As Integer 'declare variable within one block...
End If
If x=1 Then
y=2 'and use it in another. As a C++ developer, I HATED this tinker-toy part of VB6.
End If 'You would think someone's mother would have taught them better...

The above would make no sense under VB.NET because local variable “ y” would not be accessible
outside the scope of its declaration block. Some VB6 programmers used this technique to declare
variables only if they are needed. Perhaps unbeknownst to them, this also generated more code
overhead than it was worth, eating more time. Indeed, were the above to be upgraded to VB.NET,
the resulting code would have automatically moved the declaration to wider scope, resulting in:
Dim y As Integer 'define y at a scoping level that scoping rules will not nag us about.
If x=1 Then 'I am still wondering if the "programmer" declared the x variable first...
End If ' or if they even know what a variable is...
If x=1 Then ' or if some bald professor in a wheelchair mutates them into existence for them...
y=2
End If

23. VarType is not supported. Addressed in VB2005. Since all variables in .NET are objects, such a
command made little sense because the only result would be Object. The VarType command was
originally designed to determine variable types within Variants, which do not exist in .NET.
However, VB2005 reintroduced the VarType method, which works like the VB6 method, but the
VB.NET version properly identifies variable types. Also, when used with Objects, the new Universal
Data Type, it will return the actual data type stored there. We can compare these against the
VarientType enumeration values, or use the result’s ToString method to render a text rendition of the
type. Hence, if an integer is stored in an object variable named genVar, then VarType(genVar).ToString,
or genVar.GetType.Name, will yield “Integer”.

24. Empty is not supported. Under VB6, Variants are initialized to Empty, which automatically
converted to zero when used in a numeric expression or to an empty string when used in a string
expression. Under VB.NET, Variants are not used but are replaced by the Object type. Object
variables are initialized to Nothing, which automatically converts to zero when used in a numeric
expression or to an empty string when used in a string expression. Using Nothing instead of a special
Empty value reduces complexity in the language and allows for better language interoperability.

25. Null is not supported. Under VB6, Null values are Variant subtypes indicating that a variable
contains no valid data. Null values would “propagate” through expressions and functions. If any part
of an expression evaluates to Null, the entire expression evaluates to Null. Passing Null as an
argument to functions caused those functions to only return Null.
NOTE: The Null value and the concept of Null Propagation was a concept borrowed from Database design.
Under VB.NET, Null propagation is not supported because Variants are not supported. Test for “Is
Nothing” or “IsNot Nothing” instead of Null. However, the model for programming data with
ADO.NET, which uses VBA, based upon VB6, is to test fields explicitly for Null before retrieving
their values. Variants containing Null are marshaled into the CLR as objects of type DBNull
(Database Null). VB.NET makes the rule for Null more intuitive—string functions, such as Left,
which under VB6 might return a Null, always return a string as we would expect, even if empty.

26. IsEmpty is not supported. No variants are supported under VB.NET, so there is no need for it. Test
for “Is Nothing” or “IsNot Nothing” instead of IsEmpty. See the notes for #24.
 Page 177

27. IsMissing is not supported. Under VB6, optional Variant parameters with no default values assigned
to them were initialized to a special error code that could be detected by the IsMissing test. VB.NET
requires that default values be specified for all optional parameters, so IsMissing has no practical
use. This simplifies the language by reducing the number of special values in the language, and
allows the internally constructed Delegate classes that wrap these methods to operate much faster..

28. IsNull is not supported. Because Variants gave way to Objects, the test should be for “ Is Nothing”
rather than “IsNull”. See the note for # 25.

29. IsObject is not supported. This VB6 command is totally useless under VB.NET because all items in
VB.NET are Objects, so if this term still existed, it would always return TRUE. Use a test for
IsReference instead, to check for a distinction between variables (value types; abstract classes) and
class-instantiated objects (reference types; concrete classes).

30. Let verb is not supported. Since everything in VB.NET is an object, this operator is totally useless
because every object is now required to be Set. As such, even the now-redundant Set keyword is no
longer required because all assignments are always assumed to be Set.

31. Core language constants do not have a "vb" prefix (vbRed became Red). Addressed in VB2005 and after by
using Microsoft’s free Visual Basic Power Packs, http://msdn.microsoft.com/en-us/vbasic/bb735936.aspx, included as
of VB2010. Add a “Microsoft.VisualBasic.PowerPacks.Vs” reference and add “ Imports
Microsoft.VisualBasic.PowerPacks.Printing.Compatibility.VB6 ” at the top of our code file, and suddenly
VB6 “vb” color codes are restored. However, though the above power packs are worth their weight in gold,
featuring design-time line and shape controls, plus “lost” VB6-style printer collections and features, such as
Form Print, I think, in the end, to get these much-coveted color constants back if we do not want to load the
above additional code overhead, we can instead just use the following constants, declared in a module
somewhere:
Public Const vbBlack As Integer = 0
Public Const vbBlue As Integer = &HFF0000
Public Const vbCyan As Integer = &HFFFF00
Public Const vbGreen As Integer = &HFF00
Public Const vbMagenta As Integer = &HFF00FF
Public Const vbRed As Integer = &HFF
Public Const vbWhite As Integer = &HFFFFFF
Public Const vbYellow As Integer = &HFFFF

32. Terminate will not fire when an object's last reference is released. Technically, it will, though maybe
not immediately, but during Garbage Collection. Garbage collection used to occur when the pool of
available resources thin, when the application terminates, or if the application was in an idle state,
though as of VB2010, it runs all the time in the background. Even so, it can be forced using its GC
invocation, GC.Collect. Nevertheless, when the last reference to the object is set to Nothing, we can
no longer touch the actual object the reference pointed to. Also, because we cannot force
reconstitution (resurrection) by re-referencing the reference object, as we were sadly famously able
to do by an odd quirk in VB6 (I have always considered this to be a VB6 bug), such as invoking a
dead object’s method, like MyObj.Count, this concern is actually no concern. Because garbage
collection, as of VB2010, is always running as a background task, this eliminated these issues with
earlier versions of VB.NET. Technically, with multiple references to an object, we can reference an
object by a still-referencing pointer after another disposed of its data, but it will not resurrect, as
VB6 was famous for doing, even if we were but testing for it being disposed of!
Under VB.NET, a tracing garbage collector walks the objects starting with the reachable references
stored in stack variables, module variables, and shared variables. This tracing process runs as a
separate thread in the background, and, as a result, an indeterminate period of time can lapse
between when the last reference to an object goes away and when a new reference is added.
 Page 178

For all the fidgeting and fits that some VB6 developers have had over this, this approach is
extremely efficient. However, in some cases, clients actually do need the ability to force an object to
release its resources on demand. The CLR uses the convention that such an object should implement
the IDisposable interface, which provides a Dispose method. When a client has finished consuming
an object that exposes a Dispose method, the client can explicitly invoke the Dispose method so that
its resources will be released immediately. For example, an object that wraps a database connection
should most certainly expose a Dispose method (the developer must provide its actual body code).
The tracing garbage collector can release objects in reference cycles correctly. Also, the performance
of the tracing garbage collector is much faster than the performance of reference counting. If we
have code that holds a resource handle open (for example, Microsoft SQL Server™ connections or
file handles), we should explicitly close the handle. The problem of not explicitly closing the handle
can be easily detected because it will force a run-time error.
NOTE: If we implement IDisposable, the Dispose method must be written in such a way that it can be invoked multiple
times, but it should only perform its actual task the first time it is invoked. This is because several connections to an
object can exist, and breaking with each through the Dispose method should normally be expected, and we may not be
able to determine which will invoke Dispose first. Also, we can guard against instances where a consumer does not, or
forgot to invoke the Dispose method by also including a Finalize method, which the Garbage Collector will invoke when
it is releasing resources, that should also invoke Dispose, but which will do nothing if Dispose has already run.

33. Object finalization code will not execute in a predictable order. And the problem is? See note for #
32. When we are finished with an object, why worry about using it again? That is just common sense
AND it is in step with the OOP paradigm. Set any object reference pointer, which is what any object
variable is, to Nothing. This throws the object the reference variable was pointing to out of scope
(visibility), eliminating any confusion. VB6 developers should have been doing this all along.
34. Implicit object creation is not delayed until first reference. A class method can be invoked through
its class code if that method does not reference fields defined within that class (I have, and will,
demonstrate this in this book). If it does involve uninstantiated fields, an exception error should be
thrown. Sadly, this critical VB6 bug was never fixed and an error was never thrown. What this bug
did was to allow VB6 programmers to invoke data-handling methods through declared, but not yet
instantiated reference variables, which immediately caused an instance of the class to spawn and be
assigned to the reference variable, which VB6 users lovingly called Implicit Object Creation.
As a result of this odd coding quirk, a rumbling throng of VB6 programmers converged on this
“shortcut” like a dust-shrouded longhorn stampede through Dodge City, and started designing their
programs to exploit this rogue behavior of implicit object instancing upon reference, and, like Jimmy
Neutron, taking this bug-as-feature for granted. The news of this “shortcut” flooded blogs and
bulletin boards, and instant “gurus” popped up like South African Meerkats, and were quick to pen
editorials and ballyhoo in halls and drawl around campfires, all vying to be the new “leading expert”
on how the rest of us lowly mortals could also modify our code and exploit the new Cool.
Unbeknownst to these yahoos, this bug was actually the root cause of their greatest bane, causing
endless instances of VB6 objects to “return from the other side” after having been set to Nothing,
causing uncounted programmers to howl in exasperated frustration, throwing their Hot Pockets
against walls, making dogs bark into the wee hours, and cats to clatter trash can lids. Countless
others began to also exploit even this ancillary behavior by coding to take explicit advantage of it.
Because of this so-called “feature” they adored so much, even a test to see if the variable was set to
Nothing caused its pointed data-less object to suddenly be resurrected, like Cher after a face-lift, and
respawn its object data. They hated, and I mean HATED this part of it, but if anyone at Microsoft,
not even their janitor. even dared to try fixing it, these programmers would lose their candle-
enshrined shortcut and start looking around for where they last put their torches and pitchforks.
 Page 179

Object spawning should only occur upon instantiation “As New”, just as VB.NET does it, which
these VB6 programmers now view, paradoxically, as a bug in VB.NET, because it will not allow
them to exploit their beloved VB6 shortcut. This VB6 derivative “feature” was also the source of a
lot of confusion regarding memory leaks. This unconventional VB6 behavior should never have
been allowed, and should have instead been considered and addressed as a critical software bug
(actually, I think Microsoft knew how to fix it... but they did not dare...).
Because of its imperfect implementation under VB6, we can consider the following code:
Dim x As MyClass 'Declare empty reference with no instantiation.
'...
Call x.MyMethod() 'Bad form: x has not yet ben instantiated as an actual object; only as an empty reference.

The above was unfortunately the same as if the following VB6 code had been written:
Dim x As MyClass 'Declare empty reference with no instantiation.
'...
If x Is Nothing Then 'If x is an empty reference (no instance yet instantiated)...
Set x = New MyClass 'Declare an instance of the class for x to point to.
End If
Call x.MyMethod() 'invoke a method of the object x is pointing to.

It should be rare that this problem will ever be an issue under VB.NET. Nevertheless, the code can
be easily modified to instantiate a new version of the class, as in the following example:
Dim x As New MyClass 'Declare a new reference.
... 'Be diligent if object has Dispose() and consumes lots, or unmanaged, resources...
If x IsNot Nothing Then x = Nothing 'If x is not an empty reference, then make it so.
x = New MyClass 'Declare an instance of the class for x to point to.

Or, more simply and more robustly:


Dim x As New MyClass 'Declare new reference.
...
x.Dispose() 'Be diligent if the object has Dispose() and consumes lots of, or unmanaged resources...
x = New MyClass 'Declare an instance of the class for x to point to.

Well, technically, when we assign a New object to a reference variable, the old object, if it exists,
will be automatically tossed to the Garbage Collector. Thus, using only “x = New MyClass” will work
just fine. However, if the object references unmanaged resources, be sure to first invoke its Dispose
method. If it does not implement Dispose, but handles releasing unmanaged data in a Finalize
method, consider placing that code in a Dispose method, but also invoke it from Finalize, in case
Dispose was not invoked by the object consumer. This will work because a Dispose method will run
only once, so invoking it directly and from a Finalize method will only run its code once.

35. Public object variables are not safe from alteration when passed as parameters. ???!!! (Imagine me
throwing my head back as I roll my eyes). Of course not! This would also be true for passing Private
objects. Do not pass any object, ByRef or ByVal. to a method that will alter a passed parameter if we
do not want it to be altered (explained below. I will refrain from saying DUH!!!). This is poor
design. But if we need to pass that data, but do not want it altered, pass it a Clone, and cast it to the
parent object's type, because a clone defaults to type Object. For example. if myObj is of class type
DeadRat, we can safely pass a clone of it as a parameter using DirectCast(myObj.Clone, DeadRat), and
at the same time the compiled code will render myObj.Clone (DirectCast is a compiler instruction)!
With scalar variables, if there is any doubt, then explicitly pass them ByVal, which will prevent it
from being altered, because an auto-cloned copy of the variable will be sent to the method instead. If
the parameter is specified as ByRef, however, we can cheat by wrapping it in parentheses, such as
(myInt32), which will treat it as an expression and therefore pass a copy! Even so, variables
referencing instances of class objects are simply references, so passing a reference variable, even
ByVal, still passes a copy of the variable that still points to the same object instance. Also remember
that strings and arrays, of any type, are also objects, not value types. So, to avoid all these headaches
with passing objects we do not want altered, always employ the Clone method described above.
 Page 180

36. Cannot expose Property procedures with mixed visibility ( Friend Set/Public Get). This is due to
the manner in which VB.NET had grouped its GET/SET properties. The idea of using mixed
visibility may be handy in very special and extremely rare instances, and in such cases we can
work around this ‘loss’ by using separately-defined properties or methods that simply address
the same field. A slight name change is in order in such instances, but because this is rare, it is
really no big deal. Simply name the exposed Public property as our user will expect. The Friend
version can be named in a manner useful by the local application code.
37. Procedure parameters are not by default passed ByRef anymore. VB6 parameters not specified as
ByVal or ByRef usually defaulted to ByRef (true only for implicit objects, but actually used ByVal
for non-implicit objects, though somehow most people missed that part of the documentation!). For
VB.NET, this change to a default of ByVal was to maintain cross-language consistency and eliminate
many program bugs. Defaulting to ByVal rather than ByRef also eliminates the problem of having a
procedure mistakenly modify a scalar variable passed in by the invoker. This also makes the default
invocation convention consistent with assignment, such that parameters are effectively bound to the
expressions passed in by an assignment of the expression to the formal parameter. Also, to avoid
confusion for users upgrading from VB6 to VB.NET, the IDE will automatically add the ByVal
keyword to any parameter declarations that the programmer entered without explicitly specifying
ByVal or ByRef. We are finally getting away from its FORTRAN roots, which by default passed all
parameters ByRef, as well as all the bugs that were caused by default ByRef parameter passing. I ran
into PLENTY of those when I was in my office bug-tracking tens of thousands of lines of other
developer’s FORTRAN code in the wee hours of the night when I should have been home sleeping.
38. ParamArray arguments are not passed ByRef anymore. See note for #37. This was used by those who
wanted to manipulate the optional parameters passed to it, but such functionality is actually more
efficiently, and much faster served by passing them instead as members of a strongly-typed or even an
Object Array, which the code can easily For...Each its way through. A much more typical scenario for
ParamArray arguments is for them not to modify variables that are passed in. Not supporting ByRef
ParamArray arguments also simplifies the ParamArray calling convention by making ParamArray
arguments normal arrays, reducing tons of code overhead. This enables ParamArray arguments to be
extended to any element type and allows functions that expect ParamArray arguments to be invoked
directly with an array rather than as an much slower and code-heavy argument list. See the reference to
the Cdecl Calling Convention used by the VB6 ParamArray on page 122. to grok why it was dropped.

39. Property parameters may not be passed ByRef anymore. Yes they can. The thing that confuses me is
that under VB6, if you passed an abstract class property, like a scalar value or a structure, as a
parameter ByRef, the value provided by the parameter was just a cloned copy, so that copy would be
modified, but not the property's actual protected value, except when the property referenced an
object, which meant a reference pointer was given. However, under VB.NET, if a property is passed
as a parameter ByRef, it will modify that property, which VB6 did not when using abstract classes.
40. Implements is not implemented the same, so must be rewritten. Actually, .NET makes polymorph
implementation of class interfaces not just easier, but also properly. Implementation is supported
under VB.NET in the way OOPL Implementation is supposed to be supported under OOPL Rules,
which is to say that it is no longer limited to faking polymorphism through class shell interfaces
(actually, this class shell interface was just one more of many VB6 cheats). Most programming
systems provide object-oriented polymorphism as it should – through inheritance. VB6 could not
support inheritance in order to provide full polymorphism, but instead supported polymorphism
emulation through multiple ActiveX interfaces. These minor changes complained about by VB6
users were in fact necessary in order to properly implement inheritance, which was a feature too
often begged for or loudly demanded by the very same VB6 users who were complaining.
 Page 181

Implements under VB.NET differed from VB6 in two essential ways:


1. Class skeletons cannot be specified in Implements statements (VB6 used class skeletons as duct-tape because
VB6 actually did not support genuine interface objects, because it did not support inheritance).
2. Every method that implements an interface method requires an Implements clause at the end of the method
declaration statement. This helps document when and from where an interface method is being implemented.
VB.NET maintains a strict distinction between interfaces and classes. Readability is also improved
by requiring an Implements clause on each method that actually implements a method from an
interface. It makes it obvious when reading code that the method is being used to implement an
interface. For example, if class "a" implements interface "b", the interface is declared for "b" and
class "a" is changed to implement the interface of "b":
Interface b
Function MyFunction() As String
End Interface

Class a
Implements b
Function b_MyFunction() As String Implements b.MyFunction 'this entry is generated when you add the above line.
End Function
End Class

41. Static is not supported as a procedure level modifier. Under VB6, Procedures could be declared with
the Static verb, which indicated that every single one of the method’s (procedure's) local variables
were preserved between invocations, even looping and temporary variables.
Under VB.NET, in keeping with OOPL specifications, the Static verb is not supported on a method,
and all static procedure-level variables must be explicitly declared with the Shared verb (typically,
Protected Shared). There is little need to have all the method variables be rendered static. Removing
this feature simplifies the language and improves its readability, because local variables are always
stack allocated, as they should be, unless explicitly declared as Shared (static).
42. Use of As New does not force auto-reinstantiation when an object is released. Actually, this was one of
the biggest problems with VB6, and this “functionality” should have been declared and addressed as a
critical language-implementation bug. Besides, why would we want to re-instantiate an object that we
had explicitly gone through the time and trouble previously to intentionally release? Such a strategy is
very poor (read as stupid) design and should never have been allowed in the first place (one of many
Jimmy Neutron “bug-as-feature” issues I have had with VB6 over the years). Does not “As New”
actually imply we want a new object, not the old one? Some people actually counted on this design bug
so they would not have to repoint numerous reference variables to the object! If a banking software bug
allowed people to drain your accounts, would you advocate that bug too?

43. Parentheses are not optional when calling procedures. Thanks go to Microsoft for finally forcing
invocation standards! One of the things I disliked about VB6 was that many method invocations
looked like variables when they were missing their parentheses (invoked without using the “Call”
keyword). Or worse, function invocations, such as “iVar = Whatsit”, as opposed to “iVar =
Whatsit()”. When examining high-level language code, it is instantly apparent if something was a
variable or a method simply by the presence of the parameter bounds and the context of their use.

44. Set is not supported for object assignment. It is implied by default. Since everything is an object,
everything is always Set. As such, the use of a Set keyword would be totally redundant. Even so, if
we do type it, it will be accepted, though automatically redacted. Is this really an issue?!

45. Parameterless default properties are not supported. Under VB6, any member could be marked as the
default for a class. Under VB.NET, rejecting parameterless default properties means that there is no
more guessing at what a class will do by default, making the code more readable because a reference
to an object variable without a member always refers to the object itself, rather than referring to the
object in some contexts and to the default property value in other contexts. For example, a statement
"Call Display(TextBox1)" might be passing the text box instance to the Display function, or it might
 Page 182

be passing the text contents of the text box. Face it – the VB6 implementation was total doggy-do for
self-documentation and should never have been allowed. VB.NET allows only properties that take
parameters to be marked as default. It is common for those properties with parameters to be indexers
into a collection. Perhaps the only property that should be treated as a default is the Item property as
implemented in a Collection Class (but the Item property just so happens to have a parameter...).
Also, removing this ambiguity eliminates the need for a separate statement to perform reference
assignment. An assignment "x = y" always means to assign the contents of variable " y" to variable
"x", rather than to assign the default property of the object that " y" references to the default property
of the object that "x" references, without specifying Let or Set, thus rendering these verbs moot.

46. Default values for Optional parameters are not optional. One should always declare a default value
for optional parameters to clarify code. For each programmer complaining, many more will wail if
an optional parameter did not specify a default. Optional variant parameters were only introduced in
VB4, and VB5 added Typed Defaults. An implicit default of 0 for scalars or Nothing for strings has
the same code overhead as explicitly declaring code-clarifying defaults (!!!), but it did not make
author intent clear, especially when undefaulted Variants were used as parameters. The principle
reason for this under .NET is due to the design of Delegates (Prototype Classes), which are used as
structural guides by the compiler to guarantee methods and their parameters are passed accurately.
At the compiler level, it needs to know what to pass if the invoker did not provide a parameter that
was optional. Even though we might think scalar values should default to 0 and strings to Nothing,
this is not good programming practice, and should be avoided. Under VB6, I always specified what
optional parameters rendered by default, just so it was clear to my critical (read this as extremely
whiny) code reviewers, who I think are only critical when it comes to other developer's code.

47. Code is not compiled to native, thus making recompilation much easier. Think about that! ALL code,
including native, with embedded metadata or not, and even p-Code, can be very easily decompiled,
and that source code can be altered and recompiled. I have done this for learning purposes since I
had a TRS-80 Model 1 (I even decompiled its Level II ROM using a decompiler I wrote in DOS
BASIC). By the way, VB6 is not native code, but a hybrid of fast, semi-compiled ActiveX p-Code
tokens (called VBC) and native support DLLs, wrapped in a load executable. Its QuickBASIC chassis
is built around the tokenized OLE2 (ActiveX) specification. We can thank marketing department
hype for obfuscating the meaning of OLE2 and of native(-ish). It is true that due to all the embedded
metadata and the Intermediate Language code, VB.NET code can be reconstructed. so Visual Studio
provides a code obfuscator, which impedes decompilation, and other obfuscators are available for
sale by third parties. But this is not some shiny new issue that cropped up because Microsoft chose
to employ a powerful Machine Independent Language. Decades ago, I decompiled Machine and p-
Code to learn how they did what they did. By the way, p-Code (pseudo-Code) was a tokenized code
resting between a high-level language, like Pascal or VB, and machine-native code, and it was also
easy to decompile into compilable source code. This thorny issue has existed ever since the heyday
of FORTRAN, the first high-level language compiler, first became available in 1957. FORTRAN
(Formula Translation) was developed from 1954 through 1957. History owes a big hail of thanks to
its developers, John W. Backus and his team: Sheldon F. Best, Harlan Herrick, Peter Sheridan, Roy
Nutt, Robert Nelson, Irving Ziller, Richard Goldberg, Lois Haibt and David Sayre. VB6 compiled
code being ActiveX p-Code, whose native runtime platform invoked native support methods (even
DOS BASIC did this!), its advocates should be mindful about throwing stones at a glass house.
 Page 183

48. Resource files have changed format and old ones are not supported. VB.NET has implemented
enhanced support for resources. As such, the need to upgrade the format was essential to keep
VB.NET current with other members of the .NET platform. One change is that forms can now be
bound to retrieve resources automatically from the new .resX-formatted resource files. Also, any
CLR class can be stored within a .resX file. To help ease the transition from VB6, the
Microsoft.VisualBasic.Compatibility namespace contains VB6 emulation members, exposing many
VB6 functions that are difficult to directly translate using the .NET paradigm, such as the VB6-style
resource saving and loading functions. This resource interface upgrade is the cost of greater
efficiency and faster processing. Due to the advances in computer technology, both in speed and
processing capability, the old resource format impeded what was otherwise possible for application
resource data. With the introduction of namespaces, such as My.Resources, My.Computer.Audio,
and My.Resources.ResourceManager, we have a tremendous amount of raw power suddenly at our
fingertips, totally eliminating the need to implement Pinvokes just to access all of that data, not to
mention that there no longer exists a need to separately go to a DOS prompt in order to compile
resources, because now the IDE can auto-compile and recompile them as we update them.

49. LSet is not supported. Addressed in VB2003. Regardless, this was not actually a safe method for
copying structures or UDTs. LSet had been used to copy one VB6 User-Defined Types, though it
often ran into trouble when copying null-terminated string data or embedded objects. This can be
done more easily in VB.NET by assigning one structure to another using the equal sign ( Struct2 =
Struct1), because structures are handled as value types (abstracted classes), not reference variables
(concrete classes), and so each variable points to its own individual copy of a structure. However,
the LSet function is still available to support its original intended implementation, which is to place
text left-aligned into a fixed-length field, padded as needed on its right side by spaces.
The VB6 form implemented LSet (and RSet) using the format:
LSet stringvar = stringexpr

Where stringvar was a string preset to a predetermined size, and strexp was a string expression.
VB.NET, from VB2003 on, implemented this command slightly differently, in this updated form:
Stringvar = LSet(stringexpr, fieldsize) 'Also specify the size of the field in characters to fill.
' If stringvar is ALREADY set to a specific length, we can instead use --
Stringvar = LSet(stringexpr, Stringvar.Length)

Where stringvar is a string variable to receive the data (initialized or not), stringexpr is a string
expression, and fieldsize is the desired resulting character length of the stringvar data, padding it as
needed on its opposite side by spaces.
50. RSet is not supported. Addressed in VB2003. The RSet function places text right-aligned into a
fixed-length field, padding it as needed on its left side by spaces. See the notes for #49.
51. UDTs are not Types, but are called Structures instead. And? Structures ARE User-Defined Types.
VB.NET variables are also structures, which is a form of a class known as an abstracted class.
However, on a more practical level, the names Type and User-Defined Type are confusing, because
classes, enumerations, and interfaces are also types that can be defined by users. These archaic terms
are vestiges of Microsoft QuickBasic, in which structures and records were the only types that a user
could define. The CLR uses the name Type in a broader sense to include all data types. For this
reason the Type statement was changed to Structure in VB.NET, which also happens to be
conveniently congruent with the terms for the same constructs under FORTRAN, C++ and C#.\
 Page 184

52. UDTs are no longer by default contiguous blocks of memory, but are objects, which can have each
member of its data stored in various locations within memory. Structures store field data internally in
a very efficient manner, which may not be contiguous, and in normal circumstances under normal
use this difference is totally invisible to the user and store members on natural architectual
boundaries to achieve 200% to 500% faster processing. However, in cases where interoperability
between VB.NET and non-CLR communication is essential, they can be easily made to store their
members in contiguous memory by implementing simple Marshaling attributes, and specifically by
pre-pending “<StructLayout(LayoutKind.Sequential)>” to the beginning of the structure to ensure that it in
fact passes its members contiguously. We must also Import System.Runtime.InteropServices into
our code to expose the above requisite Marshal class.
53. Enums will not be recognized unless fully-qualified. Was this a real problem? Explicit declaration of
enumerators clarifies code. The real power of enumerators is in being able to specify an enumerator
group name, hit the dot, and a list of their enumeration members are listed, thanks to Visual Studio’s
IntelliSense capability. This eliminates the need to remember mountains of enumerator values or
wonder what a lone enumerator value specified in code relates to. Remember, enumerators are
enumerations of something, which is typically the title or name they are grouped under. It makes
perfect sense to specify the grouping name along with the enumerator, especially in the code listing.
To not do this, one might as well revert back to the more primitive method of employing individually
declared constants. Also, full qualification does not add one byte of compiled code.
54. While/Wend loops are not supported. Yes they are, but as more uniform While..End While loops
instead. Big fracking deal. Wend was a shortcut keyword borrowed from the programming language
Pascal. To be consistent with all other block structures under VB.NET, the terminating statement for
While is now End While. This improves language consistency and readability. The new VB.NET
form brings the While structure more in line with other similar declarations, such as Select...End
Select, If...End If, With...End With, Using...End Using, and so on. Got an issue? Get a tissue.
55. GoSub/Return is not supported. We are now doing structured programming. Primitive referencing
has absolutely no place in Object-Oriented Programming, but only in procedural languages that did
not recognize method boundaries, but which VB.NET most definitely does. Well, maybe I should
take part of that back; GOTO is allowed in VB.NET, but limited to the the block it is used within.

56. On/GoTo is not supported. See note for # 55. However, “ On Error Goto 0” is still supported, to kill
error detection code initiated by “On Error Resume Next”, though one should most definitely instead use
the Try...Catch...End Try structure, which is much friendlier to structured programming, they can be
grouped where the error might be encountered, and in my mind are much easier to use.

57. On/GoSub is not supported. See note for # 55.

58. Line numbers are not supported. Labels may be numeric. You still want to use line numbers? I hear
some garage sales might offer old computers featuring DOS BASIC. See the note for # 55.

59. Erl is not supported. No line numbers, remember? PUH-LEASE, if you want to do DOS BASIC
programming, then go to a garage sale and pick up some Medieval computer Leonardo da Vinci
might have used to design flying contraptions.

60. The MsgBox function is not supported. With the release of VB2005, the old VB6 MsgBox
functionality was reintroduced. However, a more verbose MessageBox function is, and always was
available for VB.NET, which features a very useful Parent/Owner parameter.
 Page 185

61. The DoEvents function is not supported. This was reintroduced in VB2003. It is available from two
places: as a part of the System.Windows.Forms namespace, under the Application class, and now as
a part of the My.Application namespace. Use “Application.DoEvents” if we have any window forms
loaded, which is usually the case. We can also fully qualify the command using either
“My.Application.DoEvents” or as “System.Windows.Forms.Application.DoEvents”.

62. The Date statement is not supported. It most certainly is supported, and is much more powerful than
its VB6 counterpart, though it does not by default return the more primitive OLE Automation-format
double-precision value as it did under VB6. However, to address this in situations where users do
need the old OLE Automation format, we can employ the ToOADate and FromOADate functions to
convert it (both date and time) to and from the VB6 format. Following is an example of usage:
Dim DtNET, DtNET2 As Date
Dim DtVB6 As Double

DtNET = Now 'get present date and time.


DtVB6 = DtNET.ToOADate() 'Convert VB.NET date/time format to VB6 format.
Debug.Print("VB6 Format Date: {0}", DtVB6) 'display result of it.
DtNET2 = Date.FromOADate(DtVB6) 'convert VB6 format date/time to VB.NET format.
Debug.Print("VB.NET Format Date: {0}", DtNET2) 'display result of it.

63. The Time statement is not supported. It most certainly is, and is much more powerful, though it no
longer returns the more primitive OLE Automation-format double-precision value as it did under
VB6. However, we can employ the ToOADate and FromOADate functions to convert it to and from
the old format. See # 62.

64. And, Or, XOr, and Not are not bitwise operators. Addressed in Beta2. IDIOTS!!!! They should have
remained Logical and introduced new operators for bit-wise operations. Actually, Microsoft did
initially introduce BitAnd, BitOr, and BitXor, which were for use in bit-wise operations, but the user
uproar was deafening. Too bad these users did not engage their brains at that time, because they
would have seen that it would have made VB much less confusing when it comes to quickly
detecting if the code is doing bit-wise operations or logical operations. Oh, well. MS had also
dropped BitAnd, BitOr, and BitXor by the time VB2002 was released. These operators would have
been greatly appreciated if they had remained. Even so, these functions are easy enough to emulate.
65. Comparison operators are not evaluated before logical operators. Addressed in Beta2.
66. Sqr is not supported. Use Math.Sqrt. Big Woopie Ding. Better: Import Math and use Sqrt.
67. Sgn is not supported. Use Math.Sign. Better: Import Math and use Sign.
68. Atn is not supported. Use Math.Atan. Better: Import Math and use Atan.

69. The String function is not supported. Yes, it is! And it is also MUCH more powerful, providing
many methods to do just about anything we might imagine doing with string objects. The thing that
might confuse users is that because strings are a reference type, and so to use the String command as
they may have been used to using it under VB6, they have to assign values from it through string
instantiation, such as “Dim s As String = New String("x"c, 32)” or as “Dim s As New String("x"c, 32)”.

70. Control arrays are not supported. Under VB6, a control array was a group of controls that share the
same name and type. They also share the same event procedures. A control array has at least one
element and can grow to as many elements as our system resources and memory permit. Elements of
the same control array have their own property settings. Under VB.NET, The Windows Form
architecture natively handles many of the scenarios for which control arrays were used. For instance,
in Windows Forms we can handle more than one event on more than one control with a single event
 Page 186

handler. However, a Control Array Windows Forms extender control found in the
Microsoft.VisualBasic.Compatibility namespace revives the old VB6 control array feature. I
personally choose not to use it, but prefer instead to build my own array of controls and add event
support through the AddHandler method. Also refer to Notes on the Loss of VB6 Control Arrays
and How to Quickly Create Them on page 80, which explains how to create them fast and with
almost no code. Also, as of VB2008, forms once again have regular Controls and Forms collections.

71. The native Forms collection is not supported. Addressed in VB2008. A VB6 Forms collection was a
collection whose elements represented each loaded form in an application. The collection included
the application's MDI form, MDI child forms, and non-MDI forms. The Forms collection had a
single property, Count, which specified the number of elements in the collection.
Its newer My.Application.OpenForms collection, which redirects to the System.Windows.Form.Forms
collection, returns a FormCollection object (System.Windows.Form.FormCollection) that contains
all of the current application's forms that are presently open. Its behavior is identical to the VB6
Forms collection, and it can be used in exactly the same way.

72. UnloadMode detection is not offered, as QueryUnload is history. It still exists, but in a different and
more logical form. We now use the FormClosing event in place of the QueryUnload event and
inspect its e.CloseReason property to check for why the form is closing (formerly UnloadMode
under VB6). Set the e.Cancel to True if we wish to force canceling form closing.

73. ListBox controls do not offer an ItemData property. In VB6, the ItemData property for a ListBox or
ComboBox control could be set at design time in the Properties window to associate an Integer with
a ListBox or ComboBox item, often used to associate an ID number with a text string. In VB.NET
the ItemData property no longer exists, but this is due to the more powerful functionality given to
ListBoxes/ComboBoxes, where list items are now able to be of any object (but with the singular
proviso that the provided Object also features a ToString method), not just text. However, the
GetItemData and SetItemData methods from the VB6 Compatibility Library can be used to emulate
the behavior of ItemData (set a project property reference to Microsoft.VisualBasic.Compatibility,
and then import this reference at the top of our file to fill the list).
If we upgraded a project from VB6 to VB.NET, we may notice that the upgrade uses a VB6.SetItemData
method to initialize the design time ItemData information, usually invoked in the constructor of the form
(Public Sub New). To access ItemData, it used the VB6.GetItemData method to emulate the
functionality of the lost VB6 ItemData property. For example, Dim I As Integer = VB6.GetItemData(List1,
List1.SelectedIndex). To get text from a List item, we use VB6.GetItemString, as for example, Dim Result As
String = VB6.GetItemString(List1, List1.SelectedIndex), or we can instead use Dim Result As String =
List1.Items(List1.SelectedIndex).ToString().

In most cases from here on out, it is assumed that we will be developing code in VB.NET from
scratch. As such, we can greatly simplify all this by instead employing a very small custom class and
gain complete control over our list and reporting methods. When we realize that we can now add
any object to a Listbox or Combobox Items collection, including custom classes, it becomes clear
that this functionality is a far more flexible and superior approach than the old VB6 method, and
there is nothing lost at all in the translation. Rather it is a .NET gain (snicker!).
For example, to show we how to easily do all this, simply create a little class like the one below,
which will allow us to add text data, such as a name, and an associated ID (use it as a boilerplate for
other similar classes that we might need):
Option Strict On
Option Explicit On

Public Class ListItem


'-------------------------------
 Page 187

' protected Field Data - You can store more than just these fields here, defining its Object fields.
'-------------------------------
Public ItemString As String 'Text data for Item.
Public ItemData As Integer 'You could also have declared this as String.

'-------------------------------
' Custom constructor; used to actually add data to a ListBox or ComboBox.
' You are not limited to adding just one or two items, or of just these types.
'-------------------------------
Public Sub New(ByVal ItemString As String, ByVal ItemData As Integer)
Me.ItemString = ItemString ' You may want to add more string parameters.
Me.ItemData = ItemData ' and then combine them with a space separator in ToString.
End Sub ' or even via a custom reporting method. Limitless possibilities!

'-------------------------------
' Constructor assigning just text.
'-------------------------------
Public Sub New(ByVal ItemString As String)
Me.New(ItemString, 0)
End Sub

'-------------------------------
' Provide a text data property to override the useless default in the Item() object.
' you can also use this to combine stored items when more than one text item is added.
' NOTE: a ToString() method is REQUIRED of any object provided to a ListBox/ComboBox Collection.
'-------------------------------
Public Overrides Function ToString() As String
Return Me.ItemString
End Function
End Class

Now all we need to do is add items to our ListBox or ComboBox using this class. For example,
suppose we had a ListBox named ListBox1 on a form and we wanted to initially populate it with
some employee names and company ID numbers that are associated with those people. In the form’s
Forms_Load event, we could place the following sample code:
With ListBox1
.Items.Add(New ListItem("Carol Philips", 101)) ' Note that the VB6 AddItem became Add, which brings it in
.Items.Add(New ListItem("Jim Kim", 102)) ' line with other objects, and now a ListBox and ComboBox
.Items.Add(New ListItem("Jossef Goldberg", 103)) ' exhibit the same functionality, whereas previously this
.Items.Add(New ListItem("Patricia Doyle", 104)) ' was often confusing between the two.
.SelectedIndex = 0 'Optionally set the first item as the selected item.
End With 'By the way, New returns the old VB6 NewIndex value.

NOTE: Hopefully we can clearly see we can add this type of item data at run-time, not being restricted to design time!
Further, if we also had a Label named Label1 on our form, we could add the following code to the
ListBox1_SelectedIndexChanged event in order to reflect selections the user made in the ListBox:
Dim mList As ListItem = DirectCast(ListBox1.Items(Index), ListItem) 'Cast result to class ListItem .
' Use the ListItem object to report the ItemData and text information...
Label1.Text = mList.ItemData & " " & mList.ToString
' Alternatively, you can now use the following DIFFERENT syntax and obtain the exact same result.
Label1.Text = DirectCast(ListBox1.Items(Index), ListItem).ItemData & " " &
ListBox1.Items(Index).ToString

NOTE: The DirectCast function was used instead of the usually invoked Ctype. Ctype is compiled inline, which means
that the conversion code is part of the code that evaluates the expression, making it somewhat a macro command as in
C/C++. In some cases there is no call to a procedure to accomplish the conversion, which makes execution faster.
However, DirectCast does not use the VB run-time helper routines for conversion, so it can provide somewhat better
performance than CType when converting to and from data type Object. Ctype Returns the result of explicitly converting
an expression to a specified data type, object, structure, class, or interface. DirectCast is more restrictive, being a type
conversion operation based on inheritance or implementation, requiring an inheritance or implementation relationship
between the data types of the two arguments. This means that one type must inherit from or implement the other.
NOTE: Be aware that the return integer value from the Items.Add method will be a value that is equivalent to the VB6
NewIndex property, providing us with the indexed location that the new item within the list was added. See the next
complaint. The VB6-style NewIndex property was eliminated in VB.NET because it was not robust; it would become
unreliable if items were removed from the list before the property was read; thus it was added at the only place where it
would be guaranteed to be 100% reliable – as the return value from the Add method.
 Page 188

74. ListBox controls do not offer a NewIndex property. The VB6 NewIndex property was used to get
the index of the item most recently added to a ListBox or ComboBox control. During an upgrade to
VB.NET, the value of NewIndex cannot be exactly determined in some cases. For example, after list
items were deleted, the NewIndex property would not change. As such, because it was impossible to
provide a 100% reliable NewIndex property, OOPL-compliance rules forced the property to be
dropped. However, the return value from the VB.NET control’s Add method will provide the fresh
index number of the item just added. This will be the exact same value as the old VB6 NewIndex
value. Therefore, the return value from the Add method can be used instead of NewIndex.
For example, we could declare a private field with a name based upon NewIndex in our form and
reserve it for use with a particular ListBox or ComboBox. Then, each time we added an item to the
ListBox or ComboBox, we would save the return value to the associated variable (which is typically
tossed away by not being collected). The code can then check the appropriate NewIndex field for the
newest entry in a list. Consider the following example:
Dim NewIndex1 As Integer = -1 'Init to -1 to show nothing added (also set -1 when you clear the listbox).
... 'other application processes.
NewIndex1 = List1.Items.Add("Howdy!") 'add an item and collect the new integer index value.
List1.SelectedIndex = NewIndex1 'optional command typically used in VB6 to set the selector to the new data.

If we will not be using the NewIndex value except to set the selection to the new item (especially
useful when the list is sorted), we can then do everything in just one line of code:
List1.SelectedIndex = List1.Items.Add("Howdy!")

NOTE: The actual NewIndex property was eliminated in VB.NET because it was not robust; it would become unreliable
if items were removed from the list before the property was read; thus it was added at the only place where it could be
guaranteed to be 100% reliable – as the return value from the Add method.

75. Windowless controls are not supported. Under VB6, windowless controls, sometimes referred to as
lightweight controls, differed from regular controls in one significant way: They did not have a
handle (hWnd property) or their own graphics interface (Device Context), because of drawing to their
own Device Context, or drawing surface, they drew instead right onto their parent control. Such as
the Form. Because of this, they used fewer system resources. We created a lightweight user control
in VB6 by setting its Windowless property to True at design time. Lightweight user controls could
contain only other lightweight controls. Also, not all containers supported lightweight controls.
During an upgrade from VB6 to VB.NET, most windowless controls will default to becoming
windowed. The primary benefit of using windowless controls had been to reduce resource
consumption (drawing surface) when we have a very large number of controls on a form. However,
this applied to Windows 9x ONLY. Windows NT, Windows 2000, Windows XP and later do not have
these resource constraints, and so lightweight controls offered absolutely no benefit on these more
recent platforms. Even so, while there are significant disadvantages to using windowless controls,
for example, layout issues such as layering problems, Microsoft recognizes the value of lightweight
controls and says that it will be releasing samples that show how to achieve similar effects in
Windows Forms (they said that current to VB2002, but VB.NET still does not yet support them).
Instead, see my article, Emulating VB6 Image Control Features Under VB.NET, on page 243 to
see how to easily implement controls that will emulate the lost windowless VB6 Image control, to
include transparency backgrounds.
76. Image controls are not supported. Under VB.NET, picture controls are supported, whereas image
controls, which are actually windowless lightweight controls, are not supported simply because
VB.NET does not support lightweight (non-object) controls (this would kind of defeat the whole
Object-Oriented part of Object-Oriented Programming). During an upgrade, lightweight controls
become Window Forms. See # 75. Also see my article, Emulating VB6 Image Control Features
Under VB.NET, on page 243 to see how to implement controls that will easily emulate the lost
windowless VB6 Image control, to include transparency backgrounds.
 Page 189

77. Shape controls are not supported. Addressed in VB2010. Under VB6, The Shape control displayed a
rectangle, square, oval, circle, rounded rectangle, or rounded square. Prior to VB2010, the GDI+
classes in System.Drawing replaced this control. If we needed to draw shapes on the form, we would
use the Paint event to draw circles, squares, and so forth by using the GDI+ Draw methods, being
much faster, more powerful, and have more options than the VB6 drawing controls.
Consider the following example:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
Dim pen1 As New Pen(Color.Violet, 2) 'create a drawing pen that is 2 pixels wide.
e.Graphics.DrawRectangle(pen1, 10, 10, Me.Width - 35, Me.Height - 60) 'draw a violet rectangle on the form.
Pen1.Dispose 'dispose of the allocated resource.
End Sub

NOTE: The optional Visual Basic Power Packs controls (standard on VB2010 and after) include LineShape,
OvalShape, and RectangleShape controls that can be used to replace the VB6 Line and Shape controls. In addition to
duplicating the behavior of the VB6 Line and Shape controls, these controls add new capabilities. These include gradient
fills, run time selection, and even run time events (the Visual Basic Power Packs are available for free for Pre-VB2010 at
http://msdn.microsoft.com/en-us/vbasic/aa701257.aspx. This also includes many enhancements and VB6-style functionality for printer
objects. For example, if we add a .NET reference to Microsoft.VisualBasic.PowerPacks.Vs, we can replace the VB6 command
“Dim Printer As New Printer” with “Dim Printer As New PowerPacks.Printing.Compatibility.VB6.Printer ” and we will not have to
change any other printer-related code for the Printer object, nor even any VB6 twips values.

78. Line controls are not supported. Addressed in VB2010. Under VB6, the Line control displayed as a
horizontal, vertical, or diagonal line. Until VB2010, the .NET GDI+ classes in System.Drawing
replaced the Line and Shape controls. If we want to draw shapes on the form, override the OnPaint
or Paint event and paint circles, squares, and so forth by using the GDI+ Draw methods. Or better,
see the NOTE for #77 for free Microsoft line and shape controls for VB.NET prior to VB2010.
79. OLE Container controls are not supported. Under VB6, The OLE container control enabled us to
add OLE objects to our forms. Under VB.NET there is no OLE container control. OLE is not a part
of Object-Oriented design, and as a natural consequence it is not a part of .NET. However, if we
need the equivalent of the OLE container control, we can simply add the WebBrowser control to a
form and use it as an OLE container control. Microsoft has been trying to get away from the archaic
OLE format for some time, moving toward the sleeker and faster ActiveX/Web technology. OLE has
been around since VB4. No wonder that some older hacks miss it.
80. Label controls will not have a Caption property. They have a Text property instead, which is simply
providing compatibility to all other objects that exhibit captioning properties on a form. Under VB6,
some controls, such as Label, had a Caption property that determined the text displayed in or next to the
control. Other controls, such as TextBox, had a Text property that determined the text contained in the
control. Under VB.NET, in Windows Forms, the property that displays text in a control is consistently
called Text on all controls. This simplifies the use of controls. If all controls have the same name for the
same property, our expertise with the environment becomes greater faster.

81. The Tag property is not supported. Addressed in Beta2; a Windows Forms extended Tag control in
the compatibility library was used to provide the same functionality until VB2005 re-incorporated it
into the Forms package. Further, in Windows Forms we can also use inheritance to extend the built-
in controls and add our own properties. Having inheritance available as a tool makes the built-in
controls significantly more flexible. Not only can we add as many properties as we like, but we can
also make those properties strongly typed.
 Page 190

82. The ToolTipText property is not supported. Tooltips are, and are now handled through a ToolTip
control. Implementing it as a control helps speed the execution of code. Having it built into each
control ate time and resources. It also translates very well during a VB6 upgrade to VB.NET.
Under VB.NET, adding a ToolTip control to the form allows ux to add text to the ToolTip property
on each required control, or by adding code for each control with something like this:
Me.ToolTip1.SetToolTip(Me.btnCont, "Continue processing") 'associate a tooltip to btnCont button.

Another advantage to implementing tooltips this way is we can define multiple ToolTip controls so
we can format each tooltip control to display their data differently, if our application requires such
things. Apply control tool tips to only the tooltip control that suits how we want it displayed.

83. The TextHeight property is not supported. Use instead the more logical Font.GetHeight property of
the control for which we wish to obtain the height, as in “ Me.TextBox1.Font.GetHeight(strText)”. In
VB.NET, this returns the line spacing in pixels (picture elements) of the font assigned to it
(technically, what we call a Font should actually be referred to as a TypeFace, as the term Font
precisely refers to a specific size and style of type within a Type family). The line spacing is the
vertical distance between the base lines of two consecutive lines of text. Thus, the line spacing
includes the blank space between the lines along with the height of the character itself.
If we do not have much experience in pixels, which would be impossible if we did any kind of display
resolution work in C++ and/or Win32 Interop, but are rather more familiar with Twips or Font Points
(and perhaps more comfortable, because up through VB6 we were almost forced to live with Twips), here
is an easy breakdown between the various standards:
 There are 96 pixels (display resolution) to a logical inch. A logical inch can be a physical inch, as it was early on. On
current computer displays, the number of logical inches displayed is based upon the monitor resolution setting, which
we might select as 800x600, 1024x768, 1280x960, 1440x900, or even 1920x1080, as my old 23” wide-screen monitor is
set to. The monitor setting that actually reflects 96 dots per physical inch, as my 1920x1080 setting is (1920/96=20
inches, and 1080/96=11.25 inches, making my screen diagonal size actually 22.9469 inches), is usually referred to as a
screen’s optimal or nominal resolution. When we change these resolutions, the physical size of our monitor does not
change, but the number of displayed logical inches does.
 There are 72 points (typographical resolution) to a logical inch (or Imperial or Imperial Inch).
 There are 1440 Twips to a logical inch. Twips (abbreviated from “twentieth of an inch point”) were adopted very
early on in the life of Visual Basic, and used eagerly (and also with a lot of great confusion) up through VB6 (note that
it is still used in such apps as ShockWave Flash and the Symbian Open Source Operating System for mobile devices). It
is an intermediate resolution developed to easily translate between pixels and points using integer math.
 There are 15 Twips for each Pixel, and 20 Twips for each Point (and 567 twips to a centimenter), meaning that there
are 4 pixels for each 3 points (96/72 = 4/3), which even then would not normally be precisely translatable between these
two popular formats without resorting to fractions, yet this is entirely impractical in many cases, in particular on
platforms requiring lightening-fast integer math. 96 times 3 and 72 times 4 both equal their least common multiple; 288.
Yet, 288 times 5 equal 1440. I suppose 1440 TPI was chosen to allow for fractional points?
The broader point here is that this multiple-resolution fracas, not to mention applying physical
measurements to logical measurements, was confusing graphical interpretation to no end. For this
reason it was decided, and not arbitrarily, that Twips should be dropped in .NET. It was because of
the tremendous and often greatly under-appreciated dependence that modern software has for display
resolution standardization in the ever-broadening field of display devices that the measurement
standards were greatly simplified (and finally standardized) in the .NET platform to offer pixels as
its primary support base, rather than a hodgepodge of various resolutions that too frequently required
translation, and therefore an additional translating unit of measurement between them.

84. The TextWidth property is not supported. Use instead the more logical Font.GetWidth property of
the control for which we wish to obtain the width for, as in “Me.TextBox1.Font.GetWidth(strText)”. In
VB.NET, this returns the text width in pixels (picture elements). See the notes for #83.
 Page 191

85. Setting a Timer control's Interval to 0 does not disable it. Under VB6, the Interval property on a
Timer control returned or set the number of milliseconds between calls to the Timer event. If it was
set to 0, it disabled the Timer control. The Enabled property also determined whether the timer was
running. This was confusing for testing because even when Enabled was True, the timer would not
be “enabled” if the interval was 0. Under VB.NET, the Interval property indicates the time in
milliseconds between timer ticks, and cannot be set to 0. The Enabled property indicates whether the
timer is running. This provides a more intuitive behavior to simplify coding with Timer objects.

86. Top-level menus may not be used as context menus. In VB.NET, use a ContextMenu. The use of
top-level menus for context menus in VB6 was due to simpler design for slower computers (4 MHz
and some 2MHz at the time). Under VB6, a Menu control was used to represent each item in a menu
tree. The same Menu control could also be used simultaneously as a main menu or as a context
menu; being much like a node of a TreeView control, which could be a branch or a leaf object. This
concept was a direct integration of the underlying Application Programming Interface (which I had
used heavily even back when I was developing applications in Microsoft’s QuickC for Windows – the
precursor to Visual C++, which used a DOS Editor/IDE). Under VB.NET, a MenuItem control, not
a Menu control, represents each item in a menu tree. The MenuItem control can be added to either a
MainMenu object or to a ContextMenu object, which, under .NET, are two entirely different form
controls, and as such, a single menu unfortunately cannot be encapsulated by both at once, because
in OOPL terms, data safety would have been compromised, which is not possible in OOP.
It is reported that we can get around this snag under VB.NET by using the CloneMenu method of the
needed MenuItem object to create an exact copy of it if we would like to share that menu between a
MainMenu object and a ContextMenu object. That may be, but we are not able to do so with MenuStrip
and ContextMenuStrip. If we build menus with Menu, MenuItem, and ContextMenu, which must be
done at run-time, we can take advantage of the Clone method, because these objects can transfer and
receive arrays of Menuitems. Otherwise, with the more convenient Strip versions, we have to reconstruct
the menus. However, I have found easy solutions to these problems. See these solutions in, Adding Run-
Time Custom Menus and Cloning ToolStripMenuItems Under VB.NET, on page 282.

87. Old forms using vbPixels for Scalemode will not upgrade correctly. Addressed in VB2003.

88. DDE is not supported. Dynamic Data Exchange is supported through COM references. DDE was an
early attempt to enable the exchange of data between applications. DDE was later replaced (and
absorbed) by COM, but was still supported for backward compatibility through VB6. This makes it
archaic technology; older than COM, older than OLE, and, in terms of current technology, older than
digital dirt. DDE was introduced in VB3 to enable running applications to share data. For example,
DDE made it possible to insert a spreadsheet chart into a document. When the spreadsheet changed,
the chart in the document changed. Although DDE was still used by some VB6 applications, it was
supplanted by COM’s OLE back in VB4, which provided greater control over shared data. But even
OLE is now clunky compared to current technology, and is working its way to the digital dustbin.

On top of that, COM and its slower reference counting is not part of the .NET paradigm. As such, if
Microsoft is now trying to dump OLE and is inching away from COM, why should they maintain
DDE? Still, most VB programmers did not ever know that DDE even existed before reading this.

89. Circle is not supported. It is implemented in a different, more powerful form through the DrawEllipse
method from the System.Drawing namespace (just set the width and height to the same value). Windows
Forms has a new set of graphics commands that replace the old methods Circle, Cls, PSet, Line, and
Point. The Windows Forms package is built on top of GDI+ (Graphical Display Interface Plus), a
feature-rich 2-D text and imaging graphics library that is now directly accessible from VB.NET. VB
programmers have not been able to access these types of features in previous versions without having to
resort to Declare statements and GDI Pinvokes. While the learning curve is a little steeper, the flexibility
 Page 192

and power of GDI+ will allow developers to quickly design applications that would have taken
significantly more work in previous versions of Visual Basic. We access the DrawEllipse method using
Graphics and Pen objects. For example:
Dim g As Graphics = Me.CreateGraphics 'create a Graphics Object for the form.
'
' circle object example 1.
Dim pen1 As New Pen(Color.Black, 2) 'create 2 pixel wide pen (pen width defaults to 1 pixel).
g.DrawEllipse(pen1, 100, 100, 200, 200) 'draw a circle at x100, y100 that is 200-high/200-wide.
'
' circle object example 2.
pen1.Color = Color.LightSkyBlue 'change pen color, but keep pixel width of 2.
g.DrawEllipse(pen1, New Rectangle(50, 50, 100, 25)) 'draw ellipse at 50,50, 100-wide by 25-high. (x,y,width,height)
'
' circle object example 3.
pen1.Color = Color.Red 'change pen color.
Dim Rec As Rectangle = Rectangle.FromLTRB(75, 75, 85, 95) 'declare rectangle at (Left,Top,Right,Bottom) specs.
g.DrawEllipse(pen1, Rec) 'draw ellipse.
'
' circle object example 4.
Dim CustomDats() As Single = {1.0!, 0.5!, 1.0!, 0.5!, 1.0!, 0.5!, 2.0!, 0.5!} ' "— — — —— "
pen1.DashPattern = CustomDats 'assign the above custom pattern to the pen.
pen1.DashStyle = Drawing2D.DashStyle.Custom 'tell the pen to use the custom pattern.
pen1.Color = Color.LimeGreen 'change pen color.
pen1.Width = 5 'change pen width in pixels.
g.DrawEllipse(pen1, Rectangle.FromLTRB(25, 150, 75, 200)) 'draw ellipse with rectangle (Left/Top/Right/Bottom).
pen1.Dispose() 'dispose of allocated resources.
g.Dispose()

90. Cls is not supported. For Console applications, we can use “Console.Clear”. For Forms, at best, we
can use “Me.Refresh()” or “Me.Invalidate()” (though we should never do this within a Paint event). We
can also re-set the BackColor property to clear the palette by using the Clear method from a
Graphics object (available from the System.Drawing namespace). For example:
Me.CreateGraphics.Clear(Me.BackColor) 'Clear the form (Me) background using the specified color.
Me.Refresh() 'refresh the form (and propagate refresh to child controls).

NOTE: Do not do a “Me.Refresh()” within a Paint event. Using “e.Graphics.Clear(Me.BackColor)” there is OK.

91. Line is not supported. The Line command carried over from DOS BASIC and QuickBasic, and had
a confused parameter structure. In VB6, the Line control provided easier ways to draw lines on a
form. The VB6 Line control was a “lightweight control”. In VB.NET, there is no equivalent for the
Line control because lightweight controls are no longer supported. However, there are ways to draw
lines on a form both at design time and at run time. See points #77 and #78 for new line controls.
At design time, in a pinch, we could draw a vertical or horizontal line on a form by adding a Label
control and setting AutoSize to False, the Background color as needed, the Text property to blank, the
BorderStyle property to None, and the Width or Height to 1. At runtime, we can draw vertical,
horizontal, or diagonal lines in a form's Paint event using e.Graphics and its DrawLine method.
In VB6, we used a Line control to draw a line on top of a container control such as a PictureBox or
Frame control by adding a Line control to the container. In VB.NET, we can achieve the same effect
by invoking the DrawLine method from a Graphics object (available from the System.Drawing
namespace) in the Paint event for the container control, as shown below:
Dim pen1 As New Pen(Color.Blue, 2) 'create a pen drawing object 2 pixels wide.
e.Graphics.DrawLine(pen1, 0, 0, Me.Width, Me.Height) 'draw a line top-left to bottom right.
Pen1.Dispose() 'dispose of allocated resources.

92. PSet is not supported. Some gurus still say it can be duplicated in a Paint event using the
DrawEllipse method from a Graphics object (available from the System.Drawing namespace) to
draw a circle with a height and width set to exactly 1 pixel. Actually, they were just puppeting an
early Beta note from Microsoft before the Bitmap PSet method was released for VB2002. See the
example following the one below for a faster and more efficient example.
e.Graphics.DrawEllipse(Pens.Black, 100, 100, 1, 1) 'draw a black dot 1-high/1-wide at (100,100).
 Page 193

Alternatively, if we are using a Bitmap object (declared in the System.Drawing namespace), or an


object that is formatted as a Bitmap, such as the Image from a PictureBox, or a form's background
image, we can use the SetPixel method from the Bitmap object. Consider the following example for
setting a single red point within the image of PictureBox1:
With Me.PictureBox1 'display a red point in the center of Image object of PictureBox1.
DirectCast(.Image, Bitmap).SetPixel(.Width \ 2, .Height \ 2, Color.Red) 'directly modify Image.
.Refresh() 'refresh the display of PictureBox1 to show the change to its Image object.
End With

93. Point is not supported. It is implemented in a different form. Use the GetPixel method from a
Bitmap object (available from the System.Drawing namespace). See the notes for #89 for additional
details.

Consider the following example:


Dim Clr As Color = DirectCast(Me.PictureBox1.Image, Bitmap).GetPixel(X1, Y1) 'Get the color at point X1,Y1.

94. AutoRedraw is not supported. No longer needed; the graphics in a PictureBox now persist.
AutoRedraw was performed by always drawing to the Image object of a PictureBox, considered its
persistent drawing surface, thus making the drawing persistent. This extra step of forcing drawing to
the Image object and then refreshing it so the modification was displayed also ate lots of time and
resources, though its result was impressive. Display Controls now have tighter management through
better-designed Paint event processing that allows better persistence emulation under .NET without
eating extra time and resources, so AutoRedraw was no longer required,
95. PrintForm is not supported. The Printer is no longer a general object. However, it can be emulated. Under
VB6, the PrintForm method sent a bit-by-bit image of a Form object to the printer. Still, even this printing
feature did not work correctly on some forms. In Windows Forms, VB.NET has a printing framework that
allows us to build complex print documents quickly. It also includes a built-in Print Preview dialog box. To
upgrade VB6’s use of PrintForm, we can use this printing framework to quickly build a print document.
Also, the free Visual Basic Power Packs, referenced in the notes for #77, also features a form printer.

96. Scale is not supported. Under VB6, the ScaleMode property returned or set a value that indicated the
unit of measurement for coordinates of an object when using graphics methods or when positioning
controls. VB.NET Windows Forms simplifies form layout by always making measurements in
pixels. In addition, Windows Forms has a better way to handle resizing. The AutoScaleBaseSize
property automatically adjusts the scale according to the resolution (dpi) of the screen and font size
we use. However, in the end, scaling really slows a program down a lot.

97. The Name property for forms and controls is not exposed at runtime. Addressed in Beta2.

98. Print will not include a linefeed at the end of a line. Use PrintLine. The VB6 form was an old DOS
BASIC cheat. Having said that, I noticed in VB2005 that it was back.

99. File I/O will not be compatible, at all, and must be rewritten. Their functionality is available through
class libraries, using a platform-compatible form. Old VB6 syntax was archaic, anyway. File I/O
statements under VB6 were included in the language, a result of its non-structured origins. Under
VB.NET, File I/O operations are available through class libraries. Removing the file I/O statements
from the language allows different I/O libraries to be used from VB.NET. This would be even more
awkward if the file I/O statements were in the language, because identifiers such as Open, Close,
Print, and Write would consequently be reserved words. For example, consider the following VB6
format command:
Open "MyFile.txt" For Input As #1

This would be expressed in VB.NET (with System.IO imported) as:


 Page 194

FileOpen(1, "MyFile.txt", OpenMode.Input) 'FileOpen is a part of the System.IO namespace.

100. Printer object methods are not automatically upgraded and must be rewritten. The Printer is no
longer a general object and can be easily emulated. However, the old VB6 methods can be fully
implemented using the free Visual Basic Power Packs from Microsoft (see the note for #77 for details
and the link). With it installed, add a .NET reference to “ Microsoft.VisualBasic.PowerPacks.Vs ”, add
“Imports Microsoft.VisualBasic.PowerPacks.Printing.Compatibility ” at the top of our code file,
and finally, where we need to access the VB6-style printer object, add “Dim Printer As New
VB6.Printer”. Also refer to Black Book Tip # 57: Printing Plain, Formatted and WYSIWYG Documents
Made E-Z on page 716 to see how to create simple VB6-style printer I/O, and easily enhance it to create
truly impressive desktop-publishing style printer I/O.

101. Clipboard object methods are not automatically upgraded and must be rewritten. As of VB2005,
with the introduction of the “My” namespace, all the old VB6 clipboard functions, plus a hefty host
of additional features, have returned through the new My.Computer.Clipboard object. The only
difference now is the easier-to-access enumerator DataFormats values versus the old VB6 vbXXXX
constants we all seemed to always have to look up. Clipboard I/O is also built right into many .NET
controls, such as text boxes. Previously, the VB.NET Clipboard object (the one supported by
System.Windows.Forms.Clipboard) had no direct equivalents to the VB6 clipboard, and during a
VB6 to VB.NET upgrade, any code that referenced the Clipboard object was not upgraded and had
to be rewritten. No more. It now upgrades with perfect ease, where the only upgrading simply
involves constant values. The following example shows how to modify code that used the VB6
Clipboard object to the alternate VB.NET System.Windows.Forms.Clipboard format:
' VB6 code 'VB2005+ notes.
Clipboard.Clear 'supported in VB.NET.
Clipboard.SetText "hello", vbCFText 'supported in VB.NET as Clipboard.SetText("hello", DataFormats.Text).
If Clipboard.GetFormat(vbCFText) Then 'supported in VB.NET as Clipboard.GetFormat(DataFormats.Text).
TextBox1.Text = Clipboard.GetText(vbCFText) 'supported in VB.NET as Clipboard.GetText(DataFormats.Text).
End If

'Previous (old) VB.NET code (VB2002-VB2003 allowed only these formats) 'VB6 code comparison.
'Clipboard.Clear.
Clipboard.SetDataObject(New DataObject(DataFormats.Text, "hello")) 'Clipboard.SetText "hello", vbCFText.
If Clipboard.GetDataObject.GetDataPresent(DataFormats.Text) Then 'If Clipboard.GetFormat(vbCFText) Then.
TextBox1.Text = CStr(Clipboard.GetDataObject.GetData(DataFormats.Text)) ' TextBox1.Text = Clipboard.GetText(vbCFText)
End If 'End If

102. The Err object is not shared between managed (.NET) and unmanaged (ActiveX) code. It is a
different object, altogether. Also, the CLR does not support unmanaged code.

103. The App object is not shared between managed (.NET) and unmanaged (ActiveX) code. It is a
different object, altogether. Also, the CLR does expose support for unmanaged code.
104. Screen.MousePointer does not have a direct replacement. This had been addressed, but that
solution no longer works with later versions of VB. However, see “Black Book Tip # 32: Dealing
with the Form Cursor not displaying over Form Controls” on page 568 for a working solution.

105. Webclasses are not supported. For VB.NET, use Web Forms instead. Webclasses were dropped
because they would not always work. A Webclass Designer application, also known as a VB IIS
(Internet Information Server) application, is an in-process COM component (DLL) that runs with
IIS as its client. An IIS application also required the presence of the server of the Microsoft runtime
DLL, MSWCRUN.DLL, and other files used to create a project, and any supporting files that Web
pages require, to also install and run on the Client computer. Web Forms have no such restrictions,
and runs entirely from the server, like HTML.
 Page 195

106. DHTML projects are not supported. Use Web Forms. VB6 DHTML projects are a different
paradigm. Under VB6, DHTML applications contained DHTML pages and client-side ActiveX
DLLs. Under VB.NET, Web Forms support broad-reach applications through standard HTML. Rich
applications can be supported in a much more secure way by using VB.NET’s Windows Forms
controls hosted in a browser, or with a downloaded “safe Windows Form” EXE. This code runs
inside of a secure sandbox, so it cannot harm a client computer. While ActiveX documents and
DHTML applications cannot be directly upgraded, we can still navigate between ActiveX documents,
DHTML applications, and Web Forms.

107. UserControl projects are not supported. A cost to VB6, yes, but the changes to .NET brought it
into compatibility with other platforms. .NET user controls are also MUCH easier to develop and
use. Many of the differences among the VB6 and VB.NET UserControl classes are similar to the
differences between the VB6 and VB.NET Form classes.

108. ActiveX Document projects are not supported. Formerly known as "Document Objects"
(DocObjects), ActiveX Documents were extensions to Microsoft's OLE compound document
architecture that allowed a container application to use the full capabilities of server applications. As
of Version 3.0, Internet Explorer (IE) was made an ActiveX Documents "container". Microsoft
Office applications and the HTML viewer used to render Web pages are ActiveX Document
"servers". Thus, IE is able to view a Web page, an Excel spreadsheet, or a Word document by
launching the appropriate server application. Under VB6, ActiveX Documents could appear within
Internet browser windows and offered built-in viewport scrolling, hyperlinks, and menu negotiation.
While ActiveX Documents and DHTML applications cannot be directly upgraded, we can still
navigate between ActiveX Documents, DHTML applications, and Web Forms. Thus, we can replace
them with user controls containing web browsers and scrollers, or with a web browser on a form.

109. The IDE Extensibility Model is not backwardly compatible. Of course not, it is a brand new
IDE. The VB6 IDE was designed to operate specifically with VB. The .NET IDE is language-neutral
and will operate with all Visual Studio languages: C++, C#, VB.NET, ADO.NET, Visual Web
Developer, and F#, plus any CLS-compliant language we wish to add into Visual Studio. That is,
except for the free language-locked versions of Visual Studio Express.

110. Run->Break->Edit->Continue development is not supported. Addressed in VB 2005.

111. The Immediate window will not work in Design mode. Addressed in Vb2008

112. SDI will not be an option in the IDE – MDI or nothing. This was true only during the initial Beta
trials, but not after that. Also, an SDI (Single Document Interface) application in VB.NET can be
made into an MDI (Multiple Document Interface) easily. MDI applications are applications in which
we can view and work with several documents at once. An example of an MDI application is
Microsoft Word or Excel. Word allows us to work with several documents at once. In contrast, SDI
applications are the applications which allow us to work with a single document at once. An example
of an SDI application is Microsoft WordPad or Notepad.

113. Debug.Print is not supported. VB2008 reintroduced the Debug.Print method, which is sent to
the new Immediate Window, just as it had been done under VB6. Previous to that, through VB2005,
we could use the Print command, whose output went to the Output page.
 Page 196

114. Debug.Assert is not supported. VB2008 reintroduced the Debug.Assert method, which is sent to
the new Immediate Window, just as it had been done under VB6. Previous to that, through VB2005,
we could use the Assert command, whose output went to the Output page.
115. Data binding with DAO is not supported. DAO is a COM (Common Object Model) technology, and is
still fully functional in VB.NET as code through a COM interface. Under VB6, Controls on Visual Basic
forms can be bound to DAO data sources. Under VB.NET, ADO.NET offers read/write data binding to
controls for Windows Forms and read-only data binding for Web Forms. VB6 ADO data binding is upgraded
to the new ADO.NET data binding. However, DAO data binding cannot be upgraded and will add errors to
the upgrade report. DAO is not upgraded because its technology, even under VB6, was being passed up in
favor of the more powerful and capable ADO technology. Indeed, when VB6 was still actively supported,
technical support was dropped for DAO, yet it was maintained as legacy code because the initial release of
VB6 supported it. As such, why should it suddenly be resurrected and supported in a newer incarnation of VB
when Microsoft was trying to clean out the clutter from its VB6 closets? It does not make sense.
ADO.NET provides classes for disconnected data access. These classes provide performance and
scalability improvements over previous versions of ActiveX Data Objects (ADO) when used in
distributed applications. They also allow simpler integration of XML data with our database data.

116. Data binding with RDO is not supported. RDO is a COM (Common Object Model) technology, and is still
fully functional in VB.NET as code through a COM interface. See notes in #115. Under VB6, Controls on
Visual Basic forms can be bound to RDO data sources. Under VB.NET, ADO.NET offers read/write data
binding to controls for Windows Forms and read-only data binding for Web Forms. VB6 ADO data binding is
upgraded to the new ADO.NET data binding. However, RDO data binding cannot be upgraded and will add
errors to the upgrade report. They are not upgraded because its technology, even under VB6, was being passed
up in favor of the more powerful and capable DAO, and even later the ADO technologies. Indeed, when VB6
was still actively supported, technical support was dropped for RDO, which is even older and more primitive
technology than DAO, which was also dropped, yet it was maintained as legacy code because the initial
release of VB6 supported it. As such, why should it suddenly be resurrected and supported in a newer
incarnation of VB when Microsoft was trying to clean out dated clutter from its VB closets?
 Page 197

String Format Changes Between VB6 and VB.NET.


(Adapted from Microsoft sources) By understanding how VB6, COM, and .NET store strings, we can
better understand how to deal with developing and porting applications from VB6 to VB.NET.
VB6 and other COM-based applications employ a string format known as BSTR (Bee-String), which is
defined in the OLE 2.0 specification and which is also a part of the Microsoft ActiveX specification (MS
Marketing’s new name for what had been OLE2). When we define a string under VB6, such as this:
Dim str As String: str = "help"

This string’s data is stored in memory like this:

In the above diagram, the BSTR is a Pointer to Unicode data, being a 32-bit unsigned IntPtr, and it
points to the base of a 16-bit Unicode Array of type Char. Uncharacteristically, this array is preceded by
a 32-bit Length Descriptor, though the BSTR points beyond this Length Descriptor and directly to the
first character of the Unicode Array. Technically, this IntPtr, Length Descriptor, and Unicode Array
should not be confused as being a single entity, but rather as three separate definitions bound together by
the BSTR design. Also, we would be incorrect to call the Unicode Array a String, except as a Unicode
String; an array of sequentially stacked Chars, where each Char is a 16-bit unsigned integer scalar. Also,
even though we would be technically correct to refer to a COM BSTR as a VB String, we also cannot
refer to it simply as a String, but by its unequivocal name of Bee-String.
NOTE: The 32-bit Length Descriptor is part of the BSTR design, not a part of the Unicode String specification,
The Unicode Array that is pointed to by a BSTR must always be preceded by a 32-bit (4-byte) Length
Descriptor and is always terminated by a single null 2-byte character (&H0000). The Length Descriptor
is not typical for Unicode Arrays, but is requisite for defining data for type BSTR. Also, be mindful that
the 32-bit Length Descriptor is a marshaling command that specifies the number of bytes, not the
number of characters, to allocate space for in the Unicode Array, excluding the terminating 16-bit null
character. Since the array is Unicode, the actual character count is always one-half this byte count.
Additional null characters can be embedded anywhere within a BSTR Unicode Array, so we cannot rely
on a null character to signal the end of its string. Thus, the Length Descriptor is vital to its definition.
NOTE: Under VB6, the address of a string’s BSTR pointer was returned by the VB6 VarPtr function, and the address of the
characters of the Unicode Array, beyond the 32-bit Length Descriptor, was returned by the VB6 StrPtr function. Access to the
BSTR pointer, however, is not typical under VB.NET because OLE2 is not part of the .NET Common Language Specification.
We should also emphasize that an embedded null Unicode character is a 16-bit zero (2-bytes), not an 8-
bit zero (1-byte). Be mindful of this when testing for null characters in Unicode Strings when indexing
in a Byte array, not by Char. Further, because a BSTR can contain embedded nulls, the Unicode
terminating null is not of much use, at least as far as COM, ActiveX/OLE2, and VB6 are concerned.
However, its presence is extremely important for Win32. The reason is that the default Win32 Unicode
String (denoted in Win32 by LPWSTR; Long Pointer to Wide–16-bit–String) is defined as a pointer to a
null-terminated Unicode character array, which, by the way, is not allowed to contain embedded nulls.
This explains why BSTR's are null terminated, though it does not use it. A BSTR is just a pointer to an
LPWSTR with an added Int32 byte Length Descriptor leading it, allowing for embedded nulls.
 Page 198

C++ and Win32 use the string data types LPSTR (8-bit ANSI) and LPWSTR (16-bit Unicode).
An LPSTR string is defined as a pointer to a null-terminated ANSI character array. However, because
the only way that we can tell when an LPSTR string ends is by the location of the terminating null,
LPSTRs are not allowed to contain embedded null characters. Similarly, an LPWSTR is a pointer to a
null-terminated Unicode character set with no embedded nulls. These string data types are shown below.

NOTE: We may encounter the data types specified in some documentation as LPCSTR and LPCWSTR. The embedded C
means Constant and indicates an instance of this data type cannot be changed by any Pinvoke Signature that uses this type.
Otherwise, an LPCSTR is just like LPSTR, and, similarly, an LPCWSTR is just like LPWSTR, except that a pointer to a
disposable clone of the string is passed as a parameter by the compiler, rather than the actual string itself.
When sending a string to a Pinvoke, we may notice that it is usually sent ByVal. If the Pinvoke will be
modifying it on rare occasions, be sure to either resort to using a ByVal String or StringBuilder buffer,
or prefixing a ByRef string with the “<MarshalAs(UnmanagedType.VbByRefStr)>” modifier (available by
importing the System.Runtime.InteropServices namespace). As noted in this tome to death because I get
this question all the time, as of VB2005, we can once again, as we were able to do under VB6, pass a
string ByVal to a Pinvoke in order for it to be modified by the Pinvoke method, which also made it fully
compliant to OOP. This also eliminated a lot of confusion that people had with being told to pass
StringBuilder strings ByVal and passing strings ByRef using the VbByRefStr parameter. But even so,
considering that StringBuilder text manipulation is 200 times faster than a regular string, it is strongly
recommended that one should look into them when performing time-critical string manipulation.
Within native VB.NET code, passing a string ByVal or ByRef as a parameter to .NET methods will work exactly as
expected, passing the string address when sent ByRef, and as an immutable string when ByVal. However, sending a string
ByRef to unmanaged interop Pinvokes served no purpose because the CLR would pass them as if ByVal, as immutable, for
managed code self-protection, except when preceded by the special VbByRefStr prefix shown above (giving the
programmer time to think about what they are doing), or when passed ByVal for full VB6 (and OOPL) compatibility as of
VB2005. If we are confused, then, as a rule of thumb, forget about passing interop strings ByRef and just use ByVal. It is
interesting that Microsoft decided to violate OOP-compliance in an attempt to try and save users from themselves. As a
result, however, they ended up confusing those users even more. We are a stubborn lot, full of uncoordinated assumptions.

Strings in VB.NET are 16-bit Unicode, just as it had been under VB6. When we send a string of text to a
Pinvoke, the Unicode string may need conversion to 8-bit ANSI if the target Pinvoke expects it, and
converts it back to Unicode if it returns or modifies a string. Although internal bookkeeping can figure
this out virtually all the time, the system does need our help and our permission to operate on our string
this way. Hence, if we must pass or retrieve character data that is 8-bits wide, we should marshal it in the
Pinvoke declaration accordingly, or make this conversion transition easy by simply inserting the ‘ Auto’
verb after the ‘Declare’ Pinvoke declaration verb and trust internal bookkeeping to get it right.
See the article, Adapting Win32 String Data Types for VB.NET Interop on the next page, or, soon
following, “Passing Data Between Protected and Unprotected Memory Under VB.NET” on page 212
for additional “special case” details on sending strings through Pinvokes, where information might be
passed between protected managed class data space and unmanaged COM data space.
NOTE: VB6 is a COM program built under the OLE2 specification, thus making VB6 an ActiveX application! Cool trivia!
 Page 199

Adapting Win32 String Data Types for VB.NET Interop.


(Adapted from Microsoft sources) Most Pinvokes can be used just as they were under VB6, with the
exception of adjusting integer data type naming; the VB6 Long is now a VB.NET Integer, and the VB6
Integer is now a VB.NET Short. If we upgrade Pinvoke declarations manually, we can simply rename
each Pinvoke Integer to Short, rename each Long to Integer, and then most-all Pinvokes will work just
as they did under VB6. If we are using the VB6 Upgrade Wizard, these changes are made automatically.
Also, for any String parameters, we should include Auto directly after the Declare verb, except if the
Declare line also specifies an Alias, which will also typically declare the needed text conversion of “A”
(8-bit ANSI), as in SendMessageA or “W” (Wide 16-bit Unicode), as in SendMessageW. If there is no type
tag in an alias declaration and there are string parameters, we can assume them to be Unicode, The Auto
verb forces VB.NET conversion of Unicode text to the platform-specified format as required by the
Pinvoke. Although I prefer to specify the correct data type to achieve the fastest operation, like Ansi or
Unicode, Auto is a convenient shortcut, because .NET is very good at determining the appropriate type.
NOTE: If the Pinvoke docs specify ByRef strings, we can safely change them to ByVal for them to work under VB.NET, just
as this trick did under VB6, but we may instead prefer to specify the UnmanagedType.VBByRefStr marshaling directive to allow
VB.NET to pass a mutable string to a Pinvoke ByRef. These processes will be described in more detail shortly.
To start, consider the following declaration for the VB6 GetVersion API, and a method to access it:
Private Declare Function GetVersion Lib "kernel32" () As Long
Sub GetVer()
MsgBox("System Version is " & CStr(GetVersion()))
End Sub

This can be upgraded to VB.NET as follows (note that Long becomes Integer in this example):
Private Declare Function GetVersion Lib "kernel32" () As Integer
Sub GetVer()
MsgBox("System Version is " & GetVersion().ToString)
End Sub

Fixed-Length Strings.
On top of numeric data type upgrades, VB6 has a fixed-length string data type ( String * CharCount) that
VB.NET does not directly support. In most cases, in VB6 we can perform a more compatible action
using a normal string filled to a particular length. Consider the following original VB6 code:
Private Declare Function GetUserName Lib "advapi32.dll" Alias "GetUserNameA" (
ByVal lpBuffer As String, ByRef nSize As Long) As Long
Sub GetUser()
Dim Ret As Long
Dim UserName As String
Dim Buffer As String * 25 ' set Buffer to 25 16-bit Unicode characters.
Ret = GetUserName(Buffer, 25) ' the API requires 25 8-bit ANSI character string, so autoconvert types.
UserName = Left$(Buffer, Ret) ' Because Buffer is actually null-terminated, this extra step is not really required and
MsgBox(UserName) ' we could have simply specified Buffer here instead of UserName.
End Sub

NOTE: Notice that we are passing a malleable string parameter using ByVal, which is a beloved VB6 technique that we
have been able to again employ since VB2005. Also notice that even though we are passing a string, we did not use the Auto
verb because Alias is already specifying the ANSI version of the method, as indicated by the “ A” post-fixed to the alias. See
the note below for additional details.
The Buffer string can be better-defined under VB6 by instead using a normal string set to a length of 25:
Dim Buffer As String
Buffer = Space(25) ' set Buffer to 25 16-bit Unicode characters.

The above VB6 subroutine can be manually upgraded cleanly and without issues to VB.NET as follows:
Declare Function GetUserName Lib "advapi32.dll" Alias "GetUserNameA" (
ByVal lpBuffer As String, ByRef nSize As Integer) As Integer
Sub GetUser()
Dim UserName As String = Space(25) 'Init receiving buffer. GetUserName writes max 24 chars + 1 more for a null terminator.
' NOTE: We could have alternatively specified a VB.NET Fixed String using: <VbFixedArray(24)> Dim UserName As Char().
' Notice further that we set the upper bounds index to 24, which specifies 25 characters, when counting the index 0 Char.
GetUserName(UserName, 25) 'Null terminates returned string, so no need to track returned length integer.
MsgBox(UserName) 'display result. Null-terminated strings end at their embedded null.
End Sub
 Page 200

NOTE: When string parameters are involved, we are quite often advised to insert Auto between Declare and Function.
However, due to an Alias also being declared, we should avoid including Auto because we are already informing the
compiler of the method within the DLL to employ, instead of leaving it to the compiler to determine it automatically. For
example, the above Pinvoke could have alternatively been legally declared as “Declare Auto Function GetUserName Lib
"advapi32.dll" (ByVal lpBuffer As String, ByRef nSize As Integer) As Integer ”.

In most cases, VB.NET best handles passing strings to Pinvokes when we also optionally declare how
we want strings to be passed. Under VB.NET, the characters within the string (by default 16-bit
Unicode) can optionally be specifically marshaled to a Pinvoke as Unicode, ANSI, or in a platform-
dependent manner. ANSI was the default on Windows 95/98 and Windows Millennium Edition. All later
Windows platforms default to Unicode. Hence, we could specify ANSI or Unicode in place of the Auto
verb. Maintaining Unicode, however, also executes faster because no type conversions are needed.

Strings Used in Interfaces, such as Structures.


If an interface uses multiple string parameters that require more than one string format, such as one
being ANSI and another being Unicode, we can properly marshal those rare situations by prefixing each
parameter string variable in the appropriate format using a “ <MarshalAs(UnmanagedType.ANSI)>” or
“<MarshalAs(UnmanagedType.Unicode)>”, such as in “<MarshalAs(UnmanagedType.ANSI)> ByVal myString As String ”.
For these cases, the following table shows the marshaling options used for string data types when passed
as unmanaged interface arguments. The MarshalAsAttribute attribute provides three UnmanagedType
enumeration values that can be used to marshal string support for unmanaged COM interfaces:
Enumeration type Description of unmanaged format
UnmanagedType.BStr (default) A COM-style BSTR with a prefixed 32-bit integer text byte length and 16-bit Unicode characters.
UnmanagedType.LPStr A pointer to a null-terminated array of ANSI (8-bit) characters (.NET type Byte).
UnmanagedType.LPWStr A pointer to a null-terminated array of Unicode (16-bit) characters (.NET type Char).

NOTE: This table applies to strings. However, for StringBuilder buffers, the only type options it is allowed are
UnmanagedType.LPStr and UnmanagedType.LPWStr, for 8-bit and 16-bit strings, respectively.
NOTE: The BSTR format was just covered in “String Format Changes between VB6 and VB.NET”. Also, the BSTR type
used by VB.NET for Win32 Interop specifies just its 4-byte Integer byte-size field followed by a Unicode string. It does not
include the actual VB6 BSTR pointer field that VB6 used to point to the start of the Unicode string data. Further, be aware
that a BSTR can contain embedded 16-bit nulls, because its length is determined by its leading byte-length Integer field, not
by the Unicode string's default terminating null character.

Strings Used in Platform Invocations.


Platform invocation, the technique used to consume unmanaged DLL functions in an unmanaged DLL
library from the .NET framework (also referred to as API Calls), copies string arguments, converting
from the .NET Framework format (Unicode) to the platform unmanaged format. Prior to VB2005,
strings were treated as immutable and were not copied back from unmanaged memory to managed
memory when the invocation returned, except when used with the VBByRefStr option, as shown in the
examples below. By the way. this was actually an Object-Oriented Programming Language infraction,
which is why it was “fixed on the sly.”
For virtually all Pinvoke declarations, we are usually best served by simply declaring the Pinvoke with
the Auto verb, as described earlier. Indeed, we may seldom, if ever have to worry about the following
special marshalling functionality, but in cases where we do, the following table lists the marshaling options
for strings when marshaled as a method argument of a Pinvoke Signature. The MarshalAsAttribute attribute
provides several UnmanagedType enumeration values to marshal such strings:
Enumeration type Description of unmanaged format
UnmanagedType.AnsiBStr A COM-style BSTR with a 32-bit prefixed text byte length and a string of 8-bit ANSI characters.
UnmanagedType.BStr A COM-style BSTR with a 32-bit prefixed length and 16-bit Unicode characters (already refrenced).
UnmanagedType.LPStr A pointer to a null-terminated array of 8-bit ANSI characters. This is typical for most Pinvokes that uses
8-bit text characters (already refrenced).
UnmanagedType.LPTStr (default) A pointer to a null-terminated array of platform-dependent (platform-defined) characters.
UnmanagedType.LPWStr A pointer to a null-terminated array of Unicode characters (this is the format used by .NET strings). This
is typical for Pinvokes that employ 16-bit text strings (already refrenced).
 Page 201

Enumeration type Description of unmanaged format


UnmanagedType.TBStr A COM-style BSTR with a 16-bit prefixed length and platform-dependent characters.
UnmanagedType.VBByRefStr A value that enables VB.NET to change a string in unmanaged code and have the results reflected in
managed code. This value type modifier is supported only for platform invoke (there is no need for it in
in-app fully managed processes). Be sure to also specify the associated string ByRef, as in
“<MarshalAs(UnmanagedType.VBByRefStr)> ByRef myString As String”. NOTE: As of
VB2005, simply passing a string ByVal to a Pinvoke will also perform this same task, duplicating the
exact same functionality performed by VB6, thus tightening VB6/VB.NET platform compatibility.

NOTE: The above table applies only to strings. For StringBuilder buffers, the only options allowed are LPStr, LPTStr, and
LPWStr. However, I would recommend that we instead simply resort to declaring our Pinvoke using Declare Auto, if we are
not sure, and let the platform determine how to configure it. If that issues an unhandled exception warning, then the usual
solution for that is to try LPWStr. If that fails, it is probably due to the very rare instances where the Pinvoke is trying to
modify a string pointed to by one of its parameters. In this case, resort to invoking it ByRefStr or ByVal, or resort to a ByVal
StringBuilder buffer.
The following type definitions show the correct use of the MarshalAsAttribute for platform invocations:
Class StringLibAPI 'HINT: For knowledgeable developers, this little class puts INCREDIBLE POWER into your hands!
'Just remember: “With great power comes great responsibility.”
Public Declare Sub PassLPStr Lib "StringLib.Dll" (<MarshalAs(UnmanagedType.LPStr)> str As String)
Public Declare Sub PassLPWStr Lib "StringLib.Dll" (<MarshalAs(UnmanagedType.LPWStr)> str As String)
Public Declare Sub PassLPTStr Lib "StringLib.Dll" (<MarshalAs(UnmanagedType.LPTStr)> str As String)
Public Declare Sub PassBStr Lib "StringLib.Dll" (<MarshalAs(UnmanagedType.BStr)> str As String)
Public Declare Sub PassAnsiBStr Lib "StringLib.Dll" (<MarshalAs(UnmanagedType.AnsiBStr)> str As String)
Public Declare Sub PassTBStr Lib "StringLib.Dll" (<MarshalAs(UnmanagedType.TBStr)> str As String)
End Class

NOTE: The "Auto" verb is used to let the target platform determine the suitable character width (ANSI or Unicode). This is
much easier than employing sometimes complex marshalling commands on each individual string, as shown above. My rule
of thumb is this: If Strings are passed to, or as a result from a Pinvoke, then just add the “Auto” verb to the declaration
and ignore adding marshaling commands. However, we should avoid using the Auto keyword in declarations that will also
specify the actual method to invoke by an Alias. Consider this:
Declare Function GetWindowsDirectory Lib "kernel32" Alias "GetWindowsDirectoryA" (
<MarshalAs(UnmanagedType.VBByRefStr)> ByRef lpBuffer As String,
ByVal nSize As Integer) As Integer

Dim S As String = New String(Chr(0), 260) 'init receiving buffer.


Dim I As Integer = GetWindowsDirectory(S, 260) 'now get Windows directory to S and its data length to I.

If we were to insert Auto after Declare in the above Pinvoke Signature, the ‘S’ string variable would contain garbage after
the Pinvoke execution. This is because Auto would tell the compiler to convert the 16-bit Unicode string to 8-bit ANSI, but by
also specifying the ANSI method to invoke using the Alias, “GetWindowsDirectoryA” in this case, this would also tell it to
convert the text back to Unicode, because it knows the text is now ANSI, so garbage can result. I tend to beat this topic to
death in this tome because it has caused a great many developers uncounted hours of frustration.

Strings Used in Structures.


Strings are valid members of Structures, though StringBuilder buffers are not. The table below shows
the options available for managing string data types of fields that may be required by certain Pinvokes.
Again, we may seldom need to worry about implementing this information because in most-all instances
such specialized massaging is not needed. But if we ever do, the MarshalAsAttribute attribute provides
several UnmanagedType enumeration values to marshal strings to a field in a specific manner:
Enumeration type Description of unmanaged format
UnmanagedType.BStr A COM-style BSTR with a prefixed 32-bit byte length and 16-bit Unicode characters (already referenced).
UnmanagedType.LPStr A pointer to a null-terminated array of 8-bit ANSI characters (already referenced).
UnmanagedType.LPTStr A pointer to a null-terminated array of platform-dependent characters (already referenced).
UnmanagedType.LPWStr A pointer to a null-terminated array of Unicode characters (default; already referenced).
UnmanagedType.ByValTStr A fixed-length array of characters; the type (8-bit ANSI or 16-bit Unicode) is determined by the character set of
the containing structure. Default is 16-bit Unicode. This should be used for Win32 Types TCHAR and BYTE.

NOTES: The ByValTStr type is used for inline, fixed-length character strings within a structure. Other types typically apply
to string references contained within structures that are passed by reference. The CharSet argument of the StructLayout
attribute that is applied to the containing structure determines the character format of strings in structures. The following
example structures contain string references and inline strings, as well as ANSI, Unicode, and platform-dependent
characters. Also note that the SizeConst marshalling parameter actually specifies the number of Bytes in the string, not
Characters; hence, the byte count should be double the character count for 16-bit Unicode if we require Wide strings.
 Page 202

Here is how to use MarshalAsAttribute to different string formats within the same structure:
<StructLayout(LayoutKind.Sequential, CharSet:= CharSet.Ansi)>
Structure StringInfoA ' ANSI (8-bit)
<MarshalAs(UnmanagedType.LPStr)> Public f1 As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst := 128)> Public f2 As String
End Structure
'********************************************************************************************
<StructLayout(LayoutKind.Sequential, CharSet:= CharSet.Unicode)>
Structure StringInfoW ' Unicode (16-bit)
<MarshalAs(UnmanagedType.LPWStr)> Public f1 As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst := 256)> Public f2 As String
<MarshalAs(UnmanagedType.BStr)> Public f3 As String
End Structure
'********************************************************************************************
<StructLayout(LayoutKind.Sequential, CharSet:= CharSet.Auto)>
Structure StringInfoT ' Generic (platform-defined bit size)
<MarshalAs(UnmanagedType.LPTStr)> Public f1 As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst := 128)> Public f2 As String
End Structure

Fixed-Length String Buffers.


Sometimes we must send a fixed-length string to unmanaged code to be modified. Simply passing a
string ByRef does not work with interop strings because the CLR cringes at passing managed text to
unmanaged code, so it passes a string as immutable (technically a disposable clone). The fast solution is
to pass the string ByVal, emulating VB6 Pinvoke behavior, because of the fact that a string is an object
(a reference type), and when passing a reference to a string as a parameter, whether ByRef or as a copy
ByVal, it will still send its value, which is a pointer to the base address of the string (I consider this
undue confusion over ByRef/ByVal string interop by VB.NET an unnecessary flaw in VB.NET design, but
I understand the intent).
Microsoft recommends passing a StringBuilder, a System.Text member, as the argument instead of a
string. A StringBuilder’s data can be modified by the callee because it is a class, and when we pass an
object as a parameter (Oh? Say a String?), what is passed is a pointer to its data, which is its string.
A StringBuilder can also be initialized to a fixed length. When passed to a Pinvoke, we should account
for one character of this capacity being reserved for a null string terminator, which most Pinvoke strings
expect. The following example demonstrates how a StringBuilder can be initialized to a fixed length,
passed to a modifying Pinvoke, and how to copy the returned modified text to a string variable:
Imports System.Text ' enable easy access to the StringBuilder member.

Public Class Win32API ' Note below that ByVal for Strings STILL sends the text pointer.
Public Declare Auto Sub GetWindowText Lib "User32.Dll" (
ByVal hwnd As IntPtr, ByVal sText As StringBuilder, ByVal nMaxCount As Integer)
End Class

Public Class Window


Friend hWnd As IntPtr = Form1.Handle ' Friend handle to a Window.
Public Function GetWindowTitle() As String ' Invoke using myStr = Window.GetWindowTitle().
Dim sb As New StringBuilder(260) ' define string buffer of 260 characters for Pinvoke.
Win32API.GetWindowText(hWnd, sb, sb.Capacity) ' send sb object ByVal, which sends a pointer to its text buffer.
Return sb.ToString() ' Convert StringBuilder array to String text using its ToString() method.
End Function
End Class

Dealing with “As Any” in Pinvoke Signatures.


Using the As Any type in a Declare-style signature is not supported without marshaling by VB.NET.
Variables of type As Any were used in VB6 as a way to pass a variable that was typically either a String
or an Integer value of Zero; we could replace this VB6 usage by declaring two separate forms of the
same Pinvoke, one with longs and one with strings. For example, the VB6 GetPrivateProfileString
signature has a parameter lpKeyName declared as type As Any, as shown below:
 Page 203

Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA"


(ByVal lpApplicationName As String,
ByVal lpKeyName As Any,
ByVal lpDefault As String,
ByVal lpReturnedString As String,
ByVal nSize As Long,
ByVal lpFileName As String) As Long

Under VB.NET, we can replace the error-generating "As Any" in the above Pinvoke Signature with two
separate overloaded versions; one that will accept a string, and another that will accept an integer:
Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA"
(ByVal lpApplicationName As String,
ByVal lpKeyName As String,
ByVal lpDefault As String,
ByVal lpReturnedString As String,
ByVal nSize As Integer,
ByVal lpFileName As String) As Integer

Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA"


(ByVal lpApplicationName As String,
ByVal lpKeyName As Integer,
ByVal lpDefault As String,
ByVal lpReturnedString As String,
ByVal nSize As Integer,
ByVal lpFileName As String) As Integer

NOTE: We are allowed to optionally include the “Overloads” term at the start of the above two declarations, but this is not
allowed in non-instantiated (non-inheritable) classes, such as Modules. However, because they can be declared as the above
in all cases, I tend to simply forget about the “Overloads” term, so I will have one less warning issued to me.
When we use a Null (zero) as the lpKeyName parameter, the compiler will automatically select the
version of GetPrivateProfileString that will accept an Integer without us needing to think about it.
NOTE: Actually, under VB.NET, we could get by using only the above string version, because when we want to pass a null
value rather than a string, we can pass VbNullString or Nothing (same value) instead, which accomplishes the same thing.
If we wanted to define them separately under VB6, and doing it this way will also mean that the
functions will upgrade to VB.NET without a warning, we can define two separate declarations with two
separate names; GetPrivateProfileStringStr and GetPrivateProfileStringInt:
' version using a String
Declare Function GetPrivateProfileStringStr Lib "kernel32" Alias "GetPrivateProfileStringA"
(ByVal lpApplicationName As String,
ByVal lpKeyName As String,
ByVal lpDefault As String,
ByVal lpReturnedString As String,
ByVal nSize As Long,
ByVal lpFileName As String) As Long

' version using an Integer


Declare Function GetPrivateProfileStringInt Lib "kernel32" Alias "GetPrivateProfileStringA"
(ByVal lpApplicationName As String,
ByVal lpKeyName As Long,
ByVal lpDefault As String,
ByVal lpReturnedString As String,
ByVal nSize As Long,
ByVal lpFileName As String) As Long

Using the AsAny Marshalling Parameter.


Now that I have said that “As Any” cannot be used in Pinvoke Signatures without marshaling (note the
sly use of words), and I had gone through all the time and trouble to explain the proper ways to address
this problem through overloading (the “classic” VB As Any solution was actually a hack that allowed it
to address overloaded methods declared in a DLL in the first place, simply because it did not support
method or operator overloading), we can in fact pass parameters AsAny (note the lack of a space in the
term), as an unmanaged type in the System.Runtime.InteropServices namespace. Although I much
prefer the proper overloading solution, we might just differ with me in this philosophy. Hence, for
completeness, here is how to pass lpKeyName in the GetPrivateProfileString Pinvoke As Any (be sure
to declare “Imports System.Runtime.InteropServices ” at the top of the file):
 Page 204

Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA"


(ByVal lpApplicationName As String,
<MarshalAs(UnmanagedType.AsAny)> ByVal lpKeyName As Object,
ByVal lpDefault As String,
ByVal lpReturnedString As String,
ByVal nSize As Integer,
ByVal lpFileName As String) As Long

NOTE: Because type Object is a generic type in VB.NET, being the base type of all objects and value types can be
automatically corralled into an object wrapper, passing any reference or value to the above lpKeyName parameters will be
accepted. This, however, is in fact the danger of As Any, and why it should be completely dropped from usage: it is able to
pass anything unchecked. This is primarily why I much prefer the overloading method, which also has the significant added
benefit of extremely strong type-safety, and the overloading technique also reduces bugs and the chance for our code to fail.
NOTE: I know, I know... You, unlike the rest of us petty mortals, know what you are doing when using As Any. But how many
times has that line been followed by, “...What the...?”

List of All .NET UnmanagedType Members.


The following list features all the UnmanagedType enumerations used by .NET. Using these members of
UnmanagedType in a <MarshalAs(UnmanagedType)> declaration marshals parameters or fields to
unmanaged code as described on MSDN (search for “unmanagedtype enumeration msdn”):
UnmanagedType Member Description
AnsiBStr An ANSI character string that is a length-prefixed single byte. We can use
this member on the String data type.
AsAny A dynamic type that determines the type of an object at run-time and
marshals the object as that type. This member is valid for platform invoke
methods only.
Bool A 4-byte Boolean value (true != 0, false = 0). This is the Win32 BOOL
type.
BStr A Unicode string that is a length-prefixed double byte. We can use this
member, which is the default string in COM, on the String data type.
ByValArray When the MarshalAsAttribute.Value property is set to ByValArray, the
SizeConst field must be set to indicate the number of elements in the
array. The ArraySubType field can optionally contain the
UnmanagedType of the array elements when it is necessary to differentiate
among string types. We can use this UnmanagedType only on an array
that whose elements appear as fields in a structure.
ByValTStr Used for in-line, fixed-length character arrays that appear within a
structure. The character type used with ByValTStr is determined by the
System.Runtime.InteropServices.CharSet argument of the
System.Runtime.InteropServices.StructLayoutAttribute attribute
applied to the containing structure. Always use the
MarshalAsAttribute.SizeConst field to indicate the size of the array.
Currency A currency type. Used on a System.Decimal to marshal the decimal value
as a COM currency type instead of as a Decimal.
CustomMarshaler Specifies the custom marshaler class when used with the
MarshalAsAttribute.MarshalType or
MarshalAsAttribute.MarshalTypeRef field. The
MarshalAsAttribute.MarshalCookie field can be used to pass additional
information to the custom marshaler. We can use this member on any
reference type.
Error A native type that is associated with an I4 or an U4 and that causes the
parameter to be exported as an HRESULT in the exported type library.
FunctionPtr An integer that can be used as a C-style function pointer. We can use this
member on a Delegate data type or on a type that inherits from a Delegate.
HString A Windows Runtime string. We can use this member on the
System.String data type.
 Page 205

UnmanagedType Member Description


I1 A 1-byte signed integer. We can use this member to transform a Boolean
value into a 1-byte, C-style bool (true = 1, false = 0).
I2 A 2-byte signed integer.
I4 A 4-byte signed integer.
I8 An 8-byte signed integer.
IDispatch A COM IDispatch pointer (Object in Microsoft Visual Basic 6.0).
IInspectable A Windows Runtime interface pointer. We can use this member on the
Object data type.
Interface A COM interface pointer. The Guid of the interface is obtained from the
class metadata. Use this member to specify the exact interface type or the
default interface type if we apply it to a class. This member produces the
same behavior as UnmanagedType.IUnknown when we apply it to the
Object data type.
IUnknown A COM IUnknown pointer. We can use this member on the Object data
type.
LPArray A pointer to the first element of a C-style array. When marshaling from
managed to unmanaged code, the length of the array is determined by the
length of the managed array. When marshaling from unmanaged to
managed code, the length of the array is determined from the
MarshalAsAttribute.SizeConst and
MarshalAsAttribute.SizeParamIndex fields, optionally followed by the
unmanaged type of the elements within the array when it is necessary to
differentiate among string types.
LPStr A single byte, null-terminated ANSI character string. We can use this
member on the System.String and System.Text.StringBuilder data types.
LPStruct A pointer to a C-style structure that we use to marshal managed formatted
classes. This member is valid for platform invoke methods only.
LPTStr A platform-dependent character string: ANSI on Windows 98, and
Unicode on Windows NT and Windows XP. This value is supported only
for platform invoke and not for COM interop, because exporting a string
of type LPTStr is not supported.
LPWStr A 2-byte, null-terminated Unicode character string.
R4 A 4-byte floating-point number.
R8 An 8-byte floating-point number.
SafeArray A SafeArray, which is a self-describing array that carries the type, rank,
and bounds of the associated array data. We can use this member with the
MarshalAsAttribute.SafeArraySubType field to override the default
element type.
Struct A VARIANT, which is used to marshal managed formatted classes and
value types.
SysInt A platform-dependent, signed integer: 4 bytes on 32-bit Windows, 8 bytes
on 64-bit Windows.
SysUInt A platform-dependent, unsigned integer: 4 bytes on 32-bit Windows, 8
bytes on 64-bit Windows.
TBStr A length-prefixed, platform-dependent char string: ANSI on Windows 98,
Unicode on Windows NT. We rarely use this BSTR-like member.
U1 A 1-byte unsigned integer.
U2 A 2-byte unsigned integer.
U4 A 4-byte unsigned integer.
U8 An 8-byte unsigned integer.
VariantBool A 2-byte, OLE-defined VARIANT_BOOL type (true = -1, false = 0).
VBByRefStr This enables VB to change an unmanaged string to reflect results in
managed code. This value is only supported for platform invoke.
 Page 206

VB6 User-Defined Types Compared to VB.NET Structures.


VB6 User-Defined Types had a primitive structure, but they were quite straightforward in that they did
exactly what they were expected to do – to combine a group of variable fields into a combined object
space. Further, each member was stored in memory in the same sequential order they had been declared
within their source code. Even further, if fixed strings were declared within it, UDTs could also maintain
a uniform size, due to fixed strings, just like normal strings, being scalar types under VB6, and so they
were fully embedded within the object's space, which was perfect for processing fixed-size records in
random access files. In addition, VB6 stored all its UDT members using natural alignment, where each
field type aligned itself on multiples of its own size from the start of their shared object space, itself
beginning on a multiple of its CPU Integer size. What this boils down to is that byte fields could align
anywhere, but 16-bit values, like Integers and strings. aligned on 2-byte boundaries, 32-bit values
aligned on 4-byte boundaries, and 64-bit values aligned on 8-byte boundaries. This schema provided the
most optimal conservation of storage space in the absence of byte-packing.
Finally, UDTs, like classes, are assigned to a portion of memory space, but Structures are more limited
in that they are an abstracted class, and so their object space is simply assigned, not instantiated. What
this means is that Structure fields are not auto-initialized upon creation like classes are, setting each field
variable to a default value, such as zero or Nothing, and a field cannot be assigned a default value/
Consider the following imaginary VB6 structure:
Friend Structure myStruct
Friend Byt As Byte '1 Byte (8-bits) of storage.
Friend Int As Integer '2 Bytes (16-bits) of storage.
Friend Lng As Long '4 Bytes (32-bits) of storage.
Friend Dbl As Double '8 Bytes (64-bits) of storage.
End Structure

The above structure would be aligned in memory like this under storage rules for VB6, where X
represents memory byte locations that are assigned to a field variable, and 0 represents memory byte
locations that are not, being unassigned memory gaps between fields:
'VB6 Sequential Fields with Object Natural Alignment Layout (Microsoft Default).
Byt: X 0 'alignment on 8-bit boundary, followed by an unallocated gap due to the following 16-bit field.
Int: X X 'alignment on 16-bit boundary.
Lng: X X X X 'alignment on 32-bit boundary.
Dbl: X X X X X X X X 'alignment on 64-bit boundary.

As can be seen, only the memory area associated with the Byt field is followed by unassigned space.
Indeed, it would be best to assign Byt to the end of the list. But, if we did that, then there would be two
unassigned bytes after Int, because the Lng field would have to align on a 32-bit boundary, and so on.
Now consider the following imaginary VB.NET structure, set up for identical Win32 Interop:
<StructLayout(LayoutKind.Sequential)>
Friend Structure myStruct 'LayoutKind.Sequential forces fields to be sequenced in their source-code-defined order.
Friend Byt As Byte 'Byte under VB6.
Friend Srt As Short 'Integer under VB6.
Friend Int As Integer 'Long under VB6.
Friend Dbl As Double 'Double under VB6.
End Structure

By default, it would be aligned in memory like this, were it to follow storage rules for VB6, using the
compiler marshaling instruction <StructLayout(LayoutKind.Sequential)>:
'Object Natural Alignment Layout (Microsoft Default).
Byt: X 0 'alignment on 8-bit boundary, followed by an unallocated gap due to following 16-bit field.
Srt: X X 'alignment on 16-bit boundary.
Int: X X X X 'alignment on 32-bit boundary.
Dbl: X X X X X X X X 'alignment on 64-bit boundary.

If this object data were to be aligned on 32-bit architectural boundaries, it would be stored like this:
'Object 32-bit Architectural Alignment Layout (with LayoutKind.Sequential,Pack:=4 marshaling parameters used).
Byt: X 0 0 0 'alighment on 32-bit boundary.
Srt: X X 0 0 'alighment on 32-bit boundary.
Int: X X X X 'alighment on 32-bit boundary.
 Page 207

Dbl: X X X X X X X X 'alignment on 32-bit boundary.

And if this object data were to be aligned on 64-bit architectural boundaries, it would be stored like this:
'Object 64-bit Architectural Alignment Layout (with LayoutKind.Sequential,Pack:=8 marshaling parameters used).
Byt: X 0 0 0 0 0 0 0 'alignment on 64-bit boundary.
Srt: X X 0 0 0 0 0 0 'alignment on 64-bit boundary.
Int: X X X X 0 0 0 0 'alignment on 64-bit boundary.
Dbl: X X X X X X X X 'alignment on 64-bit boundary.

The best possible field layout for this structure, even for a class, and how .NET itself would internally
store the above structure members if the LayoutKind.Sequential marshaling instruction was not used, is
shown below, as if it had been declared in the following order:
'Object Natural Alignment Layout (Microsoft Default).
Friend Dbl As Double 'Dbl: X X X X X X X X 'largest member stored first on a multiple of itself (64-bits).
Friend Int As Integer 'Int: X X X X 'this field properly aligns on a multiple of itself (32-bits).
Friend Srt As Short 'Srt: X X 'this field properly aligns on a multiple of itself (16-bits).
Friend Byt As Byte 'Byt: X 'this field properly aligns on a multiple of itself (8-bits).

NOTE: Be aware that if we assign class objects to fields, to include unmarshaled strings or unmarshaled arrays, the only
part of them that will be assigned to the field data would be as 4-byte IntPtrs, but fixed strings or arrays will be embedded.
However, we can get around gaps that could cause data corruption in data streams by making allowances
for anticipated gaps. One quick solution is to add another Byte field that is unused, save for reserving
space so that the next declared field within the structure or class falls on a multiple of its size. Consider
the above VB.NET structure that is amended with a filler Byte field to address the noted memory gap:
<StructLayout(LayoutKind.Sequential)>
Friend Structure myStruct
Friend Byt As Byte 'Byte under VB6.
Private Und As Byte 'Filler field to allow for the memory gap that would otherwise follow Byt.
Friend Srt As Short 'Integer under VB6.
Friend Int As Integer 'Long under VB6.
Friend Dbl As Double 'Double under VB6.
End Structure

In the above declaration, all of its object space is now reserved under natural alignment, like so:
'Object Natural Alignment Layout (Microsoft Default with LayoutKind.Sequential marshaling parameter used).
Byt: X
Und: X
Srt: X X
Int: X X X X
Dbl: X X X X X X X X

In memory, this is stored in sequential byte positions like the following, where B represents an
individual byte of Byt, U represents an individual byte of Und, S represents individual bytes of Srt, I
represents individual bytes of Int, and D represents individual bytes of Dbl:
BUSSIIIIDDDDDDDD 'no memory gaps present in this structure's object space.

Most Win32 Pinvoke Signatures assume that each field will also be stored in their declared sequence,
but on byte boundaries, called single-byte packing, meaning that each field is laid out in memory
immediately after the previous, with no gaps, though, on rare occasions, we saw a byte or 16-bit integer
member declared ‘Undefined’ to address natural boundary gaps. This potential VB6 incompatibility for
the most part was invisible to the VB6 user because the structures expected by Pinvoke Signatures are
also laid out in such a way that each field would naturally fall on its expected boundary, which is why
string fields were laid out in fixed sizes, byte fields followed or filled in other fields, all so there will be
no inadvertent gaps in memory. Further, most windows-based Pinvokes exhibited explicit padding so
that they were fully compatible with languages that use natural alignments and lacked single-byte
packing capabilities, like VB6.
In more practical terms, consider the following imaginary C++ Pinvoke structure:
Typedef struct userType {
BYTE a; // 1-byte unsigned char.
short b; // 2-byte integer.
int c; // 4-byte integer.
BYTE d[4]; // 4-byte unsigned byte array ([0] – [3]).
 Page 208

Char e[8]; // 8-byte signed byte ANSI string.


};

With the default single-byte packing of C++, the structure’s data is expected to be, and is, laid out as in
the following illustration, where each character represents a single byte of memory space:
abbccccddddeeeeeeee = 19 bytes of data.

Now, were we to convert this structure to a VB6 User Defined Type, such as the following:
Type userType
a As Byte ' 1-byte unsigned byte.
b As Integer ' 2-byte integer.
c As Long ' 4-byte integer.
d(3) As Byte ' 4-byte array (element 0-3).
e As String * 8 ' 8-byte ANSI string.
End Type

The above VB6 UDT, because it always uses natural alignment, but not supporting single-byte packing,
would store itself in memory and would pass itself to a Pinvoke in the following format:
aØbbccccddddeeeeeeee = 20 bytes of data.

Notice in the above layout that a null byte is inserted after the ‘a’ byte value. This is because the second
entry, b, a 2-byte integer, must align on even boundaries following natural boundary rules. The best
solution to this problem is to place the variables with the smallest size, such as byte and string, after the
larger sizes, or arrange them so that each consecutive field will be placed on the appropriate boundary
naturally. But this is not always practical, especially when the Pinvoke is not one of our own design.
VB6 UDTs gave way to VB.NET Structures, and with the name change came many powerful features,
but also architectural differences. Apart from needing to declare the storage class of each data field
under VB.NET, another important difference is that the fields of the structures may be stored internally
in a different, but 100% predictable, more memory-efficient order, mostly to avoid boundary gaps, than
they are specified, unless marshaling coerces its layout. Also, VB.NET does not actually support fixed-
length strings or dynamic arrays without being specially instructed to do so, again, through marshaling.
The System.Runtime.InteropServices namespace is the solution to these formatting differences. By
prefixing “<StructLayout(LayoutKind.Sequential)>” to the structure declaration, the fields are passed to a
Win32 Pinvoke in the structure-declared order. By including “ CharSet:=CharSet.Auto” to the attributes,
VB.NET’s Unicode Strings can convert to 8-bit ANSI or remain Unicode as required by any Pinvoke
using the structure. More, a feature VB.NET attributes has that VB6 lacked was the ability to specify
that a structure should be internally stored and passed using single-byte packing by including “ Pack:=1”
in its attributes. All this yields “<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto, Pack:=1)> ”.
If a Win32 ANSI Pinvoke uses 8-bit ANSI characters rather than the default 16-bit Unicode, we would
need to declare a fixed-length string that is 8 1-byte characters long, not the standard 8 2-byte Unicode
characters long. For that we can also turn to marshaling (alternatively, the fixed-length string, as shown
previously in this document, can be addressed by pre-pending it with the much simpler attribute
“<VBFixedString(nnn)>” marshal command to specify a byte length). Finally, because fixed-sized arrays
are not normally allowed in structures, we can also resort to marshaling again to address that issue.
Putting this all together, the following VB.NET structure is compatible to the C/C++ structure:
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto, Pack:=1)>
Structure userType
Private a As Byte ' 1-byte unsigned byte.
Private b As Short ' 2-byte integer.
Private c As Integer ' 4-byte integer.
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=4)>
Private d() As Byte ' 4-byte array. Marshaling allows fixed arrays, OR pre-pend with the easier <VBFixedArray(4)>.
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=8)>
Private e As String ' 8-byte string. Alternatively, pre-pend with the easier <VBFixedString(8)>.
End Structure 'TRIVIA: These Marshaling directives allow string and array data to be embedded in the structure/class.

NOTE: I normally specify the actual string type being used, such as Ansi or Unicode instead of Auto, but that is just me, not
a requirement). Even with Structures, passing dynamic strings in structures that have attributes such as the above to a
 Page 209

Pinvoke can be relied upon to do the proper conversions, as needed, because VB.NET will internally construct a properly
sized memory block for transport. However, if CharSet:=CharSet.Auto is used to define the structure, its string data will
normally be stored as Unicode, and then converted to ANSI before transport only if the Pinvokes expects ANSI, in which case
it will later need to convert it back to Unicode if it is expected to return data. By specifying the actual expected data type
instead of using Auto, such as Ansi or Unicode, thus matching expected types of the Pinvoke we are employing with the
structure, we can minimize string format conversions, and as a consequence increase overall program execution speed.
NOTE: One Unmanaged Type provided by the System.Runtime.InteropServices namespace is AsAny. VB6 users early on
complained (read as whined) to no end that VB.NET “lacked” the ability to pass parameters as any type to a Pinvoke, which
can accept various types. The whole point to adding“As Any” to VB6 was as duct-tape, albeit a very unsafe one, to get
around the fact that it was not possible to implement function overloading, where differing parameter types could be used
with same-named methods. But now that VB.NET allows them to perform function overloading, users complained they no
longer had their work-around duct-taped hack. However, VB2005 quietly added this unsafe parameter modifier. See the later
article, Passing Data Between Protected and Unprotected Memory Under VB.NET, on page 212 for more details. Although
this will fall on deaf ears, using As Any is unsafe because method data integrity cannot be guaranteed. But then, I keep
asking for C#'s Unsafe Blocks and Pointers be added to VB.NET. I can do it, but my method is a (legal) interop hack.
There are even more powerful differences between Structures and UDTs. Structures are Class-like. Like a
Reference Class, a Structure can also contain methods and properties, to include constructors ( Sub New).
However, even though a structure can have constructors, they must be declared with parameters, because the
default constructor is reserved and system-defined. Unlike Reference Classes, Structures are Abstract
Classes, and as such they cannot be inherited, nor can they inherit from other classes or implement interfaces.
There is never such a thing as an empty Structure. Although Structures are an abstract class, defining them in
either of the following manners will generate the same structure, though the first will have un-initialized
content, and the second will use the New verb, not to instantiate a new reference, but rather to initialize its
content to zeroes or Nothing, which is useful if members will be modified without first being initialized:
Dim A As myStruct ' this is the typical declaration protocol.
Dim A As New MyStruct ' this is also handy when VB.NET nags at us that a field is accessed before content has been declared.

Another advantage to the latter syntax is the ability to optionally use properties and parameterized
constructors, such as “Dim A As New MyStruct("Initial String")”, considering the following Structure:
Public Structure myStruct
Private m_myStr As String ' actual field data for structure.

Public Property myStr As String ' properties to manipulate our field data within our structure.
Get
Return m_myStr
End Get
Set(ByVal value As String)
m_myStr = value
End Set
End Property

Public Sub New(ByVal InitialString As String) ' optional constructor to initialize _myStr string...
m_myStr = InitialString 'otherwise m_myStr will be set to Nothing.
End Sub
End Structure

The above shows some of the possibilities of structures, though practically, the above would be best
served by a Class. However, we can copy an individually modifiable instance of a structure, whereas
class variables are actually reference pointers, so changing one reference will in fact change their object
target, so all other reference variables assigned to it will also ‘see’ the change. Structures, on the other
hand, being abstract classes, are treated as value types, and so a change to one copy will in no way affect
any other copies, principally because each structure variable points to its own copy of its data.
The major outward differences between Classes and Structures are that all declarations inside a Structure
are by default Public (a Dim is treated as Public, unlike a Field Dim in a module, which is treated as
Friend). Classes are reference types, but Structures are value types. A Structure’s primary purpose is to
group data, whereas a Class’s primary purpose is to group data and use methods to protect and operate
on that data. Two Class variables can point to the same Object, but two Structure variables are always
isolated from each other, holding individual copies of their data. If we need the ability to change the data
through one variable and access the changed values through another variable, then we must use a class.
 Page 210

Consider the following code, employing the above myStruct Structure definition:
Dim A As myStruct ' create a copy of myStruct as variable 'A'.
Dim B As New myStruct("Hello") ' create a copy of myStruct as variable 'B', and assign it new text.
Dim C As myStruct ' create a copy of myStruct as variable 'C'.
Dim D As myStruct = B ' create a copy of myStruct as variable 'D', and assign it a copy of the data in 'B'.

C = A ' copy data from 'A' to 'C'.


A.myStr = "Testing" ' Assign new data to 'A'.
D.myStr = "Goodbye" ' Assign new data to 'D'.

Debug.Print("A={0}", A.myStr) ' Display individual results.


Debug.Print("B={0}", B.myStr)
Debug.Print("C={0}", C.myStr)
Debug.Print("D={0}", D.myStr)

The output from this short program is:


A=Testing
B=Hello
C=
D=Goodbye

What this little experiment clarifies is that Structures are value types, not reference types. As we can see,
Structure A is declared with no initial value and Structure B is declared with “Hello” as its initial text.
Next Structure C is declared just like A, then Structure D is declared and set to B. Next, Structure C is
set to A, Structure A is subsequently set to “Testing”, and Structure D is set to “GoodBye”. Finally, the
resulting contents of these 4 structures are printed to the Debug Output.
Notice the output. Even though Structure D was set to B, Structure B did not change when Structure D
was later changed to “Goodbye”, but remained being set to “Hello”. Likewise, Structure C was set to
Structure A, but when Structure A was changed to “Testing”, Structure C remained blank.
What this shows us is that unlike Classes, Structures are not reference types. Had they been, then
Structures B and C would have reflected what had been assigned to Structures D and A, respectively.

Proving VB6 User Defined Type Members are Properties, Not Variable Fields.
One more very important difference between VB6 User-Defined Types and VB.NET Structures is in how
their Field variables are actually stored. VB.NET fields are just that, Fields. They act just like variables.
Are VB6 UDT Fields defined the same way? NO! They are actually compiled as Properties! Everyone
always looks at me like an arm just sprouted from my head when I tell them this little fact, but once I
prove it, they later swagger around, drawling that they “knew it all along”.
We can prove it simply by passing a UDT text field to a method ByRef, which is supposed to alter it.
Consider the following segment of VB6 code that is supposed to alter a string field of a UDT:
'VB6 test to demonstrate that VB6 UDT fields are Properties.
Public Type Struct
Dim Txt As String 'Field Dim within a UDT is treated as Public.
End Type

'sample method to prove VB6 Fields are Properties, not normal (VB.NET-style) variable fields.
Public Function TestString() As String
Dim myStruct As Struct 'new instance of Structure.
myStruct.Txt = "abcdefg" 'assign lowercase data.
Call MakeUpper(myStruct.Txt) 'convert to uppercase (we HOPE!).
Debug.Print myStruct.Txt 'display result in Debug Output.
TextString = myStruct.Txt 'return result.
End Function

method to convert passed parameter to uppercase.


Sub MakeUpper(ByRef StrData As String) 'ByRef to alter string parameter.
StrData = UCase(StrData) 'parameter to uppercase.
End Sub

When TestString is invoked, it creates a Struct Structure and assigns its data to the lowercase text
“abcdefg”. it then passes this string to MakeUpper as a ByRef parameter to alter the string and converts it
 Page 211

to uppercase. Next, it will Debug.Print the field for verification. Next, it assigns that field to the
TestString function for return. Finally. it returns the result to its outward invoker.
If we invoke TestString, what do we think the result in the Debug Output will be? Will it be “ ABCDEFG”
or “abcdefg”? The result will be “abcdefg”. Why? Because MakeUpper altered only a copy of the
MyStruct.Txt field. It did not alter the Txt Field itself!
Let us prove it. Following is a VB.NET version that openly treats the Txt field as a Property:
'VB.NET Verification to demonstrate that VB6 UDT fields are Properties.
Public Structure Struct
Private _Txt As String 'hide (protect) string field in structure. Using Dim would make it Public.

Public Property Txt As String 'Use Property to duplicate VB6 UDT Field processing.
Get
Return _Txt 'return copy of data.
End Get
Set(ByVal value As String)
_Txt = value 'assign data.
End Set
End Property
End Structure

'sample method to prove VB6 Fields are Properties, not normal (VB.NET-style) variable fields.
Public Function TestString() As String
Dim myStruct As Struct 'new instance of Structure.
myStruct.Txt = "abcdefg" 'assign lowercase data.
MakeUpper(myStruct.Txt) 'convert to uppercase (we hope!).
Debug.Print myStruct.Txt 'display result in Debug Output.
Return myStruct.Txt 'return result.
End Function

method to convert passed parameter to uppercase.


Sub MakeUpper(ByRef StrData As String) 'ByRef to alter string parameter.
StrData = StrData.ToUpper 'parameter to uppercase.
End Sub

Now, think about what happens when we invoke this version of TestString. I am really surprised a mob
of angery VB6 Aficionados are not all worked up that VB.NET does not also work incorrectly!
 Page 212

Passing Data Between Protected and Unprotected Memory Under VB.NET.


(Based on an article by James Mimeault) Frequently developers have to cope with data file formats
containing fixed length records. In like wise, processing them as fixed-length records is typically the
fastest and most straightforward manner of processing such files because of the simple fact that the data
has a known size. In most cases the RtlMoveMemory Pinvoke method is the perfect tool for these
situations because it virtually eliminates the alternative of having to byte-parse this data. This article will
demonstrate how to use the Windows Pinvoke RtlMoveMemory (normally expressed in VB Pinvokes by
the more common name of CopyMemory) to populate Class objects using Strings as input.
Normally we do not need to perform the seemingly redundant task of first copying data to an intermediate
location before we can place it where we really want it. This would be like getting directions in Maine: “You
can’t get there from here. You have to go somewhere else first.” However, passing unmanaged data directly
into managed .NET class space presents us with a rare and rather unique problem: if we try to employ
RtlMoveMemory to copy unmanaged data to a .NET class, an exception error will occur because we would
be trying to use an unmanaged COM function to write to the managed Heap space of a class (this is allowed,
however, with an abstract class, such as a Structure, which is stored on the unprotected program Stack, not
in the protected Heap). To copy data to Heap space, we must achieve this using managed memory methods.
A popular solution is to transfer the data with an unmanaged method to a neutral, non-Heap buffer accessible
to both managed and unmanaged methods, and then use a managed method to marshal that data into the
actual target structure in Heap memory. This is a technique that most VB.NET gurus will advise. They do so
because it does in fact work. We will first explore this useful technique, just so we can understand this
popular solution, but afterward I will demonstrate a much better solution I devised that executes much faster,
uses less resources, and it does it all within managed memory without employing unmanaged methods.
We will wade through this whole process by starting with a new Console Application. You can follow this
using VB.NET on your own system. To avoid wandering off topic, when we get to the code blocks, feel free
to simply copy and paste them into your program. But even so, please be sure to read their comments.
Step 1: Create a Test Project named “CopyMemorySample”.
1. Open Visual Studio .NET and select the 'New Project' option.
2. Select Visual Basic Projects from the Project Types list if it is not already selected.
3. Select Console Application from the Templates list (console apps are fun and great for experimental endeavors).
4. Type CopyMemorySample in the Name box.
5. Click the OK button or just hit the Enter key.
6. The project template will open and we will see the module 'Module1' in the Code Viewer. It is normally a good idea
to give descriptive names to our code components, but for this small project we will just leave Module1 alone.
Step 2: Add a “WINAPI” Class to Store Our CopyMemory Pinvoke Declaration.
1. From the Main Menu ribbon, select Project.
2. Select 'Add Class' from the Project menu.
3. Type WINAPI in the Name box.
4. Click the Open button or just hit the Enter key.
5. Copy or type in the following code so that our WINAPI Class will now look like this:
Public Class WINAPI
' Declare the Windows Pinvoke Signature CopyMemory. Note that all variables are ByVal.
' pDst is passed ByVal because we want CopyMemory to go to that location and modify
' the data that is pointed to by the IntPtr, and NOT alter the IntPtr value itself.
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal pDst As IntPtr, ByVal pSrc As String,
ByVal ByteLen As Integer)
End Class

Step 3: Add a “clsTest” Class Formatted to Receive a Fixed 60-Char Record of Test Data.
1. From the Main Menu ribbon, select Project.
2. Select 'Add Class' from the Project menu.
3. Type clsTest in the Name box.
4. Click the Open button or just hit the Enter key.
5. Now, add the following Class declaration that contains our fixed-length data, consisting of three strings; the first 10
characters long (ID), the second 20 characters long (Name), and the third 30 characters long (m_chAddress):
 Page 213

Imports System.Runtime.InteropServices ' interop Namespace used.

' StructLayout(LayoutKind.Sequential) dictates that the values within the structure/class will be
' guaranteed to be stored sequentially in memory. In other words; ID first, Name second, and
' Address third in one sequential block of memory (otherwise, .NET might try to use a more efficient layout).
<StructLayout(LayoutKind.Sequential)>
Public Class clsTest
' MarshalAs informs the Compiler of the intended type and size of the data we are trying to work with.
' SizeConst specifies the data as the count of Char array elements, not bytes as <VbFixedString()> will do,
' however, keep in mind that marshaling these unmanaged types will assume 16-bit Unicode characters.
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=10)> Public ID As Char() '<- first field (10 Chars, 20-bytes)
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=20)> Public Name As Char() '<- second field (20 Chars, 40-bytes)
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=30)> Private m_chAddress As Char() '<- third field (60-bytes)

' No problem with properties either, because only the field members are counted as object data.
Public Property Address() As Char()
Get
Return m_chAddress
End Get
Set(ByVal Value As Char())
m_chAddress = Value
End Set
End Property
End Class 'clsTest: Note that because a Class and Structure are kissing cousins, this could have been a Structure

6. Switch the Code View back to Module1 by clicking the Module1 tab at the top of the Code View window.
7. Add the function definition below to Module1, below the empty Sub Main block:
' copy a string of data from 'Source' into a class's properties at 'Target' using the Win32 CopyMemory() Pinvoke.
Private Function CopyStringToClass(ByVal Source As String, ByVal Target As Object) As Object

' If the Target (a pointer to a .NET Class) is nothing, immediately return nothing.
If Target Is Nothing Then Return Nothing

' Declare an IntPtr that will hold a memory address. Do not try to copy a string from unmanaged memory into
' a class property using the Win32 CopyMemory() Pinvoke because that WILL trigger an exception error.
Dim p_objTarget As IntPtr ' use this to store the address of general system-allocated Heap space

Try
' Invoke AllocHGlobal() to allocate enough memory on the system heap for the data members of the
' 'Target' object. AllocHGlobal() will return a memory pointer to this new storage space. The SizeOf()
' function will only return the size of the data members of the Target class, NOT its program code.
p_objTarget = Marshal.AllocHGlobal(Marshal.SizeOf(Target))

' To copy Target to p_objTarget on the local heap, use StructureToPtr, as used in the following line of
' code. This would be useful for Pinvokes that are expecting to receive PRE-FILLED data members.
'Marshal.StructureToPtr(Target, p_objTarget, True) ' copy a structure (class data) to the p_ObjTarget address.

' Use CopyMemory() to take the data from the unmanaged source pointer (Source) and copy it to the allocated
' block of memory on the heap that is pointed to by p_ObjTarget (which, coincidentally is the same
' size as the source string. We had defined set-aside sizes for members in our clsTest class definition).
WINAPI.CopyMemory(p_objTarget, Source, Marshal.SizeOf(Target)) ' this will copy only DATA members.

' Now, tell the Marshaler to copy the data that is now in intermediate memory (the result of CopyMemory()),
' pointed to by p_objTarget, into our .NET instance of the Target class object (of type clsTest) on the Heap.
Marshal.PtrToStructure(p_objTarget, Target) ' Copy the data at the pointed address to the Target class data.

' Always free the intermediate memory that was allocated, otherwise we will create a memory leak.
Marshal.FreeHGlobal(p_objTarget) 'Free the intermediate storage data associated with the provided address.

Catch ex As Exception
' An exception could occur if the system is out of memory and the block of memory could not be set
' aside for you (this would be a RARE beast, unless you had 100s of thousands or millions of these).
ReportExError(ex)
End Try

' Send the result back to the invoker for printing.


Return Target

End Function 'CopyStringToClass

What We Have So Far Achieved.


 The Function CopyStringToClass takes a String and a Class Object instance as parameters.
 Notice that we did not need to declare a variable of class 'WINAPI' and initialize it using the New keyword. This is
because this class does not have Procedure-Level Fields (variables) defined. We will just use its defined Pinvoke.
 We declare a variable of type 'IntPtr' to hold a memory address of the data space set aside in intermediate memory.
 Using a Try...Catch block, we will trap any exceptions.
 Using the System.Runtime.InteropServices.Marshal class, we allocate a 'Target'-sized block of intermediate
memory and assign the returned pointer to that memory to p_objTarget, which is an IntPtr object.
 Page 214

 We use the member function CopyMemory of the WINAPI class to copy the string into the intermediate memory
that we set aside for clsTest (the 'Target'-sized block in the heap space).
 We invoked the System.Runtime.InteropServices.Marshal.PtrToStructure method to copy the data from the
intermediate memory that is pointed to by p_objTarget into the Target class object’s data area in the Heap space.
 We invoked the System.Runtime.InteropServices.Marshal.FreeHGlobal method to free the block of intermediate
memory that we allocated for the copy. If this is not done, it will result in a memory leak.
 We return the now-populated class back to the invoker.

Step 4: Finishing With Startup Code.


Now add the following code to Module1, to declare our Sub Main method and its dependent subroutines:
Imports System.Runtime.InteropServices

Module Module1
Sub Main()
' 60 char string for sample data, consisting of 10 (ID), then 20 (Name), and then 30 character segments.
Dim strSource As String = "0123456789" &
"Donald J Trump " &
"1600 Pennsylvania Ave. "
' Sampling index --------> 123456789012345678901234567890.
' Declare the Target parameter for the CopyStringToClass function, which will also receive the return value.
Dim clsTarget As New clsTest 'instantiate an instance of the clsTest class and reference it in clsTarget.

' Copy the strSource string to the target Class (note the returned Object must be cast back to a clsTest type).
clsTarget = DirectCast(CopyStringToClass(strSource, clsTarget), clsTest) 'cast returned object to type clsTest.
PrintResultsOfCopy(strSource, clsTarget) 'invoke reporting method to show results (see its definition below).
End Sub 'Main
'----------------------------------------------------------------------------
' Ask the user to press <Enter> and return.
'----------------------------------------------------------------------------
Private Sub PromptForEnter()
Console.WriteLine()
Console.Write("Press <Enter> to continue...")
Console.ReadLine()
End Sub 'PromptForEnter
'----------------------------------------------------------------------------
' let's see what we got from our Source
'----------------------------------------------------------------------------
Private Sub PrintResultsOfCopy(ByVal Source As String, ByVal Value As clsTest)
Console.WriteLine(ControlChars.CrLf & "Results of CopyMemory for clsTest:")
Console.WriteLine("Source String = " & Source)
Console.WriteLine("clsTest.ID = " & CType(Value.ID, String)) 'cast this text of Char() to String.
Console.WriteLine("clsTest.Name = " & CType(Value.Name, String)) 'cast this text of Char() to String.
Console.WriteLine("clsTest.Address = " & CType(Value.Address, String)) 'cast this text of Char() to String.
' Prompt for user input.
PromptForEnter()
End Sub 'PrintResultsOfCopy
'----------------------------------------------------------------------------
'exception display, prompt and return.
'----------------------------------------------------------------------------
Private Sub ReportExError(ByVal e As System.Exception)
Console.WriteLine()
Console.WriteLine("Exception Caught: " & e.Message)
Console.WriteLine()
' Prompt for user input
PromptForEnter()
End Sub 'ReportExError

End Module

Step 5: Running the CopyMemorySample Program.


1. In the Visual Studio.NET IDE, press the <F5> key.
2. A Console window will appear and show ux that our code works if we entered it exactly as shown above.
3. When we are done, press <Enter> to close the program.
Source String = 0123456789Abraham Lincoln 1234 Pennsylvania Blvd.
clsTest.ID = 0123456789
clsTest.Name = Donald J Trump
clsTest.Address = 1600 Pennsylvania Ave.

NOTE: The MSDN documentation for “<MarshalAs(UnManagedType.ByValTStr)>” shows that it can be used to marshal
strings for Classes and Structures, it does not work exactly as we might expect. One character will be “lost” for every string
simply because, for strings that are declared as strings, its final character position will be overwritten by a null terminator to
mark the end of the string. This is the reason why we instead used Char Arrays (As Char()) in our examples, which are
directly compatible with strings (because, internally, strings are just arrays of type Char), and VB.NET is kind enough to
account for treating Char Arrays as String objects, even if they do not contain null terminators.
 Page 215

Optimizing this Code to Run Much Faster.


In reviewing this code, we see that we are allocating a memory buffer that is sized to 60 16-bit Unicode
characters of space, occupying 120 bytes of memory. The SizeOf method returns the number of bytes of
the data members. The trick here is that many users get hung up on the SizeConst attribute parameters in
the clsTest class, assuming it specifies a byte count (I did as well the first time I rode this bronco).
However, we are marshaling an array, so in this case SizeConst designates an array element count, and
we allocated 3 strings (Char arrays) that totals 60 characters. So we should expect 120 bytes. For
example, if we added this test line, “Dim Cnt As integer = Marshal.SizeOf(Target)”, at the head of the
Try block in the CopyStringToClass function, which is prior to conversion, the returned byte count in
Cnt would report Target’s size to be 120 bytes. Use these little developmental verification tests often!
NOTE: Unlike the VbFixedArray attribute, whose size constant designates the Upper Bounds of the array, the ByValArray
attribute size constant specifies the total number of array elements, including its zeroth member.
We then copy our 60-character string to that 120-byte memory buffer. RtlMoveMemory will internally
receive the 60 Unicode characters through the .NET CLR. We finally marshal it to the structure and then
release our allocated storage resource.
It occurred to me at this point that we could instead use just a simple Byte array in place of the allocated
neutral storage. This would be a means of standing in for the Unicode text data that the PtrToStructure
method is expecting to work with. If we do this, we can completely eliminate the non-heap memory
allocation and de-allocation process, and consequentially also completely eliminate the need to use the
CopyMemory Pinvoke, and we can keep everything within managed memory.
Now consider the following replacement CopyStringToClass function that does not use the
CopyMemory Pinvoke, nor does it employ heap allocation/deallocation. This results in a much faster
process. It does, however, use a form of internal allocation/deallocation in the added VarPtr function
using managed memory methods, but only as needed to get the memory address of the Bytes array,
' copy a string into a class's properties WITHOUT using CopyMemory
Private Function CopyStringToClass(ByVal Source As String, ByVal Target As Object) As Object
If Target Is Nothing Then Return Nothing 'If the Target is nothing, immediately return nothing.
Dim Bytes() As Byte 'Buffer to hold our 16-bit Unicode string.
Try
' 60 char string for sample data, consisting of 10, then 20, and then 30 character segments.
' Convert unicode string to a Unicode string of bytes, because PtrToStructure will also expect 16-bit characters.
' Convert Unicode so that each 2 consecutive bytes holds 1 16-bit Char.
Bytes = System.Text.Encoding.Unicode.GetBytes(Source & ChrW(0)) 'add a 16-bit null terminator.
' Get the address of the Byte() array as an IntPtr value and tell the Marshaler to copy
' the data in the Bytes array into our instance of the Target class object (clsTest).
Marshal.PtrToStructure(VarPtr(Bytes), Target)
Catch ex As Exception
ReportExError(ex) 'An exception occurs if the system is out of memory.
End Try
Return Target 'Send the results back for printing.
End Function

'VB.NET version of VB6 VarPtr, ObjPtr, StrPtr, etc.


Public Function VarPtr(ByVal o As Object) As IntPtr 'Use Object as a 'catch all' universal data type
Dim GC As GCHandle = GCHandle.Alloc(o, GCHandleType.Pinned) 'Get a trackable handle and pin the obj address
VarPtr = GC.AddrOfPinnedObject 'Get address of pinned object (the variable's data address)
GC.Free() 'Free the allocated space used
End Function

This version may not appear to run differently, but it runs faster and eliminates a lot of overhead.
This new code also shows us a practical reason for using the VB.NETversion of the VB6 VarPtr
function. This tiny function can be used to return the base address of scalar types, strings, structures,
class objects, and arrays – there is no need for differently-named functions, as VB6 required. This one
template handles them all. It also demonstrates how to perform C-style pointer-based memory access
from VB.NET. My secret weapon is now out! Power users can use it to copy class or structure field
blocks hyper fast! Armed with VarPtr and with the StructureToPtr and PtrToStructure methods, which
are Managed versions of RtlMoveMemory, we will be chortling like a 1930s B-Movie mad scientist. Be
sure to wear a white lab coat for effect, Having a friend hunching over, saying “Yeth, Mathtah!” helps.
 Page 216

Comparing VB System I/O Commands; Dir Vs. FSO Vs. FileStreams.


VB.NET contains a vast repertoire of functionality that we will discover and rediscover as we continue
to explore deeper and ever deeper within the trees of namespaces under such headings as My, System,
Microsoft, Environment, and so on. One glorious gem is the file/folder Input/Output (I/O) functionality
found in System.IO. I have been told over and over by users that scanning directory folders using the
VB6-style Dir command was good enough for everyone, or that they now use FSO (a File System
Object) to do “super-fast” file/directory I/O. But like any developer, Microsoft is often very good in their
language development and at making their functionality work faster and faster (I wish they could have
said the same about their Office 2007 product, which seemed to run as if it were written in DOS Basic).
VB started with the Dir function to scan the directory structure of a system. The next step was file
streams using the Windows Scripting Host Object Model (WSHOM) available to VB6 through its
WSHOM.OCX (this actually redirects control to IWshRuntimeLibrary.DLL), or, alternatively, using the
Microsoft Scripting Runtime through Scripting.DLL. But now we have what I like to call Turbo
Streaming, using the System.IO namespace of the .NET Framework, where the code seems to be written
much closer to the machine.
As an experiment, I wrote three subroutines in a Console application that used each method to scan
through every folder on my old C:\ drive. All I was doing was scanning each and every folder for sub-
folders, recursing my code to access every single folder on the drive. The results were very surprising.
Using DIR took 5 minutes and 18.227 seconds to scan through 124,352 folders (about 391 folders per
second). Using FSO, it took a very impressive 1 minute and 0.848 seconds, which I think justified my
own transition to that method while developing code under VB6 (about 2,072 folders per second). But I
was really impressed with the System.IO approach, from which I just used the GetDirectories method.
This returned an array of strings, where each array element represents the full drive path to a sub-folder.
It took all of 20.113 seconds. And that is for 124,352 folders (about 6,217 folders per second). What
follows is the console application I wrote to test it. We will also have to add a COM reference to
“Windows Scripting Host Object Model” in the project properties so that the FSO method will work, or,
if we prefer, instead add a COM reference to “Microsoft Scripting Runtime”:
Option Explicit On
Option Strict On

Imports IWshRuntimeLibrary ' This requires a COM reference to 'Windows Scripting Host Object Model.
'Imports Scripting 'Alternatively, you can add a COM reference to 'Microsoft Scripting Runtime.

Module Module1
'
' test 3 diffrent directory scanning methods.
'
' The first uses the old standby VB6-style DIR() command.
' The second uses a FileSystemObject defined in the IWshRuntimeLibrary.DLL (used by WSHOM.OCX under VB6).
' The third uses the SYSTEM.IO.GetDirectories command.
' NOTE: I pre-scanned the entire drive to make the tests fair!
Sub Main()
Console.WriteLine("Scanning using Old Dir I/O").
Dim startTime As Date = Now
ODoDir("C:\")
Console.WriteLine("Old Dir process time: {0}", Now.Subtract(startTime).ToString())

Console.WriteLine("Scanning using FSO Dir I/O").


startTime = Now
fsoDoDir("C:\")
Console.WriteLine("FSO Dir process time: {0}", Now.Subtract(startTime).ToString())

Console.WriteLine("Scanning using New Dir I/O").


startTime = Now
NDoDir("C:\")
Console.WriteLine("New Dir process time: {0}", Now.Subtract(startTime).ToString())
End Sub

'*******************************************************
' process directory scan using VB6-style DIR() command.
'*******************************************************
Private Sub ODoDir(ByVal dPath As String)
Dim path As String = dPath ' add trailing slash to dir path if not there.
If Right(dPath, 1) <> "\" Then
 Page 217

path &= "\"


End If

Dim col As New System.Collections.Generic.List(Of String)


'get all sub-folders
Dim S As String = Dir(path & "*.*", vbDirectory) 'check for subfolders.
Do While CBool(Len(S)) 'found one?.
If Left(S, 1) <> "." Then 'if not "." or ".."
If CBool(GetAttr(path & S) And FileAttribute.Directory) Then
col.Add(path & S) 'found one, so add to temp list.
End If
End If
S = Dir() 'check for another.
Loop
' now scan subfolders (using a collection prevents recursing from walking over.
'the Dir() process this current level is using.
With col
Do While CBool(.Count)
ODoDir(.Item(0)) 'recurse through subfolders.
.RemoveAt(0) 'remove entry from list.
Loop
End With
col = Nothing 'break connection to collection object.
End Sub

'*******************************************************
' process directory scan using VB6-style FSO object.
'*******************************************************
Private Sub fsoDoDir(ByVal dPath As String)
Dim path As String = dPath ' add trailing slash to dir path if not there.
If Right(dPath, 1) <> "\" Then
path &= "\"
End If

Dim FSO As New FileSystemObject


' get all sub-folders.
Try
For Each fld As Folder In FSO.GetFolder(path).SubFolders
fsoDoDir(fld.Path)
Next
Catch ' ignore errors. Skipping normal processing is enough.
End Try
FSO = Nothing
End Sub

'*******************************************************
' process directory scan using SYSTEM.IO.GetDirectories.
'*******************************************************
Private Sub NDoDir(ByVal dPath As String)
Dim path As String = dPath ' add trailing slash to dir path if not there.
If Right(dPath, 1) <> "\" Then
path &= "\"
End If

'get all sub-folders


Try
Dim dcol() As String = System.IO.Directory.GetDirectories(path)
If dcol Is Nothing Then Exit Sub
For Each S As String In dcol
NDoDir(S)
Next
Erase dcol
Catch ' ignore errors. Skipping normal processing is enough.
End Try
End Sub
End Module

Running using Debug \ Start without debugging (Ctrl + F5) (this allows a pause for any key at the end
of the run), we get this (times can vary on each run, depending on what is going on in the background):
 Page 218

NOTE UPDATE: As of VB2010, the Dir command has been redesigned to run almost as fast as File System Objects

Releasing COM Objects from Memory Under VB.NET.


(Based on information from Microsoft sources) We open an Excel instance in a VB.NET app and then close it,
but later discover that Excel actually hangs around in memory once we had finished and our main
application had closed. This is technically called a memory leak. Is it really such a big deal? YES!
In VB6, “Set Obj = Nothing” released memory allocated to an object. So why does this not always work
in VB.NET? It is because our VB.NET application might be referencing COM objects, which function
outside the managed space of the .NET Framework. As such, the .NET Garbage Collector (GC) will not
be responsible for, nor can it handle this kind of memory management. We can always rely on the
Garbage Collector to perform all the requisite memory management tasks for our managed code, but
unmanaged resources often require explicit, developer-written cleanup because COM cleanup is not
always uniform, so there is no way to invoke a simple cleanup invocation to address all possible
scenarios. Thus, to find a solution, we must first understand how .NET interacts with COM objects.
When referencing COM objects from our VB.NET application, it automatically wraps all unmanaged
COM references within something called an RCW (Runtime Callable Wrapper). If we examine the
RCW documentation, we will see that an RCW must be explicitly released within our code as part of a
developer-written cleanup process through the "Marshal.ReleaseComObject" method, available in the
System.Runtime.InteropServices namespace. Consider the following coding situation, which it will also
require a .NET reference to “Microsoft.Office.Interop”, if we have MS Office installed:
Imports Microsoft.Office.Interop 'we must be sure to first add a .NET reference to 'Microsoft.Office.Interop.
Public Class Form1
Dim objEx As New Excel.Application 'create a new Excel instance.
Dim objWB As Excel.Workbook = objEx.Workbooks.Add() 'add a workbook to the Excel object.
Dim objWS As Excel.Worksheet =
DirectCast(objWB.Worksheets.Add(), Excel.Worksheet) 'Add a worksheet to the workbook.

' display the Excel spreadsheet and then fill it with some data.
Private Sub btnOpenExcelApp_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnOpenExcelApp.Click
objEx.Visible = True 'show the Excel spreadsheet application to the user.
For intRow As Integer = 1 To 7 'insert some weekday data to the spreadsheet.
objWS.Range("A" & intRow.ToString).Value = Date.Today.AddDays(intRow).ToString("dddd")
Next
End Sub

' close down our Excel application (To do this, first bring this app back up in front of Excel to access the Close button).
Private Sub btnClose_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnClose.Click
Try 'Overlook errors in case the user had already closed the Excel app.
objWS.Delete() 'delete the worksheet object (we will be prompted for saving changes).
objWB.Close() 'close the workbook object.
objEx.Quit() 'exit the Excel application.
Catch 'ignore any errors.
End Try
Me.Close() 'close this application.
End Sub

When our application exits, the Garbage Collector will release the RCW construct data from the
memory used by VB.NET, but because the actual COM objects are operating in unmanaged memory,
the memory they occupy is not released, as we may discover if we happened to open up the Task
Manager and look in the Process list, where we will find to our surprise that Excel.exe is still running.
To address this issue, each time we close an external COM application, our code should invoke the
"ReleaseComObject" Marshal command with each COM object, at least in a Dispose or Finalize
method. This is because the RCW maintains a reference count that is incremented every time a COM
interface pointer is mapped to it. The ReleaseComObject method decrements the references count of a
RCW. When the reference count reaches zero, the runtime releases all its references to the COM object:
Imoirts System.Runtime.InteropServices 'place this line somewhere within this source file.
Protected Overrides Sub Finalize()
Marshal.ReleaseComObject(objWS) 'Decrement reference counts to each COM object.
Marshal.ReleaseComObject(objWB) 'Decrement order is not important, But I like...
Marshal.ReleaseComObject(objEx) ' to back out in the reverse order of going in.
MyBase.Finalize() 'Be sure to do MyBase.Finalize() last.
End Sub
 Page 219

By adding the above lines after we finish using our COM objects, it forces the release of the objects from the system space,
because each of their reference counts will become zero. Also, because the Garbage Collector will always make a final run at
the end of an application, before it closes, it will also automatically remove the RCW code from managed memory.

Tools for REALLY Looking Under the Hood of Visual Basic .NET.
Once we start feeling comfortable with VB.NET, we may start getting curious about what it is doing
behind the green curtain. We can do so for a modest fee ($95 USD; a version that can decode 3rd party
code is $199 USD) by using Redgate Software’s .NET Reflector (http://reflector.red-gate.com). It works with
all versions of the .NET Framework from 1.0 upward. Free updates are also available.
Although there was no real documentation for using the
beta version (the free version, no longer available), simply
by watching the demo video, I was able to use it with ease.
Perhaps the only real advice I would like to pass on for
reference, other than my strong recommendation to get
.NET Reflector, is that the Reflector’s file list is dependant
upon DLL, MCL, and EXE files. As such, we can perform
a File / Open on our own DLL or EXE files generated by
the VB.NET compiler, but more importantly – those of the
.NET Framework, and using the syntax for our choice of
Visual Basic, C#, C++, Delphi, Google Chrome, or IL.
They are at C:\Windows\Microsoft.NET\Framework.
Depending on the Version of the Framework that the
feature we want to explore exists within, we will find it within one of its “V”-version subfolders; each
representing a version of .NET Framework (V1.0, V1.1,
V2.0, V3.0, V3.5, and so on).
For example, the VB6 Compatibility Library is found in
version .NET Framework 2.0, so I explore the V2.0 folder
(mine is currently named V2.0.50727, which includes the
build), finding Microsoft.VisualBasic.Compatibility.dll.
Finding these namespace files is not as difficult as we
might think. If we know the namespace we want to find –
simply browse through these few version folders. If
features are enhanced or superseded by a subsequent
version of the .NET Framework, then we will find the
namespace DLL in the newer version folder and removed
from the version it may have been introduced in (this is
why the V1.0 and V1.1 folders now have so little content).
We can then explore this namespace to our heart’s content.
But the best part is, we can disassemble the code of the
Framework and see what makes it “tick”. Suppose we
wanted to see how the FixedLengthString class of the VB6
namespace was constructed. I would browse through the
sub-folders of Microsoft.VisualBasic.Compatibility for
Microsoft.VisualBasic.Compatibility.VB6, then explore its
sub-folders for FixedLengthString, right-click it, then
select Disassemble. By selecting Analyze instead, we can
explore what system DLL’s are used by them – .NET code
 Page 220

is packed with Pinvokes to the Win32 API. Also be sure to check out the Support subfolder for many
more commands.
Suddenly, on the right side of my application, I see a list of
methods, properties and fields that are encapsulated by the
FixedLengthString class.
Almost too tempting to resist, at the bottom we will see a click-
on option entitled “Expand Methods”.
By selecting it, suddenly we see a workable approximation of
the source code (in whichever language we selected from the
dropdown in the toolbar) that will define that class, such as the
VB.NET code listing that is shown below:

Public Class FixedLengthString


' Methods
Public Sub New(ByVal MaxChars As Integer)
If (MaxChars <= 0) Then
VB6Errors.RaiseError(5, Resources.GetResourceString("Argument_InvalidValue1", "MaxChars", "", ""))
End If
Me.m_nMaxChars = MaxChars
Me.m_strValue = New String(ChrW(0), Me.m_nMaxChars)
End Sub

Public Sub New(ByVal MaxChars As Integer, ByVal InitialValue As String)


Dim num As Integer = 0
If (MaxChars <= 0) Then
VB6Errors.RaiseError(5, Resources.GetResourceString("Argument_InvalidValue1", "MaxChars", "", ""))
End If
Me.m_nMaxChars = MaxChars
num = Strings.Len(InitialValue)
If (num < MaxChars) Then
Me.m_strValue = (InitialValue & New String(" "c, (Me.m_nMaxChars - num)))
ElseIf (num = MaxChars) Then
Me.m_strValue = InitialValue
Else
Me.m_strValue = Strings.Left(InitialValue, Me.m_nMaxChars)
End If
End Sub

Public Overrides Function ToString() As String


Return Me.Value
End Function

' Properties
Public Property Value() As String
Get
Return Me.m_strValue
End Get
Set(ByVal Value As String)
If (Strings.Len(Value) >= Me.m_nMaxChars) Then
Me.m_strValue = Strings.Left(Value, Me.m_nMaxChars)
Else
Me.m_strValue = (Value & New String(" "c, (Me.m_nMaxChars - Strings.Len(Value))))
End If
End Set
End Property

' Fields
Protected m_nMaxChars As Integer
Protected m_strValue As String
End Class

The great thing about this is that we can quickly switch between languages and see how it would have
been coded in another language. Also, if we are trying to pick up C# as another language, we can load
our EXE application and view it not as the VB.NET program code we wrote it in, but as a C# program,
giving us a leg up on learning how to transition some of our VB.NET skills to C#. What is really
impressive (but perhaps this is because I started my career out as an Assembly Language Developer) is
 Page 221

to view it in IL. That is some seriously tight, compact code. This exercise also points out that in release
code we should employ .NET’s built in Dotfuscator, or a 3rd-party Code Obfuscator.
Upgrading Administrative Rights Checks.
Running applications that require Administrative Privileges.
One thing that was driving me crazy when I first started using
Visual Studio.NET on Windows Vista (and this applies to my
subsequent operating systems), was that some applications that
must access the protected parts of the registry or else add or delete
protected files, especially in protected directory branches, require
administrator rights to do so. Further, in order for our own code to
even consider running if it requires such administrative rights, it
should also be sure to check to see if the current user logged on
actually has administrative rights.
A quick way to ensure an application that requires Administrator
rights will prompt for it, such as a registry-update utility, is to
right-click the file, or a shortcut to it, and then select Properties.
On its Compatibility tab, in the Privilege Level box, place a
checkmark in the “Run this program as an administrator” option.
Of course, to even set this option, we must already have
Administrative privileges.
However, instead of expecting our users to perform this application properties adjustment with their own
copies of your program, we can do it for them, and in-code.
We can very quickly modify our Assembly information so that the little window that asks us if we trust
the application and so allow it to run with administrator privileges will always come up. This is easy to
do. Go to the project’s properties (Project / Properties from the menu), and make sure the Application
tab is displayed. One of the buttons on this tab is labeled “View UAC Settings” (User Account Control)
under VB2008, or “View Window Settings” under VB2010 and later. Select it. In the middle of the
displayed text file is a large comment section (by default it is Green) headed by “UAC Manifest
Options”. We will see three different options offered within the comments:
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />

If we look below this comment area, we will see the first non-comment line:
<requestedExecutionLevel level="asInvoker" uiAccess="false" />

All we need to do, based upon the suggestions in the comments, is change “ asInvoker” to
“requireAdministrator”. Once we have saved and compiled this change, when our application launches it
will request that we enable administrative privileges, all without our user having to do anything.
NOTE: This is one of the last things that we should do, because otherwise we will have to restart Visual Studio the first time
we test-run our app from the IDE (simply restart with Administrative Privileges). Of course, after we have done this, we will
not be re-prompted until after we leave VS and start a new VS session with our app. It is not really a big pain, but it is one I
prefer to avoid as long as possible.
However, having said that, we can also bypass this by simply starting Visual Studio
with Administrative rights. We can do this by right-clicking the Visual Studio icon and
selecting “Run as administrator”. When we do this, Visual Studio will not start right
up, but will first greet us with a User Account Control prompt to select the Continue
button to resume. Doing so then launches Visual Studio. Cancel of course exits
without launching Visual Studio.
 Page 222

Check for the User having Administrative Rights.


Previous to the introduction of VB2005’s gratefully welcomed “My” Namespace, we had to do a few
things to check to see if the current user had administrator privileges. As a result, I had written a small
module that I included in my applications that needed to check this. My original code follows here:
Option Strict On
Option Explicit On

Imports System.Security.Principal

Module modCheckAdminPrivileges
'********************************************************************************
' modCheckAdminPrivileges: check current User for Administrative Privileges
'
' CheckAdminPrivileges(): return True if User has Administrators Privileges
'********************************************************************************
Public Function CheckAdminPrivileges() As Boolean
Dim principal As New WindowsPrincipal(WindowsIdentity.GetCurrent())
Return principal.IsInRole("Administrators")
End Function
End Module

However, with the introduction of the “My” namespace, the test has been reduced to a single expression:
Module modCheckAdminPrivileges
'********************************************************************************
' modCheckAdminPrivileges: check current User for Administrative Privileges
'
' CheckAdminPrivileges(): return True if User has Administrators Privileges
'********************************************************************************
Public Function CheckAdminPrivileges() As Boolean
Return My.User.CurrentPrincipal.IsInRole("Administrators")
End Function
End Module

As can be seen, we could actually transpose the function expression right to within our test, such as “If
My.User.CurrentPrincipal.IsInRole("Administrators") Then MsgBox("You have Privileges!") ”, all without
needing to import System.Security.Principal or instantiate any objects.

Running Applications Requiring Administrative Rights without Being Prompted.


User Account Control (UAC) can help us prevent unauthorized changes to our
computer. It works by prompting us for permission when a task requires
administrative rights, such as installing software, changing settings that affect other
users, or writing to protected memory. It does this by verifying that confirmation is
coming from our physical keyboard or mouse.
However, if dealing with the UAC dialog box gives us too much stress, or simply
is the most annoying thing you have ever encountered in your life on your
computer, we are in fact able to turn this dialog off so that it will always assume
that our answer is YES or ALLOW and that it does have administrator
permission.
Having said that, Microsoft does not recommend we turn UAC off. If we do turn it
off, they also suggest that we turn it back on as soon as possible.
Having read the above, we may already be asking the question, “OK, so get to it! How do we do it?”
There are four ways of doing it: through the Control Panel, through MSCONFIG, through our User
Account Settings, or through editing the Registry using RegEdit.
NOTE: If we do turn UAC off, we will see a constant reminder that it is off in
the notification area of your status bar, describing it as some sort of problem
that must be fixed (I thought the UAC popping up all the time WAS the
problem), which, if opened, will be more than happy to offer us the option to
allow it to turn UAC back on for us.
 Page 223

Disabling User Account Control Through the User Account Settings.


I think the easiest way to do it is through the User Account Settings:
1. Click the Start button .
2. Click your user icon.
3. Click the Turn User Account Control on or off option.
4. Clear the check box for Use User Account Control (UAC) to help protect your computer, and
then click OK.
5. Reboot your system.

. Disabling User Account Control through the Control Panel.


On MSDN, Microsoft usually recommends that we do it through the
Control Panel:
1. Click the Start button .
2. Click Control Panel.
3. Click User Accounts and Family Safety (or click User Accounts, if you are connected to a
network domain).
4. Click User Accounts. (this will take you to the above dialog box).
5. Click the Turn User Account Control on or off option.
6. Clear the check box for Use User Account Control (UAC) to help protect your computer, and then click OK.
7. Reboot your system.

Disabling User Account Control through MSCONFIG.


An easier way to do it is to use MSCONFIG (sadly lacking on Windows XP).
NOTE: MSCONFIG also allows us to easily reboot our system, such as to or away from Safe Mode without having to tap
away on the F8 key like a crazed woodpecker on crack during a reboot in order to bring up the system boot options.
Do the following:
1. Bring up the Run menu from the Start menu (you must have this option enabled in the
Customize Start Menu options – if not, right-click the Start Button, select Properties, Select the
Customize button, then scroll down and put a checkmark in the Run command option). Or,
simply type -R.
2. Enter MSCONFIG.
3. Click the Tools tab.
4. Scroll down to, and click the Disable UAC (Windows Vista) or Change UAC option.
5. Click the Launch button. If Windows 7/8, select the lowest setting and choose OK.
6. Reboot your system.

Disabling User Account Control through Editing the Registry.


The last resort is to modify the registry using the RegEdit utility (Warning – for advanced users only).
1. Before making any changes, you may want to be sure that you have made a valid backup of your registry.
2. Launch RegEdit. If you do not know how to do this, stop right here and use one of the above methods. Like I said, this should be done by people
who know what they are doing.
3. Go to the location: HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System
4. Change the value of the EnableLUA entry from 1 to 0.
5. Exit RegEdit.
6. Reboot your system.

That being said, an option in the lower-right corner on


the Windows 8.1 and later User Account Control dialog
is: “Change how these notifications appear”. If we were
to select it, we would be taken to the User Account
Control Settings dialog box, where we can safely change
its priority level.
 Page 224

Cloning Structures Containing Object References Made E-Z.


An extremely important point that we might sometimes forget about, especially when we are in a rush to
beat a deadline, is that we must take particular care when copying structures containing objects, such as
normal strings, arrays, or any instance brought into existence that was declared “As New”.
We got excited when VB2003 finally allowed us to copy structures using the Equals operator, which
seems to employ methods similar to StructureToPtr or PtrToStructure Marshaling commands, they
being data-safe versions of the Win32 RtlMoveMemory function, to instantly copy whole structure or
class object data blocks. Prior to that, we were forced to manually copy each structure field member one
at a time. VB6 devotees laughed in derision because they could rapidly copy whole structures using the
LSet or CopyMemory commands, and we could not, though they should watch out for potential memory
gaps due to field object alignment issues (refer to VB6 User-Defined Types Compared to VB.NET
Structures on page 206), or if the UDT contained object references, where the objects would not be
copied, but only their references. We just grumbled and buried our faces in our keyboards as we pecked
away, manually copying fields. However, one superior result of manually copying structure members is
that when we encountered object fields, most of us automatically assigned them to the new structure by
cloning them or instantiating new objects, so to ensure data integrity. This manual approach was initially
instituted so that the VB.NET Development Team could first resolve more critical .NET Framework
Common Language Specification compliance issues, before addressing the problem of copying
structures, because in early VB.NET development, their task lists were filled to overflowing.
At the time LSet was reintroduced by duplicating the more logical functionality of String.PadRight, due
to a great many VB6 cum VB.NET users crying big tears and stomping their feet, forcing Microsoft
innovation to be dialed backward, Microsoft had also cleared the table on copying Structures, finally
allowing the much-requested Equals operator to copy them. On that day, many VB.NET developers sent
their VB6 tormentors joke programs that played the SystemSounds.ManiacalLaughter audio clip.
NOTE: This VB.NET development backward movement was much the same as former VB6 users wept String.PadLeft
functionality over into RSet, and totally screwed the pooch by forcing Microsoft to drop the truly innovative binary operators
BitAnd, BitOr, and BitXor back into their Mongo past to again rest on And. Or, and Xor, causing And and Or to again have
both Binary and Logical functionality, which is structurally primitive and also seriously confuses the language.
However, because we are working in an object-oriented environment, we should exercise particular
caution when copying structures using the Equals operator, exactly as VB6 users should exercise caution
with the LSet command. As we boast to those VB6 Aficionados that we can now copy a structure “in just
one command statement”, we should be careful about what that statement is. Why? Because, when we
are copying Structures that contain objects using Equals, are we also being careful about instantiating
new and separate object clones for the receiving structure, now that we are not copying structure
members one at a time, which had given us time to think about the fact that we were copying objects?
Granted, most of us took care to ensure that we added an initialization method to our structures to
properly format any member arrays, strings, or class objects that needed to be instantiated “ As New”
within that structure, or to at least manually declare them from outside that structure.
When we copy a structure to another structure using Equals, the system dutifully copies all members for
us, but it does not create clones of object members; it simply copied their pointers to the new structure,
just like VB6 LSet did, so the new structure members now point to the same objects the source members
point to, meaning that the object members of both structures now point to the very same data. DOH!
Before our VB6 sparring partners get a chance to send that joke app back, perhaps forgetting they face
the exact same issue when they use LSet, rest easy knowing we can still copy a structure cleanly in just
one statement, though it might be best we “forget” to show them the bit of code we wrote to allow our
VB.NET structures to feature a Clone method that enables them to make clones of themselves.
With this E-Z code in place, it will copy the structure to a new variable, but it will also clone any and all
object members, to include full arrays, with no fuss and no muss. It can then be as simple as:
 Page 225

Dim LiteBulb As dumStruct = Gullom.Clone 'Use this format whenever you copy a structure containing instantiated objects.
DataMangler(Gullom.Clone) 'Pass a dummy Clone to a method having a ByRef parameter that would normally modify it.

To achieve this to ensure proper data integrity in copying structures, even under VB6, we will need to:
1. Identify each structure we will need to copy between variables in our application.
2. Determine which of these contain object references, like arrays, As New declarations, or ordinary strings or arrays,
such as those not tagged with marshaling instructions to establish fixed sizes, because an ordinary string or array in
a structure, such as Public Name As String, is stored within Structures as a 4-byte IntPtr object reference.
A. If it contains none of these, they can be safely copied using the Equals operator, or, refer to the brain-dead
Clone method shown after the main clone method below, which can keep our cloning of structures consistent.
B. If they contain objects, or we just want to be darned sure, implement the easy process outlined below.
NOTE: Structure Strings and Arrays set to fixed sizes with a marshal tag, like <VbFixedString(xxx)> or <VbFixedArray(xxx)>
are actually embedded within a structure object, so a copy using the Equals operator will copy these items just fine.
To Add a Clone method to a structure is easy! Consider the following VB.NET sample structure:
Friend Structure dumStruct
Friend myVal As Single 'normal scalar Float. Copying the structure using '=' will duplicate this just fine.
Friend myInt As Integer 'normal scalar Integer. Copying the structure using '=' will duplicate this just fine.
Friend myTag As String 'ordinary string; non-parameterized array (object pointer stored here!).
Friend myArray() As String 'this should be redimensioned to 10 (object pointer stored here!).
Friend myLoc As Location 'this should be set to a NEW Location object (object pointer stored here!).
End Structure

This structure holds 2 scalar fields the Equals operator can copy from a structure just fine, and 3 object
references; myTag, myArray, and myLoc. The 3 object fields will need to be manually copied or Cloned.
Indeed, we should also be sure to include an additional Initialize method to establish the dimensioning
of the array and the Location object instance when we are doing a normal structure assignment. This can
even make Cloning easier, because we can use that method as an object guide for cloning the structure.
First, consider the following Initialize method (this technique is strongly recommended
by Microsoft). which we can add to the bottom of the above dumStruct Structure:
Friend Sub Initialize()
myTag = String.Empty 'make empty string, because string comparisons will bellyache if it is set to Nothing.
Redim myArray(10) 'dimention our array to 11 elements (0-10).
myLoc = New Location 'get a fresh Location object.
End Sub

Of course, to repeat the process of using this Initialize method, as I have outlined elsewhere, we would
declare a variable as that structure, and then invoke its Initialize method:
'use As New to initialize all members to their default values.
Dim LocalYokel As New dumStruct 'As New also prevents the compiler nagging about referencing uninitialized variables.
LocalYokel.Initialize 'initialize object members.

Using this method as a road map, we can construct a Clone method below the Initialize method.
Friend Function Clone() As dumStruct 'make a Clone of THIS copy of the dumStruct structure.
Dim dumStructClone As dumStruct = Me 'we are building a house in-house. OOP RULES! Who needs LSet?
With dumStructClone 'this is the SAME process we did manually, but encapsulated!
' The following 2 lines are not required because the above '=' assignment copied these two scalar fields for us.
'.myVal = Me.myVal – No need. Above '=' did this! 'safely copy variables, in case OCIA weasels are around.
'.myInt = Me.myIntl – No need. Above '=' did this! 'no hardhats or steel-toed shoes needed in this light-duty area.
.myTag = Me.myTag 'E-Z string clone (.NET does it for us because we looked sweaty).
.myArray = DirectCast(Me.myArray.Clone, String()) 'Clone our array. Uses same code as .myArray = Me.myArray.Clone.
.myLoc = DirectCast(Me.myLoc.Clone, Location) 'Clone our Location object. If it lacks a Clone method, consider
' adding it if you wrote the class (E-Z), or if not, subclass it
' and add Cloning (E-Z), because the alternative is to manually
' copy members... which is the process that ALL Cloning uses!
End With
Return dumStructClone 'return with our new cloned masterpiece.
End Function 'Congratulations! You now have the savvy to clone even classes!

NOTE: If our structure contains several scalar variables, we can speed the cloning process up, as I have demonstrated
above, by changing a fresh structure declaration from something like “ Dim dumStructClone As dumStruct ” to “Dim
dumStructClone As dumStruct = Me ”. This will give dumStructClone a complete copy of the source structure, auto-cloning all
its scalar fields, to include any <VbFixedString(xxx)> or <VbFixedArray(xxx)> definitions. Even though using Equals also
copies all its object pointers as well, we can address that afterwards by replacing them with individual cloning statements.
 Page 226

If we would like to add a Clone method to structures that can actually be safely copied using just the
Equals operator because they contain only scalar fields or noted marshaling instructions, such as
<VbFixedString(xxx)> or <VbFixedArray(xxx)>, defining fixed strings or fixed arrays, which Equals can
cleanly copy, yet we still want to simplify our structure copying process to be uniform throughout our
application, we can add the following simple “Clone” method to each of those non-object structures:
Friend Function Clone() As myStruct 'Clone a structure that contains no instantiated object reference members.
Return Me 'Return containing structure and let the compiler auto-clone it for us, as if we used '='!
End Function 'Now go grab a brew from the refrigerator after all this hard and sweaty work!

By returning a reference to the Clone method's parent structure, the compiler will dutifully copy the
structure to the destination; thus “Dim billyBob As myStruct = sashayJane.Clone” is functionally
equivalent to “Dim billyBob As myStruct = sashayJane”.
With this gripped firmly in our paws, whereever we need to make a stand-alone copy of a structure to
other structure variables, we have the power of class and structure cloning in E-Z reach, plus another
experience notch in our belts! Hopefully this will not raise ethics questions in our community, because
we are not trying to modify that which was created perfectly by the Father of The Beginnings.
NOTE: If people truly understood the potential locked within our DNA that the Father of the Beginnings will later awaken
in us, the last thing anyone should ever consider is altering their DNA to “improve” their capabilities, especially when
people want to blend with non-human DNA, creating hybrid Chimeras. After all, I think there was a Great Flood that the
Father used to wipe out those hybrids that were overrunning the Earth (see also The Book of Enoch, Ethiopian edition, for
greater details, which early Hebrews, and even Jesus himself quoted as canon). Further, considering that the Father of
Perdition is trying everything in his illusion of power, through his obsession to once again live a corporeal carnal life, to
corrupt our DNA so to alienate us from our Father, ultimately to bring about our destruction through damnation, all so he
can in the end have Earth for himself. If only he possessed the mental capacity to comprehend how utterly he will fail in his
attempt (he having only a partially developed prefrontal cortex, it forcibly blinds him to such cognition). After all, he does
not, nor is he destined to rule Hell. He is destined only to be a prisoner within it (refer to my free online article, What
Separates Man from Angels and Demons, at https://drive.google.com/file/d/1QS_kBTOgdl8FdfIcrpmIVt3ScYlGq_VP/view?
usp=sharing)
NOTE: See “Adding Run-Time Custom Menus and Cloning ToolStripMenuItems Under VB.NET ” on page 282 to see how to
easily subclass a VB.NET ToolStripMenuItem and add a full working Clone method to it. Note further that several gurus
have attempted this, but always warn users that their clone example is not perfect (they say this because they were just
copying a Microsoft example). Even Microsoft says that cloning a ToolStripMenuItem is not perfect. However, if they had
added just 5 very simple lines of code, it would have been perfect! Again, see page 282 for details. Also see “Passing Data
Between Protected and Unprotected Memory Under VB.NET” on page 212 regarding StructureToPtr and PtrToStructure,
 Page 227

The Secret Truth Behind Accessing Private Class Members.


One loud complaint VB6 users wail about is that VB.NET, as in any true Object-Oriented Programming
Language, private members do not seem to be private to the class instance by other instances of the
same class. This is not true, because this is just an illusion – it just seems to be doing that, as we are
about to see very shortly.
What they mean exactly is that an instance of a class is capable of funneling to private or protected data
members of another instance of that class, but only if they have a reference to it. This typically crops up
in linked lists, where objects must reference the consecutively next or previous members in its chain,
and they might need to parse through that chain in order to find a specific member. Mind you, this is not
possible from objects of other classes. Granted, private fields or methods within a class are private to its
class instance if they are declared Protected or Private, except in the case where an instance object of a
class is referencing another instance of that exact same class, otherwise it could not even access itself!
Because each member of a class shares the same code, and it is not likely to get depressed and become
suicidal, and, if we have enough experience to analyze what is really going on, we would know that one
object is actually not accessing the protected data of another, though it might appear to be doing that, but
technically it is actually accessing itself! (this will soon be read as DUH!). My question is, why are
these brainiacs wailing? This is standard OOPL fare that anyone who has a modicum of experience in
object-based programming is familiar with and does not even think twice about, because this is a logical
consequence of an OOPL. Consider the following example, making special note of this class’s use of
protected members of other instances of this class in a doubly-linked list:
Option Explicit On
Option Strict On

Friend Class myLinkedClass


'*********************************************************************************
Protected _Next As myLinkedClass 'Protected next data cell in this cell's chain.
Protected _Prev As myLinkedClass 'Protected previous data cell in this cell's chain.
'-----------------------------------
Protected _Data As String 'actual data to define this object's Personality.
'*********************************************************************************

'*********************************************************************************
' Property : NextSibling – Public member acessed by other members of this project.
' Purpose : Return the DataCell's Next Sibling.
'*********************************************************************************
Friend ReadOnly Property NextSibling As myLinkedClass
Get
Return Me._Next 'Note that this is technically ALSO accessing a protected sibling object,
End Get 'but no one seems to be complaining about that. I wonder why?
End Property

'*********************************************************************************
' Property : PreviousSibling – Public member acessed by other members of this project.
' Purpose : Return the DataCell's Previous Sibling.
'*********************************************************************************
Friend ReadOnly Property PreviousSibling As myLinkedClass
Get
Return Me._Prev 'Note that this is technically ALSO accessing a protected sibling object.
End Get
End Property

'*********************************************************************************
' Property : FirstSibling
' Purpose : Find the first member in the current sibling chain.
' NOTE : Use private members of this and other sister objects to loop to the start of the chain.
'*********************************************************************************
Friend ReadOnly Property FirstSibling As myLinkedClass
Get
Dim sData As myLinkedClass = Me 'start search with this DataCell.
Do While sData._Prev IsNot Nothing 'while a previous sibling exists in the chain...
sData = sData._Prev 'recurse back through it (we can access private members of siblings).
Loop
Return sData 'Return the myLinkedClass object that is at the start of this sibling chain.
End Get 'Had we parsed using the slower PreviousSibling property, no one would
End Property 'complain, even though THAT property accesses a protected member. Do they
'not engage their brains before they start sounding their verbal klaxons?
 Page 228

'*********************************************************************************
' Property : LastSibling
' Purpose : Find the last member in the current sibling chain.
' NOTE : Use private members of this and other sister objects to loop to the end of the chain.
'*********************************************************************************
Friend ReadOnly Property LastSibling As myLinkedClass
Get
Dim sData As myLinkedClass = Me 'start search with this DataCell.
Do While sData._Next IsNot Nothing 'while another DataCell exists after it in the chain...
sData = sData._Next 'recurse forward through it (we can access private members of siblings).
Loop
Return sData 'return the myLinkedClass object that is at the end of this sibling chain.
End Get
End Property
'other methods are defined here ------------------------------------------------
End Class

In this example, we want to enable the ability to loop backward through a doubly-linked list to find the
root sibling object of the chain the accessed object is linked into (the data object whose PreviousSibling
is not defined) or the last sibling in the chain (the data object whose NextSibling is not defined). If the
class were ignorant of itself, it would not be able to do the above, which is to access the protected _Next
and _Prev members of the provided Sibling, which is simply much faster than using the Property
members NextSibling and PrevSibling, yet they will procure the exact same data. More common real-
world examples must also be able to access data that might not be exposed to the outside by a more
public property or method, such as the protected _Data member. Granted, we would think that two
objects of the same class are able to access Private, Dim, or even Protected members might defeat the
protected status of this data, but it does not, because when the next or previous members are being used,
it is now that object that is in control, It is like objects have Dissociative Identity Disorder; they
instantly transform into sibling objects through object referencing. Even so, a class inheriting this class
lacks the ability to touch these private members of its base class, so those members are still hidden.
Succinctly, it just appears one object accesses another, but the other object is actually accessing itself!
The really hilarious thing about all this is – classes in VB6 exhibit exactly the same behavior! I really
do have to wonder why these VB6 crybabies are not complaining about that?
 Page 229

Restoring the VB6 APP Command For Use Under VB.NET.


The much-loved, and very convenient VB6 App object is absent in VB.NET, if that point was not
already bleeding obvious from a few earlier notes within the section, Noteworthy VB.NET Features
That Differ From, or are New Since VB6, and we are left with extracting its many useful application-
related values by other, sometimes more complicated, though platform-necessary means. However, we
can implement a simple non-inheritable class and obtain this information just as easily as we did under
VB6. By including the following static App Class in our Project (no need to instantiate), we can, for
example, get the application’s version number exactly the same as we did it under VB6, using:
Dim Version As String = App.Major.ToString("0.") & App.Minor.ToString("0.") & App.Revision.ToString

Following is my App class, featuring a number of the old App statement’s many useful methods:
Option Explicit On
Option Strict On

'*********************************************************************************
'*********************************************************************************
' Class App
'This static class is used to expose VB6-style App functionality in order to easily access application information.
'DO NOT INSTANTIATE AN INSTANCE OF THIS CLASS! USE IT AS-IS.
'NOTE: Because the App class does not contain data, we do not need to, nor can we instantiate a new
' instace of the class, because the data instance would be null. However, the code for the class will
' exist without instantiation, which we can access thru the "App" name.
'These functions emulate the VB6 App command. For example, to get the current app's title,
' Dim S As String = App.Title
'
'NOTICE: The more obscure (or phased out) VB6 App features are not supported, but the principle methods are.
'*********************************************************************************
'*********************************************************************************
Imports System.Reflection.Assembly, System.IO.Path, System.Diagnostics.Process, System.Runtime.InteropServices

Friend NotInheritable Class App

Friend Shared Function FullPath() As String 'App.FullPath. Support function.


Return Application.ExecutablePath
End Function
'--------------------------------------------------------------------------
'MAIN FEATURES
'--------------------------------------------------------------------------
'new, not used in VB6.
Friend Shared Function UserName() As String 'App.UserName.
Return SystemInformation.UserName 'return logged on user name.
End Function
'----
'new, not used in VB6.
Friend Shared Function DomainUserName() As String 'App.DomainUserName.
Return System.Security.Principal.WindowsIdentity.GetCurrent.Name() 'This will return 'domain and logged on user name.
End Function
'----
Friend Shared Function AssemblyName() As String 'App.AssemblyName.
Return My.Application.Info.AssemblyName 'return assembly name assigned to the application header.
End Function
'----
'new, used in DotNET but not in VB6.
Friend Shared Function Comments() As String 'App.Comments.
Return FileVersionInfo.GetVersionInfo(FullPath()).Comments 'return Comments text assigned to the application header.
End Function
'----
Friend Shared Function CompanyName() As String 'App.CompanyName.
Return My.Application.Info.CompanyName 'return Company Name text assigned to the application header.
End Function
'----
Friend Shared Function EXEName() As String 'App.EXEName.
Return GetFileName(FullPath()) 'return applications executable filename.
End Function
'----
Friend Shared Function FileDescription() As String 'App.FileDescription.
Return My.Application.Info.Description 'return Description text assigned to the application header.
End Function
'duplicate the above vb6-style method name with the more recent name.
Friend Shared Function Description() As String 'App.Description.
Return My.Application.Info.Description 'return Description text assigned to the application header.
End Function
'----
Friend Shared Function hInstance() As Int32 'App.hInstance.
Return Marshal.GetHINSTANCE(GetCallingAssembly.GetModules()(0)).ToInt32 'return Instance Handle of the application.
 Page 230

End Function
'duplicate the above vb6-style method name using the more recent name.
Friend Shared Function Instance() As Int32 'App.Instance.
Return Marshal.GetHINSTANCE(GetCallingAssembly.GetModules()(0)).ToInt32
End Function
'----
Friend Shared Function LegalCopyright() As String 'App.LegalCopyright.
Return My.Application.Info.Copyright 'return Copyright text assigned to the application header.
End Function
'duplicate the above vb6-style method name using the more recent name.
Friend Shared Function Copyright() As String 'App.Copyright.
Return My.Application.Info.Copyright
End Function
'----
Friend Shared Function LegalTrademarks() As String 'App.LegalTrademarks.
Return My.Application.Info.Trademark 'return Trademark text assigned to the application header.
End Function
'duplicate the above vb6-style method name using the more recent name.
Friend Shared Function Trademark() As String 'App.Trademark.
Return My.Application.Info.Trademark
End Function
'----
'new, used in DotNET but not in VB6
Friend Shared Function Version() As Version 'App.Version.
Return My.Application.Info.Version 'return full application version.
End Function
'----
Friend Shared Function Major() As Int32 'App.Major.
Return My.Application.Info.Version.Major 'return Major ID of the version (MAJaR.Minor.Build.Revision).
End Function
'----
Friend Shared Function Minor() As Int32 'App.Minor.
Return My.Application.Info.Version.Minor 'return Minor ID of the version (Major.MINOR.Build.Revision).
End Function
'----
'new, used in DotNET but not in VB6.
Friend Shared Function Build() As Int32 'App.Build.
Return My.Application.Info.Version.Build 'return Build ID of the version (Major.Minor.BUILD.Revision).
End Function 'Note that VB6 actually used the BUILD as its REVISION.
'----
Friend Shared Function Revision() As Int32 'App.Revision.
Return My.Application.Info.Version.Revision 'return Revision ID of the version (Major.Minor.Build.REVISION).
End Function 'Note that VB6 actually used the BUILD as its REVISION
'----
Friend Shared Function Path() As String 'App.Path.
Return My.Application.Info.DirectoryPath 'return full directory path text of the application.
End Function
'----
Friend Shared Function PrevInstance() As Boolean 'App.PrevInstance.
Return GetProcessCount() <> 0 'Return True if a previous instance of application is running.
End Function
'----
Friend Shared Function GetProcessCount() As Int32 'App.GetProcessCount.
Return Ubound(GetProcessesByName(GetCurrentProcess.ProcessName)) 'return # of processes running in application.
End Function
'----
Friend Shared Function ProductName() As String 'App.ProductName.
Return My.Application.Info.ProductName 'return Product Name text assigned to the application header.
End Function
'----
Friend Shared Function Title() As String 'App.Title.
Return My.Application.Info.Title 'return Title text assigned to the application header.
End Function
'----
'simplify invoking the DoEvents process, to let pending painting operations and timers do their thing.
Friend Shared Sub DoEvents() 'App.DoEvents.
My.Application.DoEvents()
End Sub

End Class
 Page 231

VB.NET Generics Collection Classes.


The VB.NET Collection control returns all entries, regardless of the type delivered to it, as if they were
generic Objects. I say as if, for type Object is not quite as generic as we think of it. It only acts that way.
That is because the type of entity wrapped by an Object is exactly known to it. In that, an Object is like
the slower and far less capable VB6 Variant, but on steroids! This is why we can test an Object for
being of a type: “If Obj Is MyCustomClass...”, “If TypeOf Obj Is ListView...”, “If Obj.GetType.Equals(Me)...”, or
“If Obj.GetType.Name = "ComboBox"... ”, why we can use the DirectCast command to cast it to its original type
or to a data-comparable derived, sibling, or parent class, and why it snaps at us if we try to cast to an
incompatible type. Casting allows us to operate on an Object using class methods and properties of the specified type,
even if it was not the original type, but only if the different class has an identical data signature. This is why we can cast an
Image to type Bitmap and use Bitmap class methods and properties on it, for example.
NOTES: As indicated, if we know the object’s actual type, use DirectCast instead of CType, because it will not generate a
single additional machine code instruction, as CType sometimes does. Besides, if we do get the type wrong, the DirectCast
command will let us know if we must use the CType instruction through an error message at compilation.
Microsoft recommends that we use CType instead of DirectCast, saying that it is safer. That could be if we are not mindful
enough to ensure that the objects are exactly of the type that we know them to be. As such, I strongly recommend using
DirectCast, but use CType whenever we actually do want to convert one type to another. Though CType will not generate
additional code if we do specify a compatible type, if we accidentally specify a wrong type, we may not get an error report
as we otherwise would, which could result in a shadowy bug that can be much more difficult to track down!
When VB.NET took its first major step forward with the release of VB2005, it also introduced many
new and powerful features. Among them were Edit-and-Continue, design-time expression evaluation,
the My namespace, the Using and Continue keywords, data source binding, partial classes, operator
overloading, unsigned integers, and to top it all off, it introduced Generic Collections and Generics.

Generic Collections.
Generic Collections are accessed through the System.Collections namespace. Just like the default
VB.NET Collection class, they are not strongly typed, but they do accept any type of object as data.
Likewise, they return their data as generic Objects. As such, they are still limited, just like the default
VB6-style Collection class, to late-Binding issues if used in reckless code. But used correctly, they are
extremely powerful. The built-in collections provided by the System.Collections namespace are:
ArrayList A zero-based list that uses an array whose size can be dynamically adjusted as required.
BitArray A zero-based list that is optimized to store a very tightly packed array of Boolean (True/False) values (bits of 1 or 0).
HashTable A zero-based list of Values and Keys that is most like the VB6 Collection class. This class allows you to look up Values by using
a Key. But, unlike the VB6 Collection class, the Key and the Value under VB.NET can now be of any type.
Queue A zero-based list supporting a “first in, first out” model. Use Enqueue and Dequeue to add and recover/remove items.
SortedList A zero-based list similar to HashTable, except that the keys are always sorted. This means that if you use For...Each to iterate
through its collection, you will always retrieve the items in sorted order. You can also supply it with your own comparison class.
This is important if TKey is a custom class.
Stack A zero-based list that supports a “last in, first out” model. Use Push and Pop to add and recover/remove items.

Generics.
VB2005 also allowed us to create our own strongly typed collections, known as Generics, so we could
write list classes ourselves. Strongly Typed means that we can access its members as the type they are
assigned as without casting, such as a string or class instance. For example, with a user-defined
Generics collection class named List and a user-defined class named Customer, the following statement,
“Dim customerList As New List(Of Customer) ”, could create a strongly typed list collection of type Customer
(notice the new Of operator, implying “Of Type”). Strongly-typed collections can take on numerous
forms, basing them on pre-defined templates, whether they are variables, structures, or classes.
With VB2005 we had no choice but to define our own custom Generics classes in order to implement
strongly typed collections. The advantage to doing this was the introduction of the “Of” keyword, which
is used to enable the strong typing and force faster, early type binding.
 Page 232

For instance, if we wanted to implement a Stack class (Last In, first Out) that also allowed at
instantiation to provide strong support for a specific data type, we must first declare a class that
supported stack-like operations; something that could be as simple as the following primitive class:
'*********************************************************************************
' Generic Stack – Strongly-Typed Stack Class that can support any type.
'*********************************************************************************
Public Class GenericStack(Of itemType) 'itemType; placeholder for actual type supplied during instantiation.
Private element() As itemType 'Create a dynamic array of the user-selected instantiated type.
Private pointer As Integer 'Index into the dynamic array.

'NEW (instantiate new class instance and optionally specify an initial stack depth).
Public Sub New(Optional ByVal size As Integer = 10) 'Init the stack size, optionally defining its initial stack 'depth'.
ReDim element(size – 1)
pointer = 0 'Initialize stack location to the bottom of the stack.
End Sub

'DEPTH (Returns the number of items presently on the stack).


Public ReadOnly Property Depth()
Return (pointer)
End Sub

'PUSH (push an item onto the stack)


Public Sub Push(ByVal item As itemType) 'Push an item onto the custom stack.
If pointer > Ubound(element) Then 'If stack index is out of bounds...
ReDim Preserve element(UBound(element) + 10) ' expand the stack size to accomodate a deeper depth. Increase by 10.
End If 'NOTE: PRESERVE modifier used to preserve previous content data.
element(pointer) = item ' All is well, so stuff new item into the stack.
pointer += 1 ' and bump the current stack index up.
End Sub
'
'POP (pop an item from the stack and return it to the invoker).
Public Function Pop() As itemType 'Pop an item off the custom stack.
If pointer = 0 Then 'If the current stack index is at botom of stack...
Throw New Exception("Stack is empty.") ' force a trappable error.
Else
pointer -= 1 ' else back stack index down.
Return element(pointer) ' and return last-pushed item.
End If
End Function
End Class

The type “itemType” is an arbitrarily-named Type-placeholder (I could have just declared it “Joe”). This will
be assigned an actual type during instantiation, like “ Dim Istack As New GenericStack(Of Integer)(20)”. This
creates a 20-element-deep stack that only accepts type Integer data. We can also declare it using “ Dim stackA
As New GenericStack(Of Integer)” if we want to use the default initial stack depth of 10. Regardless, the
stack will automatically expand if a deeper stack is needed, in case we push more than 10 items onto it.
We can further impose constraints on a user-defined Generic type. For example, if we want our Stack
class to only process objects of a certain type or types, say of type Employee, which could be a structure
or class, we can declare our Generic class using “ Public Class GenericStack(Of itemType As Employee) ”.
In this situation, during instantiation of the class, we would not be able to declare the object to be “Of”
any other type, but instead we would instantiate it like this: “ Dim Estack As New GenericStack”. Or, add
more constraints by separating them with commas: “Public Class GenericStack(Of itemType As Employee,
TempEmp)”. In this case, we would need to declare the instance of the class to be ‘Of’ a type, but only by
specifying the allowed types, such as “(Of Employee)” or “(Of TempEmp)”.
IMPORTANT NOTE: Bypassing Operator Comparisons Between Generic Types: we might notice that we are not able to
override Operator functions in our Generics objects as we were able to do with regular classes (those not featuring the “Of”
specification), such as would be required to compare two items of Itemtype in the above stack class. For example: Dim X As
itemType: X = elements(0): If X = elements(0) then Debug.Print(“Compare OK”). In this example, the Comparison is
flagged as an error and reports “Operator ‘=’ is not defined for types ItemType and ItemType”. This is due to the nature of
the Generics class type. For example, some have tried to define an operator, such as “=” to compare these two variables of
type ItemType by adapting the typical template solution, “ Public Shared Operator =(ByVal Item1 As itemType, ByVal Item2
As itemType) As Boolean”, but this specification fails because at least one of the parameters must be of the type
“GenericStack(Of itemType)”, which, sadly, cannot be implemented... because it is too generic. Yet, a very easy solution to
this for basic types (Integer, Double, etc.) is to perform a CompareTo comparison, such as “If X.CompareTo(elements(0)) = 0
Then...”, or even using their ToString methods, such as “If X.ToString = elements(0).ToString Then ...”
 Page 233

Generics Collection Classes.


VB2008 upped the ante by introducing many new features, such as a true conditional operator,
Anonymous types, LINQ support, Lamda expressions, XML Literals, Type Inference, and Extension
methods. But it also introduced Generics Collection Classes. Generics Collection Classes enabled the
use of type-safe zero-based collections that avoided the otherwise necessary task of also casting their
results. It also eliminated the need for us to create custom class-definitions for commonly used types in
order to enjoy strongly-typed collections, allowing us to declare our collection by implementing the new
System.Collections.Generic namespace.
Now, instead of having to first create needed Generic collection classes, though we certainly still can do
so in situations not supported by the collection types provided by the System.Collections.Generic
namespace, we are able to implement any of several built-in strongly-typed collection classes.
Therefore, without needing to write a custom class anymore, we can declare everything we need for a
strongly-typed stack collection by entering “ Dim MyStack As New System.Collections.Generic.Stack(Of
String)”. Actually, because System.Collections.Generic is automatically imported in a VB project, we
need only specify “Dim MyStack As New Stack(Of String)”. Notice that these Generic classes are designed
to be fully dynamic, meaning that we do not need to declare a “depth”, just as we could actually do in
our much simpler example, shown above. We could also take the optional parameter out of our custom
New method and initially issue a “ ReDim element(9)” to instantiate an object with a startup depth of 10
(0-9) elements.
The following pre-defined collections are supported in the System.Collections.Generic namespace,
where “Of T” represents the object type to strongly cast this collection to:
Dictionary (Of Tkey, Tvalue) This is a zero-based collection of keys and values.
LinkedList (Of T) This is a doubly-linked zero-based list. Manage the lists through LinkListNode objects.
List(Of T) This is a strongly-typed zero-based list of objects that can be accessed by an index. Methods
are provided to search, sort, and manipulate the list.
Queue(Of T) This is a queue of first-in-first-out (FIFO) zero-based collection of strongly-typed objects. Use
Enqueue and Dequeue to add and recover/remove items.
SortedDictionary (Of Tkey, Tvalue) This is a zero-based collection of key/value pairs that are sorted by key. You can also supply it
with your own comparison class if TKey is not a general type, such as string or numeric. This
is important if TKey is a custom class.
SortedList (Of Tkey, Tvalue) This is a zero-based collection of key/value pairs that are sorted by key based upon an
associated System.Collections.Generics.Icomparer(Of T) implementation. You can also supply
it with your own comparison class if TKey is not a general type, such as string or numeric. This
is important if TKey is a custom class.
Stack(Of T) This is a variable-sized stack of last-in-first-out (LIFO) zero-based collection of strongly-typed
objects. Use Push and Pop to add and recover/remove items.

NOTE: It is strongly encouraged we explore the method and property support for each of these Collection types.
NOTE: We will later explore several ways to perform E-Z custom sorts, both ascending and descending, for collections and
lists later in this tome. Just peruse the Table of Contents for SORT. We will also explore how to easily sort directory trees and
keep the folders sorted and displayed above a file list. We will also explore how to easily performs super-fast binary searches
through collection lists.
 Page 234

How to Perform E-Z Font Manipulation at Runtime Under VB.NET.


A big change for VB6 users to get used to when moving to VB.NET is Font Manipulation. Under VB6
we could toggle font properties during runtime at will, such as bold, italic, underline, strikethrough, set a
font’s size, or set the TypeFace. Under VB.NET, these are easy enough to set at development time, but at
runtime some claim that this is now impossible, causing livid consternation among many new users. The
reason for this is that VB6 used font classes within controls containing all the functionality needed to
manipulate a font’s properties, internally creating a new font instance whenever we changed one of its
properties, making we think it was still the same font object. .NET, on the other hand, employs leaner,
more efficient objects that lack a cumbersome code overhead that lay idle most of the time.
Thus, the only way to change fonts at runtime under .NET is to do it literally. As mentioned, this was
what VB6 did for us using its heftier font class. An advantage of a leaner class is that we now not only
see and know exactly what is going on, but if we transition our code to C# or C++, the move is much
smoother because this process is exactly what we would have to perform in those environments.
How do we perform on-the-fly runtime font manipulation under VB.NET? It is actually quite easy, but
we can make it even easier with the addition of a few helper functions. Let us begin at the beginning...
Easily Change Font Sizes at Runtime.
To change font sizes at runtime, all we need do is create a new font based on the one we are replacing
and then assign the new font where we need it. For example, suppose Label1 has a size of 10 points (10/72
Inch), and we want to change it to 12 points. For this, we can do something like the following:
With Me.Label1
.Font = New Font(.Font.Name, 12, .Font.Style)
End With

NOTE: This works because all parameters are processed before the new font is instantiated and before being assigned back
to the Label1 Font property. Additionally, although we can likewise specify “ Me.Label1.Font = New Font(Me.Label1.Font.Name,
12)”, it is too often very important to also retain the original Font Styles, in case the text was specifically bolded, underlined,
or italicized, unless we do not want these embellishments. Further, consider employing a “ With Label1” block.
What we did was to simply create a new instance of a font using the old font’s name and style, but we
used a new point size (12). Of course, we may not want to change it if it is already set to the desired size.
We can accommodate this with a helper function that will check for that (be sure to also import the
System.Drawing namespace in the heading of the file if we need it, which handles drawing fonts):
'****************************************************************************************************
' FontChangeSize - Set/reset Selected Font Size
' CurrentFont = font reference to change
' NewSize = point size to set
'****************************************************************************************************
Public Function FontChangeSize(ByVal CurrentFont As Font, ByVal NewSize As Single) As Font
If (Math.Round(CDbl(CurrentFont.SizeInPoints), 2) = Math.Round(CDbl(NewSize), 2)) Then
Return CurrentFont 'no need for change, so return the current font settings
End If
Return New Font(CurrentFont.Name, NewSize, CurrentFont.Style) 'the font size changed, so return a new font
End Function

To use it, we simply issue the command “Me.Label1.Font = FontChangeSize(Me.Label1.Font, 12) ”.

NOTE: We can also borrow font settings from other controls that are similarly set, or already set to the desired format. We
can even do this: “Me.Label1.Font = New Me.Font” (If we did not use the “ New” verb, then Label1 will simply piggyback a
reference to the form’s font, which is usually not what we want. Typically, we want Label1 to actually own its font).

Easily Change Font Styles at Runtime (Bold, Italic, Underline, and Strikethrough).
To change the font’s Bold, Italic, Underline, or Strikethrough state is even easier. Suppose I wanted to
change the font for Label1 to have a Bold style, I can just do this:
Me.Label1.Font = New Font(Me.Label1.Font, FontStyle.Bold) 'base the font on an existing font, but alter its style

This works great, but toggling styles might seem a bit of a problem, especially if we want to toggle a single style
option, but leaving others intact. The best approach is to test to see if a change is actually required. When a
change is really needed should be the only time that we apply the desired state.
 Page 235

We can do this easily with another helper function, like the one below:
'****************************************************************************************************
' FontChangeStyle - Support method for Bold, Italic, Underline, StrikeThrough set/reset
'****************************************************************************************************
Private Function FontChangeStyle(ByVal CurrentFont As Font,
ByVal StyleFlag As System.Drawing.FontStyle,
ByVal SetStyle As Boolean) As Font
If StyleFlag = FontStyle.Regular Then SetStyle = True ' 'Force Setting if Regular (0) specified
'mask desired style against current (do this in case of multiple selections)
Dim fntStyle As FontStyle = (CurrentFont.Style And StyleFlag) 'binary mask
'set flag to True if selected style is (or all selected styles are) already set
Dim flag As Boolean = (fntStyle = StyleFlag)
'if not an EXACT match because something is different, then FORCE a change
If Not flag AndAlso (fntStyle <> FontStyle.Regular) Then flag = Not SetStyle
If (flag = SetStyle) Then 'if nothing changes, then simply return the current font
Return CurrentFont
End If
'define a new style value minus the current selection(s), based on current font style settings
Dim newStyle As FontStyle = CurrentFont.Style And Not StyleFlag 'binary mask
If SetStyle Then 'are we setting the new style(s)?
If StyleFlag = FontStyle.Regular Then 'if Regular (0)...
newStyle = FontStyle.Regular 'force Regular (Regular is the absence of other styles)
Else
newStyle = (newStyle Or StyleFlag) 'else apply new type to new font
End If
End If
Try
Return New Font(CurrentFont, newStyle) 'return new font based upon current, with current selection set/reset
Catch
Try
Return New Font(CurrentFont, newStyle Or FontStyle.Bold) 'try applying bold (some fonts require Bold)
Catch
Return CurrentFont 'odd error (unlikely), so return current font
End Try
End Try
End Function

In the above, we set the state of flag to True if the style we selected, such as FontStyle.Bold, is already set. We
then check it against the SetStyle flag. If we want to set it (SetStyle = True), but bold is already set (flag = True),
then there is nothing to do and a reference to the old font is returned. If the state of SetFlag does not equal flag,
then we have something to do. We first define a local variable that will contain the current style flag, but without
our selected style, in case we will be toggling it off (SetStyle = False). We then check the SetStyle value, and if it
is set to True we will apply the selected style to the flag value. Finally, we define a new font based on the old one
and apply the new style change to it. The great thing about this flag is that we can set multiple styles at once.
Suppose we want to set both Bold and Italic styles to a label. We can do this by using the statement
“Me.Label1.Font = FontChangeStyle(Me.Label1.Font, FontStyle.Bold Or FontStyle.Italic, True) ”. To toggle a
selected style off, we simply change the state of the SetStyle parameter to False.

Easily Change Font Names at Runtime.


The last thing we might want to do is change the actual font (in point of fact, we should refer to this as
the TypeFace). Suppose we wanted to change the font of our label to Courier New. We could do this:
With Me.Label1
.Font = New Font("Courier New", .Font.Size, .Font.Style)
End With

But this is a ton of work if we have to do a load of it on our forms. We can instead create a simple little
function that will do all the dirty work for us, such as the following:
'****************************************************************************************************
' FontChangeName - Set/reset Selected Font Name
' CurrentFont = font reference to change
' NewName = new font family to change it to
'****************************************************************************************************
Public Function FontChangeName(ByVal CurrentFont As Font, ByVal NewName As String) As Font
If StrComp(CurrentFont.Name, NewName, CompareMethod.Text) = 0 Then
Return CurrentFont
End If
Try
Return New Font(NewName, CurrentFont.SizeInPoints, CurrentFont.Style)
Catch
Try 'some fonts require Bold to be set
Return New Font(NewName, CurrentFont.SizeInPoints, CurrentFont.Style Or FontStyle.Bold)
Catch
 Page 236

Return CurrentFont
End Try
End Try
End Function

With the above FontChangeName function, all we have to do to change the font to Consolas is execute:
“Me.Label1.Font = FontChangeName(Me.Label1.Font, "Consolas")”. We can then copy a reference to this font
to any control we need to assign it to. The cool thing about doing this is that afterward we can change the
font of all these controls from just one of them, and they will all change! OOP Rules!

Building a Suite of Runtime Font Modification Tools.


But suppose we want to set an entirely new font, or change more than one property, such as the font size
and the style. We could write, as I have, a function to do all this, but in the end, for small jobs, probably
the easiest method is to simply define a new font on the spot, just as had been demonstrated at the
beginning of this article. For example, suppose I want to change the font of Label1 to Times New
Roman, the point size to 12, and the style to Bold and Italic. All I would have to do is this:
Me.Label1.Font = New Font("Times New Roman", 12, FontStyle.Bold Or FontStyle.Italic)

Or, if we will be doing a lot of this, then a full function would be in order. Here is my complete module,
which was originally based on the font support provided by the VB6 Compatibility Library:
Imports System.Drawing

Module modFontChanges
'************************************************************************************************
' FontChangeBold - Set/reset Selected Font Bold
' CurrentFont = font reference to change
' Bold = True:Set, else Reset
'************************************************************************************************
Public Function FontChangeBold(ByVal CurrentFont As Font, ByVal Bold As Boolean) As Font
Return FontChangeStyle(CurrentFont, FontStyle.Bold, Bold)
End Function

'************************************************************************************************
' FontChangeItalic - Set/reset Selected Font Italic
' CurrentFont = font reference to change
' Italic = True:Set, else Reset
'************************************************************************************************
Public Function FontChangeItalic(ByVal CurrentFont As Font, ByVal Italic As Boolean) As Font
Return FontChangeStyle(CurrentFont, FontStyle.Italic, Italic)
End Function

'************************************************************************************************
' FontChangeUnderline - Set/reset Selected Font Underline
' CurrentFont = font reference to change
' Underline = True:Set, else Reset
'************************************************************************************************
Public Function FontChangeUnderline(ByVal CurrentFont As Font, ByVal Underline As Boolean) As Font
Return FontChangeStyle(CurrentFont, FontStyle.Underline, Underline)
End Function

'************************************************************************************************
' FontChangeStrikeout - Set/reset Selected Font Strikeout
' CurrentFont = font reference to change
' Strikeout = True:Set, else Reset
'************************************************************************************************
Public Function FontChangeStrikeout(ByVal CurrentFont As Font, ByVal Strikeout As Boolean) As Font
Return FontChangeStyle(CurrentFont, FontStyle.Strikeout, Strikeout)
End Function

'************************************************************************************************
' FontChangeSize - Set/reset Selected Font Size
' CurrentFont = font reference to change
' NewSize = point size to set
'************************************************************************************************
Public Function FontChangeSize(ByVal CurrentFont As Font, ByVal NewSize As Single) As Font
If (Math.Round(CDbl(CurrentFont.SizeInPoints), 2) = Math.Round(CDbl(NewSize), 2)) Then
Return CurrentFont
End If
Return New Font(CurrentFont.Name, NewSize, CurrentFont.Style)
End Function

'************************************************************************************************
' FontChangeName - Set/reset Selected Font Name
' CurrentFont = font reference to change
' NewName = new font family to change it to
 Page 237

'************************************************************************************************
Public Function FontChangeName(ByVal CurrentFont As Font, ByVal NewName As String) As Font
If StrComp(CurrentFont.Name, NewName, CompareMethod.Text) = 0 Then
Return CurrentFont
End If
Try
Return New Font(NewName, CurrentFont.SizeInPoints, CurrentFont.Style)
Catch
Try 'some fonts require Bold to be set
Return New Font(NewName, CurrentFont.SizeInPoints, CurrentFont.Style Or FontStyle.Bold)
Catch
Return CurrentFont
End Try
End Try
End Function

'************************************************************************************************
' FontChangeStyle - Support method for Bold, Italic, Underline, StrikeThrough set/reset
'************************************************************************************************
Private Function FontChangeStyle(ByVal CurrentFont As Font,
ByVal StyleFlag As System.Drawing.FontStyle,
ByVal SetStyle As Boolean) As Font
If StyleFlag = FontStyle.Regular Then SetStyle = True ' 'Force Setting if Regular (0) specified
' mask desired style against current (do this in case of multiple selections)
Dim fntStyle As FontStyle = (CurrentFont.Style And StyleFlag) 'binary mask
' set flag to true if selected style is (or all selected styles are) already set
Dim flag As Boolean = (fntStyle = StyleFlag)
' if not an EXACT match because but something is different, then FORCE a change
If Not flag AndAlso (fntStyle <> FontStyle.Regular) Then flag = Not SetStyle
If (flag = SetStyle) Then 'if nothing changes, then return the current font
Return CurrentFont
End If
' define a new style value minus the current selection(s), based on current font style settings
Dim newStyle As FontStyle = CurrentFont.Style And Not StyleFlag 'binary mask
If SetStyle Then 'are we setting the new style(s)?
If StyleFlag = FontStyle.Regular Then 'if Regular (0)...
newStyle = FontStyle.Regular 'force all to Regular (Regular is no other styles)
Else
newStyle = (newStyle Or StyleFlag) 'else apply new type to new font
End If
End If
Try
Return New Font(CurrentFont, newStyle) 'return new font based upon current, with current selection set/reset
Catch
Try
Return New Font(CurrentFont, newStyle Or FontStyle.Bold) 'try applying bold (some fonts require Bold)
Catch
Return CurrentFont 'odd error (unlikely), so return current font
End Try
End Try
End Function

'************************************************************************************************
' ChangeFont - Support changing multiple properties
'************************************************************************************************
Public Function ChangeFont(ByVal CurrentFont As Font,
Optional ByVal NewName As String = vbNullString,
Optional ByVal NewSize As Single = 0,
Optional ByVal StyleFlag As System.Drawing.FontStyle = FontStyle.Regular,
Optional ByVal SetStyle As Boolean = False) As Font
Dim Changes As Boolean = False
Dim flag As Boolean = False

Dim Nam As String = CurrentFont.Name 'get current name


Dim Siz As Single = CurrentFont.Size 'get current point size
Dim Styl As FontStyle = CurrentFont.Style 'get current style flags
Dim fntStyle As FontStyle = (Styl And StyleFlag)

If CBool(Len(NewName)) Then 'if we will change the font family name...


If StrComp(Nam, NewName, CompareMethod.Text) <> 0 Then 'if name actually changes
Nam = NewName 'set new name
Changes = True
End If
End If

If NewSize <> 0 Then


' if we will change the point size, and difference is big enough to impact it visually...
If (Math.Round(CDbl(CurrentFont.SizeInPoints), 2) <> Math.Round(CDbl(NewSize), 2)) Then
Siz = NewSize 'set new size
Changes = True
End If
End If

If StyleFlag <> FontStyle.Regular Then 'if we are setting a style flag


' set flag to true if selected style is (or all selected styles are) already set
 Page 238

flag = (fntStyle = StyleFlag)


' if not EXACT match, but something IS different, then FORCE change
If Not flag AndAlso (fntStyle <> FontStyle.Regular) Then flag = Not SetStyle
If (flag <> SetStyle) Then 'if something will change...
' define a new style value minus the current selected
' type, based upon the current font style settings
Styl = DirectCast(CurrentFont.Style And Not StyleFlag, FontStyle)
If SetStyle Then 'are we setting the new style?
Styl = (Styl Or StyleFlag) 'yes, so set new style to flag
End If
Changes = True
End If
End If
'if there are changes, try returning new font
If Changes Then
Try
Return New Font(Nam, Siz, Styl)
Catch 'on error, try bolding
Try
Return New Font(Nam, Siz, Styl Or FontStyle.Bold)
Catch
End Try
End Try
End If
Return CurrentFont 'return current if nothing to change, or error
End Function
End Module
 Page 239

Drawing Labels with Transparency Made E-Z.


Countless new VB.NET programmers have complained about the trouble they have just trying to display
some stupid label in front of another control without its background painting a gray box around its text.
After all, this seemed to happen with perfect ease under VB6. There, doing so was as easy as zapping
our groin on a farm while climbing over electric fences; we just placed a label where we wanted it on a
form, such as in front of a picture, we set the label’s BackStyle to 0 (Transparent), if it was not set to
that value already, and then we moved on to the next item on our task list.
When they migrated from VB6 to VB.NET, one of the first things they might have noticed was that a
label’s BackStyle property no longer existed. DOH! If they were lucky, after some quick checking of
Microsoft’s upgrade notes in the MSDN library, they would have discovered that this property had been
rendered moot by a brand new color value, Color.Transparent. But, when they assigned it to the label's
BackColor property, nothing seemed to happen. Later, after some grumbling and perhaps a few web
searches, or, God forbid, in the hour of their most desperate need, they lowered themselves to actually
reading some documentation, they learned that a label's background will be transparent only to its parent
control., which is typically the form. Huzzah! However, after the initial flush of relief, when they tried to
assign the label’s Parent property to the control it was positioned in front of, the label just seemed to jog
off somewhere, always down and right, if it did not disappear from view altogether. >3( Grrr...
To make this whole label rendering process appear so simplistic under VB6, it had to do a number of
hidden tasks in the background. First, it had to draw itself to the form’s background. Next, it had to scan
through each and every child control of the form and check to see if any controls were both exposed and
if they were even partially covered by the label, and if so, the label had to be drawn to it, all properly
offset so that the portion covered by the label would have just that portion redrawn. This process was not
rocket science, it was very convenient and even impressive, but it also had a program execution speed
cost. This was because the label's text was not drawn just once to the form’s background surface, but it
was also individually drawn, in whole or in part, to each and every exposed form control it overlapped.
This exact same process will be demonstrated in code later in this article, and even later with images
near the end of the next article, Emulating VB6 Image Control Features Under VB.NET, on page 243.
To allow for the fastest overall execution speed, VB.NET does what VB6 should have done; it allows a
label to present a transparent background, by default, only to its parent container. Because, typically, in
most cases its parent is the only surface it will ever need to be transparent to, this schema greatly
simplifies and accelerates the code. Mind you, a parent control is always considered to be a container for
its child controls, which in turn provides any child control with its local coordinate system that will
determine where a control will be positioned on the screen.
VB6 supported certain clearly defined container controls, such as a PictureBox, GroupBox, or Form. But
under VB.NET, every control that is declared as a parent to another control automatically becomes a
“container”, albeit perhaps a primitive one without a Collection object for its subordinate controls. This
is not a new concept freshly introduced by .NET, but it is something that has always been available to all
knowledgeable developers all the way back to when Microsoft Windows was first introduced.
Rendering Transparency Labels Over Controls.
VB.NET treats every surface as if it were a drawing canvas, which it refers to on a technical level as a
Device Context or as a Graphics Interface, which is a rectangular area with a pixel length and width.
Anything drawn to it is subject to a relative location from its top-left corner, which to subordinate controls is
always assumed to be local coordinates 0,0. But the surface itself, such as a PictureBox, is also a child of at
least a form, where the PictureBox’s location is relative to the top-left corner of the form’s client area, which
is treated as coordinate 0,0 to the PictureBox, though even the client area is a child of the main form, and the
main form itself is a child of the desktop, and all these linked controls have a “home” location that their child
controls all treat as 0,0, though in genuine screen coordinates, the actual home location relative to the top-
left corner of the screen is seldom physically 0,0.
 Page 240

NOTE: To be technical, from an engineer’s point of view, the “home” location for a graphical
system would be the bottom-left corner of the screen, being a circle’s upper-right quadrant,
representing 90 degrees in a 360 degree circle, being the only quadrant where the horizontal and
vertical axis, X and Y, are both positive. Indeed, internally, this is exactly how the Graphical User
Interface (GUI) of Windows works. Having been for almost a decade a software engineer at an
industry standard CAD/CAM company, I am all too familiar with this process. However, Microsoft
wisely inverted the vertical axis in order to make the coordinate system simpler for users, who
usually think in terms of starting at the top of the page and moving downward, instead of upwards.
What is important here is that an object’s canvas does not go on to infinity, so anything that is “drawn”
outside the displayable range of its parent object (its container) is simply clipped off. For example, if we
have a parent canvas that is sized to 300 by 300 pixels, but we are drawing an image onto it that starts at
coordinate 500,500, then that image will not be exposed through the parental viewable ‘window’ at all;
its display data will simply be discarded because the image would have been drawn outside the defined
display range of the canvas, unless we change the target coordinate for the top-left corner of the image
so that it will be painted within an exposed area of the canvas, such as 100,100.
In understanding this, the process of placing a label with a transparent background over an image under
VB.NET becomes quite simple to do. It requires just three quick steps: 1) Set the label’s parent to the
control it is over, because background transparency will only work between a child and its parent. 2) Set
the background color of the label to Transparent. 3) Set the label’s Left and Top location to relative
offsets within a range that will make it displayable within its new parent’s exposed canvas.
That is it! So where does all the frustration come from? It comes from not ensuring that the relative
offsets of the label will place it within the display surface of its new parent. Whatever the label’s Top and
Left properties were set to before will determine its initial offset within its new parent’s home corner if
they are not updated. So, if a 80 by 80 pixel PictureBox has its top-left corner at coordinate 100,100,
then a 64 by 24 label, whose parent is presently the form, though it might appear to be centered over the
PictureBox at design time, will simply disappear from view, because its top-left offset to its old parent
control (the form) was set to 136,108. This offset, which will never automatically update (nor should it
ever be expected to, just as it did not do so even under VB6), will consequentially become its offset
under its new parent’s relative home location. These coordinates, if not corrected, can push its new
relative display location completely off the PictureBox’s canvas, rightward and downward.
Consider the following code, which we can place in the Form Load event of a new project with a
PictureBox and a Label present, and it will simply center the label’s text over the PictureBox:
Me.Label1.Parent = Me.PictureBox1 'Required: this must be done at runtime (there is no, nor should there be, an IDE property)
Me.Label1.BackColor = Color.Transparent 'Required: this can be alternatively set from the IDE at design time under the WEB colors
' The following step is required at runtime if you center the label within its NEW parent (Note that '\' indicates Integer Division)
Me.Label1.Location = New Point((Me.PictureBox1.Width - Me.Label1.Width) \ 2, (Me.PictureBox1.Height - Me.Label1.Height) \ 2)

As we can see in the above, we need to set these lines at runtime. We must set the parent, we must set the
background color to Transparent, and we must ensure its origin offset is relative to its new parent.
However, in most cases we will not want to center the label over its new parent. Typically, at
development time we would want to position the label and the image to the locations where we will
want to see them at runtime, just like we may have done under VB6. We can do this quite easily by
simply subtracting the image’s top and left offsets from the label’s top and left offsets.
To demonstrate, drop a PictureBox and a Label on a form. Set the Label, Label1, to some text, such as
“Which is X and Z?” Set the PictureBox, PictureBox1, to some image. Position PictureBox1 where we
want it, then move Label1 somewhere over the image (we may need to select Format / Order / Bring to
Front to also display Label1 on top of PictureBox1, just so we will not lose track of where we placed it).
Now, just add the following block of code in our Form Load event:
 Page 241

'Set Label1 over Picture1, relative to their current form-based positioning.


With Me.Label1
.Parent = Me.PictureBox1 'set Label1 parent to Picture1.
.BackColor = Color.Transparent 'set Label1 background color to transparent.
.Left -= .Parent.Left 'adjust to relative coordinates of new parent.
.Top -= .Parent.Top
End With '(too bad we cannot do this: .Location = .Location.Subtract(.Parent.Location))

When we run this, by appearances we will see the text displayed exactly where
we had placed it on the form during development.

Rendering Text Directly onto Controls with Ease.


A faster-executing approach is to simply draw text directly on a control, such as a PictureBox Image or
even to the form itself. In this case, all we need, again, is a PictureBox with an image and a hidden or
invisible Label. We can also do this with raw text, but then we have to come up with font information,
though we could easily cook it from scratch (see How to Perform E-Z Font Manipulation at Runtime
Under VB.NET on page 234), or borrow it from another object, such as a control or the form.
Consider the following overly bloated code from my first-ever VB attempt, faithfully implementing all
the rules laid down by Microsoft and dutifully parroted by online gurus (I did this because I was just
learning at the time, but kept it to demonstrate later how little code is really needed):
Private Sub DrawTextOnImage(ByRef picBox As PictureBox, ByRef Lbl As Label)
' Read and save the Image Dimensions of the PictureBox to a Size structure.
Dim TmpSize As System.Drawing.Size 'these 3 lines can be shortened to Dim TmpSize As Size = picBox.Image.Size.
TmpSize.Height = picBox.Image.Height '(NOTE that System.Drawing namespace does not need to be specified).
TmpSize.Width = picBox.Image.Width
' Create a new Bitmap object of a size defined by the Size structure and also grab a copy of the image data at the same time.
Dim NewBitmap As New System.Drawing.Bitmap(picBox.Image, TmpSize) 'TmpSize parameter not really needed (no rescaling).
' Create a Graphical interface object that can be used to directly interact with the new Bitmap image.
Dim Graphic As System.Drawing.Graphics = System.Drawing.Graphics.FromImage(NewBitmap)
' Set up a color 'brush' to use for drawing text through the graphical interface object and set it to Black.
Dim Brush As New SolidBrush(Lbl.ForeColor) 'set the color of the text (the color to paint the text with).
' Compute how to relatively position the label text on the bitmap
Dim X As Single = CSng(Lbl.Left - picBox.Left) 'Adjust to relative coordinates within the target Bitmap object.
Dim Y As Single = CSng(Lbl.Top - picBox.Top) 'NOTE: We can actually use Integers instead of Singles here.
' Draw the Label Text on the Bitmap thru the Graphic interface using text, a font, a brush, and local starting coordinates.
Graphic.DrawString(Lbl.Text, Lbl.Font, Brush, X, Y)
' Update the PictureBox image object by assigning to it the new, updated bitmap copy.
picBox.Image = NewBitmap 'set the Image reference pointer to the new Bitmap object.
' Dispose of what we do not need (we do not dispose of Bitmap object because the PictureBox image now references it).
Brush.Dispose() 'destroy resources that we no longer need.
Graphic.Dispose()
End Sub

Supplied with a PictureBox, we first create a new bitmap, sizing it by employing the PictureBox image
size and current graphic contents. We next set up a graphics interface so that we can perform any
drawing directly onto the bitmap copy we had extracted from the PictureBox. We then define a color
brush to draw the text with and compute how to position the label on the image. We then draw the text
directly to the bitmap using the text from the label, its font definition, plus the brush and the computed
coordinates to paint and position the text. Once completed, we assign the new bitmap to the PictureBox
image, then dispose of what objects we can, though we do not dispose of NewBitmap because its data is
now being used by the PictureBox – its old data it replaces has been tossed aside, now unreferenced, and
will await disposal by the background GarbageCollector process that is always running.
The above listing is along the lines of how most gurus will show us how to perform this task. However, I
am always looking to make my code run as fast as possible and use the fewest requisite instructions. In
reviewing the above first draft, I rewrote the method to skip setting aside objects to hold dimensions and
copies of bitmaps because all of this is actually redundant, and so I instead allow the graphical interface
to draw directly to the PictureBox’s image, because all the above prep work was not really necessary.
This results in fewer created objects, it removes superfluous processes, and executes considerably faster.
Mull over this much terser, more direct, and much faster version of the above code:
 Page 242

Private Sub DrawTextOnImage(ByRef picBox As PictureBox, ByRef Lbl As Label)


' Obtain a reference to the existing Graphical interface for the PictureBox image data.
Dim Graphic As Graphics = Graphics.FromImage(picBox.Image)
' Draw Label Text directly onto the Image in the PictureBox, using...
' 1) The Font information associated with the Label, 2) A color brush for painting the text,
' and 3) Computing how to relatively locate the label text onto the PictureBox Image.
Dim Brush As New SolidBrush(Lbl.ForeColor) 'set the color of the text (the color to paint the text with).
' Note: Automatic Csng() done within drawing method, below, for integer coordinate calculations.
Graphic.DrawString(Lbl.Text, Lbl.Font, Brush, Lbl.Left - picBox.Left, Lbl.Top - picBox.Top)
Brush.Dispose() 'dispose of the objects we no longer need.
Graphic.Dispose()
End Sub

NOTE: Because the text is now embedded in the run-time image (but not within the design-time image), the text is persistent
(the update does not disappear after a Refresh). But, if we require overlaid text labels when we change images, whether
refreshing an image from an ImageList, file, resource pool, or because the images are changing to simulate animation, we
will need to invoke the above method again for each image update, as opposed to using the earlier described methods.

Rendering Text across Multiple Controls with Ease.


If we have a label that must be displayed across not just
its parent, such as the form, but also across one or more
other controls, such as PictureBoxes, we can adapt the
following code to enable this functionality. This example
assumes that Label1 is the label that will be drawn across
the form and to one or more controls:
NOTE: Form1 is Label1’s parent to make calculations easier. All the images
were huge and were manually-stretched (compressed).
'-------------------------------------------------------------------------------
'Demonstrate drawing a Label, Label1, on a form and across form controls.
'-------------------------------------------------------------------------------
Public Class Form1
' Initialize by rendering label that will cover controls to not paint its background.
Private Sub Form1_Load(sender As System.Object, e As EventArgs) Handles MyBase.Load.
Me.Label1.Visible = False 'make sure label covering controls is not visible.
End Sub

'---------------------------------------------------------------------------
'Paint form and layer Label on top of it and any child controls.
'---------------------------------------------------------------------------
Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
With Me.Label1
Dim brsh As New SolidBrush(.ForeColor) 'create brush to draw text on form.
e.Graphics.DrawString(.Text, .Font, brsh, .Location) 'draw text on form.
brsh.Dispose() 'clean up resources.
For Each ctl As Control In Me.Controls 'check each control on the form.
If ctl.Visible AndAlso ctl.Bounds.IntersectsWith(.Bounds) Then 'if label intersects with Ctl...
AddHandler ctl.Paint, AddressOf Ctl_Paint 'allow label to draw over control.
ctl.Refresh() 'force control's paint event to fire.
End If
Next
End With
End Sub

'---------------------------------------------------------------------------
'paint all controls covered by label.
'---------------------------------------------------------------------------
Private Sub Ctl_Paint(ByVal sender As Object, e As PaintEventArgs)
With CType(sender, Control) 'Note use of CType, not my fav, DirectCast.
Dim offsetLoc As Point = Point.Subtract(Me.Label1.Location,
New Size(.Location.X, .Location.Y)) 'compute offset location to Ctl.
With Me.Label1
Dim brsh As New SolidBrush(.ForeColor) 'create brush to draw text on control.
e.Graphics.DrawString(.Text, .Font, brsh, offsetLoc) 'draw text at computed offset.
brsh.Dispose() 'clean up resources.
End With
RemoveHandler .Paint, AddressOf Ctl_Paint 'no longer needed, so remove.
End With
End Sub
End Class
 Page 243

Emulating VB6 Image Control Features Under VB.NET.


When migrating to VB.NET, one component most VB6 users mourned the loss of were Image controls.
Unlike a PictureBox, if an Image control had loaded an icon or an image whose format featured
transparency, graphics behind it would bleed through any of its transparent regions, even if it covered
several controls. This was perfect for rendering very impressive graphics and animation. Sadly, Image
controls, like VB6-style Frames, Labels, Lines, and Shapes, were Lightweight or Windowless controls,
meaning they completely lacked a Device Context, also referred to as a Graphics Interface, and so they
simply drew their images directly onto any background control, such as a Form. This also meant they
lacked a Windows Handle and were thus classless, which consequently meant they were absolutely
useless to object-based platforms like .NET. But it was just as well, because they also noticeably slowed
program execution speed, especially if the form featured quite a number of them. They also suffered
unreliable issues in their layering emulation, where sometimes when one tried to click them, a control in
their background reacted instead. Further, some containers balked at them when we tried to add them to
its collection. And the worst problem was that most users over-used them, even using them when a
PictureBox would have served better and would not have caused the program to operate more slowly.
NOTE: VB.NET Labels work because they exhibit an object presence, but allow background transparency to emulate VB6
Label behavior. VB.NET Line and Shape controls work like VB.NET Labels.
Because it is not possible to use classless controls under VB.NET, if we need such functionality, we
must do what we Visual C++ developers had to do – we drew them ourselves. This is exactly what VB6
did for Lightweight controls – they did not actually have graphical interface display space allocated for
them, but simply painted themselves to any controls behind it that possessed a graphics interface. Hence,
the images they rendered at design-time were just placeholder images so we could visualize their display
presence and specify dimensions and properties, xuch as where on the form they would be drawn.
Hidden background code enabled us to click where these images were painted to trigger reactive events.
It was like we were seeing a ghost on the screen; it was not actually there, but it sure did seem like it.
I say VB.NET should feature object-based Image controls. In a limited way it already does; set a Label
or PictureBox BackColor property to Transparent and ensure any image features transparency, but this is
limited to exhibiting transparency only to its parenting control, which is usually the Form. But as
importantly, we must endeavor also to make any such emulation work swiftly. There is no point in doing
it if it runs slow and results in some screen flicker, which moving VB6 Image controls tended to do.
My very first consideration toward achieving this goal had been to use a VB.NET Form, because forms
can be borderless so that its client area will cover its whole surface to be like image controls, its client
area can be drawn to, and, importantly, it also features a property named TransparencyKey, which, if set
to a color value, will in turn adjust the opacity of its client area. So, if we were to set a form to be
borderless and if its transparency key was set to that of the desired transparency region of an image, we
should find ourselves with the ability to bring about a .NET version of Image controls.
Painting Images On A Form.
Before embarking on such a project, we need to start simple, to verify what we want to do, to ensure that
we can actually accomplish these things. Thus, we should first look at drawing images on a form in
order to perform the core feature of Image controls. It just so happens that I have two 32x32-pixel icons
in an About Box that I want to overlap for a game program I have been tinkering with. I had originally
written the game for VB6, but I have ported it over to VB.NET. Everything is working great, but the
graphics are not presently as pristine as the version running under VB6. This is the perfect test bed for
me because it involves floating cursors and round marbles that move across graphic objects over a
triangular board, featuring animation, shadows, and graphical zoom. Everything works, but the round
objects and fake cursors look like they are floating around on their own personal flying carpets, because
they presently appear to be gliding about on what are obviously square-shaped backgrounds.
 Page 244

So, I will simply try to first address these overlapping icons, creating a logo for my
game. Here is what it looks like in design mode (a red marble overlapping a round pit):
Of course, I would prefer the red marble not have a square corner overlapping the pit
icon, which ruins the intended effect. Presently they are defined on two 32x32 pixel
PictureBox controls (formerly VB6 Image controls), each with their icon images already
loaded to them, and of course they are displayed as flat with a BorderStyle set to None.
The controls are named Image1_0 (the pit image, Black.ico, at pixel location 20,20), and Image1_1 (the
red marble, Red.ico, at 6,3). For safety, their SizeMode parameter is set to StretchImage, though this is
not really necessary due to the icon size already matching their PictureBox containers.
Because I tend to group objects to keep them in order, I will group my PictureBox controls into an array
named Image1 to store references to these two controls at the top of the form code, but still within the
body of the Form. This just makes them easier to reference later:
Dim Image1(1) As PictureBox 'local image collection for two items: 0 and 1.

Next, somewhere within my Form_Load event I am going to assign these two controls to the array.
Also, because at run-time I will want them completely out of the way, visually anyway, so I can draw
them during a paint event without having to worry about the controls being painted over my work
afterwards (a Form Paint event will do that as it refreshes its controls), I will also render them invisible:
Me.Image1(0) = Image1_0 'build a PictureBox reference container list (collection).
Me.Image1(1) = Image1_1 'You could just do this: Dim Image1() As PictureBox = {Image1_0, Image1_1}.
'NOTE: referencing the objects through Image1() accesses the very same objects that are pointed to by Image1_0 and Image1_1.
Me.Image1(0).Visible = False 'set both logo PictureBoxes invisible.
Me.Image1(1).Visible = False 'we will be drawing them to the form in a Paint event.

The preliminary groundwork is now complete. All that is left to do is to paint them as needed.
The choice of the event through which to paint them might seem to be a matter of personal preference to
many new programmers; to use either the Paint or the OnPaint events. After all, both will fire before
controls on the form are painted. There is just cause for using either, though to a great many developers,
sometimes the reasons may seem mysterious, or it just seems that one somehow works, or works better
in some situations than others. However, there is an important reason these two separate events exist.
The Paint event fires after the form background is repainted and after any form background image or
form-level text is rendered (if they exist), though, as I said, before any of its child controls have been
painted. Once the Paint event exits, it ensures that each visible child control will process their own
Paint event that triggers any time a part of their assigned display surface has been altered, thus rendering
their drawing areas “invalidated” (this means that its screen-painted image no longer validly reflects its
current embedded image), which form background clearing and painting definitely does to them.
Unlike form Paint, OnPaint will fire immediately after the form background is erased, but before any
form image or form text is drawn, and before a Paint event fires. This method is useful for changing the
form’s background image, its text, or its font before they are rendered. By implementing it, however, the
form Paint event will not fire unless we invoke MyBase.Paint at the end of our OnPaint code, which
we must do, because our OnPaint code over-rides default code that would have invoked MyBase.Paint,
which does most of the form's rendering duties, even if we do not write our own Paint event code!
Developers rightly prefer the Form_Paint event because it ensures child controls are rendered and they can also
choose the Paint event right from the Method menu of the Form and move on, but it is worth the time to construct
the OnPaint event when we want to initialize its form image or text at the start of the event. Besides, there is
nothing to it. Simply remember that it starts with “Protected Overrides”. So all we have to do is type “Prot”, then
Space, then “Ov”, then DownArrow, DownArrow,
DownArrow, Space, to select “Overrides” from a
dropdown menu, then type “OnPai(”, exactly like that,
and suddenly I see all the following appearing in my code:
 Page 245

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)


MyBase.OnPaint(e)_
End Sub

NOTE: Though the example I am building here will later rightly use the Paint event, I will use the form-based OnPaint
event for now, just to familiarize you with it. Please note this event's lack of a sender parameter, which is not necessary here
because the sender is already known, being the form the ON-event is tied to (Me). Further, no Handles clause is required.
And my cursor is blinking at the end of the statement ending in “ (e)”. And this is good, because we
want to place the rest of our paint code after invoking the OnPaint method of the base class (MyBase).
Notice that the event argument, e, is defined as PaintEventArg. This is perfect, because we can use this
argument to invoke the DrawImage method we need to render a bitmap image onto the form’s surface.
The way most online gurus tell us to paint images involves creating a Bitmap object, grabbing a copy of
an image into them, and then drawing that bitmap onto the form. The biggest advantage to using a
Bitmap object is that it has full access to the MakeTransparent method, which renders a selected color
as a transparency key, and the GetPixel method, which grabs a color from a specified bitmap pixel
coordinate, working just like the VB6 Point command. So, this involves created objects that must be
disposed of. Fine; let us try that to start, just to settle our curiosity about what those gurus rant about.
Consider the following initial version of my OnPaint event, following the guru recommendations:
'*******************************************************************************
' Subroutine Name : OnPaint event (takes place immediately after background erasure, but before form image, text, or controls).
' Purpose : paint graphics that overlap with transparency.
'*******************************************************************************
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
MyBase.OnPaint(e) 'do some required housekeeping first.
Dim Bmp0 As New Bitmap(Me.Image1(0).Image) 'create a new bitmap, copied from the 'lower' image.
e.Graphics.DrawImage(Bmp0, Me.Image1(0).Location) 'draw the 'lower' image onto the form (icon background matches form).
Dim Bmp1 As New Bitmap(Me.Image1(1).Image) 'grab copy of 'upper' image (new instance for disposal purposes).
Bmp1.MakeTransparent(Bmp1.GetPixel(0, 0)) '(re-)render a transparent border on upper image.
e.Graphics.DrawImage(Bmp1, Me.Image1(1).Location) 'draw 'upper' image over 'lower' image, minus transparent regions.
Bmp0.Dispose() 'be sure to clean up created resources that are no longer used to
Bmp1.Dispose() ' avoid memory leaks and bypass waiting for garbage collection.
MyBase.Paint() 'BE SURE TO CLOSE OUT OnPaint() WITH MyBase.Paint()!
End Sub

Above, I created two separate Bitmap objects so that I can easily dispose of them afterward (it
is a good idea to check each control we create to see if they feature a Dispose method, because
we can destroy their resources immediately, much like VB6 did by assigning “Nothing” to an
object – well... VB6 did not actually destroy it right away...). Also notice that the “e” event
argument parameter enables access to the graphical interface to the form ( e.Graphics).
Notice that with the first image, Image1(0) I simply drew the image on the form using the DrawImage
method exposed by the “e” PaintEventArg, which is associated with the form being operated on. Next, I
created another bitmap from Image1(1), applied a transparency color to it by grabbing the color of the
pixel at coordinate 0,0 of the bitmap, which I knew to be a location that was set to a transparency region
of the image, being its border. I then drew that image to the form at the coordinates defined for the
source PictureBox. Finally, I disposed of (destroyed) the resources for my created Bitmap objects.
Try it. Place a couple of small PictureBox controls on a form, name them Image1_0 and Image1_1,
place an icon or image that have a transparent background into each (setting the PictureBox SizeMode to
AutoSize or StretchImage is also helpful), overlap Image1_1 above Image1_0, then apply the coding that
I have laid out below. Using just the following code renders them overlapped perfectly:
Public Class Form1
Dim Image1() As PictureBox = {Image1_0, Image1_1} 'local image collection for two items.

Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load


For Each img As PictureBox In Me.Image1
img.Visible = False 'set both logo PictureBoxes invisible.
Next
End Sub

'*******************************************************************************
' Subroutine Name : OnPaint event (takes place immediately after background painting, but before form image, text, or controls).
' Purpose : paint graphics that overlap with transparency.
'*******************************************************************************
 Page 246

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)


MyBase.OnPaint(e) 'do some required housekeeping first.
Dim Bmp0 As New Bitmap(Me.Image1(0).Image) 'create a new bitmap, copied from the 'lower' image.
e.Graphics.DrawImage(Bmp0, Me.Image1(0).Location) 'draw the 'lower' image onto the form (icon background matches form).
Dim Bmp1 As New Bitmap(Me.Image1(1).Image) 'grab copy of 'upper' image (new instance for disposal purposes).
Bmp1.MakeTransparent(Bmp1.GetPixel(0, 0)) '(re-)render a transparent border on the upper image
e.Graphics.DrawImage(Bmp1, Me.Image1(1).Location) 'draw 'upper' image over 'lower' image, minus transparent regions.
Bmp0.Dispose() 'be sure to clean up created resources that are no longer used to
Bmp1.Dispose() ' avoid memory leaks and bypass waiting for garbage collection.
MyBase.Paint() 'BE SURE TO CLOSE OUT OnPaint() WITH MyBase.Paint()!
End Sub
End Class

When I studied this more closely, I recognized lots of waste (sometimes I wonder if I have OCD). Why
must we create a bitmap clone of an image just so we can render that image to the form, totally ignoring
the original image we had copied it from? In the second place, we might require a reference to a Bitmap
so to gain access to the MakeTransparent and GetPixel methods, but that does not actually mean we
have to create a separate clone of the second image just to get it. Why not simply reference the original?
We must remember that to use a reference variable, we do not actually need to assign a brand new object
to it; we can just as easily assign a pre-existing object to the variable, which would not be a new
instance. Thus, why not simply declare a reference pointer and have it point to the master image,
therefore using resources that already exist rather than creating temporary new ones that will just as
quickly be discarded afterward? Besides, Bitmaps and Images in fact have identical object signatures.
With that in mind, consider my revision to our OnPaint event, which also runs much faster:
'*******************************************************************************
' Subroutine Name : OnPaint event (happens immediately after background erasure, but before form image, text, or controls).
' Purpose : paint graphics that overlap with transparency.
'*******************************************************************************
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
MyBase.OnPaint(e) 'do some required housekeeping first.
e.Graphics.DrawImage(Me.Image1(0).Image, Me.Image1(0).Location) 'draw 'lower' image DIRECTLY from original image.
Dim Bmp As Bitmap = DirectCast(Me.Image1(1).Image, Bitmap) 'point a Bitmap REFERENCE to 'upper' image.
Bmp.MakeTransparent(Bmp.GetPixel(0, 0)) 'render a transparent border on 'upper' image.
e.Graphics.DrawImage(Bmp, Me.Image1(1).Location) 'draw 'upper' image over 'lower' image, minus transparency.
MyBase.Paint() 'BE SURE TO CLOSE OUT OnPaint() WITH MyBase.Paint()!
End Sub 'We did not create new resources, so no need to clean up.

Notice I rendered the lower image directly to the form using e.Graphics.Drawimage. The upper image
required access to a Bitmap, but that does not mean we must instantiate new objects. What we did
instead was define a bitmap reference that simply has access to the Image1(1) image, therefore working
with it directly. This way we can actually use the PictureBox image’s data, but we just treat it like a
Bitmap. We can do this through DirectCast because their data and formats are identical (this is also why
we can assign a Bitmap object directly to the Image property of a PictureBox). Therefore, when we
render the Bmp variable to the form using DrawImage, we are actually accessing and drawing directly
from the PictureBox image. And since the Bmp variable is simply a reference variable without a locally
instantiated resource that must be disposed of, we do not have to do any cleanup afterward. Indeed, were
we to suffer a brain fart and dispose of it, our image would disappear because its bitmap data is
presently assigned to the Image1(1) PictureBox.
When I take a really good look at even this shorter and faster code, I see that I am still doing far more
programming than I really need to, not to mention still using more resources than is required. It occurred
to me after I was finally done being impressed by it that I could have instead not used the resources of
PictureBox controls at all, let alone the additional time required to poke and prod and finally render their
images. Instead, I could have simply stored the icons, Black.ico and Red.ico, as icons in my program’s
resources and then just rendered them directly to my form during its paint event. Unlike images, when
we paint icons using the DrawIcon method, their transparency color is not rendered (actually, they are
not rendered when added to a PictureBox Image property, but they do let the PictureBox background
color bleed through its transparency regions when the PictureBox is displayed). If I were to paint them
to the background directly from the program resources, I would reduce my OnPaint method to just four
lines of code (two if I used the Paint event instead), because that would be all that would be required to
be done, and I would not have to worry at all about transparency colors, bitmap references, or anything!
 Page 247

The first thing I would need to do is place my two icons into the program’s resources. I add them to my
Icon resources by selecting Project / Properties, choose the Resources tab, select Icons from the
dropdown list, choose the drop triangle on Add Resources, and select Add Existing File. I then browse
to my game’s icon storage location and select my two icons, Black.ico and Red.ico. Finally, I hit the
Open button to load them into my application resources. Notice that it will automatically name the
resources by using the file names, less their .ICO extension; Black and Red. We can afterwards right-
click them in the resource display and rename them if we so desire. When we close the Properties tab,
the images references are automatically saved to the program’s XML-based ResX resource file. There is
no need to separately compile it, as was the case with VB6, because compiling is done automatically.
Next, delete all the current code except the form class shell, and also delete the two PictureBox controls.
Now, the only thing left to do is to render the icons. The black pit should have a top/left pixel coordinate
set at 20,20, and the Red marble should be at coordinate 6,3, above and left of the Black icon, over-
writing a portion of it (these were the old PictureBox locations for these marbles). A newer, sleeker, and
much shorter Paint event, replacing the deleted OnPaint event, would look something like this:
Private Sub Form_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
e.Graphics.DrawIcon(My.Resources.Black, 20, 20) 'render black icon at old Image1(0) coordinates: 20,20.
e.Graphics.DrawIcon(My.Resources.Red, 6, 3) 'render red icon at old Image1(1) coordinates: 6,3, overlap the black icon.
End Sub

Running this worked perfectly! Not only did I dramatically reduce the application’s resource load, but I
saved an incredible amount of execution time to boot.
You know, the main goal of this experimental endeavor is starting to look achievable. We have just
performed the core task of a VB6 Image control – to paint its image and keep it persistent.
My game includes a Training Wheels Mode (a Cheat Mode), where a cyan circle is
drawn around a pointed-to location if selecting it can lead to victory (well, as long as we
actually choose the various paths that it offers us – there are 32,768 possible solutions,
though everyone seems to think there is only one). The way I did this was to place a
Cyan circle around marbles or pits if choosing them can lead to victory; this circle being
displayed only when the cursor moves over the marble or pit in question. The way I had accomplished this
task under VB6 was to use an Image control that contained a simple circle Icon, Highlight.ico, and I moved
it to the exact location of the marble/pit in question. Finally, I set its visibility to True. I hang my head when I
admit that I had done it that way, which is grossly inefficient, but... Boy, did it work great!!!
But under VB.NET I find myself in a situation where I can no longer use a VB6 Image control to do
this, and so my alternatives are what they should have been in the first place. What I really should have
done was to simply draw a circle there. However, the cheat using an Image control and Icon was just too
easy. This option may be out of the question, but because we are looking at ideas on how to emulate the
things that a VB6 Image control did, let us start by trying to display an icon over the top of an image.
Painting on Controls.
I have a 16-color 32x32 Icon named Highlight.ico. I add it to my Icon resources just as I did for
my Black and Red icons. I select Project / Properties, choose the Resources tab, select Icons
from the left dropdown list, choose the drop triangle on Add Resources, and select Add Existing File. I
then browse to my game’s icon storage location and select the Highlight.ico file. I then hit the Open
button to load it into my application resources. Closing the Properties tab updates resource references.
Where I need to paint it is in the Paint event for each of the PictureBox controls that I may want to draw
the circle onto. If I try this in the form’s Paint event, the results would seem to be invisible. This is
because this event provides persistence only on the form’s primary surface, and so it will render them
behind the PictureBox controls that I really want to draw on top of, because these controls are placed in
front of the form’s surface since they are painted after the form’s surface is painted. By instead drawing
it to the actual PictureBox’s Paint event, I will know that when the control’s Paint event fires, it will
have already drawn its own image data. It now allows us to do our own thing on its canvas.
 Page 248

Afterward, behind the green curtain within the operating system, it will invoke a validation on the
control’s rectangular area to tell the system kernel that everything is still hunky-dory and that any
drawing we have done will be considered by the kernel's graphics driver as being valid.
Because I will be handling all the pits and marbles identically, I can write a generic paint event that will
handle all 15 of the needed 32x32-pixel PictureBoxes, which are named Image1_1 through Image1_15.
In the following sample code, we will assume a global Integer variable named ShowHelpIdx holds the pit
index being examined, as an Integer function named GetNameIndex will extract the index number of the
target control from its assigned control name,
'*******************************************************************************
' Subroutine Name : Image1_Paint – invoked by system after control is already painted.
' Purpose : Support Image1() collection. Allows us to amend our own embellishments.
' Comment : Event handlers for this method will be applied seperately.
'*******************************************************************************
Private Sub Image1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs)
If ShowHelpIdx = GetNameIndex(sender) Then 'is choice valid? (sender trailing value is _1 through _15).
Dim halo As New Icon(My.Resources.HighLight, 32, 32) 'so aquire circle resource icon (HighLight.ico), 32x32...
e.Graphics.DrawIcon(halo, 0, 0) 'draw icon on 32x32 sender image starting at its home location...
halo.Dispose() 'and finally remove the created resources.
End If
End Sub

Here, a global integer variable named ShowHelpIdx will be set to a location (pit) value, 1-15, if help can
be shown on that control. We first extract the ID of the control from the trailing digits in its name (i.e.,
Image1_12 has ID # 12) using the soon to be revealed GetNameIndex method, which will extract the
ID number from the end of the selected control’s name, and see if it matches ShowHelpIdx. If so, we will
paint a help circle atop this image. In this case, we extract our Highlight icon from our resources and
draw it onto the control, which matches it in size, 32 pixels wide by 32 pixels high.
Notice within the above code that I drew the icon at X and Y coordinates 0,0. This is because we are
laying it over the top of a 32x32 borderless PictureBox, whose local coordinate system we are using at
this point. Before you ask (if you are new to this), the event already “knows” which control to draw to
through its “sender” object and also its “e” parameter. This makes our work almost too easy.
And speaking of easy, since all we are doing is rendering the icon, we can further reduce the three lines
of code within the above If...End If block down to just one by rendering directly from our resources:
Private Sub Image1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs)
If ShowHelpIdx = GetNameIndex(sender) Then 'is choice valid? (sender trailing value is _1 thru _15).
e.Graphics.DrawIcon(My.Resources.HighLight, 0, 0) 'draw icon on 32x32 sender image starting at its home location...
End If
End Sub

Also, although we have 15 images supported by the Image1_Paint event, we might notice that there was no
Handles clause declared in the header of the method. This is nothing magical. Instead of attaching controls to
the event using Handles, I will instead attach the image controls to it in-code using the AddHandler
command. Though this topic is covered in the earlier extended note, “Notes on the Loss of VB6 Control
Arrays and How to Quickly Create Them” on page 80, I will briefly explain this useful technique here.
As we now know, I like ordering similar controls together in reference arrays. Normally, I simply name
them with a trailing index value, which is preceded by an underscore. Take Image1_1 or Image1_15, for
instance. This way I can keep sequential track of them and I can collect them into a common array.
For these PictureBox controls I have been featuring here, I declared their common array like this:
Public Const MaxSlots As Integer = 15 'set number of pits on the board.
Public Image1(MaxSlots) As PictureBox 'set aside room for storing the marble pit images.

I then fill the array with my controls in my Form_Load event:


' fill our collection of primary images on game (pits)
Image1(0) = ImgShadow '(Black marble I use for a shadow effect).
For Each Ctl As Control In Me.Controls 'check each control...
If TypeOf Ctl Is PictureBox AndAlso Ctl.Name.StartsWith("Image1_") Then 'if a PictureBox and its name starts with Image1_...
Image1(GetNameIndex(Ctl)) = DirectCast(Ctl, PictureBox) ' assign array element with it.
End If
Next
 Page 249

We could alternatively both declare and initialize the array directly and more quickly like this:
'create and fill our collection of primary images on game (pits).
Public Image1() As PictureBox = {ImgShadow, Image1_1, Image1_2, Image1_3, Image1_4, Image1_5, Image1_6, Image1_7,
Image1_8, Image1_9, Image1_10, Image1_11, Image1_12, Image1_13, Image1_14, Image1_15}

I next assign event handlers for Click, MouseMove, Paint, and perhaps any other required events:
'Assign event handlers by taking care of OOPL object referencing and For-Each looping.
' You can also apply this technique to members of a strongly-typed Generics List, if you prefer them over arrays.
For Each pic As PictureBox In Image1
If pic.Name <> "ImgShadow" Then 'do not apply events to the ImgShadow picturebox.
AddHandler pic.Paint, AddressOf Image1_Paint 'assign event handlers to each item in the Image1 array.
AddHandler pic.MouseMove, AddressOf Image1_MouseMove 'other methods you might want to also include...
AddHandler pic.Click, AddressOf Image1_Click
pic.BackColor = Color.Transparent 'be sure background will not bleed into image background.
End If
Next

This small bit of work does two important things. First, it keeps my event code looking tidy. Second, I am
able to index and enumerate (do a For...Each) through my control array, Image1(), if I need to (also note
that I actually do keep something in Image(0), which is my Shadow image to create a cool scaling shadow
3D effect when a ball is picked up from one pit, moved, and then dropped into another pit).
When I tested this code, I found that the icon is drawn to the form magnificently. And the best part is, the
transparent border and center associated with the icon is preserved and rendered transparently, leaving only a
Cyan circle to be drawn onto the target control. What we have just done is what Image controls did in VB6. Sadly,
a PictureBox background will bleed through a stored icon’s transparency regions, unless we also, as we did above,
set the PictureBox’s BackColor property to Transparent. If it were a Bitmap, it will draw a solid image. If it were
an Icon, it renders only its non-transparent portions of the image.
Drawing Image resources is just as easy as icons, but in that case we use the DrawImage function, as
shown earlier. We can also designate a transparency color for non-icons, also shown earlier.
Finally, in case anyone thought my GetNameIndex function did anything mysterious, here it is:
Module modGetNameIndex
'*******************************************************************************
' Subroutine Name : GetNameIndex
' Purpose : Return index based on Name (assume index value trails name after underscore).
' : Add as many type checks as you need. Example: Label1_13 for ID=13.
'*******************************************************************************
Public Function GetNameIndex(ByVal sender As Object) As Integer
Dim Nam As String = Nothing 'assign initial data so VB will not accuse us of processing an uninitialized variable.
Select Case sender.GetType.Name 'Get the Object control type name.
Case GetType(Label).Name 'Using GetType().Name is safer than using a text string name, such as “Label”.
Nam = DirectCast(sender, Label).Name 'Handle Labels, Get name.
Case GetType(PictureBox).Name
Nam = DirectCast(sender, PictureBox).Name 'Handle PictureBoxes. Get name.
Case GetType(Button).Name
Nam = DirectCast(sender, Button).Name 'Handle Button. Get name.
'If you need them, you can add more Case tests here...
End Select
'---------------------------------------------------------------------------------------
If Nam IsNot Nothing Then 'something to process? (this is the reason we initialized it to vbNullString).
Dim I As Integer = Nam.LastIndexOf("_"c) + 1 'find trailing underscore and add 1.
If CBool(I) Then 'found one? Non-zero means Yes (it was -1 if it had failed, before we added 1).
Return CInt(Nam.Substring(I)) 'yes, return trailing value (this works due to 0-based Substring method).
End If '(for Substring, the index value of I is 1 higher than Instr's 1-based index).
End If
Return -1 'return a flag to indicate Unsupported Type Encountered.
End Function
End Module

Drawing on Controls.
Now that we have attached our Image1_Paint event to all the PictureBoxes, let us take a look at an
alternative, as well as the proper way of doing the graphic work we just did, but this time by simply drawing
a Cyan circle from within the Image1_Paint event, which uses even less time and resources.
'*******************************************************************************
' Subroutine Name : Image1_Paint
' Purpose : Support Image1() collection.
'*******************************************************************************
Private Sub Image1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs)
Dim Index As Integer = GetNameIndex(sender) 'get index of control (the trailing value _1 through _15).
If ShowHelpIdx = Index Then 'is this choice valid?.
Dim pn As New Pen(Color.Cyan, 2.0!) 'set up a 2-pixel-wide Cyan pen.
e.Graphics.DrawEllipse(pn, Image1(Index).ClientRectangle) 'draw a circle within the client rectangle bounds (32x32).
pn.Dispose() 'release resources of our created pen.
End If
End Sub
 Page 250

Above, we got the index of the control from its name through our GetNameIndex method and saved it,
because we will also use it when we draw our circle via DrawEllipse. Although we can specify a default pen
right within the DrawEllipse method that will not cost us any resources, that pen is only 1 pixel wide, and it
is not sufficient enough to leave a significant enough visual impact for me on our image. This effect ended up
working and looking much better than my VB6 Image version.
So far, we have duplicated the core of what a VB6 Image control did, which is namely displaying
persistent images to the run-time screen. These experiments also demonstrate that employing a resource
image or icon is a superior alternative to a VB6 Image control; that drawing them directly to the form or
onto another control is a preferable alternative to overlapping more resource-hungry VB6 controls. The
paradox here is, however, this is exactly what VB6 Image Controls did and how they did it!
Image with Transparency Rendering Over Multiple Controls.
One thing VB6 Image controls did that we have not yet done is draw images with transparent borders
across multiple controls simultaneously, which is what my marbles must do when one is selected, raised,
moved, and dropped into a new slot, sliding across the form and other slots as needed.
But if we think we can just wait until all other controls are painted and then paint our marble to the form
based upon a shared coordinate system used by the other controls, we are going to find that when the
image is rendered, it will just draw only on the form’s surface, behind all the other controls. Most people
think they can just draw such an image to the background form. But what ends up happening is that the
program does exactly what it is told to do — the image is dutifully drawn on the background form,
which is layered behind the other controls that are, as the form’s children, actually layered in front of it.
Child objects are always painted after their parent object has already been painted.
To make the image appear on child controls is to take a look at the rectangular region where we want to
render the moving object, both where it was and where it will be moved slightly to, to craft a transitional
“movie effect”, like two frames of a movie, to create the illusion that it is moving. We must invalidate
both the original and the destination regions so the paint event can update them. This allows the system
to update only these small regions of the screen, maintaining persistence on the rest of the screen and
reduce any possibility of flicker. This can be done using the form’s Invalidate method, which passes a
rectangle for the area to invalidate to the system, which in turn adds it into its internal Update Region
cache that will accumulate all pending changes until the form’s Paint event is fired.
NOTE: Alternatively, instead of waiting for the system message queue to empty so that a paint message can be initiated
(Paint events do not fire until the message queue is idle), we could simply force it by instead sending an UpdateWindow
Pinvoke along with the handle of the form to make it update (Paint) immediately, though I have personally noticed no
conspicuous difference in operation so far that would actually merit taking such action.
We will not have to do anything but let the system repaint over the ‘old’ location, because we want to
erase our image from the place where it is currently rendered anyway. Instead, we want to focus only on
where we are moving the image to. We will of course have to render the object in the background form’s
Paint event, wherever the image will cover the background surface, between the “upper” controls, and
we will also have to render it on each of the controls it might be moving over by ensuring a refresh will
be sent to each control that the image will be crossing, even partially. In the Paint event for these
controls, we will have to compute the image’s relative position for each refreshed control and render the
image to that computed location, which is fortunately insanely easy to do. The control will automatically
clip any image portion that cannot be rendered to its own exposed Graphics Interface.
What all this boils down to is that we may have to render the image more than once, but always to the
form background, even if its rendering is hidden behind a control, for reasons of code simplicity, and
any additional times for each of the controls the rendered image covers or only partially covers.
Before we continue, I must point out that drawing can be expensive, time-wise, which is why a VB6
Image control slowed a program’s over-all execution speed. Although this is an ideal strategy, it does
involve time to check all form controls to see if it overlaps them, and to draw itself to each of them if
 Page 251

they do, properly offset to cover only portions that are overlapped. Spending too much time doing
something can force delays, and delays can result in flicker. Even great animation can look like ejected
matter from the south side of a north-bound horse if image flickering results. Regardless, under VB.NET
such effects still perform magnificently if we simply minimize any Image control emulation.
The process of executing this task is amazingly simple. Also, the absolute best choice for an animation
control is not a control at all, but rather an icon or a translucency-enabled image stored in a local variable or
in the program’s resources, using that as our Image “control” (an Icon resource stored in the Icon region of
the project resources returns an Icon object, and a Bitmap resource stored in the Image region of the project
resources returns an Image object). For example, consider the following header data in a form:
Protected imgXY As New Point(0, 0) 'Animation image top/left location for 32x32 Red.ico marble stored in program resources.
Private imgSize As Size = My.Resources.red.Size 'get icon image size.
Private XYoffset As New Size(4, 4) 'Animation increment values. Increase values for faster animation, decrease for slower.
Private imgRect As New Rectangle(imgXY, imgSize) 'Set aside initial rectangle reference variable.

The next thing to do is that each time we will move the object, we must invalidate the area where the
object is presently, update its coordinates to its new location, and finally invalidate that new area as well
(we could just invalidate the entire form, which is also a VB.NET option ( Me.Invalidate), but that is,
relatively, a slower process, because the entire form will then be repainted). For example, assuming
Point animXY holds a moving 32x32-pixel image’s current top-left coordinate:
Me.Invalidate(imgRect) 'invalidate the rectangular region at animXY for 32x32 pixels.
imgRect.Location = Point.Add(imgRect.Location, XYoffset) 'Update current position to its new location.
Me.Invalidate(imgRect) 'invalidate the destination rectangular region.

A control’s paint event is processed once the internal message queue is empty (idle), unless we preempt
it and force the Paint event to run immediately by issuing an UpdateWindow Pinvoke (Private Declare
Function UpdateWindow Lib "User32.dll" (ByVal hWnd As IntPtr) As Boolean ), although this should seldom, if ever
be necessary, and I have yet to ever have need of it in any of my code.
The first thing we will do in the form’s Paint event is to draw the image on the form at its new location
(its previous location will have already been “erased” when the form’s background was refreshed).
Next, we would check to see if the image we are moving will also cover any of our controls. If so, we
must be certain to issue a Refresh for each overlapped control. The Refresh method will invalidate a
control’s canvas, and thus queue the control for updates through a Paint event we assign to it, below:
Private Sub myForm_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
e.Graphics.DrawIcon(My.Resources.red, imgRect) 'Draw the image to animate (Red.ico) from the resources to the form background.
' Now scan for each control to see if they will intersect with the present image's location.
For Each Ctl As Control In Me.Controls
With Ctl 'If the control is visible and its Bounds, which is the control's bounding rectangle, intersects with the target...
If .Visible AndAlso .Bounds.IntersectsWith(imgRect) Then
' the following AddHandler line runs faster if we add it in a for-each for all form controls in the form's Load event:
AddHandler Ctl.Paint, AddressOf Ctl_Paint '(we get an error here until the Ctl_Paint event code is actually written)
.Refresh() 'Refresh will invalidate control if they intersect. This will cause the animation image to be drawn on it.
End If
End With
Next
End Sub

In our common Ctl_Paint event, it fires after the control’s primary image is already painted by its base
class, which afterwards chains into our event, firing only for the controls that were issued a refresh.
Here, we compute where the animated image will be relative to the current control and paint the image
over the top of it so that it precisely lines up to the image being drawn on the parent form beneath it:
Private Sub Ctl_Paint(ByVal sender As Object, ByVal e As PaintEventArgs)
' if the control intersects with the current location of the painted image, then paint the image onto the control.
Dim Ctl As Control = DirectCast(sender, Control)
With Ctl
If .Visible AndAlso .Bounds.IntersectsWith(ImgRct) Then 'Bounds is the control's bounding rectangle.
e.Graphics.DrawIcon(My.Resources.red, imgRect.X - .Left, imgRect.Y - .Top) 'draw the image offset from the control.
End If
' the RemoveHandler line is not required if we instead placed an AddHandler to Ctl_Paint for each control in the form's load event.
RemoveHandler Ctl.Paint, AddressOf Ctl_Paint 'remove this handler from control, previously added by Form_Paint.
End With
End Sub

The above method works extraordinarily well, and I will demonstrate it next with complete code.
 Page 252

Emulating VB6 Image Controls Using a PictureBox.


Let us take another go at this by using the techniques employed by
VB6 Image controls. Actually, most of this code has just been
shown above in non-committal sample form, but I think it bears
repeating, especially in a form that features complete code that we
can copy and paste to our system to more practically demonstrate its
functionality (and especially to a cool-looking demo).
For this example I will simply draw an image that will travel at an
angle across the form and any controls that it will encounter, and
reflect at the form’s boundaries; bouncing around it.
The first step is to create a new project that I named TestImageControl1. I sized it to about one quarter
my screen size, though we are free to size it however we want to. Just try to keep the client area
relatively square, but being perfectly square is not important or desired. Just quickly “eyeball” it. Indeed,
the less square it is, the more interesting the object bounce movement will end up being.
The second step is to add PictureBoxes. Add as many as we want, size and position them however we
want, and load them with pictures or icons we have stored on our system. We will probably need to set
each PictureBox’s SizeMode parameter to StretchImage or AutoSize to fill each PictureBox with its
image. We can even assign a huge image, say a desktop image, to the form BackgroundImage property.
The next step is to add a final PictureBox, named Animation, and fill it with a bitmap image or icon
with an image border that is either transparent, or else a solid, unique color that we will in turn render
transparent (an icon stored in a PictureBox Image property retains its transparency, though displaying
the PictureBox itself will cause its background color to bleed through the icon’s transparency regions).
Next, add a timer control, Timer1, to the form and set its Interval parameter to 10 (default).
Finally, add the following code to our form:
Public Class Form1
'*******************************************************************************
'Method Name: Form1_Load
'Purpose : Initialize project.
'*******************************************************************************
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
With Me.Animation
.Visible = False 'turn off object to be painted.
If .Image Is Nothing Then 'if an image is not loaded (oops!)...
.Image = New Bitmap(.Width, .Height) 'create blank image (of background color) as a background to draw to.
Graphics.FromImage(.Image).Clear(.BackColor) 'clear image using the background color of PictureBox1.
End If
Dim Bmp As Bitmap = DirectCast(.Image, Bitmap) 'get a reference to the animation PictureBox image.
Bmp.MakeTransparent(Bmp.GetPixel(0, 0)) 'render a transparent border on the image if not transparent already.
End With
For Each Ctl As Control In Controls 'add the Ctl_Paint event handler to each control on the form...
If Ctl.Visible Then '(except for the invisible Animation image control).
AddHandler Ctl.Paint, AddressOf Ctl_Paint
End If
Next
Me.Timer1.Enabled = True 'Enable the animation timer.
End Sub

'*******************************************************************************
'Method Name: Form1_Paint
'Purpose : Process painting of current form.
'*******************************************************************************
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
e.Graphics.DrawImage(Me.Animation.Image, Me.Animation.Left, Me.Animation.Top) 'draw image on form background.
' scan for each control that intersects with the animation control
For Each Ctl As Control In Controls
If Ctl.Visible AndAlso Ctl.ClientRectangle.IntersectsWith(Me.Animation.ClientRectangle) Then
' Refresh the control if it intersects with the animation image. This will cause...
' the animation image to be drawn on it through the control's paint event.
Ctl.Refresh()
End If
Next
End Sub

'*******************************************************************************
'Method Name: Ctl_Paint
'Purpose : Paint the animation image to each intersected control.
'*******************************************************************************
 Page 253

Private Sub Ctl_Paint(ByVal sender As Object, ByVal e As PaintEventArgs)


Dim Ctl As Control = DirectCast(sender, Control)
' if the control intersects with the animation image, then draw the image onto the control
If Ctl.Visible AndAlso Ctl.ClientRectangle.IntersectsWith(Me.Animation.ClientRectangle) Then
e.Graphics.DrawImage(Me.Animation.Image, Me.Animation.Left - Ctl.Left, Me.Animation.Top - Ctl.Top)
End If
End Sub

'*******************************************************************************
'Method Name: Timer1_Tick
'Purpose : Move the Animation image across the screen.
'*******************************************************************************
Private XYoffset As New Size(4, 4) 'increase values for faster, lower for slower

Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Timer1.Tick


With Me.Animation
Me.Invalidate(.Bounds) 'allow old location to be refreshed to original data.
.Location = Point.Add(.Location, XYoffset) 'apply movement offsets (higher values for faster, lower for slower).
Me.Invalidate(.Bounds) 'tell form Paint event to also update the new target region.
' if the animation image slides out of sight, then change its direction.
If .Left + .Width >= Me.ClientRectangle.Width OrElse .Left <= 0 Then
XYoffset.Width = -XYoffset.Width
End If
If .Top + .Height >= Me.ClientRectangle.Height OrElse .Top <= 0 Then
XYoffset.Height = -XYoffset.Height
End If
End With
End Sub
End Class

Now run it to test it. The small bitmap image should be seen bouncing around the form, crossing all the
other images displayed upon it. The less square the form is, the more varied the bouncing.
Emulating VB6 Image Controls Using a Resource Icon.
As demonstrated earlier in this article, it was recommended that we use an icon or bitmap stored in our
resources, or even an ImageList, to use in place of a VB6-type “Image control”. This requires only
minor changes to our program. I will again use my red marble icon, Red.ico, expressed in the following
code as “My.Resources.Red”, but we should of course change this to use your own preferred icon.
NOTE: If we do not want to go through all the following work again, then simply use the previous project, remove the
Animation PictureBox, and then add an icon or image to your project resources.
The first step is to simply create a new project, which I called TestImageControl2. As before, I sized it to
about one quarter my screen size, though, again, we are free to size it however we wish to. Just try to
keep the client area relatively square (but not quite – for better bouncing effects).
Again, the second step is to add PictureBoxes. Add as many as we want, size and position them however
we want, and load them with images or icons we have stored on our system. We will again probably
need to set each PictureBox SizeMode parameter to StretchImage or AutoSize to fill each PictureBox
with its image.
The next step is different. Instead of adding a final PictureBox named Animation, as we did previously,
we will simply add an icon to our project’s resources. Add any icon of our choosing, but unlike the
previous examples, we can now use an ordinary icon with a transparent border.
Next, as before, add a timer control, Timer1, to the form with its Interval parameter set to 10.
Finally, add the following code to our form (or replace the existing code with it):
Public Class Form1
Protected Img As Icon = My.Resources.Red 'image to animate and paint.
Protected ImgRct As Rectangle 'location/size of Img.
Protected ImgWH As Size 'storage for dimensions of Img.
Protected animXY As New Point(0, 0) 'start X/Y coordinate for Img.
'Animation movement rate
Protected XYoffset As New Size(4, 4) 'increase value for faster, lower for slower.

'*******************************************************************************
'Method Name: Form1_Load
'Purpose : Initialize project
'*******************************************************************************
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
ImgWH = Img.Size 'gather dimensions of image to paint.
ImgRct = New Rectangle(animXY, ImgWH) 'set up initial positioning of image to paint.
' add the Control_Paint event handler to each control on the form.
For Each Ctl As Control In Controls
AddHandler Ctl.Paint, AddressOf Ctl_Paint
 Page 254

Next
Me.Timer1.Enabled = True 'Enable the animation timer.
End Sub

'*******************************************************************************
'Method Name: Form1_Paint
'Purpose : Process painting of current form.
'*******************************************************************************
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
' draw the image on the form background.
e.Graphics.DrawIcon(Img, animXY.X, animXY.Y)
' scan for each control that intersects with the animation control.
For Each Ctl As Control In Controls
With Ctl 'If the control is visible and its Bounds, which is the controls bounding rectangle, intersects with the target...
If .Visible AndAlso .Bounds.IntersectsWith(ImgRct) Then
.Refresh() 'Refresh will invalidate Cntrl if they intersect. This causes the animation image to be drawn on it.
End If
End With
Next
End Sub

'*******************************************************************************
'Method Name: Ctl_Paint
'Purpose : Paint the animation image to each intersected control.
'*******************************************************************************
Private Sub Ctl_Paint(ByVal sender As Object, ByVal e As PaintEventArgs)
' if the control intersects with the animation image, then draw the image onto the control.
With DirectCast(sender, Control)
If .Visible AndAlso .Bounds.IntersectsWith(ImgRct) Then 'Bounds is the control's bounding rectangle.
e.Graphics.DrawIcon(Img, animXY.X - .Left, animXY.Y - .Top) 'draw the image offset from the control.
End If
End With
End Sub

'*******************************************************************************
'Method Name: Timer1_Tick
'Purpose : Move the Animation image across the screen.
'*******************************************************************************
Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Timer1.Tick
' allow old location to be refreshed to original data.
Me.Invalidate(ImgRct)
' ensure affected controls get possible remnants removed in case the offset update will no longer intersect with it.
For Each Ctl As Control In Controls
With Ctl 'If the control is visible and its Bounds intersected with the target...
If .Visible AndAlso .Bounds.IntersectsWith(ImgRct) Then
Ctl.Invalidate()
End If
End With
Next
' apply movement offsets (higher values for faster, lower for slower).
animXY = Point.Add(animXY, XYoffset)
' update Img positioning.
ImgRct = New Rectangle(animXY, ImgWH)
' tell form Paint event to also update the new location.
Me.Invalidate(ImgRct)
' if the animation image slides out of sight, then flip its direction.
If animXY.X + ImgWH.Width >= Me.ClientRectangle.Width OrElse animXY.X <= 0 Then
XYoffset.Width = -XYoffset.Width
End If
If animXY.Y + ImgWH.Height >= Me.ClientRectangle.Height OrElse animXY.Y <= 0 Then
XYoffset.Height = -XYoffset.Height
End If
End Sub
End Class

Emulating VB6 Image Controls Using a Borderless Form.


I had seen promise in the idea of using borderless forms to stand in for controls, which, if we recall, had
been my initial idea in this article. The only thing we must consider when using this technique, which is
normally quite impressive, is that we must think in terms of screen coordinates rather than local
coordinates (client coordinates; in relation to the background form or window). This is because a form,
except for an MDI child form, is always subordinate to the screen, even if it set its own Owner property
to another form. I recall that I had used a Pinvoke in C++ and VB6 that could quickly convert local to
screen coordinates named ClientToScreen. Under VB.NET, the C++/VB6 code would require the
following translated declarations:
Private Structure POINTAPI 'NOTE: You can use a VB.NET Point rather than this POINTAPI structure – their data signatures are IDENTICAL.
Dim X As Integer 'horizontal point from left edge.
Dim Y As Integer 'vertical point from top edge.
End Structure

Private Declare Function ClientToScreen Lib "user32" (ByVal hWnd As IntPtr, ByRef lpPoint As POINTAPI) As Integer
 Page 255

To use it, I would fill a variable of type POINTAPI to a coordinate, and then invoke ClientToScreen,
providing the window handle of my form that the local coordinate is relative to. For example:
Dim pt As New POINTAPI 'defn a structure variable (I use New to avoid uninitialized variable warnings, tho Dim pt As Point is OK).
pt.X = Image1(CurIndex).Left 'fill with top/left corner coordinate of a PictureBox control.
pt.Y = Image1(CurIndex).Top 'You could also have just used: Dim pt As New Point(Image1(CurIndex).Location).
ClientToScreen(Me.Handle, pt) 'convert to screen-local coordinates. Struct pt will afterward contain the new Screen coordinates.

But VB.NET makes this even easier! This is an example of something that we will discover a lot as we snoop
around VB.NET. VB.NET has this functionality build right in to a form or control’s PointToScreen method,
and conversely, its PointToClient method. To use it and replace all the above is as easy as this:
With Image1(CurIndex)
Dim pt As Point = .Parent.PointToScreen(.Location) 'compute local pt structure values to screen coordinate.
End With 'reference control's parent in case placed on container other than form.

Easy-Breezy. Now all I need to do is add a borderless form, load an image or icon to it, which I will
accomplish by using a borderless PictureBox control, sized to the same dimensions as my form (I use a
PictureBox instead of the form’s background image, which we are free to use, because I will often resize the
PictureBox within the bounds of the parent form, to emulate zooming or gaming “bounce” effects), set the
form’s transparency color to match the transparency color for my image, and I am off to the races. When I
finally decided to implement this idea, it was surprisingly easy and fast to develop. In a matter of a couple
short hours I went from wondering if it would actually work to putting the finishing touches on it. The best
part is that the resulting animation application is smooth, fast, and impressive.
I will outline the steps I used, but we must adapt them to our needs. I ran into a minor snag, and that was that a
form’s BackColor property will not, by default anyway, accept transparency colors. This initially prevented me
from using icons with transparent borders, though I later discovered two tricks to correct it. What made this
initially troublesome was that one of my game’s effects is that when a marble is removed from the board, it does
so by shrinking to nothing, which I did by successively reducing the PictureBox height and width by 2 pixels and
increasing its left and top coordinates by 1, thus keeping it centered on its form. Before the fixes, that would
inevitably expose its form background, which displays as a gray 32x32 square (ugh!). Initially, to avoid that lousy
effect, I decided to set the form’s background color to my image's magenta border color, used for transparency.
We normally get a program exception if the border color was transparent. Later, I realized that using
“Me.SetStyle(ControlStyles.SupportsTransparentBackColor, True) ” would solve this problem,
and I later discovered that using “Me.BackColor = Color.FromArgb(&HFF, Clr)” will also work
(internally, the system ignores testing Alpha component values). I had first dealt with it
by converting my icons to bitmaps and painting an unused color into their “transparency”
regions (Magenta in my case), though we are free to use icons or images with
transparency if we use either of the above two BackColor solutions. The down side of my first workaround
was that I would have to maintain two sets of images; one as icons, and the other as images (ack!).
NOTE: I use IconWorkshop from Axialis, www.axialis.com, which is the best icon editor I have ever used, and inexpensive. A
quick web search for “icon editor free download” also yields plenty of free editors, though obviously their quality is
unknown to me. However, the icon editor within Visual Studio is acceptable, features transparency, and is readily at hand.
To get our feet wet, I will walk through a very quick and very easy project where we will be able to see
the results of this technique by our own hand using minimal code. I will lay out the types of images I am
using so we can easily adapt these instructions to instead suit our own images or icons.
Create a new Windows Form Application named ImageEmulate. Next, add a PictureBox control,
PictureBox1, on Form1 and then load a really large image file to PictureBox1. Desktop background
images do this pretty well. Next, size Form1 and PictureBox1 to accommodate the large image. Set the
PictureBox so that it completely fills the borders of the Form (we can simply set its Dock property to
Fill if onewants to, which will cause it to fill the form). All this is just to provide a canvas to play with
the “Image” control we are about to make. Finally, set the form’s WindowState property to Maximized.
Next, we create our image “control”. Once created, we can instantiate as many of them as we need from
it, if our application requires several. Note also that I will keep this “control” as simple as possible, and
though from it I could develop an actual control, I will just lay out the raw data for now and treat it as a
form class of the whole project. The steps to creating our custom Image class are as follows:
 Page 256

1. Add a new form to our project, naming it frmImage.


2. Add a PictureBox to that new form and name it Img.
3. Ensure the Img Image property is loaded with a bitmap or icon with either a solid or transparent border,
representative of the images we will be using, exhibiting a common border color, or it could simply be the only
image we will be using.
4. Open the code for the frmImage form and add the following:
Private Sub frmImage_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
Dim Clr As Color = Me.BackColor 'keep a copy of the form background color.
With Me
.Icon = Nothing 'remove things that cause sizing to change.
.Text = Nothing 'this caption may cause headaches if not blank.
.FormBorderStyle = FormBorderStyle.None 'make sure we are borderless.
.ShowInTaskbar = False 'do not display this form in the taskbar.
With Me.Img
.BorderStyle = BorderStyle.None 'make sure we are borderless.
.SizeMode = PictureBoxSizeMode.AutoSize 'in case we animate the picture control.
.Left = 0 'fit picture control tightly into the form.
.Top = 0
Me.Width = .Width 'ensure image form size matches the picturebox.
Me.Height = .Height
Me.Left = -.Width 'hide form off-screen to avoid once-in-a-blue-moon flash brief initial flicker.
Me.Top = -.Height
Clr = DirectCast(.Image, Bitmap).GetPixel(0, .Height - 1) 'get sample color to use for transparency key.
If (Clr.ToArgb And &HFF000000) <> 255 Then 'if color contains transparency...
Clr = Me.Img.BackColor 'grab PictureBox background color, which will be showing through transparency
End If
End With
'because form's backcolor property will choke on a transparency color, we can assign it without a transparency value, but the
'TransparencyKey property ignores a color's Alpha setting, so it will still render form background as transparent! This workaround,
'figured out later, would have eliminated the need to use a PictureBox on the form and let us to use form’s BackgroundImage!
.BackColor = Color.FromArgb(&HFF, Clr) 'set form background to border color sample (avoid icon transparency issue).
.TransparencyKey = Clr 'set transparency to same (this also ignores transparency assigned to color).
End With
m_FormLoaded = True 'tell the world we are now loaded (not used here, but usefull in more involved
apps...)
End Sub

5. Add a timer named Timer1 to Form1, and ensure that its Interval property is set to 10.
6. Add the following code to the Form1 code page:
Public Class Form1
Dim frmImage As frmImage

Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load


frmImage = New frmImage 'define new frmImage Control.
With frmImage
.Owner = Me 'IMPORTANT! This will ensure frmImage will always display IN FRONT OF the owner form.
.Show() 'make visible, launch Form_Load event first time, so position AFTER Show().
Me.Select() 'IMPORTANT! Keep focus on the main form, otherwise its border will flicker.
.Left = 0 'set image to left of form (must do AFTER Show, in case frmImage Form_Load event fires).
.Top = (Me.PictureBox1.Height - 32) \ 2 'set it halfway down the background form.
End With
Me.Timer1.Enabled = True 'enable animation timer.
End Sub

Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles Timer1.Tick


frmImage.Left += 4 'move image rightward by 4 pixels each time (increase or decrease for speed, but avoid 0!).
frmImage.Refresh() 'refresh its display when image moves to avoid delayed-update jerkiness.
If frmImage.Left > Me.Width Then 'if image has scrolled off the screen...
Me.Timer1.Enabled = False 'turn off timer.
frmImage.Hide() 'hide image (same as frmImage.Visible = False).
Me.Select() 'prevent your form from being pushed down on display stack after the Hide().
frmImage.Close() 'dispose of resources.
Me.Hide
Me.Close() 'close and auto-dispose current form/application.
End If
End Sub
End Class

NOTE: Although the last Me.Select is actually not required in this situation, I put it here to highlight its importance
during normal processing, in an application that might continue operating after the animation is completed...
Now just run the program. We should see the main Form1 fill the screen, and we should see our
image/icon track left-to-right across the screen, with transparency regions... well, transparent.
Important Tips (this is why I had said that this process was not as easy as it might atfirst appear):
1. Setting frmImage’s Owner property to the primary form will ensure that our image will always display above the form. Do
not try instead to set frmImage’s TopMost property to True. By always setting the Owner of the image form, this keeps
related windows local to its display level. TopMost will simply cause layering issues.
2. If we use multiple images, each consecutive shown image will have higher display precedence.
3. Always position the Image form after we select Show, in case its Form_Load event fired, which resets things by hiding it
in the clipped area. Concerned about flicker? I have never seen it as long as I position it immediately.
4. Always Refresh the Image after any move to avoid jerkiness (the refresh will auto-cascade to Img).
5. If we want to display/load a new image in the form upon Show, actually set the new image after Show, then immediately
do a image form Refresh. This is in case its Form_Load event initially fires upon Show, which would reset the image to its
 Page 257

default. As long as we follow this with a form Refresh, all will look good. I typically Show it, position it, set the new
image, and then do a Refresh, and all continues to look great. All this keeps the message queue busy, and painting will not
start until the queue is idle. I dislike this Form-as-Image Control idea due to all this form juggling. Sometimes it feels like
herding cats.
6. Do a Me.Select on the background form right after showing our image. This keeps focus on the main form and avoids a
lot of flicker in the main form’s border from switching between selected (has focus) and inactive (does not have focus).
7. Do a Me.Select (faster than Me.Activate) after hiding the last Image form; sometimes the main application form gets
pushed down the display stack, behind any other apps that might happen to be up. I could be a world-class cat herder!

Room for Improvement.


Form improvements can be vaste. We could define a function that so we can load an image and specify
if it should be treated as a bitmap or as an icon, or better, just let it load an icon or bitmap with equal
ease. We could add a different function to sample either a default or a specified point on the image to
access its transparency color, or provide it ourselves. We could define a positioning function where we
give it the coordinates on its owner form and it will compute screen coordinates for us, though providing
absolute screen coordinates should also be an option, and to auto-refresh the form upon such a move. We
could set the form up to set its Opacity property to zero prior to a Show, so that the position and bitmap
could be set, and then display the form by setting its Opacity to 100, though it would be simpler to just
move the form initially off the window, as we had done previously. We could also set it up so that when
it performs Show or Hide, that it in turn automatically issues the Select method on its owner form.
Following is a new frmImage implementation that features most of these ideas. Please note that if we
plan to show the image, but also set it to a new image and move it, then the move should occur after the
image set, because the move functions (both local and absolute) will also issue a form Refresh. Finally,
notice that the shaded methods below indicate optional overloaded methods.
'****************************************************************************************
' frmImage - Image control emulator for VB.NET
'****************************************************************************************
Friend Class frmImage
Private m_FormLoaded As Boolean = False 'flag can be used by TextChange and Resize events to see if form has finished loading.

'*******************************************************************************
' Subroutine Name : Form _Load event for frmImage.
' Purpose : Return index based on Name (assume index trails name after underscore).
' : Add as many type checks as you need. Example: Label1_13 for ID=13.
'*******************************************************************************
Private Sub frmImage_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
With Me
.Icon = Nothing 'remove things that cause sizing to change.
.Text = Nothing 'this caption can cause headaches if not blank.
.FormBorderStyle = FormBorderStyle.None 'make sure we are borderless.
With Me.Img
.BorderStyle = BorderStyle.None 'make sure we are borderless.
.SizeMode = PictureBoxSizeMode.AutoSize 'in case we animate picture control.
.Left = 0 'fit picture control tightly in the form.
.Top = 0
Me.Width = .Width 'ensure form size set to 32x32.
Me.Height = .Height
Me.Left = -.Width 'hide form off-screen, to avoid once-in-a-blue-moon flash.
End With
Dim Clr As Color
If Me.Img.Image IsNot Nothing Then
Clr = DirectCast(Me.Img.Image, Bitmap).GetPixel(0, 0) 'get sample color to use for transparency key.
If (Clr.ToArgb And &HFF000000) <> 255 Then 'if color contains transparency...
Clr = Me.Img.BackColor 'then grab picturebox background color, which will be showing through.
End If
Else
Clr = Me.Img.BackColor 'else grab picturebox background color, which will be showing through.
End If
'a form's backcolor property will choke on a transparency color an icon might have; we can assign it without a transparency value, but the
'TransparencyKey property ignores color's Alpha transparency setting, so it will render the form's background transparent! This workaround,
'figured out later, would actually eliminate the need to use a PictureBox and allowed us to use the form’s BackgroundImage instead!
.BackColor = Color.FromArgb(&HFF, Clr) 'set form background color to a border color sample (avoid icon Alpha transp. Issue).
.TransparencyKey = Clr 'set transp. to the same (this ignores any transp. assigned to the color).
End With
m_FormLoaded = True 'tell the world we are now loaded (not used here, but...)
End Sub

'*******************************************************************************
' Subroutine Name : New
' Purpose : Default object instantiator.
'*******************************************************************************
Public Sub New()
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
End Sub

'*******************************************************************************
' Subroutine Name : New (OverLoad)
 Page 258

' Purpose : Instantiate and set owner.


'*******************************************************************************
Public Sub New(ByVal Owner As Form)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
End Sub

'*******************************************************************************
' Subroutine Name : New (OverLoad)
' Purpose : set owner, load Image, Set transparency.
'*******************************************************************************
Public Sub New(ByVal Owner As Form, ByVal NewImg As Image,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
Me.NewImage(NewImg, UseTransparency, TransparencyColor)
End Sub

'*******************************************************************************
' Subroutine Name : New (OverLoad)
' Purpose : set owner, load Image, Set transparency.
'*******************************************************************************
Public Sub New(ByVal Owner As Form, ByVal NewImg As Image,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
Me.NewImage(NewImg, UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub

'*******************************************************************************
' Subroutine Name : New (OverLoad)
' Purpose : set owner, load Bitmap, Set transparency.
'*******************************************************************************
Public Sub New(ByVal Owner As Form, ByVal NewImg As Bitmap,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
Me.NewImage(NewImg, UseTransparency, TransparencyColor)
End Sub
'*******************************************************************************
' Subroutine Name : New (OverLoad)
' Purpose : set owner, load Bitmap, Set transparency.
'*******************************************************************************
Public Sub New(ByVal Owner As Form, ByVal NewImg As Bitmap,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
Me.NewImage(NewImg, UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub

'*******************************************************************************
' Subroutine Name : New (OverLoad)
' Purpose : set owner, load Icon, Set transparency.
'*******************************************************************************
Public Sub New(ByVal Owner As Form, ByVal NewImg As Icon,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
Me.NewImage(NewImg, UseTransparency, TransparencyColor)
End Sub

'*******************************************************************************
' Subroutine Name : New (OverLoad)
' Purpose : set owner, load Icon, Set transparency.
'*******************************************************************************
Public Sub New(ByVal Owner As Form, ByVal NewImg As Icon,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
Me.NewImage(NewImg, UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub

'*******************************************************************************
' Subroutine Name : New (OverLoad)
' Purpose : set owner, load file image, set transparency.
'*******************************************************************************
Public Sub New(ByVal Owner As Form, ByVal NewImg As String,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
Me.NewImage(NewImg, UseTransparency, TransparencyColor)
 Page 259

End Sub

'*******************************************************************************
' Subroutine Name : New (OverLoad)
' Purpose : set owner, load file image, set transparency.
'*******************************************************************************
Public Sub New(ByVal Owner As Form, ByVal NewImg As String,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
MyBase.New() ' let MyBase do its thing first...
InitializeComponent() ' This call is required by the Windows Form Designer.
Me.Owner = Owner
Me.NewImage(NewImg, UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub

'*******************************************************************************
' Subroutine Name : NewImage
' Purpose : Set new image, optionally set transparency key.
' : If also moving control, set this before moving (moving does refresh).
'*******************************************************************************
Public Overloads Sub NewImage(ByRef NewImg As Image,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
If NewImg Is Nothing Then Exit Sub
Me.Img.Image = DirectCast(NewImg.Clone, Image) 'load the image into the picturebox.
UpdateImage(NewImg.Width, NewImg.Height, UseTransparency, TransparencyColor)
End Sub

'*******************************************************************************
' Subroutine Name : NewImage (OverLoad)
' Purpose : Set new image, optionally set transparency key.
' : If also moving control, set this before moving (moving does refresh).
'*******************************************************************************
Public Overloads Sub NewImage(ByRef NewImg As Image,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
If NewImg Is Nothing Then Exit Sub
Me.Img.Image = DirectCast(NewImg.Clone, Image) 'load the image into the picturebox.
UpdateImage(NewImg.Width, NewImg.Height, UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub

'*******************************************************************************
' Subroutine Name : NewImage (OverLoad)
' Purpose : Set new image from Bitmap, optionally set transparency key.
' : If also moving control, set this before moving (moving does refresh).
'*******************************************************************************
Public Overloads Sub NewImage(ByRef NewImg As Bitmap,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
If NewImg Is Nothing Then Exit Sub
Me.Img.Image = New Bitmap(NewImg) 'load the image into the picturebox.
UpdateImage(NewImg.Width, NewImg.Height, UseTransparency, TransparencyColor)
End Sub
'*******************************************************************************
' Subroutine Name : NewImage (OverLoad)
' Purpose : Set new image from Bitmap, optionally set transparency key.
' : If also moving control, set this before moving (moving does refresh).
'*******************************************************************************
Public Overloads Sub NewImage(ByRef NewImg As Bitmap,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
If NewImg Is Nothing Then Exit Sub
Me.Img.Image = New Bitmap(NewImg) 'load the image into the picturebox.
UpdateImage(NewImg.Width, NewImg.Height, UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub
'*******************************************************************************
' Subroutine Name : NewImage (OverLoad)
' Purpose : Set new image from File, optionally set transparency key.
' : If also moving control, set this before moving (moving does refresh).
'*******************************************************************************
Public Overloads Sub NewImage(ByVal NewImg As String,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
If NewImg = vbNullString Then Exit Sub
Dim X As Integer = 32
Dim Y As Integer = 32
Try
Me.Img.Image = Image.FromFile(NewImg) 'try to load as a bitmap.
X = Img.Width
Y = Img.Height
Catch
Try
Me.Img.Image = New Icon(NewImg).ToBitmap 'try to load as an icon.
X = Img.Width
Y = Img.Height
Catch
Exit Sub
End Try
End Try
UpdateImage(X, Y, UseTransparency, TransparencyColor)
End Sub

'*******************************************************************************
' Subroutine Name : NewImage (OverLoad)
' Purpose : Set new image from File, optionally set transparency key.
' : If also moving control, set this before moving (moving does refresh).
 Page 260

'*******************************************************************************
Public Overloads Sub NewImage(ByVal NewImg As String,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
If NewImg = vbNullString Then Exit Sub
Dim X As Integer = 32
Dim Y As Integer = 32
Try
Me.Img.Image = Image.FromFile(NewImg) 'try to load as a bitmap.
X = Img.Width
Y = Img.Height
Catch
Try
Me.Img.Image = New Icon(NewImg).ToBitmap 'try to load as an icon.
X = Img.Width
Y = Img.Height
Catch
Exit Sub
End Try
End Try
UpdateImage(X, Y, UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub

'*******************************************************************************
' Subroutine Name : NewImage (OverLoad)
' Purpose : Set new image from Icon, optionally set transparency key.
' : If also moving control, set this before moving (moving does refresh).
'*******************************************************************************
Public Overloads Sub NewImage(ByRef NewImg As Icon,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
If NewImg Is Nothing Then Exit Sub

Me.Img.Image = New Bitmap(NewImg.ToBitmap) 'load icon to picturebox.


UpdateImage(NewImg.Width, NewImg.Height, UseTransparency, TransparencyColor)
End Sub

'*******************************************************************************
' Subroutine Name : NewImage (OverLoad)
' Purpose : Set new image from Icon, optionally set transparency key.
' : If also moving control, set this before moving (moving does refresh).
'*******************************************************************************
Public Overloads Sub NewImage(ByRef NewImg As Icon,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
If NewImg Is Nothing Then Exit Sub

Me.Img.Image = New Bitmap(NewImg.ToBitmap) 'load icon to picturebox.


UpdateImage(NewImg.Width, NewImg.Height, UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub

'*******************************************************************************
' Subroutine Name : UpdateImage
' Purpose : Set a new image to the form. optionally update transparency.
'*******************************************************************************
Private Overloads Sub UpdateImage(ByVal NewWidth As Integer, ByVal NewHeight As Integer,
ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
Me.Width = NewWidth
Me.Height = NewHeight
Me.Img.Width = NewWidth
Me.Img.Height = NewHeight
CheckForTransparency(UseTransparency, TransparencyColor)
End Sub

'*******************************************************************************
' Subroutine Name : UpdateImage (OverLoad)
' Purpose : Set a new image to the form. optionally update transparency.
'*******************************************************************************
Private Overloads Sub UpdateImage(ByVal NewWidth As Integer, ByVal NewHeight As Integer,
Optional ByVal UseTransparency As Boolean = False,
Optional ByVal TransparencytSampleX As Integer = 0,
Optional ByVal TransparencytSampleY As Integer = 0)
Me.Width = NewWidth
Me.Height = NewHeight
Me.Img.Width = NewWidth
Me.Img.Height = NewHeight
CheckForTransparency(UseTransparency, TransparencytSampleX, TransparencytSampleY)
End Sub

'*******************************************************************************
' Subroutine Name : CheckForTransparency
' Purpose : If transparency option selected, get it.
'*******************************************************************************
Private Overloads Sub CheckForTransparency(ByVal UseTransparency As Boolean,
ByVal TransparencyColor As Color)
If Me.Img.Image IsNot Nothing AndAlso UseTransparency = True Then
NewTransparencyKey(TransparencyColor)
End If
End Sub

'*******************************************************************************
' Subroutine Name : CheckForTransparency (OverLoad)
' Purpose : If transparency option selected, get it.
'*******************************************************************************
Private Overloads Sub CheckForTransparency(ByVal UseTransparency As Boolean,
ByVal TransparencytSampleX As Integer,
 Page 261

ByVal TransparencytSampleY As Integer)


If Me.Img.Image IsNot Nothing AndAlso UseTransparency = True Then
NewTransparencyKey(TransparencytSampleX, TransparencytSampleY)
End If
End Sub

'*******************************************************************************
' Subroutine Name : NewTransparencyKey
' Purpose : Set new TransparencyKey value.
'*******************************************************************************
Public Overloads Sub NewTransparencyKey(ByVal NewTransparencyKeyColor As Color)
Try 'this might fail if the image has a transparency color
Me.BackColor = NewTransparencyKeyColor 'set form background color to a border color sample
Catch
End Try
Me.TransparencyKey = NewTransparencyKeyColor 'set transparency to the same.
End Sub

'*******************************************************************************
' Subroutine Name : NewTransparencyKey (OverLoad)
' Purpose : Set new TransparencyKey value.
'*******************************************************************************
Public Overloads Sub NewTransparencyKey(ByVal NewTransparencyKeyPoint As Point)
NewTransparencyKey(NewTransparencyKeyPoint.X, NewTransparencyKeyPoint.Y)
End Sub

'*******************************************************************************
' Subroutine Name : NewTransparencyKey (OverLoad)
' Purpose : Set new TransparencyKey value.
'*******************************************************************************
Public Overloads Sub NewTransparencyKey(Optional ByVal TransparencytSampleX As Integer = 0, Optional ByVal TransparencytSampleY As Integer = 0)
Dim Clr As Color
If Me.Img.Image IsNot Nothing Then
Dim bmp As Bitmap = DirectCast(Me.Img.Image, Bitmap) 'set a bitmap reference to image.
Clr = bmp.GetPixel(TransparencytSampleX,
TransparencytSampleY) 'get sample color to use for transparency key.
Else
Clr = Color.FromArgb(0, 0, 0, 0) 'use default transparency key.
End If
Try 'this might fail if the image has a transparency color.
Me.BackColor = Clr 'set form background color to a border color sample.
Catch
End Try
Me.TransparencyKey = Clr 'set transparency to the same.
End Sub

'*******************************************************************************
' Subroutine Name : MoveToLocal
' Purpose : Set new location for form using local coordinates, with refresh.
'*******************************************************************************
Public Overloads Sub MoveToLocal(ByVal X As Integer, ByVal Y As Integer)
Dim pt As Point = Me.Owner.PointToScreen(New Point(X, Y)) 'convert local to screen coordinates.
Me.Location = pt 'locate form.
Me.Refresh() 'refresh form.
Me.Owner.Select() 'ensure focus on the owner.
End Sub

'*******************************************************************************
' Subroutine Name : MoveToLocal (OverLoad)
' Purpose : Set new location for form using local coordonates, with refresh.
'*******************************************************************************
Public Overloads Sub MoveToLocal(ByRef pt As Point)
MoveToLocal(pt.X, pt.Y)
End Sub

'*******************************************************************************
' Subroutine Name : MoveToAbsolute
' Purpose : Set new location for form to absolute Screen location, with refresh.
'*******************************************************************************
Public Overloads Sub MoveToAbsolute(ByVal X As Integer, ByVal Y As Integer)
Me.Left = X 'locate form.
Me.Top = Y
Me.Refresh() 'refresh form.
If Me.Owner IsNot Nothing Then
Me.Owner.Select() 'ensure focus on the owner.
End If
End Sub

'*******************************************************************************
' Subroutine Name : MoveToAbsolute (OverLoad)
' Purpose : Set new location for form to absolute Screen location, with refresh.
'*******************************************************************************
Public Overloads Sub MoveToAbsolute(ByVal pt As Point)
MoveToAbsolute(pt.X, pt.Y)
End Sub

'*******************************************************************************
' Subroutine Name : Show
' Purpose : replace form Show function.
'*******************************************************************************
Public Overloads Sub Show()
MyBase.Show()
If Me.Owner IsNot Nothing Then
Me.Owner.Select() 'ensure focus on the owner.
End If
End Sub

'*******************************************************************************
' Subroutine Name : Show (OverLoad)
 Page 262

' Purpose : replace form Show function.


'*******************************************************************************
Public Overloads Sub Show(ByVal Owner As Form)
Me.Owner = Owner
Me.Show()
End Sub

'*******************************************************************************
' Subroutine Name : Hide
' Purpose : replace form Hide function.
'*******************************************************************************
Public Overloads Sub Hide()
MyBase.Hide()
Me.Left = -Me.Width 'hide form off-screen, to avoid once-in-a-blue-moon flash.
If Me.Owner IsNot Nothing Then
Me.Owner.Select() 'ensure focus on the owner.
End If
End Sub

'*******************************************************************************
' Subroutine Name : Close
' Purpose : replace form Close function.
'*******************************************************************************
Public Overloads Sub Close()
Me.Hide() 'hide by invoking above
MyBase.Close()
Me.Dispose() 'then dispose of resources (this will work even if MyBase.Close had invoked Dispose).
End Sub

'******************************************************************************
' Copyright © 2010-2021 David Ross Goben. All rights reserved.
' You are free to use and modify this in any way without compensating me,
' as long as it does not profit you in return. If it does, then we will talk...
'******************************************************************************
End Class

This code represents almost a complete user control. I have tested the above form, even with different-
sized icons, and, as we can see, quite an abundance of overloaded instantiation and update options, and
so far it has worked perfectly. It is now a standard part of my 2D game software bag of tricks.
This also gives us some idea how various options are applied, such as loading images, whether Image,
Bitmap, Icon, or File, if we want transparency, and the point we want to sample. Obviously, in writing
our own controls we will want to start simple, as I had demonstrated earlier, and then, once things are
working, to begin adding and testing enhanced variations (Bells and Whistles) as desired.
Notice especially that I have addressed the issues regarding performing a select of the owner form after
a Show or Hide, to ensure the owner is not covered by another app. Also, a form Refresh and Owner
form Select is performed after a move; and moves can be in either local or absolute (screen) coordinates.
Note finally that Hide moves the form off-screen to avoid minor flickering when things get really busy.
The biggest problem with this class is that, even though it works quite impressively in such situations as
a game, it can run into trouble if the form beneath it is moved by the user while the image control is still
moving. Though it is no disaster, it detracts from its visual intent. As such, it is presently not ideal.
Super-Fast No-Holds-Barred VB6 Image Emulation.
If all we want to do is to have a borderless PictureBox with a transparent background resting on a form’s
background, then this is almost no work at all. For example, suppose we have a PictureBox that contains
an image or icon named Image1. In the form’s Load event, just add code like these two lines:
' render the background of an image control transparent
Dim Bmp As Bitmap = DirectCast(Me.Image1.Image, Bitmap) 'set aside a bitmap interface for the image control.
Bmp.MakeTransparent(Bmp.GetPixel(0, 0)) 'render background transparent to the form (0,0 is known transparent location).

Because the PictureBox assumes the background color of its parent, the form, the actual PictureBox
background is not apparent. However, if we have a background image for the form, then we must render
the PictureBox background transparent by also setting its BackColor property to Transparent:
Me.Image1.BackColor = Color.Transparent 'note we must ALSO render the PictureBox background transparent (this differs from the image).

But what if we want to display the image in our PictureBox, named PictureBox1, on top of another
PictureBox’s image named PictureBox2? This is also easy to do, but we will need to make PictureBox1
Image1’s parent and also render its background transparent. We can simply combine the above method with
the technique we used in the previous article, Drawing Labels with Transparent Backgrounds over Images,
on page 239, where we displayed a label with a transparent background in front of an image control:
 Page 263

With Me.PictureBox1
Dim Bmp As Bitmap = DirectCast(.Image, Bitmap) 'Set aside a bitmap interface for the image control.
Bmp.MakeTransparent(Bmp.GetPixel(0, 0)) 'Render its background transparent (0,0 is a known image background location).
.Parent = Me.PictureBox2 'Set image's parent control.
.BackColor = Color.Transparent 'We must ALSO render the PictureBox background transparent (differs from the image).
.Parent.BackColor = .BackColor 'Also set the parent's picturebox BackColor to transparent.
.Left -= .Parent.Left 'Adjust offsets to remain in same relative location over new parent (old was Form).
.Top -= .Parent.Top
End With

NOTE: As we can see, aside from rendering the actual image’s background transparent, we must ALSO render the
PictureBox’s background transparent; otherwise the PictureBox background color will remain visible.

Emulating Mouse Interaction on Drawn Images.


All the above code provides the display functions of VB6 Image controls. The only thing missing is the
ability to click them and react to those events, but that aspect usually requires hooks into the thread’s
message queue to check for clicks that occur in the client rectangle area of our faux Image control. If we
were to do this, we can probably be fairly safe in assuming that these images will be displayed in front of all
other controls, and so a pick, even if it is on another control, but it is also within the bounds of our faux
Image control, that we can trigger event notifications for our Image control (respecting the layering of image
controls adds another level of complexity to the code, which even VB6 was not entirely successful in doing).
An alternative\ is to check for a click on the faux Image area through the Click events for the form and
for its other controls. If one is found, invoke a faux Image click event and exit the regular event without
processing it. This would eliminate any need to add any nervous message queue hooking and unhooking.
For example, we can modify the Form Load event to add handlers for Mouse Move and Mouse Click
events that will be used:
' Set up control events
For Each Ctl As Control In Controls
AddHandler Ctl.MouseMove, AddressOf Ctl_MouseMove 'Support the mouse moving over controls that are covered by a moving image.
AddHandler Ctl.MouseClick, AddressOf Ctl_Click 'Support clicking the mouse on a control that is covered by a moving image.
Next

We could then add the following methods:


'*******************************************************************************
'Method Name: Form1_MouseMove
'Purpose : keep track of mouse movement.
'*******************************************************************************
Protected MseLT As Point 'local storage for mouse movement.

Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseMove


MseLT = e.Location 'keep track of mouse movement.
End Sub

'*******************************************************************************
'Method Name: Ctl_MouseMove
'Purpose : keep track of mouse movement over controls.
'*******************************************************************************
Private Sub Ctl_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs)
With DirectCast(sender, Control)
If .Visible Then
'keep track of mouse movement on controls (compute form offsets)
MseLT = Point.Add(e.Location, New Size(.Left, .Top))
End If
End With
End Sub

'*******************************************************************************
'Method Name: Form1_Click
'Purpose : Check for clicking on Image control on Form
'*******************************************************************************
Private Sub Form1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Click
If Me.ClientRectangle.Contains(MseLT) Then 'if the two indexes intersect upon the form.
Debug.Print("Clicked on form")
End If
End Sub

'*******************************************************************************
'Method Name: Ctl_Click
'Purpose : Check for clicking on an Image control on Control
'*******************************************************************************
Private Sub Ctl_Click(ByVal sender As Object, ByVal e As EventArgs)
Dim Ctl As Control = DirectCast(sender, Control)
If Ctl.ClientRectangle.Contains(MseLT.X – Ctl.Left, MseLT.Y – Ctl.Top) Then 'if the two indexes intersect upon the control...
Debug.Print("Clicked on Image control" & Ctl.Name &
": Local X=" & (MseLT.X - Ctl.Left).ToString & ", Local Y=" & (MseLT.Y - Ctl.Top).ToString)
End If
End Sub
 Page 264

Now we can detect clicks on images without needing message queue interception hooks, though maybe
checking for MouseUp events instead would actually be more productive for an application, or, to be more
robust, to only throw a faux click event if a MouseUp event occured over the same control as a MouseDown
event. All that we need to do to complete this code is write methods to support clicks on these images.
Conclusion.
I realize that some of us will see the work laid out here as a whole lot of unnecessary effort that one did
not have to do in VB6, though we must appreciate the fact that VB6 Image controls slowed program
execution speed down, several such controls could slow the program down significantly, and sadly, most
VB6 programmers over-used Image controls because their transparency feature made them preferable to
work with over faster PictureBoxes. But I hope that most of us can also see that it is not at all difficult to
add the functionality to VB.NET that many of us had under VB6 with Image controls, nor is creating an
Image control or classes, based upon the code I have provided, very difficult. Even the code I had
provided for the complete Image class, if we had removed the many, many, many, many variations for
the overloaded methods, would have also been rather short and to the quick.
But most importantly, I hope that we will see that by VB.NET allowing us to get into the guts of our
project, it has also given us much greater control over its operation. The initial learning process might
make us grumble, but once we master it, we tend to fall in love with it. Personally, I am so impressed
with what I can now do with images under VB.NET that I would not want to go back to VB6.
Indeed, for the most part, we used an Image control under VB6 to simply display stationary images on a
form (where we should have used a PictureBox). If we store these images in our VB.NET project
resources or in an ImageList, rendering them on our form and controls is extremely fast, and an image
can be rendered with just a single line of code from within a Form or control Paint event. The updates
are clean and crisp and with no lags or flicker as would often happen using a VB6 Image control.
Even so, I hope, in the end, that Microsoft will one day instate regular Image controls under VB.NET,
considering how easy the technology is to implement, as I have demonstrated. Besides, it is a small step
away from the shape controls that now come standard as of VB2010. Even so, I hope that users will not
then begin to over-use them, thus slowing the over-all execution speed of their programs once again.
 Page 265

Implementing E-Z Unions Under VB.NET.


A feature many VB6 developers wished for was a Union
(Inspired by a short article on FieldOffsets by Paul Kimmel)
class, where variable fields could overlap each other. I once wrote a VB6 Union class. It worked quite
well for sharing common memory between variables of different sizes. It employed a fixed-length string
as a generic storage medium in a long chain of 8-byte blocks. It moved data to and from cardinal value
types, whether Double, Single, Long, Integer, or Byte using the rtlMoveMemory Win32 API to and from
that string space. It was perfect for what we needed it for, it did exactly what was expected of it, but the
process of supporting VB6 Unions was rather cumbersome, especially if we compare it to equitable
VB.NET functionality, which can in fact support Unions with no extra physical code beyond that needed
to declare a simple Structure or Class, but at the same time adding no code to our application, save for
the length of its longest field. To put an exclamation point on this, optimized VB6 code to provide just
minimal Union support, and in 6-point type, still fills 3 whole pages! (And no, that was not just my
verbose comments, plus maybe a single VB6 line of instruction code!)
Had I chosen to upgrade that class DLL to VB.NET, I would need to build a massive torrent of overloads
to the rtlCopyMemory Pinvoke to address the “As Any” issue, or change the fixed-length strings to
dynamic. This is all quite easy to do, but all such effort would have been to provide functionality that is
already built right into VB.NET. Unlike VB6, VB.NET is very Union-friendly, and it has readily-
accessible functionality to support them, even though this simple process is not widely documented.
A VB.NET Union is just a normal Class or Structure, but uses compiler directives to allow selected
fields to share a common address, or even each being assigned specific starting addresses. Consider a
union of a Char and an Integer field. Such a union would be 32 bits in size, because a union will always
minimally be the length of its largest field. If we assign the value 65 to the Integer member, the union
assigns the Char the same value, yielding the character “A”. Change one member and the other changes.
A union is commonly used to map more than one cardinal field to the same address. After all, cardinal
fields (numeric fields) happen to agreeably overlap. A common application for a Union in .NET is for
interop, which is to implement code that had not been written in .NET. When we use Unions or interop
code in general, we will need to employ the System.Runtime.InteropServices namespace. This
namespace features the StructLayout compiler directive, which is exactly what we need to create
Unions. It also holds many other features that we should most definitely explore on our own.
To implement a union in VB.NET takes just 5 very easy steps:
1. Add an “Imports System.Runtime.InteropServices” statement to the top of the source file.
2. Define a named structure or class, such as a structure named isUnion, for example.
3. Precede the definition of the structure with the “<StructLayout(LayoutKind.Explicit)>” attribute.
4. Apply the required field members to the structure, such as an Integer and a Char in the above example.
5. Precede each field member with “<FieldOffset(0)>”, each initialized to a parameter of 0 to define a Union.
Now consider the following example Union structure:
<StructLayout(LayoutKind.Explicit)>
Public Structure isUnion
<FieldOffset(0)> Public iMember As Integer '32-bit Integer value.
<FieldOffset(0)> Public sMember As Single '32-bit Floating-Point value.
End Structure

NOTE: We are not limited to using just Integers and Singles. We can use other types too, though we cannot mix reference
types (objects or arrays, and remembering that Strings are arrays of type Char) with cardinal types (integer, floats, and
single Chars), though think about a Union of an Image object and a Bitmap, which share identical data footprints.
In the above example, we can use this simple structure to detect if the user had supplied an integer or
floating-point value, much as we might notice that Graphics functions that expect floating-point values
can auto-promote supplied integer values, as I mention periodically in this tome. For example: if the
floating-point sMember was set to 2.0F (or 2.0!), the Integer iMember would reflect a value of
1073741824. This is very large (1,073,741,824), and would likely indicate the user had supplied a floating
 Page 266

point pen width value. If the integer iMember was set to 2, the floating-point sMember would reflect a
value of 2.802597E-45. This is very small, and would likely indicate the user had supplied an integer pen
width value, unless we were planning to inscribe the Holy Bible on a flake of talcum powder.
The key to this whole functionality is to precede each class or structure field with a FieldOffset attribute. The
FieldOffset attribute is used to assign the byte offset from the start address of the object's space where to
index each field. By default, each field is stacked sequentially after the previous field on natural boundaries.
A natural boundary is where each consecutive field begins on a memory bounds that is a multiple of its size
from the start of its object space, unless we add a Pack:=1 parameter to the StructLayout list so they would
instead align on single-byte boundaries. By informing Visual Basic that each field will begin at offset 0
within the class or structure, they will simply overlap each other, thus defining a Union.
Of course, we are not limited to specifying simply a 0 offset for each field, which seems obvious by the
need to specify a byte offset value at all. We can instead apply differing offsets, such as using 2 Shorts
with an Integer to address each 16 bits of a 32-bit Integer; just set one of the Short offsets to 2,
indicating the third byte of the 32-bit (4-byte) field:
<StructLayout(LayoutKind.Explicit)>
Public Structure Integer_Union
<FieldOffset(0)> Private IntValue As UInteger 'Integer value (32-bits).
<FieldOffset(0)> Private ShortLeft As UShort 'Left (lower) 16 bits.
<FieldOffset(2)> Private ShortRight As Ushort 'Right (upper) 16 bits.
<FieldOffset(0)> Private Char0 As Char 'Left (lower) 16 bits 'Individual Chars are Scalar! Same size as VB6 Integer,
<FieldOffset(2)> Private Char1 As Char 'Right (upper) 16 bits 'and VB6 fans whine about VB.NET using 32-bit Integers?
<FieldOffset(0)> Private Byte0 As Byte 'First 8 bits of 32-bit field.
<FieldOffset(1)> Private Byte1 As Byte 'Second 8 bits of 32-bit field.
<FieldOffset(2)> Private Byte2 As Byte 'Third 8 bits of 32-bit field.
<FieldOffset(3)> Private Byte3 As Byte 'Fourth 8 bits of 32-bit field.
End Structure

NOTE: All field members of a class or structure actually have a FieldOffset value assigned to them internally, simply not
displayed. Normally they are defined in such a way to lay them one after the other on natural boundaries. But by explicitly
specifying offsets, we can manually declare specifically where their starting offset should be assigned.
This functionality also allows us to emulate CPU register structures, which provide us with tools that we
can, for example, use to write our own CPU emulator. Indeed, I often used a Z80 CPU emulator to run
older-than-dirt 8-bit TRS-80 Model 4 software on my PC.
Implementing Unions under VB.NET is almost insanely simple, yet I noticed that C#, which is a .NET
managed version of Visual C++, also does not openly document this simple union structure, but which
also happens to implement and fully support unions in the exact same manner that we have
demonstrated here.
NOTE: C# and VB.NET were actually developed separately, regardless of many false rumors that VB.NET was based on C#,
or that C# was based on VB.NET (depending on who we are listening to). They were both built on a Managed C++
foundation. Yet, they borrowed so many features from each other that they are now incredibly alike, which is why C# looks
like VB.NET, but using C++ syntax. In fact, in 2009 Microsoft decided that the best way to advance their .NET language
suite was through a process called Co-Evolution, so when a feature is added to C#, it is also added to VB.NET and F#, and
of course this equally applies from the other directions, from VB.NET and F#. I wish that this also applied to C#’s ability to
declare Unsafe blocks and use actual pointer processes within them. I have had need of that ability in VB.NET many times.
Although I can code around it, it would be nice to add it as a language feature. They could also remove the totally useless
requirement to marshal ByRef strings (that was borderline harassment), which is brain-dead simple to code around anyway,
even though they would have to maintain support of the marshaling syntax in older code that had been forced to use it.
NOTE: F#, in Microsoft’s own words, “enables users to write simple code to solve complex problems... F# has strong
support for parallelism and concurrency, through its support for immutability and asynchronous programming, and tools
such as the F# Interactive enable exploring data interactively, analyzing, visualizing and testing against live data sources...
F# is a productive language for developers working in technical, algorithmic, parallel and data-rich areas. This has
included applications in domains such as financial services, data analytics, games, sciences and machine learning.” F#
comes installed as of Visual Studio 2010. but it also works with Visual Studio 2008, and we can download it for free at:
http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/.
 Page 267

Structures that use Sub-Structures and Union Sub-Structures.


Some structures need to employ additional structures defined within them. The cleanest way to handle
them under VB.NET is to first declare any sub-structure as a stand-alone, and then in the main structure,
declare one of its member fields as the sub-structure.
For example, consider this imaginary Win32 structure that declares a structure within a structure:
typedef struct _prnInfo { //Assign structure to_prnInfo (and to PRNINFO at the end of the structure).
TCHAR PrinterName[32]; //TCHAR is a Win32 String of 8-bit BYTES. Not Unicode and not ANSI (no null terminator).
long PrinterID; // TCHAR strings are typically fields of space characters with text data, like LSet/RSet.
Struct {
short Orientation; //NOTE:
short PaperSize; //Though declaring structures within structures is legal, I consider it messy and it
short PaperLength; // detracts from clearly delineating the parenting structure. Were we to break this
short PaperWidth; // sub-structure out and then reference it as a member field within the parent, it
short Scale; // would not consume even one byte of extra memory. This will be more clearly
short Copies; // demonstrated later when we look at a full working DEVMODE structure that uses both
short DefaultSource; // substructures and Unions.
short PrintQuality;
} DETAILS; //assign this inner structure to _prnInfo as a DETAILS section.
} PRNINFO; //also assign full structure, _prnInfo, to PRNINFO.

For us to use this structure in VB.NET, we would need to declare the inner DETAILS structure separately
from the PRNINFO structure. For example:
<StructLayout(LayoutKind.Sequential)>
Friend Structure mDETAILS
Friend Orientation As Short
Friend PaperSize As Short
Friend PaperLength As Short
Friend PaperWidth As Short
Friend Scale As Short
Friend Copies As Short
Friend DefaultSource As Short
Friend PrintQuality As Short
End Structure

<StructLayout(LayoutKind.Sequential)>
Friend Structure PRNINFO
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=32)> Friend PrinterName As String 'handle TCHAR; System-dependent string..
Friend PrinterID As Integer
Friend PaperLength As Short
Friend DETAILS As mDETAILS 'reference substructure.
End Structure

We would declare a variable of type PRNINFO using something like “Dim Pinfo As PRNINFO”. This will
also automatically create space and mapping to the embedded mDETAILS structure. To access any of the
members of the embedded mDETAILS structure, and just like under C, C++, and C#, we would have to
reference them through the DETAILS member of Pinfo. For example: “Pinfo.DETAILS.Scale” to assign or
obtain the Scale member of the embedded mDETAILS structure.
Now consider this more elaborate Win32 DEVMODE structure that is designed to support both Printer
Devices and Display Devices, which employs both embedded structures and Unions:
typedef struct _devicemodeA { //Declare a Structure of type _devicemode.
TCHAR dmDeviceName[CCHDEVICENAME]; //TCHAR = Unmanaged Type ByValTStr. vbFixedString works with CHAR, but not with this.
WORD dmSpecVersion; //WORD = Short Integer or Int16.
WORD dmDriverVersion; //NOTE: ANSI TCHAR is equivalent to 8-bit BYTE. The Unicode-like version is WCHAR.
WORD dmSize;
WORD dmDriverExtra;
DWORD dmFields; //DWORD = Integer or Int32.
union { //This union will allow the following two structures to use the same space.
struct { //this first structure declares 8 short integers, consuming 16 bytes.
short dmOrientation;
short dmPaperSize;
short dmPaperLength;
short dmPaperWidth;
short dmScale;
short dmCopies;
short dmDefaultSource;
short dmPrintQuality;
};
struct { //this second structure also consumes 16 bytes.
POINTL dmPosition; //POINTL is the same as a .NET Point structure (two-Int32 values, using 8 bytes).
DWORD dmDisplayOrientation;
DWORD dmDisplayFixedOutput;
};
};
short dmColor;
short dmDuplex;
 Page 268

short dmYResolution;
short dmTTOption;
short dmCollate;
TCHAR dmFormName[CCHFORMNAME];
WORD dmLogPixels;
DWORD dmBitsPerPel;
DWORD dmPelsWidth;
DWORD dmPelsHeight;
union { //this union allows dmDisplayFlags and dmNup to occupy the same space.
DWORD dmDisplayFlags;
DWORD dmNup;
};
DWORD dmDisplayFrequency;
#if (WINVER >= 0x0400) //if WinNT40 or greater...
DWORD dmICMMethod; // the following declarations are used only by a printer device...
DWORD dmICMIntent;
DWORD dmMediaType;
DWORD dmDitherType;
DWORD dmReserved1; //presently must be set to 0.
DWORD dmReserved2; //presently must be set to 0
#if (WINVER >= 0x0500) || (_WIN32_WINNT >= 0x0400) //if Window98/WinNT40 or greater....
DWORD dmPanningWidth; //presently must be set to 0 (used by printers).
DWORD dmPanningHeight; //presently must be set to 0 (used by printers).
#endif
#endif
} DEVMODEA, *PDEVMODEA, *LPDEVMODEA; //assign structure to DEVMODE, pointer PDEVMODE, and long pointer DEVMODE

If we understand that WORD and short represent VB Short integers (Int16), DWORD represents VB Integers
(Int32), POINTL is a VB Point structure, that TCHAR is an unmanaged ByVal Fixed-Length 8-bit String of
BYTE (UInt8) without a null terminator, and that Union allows more than one set of fields to occupy the
same space, we can construct a working VB.NET DEVMODE structure for both Printers and Screens!
Following is listed my DEVMODE structure for VB.NET. Be sure to also include “ Imports
System.Runtime.InteropServices” at the top of our class or module, because this structure is expected to
be used by unmanaged Win32 interop methods:
'create a 16-byte union of 8 int16 printer values over a Point (8 bytes) and 2 int32 values (8 bytes)
<StructLayout(LayoutKind.Explicit)>
Friend Structure DEVMODE_union1
' struct {
<FieldOffset(0)> Friend dmOrientation As Int16 'For printer only, selects the orientation of the paper.
<FieldOffset(2)> Friend dmPaperSize As Int16 'For printer only, selects the size of the paper to print on.
<FieldOffset(4)> Friend dmPaperLength As Int16 'For printer only, overrides the length of the paper specified by the dmPaperSize member.
<FieldOffset(6)> Friend dmPaperWidth As Int16 'For printer only, overrides the width of the paper specified by the dmPaperSize member.
<FieldOffset(8)> Friend dmScale As Int16 'Specifies the factor*100 by which the printed output is to be scaled (1=.01).
<FieldOffset(10)> Friend dmCopies As Int16 'Selects the number of copies printed if the device supports multiple-page copies.
<FieldOffset(12)> Friend dmDefaultSource As Int16 'Specifies the paper source (0,1,2...)
<FieldOffset(14)> Friend dmPrintQuality As Int16 'Specifies the printer resolution.
' }
' struct {
<FieldOffset(0)> Public dmPosition As Point
<FieldOffset(8)> Friend dmDisplayOrientation As Int32 'For display only, the orientation at which images should be presented.
<FieldOffset(12)> Friend dmDisplayFixedOutput As Int32 'For fixed-resolution displays, how it presents a low-res mode on a higher-res display.
' }
End Structure

'create a 4-byte union of two overlapping int32 values (dmDisplayFlags for display, dmNup for a printer).
<StructLayout(LayoutKind.Explicit)>
Friend Structure DEVMODE_union2
<FieldOffset(0)> Friend dmDisplayFlags As Int32 'Specifies the device's display mode.
<FieldOffset(0)> Friend dmNup As Int32 'Specifies where the NUP is done (N-Up = # pages rendered on 1 sheet).
End Structure

<StructLayout(LayoutKind.Sequential)>
Friend Structure DEVMODE
Friend Const CCHDEVICENAME As Int32 = 32 'length for friendly device name.
Friend Const CCHFORMNAME As Int32 = 32 'length for form name.

'friendly device name (do not use shortform <VBFixedArray(CCHDEVICENAME)> or <VBFixedString(CCHDEVICENAME)>. Must be ByValTStr).
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=CCHDEVICENAME)> Friend dmDeviceName As String
Friend dmSpecVersion As Int16 'The version number of the initialization data specification on which the structure is based.
Friend dmDriverVersion As Int16 'The driver version number assigned by the driver developer
Friend dmSize As Int16 'Specifies the size, in bytes, of the DEVMODE structure (this must be set by user: Len(DevModeStruct)).
Friend dmDriverExtra As Int16 'number of bytes of private driver-data that follow this structure (not included in dmSize).
Friend dmFields As Int32 'Specifies whether certain members of the DEVMODE structure have been initialized.
'---------------------------------------
Friend u1 As DEVMODE_union1 'reference substructure Union.
'---------------------------------------
Friend dmColor As Int16 'Switches between color and monochrome on color printers.
Friend dmDuplex As Int16 'Selects duplex or double-sided printing for printers capable of duplex printing.
Friend dmYResolution As Int16 'Specifies the y-resolution, in dots per inch, of the printer.
Friend dmTTOption As Int16 'Specifies how TrueType fonts should be printed.
Friend dmCollate As Int16 'Specifies whether collation should be used when printing multiple copies.
'specify the name of the form to use; for example, "Letter" or "Legal".
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=CCHFORMNAME)> Friend dmFormName As String
Friend dmLogPixels As Int16 'The number of pixels per logical inch. Printer drivers do not use this member.
Friend dmBitsPerPel As Int32 'Specifies the color resolution, in bits per pixel, of the display device.
Friend dmPelsWidth As Int32 'Specifies the width, in pixels, of the visible device surface.
Friend dmPelsHeight As Int32 'Specifies the height, in pixels, of the visible device surface
'---------------------------------------
Friend u2 As DEVMODE_union2 'reference substructure Union.
'---------------------------------------
Friend dmDisplayFrequency As Int32 'Specifies the frequency, in hertz (cycles per second), of the display device in a particular mode
 Page 269

'All the following are for Image Color Management For printers
Friend dmICMMethod As Int32 'For ICM applications, the system examines this member to determine how to handle ICM support
Friend dmICMIntent As Int32 'Specifies which color matching method, or intent, should be used by default
Friend dmMediaType As Int32 'Specifies the type of media being printed on, such as standard, glossy, or transparency
Friend dmDitherType As Int32 'Specifies how dithering is to be done, such as none, course, fine, lineart, or grayscale
Friend dmReserved1 As Int32 'Not used; must be zero
Friend dmReserved2 As Int32 'Not used; must be zero
Friend dmPanningWidth As Int32 'This member must be zero
Friend dmPanningHeight As Int32 'This member must be zero
End Structure

Unlike many variations of this DEVMODE structure that we might have seen spread around on the web,
though all lacking unions and being configured for exclusive use by either printers or display devices,
but not both, this version of it actually works, and for both.
For a practical example of using this structure to get a list of display modes available to our computer, to
include properties that most implementations of it miss, which would clearly show that all the seemingly
duplicate settings are not in fact duplicates at all, please be sure to refer to Black Book Tip # 56:
Getting, Enumerating, and Changing Screen Settings, on page 708.
NOTES:
Hilarious digression – I had one self-described online “guru” who tried to force me to remove this section of this article for
copyright infringement, because he published the above VB.NET DEVMODE structure in his online blog, word-for-word, 2-
½ years after I first published it online. I just laughed in his and his two-bit lawyer's faces. Turns out he copied my work,
forgot where he got it, and then, when enough time for memory lapses to set in had passed, someone told him about this
book. It also turned out he was simply embarrassed he had been caught by the short hairs, and preferred to file a lawsuit
rather than admit he made an mistake (my Dad often joked that he thought he was wrong once, but he was mistaken!). This
came as no surprise to me, because years ago this very sane dweeb used to constantly contact me via email about how to do
some presumed impossible task. I would give him the code, and he would publish it online, boasting about how he discovered
this solution. Did he think no one like me or my friends read his blogs? It was mild entertainment for me, because a lot of my
associates would flood his blog with countless snide comments about where that solution actually came from.
By the way, feel free to use any code herein within your own work, I am more interested in solutions that work, so if the code
herein helps you to solve a problem or helps you achieve guru status at your job, you are welcome! Just do not contact me
because one of your customers has a complaint about your application of the code. I am very expensive, and I do not want to
have to call 911 when you see the bill for my custom work.
If one is curious about why I am so cynical about online bloggers who are not described as ( I love these), but describe
themselves as gurus, this guy, and an unending line of other wanna-be internet-famous “authorities”, have stolen my work
endlessly. I really do not mind, because this gets the solution out there to people who really need it. Besides, if I really
minded, I would not be writing under so many different pseudonyms.
And WHY would anyone ever want to be famous? That is insanity itself. I once had my own brush with fame when I worked
for 80 Micro magazine in the 80s and early 90s. Some readers would demand elaborate custom applications because they
were a “subscriber”. Hmm. What part of their $12.95/year fortune was my cut? This is why I do most of my writing
anonymously, or by a pen name. Indeed, Bev Woodbury and I co-wrote their extremely popular Feedback Loop column
under the pseudonym, Mercedes Silver, taking the identity of a fictitious 14-year-old computer genius, first made notorious
in an earlier popular series, Gamer's Cafe. Had Bev and I used our real names, we would not know rest. As Mercedes, we
could simply ignore all the insane demands and focus on real reader questions.
I live in glorious anonymity, and will not have it any other way. If you ever get some fame, then you will understand why
most sane people, to include even myself, soon choose a hermit's life. People incessantly holding their hands out, or
expecting “coin” for their admiration, will drive you mad.
 Page 270

Understanding VB.NET Imports.


The Imports command, which is placed in the heading of a class file
before the actual class declaration, allows us to import, for direct access
without instantiation, all Public/Friend Shared members of a non-
inheritable class, whether from the .NET Framework or from our own
library. Thus, after we import a class, a local copy of the class code is
loaded and we can invoke any member of it without having to also
qualify it with its class name (though this can never hurt).

Namespaces, Modules, and Non-Inheritable Classes: A Rose by Any Other Name...


An example of local loading of class code is a VB Module file. Under VB6, these were used to place
code for global application access. To emulate this under VB.NET, when a Module file is loaded, it is
actually a class (the body of the module is embraced by a Module class body), which is automatically
app-globally imported, and all its non-private members are treated as Shared Public or Shared Friend.
NOTE: You may be interested to know that VB.NET module files are, as you may have guessed, actual Class files. However,
internally, VB.NET globally imports any module files present in the project so that we have effectively imported these classes,
which explains why imported namespaces can be treated just as though they were modules included locally in any file where
they are marked as imported. Note further that all classes that are intended to be imported are also declared NonInheritable.
This special property is assumed as a behind-the-scenes default for VB.Net Module classes.
NOTE: Some new C# developers have expressed jealousy that they cannot declare VB-style modules. But they can, though
they must declare it as a non-inheritable class with its Public/Friend members declared Shared. Then, by importing it, C#
users can use them exactly like VB modules, fully emulating VB usage. Even so, though it does not generate any additional
compiler code, it is probably best to go ahead and specify the parent module, which helps self-document the code.
We can easily emulate this by declaring Public/Friend methods within a standard class file as Public
Shared, declare Dimmed or Friend methods as Friend Shared, and declare the class as NotInheritable.
Finally, in the headings of other class files that will use it, reference this class using the Imports
command. For example, consider this non-inheritable class declared within a project:
Public NotInheritable Class Class1

Public Shared Function Multiply(ByVal X As Integer, ByVal Y As Integer) As Integer


Try
Return X * Y
Catch
Throw New Exception("Mighty tall integers for a greenhorn, pilgrim! Them's overflow integers!")
End Try
End Function
Return 0
End Class

We can then import and use this class right within the project like this:
Imports MyApplication.Class1 'import local instance of this project’s application class.

Public Class Form1

Private Sub Test()


Dim Result As Integer = Multiply(7, 63) 'invoke shared method.
End Sub

End Class

If we use the Imports statement as described above, we can use all of its exposed members that are
declared as Public Shared and Friend Shared within that namespace without qualification, provided
they are also unique to the project. If our project contains Imports statements for namespaces that
contain members with the same name, we must fully qualify that member path when we use it.
However, even though we do this, it will not cause our application to generate any additional code.
Long-pathing or assumed-pathing, also known as short-circuit pathing, will still generate the exact
same compiled code with no additional overhead.
 Page 271

Using Imports Aliases.


When namespaces have members whose names may conflict because they share the same name, often
called namespace collision or namespace pollution, one can employ aliases (assumed-pathing) to create
abbreviated names to use in the place of these long paths.
For example, the following code illustrates an alias called LB for a fully qualified path:
Imports LB = System.Windows.Forms.ListBox

The above statement creates a short-cut reference to System.Windows.Forms.ListBox.


We can even use aliases to more easily create object reference variables, such as the following:
Dim LBreference As LB

Within a namespace we can define items such as modules, interfaces, classes, delegates (see the next
article for in in-depth look at Delegates), enumerations, structures, and other namespaces. However, we
cannot define items such as properties, procedures, variables and events at the namespace level. These
items must be declared as members of a namespace body.
Also, when we import several namespaces, we can do so on individual lines, or on one line, separating
each consecutive namespace from the previous with a comma. For example:
Imports System.Runtime.InteropServices
Imports VB = Microsoft.VisualBasic

Versus:
Imports System.Runtime.InteropServices, VB = Microsoft.VisualBasic

Referencing Class Code without Importing or Instantiation.


Although not directly related to importing, this subject is often lumped into the mix, and that is that we
can create a class, add a number of Pinvoke or even Constant declarations within it, and then access
them without having to either import or instantiate the class. This is because the code for a class is
always resident in the program’s code space, even if we never instantiate an instance of that class, which
is otherwise required to use any data members of a class. Consider this class declaration:
Friend Class WinPinvoke

' The LockWindowUpdate function disables or enables drawing in the specified window.
' Only 1 window can be locked at a time.
' Unlock a locked form or control by passing a value of 0 to the function.
Friend Declare Function LockWindowUpdate Lib "user32.DLL" Alias "LockWindowUpdate" (
ByVal HandleToLock As IntPtr) As Integer

'Toggle Lock a WorkStation. Locking/Unlocking a WorkStation protects it from unauthorized use.


Friend Declare Function LockWorkStation Lib "user32.DLL" () As Integer
Friend Const LockedWorkStation As String = "Locked Workstation from unauthorized use."
Friend Const UnlockedWorkStation As String = "Unlocked Workstation."

End Class

We can invoke the declarations within this class without importing it or instantiating it by simply
specifying the class name and the desired member, separated by a dot. For example:
Public Class Form1

Private Sub Test2()


If CBool(WinPinvoke.LockWorkStation()) Then
MsgBox(WinPinvoke.LockedWorkStation, MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "Locked")
Else
MsgBox(WinPinvoke.UnlockedWorkStation, MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "Unlocked")
End If
End Sub

End Class
 Page 272

Understanding VB.NET Delegates.


Delegates are one of VB.NET’s most valuable features, even though they are thinly documented and often
viciously railed against by the uninformed. They are used to safely invoke other methods, such as VB.NET
events and Callbacks. They are also called type-safe function pointers, similar to pointers to functions used in
other programming languages. Unlike primitive function pointers used by C/C++ and VB6, .NET Delegates
enforce method parameter type use and are able to reference both shared methods, being invoked like VB6
module methods, and instance methods, which are invoked without a specific class instance. In most cases
Delegates are benign and are usually constructed right under our noses, slipping by completely unnoticed.
However, with just a little understanding of them, we can perform virtual coding miracles.
NOTE: Did you know that Pinvoke declarations are actually a special type of Delegate? Also, though I will not cover it here
so to confuse you further, Delegates themselves are actually Class objects with malleable properties and constructors!
Perhaps the simplest way to view a Delegate is as a Method Prototype, or Signature, being used like a map to lay
out attributes that associated method invocations must conform to, such as parameter count, parameter types, and
possible return type. Even so, the parameter names a Delegate might declare can differ from those we actually
employ in the methods they represent, they being just placeholders for the actual parameters we choose to use.
Delegates can also represent like-formatted methods. This is important since a method provided a VB6 AddressOf
was by name only, so if parameters were misunderstood and not handled correctly, program crashes might occur.
Thus, a Delegate safeguards against such crashes by informing .NET of the manner in which it should handle the
method declaration a Delegate represents. This is even more important when we also factor in overloaded
methods, where parameter mapping can get confusing. By declaring a unique prototype for each specific instance
of use for a method and its variations, confusion over code method requirements can be safely eliminated.

Delegates in Events.
In a typical VB6 event, like the code used to support a button click, we might see some event-handling
code that looks much like the following, which hides the actual event/control linkage from you:
Private Sub cmdGo_Click()

Under VB.NET, a similar method is exposed like this, revealing the control/event linkage:
Private Sub cmdGo_Click(ByVal sender As Object, ByVal e As EventArgs) Handles cmdGo.Click

The sender parameter references the specific control being processed, used best when multiple like-controls
are assigned that event, similar to control arrays in VB6. Here, we can cast sender to the object type with no
actual extra code, such as “DirectCast(sender, Button)”. The e parameter further exposes additional features
available to the sender control. In typical cases, this event is often used only to support the cmdGo button, so
we need only know the Click event had occurred, so thee parameters are usually ignored. In such a classic
case, we could actually remove the two parameters from the declaration manually to clean up its appearance:
Private Sub cmdGo_Click() Handles cmdGo.Click

When this happens, behind the scenes VB.NET supports this altered definition by substituting the usual Delegate
“__wEvent(ByVal __sender As Object, ByVal __eventArgs As EventArgs) ” with the overload, “__wEvent()”. If it could
not support this substitution, such a user-edit would not be permitted. As it is, VB.NET must jump through hoops
in the background compile process just to give users this much-demanded, though time-wasting feature.
NOTE: The above makes clearer a major difference between VB6 and VB.NET: the Handles keyword. The cmdGo.Click
reference noted after it is shorthand for what would otherwise be the need for a separate coded line consisting of “ AddHandler
CmdGo.Click, AddressOf CmdGo_Click ”, which informs the compiler that this method will be used to handle the Click event
for the control. This longhand expression, along with its shorthand version when attached to the event header, specifies that
when a Click event occurs on the CmdGo control (the CmdGo control, due to it being declared in auto-generated
WithEvents code, has various event members internally defined that can be accessed via a Dot operator), to handle the
event through the provided CmdGo_Click method. Had we several controls linked to the event, emulating what was done by
a VB6 Control Array (normally done manually under VB.NET (see page 80 to easily create Control Arrays), then a list of
control event handler delegates could follow the Handles token, separated by commas. This is also where the sender object
becomes very important, because it will identify the actual control being processed, so we could have a test of something like
this: “If sender Is Image1(3) Then... ”, referencing a PictureBox array named Image1() containing a list of PictureBoxes.
Or, for example, adding separate lines in the Form Load event could apply the AddHandler keyword to each control
concerned with the target event, in order for each to associate a particular event with the very same handler method.
 Page 273

Delegates and AddressOf.


The AddressOf operator usually exposes the existence of Delegates to most users. Like VB6, the VB.NET
AddressOf operator extracts the address of the method declared after it, but .NET also expects an explicit
Delegate be declared to verify that the provided method’s format matches the expected signature. Also, when
the specified method is an instance method (a method of an object instance of a class) then the method
delegate refers to both the instance’s data and its method (something illegal under VB6). In this case, when
the delegate is referenced, the method of the specified instance is invoked, and the instance’s data is used.
Note that when using the AddressOf operator that we must also manually pre-declare a Delegate for the
target method, except for Event methods, such as btnSend_Click, and when passing classes or functions that
are expected to match a pre-defined format, such as, for instance, a comparator method for a sort. This is
because under VB.NET, the AddressOf operator not only provides the method address to a Pinvoke, as VB6
had done, but VB.NET must also internally understand the target’s parameter list and optional return type in
order to more robustly trap errors that had too often plagued countless VB6/C++ users of AddressOf.
NOTE: The reason a Delegate must be created is in case the signature is used by more than one method, though in most
cases it is not (I have suggested to Microsoft how to auto-declare such delegates so the user is not burdened by the process).
Consider the following VB6 code using AddressOf, its VB.NET upgrade, and the warning issued:
' VB6
Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA" _
(ByVal hwnd As Long, ByVal attr As Long, ByVal lVal As Long) As Long
' ▲
Sub SubClassWindow(ByVal hwnd As Long) ' ┗———————————————┓
If PrevProcPtr = 0 Then ' ▼
PrevProcPtr = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf SubClassProc)
End If
End Sub

' The above VB6 code after being upgraded to VB.NET:


Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA"
(ByVal hwnd As Integer, ByVal attr As Integer, ByVal lVal As Integer) As Integer '(VB.NET expects lVal to be of a Delegate type).
' ▲
Sub SubClassWindow(ByVal hwnd As Integer) ' ┗—————————————————————————————————————┓
If PrevProcPtr = 0 Then ' │
' UPGRADE_WARNING: Add a delegate for AddressOf SubClassProc. For More: BLAH-BLAH-BLAH │
PrevProcPtr = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf SubClassProc) ' ◄———————————————┛
End If
End Sub

The VB.NET AddressOf operator requires a Delegate reference for signature verification when it will invoke
a method address. In this case we must provide a Delegate declaration for the SubClassProc method to the
lval parameter in the SetWindowsLong method so that the compiler can guarantee program integrity.
Basically, it needs to know that the method address provided to lval in SetWindowsLong is as is expected.
To declare this needed Delegate is stupidly easy: we simply supply our Delegate with the parameter and
type declarations associated with our target method, in this case SubClassProc. This can be as simple as
copying the SubClassProc declaration heading, adding the “Delegate” verb ahead of the “Function” (or
“Sub”) instruction, and providing a unique name for this new Delegate method. I ‘cheat’ by simply
appending “Delegate” to the original method’s name, then place it before any references to it or its
targets. Suppose our VB.NET SubClassProc method was declared as follows:
Private Function SubClassProc(ByVal hwnd As Integer, ByVal msg As Integer,
ByVal wParam As Integer, ByVal lParam As Integer) As Integer

To create a Delegate fully compatible with the above method, we simply emulate its Pinvoke Signature
in our Delegate declaration. Let us assume that we want to call this Delegate SubClassProcDelegate:
Private Delegate Function SubClassProcDelegate(ByVal hwnd As Integer, ByVal msg As Integer,
ByVal wParam As Integer, ByVal lParam As Integer) As Integer

Once declared, we must also refer to the Delegate within the invocation where the AddressOf operator is
to be employed. We do this so that the compiler will fully understand the actual provided method’s
signature. In our example’s case, this is the last parameter of the SetWindowLong Pinvoke definition,
which is to actually employ the referenced subclassing method’s memory address, given by AddressOf:
Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA"
(ByVal hwnd As Integer, ByVal attr As Integer, ByVal lVal As SubClassProcDelegate) As Integer 'replaced: ByVal lVal As Integer

NOTE: If we will use SetWindowLong differently elsewhere, we can declare overloads to match the needed signatures.
 Page 274

Notice that the last parameter, previously declared “ByVal lVal As Integer” is now declared “ByVal lVal
As SubClassProcDelegate” (see the note below if you are confused over how this substitution parameter
addressing will not confuse the Pinvoke, which was previously expecting just a simple 32-bit Integer).
Now, within our code, we can invoke SetWindowLong and its use of the AddressOf operator in the exact
same manner that we had previously done it under VB6:
Sub SubClassWindow(ByVal hwnd As Integer)
If PrevProcPtr = 0 Then 'You can also document the Delegate use by optionally using
PrevProcPtr = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf SubClassProc) 'code normally added "behind the green curtain" with:
End If 'PrevProcPtr = SetWindowLong(hwnd, GWL_WNDPROC, New SubClassProcDelegate(AddressOf SubClassProc))
End Sub 'Use this format if you love to type bunches and bunches!

NOTE: Keep in mind that even though we have declared a Delegate in place of the required 32-bit Integer for the method
address we are seeking, specified by the AddressOf operator, the AddressOf still provides a 32-bit integer address of the
procedure, because our Delegate declaration is internally expanded into a Class definition, which will of course return a 32-
bit Integer (technically an IntPtr). So even though the structure member or method parameter may need to be declared as the
Delegate type in the Pinvoke Signature prototype, in the end, at the compiled machine code level, it is still physically just a
single method address that is 32-bits wide, but made safer by allowing only methods that match the specified signature.
I can only hope this slightly annoying requirement for additional developer-generated code can be addressed in a
future edition of VB.NET so that it can auto-generate delegates when AddressOf is used, because the compiler
does have knowledge of the target method, though conflicts could crop up if that method name has overloads
defined, but it is possible to overcome even this issue through target method verification; clarification through
prototype specification whenever naming collisions occur. Regardless, if we compare the rather smooth upgrade
transition between VB6 and at least VB2008, set against the sometimes brutal upgrade to VB2002 or VB2003,
where we often stumbled out of a conversion suffering conditions not yet psychoanalytically identified, and how
with each succeeding release, from VB2005 onward, this upgrade process has become progressively easier and
leaving fewer and fewer “To-Do” items and upgrade warnings for the developer to deal with after an upgrade, I
can imagine a time will come when upgrading will require little attention at all by the developer, except in only
the most extreme upgrade situations.

Delegates and Callbacks.


Another powerful way to employ Delegates is to solve a problem that in VB6 prohibited it from being able to
directly assign a method address to a user-defined type member. In numerous Windows Pinvokes there is a
requirement for a thing called a Callback Function. Callbacks are hooks into pre-written code that allows
the Pinvoke being summoned to actually in turn invoke or call back one or more of our own custom-written
methods. This is especially useful in situations where a Pinvoke will need to inform us of periodic progress
updates, perhaps after timed intervals while it is processing data, or even when a background task has
completed. For example, web browsers can often take time to load a busy web page and may need to send
back updates so that we can reflect progress on a gauging control on our interface to keep their users staring
blankly like zombies at their screens, much akin to people who watch fishing programs.
The Pinvoke documentation for these special system methods provide us with a method prototype (a
Delegate) for exactly how each of our callback methods should be declared, detailing parameters, their
types, what they represent, and what information we in turn need to provide and sometimes send back as
a result when our method terminates or as the Pinvoke task progresses.
Under VB6, this callback was worked around in shared methods by adding a simple generic FARPROC
function declared within a module so we could assign a method address to a structure member, such as
“myStruct.cbFn = FARPROC(AddressOf myFn)”:
'******************************************************************************
' Function Name : FARPROC (VB6)
' Purpose : A dummy procedure that receives and returns the value of the AddressOf operator.
' :
' : This workaround is needed because you cannot assign AddressOf directly to a member of a user-
' : defined type, but you can assign it to another long and use that (as returned here).
'******************************************************************************
Private Function FARPROC(ByRef pfn As Long) As Long
FARPROC = pfn
End Function
 Page 275

But this did not solve a similar issue when one must supply an instance method address to a structure
member within class instances, which sometimes required some mighty fancy workarounds within separate
modules, though many of these scenarios fell against coding dead-ends that could not be made to work at all,
because VB6 allowed only shared methods, such as those declared within Modules, being methods without a
specific class instance, to use the above function. Yet, VB6 users would also try to use shared methods with
class instances, which was illegal under VB6. Conversely, the above generic VB6 solution does not work at
all under VB.NET because the AddressOf operator would be looking for a Delegate to associate the
addressed method with, unless we wrote numerous overloaded FARPROC functions that each specified the
parameter type as one of the desired Delegates, but which .NET makes totally unnecessary.
Not only do VB.NET Delegates provide an elegant solution to this prickly VB6 problem, eliminating the
need for a workaround function like FARPROC, but they also completely solve the problem VB6 had when
such was required in class instances. As described previously, we would simply declare a Delegate for the
Callback procedure that we want to supply the structure member or Pinvoke parameter with, and then we just
declare the structure member or parameter type as that Delegate instead of as an Integer, bercause in the end,
they will supply the very same Integer data.
The structure member or Pinvoke parameter that is to hold the required callback address is declared as a 32-
bit Integer. Hence, we use a Delegate at the declaration level to allow the structure to accept the needed 32-
bit method address without complaint, because when the structure is in turn passed to the summoned
Pinvoke, the Pinvoke will still find just a 32-bit function address at the member location we declared as the
Delegate type, storing there the address of the actual target function.
But also remember that when passing Structures to Pinvokes that they should be passed ByRef, and that the
Structure is also marshaled with the “ <StructLayout(LayoutKind.Sequential)>” attribute. Be sure to also
include “,CharSet:=CharSet.Auto” in the attribute list if unmarshaled string members are declared within the
structure. Use “,CharSet:=CharSet.Ansi” if the structure specifically requires 8-bit strings, or
“,CharSet:=CharSet.Unicode” if they specifically require Unicode strings. However, I tend to recommend
using “CharSet:=CharSet.Auto” because it will adjust for the requirements of the system automatically.
Invoking Methods through Delegates.
Another advantage of Delegates is that we can actually use a Delegate to invoke a method. At first glance,
this might seem pointless, because we might well argue that it would simply be easier to just invoke the
actual method. However, consider a scenario where there is a list of methods to choose from, and only one
method will be used based upon a particular situation and it will be invoked from numerous locations within
the code. Would it not be easier to simply invoke a generic method name at those points and assign this
generic method the execution address of the determined method? In using a Delegate, we can declare the
delegate and all its required parameters, if any, whose signature must be reflected in each of the alternative
methods, then declare an exposed field to be of the Delegate type and assign the address of the method to it.
Suppose we have a number of functions to choose from, and only one of them will be invoked variously
throughout an application based upon a flag. The first thing we do is declare the Delegate. For our first
example we will keep it mind-numbingly simple, assuming that each of the alternative methods is a
simple subroutine with no parameters. Therefore, our generic Delegate could be declared just like this:
Friend Delegate Sub GenericMethod() 'this method delegate can also include parameters, or a return type if a Function.

We must then declare an object reference variable for the GenericMethod prototype, which acts exactly
like a Callback Hook that a pre-written method can use to invoke our custom method:
Dim GenericSub As GenericMethod 'declare an instance of a referencer to objects with a signature of GenericMethod.

Finally, within the setup code, once we determine which method is to be variously invoked, we can
assign its address to GenericSub. We would assign it like this:
GenericSub = AddressOf SelectedMethod 'assign a method that uses the GenericMethod Signature format to the referencer.

Thereafter, we can simply invoke GenericSub anywhere, as needed, within the code that has scoping
access to it to in turn invoke the actual method to be executed.
 Page 276

Using a Delegate CallBack to Perform Shell-Metzner Sorts On ANY 1D Array.


But what about invoking methods that contain parameters or even return types? All we need to
remember is that the methods we write using a Delegate must be identical to the used Delegate’s
signature. All this really means is that the number of parameters, their storage types, and any return type
if a function, matches the types specified in the Delegate. We can name our parameters whatever we
like, but it simply keeps a cleaner appearance if the parameter names match those of the Delegate, even
though the names used in a Delegate are purely arbitrary, but it does makes sense that they should be
relevant to the methods using it as a prototype.
Suppose we want to use a Shell-Metzner Sort method to sort arrays. A Shell-
Metzner Sort is astoundingly faster than the default QuickSort method used by
.NET. Although a QuickSort is said to be compact and relatively fast, a Shell-
Metzner Sort leaves QuickSort in its settled dust, sorting so fast that we might
think a QuickSort is actually the famously super-slow Shell Sort. For years,
starting on TI-59 Programmable Calculators in 1979, I have used this Shell-
Metzner algorithm to sort strings and numbers. Adapting it to use Delegate
prototypes, it can be used to sort anything, to include arrays of custom-written
classes. The sorting routine can be written generically enough, but its one
comparison line must be changed to compare the actual types provided to it.
Here is where Delegates really pay off. By defining a project-accessible Delegate
prototype for the comparison, and also defining a project-accessible field defined
as that Delegate type, we can assign a custom-written comparison method to it.
The trick to adapting a sort method to use any type object is to make the conduit
to it generic enough to accept any array, whether it be an array of Strings,
Doubles, or of class objects (scalar, or numeric types, not being concrete objects,
cannot normally use this technique, but I will show us how to easily work around
that restriction). The second thing we need to write is a comparison method to
actually compare two objects that will be provided to it by the sorting method.
By declaring the interface to use Object types, we can cast them as needed to
their proper types when we absolutely must have such knowledge in our custom method.
We will declare our Delegate prototype to accept two parameters, which are to hold the two items to be
compared against each other, and like the results of CompareTo and StrComp comparisons, it must return an
integer value of -1 if the first object is less than the second object, return 0 if the two objects are equal, and
return 1 if the first object is greater than the second object. What is more, we can provide the sorting method
with a Boolean flag that can be used to indicate if we want to perform a Descending Sort or not. Following
that, we should declare a field that will be assigned the Delegate as a type, and also assign to it the address of
our custom-written comparison method that uses the Delegate as its prototype. This sounds more
complicated than it really is. To simplify it, first examine our first two required declarations, which we can
place at the start of the module body that will also include our Sort sorting method:
'---------------------------------------------------------------------------------
Friend Delegate Function SortComparerDelegate(ByVal LeftItem As Object, ByVal RightItem As Object) As Int32 'prototype for comparer.
Friend SortComparer As SortComparerDelegate = Nothing 'assign address of compare method to this hook before invoking Sort() method.
'---------------------------------------------------------------------------------

Below that, we write our generic Sort method that will invoke the SortComparer callback:
'*********************************************************************************
' Method : Sort
' Purpose : Sorting reference-type arrays Method (Strings; class objects).
' This method invokes SortComparer, which should first be assigned the
' address of a custom item comparison method that is defined to match
' the parameter definitions in the ComparerDelegate declaration.
'*********************************************************************************
Friend Function Sort(ByRef SortArray() As Object, Optional ByVal SortDescending As Boolean = False) As Boolean
' Make sure SortComparer is assigned to something
If SortComparer Is Nothing Then 'has SortComparer been assigned the address of a comparer method?
Return False 'no, so report failure.
End If
' get number of elements to sort. Exit if this is not an array.
 Page 277

Dim NumberofItems As Integer


Try
NumberofItems = UBound(SortArray) + 1 'number of items to sort.
Catch
SortComparer = Nothing 'error, so make sure comparer is reset.
Return False 'in dicate and error (the Array was not dimensioned).
End Try
' determine if we are sorting in Ascending or Descending order.
Dim AscDecFlag As Integer = 1 'default sort direction to ascending.
If SortDescending Then 'Descending flag specified?
AscDecFlag = -1 'yes, so we will be doing descending.
End If
' now perform the sort.
Dim HalfDown As Integer = NumberofItems 'number of items to sort.

Do While CBool(HalfDown \ 2) 'while counter can be halved.


HalfDown \= 2 'back down by ½.
Dim HalfUp As Int32 = NumberofItems - HalfDown 'look in upper half.
Dim IncIndex As Int32 = 0 'init index to start of array.
Do While IncIndex < HalfUp 'do while we can index range.
Dim IndexLo As Int32 = IncIndex 'set base.
Do
Dim IndexHi As Integer = IndexLo + HalfDown
'invoke the sort comparer method
If SortComparer(SortArray(IndexLo), SortArray(IndexHi)) = AscDecFlag Then 'check comparison result.
Dim Tmp As Object = SortArray(IndexLo) 'swap data if the first is greater than the second.
SortArray(IndexLo) = SortArray(IndexHi)
SortArray(IndexHi) = Tmp
IndexLo = IndexLo - HalfDown 'back up index.
Else
IncIndex += 1 'else bump counter.
Exit Do
End If
Loop While IndexLo >= 0 'while more things to check.
Loop
Loop
SortComparer = Nothing 'make sure comparer is reset.
Return True 'return success.
End Function

As we can see, the Sort method is expecting to receive the array as an array of type Object: “ ByRef
SortArray() As Object”. It also contains an optional Boolean parameter that specifies how to sort the
array. By default, it is sorted in Ascending order ( SortDescending = False). By providing the
SortDescending parameter with a True value, it will sort the array in Descending order.
The highlighted line is where the SortComparer method is invoked. We must assign it our own custom
comparison method before we can actually invoke the Sort method.
What follows is a generic method template that we can use to write our own comparer support. The
following template example is defined to compare two Strings, returning the standard StrComp or
CompareTo result of -1 for left is less than right, 0 for equal, and 1 for left is greater than right:
'*********************************************************************************
' Method : CustomComparer
' Purpose : Sample User-Defined Compare Function to support the Sort() method.
'*********************************************************************************
' USAGE:1) define your class that will be sorted, or choose a variable type.
' 2) choose which member field in the custom class is to be used to perform sort comparisons on.
' 3) Create a version of the Compare() method below that will compare on that member, which will
' be provided to the method as LeftItem and RightItem, for that member of two array elements.
' 4) Next assign your Compare method and invoke the sort as follows:
' SortComparer = AddressOf MyCustomCompare. Note we cannot pass it the address of just ANY method, but the
' Sort(MyArrayOfClass) 'provided method MUST match the Delegate, or an error is thrown.
'*********************************************************************************
Friend Function CustomComparer(ByVal LeftItem As Object, ByVal RightItem As Object) As Integer
Try
If LeftItem Is Nothing Then 'if LeftItem is Nothing...
If RightItem Is Nothing Then 'and RightItem is ALSO Nothing...
Return 0 'then they are equal.
Else
Return -1 'otherwise, RightItem>LeftItem, because LeftItem is Nothing and RightItem is not.
End If
ElseIf RightItem Is Nothing Then 'Else LeftItem is not Nothing but if RightItem is Nothing...
Return 1 'LeftItem>RightItem because RightItem is Nothing and LeftItem is not.
Else '-- But... Who's on First? Sorry, I could not resist the joke.
'Perform required numeric/text comparison. Specify the same target field to sort on as needed. For
'example, sorting on an object's Title field, where we would rewrite the following line to:
'Select Litem.Name.CompareTo(Ritem.Name) 'compare class object members if both LeftItem and RightItem are not Nothing...
'NOTE: string compares are best served by the StrComp() method, especially if you do not care about character case:
Select Case StrComp(DirectCast(LeftItem, String), DirectCast(RightItem, String), CompareMethod.Text)
Case 1
Return 1 'LeftItem>RightItem ( 1)(reflect CompareTo result).
Case -1
Return -1 'LeftItem<RightItem (-1) (reflect CompareTo result).
Case Else 'LeftItem=RightItem ( 0)
 Page 278

'If you are not comparing strings, you may want to replace the following line with a simple "Return 0".
'For strings, if the comparison matches, we will further want to compare the lengths of the strings,
'so that exact matches with X might actually have different lengths, such as if X was shorter than Y.
Return DirectCast(LeftItem, String).Length.CompareTo(DirectCast(RightItem, String).Length) 'compare lengths.
'Return 0 'x = y for most numeric types, unless we compare string fields of an object. Or,
'Return Litem.Name.Length.CompareTo(Ritem.Name.Length) 'class object string member compare.
End Select
End If
Catch
Return 0 'default to EQUAL if not of proper types.
End Try
End Function

Were we actually to use this method template for sorting Strings, we would first create a copy of it, cut
out the alternative notes, and simply name it CompareStrings, such as the following:
'*********************************************************************************
' Method : CompareStrings
' Purpose : Compare Function to support the Sort() method for sorting a String array.
'*********************************************************************************
Friend Function CompareStrings(ByVal LeftItem As Object, ByVal RightItem As Object) As Integer
Try
If LeftItem Is Nothing Then 'if LeftItem is Nothing...
If RightItem Is Nothing Then 'and Y is ALSO Nothing...
Return 0 'then they are equal.
Else
Return -1 'otherwise, RightItem>LeftItem, because LeftItem is Nothing and RightItem is not.
End If
ElseIf RightItem Is Nothing Then 'Else LeftItem is not Nothing but if RightItem is Nothing...
Return 1 'LeftItem>RightItem because RightItem is Nothing and LeftItem is not.
Else '...I feel like an auctioneer...
Select Case StrComp(DirectCast(LeftItem, String), DirectCast(RightItem, String), CompareMethod.Text)
Case 1
Return 1 'LeftItem>RightItem ( 1)(reflect CompareTo result).
Case -1
Return -1 'LeftItem<RightItem (-1) (reflect CompareTo result).
Case Else 'LeftItem=RightItem ( 0).
Return DirectCast(LeftItem, String).Length.CompareTo(DirectCast(RightItem, String).Length) ‘x = y, so compare lengths.
End Select
End If
Catch
Return 0 ‘default to EQUAL if not of proper types.
End Try
End Function

When we want to sort, in our preparation code we would assign the address of the CompareString
method to the SortComparer field (SortComparer = AddressOf CompareStrings), and then pass the array of
strings that we want sorted to the Sort method by recasting it to an array of type Object. For example:
Dim MyArray() As String = {"Bob", "Zed", "Allen", "Rick"}
SortComparer = AddressOf CompareStrings 'set our custom compare method to sort with.
Sort(DirectCast(MyArray, Object())) 'sort it in ascending order (default) as an object-type array.

For Each str As String In MyArray 'display the data in sorted order.
Debug.Print(str)
Next

Of course, we would not want to make it messy, or forget to cast our array to type Object so that the
compiler has an excuse to scold us, so what we would do to avoid that embarrassment is write a small
helper function in the module named SortStringArray that we can invoke using something like this:
Dim MyArray() As String = {"Bob", "Zed", "Allen", "Rick"}
SortStringArray(MyArray) 'sort it as an object-type array by way of a helper method.

For Each str As String In MyArray 'display the data in sorted order.
Debug.Print(str)
Next

Here is the SortStringArray helper method, which we should insert above our CompareString method:
'*********************************************************************************
' Method : SortStringArray
' Purpose : Sort a string array.
'*********************************************************************************
Friend Function SortStringArray(ByRef SortArray() As String, Optional ByVal SortDescending As Boolean = False) As Boolean
If SortArray Is Nothing Then Return Nothing ' 'user provided undimensioned array.
SortComparer = AddressOf CompareStrings 'set our custom compare method to sort with
Return Sort(DirectCast(SortArray, Object()),SortDescending) 'sort it as an object array (you can use CObj instead of DirectCast).
End Function
 Page 279

But what about scalar arrays, like Integer and Double, which cannot be cast to type Object? If we were to try
to cast them via DirectCast or CType to Object arrays, the compiler will snap its fingers right under our noses
and scold us like a school teacher that we (well... it) cannot cast Abstract Arrays to type Object.
Granted, we are able to use a variable of type Object to hold an individual scalar value, where it will
automatically “box” a class wrapper around it, but we are not so lucky with arrays, because a scalar array
cannot be boxed like individual scalars can. We might find this a bit confusing because, in .NET, all arrays
are Objects. The problem is that even though scalar arrays are pointed to by their referencing variable, which
technically qualifies them as Objects, its array data is still stacked in orderly rows of its Type size. What we
need to do is put them into an array where all members are stacked in even rows of Type Object, which are
pointers. Therefore, we are able to create an Object array that can contain individually object-boxed scalar
values to satisfy the compiler, and then pass that array to the Sort method. To work around this issue only
involves a small bit of work. We can either copy each scalar to an Object array, or we can create a small class
wrapper for a scalar value and at a tiny cost in time to copy values back and forth between the scalar array
and our object array. Consider the following SortIntegerArray and CompareIntegers declarations:
'*********************************************************************************
' Method : SortIntegerArray
' Purpose : Sort an integer array.
'
'EXAMPLE: Sorting an Integer array:
' Dim Ary() As Integer = {5, 26, 14, 3, 17} 'create an integer array to sort.
' SortIntegerArray(Ary) 'sort the array.
'
' For Each IntV As Integer In Ary 'display the result.
' Debug.Print(IntV.ToString)
' Next
'*********************************************************************************
Friend Function SortIntegerArray(ByRef SortArray() As Integer, Optional ByVal SortDescending As Boolean = False) As Boolean
If SortArray Is Nothing Then Return Nothing ' 'user provided undimensioned array.
Dim UBvalue As Int32 = UBound(SortArray) 'get ubound of Integer SortArray.
Dim Ary(UBvalue) As Object 'create a temporary object array.
For Idx As Integer = 0 To UBvalue
Ary(Idx) = SortArray(Idx) 'build object array.
Next
SortComparer = AddressOf CompareIntegers 'set our custom compare method to sort with.
Dim Result As Boolean = Sort(Ary, SortDescending) 'sort it as an object-type array.
For Idx As Integer = 0 To UBvalue 'copy the result back to the integer array.
SortArray(Idx) = DirectCast(Ary(Idx), Integer)
Next
Return Result
End Function

'*********************************************************************************
' Method : CompareIntegers
' Purpose : Compare Function to support the Sort() method for sorting an Integer array.
'*********************************************************************************
Private Function CompareIntegers(ByVal LeftItem As Object, ByVal RightItem As Object) As Integer
Try
If LeftItem Is Nothing Then 'if LeftItem is Nothing...
If RightItem Is Nothing Then 'and RightItem is ALSO Nothing...
Return 0 'then they are equal.
Else
Return -1 'otherwise, RightItem>LeftItem, because LeftItem is Nothing and RightItem is not.
End If
ElseIf RightItem Is Nothing Then 'Else LeftItem is not Nothing but if RightItem is Nothing...
Return 1 'LeftItem>RightItem because RightItem is Nothing and LeftItem is not.
Else 'compare Integers. Peter Piper picked a peck of pickled peppers...
Return DirectCast(LeftItem, Integer).CompareTo(DirectCast(RightItem, Integer))
End If
Catch
Return 0 'default to EQUAL if not of proper types.
End Try
End Function

The CompareIntegers method is much like the CompareStrings method, but it needs less testing. The
most interesting part is the SortIntegerArray helper function. There, we create an array of Type Object
that is sized the same as the delivered Integer array. We then populate it with the values of the Integer
array. Next, we assign our CompareIntegers method to the SortComparer Delegate callback hook. Next,
we simply invoke the sort. Finally, we repopulate the Integer array with the sorted list and return.
With just a few modifications to the above, we can likewise create a SortDoubleArray method and a
CompareDoubles method (or for any other scalar):
 Page 280

'*********************************************************************************
' Method : SortDoubleArray
' Purpose : Sort a double array.
'
'EXAMPLE: Sorting a Double array:
' Dim dAry() As Double = {72.3, 16.06, 14.0, 3.17, 17.32} 'create a Double array to sort.
' SortDoubleArray(dAry) 'sort the array.
'
' For Each DblV As Double In dAry 'display the result.
' Debug.Print(DblV.ToString)
' Next
'*********************************************************************************
Friend Function SortDoubleArray(ByRef SortArray() As Double, Optional ByVal SortDescending As Boolean = False) As Boolean
If SortArray Is Nothing Then Return Nothing ' 'user provided undimensioned array.
Dim UBvalue As Int32 = UBound(SortArray) 'get ubound of Double SortArray.
Dim Ary(UBvalue) As Object 'create a temporary object array.
For Idx As Integer = 0 To UBvalue
Ary(Idx) = SortArray(Idx) 'build object array.
Next
SortComparer = AddressOf CompareDoubles 'set our custom compare method to sort with.
Dim Result As Boolean = Sort(Ary, SortDescending) 'sort it as an object-type array.
For Idx As Integer = 0 To UBvalue 'copy the result back to the Double array.
SortArray(Idx) = DirectCast(Ary(Idx), Double)
Next
Return Result
End Function

'*********************************************************************************
' Method : CompareDoubles
' Purpose : Compare Function to support the Sort() method for sorting a Double array.
'*********************************************************************************
Private Function CompareDoubles(ByVal LeftItem As Object, ByVal RightItem As Object) As Integer
Try
If LeftItem Is Nothing Then 'if LeftItem is Nothing...
If RightItem Is Nothing Then 'and RightItem is ALSO Nothing...
Return 0 'then they are equal.
Else
Return -1 'otherwise, RightItem>LeftItem, because LeftItem is Nothing and RightItem is not.
End If
ElseIf RightItem Is Nothing Then 'Else LeftItem is not Nothing but if RightItem is Nothing...
Return 1 'LeftItem>RightItem because RightItem is Nothing and LeftItem is not.
Else 'compare Doubles. Bugs Black Blood, Bugs Black Blood, Bugs Black Blood...
Return DirectCast(LeftItem, Double).CompareTo(DirectCast(RightItem, Double))
End If
Catch
Return 0 'default to EQUAL if not of proper types.
End Try
End Function

Closing Notes on Delegates.


In case you have not thought of it, when we create a delegate for methods that features optional
parameters, be aware that optional parameters are not permitted in a Delegate declaration or when
invoking a method through a Delegate. What this means is that when using a Delegate, we cannot skip
optional parameters, but we must supply all of them. Therefore, when we define a Delegate for such a
method, simply specify any optional parameters instead as absolute parameters. For example, if the
original method heading is:
Friend Function myMsgBox(ByVal ParentForm As Form,
ByVal Prompt As String,
Optional ByVal MsgBoxFlags As MsgBoxStyle = MsgBoxStyle.OkOnly Or MsgBoxStyle.Information,
Optional ByVal Title As String = Nothing,
Optional ByVal RegistryKeyForCheckBoxValue As String = Nothing,
Optional ByVal CustomButtonText1 As String = Nothing,
Optional ByVal CustomButtonText2 As String = Nothing,
Optional ByVal CustomButtonText3 As String = Nothing) As MsgBoxResult

The Delegate we create for the above function can be formatted like the following:
Friend Delegate Function myMsgBoxDelegate(ByVal ParentForm As Form,
ByVal Prompt As String,
ByVal MsgBoxFlags As MsgBoxStyle,
ByVal Title As String,
ByVal RegistryKeyForCheckBoxValue As String,
ByVal CustomButtonText1 As String,
ByVal CustomButtonText2 As String,
ByVal CustomButtonText3 As String) As MsgBoxResult
 Page 281

As we can see, all optional parameters are specified as “required” parameters. As already indicated,
when the main method, myMsgBox in this case, is invoked, all optional parameters are always fed to it,
so internally the function is always invoked as we see in the above delegate. What the compiler does is
simply add a small bit of code to supply any parameters to the function that we did not, which is one of
the main reasons why we must provide default values for optional parameters we declare.
For example, if we invoked the myMsgBox function like so:
myMsgBox(Me, "Testing Optional Parameters")

Internally, it is submitted like this:


myMsgBox(Me, "Testing Optional Parameters",
MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, Nothing, Nothing, Nothing, Nothing, Nothing)

This exercise also highlights one of the reasons why providing default values are so important. Granted,
strings do have a default value of Nothing, numbers default to 0, and Booleans to False, but by providing
them, a reviewer then knows what the default is. Besides, I personally do not like unspecified default
values, because they can vary between platforms and languages. On top of that, I much prefer a default
value for string text being String.Empty rather than Nothing or VbNullString. In large part, this is because
Dim textLength As Integer = myString.Length can trigger an exception error if myString is set to Nothing,
whereas it will return 0 if it is set to String.Empty. Technically, this specifies "".
Further, if we had created a Delegate reference and assigned our myMsgBox method to it, such as:
Dim delMsgBox As myMsgBoxDelegate = myMsgBox

When we invoke myMsgBox by way of delMsgBox, we must specify ALL parameters:


delMsgBox(Me, "Testing Optional Parameters",
MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, Nothing, Nothing, Nothing, Nothing, Nothing)

RAD CLR SECRET: One of the things most users of VB.NET are not aware of is how important working in a Managed
Space is. We read it all the time, but the details of why this is important is lost to most of us, who read only as much
documentation as we think we have to, which is never enough. We typically do not think Managed Space is important. and if
we use the AddressOf compiler directive, we tend to absolutely despise Delegates. But the truth is, these things are saving
our bacon beyond our imagination. Many C++ developers understand it, which is part of why they belittle VB so much, even
though most of them will either not admit, or even understand why. They sometimes do so only because their upheld gurus
voice such condemnation. It is because working within the .NET managed space means every object created within it is a
managed object, except of course those instantiated through Win32 allocation methods, typically via Pinvokes or by running
COM processes. This means that each managed object registers itself with the CLR (Common Language Runtime). This way,
when we exit our application, if we forgot to release any managed object we created, instead of in turn creating a memory
leak for each and every object we did not release, eventually locking up our system memory and usually requiring a hard
reboot (if it does not unexpectedly do it for us), the CLR will scan its registration list by way of the Garbage Collector and
release them all. This is exactly the reason why Microsoft strongly recommends we use .NET Framework methods as much as
possible, even when allocating memory space on the Heap, because even those .NET-provided methods are registered with
the CLR, so it can release it in cases where we went insane and, during our dissonant state of mind, we forgot to release it.
How many times have we bothered to Dispose of every Form we use? Or Dispose of every control we placed on each Form?
How many times have we created “temporary” objects and not disposed of them, just leaving them for the Garbage Collector
to dispose of them for us? How do we think it does that? Now we know. Granted, this process is accelerated by parenting
.NET objects scanning through their child objects and disposing of them, and by us specifically going through and releasing
our objects, but the CLR ensures that every managed object created under it is released before the application will exit.
Delegate Classes are part of this. All events, functions and subroutines are registered in the CLR as Delegate Classes, and
the Delegates ensure that methods using their sugnatures comply with them and are bound to them, but they also ensure all
these bound methods are released, including Static and Shared Fields. If we knew how pampered we were, we would
understand why we are despised by other language developers. It is not only because VB.NET is the most popular computer
language on the planet, it is not only because VB.NET is the fastest to use computer language on the planet, where one
can develop robust code up to 168 times faster than C++. but it is also because VB.NET is the most powerful computer
language on the planet. We are so-o-o-o-o pampered with this incredible RAD (Rapid Application Development) language!
 Page 282

Adding Run-Time Custom Menus and Cloning ToolStripMenuItems Under VB.NET.


Many online gurus, and especially VB6 critics of VB.NET, never tire of telling us that VB.NET cannot
use top-level menus as context menus, that we cannot clone dropdown menus, or that we cannot build
on-the-fly menus as easily as under VB6. However, that is not just categorically untrue, but as we will
herein discover, we can in fact do much more with menus than that which VB6 could offer. With tools
already existing in our digital toolbox, plus those that are quite easy to create, which I will provide code
for in this article, we can eliminate every single one of these presumed issues and render them all false.
Using top-level menus as context menus in VB6 resulted from the primitive non-object-oriented design
required by older Microsoft Windows editions. But this was not their choice, but due to the then-current
16-bit architecture and 4MHz CPU clock speed limit. There, a ContextMenu and a MenuItem were
interchangeable. A VB6 MenuItem could be used as any child member of a main menu, much as a
TreeView’s Node object can be used as a root, as a branch, or as a leaf. Also, the same MenuItem could
be used simultaneously in a main menu and in a context menu. However, VB.NET by default uses a far
more powerful ToolStripMenuItem object to represent each leaf of a menu tree. Also, unlike the ancient
MenuItem, a ToolStripMenuItem can only be rooted to a MenuSrip or to a ContextMenuStrip object, but
not to both at the same time. A MenuSrip and ContextMenuStrip are entirely different objects. Also, their
members cannot be encapsulated by more than one object because data safety would otherwise be
compromised, which is not possible in a well-behaved OOPL like VB.NET. The solution to this
dilemma is through either copying or cloning them, much as we might copy strings or clone arrays.
It is proclaimed by many online gurus that we can get around such VB.NET ‘incompatibility’ with VB6
by using the CloneMenu method of a MenuItem to cobble together a copy of it and its children so we can
'share' a full menu construct between a MainMenu and a ContextMenu. Even so, we are not, by default,
able to do so with even the MenuStrip and ContextMenuStrip of VB6, which are the default menu
interfaces offered by VB.NET. However, this article will show you how to do exactly that.
Were we to fabricate our VB.NET menus using MainMenu, ContextMenu, and MenuItem objects, which
can be done quite easily at run-time, and very soon from the IDE, we can take full advantage of their
CloneMenu method. Conversely, with the default and more powerful strip versions, we usually have to
go through manual menu construction channels to 'copy' them, though we will return to address this
issue shortly, where we will achieve this supposed ‘impossible’ task, and quite easily.
NOTE: If we read the menu documentation, we will discover the MergeMenu method of a MainMenu and ContextMenu.
With this we will not need to bother with cloning – it will clone it for us – and we can employ main menu items as context
menus just as easily as we did under VB6. We will explore both of these methods, CloneMenu and MergeMenu, shortly.
My very first ever, clumsy attempt to resolve this perceived huge problem was to manually convert an
existing MenuStrip to a MainMenu. From this I could grab ready-made menu members and experiment
with how to employ them as popup menus. This process ended up being extremely complicated, and it
clearly showed me why menu strips were so vehemently barfed at by VB6 programmers migrating to
VB.NET! But all that convoluted code was hiding all its actual simplicity, as I am soon to demonstrate.

Understanding Menus by Converting a MenuStrip to a MainMenu.


I am going to first show you how I went through the troublesome task of converting a MenuStrip menu
into a MainMenu and then I will show you its underlying simplicity by creating Main Menus and
Context Menus from scratch. By first learning how to convert a MenuStrip into the simpler MainMenu
format, we can better understand general menu mechanics. So grab some popcorn and read on.
The first step is to create a new Windows Form Application. I called it MenuTesting, and I will be using
that project later on. I will also be grabbing data from it and storing it in NotePad.
NOTE: If you have not placed a shortcut to NotePad onto your QuickLaunch bar, you will find NotePad at Start / All Programs / Accessories
(or Programs / Windows Accessories). Please also consider placing a copy of its shortcut into our SendTo folder so that you can right-click a
file and send it directly to Notepad by using the Send To menu. It is located at %APPDATA%\Microsoft\Windows\SendTo , which can be
pasted into the File Explorer Address Bar and it will accept it!
 Page 283

Next, I added a MenuStrip to the form and renamed the default MenuStrip1 to mnuMain.
Next, I added some menu items. Along the menu ribbon, I added “&File” and “&Edit”. Under “File”, I
added “E&xit”. Under “Edit”, I added “Cut”, “Copy”, and “Paste”. Optionally, we can add Shortcut
keys that will display on the right side of the menu. For “Exit”, I like to use “Alt+F4”; the application
close command. So in the ShortcutKeys property of the “Exit” entry (actually automatically named
ExitToolStripMenuItem), I placed a check in the Alt checkbox, and from the dropdown list I selected
F4, and then close the Shortcutkeys property’s dropdown. Now, “Alt+F4” will appear alongside “Exit”
in the menu. I also added “Ctrl+X” for Cut, “Ctrl+C” for Copy, and “Ctrl+V” for Paste.
NOTE: Save this project, if you went ahead and created it. We will be using it later when we learn how to create a
ContextMenuStrip from a MainMenuStrip. However, you will also be able to just copy that code listing later.
We have all we need from this project, at least for now. Save it (if you made it). In the Solution
Explorer, ensure the “Show All Files” button is set so we can access our Form Designer code;
the code that VB.NET automatically generates for us. Click the Plus sign beside Form1 in the
Solution Explorer to open up its now-visible ‘directory tree’ and open Form1.Designer.vb. What we will
see is a slew of code that should be similar to the following (well, I have commented it heavily to clarify
the edits that we (well... I) will need to perform in Notepad, and I also struck through lines to delete):
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class Form1
Inherits System.Windows.Forms.Form
'Form overrides dispose to clean up the component list.
<System.Diagnostics.DebuggerNonUserCode()> _
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
Try
If disposing AndAlso components IsNot Nothing Then 'Notice they built their Dispose mechanism without
components.Dispose() 'Implementing IDispose, but built it from 'scratch'.
End If 'No need for a SAFE Dispose() entry in a 'protected' area.
Finally
MyBase.Dispose(disposing)
End Try
End Sub
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer 'Have you ever tried peeking in here?

'NOTE: The following procedure is required by the Windows Form Designer


'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor. '<-- Attention! This is the code police! We know you're in there!
<System.Diagnostics.DebuggerStepThrough()> _ ' Release the NotePad and come on out with your hands up!
Private Sub InitializeComponent() '<-- Try and stop us, coppahs! You'll never take us unedited!
Me.mnuMain = New System.Windows.Forms.MenuStrip 'change this to: Me.mnuMain = New MainMenu
Me.FileToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem 'change this to: Me.FileToolStripMenuItem = New MenuItem
Me.EditToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem 'change this to: Me.EditToolStripMenuItem = New MenuItem
Me.ExitToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem 'change this to: Me.ExitToolStripMenuItem = New MenuItem
Me.CutToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem 'change this to: Me.CutToolStripMenuItem = New MenuItem
Me.CopyToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem 'change this to: Me.CopyToolStripMenuItem = New MenuItem
Me.PasteToolStripMenuItem = New System.Windows.Forms.ToolStripMenuItem 'change this to: Me.PasteToolStripMenuItem = New MenuItem
Me.mnuMain.SuspendLayout()
Me.SuspendLayout()
'
'mnuMain
'
'Change "Me.mnuMain.Items.AddRange" below to "Me.mnuMain.MenuItems.AddRange"
'Change "New System.Windows.Forms.ToolStripItem()" below to "New MenuItem()"
Me.mnuMain.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.FileToolStripMenuItem, Me.EditToolStripMenuItem})
Me.mnuMain.Location = New System.Drawing.Point(0, 0) 'It auto-docks here ANYWAY, unless Dock prop. non-default.
Me.mnuMain.Name = "mnuMain"
Me.mnuMain.Size = New System.Drawing.Size(284, 24) 'It auto-sizes to Client width ANYWAY. I am starting to
Me.mnuMain.TabIndex = 0 ' think the Forms Generator bloviates more than me!
Me.mnuMain.Text = "MenuStrip1"
'
'FileToolStripMenuItem
'
'Change "Me.FileToolStripMenuItem.DropDownItems.AddRange" below to "Me.FileToolStripMenuItem.MenuItems.AddRange"
'Change "New System.Windows.Forms.ToolStripItem()" below to "New MenuItem()"
Me.FileToolStripMenuItem.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.ExitToolStripMenuItem})
Me.FileToolStripMenuItem.Name = "FileToolStripMenuItem"
Me.FileToolStripMenuItem.Size = New System.Drawing.Size(37, 20) 'This control autosizes to this! Why redundantly code it?
Me.FileToolStripMenuItem.Text = "&File" 'These last 3 lines could have been done in one line of code.
'
'EditToolStripMenuItem
'
'Change "Me.EditToolStripMenuItem.DropDownItems.AddRange" below to "Me.EditToolStripMenuItem.MenuItems.AddRange"
'Change "New System.Windows.Forms.ToolStripItem()" below to "New MenuItem()"
Me.EditToolStripMenuItem.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() _
{Me.CutToolStripMenuItem, Me.CopyToolStripMenuItem, Me.PasteToolStripMenuItem})
Me.EditToolStripMenuItem.Name = "EditToolStripMenuItem"
 Page 284

Me.EditToolStripMenuItem.Size = New System.Drawing.Size(39, 20) 'A TON of this code is not actually necessary, ANYWAY. I
Me.EditToolStripMenuItem.Text = "&Edit" ' think the code generator just wants to look important!
'
'ExitToolStripMenuItem
'
Me.ExitToolStripMenuItem.Name = "ExitToolStripMenuItem"
'Change the whole long line below to: Me.ExitToolStripMenuItem.Shortcut = Shortcut.AltF4
Me.ExitToolStripMenuItem.ShortcutKeys = _
CType((System.Windows.Forms.Keys.Alt Or System.Windows.Forms.Keys.F4), System.Windows.Forms.Keys)
Me.ExitToolStripMenuItem.Size = New System.Drawing.Size(152, 22)
Me.ExitToolStripMenuItem.Text = "E&xit"
'
'CutToolStripMenuItem
'
Me.CutToolStripMenuItem.Name = "CutToolStripMenuItem"
'Change the whole long line below to: Me.CutToolStripMenuItem.Shortcut = Shortcut.CtrlX
Me.CutToolStripMenuItem.ShortcutKeys = _
CType((System.Windows.Forms.Keys.Control Or System.Windows.Forms.Keys.X), System.Windows.Forms.Keys)
Me.CutToolStripMenuItem.Size = New System.Drawing.Size(152, 22)
Me.CutToolStripMenuItem.Text = "Cut"
'
'CopyToolStripMenuItem
'
Me.CopyToolStripMenuItem.Name = "CopyToolStripMenuItem"
'Change the whole long line below to: Me.CopyToolStripMenuItem.Shortcut = Shortcut.CtrlC
Me.CopyToolStripMenuItem.ShortcutKeys = _
CType((System.Windows.Forms.Keys.Control Or System.Windows.Forms.Keys.C), System.Windows.Forms.Keys)
Me.CopyToolStripMenuItem.Size = New System.Drawing.Size(152, 22)
Me.CopyToolStripMenuItem.Text = "Copy"
'
'PasteToolStripMenuItem 'Are we there yet?
' 'Don't make me turn this auto-generator around!
Me.PasteToolStripMenuItem.Name = "PasteToolStripMenuItem"
'Change the whole long line below to: Me.PasteToolStripMenuItem.Shortcut = Shortcut.CtrlV
Me.PasteToolStripMenuItem.ShortcutKeys = _
CType((System.Windows.Forms.Keys.Control Or System.Windows.Forms.Keys.V), System.Windows.Forms.Keys)
Me.PasteToolStripMenuItem.Size = New System.Drawing.Size(152, 22)
Me.PasteToolStripMenuItem.Text = "Paste"
'
'Form1
'
Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!)
Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
Me.ClientSize = New System.Drawing.Size(284, 264)
Me.Controls.Add(Me.mnuMain) '<-- NOTE THIS LINE. IT WILL BECOME IMPORTANT LATER TO TOOLSTRIPS!
' change the following line to: Me.Menu = mnuMain
Me.MainMenuStrip = Me.mnuMain
Me.Name = "Form1"
Me.Text = "Form1"
Me.mnuMain.ResumeLayout(False)
Me.mnuMain.PerformLayout()
Me.ResumeLayout(False)
Me.PerformLayout()
End Sub
Friend WithEvents mnuMain As System.Windows.Forms.MenuStrip 'change "System...MenuStrip" to "MainMenu"
Friend WithEvents FileToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem 'change "System...ToolStripMenuItem" to "MenuItem"
Friend WithEvents ExitToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem 'ditto
Friend WithEvents EditToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem 'ditto BTW, if we declared As New here, separate
Friend WithEvents CutToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem 'ditto instantiation above would not be needed!
Friend WithEvents CopyToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem 'ditto
Friend WithEvents PasteToolStripMenuItem As System.Windows.Forms.ToolStripMenuItem 'ditto
End Class

Notice I have added quite a number of editing instructions to the above raw form data (I also added
continuation tags for the lines that ran over). I copied all this text and pasted it into NotePad.
Next, in Notepad, I deleted all the StrikeThrough lines, shown above. Finally, I applied the edits I had
annotated. You might want to follow along and copy my resulting data into NotePad, shown below:
Me.mnuMain = New MainMenu
Me.FileToolStripMenuItem = New MenuItem
Me.EditToolStripMenuItem = New MenuItem
Me.ExitToolStripMenuItem = New MenuItem
Me.CutToolStripMenuItem = New MenuItem
Me.CopyToolStripMenuItem = New MenuItem
Me.PasteToolStripMenuItem = New MenuItem
'
'mnuMain
'
Me.mnuMain.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.FileToolStripMenuItem, Me.EditToolStripMenuItem})
'
'FileToolStripMenuItem
'
Me.FileToolStripMenuItem.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.ExitToolStripMenuItem})
Me.FileToolStripMenuItem.Text = "&File"
'
 Page 285

'EditToolStripMenuItem
'
Me.EditToolStripMenuItem.MenuItems.AddRange(New System.Windows.Forms.MenuItem() _
{Me.CutToolStripMenuItem, Me.CopyToolStripMenuItem, Me.PasteToolStripMenuItem})
Me.EditToolStripMenuItem.Text = "&Edit"
'
'ExitToolStripMenuItem
'
Me.ExitToolStripMenuItem.Shortcut = Shortcut.AltF4
Me.ExitToolStripMenuItem.Text = "E&xit"
'
'CutToolStripMenuItem
'
Me.CutToolStripMenuItem.Shortcut = Shortcut.CtrlX
Me.CutToolStripMenuItem.Text = "&Cut"
'
'CopyToolStripMenuItem
'
Me.CopyToolStripMenuItem.Shortcut = Shortcut.CtrlC
Me.CopyToolStripMenuItem.Text = "C&opy"
'
'PasteToolStripMenuItem
'
Me.PasteToolStripMenuItem.Shortcut = Shortcut.CtrlV
Me.PasteToolStripMenuItem.Text = "&Paste"
'
'Form1
'
Me.Menu = mnuMain
Friend WithEvents mnuMain As MainMenu
Friend WithEvents FileToolStripMenuItem As MenuItem
Friend WithEvents ExitToolStripMenuItem As MenuItem
Friend WithEvents EditToolStripMenuItem As MenuItem
Friend WithEvents CutToolStripMenuItem As MenuItem
Friend WithEvents CopyToolStripMenuItem As MenuItem
Friend WithEvents PasteToolStripMenuItem As MenuItem

As we can see, the edits I applied to the MenuStrip data to convert them to MenuItems was not too
difficult, though exhausting. This is the main reason new VB.NET users cringe from menustrips like
vampires from sunlight. This also shows what is needed for MainMenu construction and that building
MainMenus are simpler and require less code than MenuStrips. but constructing either actually requires
far less code than even shown here, which we will soon discover will be better than 75% shorter!
Armed with the above text, we are ready to create a project to take full advantage of my work so far.
Create a new Form App. Once loaded, double-click the form to go to the Form1_Load event. From the
above listing, copy the last 7 lines (all beginning with “Friend WithEvents”), and paste them above the
Form1_Load event. Next, copy the rest of the other data above and paste it to within the Form1_Load
event code. Our Form1.vb code file should now look like this (note that I added still more comments):
Public Class Form1 'For how we are using these menus, the WithEvents verbs in the lines below are not really necessary
Friend WithEvents mnuMain As MainMenu 'These declarations are handy for form construction from the IDE, so we can take
Friend WithEvents FileToolStripMenuItem As MenuItem 'advantage of associated events from the drop-down menus, but not for on-thge-fly
Friend WithEvents ExitToolStripMenuItem As MenuItem 'menus, as you will soon learn. We could have also declared each of these'As New'.
Friend WithEvents EditToolStripMenuItem As MenuItem 'When I construct main menus from scratch, I use much simpler names than those
Friend WithEvents CutToolStripMenuItem As MenuItem 'created for us by the MenuStrip. I use names such as FileMenu, EditMenu,
Friend WithEvents CopyToolStripMenuItem As MenuItem 'and the like. A WithEvents declaration CAN be made, but is not necessary, for
Friend WithEvents PasteToolStripMenuItem As MenuItem 'every MainMenu and each main menu item or submenu item.
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Me.mnuMain = New MainMenu 'instantiate new event-supported objects for use in our app
Me.FileToolStripMenuItem = New MenuItem
Me.EditToolStripMenuItem = New MenuItem 'NOTE: if we had declared each of the above 'Friend WithEvents' declarations
Me.ExitToolStripMenuItem = New MenuItem 'AS NEW (for example, Friend WithEvents mnuMain As New MainMenu, or
Me.CutToolStripMenuItem = New MenuItem 'Friend WithEvents FileToolStripMenuItem As New MenuItem, then these latter
Me.CopyToolStripMenuItem = New MenuItem 'instantiation declarations would have been totally unnecessary. Try it!
Me.PasteToolStripMenuItem = New MenuItem
'
'mnuMain (attach File and Edit menu items to the Main Menu)
Me.mnuMain.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.FileToolStripMenuItem, Me.EditToolStripMenuItem})
'
'FileToolStripMenuItem (attach EXIT submenu items to the File menu item)
Me.FileToolStripMenuItem.MenuItems.AddRange(New System.Windows.Forms.MenuItem() {Me.ExitToolStripMenuItem})
Me.FileToolStripMenuItem.Text = "&File"
'
'EditToolStripMenuItem (attach submenu CUT, COPY, and PASTE items to the Edit menu item)
Me.EditToolStripMenuItem.MenuItems.AddRange(New System.Windows.Forms.MenuItem() _
{Me.CutToolStripMenuItem, Me.CopyToolStripMenuItem, Me.PasteToolStripMenuItem})
Me.EditToolStripMenuItem.Text = "&Edit"
'
'ExitToolStripMenuItem (define the additional features for the Exit submenu item)
 Page 286

Me.ExitToolStripMenuItem.Shortcut = Shortcut.AltF4
Me.ExitToolStripMenuItem.Text = "E&xit"
'
'CutToolStripMenuItem (define the additional features for the Cut submenu item)
Me.CutToolStripMenuItem.Shortcut = Shortcut.CtrlX
Me.CutToolStripMenuItem.Text = "Cut"
'
'CopyToolStripMenuItem (define the additional features for the Copy submenu item)
Me.CopyToolStripMenuItem.Shortcut = Shortcut.CtrlC
Me.CopyToolStripMenuItem.Text = "Copy"
'
'PasteToolStripMenuItem (define the the additional features for the Paste submenu item)
Me.PasteToolStripMenuItem.Shortcut = Shortcut.CtrlV
Me.PasteToolStripMenuItem.Text = "Paste"
'
'Form1 (finally attach our newly constructed Main Menu to the form)
Me.Menu = mnuMain
End Sub
End Class

We are now ready to test the code. If no syntax errors were reported, run the program
as-is, without event handler code for the menu items. We should see the form come
up, complete with a menu, which we can open and move around in (we do not have
any event code to support them yet, so do not be concerned).
We will add event code manually (well, we could have written that code in the MenuStrip project). We
are usually concerned with only Click events. We construct such events in 4 parts; 3 parts to build the
event heading (name, parameters, and optional handlers), and the 4th part to build the event body.
To keep this simple, we will name our events Exit_Click, Cut_Click, Copy_Click, and Paste_Click.
Note the “_Click” part is not required, as it was under VB6, and we could even name the Exit_Click
event Bob, but the VB6-style name simply makes the most sense and is also descriptive of what its
purpose is.
NOTE: The _Click format comes from the old VB1-VB6 object_eventname naming convention, which VB.NET is not
subject to, due to its implementation of Handler tags and assignments.
We usually begin an event declaration with “Private Sub”, then the event name, such as “Exit_Click”,
then the standard parameter list “(ByVal sender As Object, ByVal e As EventArgs)”. Finally we decide
what event this code should handle. So we type “ Handles”, type a space, then select from its context
menu the event we want the new subroutine to handle, such as “ ExitToolStripMenuItem”, type a dot, and
then select the “Click” event. Lastly, press the Return key (this events recognition is thanks to declaring
the menu members “WithEvents”). The empty body of our event code should look like this:
Private Sub Exit_Click(ByVal sender As Object, ByVal e As EventArgs) Handles ExitToolStripMenuItem.Click
End Sub

NOTE: Because these controls are declared “WithEvents”, we could have selected the “ExitToolStripMenuItem” control
from the left code page dropdown and selected its “Click” event from the right one to construct the above event body.
Now we need to enter some code to react to the click. For this event we simply want to exit the program,
so type into the body “Me.Close” or even “Application.Exit”. Our event code will now look like this:
Private Sub Exit_Click(ByVal sender As Object, ByVal e As EventArgs) Handles ExitToolStripMenuItem.Click
Me.Close()
End Sub

Next, add code for other required menu selection events (Cut, Copy, and Paste). Just display a message:
Private Sub Cut_Click(ByVal sender As Object, ByVal e As EventArgs) Handles CutToolStripMenuItem.Click
MsgBox("Cut")
End Sub
Private Sub Copy_Click(ByVal sender As Object, ByVal e As EventArgs) Handles CopyToolStripMenuItem.Click
MsgBox("Copy")
End Sub
Private Sub Paste_Click(ByVal sender As Object, ByVal e As EventArgs) Handles PasteToolStripMenuItem.Click
MsgBox("Paste")
End Sub

Now test the above code, to make sure everything is working properly.
 Page 287

Optimizing Menu Design Code.


We can make the still-bulky Form1_Load code much shorter. When instantiating MenuItems, we can also
give them titles, an optional image, an event handler, an optional shortcut key, and all in one instruction
line, eliminating the need for one or two extra lines of code, as had been done in the above translated
program. Consider the following new and compacted version providing everything the previous code did
(we will soon make even this look top-heavy). Be sure to also delete all the WithEvents code lines.
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
Me.Menu = New MainMenu 'Notice that we can construct a new main menu right within the form’s Menu reference, if we want to.
' Build File menu items
Dim FileMenu As New MenuItem("&File") 'File header item.
Dim ExitMenu As New MenuItem("E&xit", New EventHandler(AddressOf Exit_Click), Shortcut.AltF4) 'shortcuts are optional, not required.
' Build Edit menu items
Dim EditMenu As New MenuItem("&Edit") 'Edit header item.
Dim CutMenu As New MenuItem("Cut", New EventHandler(AddressOf Cut_Click), Shortcut.CtrlX)
Dim CopyMenu As New MenuItem("Copy", New EventHandler(AddressOf Copy_Click), Shortcut.CtrlC)
Dim PasteMenu As New MenuItem("Paste", New EventHandler(AddressOf Paste_Click), Shortcut.CtrlV)
' Link the parent menus File and Edit to their associated submenu items.
' This also stores references to all these locally-created objects in persistent memory throgh the main menu object,
' eliminating any need to maintain local references to keep them persistent.
FileMenu.MenuItems.AddRange(New MenuItem() {ExitMenu})
EditMenu.MenuItems.AddRange(New MenuItem() {CutMenu, CopyMenu, PasteMenu})
' Finally, link the main menu to the two subordinate menu headers File and Edit (note that we now use Me.Menu, rather than mnuMain).
Me.Menu.MenuItems.AddRange(New MenuItem() {FileMenu, EditMenu}) 'all locals are now referenced & linked to the main menu structure.
End Sub

Also, because the events are now assigned their event handlers from within the above new code, we no
longer require specifying the WithEvents verb in our declarations and we must also be sure to eliminate
the handlers that we had initially placed at the end of our click event code; otherwise the events will
actually fire the event code twice in a row, because there are presently two handlers declared; one in the
menu item declaration, and a second at the end of the click event methods. So here are our event
methods without their trailing handlers being assigned directly to them, which we now no longer need:
Private Sub Exit_Click() 'Note that VB.NET allows us to also skip typing the event parameters!
Me.Close() 'The code will STILL work OK. Handy for simple one-control events.
End Sub 'And especially when we are HAND-CODING event methods.

Private Sub Cut_Click() 'Commando (no underwear) versions of event code...


MsgBox("Cut")
End Sub

Private Sub Copy_Click() 'Feeling free and breezy...


MsgBox("Copy")
End Sub

Private Sub Paste_Click() 'I know you were looking here to see if I had something snappy to say...
MsgBox("Paste")
End Sub

Run it after updating the code to test the results.

Building ContextMenus from MainMenu Items.


Now suppose we want to right-click the screen and pop up the Edit menu contents. In order to do that
we will either need to add a MouseDown event on Form1 or construct the context menu and apply it as
the form’s current default context menu. If we use a MouseDown event, then when this event is
triggered, we will check to see if the right mouse button is down. If so, we will display our Edit menu.
Let’s try it using the MouseDown event. With our code for Form1 up, select “(Form Events)” and
“MouseDown” from the two menus at the top of the code form. With an empty Form1_MouseDown
code block opened up, enter the following code, making the event code look like this,
Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
If e.Button = MouseButtons.Right Then 'if the mouse-down was with the right button...
Dim cMenu As New ContextMenu 'declare a new context menu.
cMenu.MenuItems.Add(Me.Menu.MenuItems(1).CloneMenu) 'clone the EDIT menu (Cloning makes duplicates of handlers and any submenus)
cMenu.Show(Cursor.Position) 'show the EDIT menu (MenuItem(1)) with top-left at the mouse pointer location.
End If 'NOTE: The above cMenu.Show() example will work even if you have form controls under the cursor.
End Sub 'Note also we are not tied to our menus having to be tied to the form! This is why I call them "on-the-fly".

This “on-the-fly” approach copies the edit menu item itself, along with its
subordinate objects. This is OK, and this is useful for duplicating drop downs on
 Page 288

multi-menu forms, but most people instead want the sub-items of the menu only, to
emulate exactly what VB6 had done when using main menu items as a context menu.
To do that, we could easily enumerate through the sub-menu items and clone them. It
only involves replacing one line with three very easy lines of code. Replace the one
highlighted line in the above listing with the following 3-line For...Each loop code:
For Each itm As MenuItem In Me.Menu.MenuItems(1).MenuItems 'scan each sub-item in the Edit menu (Me.Menu.MenuItems(1)).
cMenu.MenuItems.Add(itm.CloneMenu) 'add an item (Cloning creates a new dupe, incl. event handler).
Next 'do all of them.

With the above change, the “Edit” portion of the menu will not be displayed.
However, a more elegant way to display this same result, significantly cut down on
processing time, and to bypass manually implementing the cloning process
altogether, is to employ the MergeMenu method. With it we can reduce the above
replacement code back to a single line again, consisting of:
cMenu.MergeMenu(Me.Menu.MenuItems(1)) 'merge/clone the Edit menu's dropdown items into the context menu.

Another way to display a context menu is to set it up and assign it as the Form Context Menu. All we
need to do is add the following code to the bottom of the Form1_Load event code:
Dim cMenu As New ContextMenu 'declare a new context menu.
cMenu.MergeMenu(Me.Menu.MenuItems(1)) 'merge Edit menu's items into the context menu (EditMenu reference still active here).
Me.ContextMenu = cMenu 'set as default form context menu.

NOTE: Be sure to also remove or comment out the Form1_MouseDown event code if we do the above, to avoid conflicts.
Notice further that we do not actually need to set this short snippet of code in the Load event, but we can instead place it
anywhere that we really need it, changing it as often as we require to suit the needs of our application.
Still another way to create context menus is to construct them on the fly and directly to the form’s menu
controls. It is so easy that it is hardly worth all the fiery jagged boulders so many VB6 users catapulted
at VB.NET. Plus, if we have already written our event handling methods, it is super-easy. Consider the
following quick context menu construct, which we can place anywhere in our code where needed:
Me.ContextMenu = New ContextMenu 'We can create the new context menu right in the Me.ContextMenu reference.
Me.ContextMenu.MergeMenu(Me.Menu.MenuItems(1)) 'merge/clone the Edit menu's submenu items into the new context menu.

Or try this, being more of a “roll-your-own” version, not requiring the presence of a main menu at all:
Me.ContextMenu = New ContextMenu 'Notice we can create the form’s new context menu right within it’s ContextMenu reference.
Me.ContextMenu.MenuItems.Add("Cut", New EventHandler(AddressOf Cut_Click)) 'We create new Delegates using New EventHandler(). So if
Me.ContextMenu.MenuItems.Add("Copy", New EventHandler(AddressOf Copy_Click)) 'you like this feature, but claim to hate Delegates...
Me.ContextMenu.MenuItems.Add("Paste", New EventHandler(AddressOf Paste_Click)) 'you need to find an anonymous support group.

NOTE: If we have already added a new menu or context menu to our form, such as in our Form_Load event, or even added
a blank menu or context menu by placing only “Me.Menu = New MainMenu” or “Me.ContextMenu = New ContextMenu” within
our Form_Load event; then elsewhere, when we want to add a new menu or context menu, instead of creating a new
MainMenu or ContextMenu object each subsequent time, we can instead simply replace the usual “Me.Menu = New
MainMenu” with “Me.Menu.MenuItems.Clear” statement when building new main menus, or “ Me.ContextMenu = New
ContextMenu” with “Me.ContextMenu.MenuItems.Clear ” statement when building new context menus . We can then go on
about adding menu items as before, but with a slight increase in speed, because we had simply cleared the main menu or
context menu objects without removing them and replacing them with newly instantiated ones, saving resources.
NOTE: If we had added a context menu via Me.ContextMenu but later wish to use context menus from right-mouse click
events, then we should also be sure to set Me.ContextMenu.Clear or set Me.ContextMenu = Nothing. Otherwise, when we
show our mouse-triggered menu, a context menu previously assigned to Me.ContextMenu will also be displayed.
IMPORTANT NOTE: If we create a context menu for a TextBox and do not assign anything to its own ContextMenuStrip
property (it is set to Nothing), it will display its own ContextMenu (TextBox and RichTextBox sport ContextMenu and
ContextMenuStrip properties). Countless programmers have gone nuts over this. We can disable it by making sure it is not
Nothing and set its ContextMenuStrip to a object using “TextBox1.ContextMenuStrip = New ContextMenuStrip”. A
RichTextBox does not have this issue. Sadly, this problem exists because users had demanded a default TextBox menu.
Alternatively, we could define a single separate handler method to take care of our context menus. In
this simple example I will in turn just invoke our current behavior, but you certainly get the idea by now:
 Page 289

'Note: Either write the following method first, or just its declaration, before adding the lines referencing
' it to the Form1_Load event, or wherever you actually need to set up the context menu.
Private Sub Context_Click(ByVal sender As Object, ByVal e As EventArgs)
Select Case DirectCast(sender, MenuItem).Text 'DirectCast, a compiler directive using no code. Like C/C++ casting.
Case "Cut"
MsgBox("Cut")
Case "Copy"
MsgBox("Copy")
Case "Paste"
MsgBox("Paste")
End Select
End Sub
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
Me.ContextMenu = New ContextMenu
Me.ContextMenu.MenuItems.Add("Cut", New EventHandler(AddressOf Context_Click)) 'use new Context_Click event handler.
Me.ContextMenu.MenuItems.Add("Copy", New EventHandler(AddressOf Context_Click)) 'use new Context_Click event handler.
Me.ContextMenu.MenuItems.Add("Paste", New EventHandler(AddressOf Context_Click)) 'use new Context_Click event handler.
End Sub

If you have not yet realized it, we can also construct context menus on-the-fly, and after the user has
already made a right-click on a form, much as shown earlier with the CloneMenu examples, but instead
creating the data for the context menu entirely from scratch. Just instantiate a ContextMenu object and
add the menu features we want to add, and finally process that ContextMenu, such as shown below:
Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
If e.Button = MouseButtons.Right Then 'if the mouse-down was with the right button...
With New ContextMenu 'declare a new context menu.
.MenuItems.Add("Cut", New EventHandler(AddressOf Cut_Click)) 'The Add() method returns an index into the MenuItems.
.MenuItems.Add("Copy", New EventHandler(AddressOf Copy_Click)) ' collection that you could capture and use to enhance
.MenuItems.Add("Paste", New EventHandler(AddressOf Paste_Click)) ' context menu entries even more.
.Show(Cursor.Position) 'show the menu with top-left at the mouse pointer location.
Ebd With
End If
End Sub

We now have all the requisite knowledge we need to create menus and context menus that do not exist in our
application in any other form; and all constructed in just moments, on-the-fly. We can probably also see that event
handler code is nothing at all magical or mysterious, being simple subroutines of any name with optional special
parameters to satisfy the internally-created Delegate prototypes for pre-defined events, and they are not even
confined to being saddled with any explicit Handles verb at design time. Further, it is also clear that we do not
need to have specific menu objects, like the declaration of a CutToolStripMenuItem object (which are generated
to construct a main menu), or even an existing menu to create context menus from the IDE at design time.
For example, the following raw code can be added to a fresh form and immediately run and tested:
Public Class Form1
Private Sub Cut_Click()
MsgBox("Cut")
End Sub

Private Sub Copy_Click()


MsgBox("Copy")
End Sub

Private Sub Paste_Click()


MsgBox("Paste")
End Sub

Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load


Me.ContextMenu = New ContextMenu
With Me.ContextMenu.MenuItems
.Add("Cut", New EventHandler(AddressOf Cut_Click))
.Add("Copy", New EventHandler(AddressOf Copy_Click))
.Add("Paste", New EventHandler(AddressOf Paste_Click))
End With
End Sub
End Class

Once we have built a few custom menus, we may just find that run-time menu construction is the fastest, easiest
solution to our application needs. All that is left for us to explore is adding the bells and whistles, such as item
enabling/disabling, item visibility, checks, RadioButton checks (a single check allowed for various options), and
icon images. If all else fails, build the menus using a test MenuStrip control, then see how they did their magic in
the Designer code in order to add such features to our manually-constructed menus. We will be swift to discover
that doing even what appears to be the most complex menu feat is actually quite easy and quick to adapt and use.
 Page 290

Adding a MainMenu and ContextMenu Control to Your VB.NET Toolbox.


Although we can easily build menus using a MainMenu object, we must do so programmatically.
However, there is a fast and easy way to add both the older-style MainMenu and ContextMenu controls
to our Toolbox, which we can use in place of the default MenuStrip and ContextMenuStrip controls.
To Add MainMenu and ContextMenu controls to our Toolbox, all we have to do is the following:
1. With a form up on the screen so that Toolbox controls are displayed, right-click the Menus & Toolbars group heading within the
Toolbox and select the “Choose items...” menu option.
2. Wait for the “Choose Toolbox Items” dialog box to come up (this may take some time on some systems, because it is enumerating a lot
of system components). When the dialog is finally up, be sure the “.Net Framework Components” tab is selected.
3. Scroll down the list until we see a “ContextMenu” entry. Fact is, we might possibly
see multiple entries. My Vista system had three. However, we are interested in one
that is not presently selected and is also the only one that is located within the
Global Assembly Cache directory. Place a checkmark in its selection box.
4. Scroll down the list until we see a “MainMenu” entry. As with the ContextMenu
entry, we might see multiple entries again. Mine also had three. However, we are
interested in the one that is not selected and is also the only one located within the
Global Assembly Cache directory. Place a checkmark in its selection box as well.
5. Select OK and we will subsequently see a ContextMenu and a MainMenu entry
added to our Menus & Toolbars list in our Toolbox, which we can immediately
begin to use.
Once we have these in our Toolbox, we can drop them on our
forms. Some people prefer to use them because they require more primitive, yet also much simpler code
to support thrm, others like them because they think these menus look better, or they prefer them for the
simple fact that its MenuItem objects do have a MdiList property available (the whole point to a later
exercise in the next article), or they prefer to use them because they also have a very powerful MergeMenu
method available so that we can quickly duplicate an existing submenu branch from the main menu to a
new context menu.
NOTE: We are going to add this MergeMenu functionality to support MenuStrip objects shortly.

Building ContextMenuStrip PopUp Menus on the Fly.


Now that we have exploited the MainMenu, ContextMenu, and MenuItem objects, let us look back to the
default VB.NET MenuStrip, ContextMenuStrip, and ToolStripMenuItem objects.
Unlike the ContextMenu object, the ContextMenuStrip object lacks any sort of MergeMenu method,
but this is because a CloneMenu method is also lacking from ToolStripMenuItem objects. It is possible
to write code to add cloning into a new object that inherits ToolStripMenuItem, as we will soon do, but
most examples I have seen will also warn us that they do not always work perfectly; but this is simply
due to menu cascading (dropdowns) not being accounted for. We will address that. Even so, it is still
easy enough to follow the lead of our last several examples and construct context menu strips on the fly.
Reload the saved MenuStrip project, MenuTesting, and add the following methods to the form’s code to
apply support for its Exit, Cut, Copy, and Paste menu items:
'I built these using the WithEvents functionality intact, so the full definitions were generated,
' but you are free to use gutted definitions, omiting the 'sender' and 'e' parameters.
Private Sub ExitMenu_Click(ByVal sender As Object, ByVal e As EventArgs) Handles ExitToolStripMenuItem.Click
Me.Close()
End Sub

Private Sub CutMenu_Click(ByVal sender As Object, ByVal e As EventArgs) Handles CutToolStripMenuItem.Click


MsgBox("Cut")
End Sub

Private Sub CopyMenu_Click(ByVal sender As Object, ByVal e As EventArgs) Handles CopyToolStripMenuItem.Click


MsgBox("Copy")
End Sub

Private Sub PasteMenu_Click(ByVal sender As Object, ByVal e As EventArgs) Handles PasteToolStripMenuItem.Click


MsgBox("Paste")
End Sub
 Page 291

Because ToolStripMenuItem controls lack a CloneMenu method, and ContextMenuStrip controls lack
a MergeMenu method, (for now, at least), we will first build context menus from scratch, much as we
have been doing in the previous examples. However, unlike with the much simpler MenuItem object,
with ToolStripMenuItem objects we will have to construct wholly new menu structures (rest assured that
we will soon eliminate this issue by easily deriving a new ToolstripMenuItem class that can be cloned).
Following is a Form MouseDown event that will create a ContextMenuStrip for the dropdown items we
had defined for the Edit menu on our main MenuStrip, constructed “the hard way” (well, at least I think
it is still pretty easy), Note the Nothing below can optionally specify an Image or Image resource.
Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
If e.Button = MouseButtons.Right Then 'if the mouse-down was with the right button...
With New ContextMenuStrip 'Replace NOTHING below with an Image from an ImageList or resources.
.Items.Add(New ToolStripMenuItem("Cut", Nothing, New EventHandler(AddressOf CutMenu_Click)))
.Items.Add(New ToolStripMenuItem("Copy", Nothing, New EventHandler(AddressOf CopyMenu_Click)))
.Items.Add(New ToolStripMenuItem("Paste", Nothing, New EventHandler(AddressOf PasteMenu_Click)))
.Show(Cursor.Position) 'display and support the new context menu.
End With
End If
End Sub

NOTE: As with a ContextMenu, when building menus from the form load event or similar, we can use Me.ContextMenuStrip
= New ContextMenuStrip , and then use the Form’s ContextMenuStrip member to build a new ContextMenuStrip, just as we
had done previously with the ContextMenu objects.
Notice that we specified the 3 ToolStripMenuItem controls for Cut, Copy, and Paste, to construct a
ContextMenuStrip that emulates our main menu’s Edit dropdown list of members.
Nice. But it would be really nice if we could simplify the developer’s job of adding entries to a menu.
But I only say that as an enticement, because we will be doing exactly that very shortly!

Building MenuStrips on the Fly.


To build a MainMenuStrip is just as easy as building a ContextMenuStrip, but with a couple different
steps. First, instead of adding the ToolStripMenuItem objects to a DropDown collection as we will with a
ContextMenuStrip or to another ToolStripMenuItem, we must add main menu items to a MenuStrip’s
DropDownItems collection. E-Z! Second, along with assigning the MenuStrip object to the form’s
MainMenuStrip property to bring it to life, such as by entering “ Me.MainMenuStrip = myMenuStrip”, we
must be sure to add it to the form’s Controls collection, such as “Me.Controls.Add(myMenuStrip)”. Failure
to perform this second step will prevent the MenuStrip from being displayed, because it is not yet
attached to our form's list of controls to process. MainMenu interfaces are processed through more
primitive levels of the system, bypassing controls processing altogether, where they would have been
capable of much more power. However, with MenuStrips, it would have been nice if the system could
just peek over and notice something in the form's MainMenuStrip property. Even so, there is a little
magic added by adding our MenuStrip to the Controls collection! With our MenuStrip in the Controls
collection, we could bypass placing it in the MainMenuStrip Property and just set the MenuStrip's
Visible Property to True. It will then display as our Main Menu! This is great for multi-menu forms!
NOTE: The above second point is very important. Without adding our MenuStrip to the Controls
collection, we will not see it! This is a point that has aggravated a lot of programmers, even though the
documentation for the MainMenuStrip clearly reports in its terse remarks, “In addition to setting the
MainMenuStrip property, we must add the MenuStrip control to the Controls collection of the form.”
Sadly, the documentation does not explain WHY we must add it to the collection, but if we read the end
of the last paragraph, we have figured it out! Too bad, like so much other documentation, we usually
have to figure out all the functionality offered. If all this stuff is actually created by committee, we would
think the documentation writers would have access to the committee notes or something. Even I
meticulously document everything I create, even if I will ever be the only one who ever reads it. This
tome started out that way, and I still use it as my personal reference.
 Page 292

Dim mMenu As New MenuStrip 'create a new MenuStrip.


With mMenu
Dim FileItem As New ToolStripMenuItem("&File") 'Build the FILE member of the Main Menu.
FileItem.DropDownItems.Add(New ToolStripMenuItem("E&xit", Nothing, AddressOf ExitMenu_Click))
Dim EditItem As New ToolStripMenuItem("&Edit") 'Build the EDIT member of the Main Menu.
'Add CUT, COPY, and PASTE members
EditItem.DropDownItems.Add(New ToolStripMenuItem("Cut", Nothing, AddressOf CutMenu_Click, Keys.Control Keys.X))
EditItem.DropDownItems.Add(New ToolStripMenuItem("Copy", Nothing, AddressOf CopyMenu_Click, Keys.Control Or Keys.C))
EditItem.DropDownItems.Add(New ToolStripMenuItem("Paste", Nothing, AddressOf PasteMenu_Click, Keys.Control Or Keys.V))
.Items.AddRange(New ToolStripMenuItem() {FileItem, EditItem}) 'Add FILE and EDIT menu items to the Main Menu
.Dock = DockStyle.Top 'dock to form top (default, but you may want a diff. Side).
End With
Me.MainMenuStrip = mMenu 'assign to Main Menu Strip.
Me.Controls.Add(mMenu) 'add to form controls so we can actually see it! Also, add it last for proper Z-Order display.

Cloning MenuStrip Items with a Cloneable ToolStripMenuItem Class.


We can implement a class that will create a clone of tool strip menu items for us, which we can then use
as a member of a new context menu, but there is a small catch. Currently we are not able to clone event
handlers contained within a ToolStripMenuItem because it’s Events list property is unexposed
(protected) except to a derived class. What this means is that we cannot clone a ToolStripMenuItem
directly, yet we are able to do so in a derived class, but then we are stuck with needing to construct our
menus manually using that derived class. This can sound like a pain, but now that we know how to
easily construct menus on-the-fly, this is not really much of a pain at all.
It bugs me that we cannot access a ToolStripMenuItem's Events list, because what we need to do is
assign the Events list of the item being cloned to the clone’s Events list, which would be just this simple
line of code: “clonedItem.Events.AddHandlers(Me.Events)”. Were we able to do that, we could easily clone
a ToolStripMenuItem without any need whatsoever for constructing a derived class to access it. Sigh...
Jessica at the Microsoft Dev Center explained “ToolStripMenuItem is quite a bit richer than MenuItem,
and we couldn't guarantee full-property fidelity when cloned.” However, the very reason for this
statement is because it could not cascade dropdown lists for cloned items. But that strikes me as bizarre,
because off the cuff I realized it requires only 5 additional lines of code to enable this cascading, and
two of those lines are the IF-block to determine if the item has dropdown items, and so the 3 remaining
lines are able to recurse through however many children and generations to fully clone the complete
object, thus providing all the full-property fidelity that Microsoft could not somehow guarantee!
For example, assuming that we named our cloneable ToolStripMenuItem class as cToolStripMenuItem,
our added CloneMenu function could initially be defined like so:
'*******************************************************************************
' Method Name : CloneMenu (thanks to jfo's coding at MSDN Blogs)
' Description : Return a Clone of the selected item.
' : Use the syntax used for instantiating a ToolStripMenuItem.
'*******************************************************************************
Public Function CloneMenu() As cToolStripMenuItem
Dim clonedItem As New cToolStripMenuItem
With clonedItem
'copy events (events is a protected EventHandlerList accessible only in a derived class).
.Events.AddHandlers(Me.Events) 'too bad we cannot access this method outside this derived class...
'-----------------------------------------------------------------------
.AccessibleName = Me.AccessibleName
.AccessibleRole = Me.AccessibleRole
.Alignment = Me.Alignment
.AllowDrop = Me.AllowDrop
.Anchor = Me.Anchor
.AutoSize = Me.AutoSize
.AutoToolTip = Me.AutoToolTip
.BackColor = Me.BackColor
.BackgroundImage = Me.BackgroundImage
.BackgroundImageLayout = Me.BackgroundImageLayout
.Checked = Me.Checked
.CheckOnClick = Me.CheckOnClick
.CheckState = Me.CheckState
.DisplayStyle = Me.DisplayStyle
.Dock = Me.Dock
.DoubleClickEnabled = Me.DoubleClickEnabled
.Enabled = Me.Enabled
.Font = Me.Font
.ForeColor = Me.ForeColor
.Image = Me.Image
 Page 293

.ImageAlign = Me.ImageAlign
.ImageScaling = Me.ImageScaling
.ImageTransparentColor = Me.ImageTransparentColor
.Margin = Me.Margin
.MergeAction = Me.MergeAction
.MergeIndex = Me.MergeIndex
.Name = Me.Name
.Overflow = Me.Overflow
.Padding = Me.Padding
.RightToLeft = Me.RightToLeft
.ShortcutKeys = .ShortcutKeys
.ShowShortcutKeys = Me.ShowShortcutKeys
.Tag = Me.Tag
.Text = Me.Text
.TextAlign = Me.TextAlign
.TextDirection = Me.TextDirection
.TextImageRelation = Me.TextImageRelation
.ToolTipText = Me.ToolTipText
.Available = Me.Available
End With
Return clonedItem 'return cloned ToolStripMenuItem and any dropdowns.
End Function

The above function is my direct VB.NET translation of the C# Clone method provided by JFO at jfo's
coding at MSDN Blogs (http://blogs.msdn.com/b/jfoscoding/archive/2005/09/28/475177.aspx). In addition, I also added all the
other properties that JFO's clone method has not bother including.
However, to make this code robust so it also includes any cascaded sub-items in all and however-many
generations of dropdowns from the object, we need only add the following before the above “End With”:
'-----------------------------------------------------------------------
'if this menu item in turn has dropdown menu items, then clone them as well. DRG
'-----------------------------------------------------------------------
If .HasDropDownItems Then 'if this item has children...
For Each mItem As cToolStripMenuItem In .DropDownItems 'process each child item.
.DropDownItems.Add(mItem.CloneMenu) 'recurse children (also allows deeper generations).
Next
End If

This code will recurse for as many downward levels as needed to clone any and all child menu items.
Of course, we will also need to easily create the new items so we can create a menu effortlessly. The
only thing is, we must remember that all members of this menu must be of type cToolStripMenuItem
rather than ToolStripMenuItem. Consider first the full cToolStripMenuItem class definition, listed below:
'*************************************************************************************
'*************************************************************************************
' cToolStripMenuItem Class
' Clone() function derived from C# source at: jfo's coding at MSDN Blogs:
' http://blogs.msdn.com/b/jfoscoding/archive/2005/09/28/475177.aspx
'
' jfo's code featured only the CloneMenu function and did not explain how to use the class.
' I added all the constructors and read the tea leaves to figure out how to implement it.
' I also added a recursive method to include dropdown items in a clone to make it richer.
'*************************************************************************************
'*************************************************************************************
Public Class cToolStripMenuItem
Inherits ToolStripMenuItem

'--------------------------------------------------------------------------------
' Method Name : New (various overloads) - DRG
' Description : the overloaded constructors are used to instantiate a ClonableToolStripMenuItem
' : using the syntax used for instantiating a ToolStripMenuItem.
'--------------------------------------------------------------------------------
'RAW instantiation
Public Sub New()
MyBase.New()
End Sub

'Instantiation with just Text


Public Sub New(ByVal text As String)
MyBase.New(text)
End Sub

'Instantiation with Text and Image


Public Sub New(ByVal text As String, ByRef Image As Image)
MyBase.New(text, Image)
End Sub

'Instantiation with Text, Image, and onClick event reference.


Public Sub New(ByVal text As String, ByRef image As Image, ByRef onClick As EventHandler)
MyBase.New(text, image, onClick)
End Sub
 Page 294

'Instantiation with Text, Image, onClick event reference, and menu item control name.
Public Sub New(ByVal text As String, ByRef Image As Image, ByRef onClick As EventHandler, ByVal name As String)
MyBase.New(text, Image, onClick, name)
End Sub

'Instantiation with Text, Image, onClick event reference, and Shortcut keys (add easy shortcut feature of MenuItem objects).
Public Sub New(ByVal text As String, ByRef Image As Image, ByRef onClick As EventHandler, ByVal shortcutKeys As Keys)
MyBase.New(text, Image, onClick, CType(shortcutKeys, Keys))
End Sub

'Instantiation with Text, Image, onClick event reference, and Shortcut keys.
Public Sub New(ByVal text As String, ByRef Image As Image, ByRef onClick As EventHandler, ByVal shortcutKeys As Shortcut)
MyBase.New(text, Image, onClick, CType(shortcutKeys, Keys))
End Sub

'--------------------------------------------------------------------------------
' Method Name : CloneMenu – DRG translation of original JFO C# code.
' Description : Return a Clone of the selected item.
' : Use the syntax used for instantiating a ToolStripMenuItem.
'--------------------------------------------------------------------------------
Public Function CloneMenu() As cToolStripMenuItem
Dim clonedItem As New cToolStripMenuItem
With clonedItem
'copy events (events is a protected EventHandlerList accessible only in a derived class).
.Events.AddHandlers(Me.Events) 'too bad we could not access this outside this derived class...
'-----------------------------------------------------------------------
.AccessibleName = Me.AccessibleName
.AccessibleRole = Me.AccessibleRole
.Alignment = Me.Alignment
.AllowDrop = Me.AllowDrop
.Anchor = Me.Anchor
.AutoSize = Me.AutoSize
.AutoToolTip = Me.AutoToolTip
.BackColor = Me.BackColor
.BackgroundImage = Me.BackgroundImage
.BackgroundImageLayout = Me.BackgroundImageLayout
.Checked = Me.Checked
.CheckOnClick = Me.CheckOnClick
.CheckState = Me.CheckState
.DisplayStyle = Me.DisplayStyle
.Dock = Me.Dock
.DoubleClickEnabled = Me.DoubleClickEnabled
.Enabled = Me.Enabled
.Font = Me.Font
.ForeColor = Me.ForeColor
.Image = Me.Image
.ImageAlign = Me.ImageAlign
.ImageScaling = Me.ImageScaling
.ImageTransparentColor = Me.ImageTransparentColor
.Margin = Me.Margin
.MergeAction = Me.MergeAction
.MergeIndex = Me.MergeIndex
.Name = Me.Name
.Overflow = Me.Overflow
.Padding = Me.Padding
.RightToLeft = Me.RightToLeft
.ShortcutKeys = .ShortcutKeys
.ShowShortcutKeys = Me.ShowShortcutKeys
.Tag = Me.Tag
.Text = Me.Text
.TextAlign = Me.TextAlign
.TextDirection = Me.TextDirection
.TextImageRelation = Me.TextImageRelation
.ToolTipText = Me.ToolTipText
.Available = Me.Available
'-----------------------------------------------------------------------
'if this menu item in turn has dropdown menu items, then clone them as well. DRG
'-----------------------------------------------------------------------
If .HasDropDownItems Then 'if this item has children...
For Each mItem As cToolStripMenuItem In .DropDownItems 'process each child item.
.DropDownItems.Add(mItem.CloneMenu) 'recurse children (also allows deeper generations).
Next
End If
End With
Return clonedItem 'return cloned ToolStripMenuItem and any dropdowns.
End Function
End Class

How do we use it? If we look to the previous example for building MenuStrips on the fly, we would do
exactly that, except that we would be using cToolStripMenuItem rather than ToolStripMenuItem:
Dim mMenu As New MenuStrip 'create a new MenuStrip.
With mMenu
Dim FileItem As New cToolStripMenuItem("&File") 'Build the FILE member of the Main Menu.
FileItem.DropDownItems.Add(New cToolStripMenuItem("E&xit", Nothing, AddressOf ExitMenu_Click))
Dim EditItem As New cToolStripMenuItem("&Edit") 'Build the EDIT member of the Main Menu.
'Add CUT, COPY, and PASTE members
EditItem.DropDownItems.Add(New cToolStripMenuItem("Cut", Nothing, AddressOf CutMenu_Click, Shortcut.CtrlX))
 Page 295

EditItem.DropDownItems.Add(New cToolStripMenuItem("Copy", Nothing, AddressOf CopyMenu_Click, Shortcut.CtrlC))


EditItem.DropDownItems.Add(New cToolStripMenuItem("Paste", Nothing, AddressOf PasteMenu_Click, Shortcut.CtrlV))
.Items.AddRange(New cToolStripMenuItem() {FileItem, EditItem}) 'Add FILE and EDIT menu items to the Main Menu.
.Dock = DockStyle.Top 'dock to top of form.
End With
Me.MainMenuStrip = mMenu 'assign to Main Menu Strip.
Me.Controls.Add(mMenu) 'add to form controls so we can actually see it! Also, add it last for proper Z-Order display.

To create a clone of a cToolStripMenuItem object is easy. If cTSMI were of type cToolStripmenuItem,


we could clone it like this: “Dim clonedItem As cToolStripMenuItem = cTSMI.CloneMenu ”. This will also
clone any child menu items, such as if cTSMI contained a dropdown list.

Using cToolStripMenuItem.CloneMenu to Create New MergeMenuStrip Methods.


Of course, one of my goals is to be able to construct a new context menu from a MainMenuStrip
member (or simply a ToolStripMenuItem that has dropdown members), as we were able to do using the
MergeMenu method of a ContextMenu object. With the above cToolStripMenuItem derived class with its
added Clone method available to use as support, we can now easily define yet another helper method in
our form that will clone a menu item’s dropdown objects to a new context menu strip. Consider the
following new MergeMenuStrip method:
'*******************************************************************************
' Method Name : MergeMenuStrip
' Description : Use this method to quickly convert main menu items in a ToolStripMenu into ContextMenuStrip objects.
'*******************************************************************************
Friend Function MergeMenuStrip(ByVal StripItem As cToolStripMenuItem) As ContextMenuStrip
Dim Cmenu As New ContextMenuStrip 'declare a new context menu strip object.
For Each itm As cToolStripMenuItem In StripItem.DropDownItems 'process all sub-items from the MainStrip Item
Cmenu.Items.Add(itm.CloneMenu) 'add each to the new context menu strip.
Next
Return Cmenu 'return the newly constructed context menu strip.
End Function

We would use it much as we used the MergeMenu function from a ContextMenu object:
'*******************************************************************************
' Method : Form1_MouseDown
' Purpose : Clone the Edit menu if we right-click the form.
'*******************************************************************************
Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
If e.Button = MouseButtons.Right Then 'if the mouse-down was with the right button...
Dim Cmenu As ContextMenuStrip 'define a context menu strip object reference.
'build a new context menu and assign it to our object reference
Cmenu = MergeMenuStrip(DirectCast(Me.MainMenuStrip.Items(1), cToolStripMenuItem))
Cmenu.Show(Cursor.Position) 'show the menu with top-left at the mouse pointer location.
End If
End Sub

Of course, the catch here is that we cannot reference sub-items from a MenuStrip without casting it to a
cToolStripMenuItem, as we can see above. However, because the Items collection of a MenuStrip returns
an object of type ToolStripItem, we could write an additional overload of MergeMenuStrip to take care
of that for us so that we will not ever need to bother about the recasting issue ourselves:
'*******************************************************************************
' Method Name : MergeMenuStrip (overload)
' Description : Use this method to quickly convert main menu items in a ToolStripMenu into ContextMenuStrip objects.
'*******************************************************************************
Friend Function MergeMenuStrip(ByVal StripItem As ToolStripItem) As ContextMenuStrip
Return MergeMenuStrip(DirectCast(StripItem, cToolStripMenuItem)) 'cast to cToolStripMenuItem and try again.
End Function

Instead of writing the whole function all over again, we just casted the ToolStripItem to the parameter
expected by the other MergeMenuStrip overload function and then invoked it. No magic, just overloads.
With this overload in place, we could simplify our invocation to this:
Cmenu = MergeMenuStrip(Me.MainMenuStrip.Items(1)) 'build a new context menu and assign it to our object reference.
 Page 296

And, we can simplify our mouse down event even further by using this valid shorthand technique:
'*******************************************************************************
' Method : Form1_MouseDown
' Purpose : Clone the Edit menu if we right-click the form.
'*******************************************************************************
Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
If e.Button = MouseButtons.Right Then 'if the mouse-down was with the right button...
MergeMenuStrip(Me.MainMenuStrip.Items(1)).Show(Cursor.Position) 'show the menu with top-left at the mouse location.
End If
End Sub

NOTE: Those new to Object-Oriented Programming might be a bit confused as to how we can use a stock
ToolStripMenuItem and operate on it using the cToolStripMenuItem class code, such as its CloneMenu method. First, a
ToolStripMenuItem and a cToolStripMenuItem have the exact same data signature footprint, so we can actually plug either
object into each other's code through Casting, which tells the Compiler to use this code over here instead of the code it was
instantiated with, as long as the “Mom” part of the compiler verifies that the object does not have any fleas and so can go
out and play with the neighbor kid. Likewise, a cToolStripMenuItem can be cast to use the ToolStripMenuItem code. The
code is just the tools to operate on the data. We can add more tools to a derived class, but the data is still the exact same data
for the class it was derived from.

Conclusion.
With the above examples as a guide to getting started, we can add more power to our form menu needs
than we may have ever dreamed possible, or ourselves capable. For all the lamenting that I have heard
over VB.NET’s new, more powerful menu system, I have yet to find anything to spit vitriol about,
except for the lack of an actual CloneOnClick or CloneMenuStrip method for ToolStripMenuItem
objects, and a MergeMenuStrip method for MainMenuStrip and ContextMenuStrip objects. However,
with the power afforded me with all the .NET development tools I have at my disposal, and my ability to
easily work around such problems with absolutely no difficulty, plus the fact that I am also armed with
MainMenu, ContextMenu, and MenuItem objects, plus the speed at which I am able to construct on-the-
fly menus, to clone them, and to easily employ them, I am not lamenting very loudly. In fact, I see it as
no real bother at all.
 Page 297

Adding Window and File Lists to VB.NET Menus.


Adding and removing custom menu items from form menus has changed between VB6 and VB.NET.
Although many VB6 aficionados loudly complain that they do not like the way VB.NET supports menu
creation, I think it really has to do with the fact that they have simply grown accustomed to emplementing
the VB6 method; no matter how complicated they can often make it. On the other hand, to my thinking, the
VB.NET method is far superior and much easier. In fact, I model my VB6 menus after VB.NET menus.

Maintaining an Active Child Windows List.


Under VB6, when using a Multiple Document Interface, or MDI, we had to
maintain separate menus on separate forms. The main menu was assigned to
an MDI Parent Form, being from a special form template that contained a
hidden VB.MDIForm attribute, which displayed its menu only when no MDI
Child Form was displayed, and another menu, assigned to an MDI Child
Form, being a form that had its MDIChild property set to True, which was
displayed when an instance of the MDI Child Form was open.
With this design approach, we could easily add a list of currently open MDI
Child Forms, referred to as windows, to one of the dropdown menus on the displayed menu bars, such as
a “&Window” menu entry by simply assigning a checkmark in the WindowList checkbox of the Menu
Editor when the target entry was selected (see the illustration above and to the right). This told the
background system to automatically add a list of presently open MDI Child Windows to the bottom of
that menu’s submenu list, complete with a leading line separator. This design approach worked well, but
there was a lot of concurrent programming code required of the developer just to support identical menu
options for both the main and the child menus, making this schema often very frustrating to work with,
especially if menu options keep changing throughout the run of an application.
NOTE: When a VB6 MDI Child Form is displayed, its own menu will replace the default MDI Parent menu on the MDI
Parent Form by reassigning its menu to its parent. The MDI Child Form would of course be displayed without that menu.
Conversely, a VB.NET MDI Parent Form will maintain the menu interfaces for both when a VB.NET
MDI Child Form is or is not displayed. This streamlines the entire process and simplifies a ton of code.
However, as a consequence, we will need to modify the MDI Parent menu for additional options that
should be made available only when MDI Child Forms are present. This is easily accomplished by
rendering select menu entries invisible when no MDI Child Forms are opened, or even easier, by
defining multiple menus on the parent and then setting the Visibility property of a menu as needed.
NOTE: One would think that setting the MainMenuStrip property would be the way to go, but it is not. Go with Visibility.
This is one of the main reasons why we must add the MenuStrip controls to the form's Controls collection, so the form can
track menus and auto-attach them to its MenuStrip interface when their visibility changes.
Also, current developmental trends favor VB.NET’s more modern approach to MDI menu processing, as opposed
to the original form, used by VB1 through VB6, which was Microsoft’s first venture into fully integrated window
design, developed for Windows 1.0, which provided the first complete, comprehensive windows/menu
environment for the PC (the original interface was licensed from Xerox, not stolen from the Macintosh (who also
licensed from Xerox), as many rumors, and a known frivolous lawsuit had it, filed only to slow Microsoft down).
Under VB.NET, when we add an MDI Parent Form to our project, the open window list is taken care of for us.
Unfortunately, many developers do not want to start with a partially constructed form with basic menu and toolbar
options already declared, nor did they want to have to face the major modifications to it that would be needed to
personalize the interface. They most often need to start with a fresh blank form, finding it easier to simply
construct their own menus and interfaces from scratch. But to many of them, this looked to be impossible because
the open window list stopped working in their custom ventures, even though they may have added a “ &Window”
or “&Windows” main menu item. Many of them, not reading documentation, simply assumed that inaccessible
features would automatically be set behind the green curtain by the MDI Parent Form template, such as some sort
of secret code hidden within the system-provided MDI Parent Form. But, fortunately, it does not.
 Page 298

Worse, if we searched the MSDN Help System for “Create MDI form”, these woes seemed to be abated...
until we realized that the examples are for MainMenu controls, which, by default, do not appear in the
VB.NET Toolbox. Yet, an example for MenuStrip controls, which are in the Toolbox, is not provided.
However, in that MSDN Help, it does tell us that we can create a VB.NET MDI Parent Form by setting a
standard Window Form’s IsMdiContainer property to True. This part works great. It also tells us that we
can create a VB.NET MDI Child Form by simply setting a standard Window Form’s MDIParent
property to an MDI Parent Form at runtime. This part also works great. For example, suppose the
following menu event code was placed within an MDI Parent Form to react when a new form instance,
based upon a form named frmChild, is to be displayed on the MDI Parent Form:
Private m_ChildFormNumber As Integer = 0 'keep track of child index for unique naming purposes

'***********************************************************
'Subroutine : mnuNewChild_Click
'Description: Add a new blank child form based upon frmChild.
'***********************************************************
Public Sub mnuNewChild_Click(ByVal sender As Object, ByVal e As EventArgs) Handles mnuNewChild.Click
Dim ChildForm As New frmChild 'Create a new instance of the child form (presently, the child form is just a normal top-level form).
ChildForm.MdiParent = Me 'Convert the new form into an MDI Child Form of this MDI Parent Form before showing it.
' the above option is only available to runtime code. The new form will now no longer be a top-level form.
m_ChildFormNumber += 1 'Increment our custom child form naming index variable.
ChildForm.Text = "Window" & m_ChildFormNumber.ToString 'Give a default title to the form based upon this index.
' update the child form number naming index.
m_ChildFormNumber = m_ChildFormNumber + 1
' <...Place any additional parent-side setup code here...>
ChildForm.Show() 'Display the child form within its MDI Parent's MDI window. DO NOT also apply a parent as a parameter to Show()!
'NOTE: DO NOT designate an owner form, such as ChildForm.Show(Me), or else an exception error event will be triggered.
End Sub

NOTE: When we set a newly instantiated form’s MdiParent property to the MDI Parent Form (usually Me), the new form is
no longer a top-level window, and therefore we cannot also specify a parent form as a parameter in its Show method, as we
might typically do. If we did do this, an exception error will be triggered because an MDI Parent for the form is already
established, which causes the Child Form to be displayed within (bounded by) the MDI Parent Form’s MDI Window Client
Area, not simply in front of the parent form as the Show(Me) property would specify. The exception event therefore helps us to
avoid such functional ambiguity.
If we add a menu to the form and further add “&Window” or “&Windows” to the main menu, we will
not see a list of open windows displayed under it (this should be a big DUH!). Worse, the Help system
supposedly “saves the day” by telling us that for the menu item that we want the window display list
shown under, being &Window” or “&Windows” in our case, we have to set that menu item’s MdiList
property to True. This was also how it was done under VB6. However, the big problem here is that we
will not find an MdiList property assigned to a MenuStrip. Any similar option also cannot be found.
Truth is, there is an MdiList property available, but it is found only on a MenuItem control, not on a
MenuStrip. However, by default, we cannot add a MainMenu control to a form from the Toolbox
without adding it (see the previous article to learn how – refer to page 290), which supports a MenuItem
and its MdiList property, except programmatically. We can add a MenuStrip from the Toolbox, but it
does not support an MdiList property. Though this seems to be the making of a complex paradox that
could potentially destroy the electromagnetic underpinning of the Universe, there is in fact an easy
solution to both problems.

Adding an Open Window List to a VB.NET MenuStrip Menu.


Adding an open windows list to a MenuStrip-type menu, for as frustrated as
many people get over it, is incredibly easy. Instead of looking to a
ToolStripMenuItem to find a property to display a window list, look instead to
the MenuStrip object. The MenuStrip contains a listed property named
MdiWindowListItem. This property should be set to the menu item that we
want to use to show the list of open MDI Child Forms. For example, on my
MDI Parent Form, my “&Windows” menu item is named WindowsMenu.
Hence, we would select the MenuStrip object and then simply select
WindowsMenu from its MdiWindowListItem dropdown list. This approach
makes much more sense than being able to potentially set an MdiList property
on every single menu item.
 Page 299

Maintaining a Most Recently Used File List.


Adding a Most Recently Used, or MRU file list to a menu, typically to a File menu dropdown list or to a
dropdown menu under the File menu, was often a bothersome pain under VB6. Apart from saving and
loading entries in persistent memory, typically via the Registry SaveSetting and GetSetting methods, there
was always the task of limiting the number of files that could be listed (anywhere from 4 to 9 recent files is
traditional), adding the most recent file to the topmost entry of the list, which involved deleting the entry
from the list if it is already there, shifting other entries down in the list, inserting the current entry to the top
of that list, and then to finally save the updated list to the registry. And there is also the user selecting an
entry from the MRU list, ensuring that this selected file still exists, deleting it from the list if it does not, and
shifting it up to the top of the MRU list as the most recently opened item if it did already exist in the list, and
again shifting any others down, and saving that result to the registry.
Under VB6, some developers resorted to adding the maximum number of MRU entries allowed, and
then render them invisible until they were needed. Actually, this was an excellent idea. Others added
entries dynamically, using an indexed menu entry, adding and removing entries via the Load and Unload
commands, but this required sometimes lots of additional code overhead. My own approach for VB6
menus had been to borrow from both methods: to use pre-declared dynamically created index entries,
but creating the maximum count all at once when the form loads, and rendering those that are not being
used invisible. This technique is fast and also makes offset indexing extremely easy.
Some prefer the VB6 approach, which cannot work on an object-oriented platform because it requires a
menu property to be owned by more than one object, which is illegal in an OOPL. However, I counter
that the VB.NET method is both easier and faster, which is why I model my VB6 menus after it. I would
show you the code for my VB6 version, but that would be just an academic exercise with several pages
that we no longer need. Suffice to say it follows my VB.NET method as tightly as possible, though it is
still a lot bulkier and also requires we duplicate a lot of code between forms. With VB.NET, this code
duplication is eliminated, and its faster, more streamlined code exists only within the MDI Parent form.
In my MDI Main form, which I always name mdiMain, I also always add a
menu separator somewhere below my File menu and name it mnuMRUSep.
With VB6, below that I must also add one menu entry named mnuMRU that has
its Index property set to 0, as shown to the right, and in the VB6 MDI form’s
Load event, I must also build a list of duplicate slots, all this being code no
longer required when using VB.NET. I next retrieve the application MRU list
from the registry and populate it to all pertinent menu entries. Later, when I try
to add an item to the MRU list, I check to see if it already exists within the list. If it does, I shift any items
higher than it down, ensuring that the new file, or the currently listed one, is at the top of that list. If it is
already at the top of the list, it is of course left alone. Finally, when the user selects a file from the list, if it is
not already at the top of the list, that entry must be moved to the top, which means the list must afterward be
saved, and these results must also be propagated to all child forms so they display identical lists. We must
always remember to save changes to the MRU list whenever we apply any alteration to the MRU list.
All this can be a lot of typing, regardless of how efficient and feature-rich it is. To minimize all this
duplicate code between applications, I consistently give all MRU entities the same names. Also, I
always name my MDI Parent Form mdiMain, and my MDI Child Form as mdiChild, and save these
forms and their code off as form templates. With that, all I have to do is simply import these forms as
local copies from my templates to significantly accelerate development for MRU-consuming MDI
applications. This can also be very quickly adapted to Single Document Interfaces as well.
The VB6 version of the code, which I will not bore you to the point of pondering suicide with, as
monumental as it was to provide a professional appearance to a VB6 application, still does not address
the issues of setting up the look and feel of the forms or supporting the application's actual purpose. That
is all still left to do, to put our personal stamp on our program. This is not to mention code to process or
open a selected file entry and opening a new child form on the mdiMain form.
 Page 300

Supporting MRU File lists under VB.NET is much easier, but the code needed to deliver a rich and
professional appearance, rather than a it-just-gets-by look and feel, might appear as daunting as code for
VB6. Indeed, it might seem to some as being almost as complicated as any VB6 code, but we are still
eliminating many pages of additional code that VB6 would require, which would tempt me to show you.
Unlike VB6, we are able to insert and remove MRU file entries from the File menu with almost no code.
Because of this single OOP enhancement, we can eliminate a lot of code that VB6 required to shift the
file entries around in a menu list. For example, to move a menu entry from one location to the top, all we
have to do in VB.NET is to remove the menu item object from its current location and reinsert it at the
beginning of the list. We do not even have to reconstruct the entry in the new location because of the fact
that VB.NET is fully object-oriented, which makes this process so much easier. We can simply use a
MenuItem reference variable to point to the object to move. We then remove the reference from the list
and reinsert it at the start of the list. Because we are using instantiated objects, as long as a reference to
an object exists, via a local reference variable in this case, then the object will not go out of scope or be
deconstructed by the Garbage Collector when a reference to the MenuItem object is removed from the
menu list. As long as a reference to the actual object exists, either through our temporary reference
variable or through a reference to it in the menu list, the object will stay in scope and not be destroyed.
NOTE: This is probably one of the most difficult concepts to grok by people new to object-oriented programming – to have a
strong understanding of objects and references, and realizing that an object reference variable is not the object itself, but is
just a reference to the actual object. Hence, when we set an object reference variable to Nothing, we are not erasing the
object itself, but only removing a reference to it. The Garbage Collector walks the program Stack, to find local, temporary
variables, and the Heap, for static/shared/global variables/fields, and determines which objects are no longer being
referenced by anything, and eliminates all such objects no longer in use. This is why it can be very important to know if an
object implements the IDisposable interface and thus features a Dispose method, which is a VB.NET attribute that will
immediately remove the object’s actual resources, not waiting for the Garbage Collector to do it.
NOTE: Although VB6 users may claim that setting an object reference to Nothing would immediately remove the object
from memory, thus declaring some sort of superiority over VB.NET, but do not get me started on that one – and this is
coming from one of VB6’s biggest advocates. This is not quite true. It may have removed data resources, but it did not truly
eliminate the object, because as long as a reference to it still exists (even through the reference variable that we just set to
Nothing no longer references it), we could still manipulate the object, thus instantly re-instantiating its data, which was
actually the only thing removed, most often by accident, which caused uncounted major headaches in the VB6 world,
because some objects just refused to die, being especially aggravating when the closing of an application depended on that
object not existing. Under VB6, if we even tried to test for an object being released, the slightest reference checking on it
immediately resurrected it. This was all due to a process loosely called Lazy Instantiation. Take a VB6 variable assigned an
object reference “As New”. A key “feature” of this variable is that after setting it to Nothing, even if we only tested it for
being Nothing, the object would be instantly resurrected. VB.NET just makes this disposal process more apparent (setting a
reference variable to Nothing should actually have simply done just that all along – just release the reference of a variable to
an object), but at the same time it also introduced features, such as the Dispose method, that eliminated all that frustration,
preventing unwanted accidental object reinstantiation. This is the same process VB6 should have adopted.
NOTE: Be aware that if we implement the IDisposable interface into our own classes, we should design the Dispose method
to actually perform its task of releasing resources only once, even though we should enable the method to be invoked more
than one time, because references to an object can be held by more than one reference pointer (and the source of much VB6
frustration), which each might also need to invoke its Dispose method. By also adding a Boolean IsDisposed flag to the
object, set to an initial False value, our custom Dispose method can first check this flag to see if it should actually do
anything. If it finds that the flag is False, it should go ahead and dispose of any resources the object had created. Once this
task has completed, it then sets the IsDisposed flag to True. Also, if it is crucial that these unmanaged resources be released,
as otherwise a memory leak can result from not deleting them, then we should also invoke the Dispose method from the
Finalize method, which is a method that the Garbage Collector will invoke when it attempts to deconstruct an object that it
encounters that no longer has any references to it. Note that VB.NET now pre-codes an IsDisposed variable.
Notice that because the MDI Parent maintains all menus, if we do need to propagate the file lists to other
menus, we can do so right within the MDI Parent. Also notice that in VB.NET there is no longer an
AutoShowChildren property, which everyone always set to False anyway, because not doing so was a
big useless pain, unless we had the rare ready-to-run child forms.
 Page 301

Other than that, there is little other difference between my VB6 and VB.NET versions, except for maybe
language refinements, such as “Fcnt += 1” instead of “Fcnt = Fcnt + 1”, for example, plus the code
juggling. Indeed, my VB6 template was derived from the code I designed for my VB.NET projects. File
lists will always be a lot of work, but we can eliminate almost all of that work by using standardized
object naming practices and designing reusable code that will take full advantage of those practices.
As indicated above, the main departure from the VB6 version is the
manner in which file entries are registered on the main menu’s File
dropdown list. Like the VB6 model, somewhere on the File menu
dropdown I will place a separator name mnuMRUSep. But unlike the VB6
model, that is all I need do. I do not have to add any File entry
placeholders in the list ahead of time, such as a mnuMRU control, as I had
to do for the VB6 model. All we need is the separator named mnuMRUSep.
The following MDI Parent Form code assumes our form is named mdiMain and
we have a menu separator named mnuMRUSep on a File menu named mnuFile.
This code is designed to be reusable, so we can plug it into any MDI project. We
can even save the form as a VB.NET Form Template, so it becomes a built-in feature. Also make an SDI version.
Public Class mdiMain

Private MaxFileListCnt As Integer = 9 'maximum number of files for list under File menu.
Private ShowFullPath As Boolean = False 'flag indicating if a full path should be shown in the MRU list.

'*************************************************************************
' MDIForm_Load: Set up instal state of form.
' Initialize MRU list
'*************************************************************************
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
' Get the user-selected maximum number of file entries (default is 9, but the user can select fewer).
MaxFileListCnt = CInt(GetSetting(My.Application.Info.Title, "MRUFiles", "MaxCount", CStr(MaxFileListCnt)))
' erase file list separator.
Me.mnuMRUSep.Visible = False
' Get a user-selected flag indicating if full paths or just the filename should be displayed in MRU list.
ShowFullPath = CBool(GetSetting(My.Application.Info.Title, "MRUFiles", "ShowFullPath", "0")) 'False (0) indicates filename-only.
' Now load the MRU list to the File menu.
GetMRUFiles()
End Sub

'*************************************************************************
' GetMRUFiles(): Get MRU File list. Assume a separator line named mnuMRUSep.
' precedes these entries, and the entries are a menu array
' named mnuMRU, with an initial index entry set to zero.
'*************************************************************************
Public Sub GetMRUFiles()
Dim Ttl As String = My.Application.Info.Title 'get application title.
Dim MaxCnt As Integer = CInt(GetSetting(Ttl, "Settings", "FileCnt", CStr(0))) 'number of entries.

If CBool(MaxCnt) Then 'anything?


Dim LoadingIndex As Integer = 0 'init loading index.
Dim mnuMRUSepIdx As Integer = Me.mnuFile.DropDownItems.IndexOfKey("mnuMRUSep") 'get index to MRU separator under mnuFile.
For Idx As Integer = 0 To MaxCnt - 1 'yes, so add to File menu.
Dim Path As String = GetSetting(Ttl, "Settings", "File" & CStr(Idx), vbNullString) 'get a saved file entry.
If Path <> vbNullString AndAlso FileIO.FileSystem.FileExists(Path) Then 'does file exists.
AddMRUFile(Path) 'yes, so add it to the file list.
LoadingIndex += 1 'bump actual offset index.
End If
Next Idx
If LoadingIndex <> MaxCnt Then 'if max menu count is different from old, then save the new list.
SaveSetting(Ttl, "Settings", "FileCnt", CStr(LoadingIndex)) 'save new count.
SaveMRUFiles() 'save new list.
End If
End If
End Sub

'*******************************************************************************
' Subroutine Name : SaveMRUFiles
' Purpose : Save MRU File List.
'*******************************************************************************
Private Sub SaveMRUFiles()
Dim Ttl As String = My.Application.Info.Title 'get application title.
Dim Cnt As Integer = CInt(GetSetting(Ttl, "Settings", "FileCnt", "0")) 'get number of file entries.

If Cnt > MaxFileListCnt Then 'do not exceed max file list count.
Cnt = MaxFileListCnt
SaveSetting(Ttl, "Settings", "FileCnt", CStr(Cnt)) 'save update.
End If
With Me.mnuFile.DropDownItems
Dim mnuMRUSepIdx As Integer = .IndexOfKey(mnuMRUSep.Name) 'get index of list separator.
For Idx As Integer = 1 To Cnt 'process each menu item, for count.
.Item(mnuMRUSepIdx + Idx).Name = "mnuMRU" & CStr(Idx - 1) 'update search key.
SaveSetting(Ttl, "Settings", "File" & CStr(Idx - 1), .Item(mnuMRUSepIdx + Idx).Tag.ToString) 'save new entry.
Next
End With
PropagateMRUs() 'propagate MRU list to other possible menus.
 Page 302

End Sub

'*************************************************************************
' AddMRUFile(): Add a file to the MRU list in the menus.
'*************************************************************************
Private Function AddMRUFile(ByVal Path As String) As Boolean 'return true if added ok.
Dim Ttl As String = My.Application.Info.Title 'get application title.
Dim mnuMRUSepIdx As Integer = mnuFile.DropDownItems.IndexOfKey("mnuMRUSep") 'get index of mnuFile separator.

Dim Fname As String = Path 'get full path to file.


If Not ShowFullPath Then 'not displaying full path?
Fname = Mid(Path, InStrRev(Path, "\") + 1) 'no, so get just the filename.
End If

Dim Idx As Integer = mnuMRUSepIdx 'init base of search – 1.


Dim FoundIt As Boolean = False 'flag indicating if an item was found.
Do
Idx += 1 'bump index offset.
If Idx > Me.mnuFile.DropDownItems.Count OrElse
Me.mnuFile.DropDownItems(Idx).GetType.Name <> "ToolStripMenuItem" OrElse
Me.mnuFile.DropDownItems(Idx).Tag Is Nothing Then Exit Do 'avoid exception errors.
FoundIt = StrComp(Me.mnuFile.DropDownItems(Idx).Tag.ToString, Path, CompareMethod.Text) = 0
If FoundIt Then Exit Do 'found a match
Loop

'If a match is found at the very top (offset index 1), then there is nothing to do, so just return the number of items in list.
If FoundIt Then
If Idx - mnuMRUSepIdx > 1 Then 'if not the first entry...
'get menu item to move.
Dim mnuItem As ToolStripMenuItem = DirectCast(Me.mnuFile.DropDownItems(Idx), ToolStripMenuItem)
'remove the entry from the file list.
Me.mnuFile.DropDownItems.RemoveAt(Me.mnuFile.DropDownItems.IndexOfKey(mnuItem.Name))
'reinsert reference after the MRU separator.
Me.mnuFile.DropDownItems.Insert(mnuMRUSepIdx + 1, mnuItem)
End If
Else
'entry not found, so insert this new entry into the MRU list immediately after the MRU separator.
Dim mnuItem As New ToolStripMenuItem(Path, Nothing, New EventHandler(AddressOf mnuFileList_Click))
Idx -= mnuMRUSepIdx 'drop menu offset count.
mnuItem.Name = "mnuFile" & CStr(Idx - 1) 'add a unique search key to it.
mnuItem.Text = Fname 'add a name to the entry.
mnuItem.Tag = Path 'save the path.
Me.mnuFile.DropDownItems.Insert(mnuMRUSepIdx + 1, mnuItem) 'insert at beginning of list.
If Idx > MaxFileListCnt Then 'if we exceeded the limits (it would be by 1 if so).
Me.mnuFile.DropDownItems.RemoveAt(mnuMRUSepIdx + Idx) 'remove current last entry in MRU list.
Idx = MaxFileListCnt 'set the counter to within bounds (Idx – 1).
End If
SaveSetting(Ttl, "Settings", "FileCnt", CStr(Idx)) 'save new file list count.
SaveMRUFiles() 'then update update list.
End If
Me.mnuFile.DropDownItems(mnuMRUSepIdx).Visible = True 'ensure separator is visible.
Return Not FoundIt 'True if an entry was actually added, False if there were no need to add a new entry.
End Function

'*******************************************************************************
' Subroutine Name : mnuFileList_Click
' Purpose : Select a previously opened itext file.
'*******************************************************************************
Public Sub mnuFileList_Click(ByVal sender As Object, ByVal e As EventArgs)
Dim Ttl As String = My.Application.Info.Title 'get application title.
Dim mnuItem As ToolStripMenuItem = DirectCast(sender, ToolStripMenuItem) 'get reference to menu item selected.
Dim Path As String = mnuItem.Tag.ToString 'save the display data.

'See if the file is already loaded. If so, then simply set focus to it.
For Each frm As Form In Me.MdiChildren
If frm.Tag.ToString = Path Then 'if paths match, then file is loaded.
frm.Focus() 'so set focus to it.
Exit Sub 'and we are all done.
End If
Next frm

'try loading it, and process the result


If LoadFile(Path) Then 'if we can open it...
Me.mnuFile.DropDownItems.RemoveAt(Me.mnuFile.DropDownItems.IndexOfKey(mnuItem.Name)) 'remove its entry from the file list.
Dim mnuMRUSepIdx As Integer = Me.mnuFile.DropDownItems.IndexOfKey(Me.mnuMRUSep.Name) 'point to MRU separator.
Me.mnuFile.DropDownItems.Insert(mnuMRUSepIdx + 1, mnuItem) 'reinsert after the MRU separator.
Else
Dim Idx As Integer = CInt(GetSetting(Ttl, "Settings", "FileCnt", "0")) 'else get number of file entries.
SaveSetting(Ttl, "Settings", "FileCnt", CStr(Idx - 1)) 'make it 1 less.
Me.mnuFile.DropDownItems.RemoveAt(Me.mnuFile.DropDownItems.IndexOfKey(mnuItem.Name)) 'remove the entry from the file list.
End If
SaveMRUFiles() 'now update the file list regardless.
End Sub

'*******************************************************************************
' Subroutine Name : PropagateMRUs (custom method for developer use)
' Purpose : propagate MRU list to other possible menus that feature a file MRU list.
'*******************************************************************************
Public Sub PropagateMRUs()
'if you have additional menu lists that will need to update their own menu lists, you should do it here.
End Sub
End Class
 Page 303

The above block of code requires no complementary code to be placed in the MDI Child Form as we
would have to do for a VB6 version, except for, as we can see in the code immediately above, the
PropagateMRUs method may require custom expansion by our code to propagate data to any other place that
requires knowledge of that list, if any. However, propogating data to other menus can be taken care of
exclusivelywithin the MDI Parent Form.
NOTE: The idea of maintaining more than one menu on a form might sound complicated, but it is really quite easy to do. We
simply place as many menus on our mdiMain form as we need, construct them each as individual menus, and then set the
Visibility property for any menu we will not be using at any one time to False, keeping only the relevant one set to True.
NOTE: Although it would seem to make more sense to set the active menu to the MDI Form’s MainMenuStrip property (and
being sure that the menu is stored in the Controls collection of the form, which it will not be if we build them in-code), this is
not the case. This has the advantage of allowing for more than one menu to be present on a form, and all being viable. As
such, set their Visibility properties as needed to be set to True or False, to display or hide relevant main menus.
Under VB6, I tend to use the File System Object interface for my File input/output. This is because it is
tremendously faster than the basic VB6 file I/O functions. With the VB.NET version, I prefer to use
Filestreams, which are built right into .NET and are really easy to use. But the best part is, they are
profoundly faster than the already fast FSO interface, which by comparison makes FSO seem slow.
Like with a VB6 version, the above code will invoke a Boolean method name LoadFile. This method will
perform the file I/O to process the selection from the file menu. Consider the following version, which will
assume that the child form, named mdiChild, is designed to load text files to a TextBox control:
'*******************************************************************************
' Subroutine Name : LoadFile
' Purpose : Load a text file into a new MDI Child Form and display it.
'*******************************************************************************
Private Function LoadFile(ByVal Path As String) As Boolean
Static FileIndex As Integer = 0 'static refernce used for unique child form names.

If Not FileIO.FileSystem.FileExists(Path) Then 'if the file does not exist...


Return False 'report failure.
End If

Me.Cursor = Cursors.WaitCursor 'show that we are busy.


Dim frm As New mdiChild 'otherwise instantiate a new Child Form.
frm.MdiParent = Me 'make the form an MDI Child of this form.
frm.Tag = Path 'save the full path to the file.
FileIndex += 1 'bump the static file naming index.
frm.Text = "Window" & CStr(FileIndex) 'apply a new title to the Child Form.
frm.Show() 'display our child form.

Dim TS As New System.IO.StreamReader(Path) 'open the file.


With frm.TextBox1
.Text = TS.ReadToEnd 'read its contents into a textbox.
.SelectionStart = 0
.SelectionLength = 0
End With
TS.Close() 'close the file.
TS.Dispose() 'dispose of resources.
Me.Cursor = Cursors.Default
Return True 'report success.
End Function

NOTE: By the way, if we are using a TextBox control to load files into, I have noticed that setting its WordWrap
property to On when we are loading really large files can significantly slow things down whenever the TextBox must
resize. By instead setting it to Off and also setting its ScrollBars property to Both, meaning that it will have both
horizontal and vertical scroll bars, this will improve our program’s processing speed to a considerable degree.
NOTE: Also notice the four highlighted lines in the above program listing. First, where we are reading the whole text
file into the TextBox, we could have alternatively opened our StreamReader using the more traditional command “Dim
TS As System.IO.StreamReader = FileIO.FileSystem.OpenTextFileReader(Path) ”. Second, if we are in fact going to read
the entire text file in one go, we can delete the declaration of the TS StreamReader altogether, along with the
“TS.Close” and “TS.Dispose” commands. After doing that, we can simply replace “ .Text = TS.ReadToEnd” with “.Text =
FileIO.FileSystem.ReadAllText(Path) ”, and we will have accomplished the very same thing, all in one line of code, and
without instantiating an object that we afterward need to close or dispose of, because these tasks will be automatically
taken care of for us..
 Page 304

Lastly, as with a VB6 version, to complete this example code for the MDI Parent Form, the following
reacts to an “Open...” menu entry, so that the user can specify a text file to load:
'*******************************************************************************
' Subroutine Name : mnuFileOpen_Click
' Purpose : let the user select a file to open.
'*******************************************************************************
Private Sub mnuFileOpen_Click(ByVal sender As Object, ByVal e As EventArgs) Handles mnuFileOpen.Click
With New OpenFileDialog
.Title = "Open API Text File" 'title for dialog box.
.CheckPathExists = True 'ensure path exists.
.CheckFileExists = True 'ensure the file exists.
.ValidateNames = True 'only valid Win32 filenames.
.DefaultExt = ".txt" 'set default extension.
.Filter = "Text Files|*.txt" 'define filter and flags.
.FileName = "*.txt" 'just for show, to highlight that only text files are allowed.
If .ShowDialog() = DialogResult.Cancel Then Exit Sub 'if the user hit the Cancel button on the dialog
.
'Otherwise, create a new child window
If LoadFile(.FileName) Then 'if the file loaded... (invoke user routine to load file to a new child window).
AddMRUFile(.FileName) 'add it to the MRU list if the file loaded OK.
End If
End With
End Sub
 Page 305

Flicker-Free Home-Brew Form Resizing (an E-Z Intro to VB.NET SubClassing).


Although VB.NET has solved the whole VB6 issue of maintaining a form’s minimal/maximal size by implementing the simple to use MinimumSize and
MaximumSize Form properties, where through either we can set a Width and/or Height sub-property to define a form’s minimum size and/or
maximum size, I am maintaining this article, first written for VB6 and now upgraded for VB.NET, because it clearly highlights the differences between VB6
and VB.NET subclassing, which are applicable as a template to a colossal range of other similar uses of subclassing.

If we need a simple solution to maintain a minimum size to a sizable VB form, traditionally we placed code
in the form’s Resize event to ensure that it had not been sized below specified minimum dimensions, except
of course when the user might have minimized the form to the taskbar. The quick and dirty solution was to
check for the form’s width going below a predetermined minimum size. If it did, it would set the form’s
width back to the predetermined minimum size. We would do likewise for the form’s height. We could of
course write code for this in a flash. The core of such code generally looked something like the following:
If Me.Width < 300 Then 'below minimum width size in pixels?
Me.Width = 300 'set to minimum width if so.
End If
If Me.Height < 500 Then 'below minimum height size in pixels?
Me.Height = 500 'set to minimum height if so.
End If

Of course, we really should not add “magic numbers” to our programs, such as the above form boundary
dimensions 300 and 500, except maybe during early developmental testing. They are referred to as magic
numbers because, when we modify and expand our code later on, these numbers may end up doing
seemingly magical things to our application, due to our maybe having imposed different sizing limits in more
visible code regions elsewhere.
Under most circumstances we should declare them as constants. Hence, we should replace all instances of
such hard-coded values with labels, such as MyFormWd and MyFormHt. We should also declare these constants in
an easy-to-find location, such as the heading of our application, much like the following two constants:
Private Const MyFormWd As Integer = 300 'minimum form width in pixels.
Private Const MyFormHt As Integer = 500 'minimum form height in pixels.

An alternative to constants, which usually requires frequent revision as a project develops, is to instead
declare them as variables, assigning them the startup size of our form at the end of the Load event, like this:
Private MyFormWd As Integer 'stores minimum form width in pixels (startup width).
Private MyFormHt As Integer 'stores minimum form height in pixels (startup height).

'***********************************************
' myForm_Load
'***********************************************
Private Sub myForm_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
' place other initialization code here

' At the end of the Load event, save the current form size to establish its minimum size.
MyFormWd = Me.Width 'save form's initial display width.
MyFormHt = Me.Height 'save form's initial display height.
End Sub

Either way, the previous primitive form size limit processing code can be updated to the following:
If Me.Width < MyFormWd Then 'below minimum width size in pixels?
Me.Width = MyFormWd 'set to minimum width if so.
End If
If Me.Height < MyFormHt Then 'below minimum height size in pixels?
Me.Height = MyFormHt 'set to minimum height if so.
End If

But if we implement such a primitive solution, we get an unprofessional result: If the user sizes the form
larger than the minimums, then all seems well. However, if they size the form below either of the
minimums, the form begins to bounce like a deranged Tasmanian Devil with hiccups as the form’s
Resize event continuously tries to re-inflate its dimensions to the minimums, until the user finally
abandons the resizing attempt.
 Page 306

The Resize event would contain code similar to the following, which is also the source of the flickering:
'***********************************************
' myForm_Resize
' React to the form dimensions changing.
'***********************************************
Private Sub myForm_Resize(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Resize
Select Case Me.WindowState
Case FormWindowState.Normal 'if the form is in a sizable state...
If Me.Width < MyFormWd Then 'if the form is below the minimum width size in pixels?
Me.Width = MyFormWd 'set to minimum width if so (this also spawns another Resize event).
End If
If Me.Height < MyFormHt Then 'is the form below the minimum height size in pixels?
Me.Height = MyFormHt 'set to minimum height if so (this also spawns another Resize event).
End If
End Select
' other control arrangement code goes here.
End Sub

From code such as this, the flicker is made all the worse by the fact that additional invocations to the
Resize event are made, not only because we had invoked it by resizing the form below the minimum in
one direction, but potentially by both. Further, while a single Resize event is still being processed, we can
end up re-invoking the event up to two more times! For example, suppose the user had sized the form to
where its dimensions are below both the minimum width and height ranges. When Me.Width is checked
for being below the minimum width (If Me.Width < MyFormWd) and it is found to be below MyFormWd, then the
form width is reset (Me.Width = MyFormWd). This act forces another invocation of the Resize event for the
simple reason that the form has been resized again, but at this point we have not even got to the form’s
height test, which will do the same thing. This is on top of the fact that while the user continues to hold
the mouse pick button down while trying to resize the form smaller than the minimum allowed in either
direction, the form will be constantly forced to internally resize to the user-selected dimensions, thus
repeatedly imposing a cascade of Resize events until the user at last restores sanity by releasing the
mouse button.

The Minimalist Solution for Form Resizing Flicker.


The simplest solution to the form flicker problem is for us to add a Timer control to our form, naming it
something like tmrResize. Set its interval to 250 (250 milliseconds, or one quarter of a second) and
ensure that it is initially disabled (by default it should already be disabled under VB.NET). Next, change
the above Resize event code to the following:
'***********************************************
' myForm_Resize
' React to the form dimensions changing.
'***********************************************
Private Sub myForm_Resize(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Resize
Select Case Me.WindowState
Case FormWindowState.Minimized 'if the form is minimized, then do nothing.
Exit Sub
Case FormWindowState.Normal 'if the form is in a sizable state...
' if the user sizes the form below desired dimensions...
If Me.Width < MyFormWd OrElse Me.Height < MyFormHt Then
With Me.tmrResize 'smooth w/timer.
.Enabled = False 'turn timer off.
Application.DoEvents() 'screen catch up.
.Enabled = True 'restart timer (this resets its timing interval).
End With
Exit Sub 'let timer event code perform the actual resizing work.
End If
End Select
' other control arrangement code goes here.
End Sub

We first disable the timer, in case it was previously enabled by our Resize event. We then let the display
update with a DoEvents. We finally re-enable (hence, reset) the timer. For now, we will ignore other form
size processing while the form is too small with an Exit Sub instruction. Notice also that we have
removed the size correction code. We will be moving that to the Timer’s Tick event.
 Page 307

Next, add the following Tick event code for the tmrResize control:
'***********************************************
' tmrResize_Tick
' Check form for being too small.
'***********************************************
Private Sub tmrResize_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrResize.Tick
' turn the timer off
Me.tmrResize.Enabled = False

' if the form is minimized, then do nothing.


If Me.WindowState = FormWindowState.Minimized Then Exit Sub

' resize to minimum dims, as needed.


If Me.Width < MyFormWd Then 'is the form below the minimum width size?
Me.Width = MyFormWd 'set to minimum width if so.
End If
If Me.Height < MyFormHt Then 'is the form below the minimum height size?
Me.Height = MyFormHt 'set to minimum height if so.
End If
End Sub

And that is all there is to the minimalist solution. We now have a smoother resizing form. As long as the
cursor is moving during a user resize, the quarter-second timer will not trigger its event, due to us
resetting the timer every time the resize even detects that the form is less than minimum.
Adding a Little Pizzazz to the Minimalist Solution.
If we want to add a little pizzazz to this, such as not forcing proper control refits until after the mouse
pick button is actually released by the user, all we have to do is make a minor modification to the above
code. To do that, we will need to add a single Pinvoke to our program that will make the transition
appear to be even smoother. The GetKeyState Pinvoke can be used to quickly check to see if the mouse
pick button is pressed or not.
NOTE: The so-called Mouse Pick Button is usually the Left Mouse Button, but this is deceiving. On a mouse oriented for
left-handedness, the Right Mouse Button is internally treated as the Left Mouse Button at the point that our Pinvoke is used
to look at it. Hence, our Pinvoke code will work for both of these cases, for a left-handed or right-handed mouse
configuration, without change.
We can declare the new Pinvoke at the top of our form, like this:
Private Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Integer) As Short
Private Const VK_LBUTTON As Integer = &H1 'Left button virtual key (actually, this is exactly the same as using Keys.LButton instead).

To take advantage of it, insert the following line as the first line within our tmrResize_Tick event code:
If GetKeyState(VK_LBUTTON) < 0S Then Exit Sub 'there is a letter 'S' after zero to force a Short Integer value for testing.

With just this miniscule refinement, form resize handling suddenly appears to be a whole lot more
professional-looking. Now, even if we pause moving the mouse while we are resizing the form to be too
small, the form will not automatically spring out to its minimally defined sized after a quarter of a
second, as long as the mouse pick button is held down.

Adding More Pizzazz to Build a More Stable Solution.


Another enhancement can jazz this up by making it more stable than with just the above refinement.
This next enhancement needs to have a Boolean flag declared at the top of the form, named something
like bResize, which will inform the Resize event when the timer event alters the form. Following are
our updated Tick and Resize event methods:
Private MyFormWd As Integer 'store minimum form width in pixels (startup width).
Private MyFormHt As Integer 'store minimum form height in pixels (startup height).

Private Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Integer) As Short
Private Const VK_LBUTTON As Integer = &H1 'Left button virtual key (you can also use Keys.LButton).

' the following flag will be set to True when resizing is being processed within the Timer Tick event.
Private bResize As Boolean = False

'***********************************************
' myForm_Load
'***********************************************
Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
' place other initialization code here.
'
' At the end of the Load event, save the current form size to establish its minimum size.
MyFormWd = Me.Width 'save form's startup width.
MyFormHt = Me.Height 'save form's startup height.
End Sub
 Page 308

'***********************************************
' Form_Resize
' React to the form dimensions changing.
'***********************************************
Private Sub Form_Resize(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Resize
If bResize Then Exit Sub 'if the timer is resizing the form, then ignore handling the resize event.
Select Case Me.WindowState
Case FormWindowState.Minimized 'if the form is minimized, then do nothing.
Exit Sub
Case FormWindowState.Normal 'if the form is in a sizable state...
' if the left mouse button is down, or if the user sizes the form below desired dimensions...
If GetKeyState(VK_LBUTTON) < 0S OrElse Me.Width < MyFormWd OrElse Me.Height < MyFormHt Then
With Me.tmrResize
.Enabled = False 'disable timer.
Application.DoEvents() 'let screen catch up.
.Enabled = True 're-enable timer.
End With
Exit Sub
End If
End Select 'Me.WindowState
' other control arrangement code goes here.
End Sub

'***********************************************
' tmrResize_Tick
' Check form for being too small.
'***********************************************
Private Sub tmrResize_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrResize.Tick

' exit this Tick cycle if the mouse pick button is still being held down.
If GetKeyState(VK_LBUTTON) < 0S Then Exit Sub

' turn the timer off.


Me.tmrResize.Enabled = False

' if the form is minimized, then do nothing.


If Me.WindowState = FormWindowState.Minimized Then Exit Sub

bResize = True 'block the resize event that will occur if we resize the form below.

' resize to minimum dims, as needed.


If Me.Width < MyFormWd Then 'is the form below the minimum width size?
Me.Width = MyFormWd 'set to minimum width if so.
End If
If Me.Height < MyFormHt Then 'is the form below the minimum height size?
Me.Height = MyFormHt 'set to minimum height if so.
End If

bResize = False 'safe now, so unblock the resize event from being processed.

Form_Resize(Me, New EventArgs()) 'now force processing the form resize event (processes OTHER than OUR resize processing).
'We invoke it here, even if we did not actually resize it here, because a Resize event had
'instigated this timer process, and other Resize event processes have not yet been handled.
End Sub

Anything beyond these enhancements is normally outside Beginner or Intermediate levels, which can
involve pixel math, implementing advanced Pinvoke invocations, system structures, copying memory,
and other topics. I think it is not as complicated as one might suppose, except that there we will always
need to remember to save any modified code before running it. But having said that...

Adding Alotta Pizzazz: Professional Results by Intercepting the Windows Message Queue.
Someone suggested that I demonstrate subclassing the form and monitoring the Windows Message
Queue for a WM_GETMINMAXINFO message being sent to the form. If the WM_GETMINMAXINFO message is found,
then this also means that an lParm parameter will, though only for this message, be pointing to a system-
resident MINMAXINFO structure. We will copy this data to a local duplicate structure, modify its contents,
and then copy the memory back. Because we are handling this message for the form, we will tag the
message as handled by setting the function’s return value to zero. However, this process may be more
complicated than I just described. But in other respects, especially with VB.NET, it is even easier.
NOTE: A message delivered through the message queue is actually just a simple integer whose value represents a
predetermined flag to the operating system. But note that the system can also deliver a message that is accompanied by
additional data, which is why it is always accompanied by the additional parameters wParam and lParam, even if not used).
Indeed, by implementing a technique such as the above, we can eliminate all the code we had previously
demonstrated in this article and in the process deliver a stunning result that is truly impressive, far
beyond what we have previously demonstrated, and its final look is truly professional-grade.
 Page 309

Subclassing Under VB6 (an exercise in patience).


Before getting into the VB.NET code, let us first review some VB6 code and history on subclassing, just
to demonstrate how superior the VB.NET approach to subclassing is...
Under VB6, subclassing was a valuable technique that enabled us to intercept Windows messages being
sent to a form or control. By intercepting these messages, we could write our own code to change or
extend the behavior of that form or control. The VB6 AddressOf keyword was essential to enabling us to
reroute Windows messages to a custom-written message processing function. Subclassing using
AddressOf under VB6 was very efficient, but it also made debugging a VB6 project extremely difficult.
If the form being subclassed received a message while VB6 was in Debug Pause, VB6 would typically
crash, hang, or in extreme cases, it would either require a system reboot to recover, or worse, it would
automatically perform that reboot for us, even if we did not want it to. But it all ended with us losing any
unsaved work we might have done.
Under VB6, there was an additional inconvenience that we had to work around, and that was that we
could not specify a target of the AddressOf operator that was within the body of a form. If we tried, we
would get an “Invalid use of AddressOf operator” error. The solution to this problem was very simple:
just place our interface code in a Module. AddressOf targets work well from within modules, because a
VB6 CallBack was not able to support instance objects, such as forms, like VB.NET is able to do.
VB6 subclassing also involved hooking a message processing method into the Windows Message
Queue, and when finished, unhooking it. The method must be declared using a format prototype to
support a Windows Processor, or WndProc for short. So we would declare our WndProc using a format
similar to the following declaration, noting that the VB6 Long integers are type Integer under VB.NET:
'***********************************************
' VB6 WindProc Handler (Long integers are 32-bit)
'***********************************************
Private Function WndProc(ByVal hWnd As Long,  Windows Handle identifying the form or control the current message is for.
ByVal uMsg As Long,  The Message ID.
ByVal wParam As Long,  if used, it usually provides a 32-bit value associated with the message ID.
ByVal lParam As Long) As Long  if used, it is usually a 32-bit pointer to data associated with the Message ID.

Using this header, we could declare a function that can process several window objects, because each
would be identified by a Windows Handle that could be checked against the hWnd parameter value. Such
multi-object handler processing is especially applicable to procedural language environments like VB6,
but typically not in object-oriented environments like VB.NET, as we will see later. Once the actual
object (our form, usually) is identified, we can further check the actual message value provided by the
uMsg parameter. If it matches a sought message ID for the matched form or control, we will process it.
NOTE: Most VB6 WndProc methods implement a Select block to wrap the hWnd tests, and an inner Select block under each
hWnd match to check for individual uMsg matches that were specific to each individual form or control.

If we provide the service for that message, we must return a value of zero that will duly inform the
system that we have handled the message, and so there is no need for further processing of it. If we do
not handle it, or if the message was not one that we were looking for, or if none of our window handles
match hWnd, then we must pass those messages on down through the message stream.
We pass messages on by using the previous hook (the hook we replaced when we hooked ourselves in)
and funnel control onward through the message queue via the CallWindowProc Pinvoke, which is
designed to pass the hWnd, uMsg, wParam, and lParam parameters to a specified address (the old hook) in a
format compatible to the WndProc prototype. We had gathered that previous hook when we had hooked
our own WndProc method into the queue through the SendMessageLong Pinvoke, which returned that
previous hook as a result, which we must in turn preserve for downstream use, and also so we can
restore it to the stream when our application is done, to sever our own inserted connection to the queue.
At the end of our application, we must unhook our WndProc method from the queue. We do this by
reinserting the previous hook into the system, consequentially over-writing and detaching our own hook.
 Page 310

In more practical terms, we would start to flesh out our VB6 subclass code by writing a WndProc
method, like the following, into a module file, separate from our form:
'***********************************************
' VB6 Subclassing WindProc Handler
' NOTE: This method will NOT work under VB.NET.
' Place this method, and its support code, listed later, into a Module file, NOT into a form, else an error will be generated.
'***********************************************
Private Function MyWndProc(ByVal hwnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
'handle messages
Select Case hwnd 'check which form the message is for.
Case Form1.hwnd 'our form? (be sure to reflect ACTUAL name of your form).
Select Case uMsg 'yes, so check for the message being one we want to trap.
Case WM_GETMINMAXINFO 'sizing message for this form?
If MyFormWd = 0 Then 'yes, but if form dimension limits have not been set yet,
MyWndProc = CallWindowProc(mPrevHook, hwnd, uMsg, wParam, lParam) 'then let system handle message
Exit Function
End If
Dim MnMxInfo As MINMAXINFO 'else set aside a local copy of the MINMAXINFO structure.
Call CopyMemory(MnMxInfo, lParam, LenB(MnMxInfo)) 'now copy system MINMAXINFO structure to our local copy.
With MnMxInfo
With .ptMinTrackSize 'set min form dimensions in pixels.
.x = MyFormWd \ Screen.TwipsPerPixelX 'form width from twips to pixels (\ = integer division).
.y = MyFormHt \ Screen.TwipsPerPixelY 'form height from twips to pixels.
End With
With .ptMaxTrackSize 'set max form dimensions in pixels.
.x = Screen.Width \ Screen.TwipsPerPixelX 'screen width in pixels.
.y = Screen.Height \ Screen.TwipsPerPixelY 'screen height in pixels.
End With
With .ptMaxPosition 'Top left when maximized.
.x = 0
.y = 0
End With
With .ptMaxSize 'Max size.
.x = Screen.Width \ Screen.TwipsPerPixelX 'allow work desktop area width for max.
.y = Screen.Height \ Screen.TwipsPerPixelY 'allow work desktop area height for max.
End With
End With
Call CopyMemory(lParam, MnMxInfo, LenB(MnMxInfo)) 'copy local MINMAXINFO structure back to the system.
MyWndProc = 0 'indicate that we handled the message.
Case Else 'if we did not find a message we wanted to trap,
' pass the message on through the message queue.
MyWndProc = CallWindowProc(mPrevHook, hwnd, uMsg, wParam, lParam)
End Select 'uMsg
End Select 'hwnd
End Function

As we can see, the above VB6-only MyWndProc function also needs a structure, MINMAXINFO (which will
itself, in turn, require yet another structure, POINTAPI). Our WndProc also requires a Pinvoke declaration
named CopyMemory, another Pinvoke declaration named CallWindowProc, and a system message
constant, WM_GETMINMAXINFO. Lastly, it will call for a procedure-level Long variable named mPrevHook.
This variable will be used to safely store the old message hook address, which will be covered shortly by
the hooking method, HookWin.
Hence, for our VB6 WndProc to work, we must also add the following short block of code above the
previous WndProc code, to support it, which will address all the above requirements:
Public mPrevHook As Long 'store previous Windows Message Queue hook here (default value is zero; inactive).
Public MyFormWd As Integer 'store minimum form width in twips (startup width). This will be filled by Form_Load event.
Public MyFormHt As Integer 'store minimum form height in twips (startup height). This will be filled by Form_Load event.

'***********************************************
' VB6 API Stuff
'***********************************************
Private Const WM_GETMINMAXINFO As Long = &H24
'
' Screen Points in Pixels (used by MINMAXINFO)
Private Type POINTAPI
x As Long
y As Long
End Type
'
' Structure for window sizing
Private Type MINMAXINFO
ptReserved As PointAPI 'Reserved; do not use.
ptMaxSize As PointAPI 'The max width (x member) and the max height (y member) of window in pixels.
ptMaxPosition As PointAPI 'The left (x member) and top (y member) of the maximized window in pixels.
ptMinTrackSize As PointAPI 'The min tracking width (x member) and the min tracking height (y member) of window in pixels.
ptMaxTrackSize As PointAPI 'The max tracking width (x member) and the max tracking height (y member) of window in pixels.
End Type
 Page 311

'
' The following method is used to invoke the old WndProc hook for downstream processing.
Private Declare Function CallWindowProc
Lib "user32" Alias "CallWindowProcA"
(ByVal lpPrevWndFunc As Long,
ByVal hwnd As Long,
ByVal uMsg As Long,
ByVal wParam As Long,
ByVal lParam As Long) As Long
'
' The following method is used to copy the system MINMAXINFO structure back and forth to our local structure.
Private Declare Sub CopyMemory
Lib "kernel32" Alias "RtlMoveMemory"
(ByRef hpvDest As Any,
ByRef hpvSource As Any,
ByVal cbCopy As Long)

All that is left is to hook MyWndProc into the Windows Message Queue, and later unhook it when we are
preparing to exit our VB6 application. We perform both of these tasks with another Pinvoke declaration
named SetWindowsLong, using a constant index named GWL_WNDPROC:
Private Const GWL_WNDPROC As Long = (-4) '(insert this constant adjacent to the WM_GETMINMAXINFO constant, shown above).
'
' The following method is used to assign/reset a WndProc address (insert this method below the CopyMemory method).
Private Declare Function SetWindowLong
Lib "user32" Alias "SetWindowLongA"
(ByVal hwnd As Long,
ByVal nIndex As Long,
ByVal dwNewLong As Long) As Long

To use the above, we will also need to write two additional methods, which I usually name HookWin and
UnhookWin. We will insert these methods below the above SetWindowLong method:
'*****************
' HookWin(): Subclass hwnd into Windows Message Queue processing.
'*****************
Public Sub HookWin(ByVal hWnd As Long, ByRef PrvhWnd As Long)
PrvhWnd = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf MyWndProc)
End Sub

'*****************
' UnhookWin(): remove subclass hook.
'*****************
Public Sub UnhookWin(ByVal hWnd As Long, ByRef PrvhWnd As Long)
If CBool(PrvhWnd) Then 'if there is something to disable.
Call SetWindowLong(hWnd, GWL_WNDPROC, PrvhWnd)
PrvhWnd = 0 'disable previous handle address.
End If
End Sub

After adding all this code, to actually hook our WndProc method into our application is fairly
straightforward. In our Form’s Load event, we would add the following lines of code:
'***********************************************
' Form_Load
'***********************************************
Private Sub Form_Load()
MyFormWd = Me.Width 'save form's startup width in twips.
MyFormHt = Me.Height 'save form's startup height in twips.

'***Disable following line when you are editing and testing other application features. Re-enable when you are finished***
Call HookWin(Me.hwnd, mPrevHook) 'Hook our WndProc method into the Windows Message Queue, save previous hook for later.
End Sub

To unhook our WndProc is just as easy. Add the following line to our form’s Unload event:
'***********************************************
' Form_UnLoad
'***********************************************
Private Sub Form_Unload(Cancel As Integer)
Call UnhookWin(Me.hwnd, mPrevHook) 'Unhook our WndProc from Windows Message Queue and put old one back, overwritng ours.
End Sub

And with that mountain of code, we achieved subclassing into the VB6 Windows Message Queue.
 Page 312

Subclassing Under VB.NET Made E-Z.


Subclassing under VB.NET is far less cumbersome than VB6. Indeed, compared to the code we just saw
for VB6, VB.NET code is virtually no code at all! First, with VB.NET we can debug step through
subclassed code without the special coding or conditional compilation as VB6 would require. Second, if
the code is incorrect, it will not fatally crash. It will instead report an error or simply hang in a wait state.
Either way, we can easily stop the program and return to IDE control. Also, unlike Windows Message
Queue processing under VB6, VB.NET is actually far more accommodating to this task, offering us
several additional service features through System.Runtime.InteropServices that make
subclassing a form almost too easy, and VB.NET will further provide us with a ready-made interface to
the Windows Message Queue, and right within the class code of the Form itself.
Under VB.NET, each form provides us access to its own WndProc interface. Therefore, VB.NET does
not require us to perform any hooking or unhooking, which had caused plenty of crashes under VB6
when code was not yet perfected, making WndProc development under VB6 a long and cautious task,
especially because while we had a WndProc method subclassed into the Windows Message Queue, we
could not single-step debug through the WndProc code. So it had to run correctly, or it did not run at all.
It was actually very frustrating to debug such code until Microsoft finally released DBGWPROC.DLL
for VB6, which gave VB6 capabilities that was a primitive precursor to those provided by VB.NET.
NOTE: Microsoft eliminated the VB6 crashes in 1997 by providing a free debugger DLL for VB6 called DBGWPROC.DLL
that actually allows us to do what I just said we could not do – single-step through VB6 code that is subclassed. It is a slight
pain to use and rather frustrating for the beginner. This DLL enables us to debug VB6 normally while a subclass is active,
but without adding any unnecessary overhead to our finished product or distributing an extra component. We can still find
this tool at Karl E. Peterson’s Classic Visual Basic website, http://vb.mvps.org/tools/ControlsAndComponents.asp.
First, because this new VB.NET subclassing project does not require any of the code we had previously
visited in this article, except for the two minimum sizing variables, we should simply start a brand new
project. We could name the startup form myForm, to be compatible to the form class code, listed below.
The second thing we need to do is to define support structures and add a Windows Message Queue
processor. Typically, this part is easy, being as easy as tripping over our own feet when we are trying to
look ‘cool’ when a gorgeous member of the opposite sex enters the room.
Unlike VB6, VB.NET WndProc code is simple. A characteristic VB.NET WndProc method for a form
eats up very little program space. A program shell for a typical VB.NET WndProc method is as follows:
'***********************************************
' WndProc()
' Override MyBase.WndProc with our own method.
' We will check our form for specific system-
' defined messages and handle them ourselves.
'***********************************************
Protected Overrides Sub WndProc(ByRef m As Message) 'establish our hook into the message queue.
' Do processing for messages for current form.
Select Case m.Msg 'check current message in queue for this form.
Case WM_MISCMSG1 'a match for a sought message?
'message support code goes here...
m.Result = IntPtr.Zero 'inform system that we handled this message.
Case WM_MISCMSG2 'a match for another sought message?
' message support code goes here...
m.Result = IntPtr.Zero 'inform system that we handled this message.
' Insert other CASE parsers here...
Case Else
MyBase.WndProc(m) 'let the system handle messages we did not trap.
End Select
End Sub

As we can see, we have cut out all the Pinvokes (VB6 APIs) that were previously needed for subclassing
under VB6, and we eliminated hooking and unhooking support methods by simply overriding the
WndProc method declared in MyBase, the base class that all form classes inherit from. We do not need to
worry about unhooking it because our override is automatically unhooked when the application ends by
the base class. Hence, under VB.NET we never have to worry about setup or takedown at all. We can
simply get to the business of parsing for the required form messages.
 Page 313

However, because in our situation we are parsing for a message ID named WM_GETMINMAXINFO (integer
value &H24, or 36 decimal), we will need to apply additional code to support it. We will of course need
to declare our constant to identify WM_GETMINMAXINFO. We will also need to declare the structure
MINMAXINFO, as well as a local copy of this structure. We will use the System Runtime Interop Services
memory marshalling method PtrToStructure to safely copy a system structure to our local copy, and its
StructureToPtr method to safely copy our local structure back to system memory. But even with all
that, it amounts to very little additional code. Peripherally, we also need our variables MyFormWd and
MyFormHt to store our minimal form sizing values.
As such, the following block of code must be added to each of our forms. Here is the full, basic form
code to fully implement subclassing form dimension limit setting under VB.NET:
' ADD SUPPORT FOR BLOCKING THE USER FROM RESIZING THIS FORM BELOW ITS STARTUP DIMENSIONS.
'
Option Strict On
Option Explicit On

Imports System.Runtime.InteropServices

Public Class myForm 'BE SURE TO IDENTIFY YOUR FORM NAME HERE.
Private MyFormWd As Integer = 0 'store minimum form width in pixels (startup width). Init to null.
Private MyFormHt As Integer = 0 'store minimum form height in pixels (startup height). Init to null.

' message to look for in the Windows Message Queue.


Private Const WM_GETMINMAXINFO As Integer = &H24

' Structure used to define a form's minimum and maximum sizing (Supports WM_GETMINMAXINFO message)
Private Structure MINMAXINFO
Private ptReserved As Point 'Reserved; do not use (NOTE: we can use a VB.NET Point instead of POINTAPI).
Dim ptMaxSize As Point 'The maximum width (x member) and the maximum height (y member) of the form.
Dim ptMaxPosition As Point 'The left (x member) and top (y member) of the form when maximized.
Dim ptMinTrackSize As Point 'The minimum width (x member) and the minimum height (y member) for the form.
Dim ptMaxTrackSize As Point 'The maximum width (x member) and the maximum height (y member) for the form.
End Structure

'***********************************************
' myForm_Load
'***********************************************
Private Sub myForm_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
' place other initialization code here.
'
' At the end of the Load event, save the current form size to establish its minimum size.
MyFormWd = Me.Width 'save form's startup width.
MyFormHt = Me.Height 'save form's startup height.
End Sub

'***********************************************
' VB.NET WndProc Intercept method
' (overrides default MyBase.WndProc)
' NOTE that this is a SUB, not a FUNCTION.
'-----------------------------------------------
' This WndProc is designed for exclusive use by the current form.
' This is much easier than the way we had to do it under VB6, where we had to
' hook and unhook the WndProc method, and crashes were common during development.
'***********************************************
Protected Overrides Sub WndProc(ByRef m As Message)
' Do processing for messages for current form. Note the above parameter is ByRef.
Select Case m.Msg
Case WM_GETMINMAXINFO 'is it our sought message?
If MyFormWd = 0 Then 'yes, but if the form dimension limits have not been set yet,
MyBase.WndProc(m) ' then let the system handle this message for now.
Exit Sub
End If
Dim mMinMaxInfo As MINMAXINFO 'we can handle it, so set aside a local copy of the MINMAXINFO structure.
' Safely copy system MINMAXINFO data from lParam into our local copy of the structure.
' Note further that mMinMaxInfo.GetType actually provides us with the target structure's memory address.
Marshal.PtrToStructure(m.LParam, mMinMaxInfo.GetType) 'parameters (from_address, to_structure_address)
' Update the current data with our new form minimum and maximum.
With mMinMaxInfo
With .ptMinTrackSize 'set the minimum size we want for the form.
.x = MyFormWd 'set the minimum width from the form's startup width.
.y = MyFormHt 'set the minimum height from the form's startup height.
End With
With .ptMaxTrackSize 'set the maximum width and height we want for the form.
.x = My.Computer.Screen.WorkingArea.Width 'allow work desktop area width for max.
.y = My.Computer.Screen.WorkingArea.Height 'allow work desktop area height for max.
End With
With .ptMaxPosition 'Top left location when the form is maximized.
.x = My.Computer.Screen.WorkingArea.Left
 Page 314

.y = My.Computer.Screen.WorkingArea.Top
End With
With .ptMaxSize 'Max size.
.x = My.Computer.Screen.WorkingArea.Width 'allow work desktop area width for max.
.y = My.Computer.Screen.WorkingArea.Height 'allow work desktop area height for max.
End With
End With
' Safely copy MinMaxInfo data to lParam location as new object (TRUE deletes old structure object at m.LParam).
Marshal.StructureToPtr(mMinMaxInfo, m.LParam, True) 'parameters (from_structure_address, to_address, True).
' Return 0 to tell the system to ignore further processing on this message because we have handled it.
m.Result = IntPtr.Zero
Case Else
MyBase.WndProc(m) 'let the system handle messages we did not trap.
End Select
End Sub
End Class

NOTE: Note the highlighted lines featuring Interop helper methods PtrToStructure and StructureToPtr. These are VB.NET
Managed and safer versions of the old VB6 standby Pinvoke, RtlCopyMemory (AKA CopyMemory). They can be used to
instantly copy blocks of data to and from classes and structures. They can even copy code to and from Unmanaged space, all
without needing to declare a Pinvoke. Used with the VB.NET version of VarPtr on page 56, power users can work miracles!
First, notice that instead of using the POINTAPI structure to declare a Point coordinate, we instead use a
VB.NET Point. Although the VB.NET version of this structure features innumerable methods and
constructors, its data signature footprint is in fact identical to its more primitive POINTAPI cousin.
Also, notice that there was no need to hook or unhook our WndProc method, as we would have had to
do under VB6 using Pinvokes. Additionally, because we are overriding the default WndProc method
already declared for the form through its base class ( MyBase.WndProc), we do not need to test for a
Window handle for the form; the base process has already done that for us. Notice also that on top of
that we do not have to pass the address of our WndProc method to the system. Notice further that in
order to pass something on through the Windows Message Queue, we need only pass the single message
packet structure (m), declared as type Message to the base class via MyBase.WndProc(m), rather than
having to use the CallWindowsProc Pinvoke. Indeed, this interface did not require the direct use of any
Pinvokes at all because we are letting the .NET Framework safely do that for us. Lastly, notice this code
must be placed within our Form class, because we are overriding its WndProc base class method.
NOTE: Experienced developers might be quick to notice how VB.NET makes WndProc handling so easy. A studious eye
would notice that our override simply plugged us into the form's own WndProc invocation address. If we were not going to
handle a message, we simply pass the message packet on to the WndProc method of MyBase.
NOTE: If we do want to place this code within a separate file (typically, we may choose to keep just myForm_Load in the main
form), we can create an auxiliary form file with a different name. Me, I simply create a new Module file, naming it similar to
my form. For example, if my form file was named myForm.vb, I might name my new Module file myFormSubclass.vb. Next, I
would completely replace the default Module myFormSubclass code block added to the Module with the following block;
converting it into an additional myForm class file, because its use of the Partial verb will tell the compiler to include this
code in the primary myForm class code, noting further that this partial class will default to Friend Scope:
'Create an extension to Class myForm that will be integrated as a component of the main body of the myForm Class.
Partial Class myForm

End Class

And I would then add my subclassing code to this partial class file, conveniently and safely storing my subclass code in a
separate file that will be merged with the default myForm class code during compilation. An alternative is to just stow the
subclass code away in a #Region/#EndRegion block within our form code (Declare #Region "Subclass", for example).
NOTE: Rather than depending on the old VB6 standby Pinvoke, RtlCopyMemory (AKA CopyMemory), we should look into
the Interop helper methods PtrToStructure and StructureToPtr, exposed by importing System.Runtime.Interopservices.
These two methods are VB.NET Managed and safer versions of RtlCopyMemory. They can be used to instantly copy blocks of
data to and from classes and structures. They can even copy code to and from Unmanaged space, all without needing to
declare a Pinvoke. Used with the VB.NET version of VarPtr on page 56, power users can work miracles!
 Page 315

Conclusion.
And that is all there is to it. To some, this WndProc code might at first seem a bit more intimidating than
the initial VB code techniques offered at the start of this article. But take my word for it, VB.NET
WndProc is Heaven compared to the medieval torture, cold gruel, and digital rats we had to suffer
through while developing subclassing under VB6, which I think I clearly demonstrated by showing us
the convoluted Pinvoke minefield we had to gingerly tip-toe through to avoid program crashes and
complementry system refreshes (the hard way), not to mention the hilarious frozen looks on our faces,
like a deer in headlights, as our brains whir to remember the last time we saved our work... or if we
saved our work, That. versus the simple and straightforward code that we had to supply to VB.NET.
Further, the structure declared in the form, MinMaxInfo, as well as the WM_GETMINMAXINFO constant can be
declared in a separate module file where all of our forms can share them. But in that case, also be sure to
declare these items as Friend instead of as Private.
Finally, remember that a VB.NET form sports two new properties not available to VB6 named
MinimumSize and MaximumSize, where we can optionally declare the minimum and/or maximum
dimensions a form can be sized to by modifying their Height and Width sub-property’s default 0
(inactive) values to the pixel dimension we require. With these parameters, we can completely forego the
above examples, because all those things are already done for us, sight unseen. However, this exercise
in exploring form subclassing is a very useful example to demonstrate how other subclassing tasks not
supplied by VB.NET can very easily be added to heighten our user's experience .
NOTE: In case you were curious, the code to handle MinimumSize and MaximumSize are actually embodied within the
hidden, inner workings of the default MyBase.WndProc method. We just got in front of that code by forcing the system to use
our own custom code instead.
 Page 316

Centering System Dialogs Over VB.NET Forms Using the Thread Message Queue.
A lot of programmers wish that they could center a message box over a
form. However, when a MessageBox is displayed, by default it is centered
instead to the desktop. If the form that invoked it is located off to the side
somewhere, this ends up looking a bit amateurish. It would certainly look
much more professional if it were to be centered over the invoking form.
It turns out that the explanation for how to actually do this is quite simple.
The Simple Explanation of Centering a Message Box on a Form.
If we monitor the system, when a message box is about to be activated, we can intercept it. We then
reposition the Message Box on the screen to center it over our form and release it. The harder part is
getting there. However, I think that it is more so a matter of simply understanding the process of getting
there rather than the actual software means of doing so, because it requires very little code to do this.
A More Detailed Explanation of Centering a Message Box on a Form.
When a window does anything, from resize, to move, to being created or destroyed, or to being
activated, a system message is sent out into a Message Queue notifying all concerned parties connected
into its pipeline of this action. One action that most people are not aware of is that besides the system
sending out messages to indicate that it is doing or did do something, prior to that it will also send out a
message that it is ABOUT to do that thing. Concerned parties are typically the system itself, the object in
question, once it is active, and any owner or parent window involved. But again, they are notified only if
they are hooked into that queue and if they are programmed to monitor that action.
We are not going to find all those messages in the default System Message Queue. That is because there
are actually more than one Message pipeline. There is the general one, often referred to as the System
Message Queue that is monitored by a WndProc, or Window Processor, which most people are familiar
with (and those who are brave enough to use it – though I think it has more to do with experience rather
than actual bravery). A WndProc method monitors the System Message Queue and has 4 parameters:
1. hWnd: This is the Integer or IntPtr handle of the object concerned. The user’s WndProc method checks this value for
being itself, such as Me.Handle, for example, or checks for a known handle that the code is designed to monitor,
whether a form, a text box, or a check box.
2. iMsg: An Integer message ID representing an associated pre-defined message regarding the hWnd window.
3. wParam: An optional 32-bit Integer parameter whose use is determined by how iMsg is defined.
4. lParam: An optional but secondary 32-bit Integer parameter whose use is determined by how iMsg is defined.
NOTE: wParam and lParam are names carried over from 16-bit Windows, where wParam was a Word-sized integer –16
bits at the time – and lParam was a Long Integer-sized parameter – then 32-bits. Now, both are 32-bit Integer values.
The other queue type is a Thread Message Queue, which is handled by what I call a ThreadProc, or Thread
Processor, though some prefer CBTProc, for CallBack Thread Processor. The CallBack part has to do with
the system using the message to see if it should continue doing what it is notifying listeners it is about to do.
A ThreadProc is hooked into a particular Thread Message Stream, typically the thread messages associated
with the current application, and the messages passing through that queue are for that thread alone, and for
anyone choosing to listen in by hooking into the stream. A Thread Processor has just 3 parameters:
1. iMsg: An Integer message ID value targeted to the thread it is being pumped through.
2. wParam: An optional 32-bit Integer parameter whose use is determined by how iMsg is defined.
3. lParam: An optional secondary 32-bit Integer parameter whose use is determined by how iMsg is defined.
A Thread is often thought of as a mysterious system creature whose identity and use are beyond the reach of
mere mortals. But a Thread is technically nothing more than a flow process; the serial code stream that
executes from one point to another. But along the way, objects, especially system objects, can get quite
verbose in notifying the system or its parent object or its child objects of what it is doing; messages essential
to maintaining order in a very complex system. We can therefore also see a Thread as the message chatter the
application objects exchange as they wiggle their way through the decision gates along their code stream.
 Page 317

NOTES: A computer runs many applications at once, and each instance of an application is known as a process. Each
process is made up of one or more threads. Each thread, having a serial flow of control, is a sequence of code that can be
responsible for one aspect of the program, or even a single task the program has assigned to it. In most applications, such as
most VB.NET applications, they run on a single thread and are therefore called single-thread applications. In more complex
environments it may be necessary to process more than one thread. For example, one thread might focus on the user
interface, and one or more others to performing behind-the-scenes tasks, such as database maintenance, background spell
checking, garbage collection, or even running Bot services to scour referenced data wells for relevant information.
The trick is when we are dealing with multiple threads. While it is guaranteed that each thread progresses through its code in
sequence, it is not known where each thread will be relative to any other, because each runs at its own pace. Problems crop
up if one thread tries to access shared data while another thread is presently using or updating it. As such, a barrier flag is
needed to signal all clashing threads to wait until the flag is cleared, to avoid the simultaneous use of a common resource to
ensure only one thread has access to the data at any time. Such an object is called a Mutex (Mutual Exclusion, and called a
class SyncLock under VB.NET), which is a synchronization primitive that is used to manage inter-process synchronization. A
Mutex is not owned by any one thread, though all threads will check it or even create it as needed. When a thread reaches a
potential clash point, it can be programmed to check if a Mutex is linked to it. If one is not, it attaches to one, and if none are
available, it creates one, and sets a flag that other threads designed to monitor that thread will interpret as a wait signal.
When the active thread completes its work, it detaches from the Mutex and the next thread in the queue has its turn at the
data. Once all concerned threads have detached themselves, the Mutex is released until the next potential clash.
If a ThreadProc is set up to catch CBT (CallBack Thread) Messages, then it will receive a HCBT_ACTIVATE
message when a subordinate window is about to be activated. If we were to hook into our application’s
Thread Message Queue, or at least the one that the current form is attached to, and we launched a message
box, we would receive this message, and so we would know that a message box is about to be activated. As it
happens, when the HCBT_ACTIVATE message is received, the wParam parameter will contain the window
handle of the window about the be activated, which is the message box in our case. We can use it to get its
bounding rectangle, use our form’s handle to get its bounding rectangle, and then we can compute how to
center the message box on the form, its size being already determined. Once the new location is computed,
we can set the message box’s new position using the SetWindowsPos Pinvoke. When we are done with that, we
can unhook ourselves from our thread’s Message Queue, because we no longer need to monitor it.
So, with that understanding of the process, looking at exactly how it is done will make more sense.
The Detailed Explanation of Centering a Message Box on a Form.
In order to center a message box over a form, the first thing we need to do is provide the message box
with the form that it shall be centered over. Because the current MessageBox object or MsgBox
command do not provide for one, we will need to create a wrapper for it so that it will. This is very easy
to do. Consider this wrapper function, designed around the MessageBox Pinvoke, which shall do nicely,
especially because this method also features a Parent Window parameter, giving the message box focus
priority over the parent form:
'*************************************************************************************
'local storage
'*************************************************************************************
Private FrmhWnd As IntPtr = IntPtr.Zero 'store parent form handle.
'*************************************************************************************
' Method : CntrMsgBox
' Description: Center a message box on a specified form using MsgBox syntax.
'*************************************************************************************
Private Declare Auto Function MessageBox Lib "user32" (ByVal hwnd As IntPtr,
ByVal lpMessage As String,
ByVal lpTitle As String,
ByVal wStyles As MsgBoxStyle) As MsgBoxResult

Public Function CntrMsgBox(ByRef ParentForm As Form,


ByVal TextMsg As String,
Optional ByVal Buttons As MsgBoxStyle = MsgBoxStyle.OkOnly,
Optional ByVal Title As String = vbNullString) As MsgBoxResult
' Set up the CBT hook.
FrmhWnd = ParentForm.Handle 'save the parent form handle for use by our ThreadProc.
'--------------------------------------
' STUFF WE HAVE YET TO DEFINE GOES HERE.
'--------------------------------------
' Display the message box.
Return MessageBox(FrmhWnd, TextMsg, Title, Buttons) 'by invoking MessageBox, it will be centered and the process unhooked.
End Function
 Page 318

To invoke CntrMsgBox, we will treat it just like a regular MsgBox command, but one where the very
first parameter is now the form we eventually want to center the message box over. For example:
CntrMsgBox(Me, "Selection Already Made!", MsgBoxStyle.Exclamation Or MsgBoxStyle.OkOnly, "NOTICE")

Or:
If CntrMsgBox(Me, "Verify deletion of '" & sFileName & "'.",
MsgBoxStyle.YesNo Or MsgBoxStyle.Question Or MsgBoxStyle.DefaultButton2,
"Verify Delete") = MsgBoxResult.No Then Exit Sub

Ideally, before we display the message box, however, we will want to hook a custom ThreadProc
method into the Thread Message Queue associated with our application so that we can detect when the
message box is actually about to be activated (after the background system code constructs it for us).
To hook our callback method into the thread queue requires the SetWindowsHookEx Pinvoke. Likewise,
we will need the UnhookWindowsHookEx Pinvoke to unhook from it. Do not get nervous. This stuff is
actually pretty safe to work with as long as we are not prone to insane experiments.
The SetWindowsHookEx and the UnhookWindowsHookEx Pinvokes are declared as follows:
'-------------------------------------------------------------------------------------
' hook a ThreadProc into the Thread Message Queue.
Declare Function SetWindowsHookEx Lib "user32.dll" Alias "SetWindowsHookExA" (ByVal idHook As Integer,
ByVal lpfn As ThreadProcCenterDelegate,
ByVal hmod As Integer,
ByVal dwThreadId As Integer) As Integer
'-------------------------------------------------------------------------------------
' unhook ThreadProc from the Thread Message Queue.
Private Declare Function UnhookWindowsHookEx Lib "user32" (ByVal hHook As Integer) As Integer
'-------------------------------------------------------------------------------------

The SetWindowsHookEx Pinvoke will hook a callback into a thread hook chain that will allow us to
monitor particular types of messages (we will get to the ThreadProcCenterDelegate soon enough).
Because the message we are interested in is the HCBT_ACTIVATE message (the “H” indicates a handle
will be invoked), we must tell this Pinvoke that we want to monitor “CBT” type messages by providing
it with a WH_CBT constant, a Word-Sized Handle related to CBT, or Callback Thread. Regardless, we need
it now. This is why many just refer to a ThreadProc as a CBTProc, even though CallBacks are just one
aspect of the Thread Processor. After all, every single WndProc is also a callback method.
We also have to provide SetWindowsHookEx with the address of our ThreadProc (this means, darn it, we
actually do have to write this callback method), plus the instance handle of our form, which simply
identifies which instance of the form we are using, because many applications can run multiple instances
of forms, let alone run multiple instances of itself. This is sort of like running two copies of Notepad at
the same time, for example, and we will also need to provide it with the Thread ID for the current
Thread so that it will know specifically which Thread Message Stream to hook us into.
The SetWindowsHookEx Pinvoke also returns the Integer handle of the new hook (callback) we just
added. We will want to store this handle away so that we can use it later when we invoke the
UnhookWindowsHookEx Pinvoke to unhook ourselves, once we have finished processing the form.
To get the Instance handle of the invoking form (not of the application), we can invoke the
GetWindowLong Pinvoke with the handle of the form and a GWL_HINSTANCE constant, which tells the
Pinvoke that we want the instance handle of the form whose window handle we just provided it with.
To get the current thread ID, we can simply invoke the GetCurrentThreadId Pinvoke. Thus far, with the
above defined, the heading of our module should now look something like this:
'*************************************************************************************
' Pinvoke stuff.
'*************************************************************************************
Private Const GWL_HINSTANCE As Integer = (-6) 'obtain window instance handle (used by GetWindowLong Pinvoke).
Private Const WH_CBT As Integer = 5 'Installs a hook procedure that receives CBT notifications.
Private Const HCBT_ACTIVATE As Integer = 5 'Thread queue message indicating the system is about to activate a window.
 Page 319

'-------------------------------------------------------------------------------------
' Hook a ThreadProc into the Message Queue.
Declare Function SetWindowsHookEx Lib "user32.dll" Alias "SetWindowsHookExA" (ByVal idHook As Integer,
ByVal lpfn As ThreadProcCenterDelegate,
ByVal hmod As Integer,
ByVal dwThreadId As Integer) As Integer
'-------------------------------------------------------------------------------------
' Unhook ThreadProc from Message Queue.
Private Declare Function UnhookWindowsHookEx Lib "user32" (ByVal hHook As Integer) As Integer
'-------------------------------------------------------------------------------------
' Get a system value for a window.
Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As IntPtr,
ByVal nIndex As Integer) As Integer
'-------------------------------------------------------------------------------------
' Get current thread ID from a window handle.
Private Declare Function GetCurrentThreadId Lib "kernel32.dll" () As Integer
'-------------------------------------------------------------------------------------
' Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message,
' such as status or error information. The message box returns an integer value that indicates which button the user clicked.
Private Declare Auto Function MessageBox Lib "user32" (ByVal hwnd As IntPtr,
ByVal lpMessage As String,
ByVal lpTitle As String,
ByVal wStyles As MsgBoxStyle) As MsgBoxResult
'*************************************************************************************
' Local storage.
'*************************************************************************************
Private hHook As Integer = 0 'store our new hook handle.
Private FrmhWnd As IntPtr 'store parent form handle.

'*************************************************************************************
' Method : CntrMsgBox
' Description: Center a message box on a specified form using MsgBox syntax.
'*************************************************************************************
Public Function CntrMsgBox(ByRef ParentForm As Form, ByVal TextMsg As String,
Optional ByRef Buttons As MsgBoxStyle = MsgBoxStyle.OkOnly Or MsgBoxStyle.Information,
Optional ByVal Title As String = vbNullString) As MsgBoxResult
' Set up the CBT hook.
FrmhWnd = ParentForm.Handle 'save parent form handle for use by ThreadProc.
Dim hInst As Integer = GetWindowLong(FrmhWnd, GWL_HINSTANCE) 'get instance handle for our form.
Dim Thread As Integer = GetCurrentThreadId() 'get the current thread ID.
'hHook = SetWindowsHookEx(WH_CBT, AddressOf ThreadProcCenter, hInst, Thread) 'insert a new hook, and save its handle.
' Display the message box.
Return MessageBox(FrmhWnd, TextMsg, Title, Buttons) 'by invoking MessageBox, it will be centered and the process unhooked.
End Function

As we can see, I have disabled the actual invocation of the SetWindowsHookEx Pinvoke until we have our
ThreadProc method, ThreadProcCenter, actually written.
So let us write it. It is probably the easiest part.
Our ThreadProcCenter method is defined by the following template shell:
'-------------------------------------------------------------------------------------
'Declare Delegate for subclassed ThreadProc procedure:
Private Delegate Function ThreadProcCenterDelegate(ByVal lMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As Integer) As Integer
'*************************************************************************************
' ThreadProcCenter: Thread Message handler hook.
'*************************************************************************************
Private Function ThreadProcCenter(ByVal iMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
' On HCBT_ACTIVATE, show the MsgBox centered over parent Form.
If iMsg = HCBT_ACTIVATE Then 'if our form receives this message, a SUBORDINATE form is about to be activated.
'>>>>>>>>>>>GUTS OF ThreadProcCenter GO HERE<<<<<<<<<<<<
' Release this WndProc hook because we now no longer need it.
UnhookWindowsHookEx(hHook)
hHook = 0
End If
Return 0 '0 allows an operation to continue to be processed (1 would prevent it).
End Function

With the above shell, we can now enable the SetWindowsHookEx line in the code above. Even with the
code we have right now, the message box will still display centered to the screen, as it did before. This is
because we have yet to change its location (that code will go into the above red zone).
In order to center the message box, we will need to get its bounding rectangle, and the “parent” form’s
bounding rectangle. We can do that with another Pinvoke, GetWindowRect. However, although this will
not affect the size of the executed compiled code by one byte, we will have to declare two versions of
this Pinvoke; one to accommodate an IntPtr window handle, which is supplied by our “parent” form,
 Page 320

and an Integer window handle, which will be supplied by our wParam parameter. Further, we will need
to provide it with rectangle structures that will receive 4 Integer values: the window’s Left, Top, Right,
and Bottom pixel positions on the screen. Also, since I already know I will need to compute the width
and the height of the received rectangles, I will add a couple of methods to compute them in the
structure. The new structure and Pinvokes we will need are declared as follows:
'-------------------------------------------------------------------------------------
' Structure RECT
' structure to declare the top-left and bottom-right pixel coordinates of a form.
'-------------------------------------------------------------------------------------
<StructLayout(LayoutKind.Sequential)>
Private Structure RECT
Public iLeft As Integer
Public iTop As Integer
Public iRight As Integer
Public iBottom As Integer
'----------------------------------
'compute rectangle width
Public Function Width() As Integer
Return iRight - iLeft
End Function
'----------------------------------
' Compute rectangle height
Public Function Height() As Integer
Return iBottom - iTop
End Function
End Structure
'-------------------------------------------------------------------------------------
' method used to get the top-left and bottom-right pixel coordinates of a form, using IntPtr.
Private Declare Function GetWindowRect Lib "user32.dll" (ByVal hwnd As IntPtr,
ByRef lpRect As RECT) As Integer
' method used to get the top-left and bottom-right pixel coordinates of a form, using Integer.
Private Declare Function GetWindowRect Lib "user32.dll" (ByVal hwnd As Integer,
ByRef lpRect As RECT) As Integer
'-------------------------------------------------------------------------------------

With that, we can now compute the new upper-left corner of the message box so that it will be centered
on the form, inserting it below the test “If iMsg = HCBT_ACTIVATE Then” in the ThreadProcCenter method:
Dim rectForm, rectMsg As RECT 'storage for bounding rectangles for the invoking form and the message box.
' Get the coordinates of the form and the message box so that you can determine where the center of the form is located.
GetWindowRect(FrmhWnd, rectForm) 'get the parent form bounding rectagle.
GetWindowRect(wParam, rectMsg) 'get the msgbox bounding rectangle (wParam is the windows Handle for the msgbox).

' Now compute Top-Left position for msgbox to center it on the parent form.
Dim X As Integer = rectForm.iLeft + rectForm.Width \ 2 - rectMsg.Width \ 2 'point to parent center, then back 1/2 msgbox width.
Dim Y As Integer = rectForm.iTop + rectForm.Height \ 2 - rectMsg.Height \ 2 'point to parent center, then back 1/2 msgbox height.

All that we have left to do is to actually position the form. As stated much earlier, this is accomplished
with the SetWindowPos Pinvoke, which is declared below:
'-------------------------------------------------------------------------------------
' Set a window position on the screen.
Private Declare Function SetWindowPos Lib "user32.dll" (ByVal hWnd As Integer,
ByVal hWndInsertAfter As Integer,
ByVal X As Integer,
ByVal Y As Integer,
ByVal cx As Integer,
ByVal cy As Integer,
ByVal wFlags As Integer) As Integer
'-------------------------------------------------------------------------------------

The 7 parameters for this Pinvoke are as follows:


1. hWnd is the handle of the window to position, provided by our ThreadProc wParam parameter.
2. hWndInsertAfter is useful if we want to change the ZOrder of windows, and place the window after a specified
window handle, or as the topmost window in the ZOrder (0), as the last (1), at the top of the non-topmost, which is
behind the lowest topmost window, or as the highest of the non-topmost windows (-2), or as the lowest of the
topmost windows, but still above all non-topmost windows (-1). That can make our brain hurt. We do not want to
move it. Leave it 0. We will use SWP_NOZORDER, below, to ignore it.
3. X is the left coordinate for the window. With Y, this defines the top-left corner of the window.
4. Y is the top coordinate for the window. With X, this defines the top-left corner of the window.
5. cx is the new width of the window. If SWP_NOSIZE specified, detailed below, then this parameter is ignored.
6. cy is the new height of the window. If SWP_NOSIZE specified, detailed below, then this parameter is ignored.
7. wFlags allows for customization of the window. It is defined as follows:
 Page 321

wFlags Value Meaning


SWP_ASYNCWINDOWPOS If the calling thread and the thread that owns the window are attached to different input queues,
0x4000 the system posts the request to the thread that owns the window. This prevents the calling
thread from blocking its execution while other threads process the request.
SWP_DEFERERASE Prevents generation of the WM_SYNCPAINT message.
&H2000
SWP_DRAWFRAME Draws a frame (defined in the window's class description) around the window.
&H20
SWP_FRAMECHANGED Applies new frame styles set using the SetWindowLong function. Sends a WM_NCCALCSIZE
&H20 message to the window, even if the window's size is not being changed. If this flag is not
specified, WM_NCCALCSIZE is sent only when the window's size is being changed.
SWP_HIDEWINDOW Hides the window.
&H80
SWP_NOACTIVATE Does not activate the window. If this flag is not set, the window is activated and moved to the
&H10 top of either the topmost or non-topmost group (depending on the setting of the
hWndInsertAfter parameter).
SWP_NOCOPYBITS Discards the entire contents of the client area. If this flag is not specified, the valid contents of
&H100 the client area are saved and copied back into the client area after the window is sized or
repositioned.
SWP_NOMOVE Retains the current position (ignores X and Y parameters).
&H2
SWP_NOOWNERZORDER Does not change the owner window's position in the Z order.
&H200
SWP_NOREDRAW Does not redraw changes. If this flag is set, no repainting of any kind occurs. This applies to the
&H8 client area, the nonclient area (including the title bar and scroll bars), and any part of the parent
window uncovered as a result of the window being moved. When this flag is set, the application
must explicitly invalidate or redraw any parts of the window and parent window that need
redrawing.
SWP_NOREPOSITION Same as the SWP_NOOWNERZORDER flag. This had functionality under Win16 that is no
&H200 longer supported because it is automatically handled in Win32.
SWP_NOSENDCHANGING Prevents the window from receiving the WM_WINDOWPOSCHANGING message.
&H400
SWP_NOSIZE Retains the current size (ignores the cx and cy parameters).
&H1
SWP_NOZORDER Retains the current Z order (ignores the hWndInsertAfter parameter).
&H4
SWP_SHOWWINDOW Displays the window.
&H40

Because we do not want to change the current ZOrder, or change its size, and we do not want the form to
be re-activated (which would force it to change position in the ZOrder stack, which we do not want), we
are thus interested in the SWP_NOZORDER, SWP_NOSIZE, and SWP_NOACTIVATE constants. So
we should declare the following constants at the top of our module with our other constants:
Private Const SWP_NOSIZE As Integer = &H1 'Retains the current form size (used by SetWIndowPos Pinvoke).
Private Const SWP_NOZORDER As Integer = &H4 'Retains the current Z order (used by SetWIndowPos Pinvoke).
Private Const SWP_NOACTIVATE As Integer = &H10 'Does not activate the window (used by SetWIndowPos Pinvoke).

With that, we can add our last line of code, inserted right after we calculated the X and Y coordinates of
our message box’s new upper-left coordinate, and begin centering message boxes over forms at will:
'now Position the msgbox to the computed coordinates
SetWindowPos(wParam, 0, X, Y, 0, 0, SWP_NOSIZE Or SWP_NOZORDER Or SWP_NOACTIVATE)

Putting It All Together.


Now that we have defined all parts of the solution, we should glue them all together so that we can copy
them into our own module and begin using it to center our own message boxes.
NOTE: We can quickly adapt this code to work with other system dialogs. Just change the highlighted code, below.
What follows is the complete modCntrMsgBox module:
 Page 322

Option Strict On
Option Explicit On

Imports System.Runtime.InteropServices

Module modCntrMsgBox
'*************************************************************************************
' modCntrMsgBox - The CntrMsgBox() will center a message box on a
' specified form, instead of the center of the screen. Simply
' invoke the CntrMsgBox() function as you would the
' MsgBox() form, except you additionally specify the form
' that the message should be centered over.
'*************************************************************************************

'*************************************************************************************
'Pinvoke stuff
'*************************************************************************************
Private Const GWL_HINSTANCE As Integer = (-6) 'instance handle flag (used by GetWindowLong Pinvoke).
Private Const SWP_NOSIZE As Integer = &H1 'Retains the current form size (used by SetWIndowPos Pinvoke).
Private Const SWP_NOZORDER As Integer = &H4 'Retains the current Z order (used by SetWIndowPos Pinvoke).
Private Const SWP_NOACTIVATE As Integer = &H10 'Does not activate the window (used by SetWIndowPos Pinvoke).
Private Const WH_CBT As Integer = 5 'Installs a hook procedure that receives CBT notifications.
Private Const HCBT_ACTIVATE As Integer = 5 'Thread queue message indicating the system is about to activate a window.
'-------------------------------------------------------------------------------------
' hook a WndProc into the Thread Message Queue.
Declare Function SetWindowsHookEx Lib "user32.dll" Alias "SetWindowsHookExA" (ByVal idHook As Integer,
ByVal lpfn As ThreadProcCenterDelegate,
ByVal hmod As Integer,
ByVal dwThreadId As Integer) As Integer
'-------------------------------------------------------------------------------------
' unhook WndProc from the Thread Message Queue
Private Declare Function UnhookWindowsHookEx Lib "user32" (ByVal hHook As Integer) As Integer
'-------------------------------------------------------------------------------------
' get a system value for a window.
Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hwnd As IntPtr,
ByVal nIndex As Integer) As Integer
'-------------------------------------------------------------------------------------
' get current thread ID from a window handle.
Private Declare Function GetCurrentThreadId Lib "kernel32.dll" () As Integer
'-------------------------------------------------------------------------------------
' set a window position on the screen.
Private Declare Function SetWindowPos Lib "user32.dll" (ByVal hWnd As Integer,
ByVal hWndInsertAfter As Integer,
ByVal X As Integer,
ByVal Y As Integer,
ByVal cx As Integer,
ByVal cy As Integer,
ByVal wFlags As Integer) As Integer
'-------------------------------------------------------------------------------------
' Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message,
' such as status or error information. The message box returns an integer value that indicates which button the user clicked.
Private Declare Auto Function MessageBox Lib "user32" (ByVal hwnd As IntPtr,
ByVal lpMessage As String,
ByVal lpTitle As String,
ByVal wStyles As MsgBoxStyle) As MsgBoxResult
'-------------------------------------------------------------------------------------
' Structure RECT
'structure to declare the top-left and bottom-right pixel coordinates of a form.
'-------------------------------------------------------------------------------------
<StructLayout(LayoutKind.Sequential)>
Private Structure RECT
Public iLeft As Integer
Public iTop As Integer
Public iRight As Integer
Public iBottom As Integer
'----------------------------------
'compute rectangle width
Public Function Width() As Integer
Return iRight - iLeft
End Function
'----------------------------------
'compute rectangle height
Public Function Height() As Integer
Return iBottom - iTop
End Function
End Structure

'-------------------------------------------------------------------------------------
' method used to get the top-left and bottom-right pixel coordinates of a form, using IntPtr.
Private Declare Function GetWindowRect Lib "user32.dll" (ByVal hwnd As IntPtr,
ByRef lpRect As RECT) As Integer
' method used to get the top-left and bottom-right pixel coordinates of a form, using Integer.
Private Declare Function GetWindowRect Lib "user32.dll" (ByVal hwnd As Integer,
ByRef lpRect As RECT) As Integer
'-------------------------------------------------------------------------------------
 Page 323

' Declare Delegate for subclassed ThreadProc procedure:


Delegate Function ThreadProcCenterDelegate(ByVal lMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As Integer) As Integer

'*************************************************************************************
'local storage.
'*************************************************************************************
Private hHook As Integer = 0 'store our new hook handle.
Private FrmhWnd As IntPtr 'store parent form handle.

'*************************************************************************************
' Method : CntrMsgBox
' Description: Center a message box on a specified form using MsgBox syntax.
'*************************************************************************************
Public Function CntrMsgBox(ByRef ParentForm As Form,
ByVal TextMsg As String,
Optional ByRef Buttons As MsgBoxStyle = MsgBoxStyle.OkOnly Or MsgBoxStyle.Information,
Optional ByVal Title As String = vbNullString) As MsgBoxResult
' Set up the CBT hook.
FrmhWnd = ParentForm.Handle 'save the parent handle for use by ThreadProc.
Dim hInst As Integer = GetWindowLong(FrmhWnd, GWL_HINSTANCE) 'get the instance handle for our form.
Dim Thread As Integer = GetCurrentThreadId() 'get the current thread ID.
hHook = SetWindowsHookEx(WH_CBT, AddressOf ThreadProcCenter, hInst, Thread) 'insert new hook, and save its handle.

' Display the message box


Return MessageBox(FrmhWnd, TextMsg, Title, Buttons) 'by invoking message box, it will center and the process unhooked.
End Function

'*************************************************************************************
' ThreadProcCenter: Thread Message handler hook
'*************************************************************************************
Private Function ThreadProcCenter(ByVal iMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
' On HCBT_ACTIVATE, show the MsgBox centered over parent Form.
If iMsg = HCBT_ACTIVATE Then 'if our form receives this message, a SUBORDINATE form is about to be activated.
Dim rectForm, rectMsg As RECT 'storage for bounding rectangles for the invoking form and the message box.
' Get the coordinates of form and message box so we can determine where the center of the form is located.
GetWindowRect(FrmhWnd, rectForm) 'get parent bounding rectagle.
GetWindowRect(wParam, rectMsg) 'get msgbox bounding rectangle (wParam is windows Handle for MwgBox)

' now compute Top-Left position for msgbox to center it on the parent form.
Dim X As Integer = rectForm.iLeft + rectForm.Width \ 2 - rectMsg.Width \ 2 'point to parent center - 1/2 width.
Dim Y As Integer = rectForm.iTop + rectForm.Height \ 2 - rectMsg.Height \ 2 'point to parent center - 1/2 height.

' now Position the msgbox to the computed coordinates


SetWindowPos(wParam, 0, X, Y, 0, 0, SWP_NOSIZE Or SWP_NOZORDER Or SWP_NOACTIVATE)

' Release this WndProc hook because we now no longer need it.
UnhookWindowsHookEx(hHook)
hHook = 0
End If
Return 0 '0 allows an operation to continue to be processed (1 would prevent it).
End Function
End Module
 Page 324

Running Our Own VB.NET Free-Threading Background Tasks.


Apart from taking advantage of the operating system's threading, we can also create our own, and quite
easily. VB6 was not able to implement free-threading without great difficulty and expert understanding
of Threading processes. However, under VB.NET, free-threading is insanely E-Z to do!
Suppose we have a method that will perform some important task, but because it can take quite some
time to run, it seems to lock our application up, unless we went through the trouble of inserting periodic
Application.DoEvent invocations within it. Instead of doing something like that, we can employ the
System.Threading.Thread class and declare this method as a free-threaded process, so it will not
consume the application thread's time and not lock up any of its processes, and all without any DoEvent.
Suppose the method we want to run in the background on its own individual thread was named
GarbageCollect(). We could create a thread instance of it with just a simple instruction line like this:
Dim thrdGC As New System.Threading.Thread(AddressOf GarbageCollect)

Then, to actually activate and run this thread, we could use the following simple instruction:
thrdGC.Start 'start the thread. It will be active until the method terminates via Return or End Sub.

We are able to raise events from the thread so it can inform our application of its progress, or when it is
finished running. We can also declare properties within the thread method's class so the application can
check in its own time of the thread's status. Our application can also check the thread object's Boolean
IsAlive property to see if the thread is currently running.
Raising our own events is easy. First, we can define an event in the class containing our Thread method,
prior to a the declaration of the method that will invoke it, like this:
Event GCfinished(ByVal LabType As String) 'Done with background task. Note Parameters are optional, such as in simpler events.

In our Threading method, when the background task finishes, prior to exiting via Return or End Sub, we
can raise the above event using the RaiseEvent instruction:
RaiseEvent GCfinished(“We be done doin' our ding!”) 'let the app know it can now access our results.

Of course, we will also need to be able to respond to such a raised event. We can do this by declaring an
event handler method designed to react to it, just like forms and form controls react to whole lists of
events, such as a Click event for a Button Control. To do this, we can declare the class that will react to
the event, such as the class containing the Thread method, with a WithEvents clause. For example, if the
class that invoked the thrdGC thread was named clsEditor, we could add the following line in the
heading or footing of our class file:
' We can specify a different name instead of the class name, but the event handler would expect to work with the new name.
WithEvents clsEditor As clsEditor

We can then write a method that can react to this event. For example:
Public Sub clsEditor_GCfinished(ByVal Msg As System.String) Handles clsEditor.GCfinished
MsgBox(Msg) 'do whatever we need to do when the thrdGC thread announces that it has finished its task.
End Sub

NOTE: We can autogenerate the method wrapper via dropdown selections found at the top of the class code tab.
One caveat we must note is that our Thread method cannot have parameters. We can get around this
using global variables, though this is not very data-safe. Instead, we could define properties in the class
containing the Thread method, which we can set after declaring the Thread object, but before launching
it with the Thread object's Start method. Another caveat is that the method cannot return a value.
However, by defining a property or raising an event, we can accomplish this quite easily. Finally,
Synchronization can also be an issue if our application needs to use the results of the thread. In such
cases, flag properties should be employed, unless you are keen to writing code using Mutex objects
(Thread Synchronization objects), although the use of properties, raising events, and taking advantage
of the Thread's IsAlive property can be used to work around employing a Mutex.
 Page 325

E-Z Ways to Draw Lines and Shapes, and to Paint Under VB.NET.
Until VB2010, VB.NET did not ship with shape controls like those that VB6 featured, though the GDI+
2D graphics that came with VB.NET were a great substitute. Even so, the advantage of the VB6 shape
controls was that we could simply visually place lines, squares, and circles on a form and forget them.

First, News of Free VB.NET Line and Shape Controls.


Although shape controls did not ship with VB.NET until VB2010, Microsoft did offer free line and
shape controls for pre-2010 releases that allow placing lines and shapes on a form just as we did under
VB6. To that end, the Visual Basic Power Packs feature the controls RectangleShape, LineShape, and
OvalShape that can be used to replace VB6 Shape controls (if they were installed, the VB6 Upgrade
Wizard used them). In addition to duplicating the behavior of these VB6 controls, it added many new
features, such as gradient fills, run-time selection, and even run-time events. The Visual Basic Power
Packs are available online for pre-VB2010 editions for free, and without subscription restrictions, from
the Microsoft Developer Network (MSDN) at http://msdn.microsoft.com/en-us/vbasic/aa701257.aspx.
This is the pitch from Microsoft: “Microsoft Visual Basic Power Packs 3.0 now includes a new DataRepeater control.
The DataRepeater control enables us to use standard Windows Forms controls to display rows of our data in a scrollable
container, giving us more flexibility and customization than standard grid controls. This version of the Visual Basic Power
Packs also includes updated versions of the previously released Visual Basic Power Packs, Line and Shape controls, the
PrintForm Component and the Printer Compatibility Library. All are included in a single assembly, making them even easier
to use and redistribute with our application.”
Once installed, we have a new Toolbox group
called “Visual Basic PowerPacks”. From it, we
can just grab a control, drop it on our form, size it,
set parameters, and go on to the next task. To the
right I demonstrated using the LineShape control,
placing a horizontal line on a form.

Drawing Lines from the Paint Event.


But even with all the power offered by the Visual Basic Power Packs, I still would rather roll my own
graphics for the simple reason of significantly faster rendering, even as I did in VB6. Most developers
will cringe, be aghast and visibly shaken at the very idea of drawing lots of lines and shapes, some
making the Sign of the Cross to ward it off, or break out into hives, but it is actually quite easy. The hard
part, I am told, is calculating line endpoints. But for us, this will be the easiest part. To determine these
points is brain-dead simple; I maintain a PictureBox with its Visibility property set to False during the
development of my application named picTemp. For example, suppose I need to draw not just a line, but
what looks to be a 3D engraved line in the location shown for a line in the above image.
I move the top-left corner of picTemp to the start point, and then drag its bottom-right corner across the
screen by using its shape grabbers (the tiny boxes) to fine-tune its location. Consider this illustration:
As we can see, I started at the upper-left of the displayed
rectangle, and shaped it over the form.
If we look at the properties for this control, we will find its Location and Size
members. Under Location, the value 19 represents the Left pixel position, and 121
is the Top position for its top-left corner. In the Size properties, the only value I
need is its Width, which is set to 513.
I now have all the information I need in order to draw a simple line on my form.
But because I actually want to draw a line with a beveled 3D effect, I will actually
draw two lines. The second line will be rendered one pixel below the first with a
lighter color. Now, roll up your sleeves.
 Page 326

To draw these new lines, I will be performing this simple task from within my form’s Paint event.
In my form code, I select the form’s Events item in the left dropdown list at the top of the code page, and
then choose the Paint event from the right dropdown. This presents me with the following event block:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
End Sub

To draw, we come to the part a great many new VB.NET users assume is difficult. They want to draw
stuff, they might even know the components they need are in the System.Drawing namespace, but they are
not quite sure how to proceed to actually do a drawing. It does not help that most online gurus might
show code that does it, but usually they do not bother spending much time explaining what it is that they
did, or have meaningful comments (or any comments) in their code to help explain what they are doing
(I think some of them just cut and paste plagiarized code, not really understanding it; just claiming it!).
To draw, we need a hook into the form’s client area; its drawing surface. The Paint event, being a
veritable playground for drawing, gives us an interface to the form's GDI+ drawing capabilities through
its “e” parameter, which is noted as PaintEventArgs. For us, its most important member is e.Graphics
(basically, this is the form’s Device Context; its client area's drawing surface). This opens up a whole
universe of drawing capabilities, and we did not even have to set things up in the project properties or
import some spooky-sounding namespace. Wow! This sounds like it might even be easy. It is!
But first, to draw, we need something to draw with. For drawing lines, we need to use a Pen object.
Fortunately, creating a Pen is also very easy. We usually do it in one of three ways – depending on how
thick (how many pixels wide) we want the line to be. Consider these three examples:
Dim pn1 As New Pen(Color.Red) 'create a red pen that is 1 pixel thick (Use pn1.Dispose to release its resources when finished)
Dim pn2 As New Pen(Color.Blue, 3) 'create a blue pen that is 3 pixels thick (Use pn2.Dispose to release its resources when finished)
Dim pn3 As Pen = Pens.ForestGreen 'Use a 1-pixel thick pre-existing read-only system-resident pen (We CANNOT use pn3.Dispose on this)

All I need is a 1-pixel pen (even though the thickness parameter specifies a single-precision value, we can use
integers instead, thanks to method overloading these drawing functions provide). For an engraved effect, I require
two colors. I will use DarkGray and White. Although we could create separate pens for both, this is wasting
resources, especially because we should dispose of them after use if we declared them “As New”. Yet, we are
saved by the fact that most any object has properties. One Pen property is Color. Another is Width. Still another is
DashStyle, which allows us to go bug-nuts and draw with fancy patterns. Note, however, that we can only modify
an instantiated (“As New”) Pen. The default ready-to-use pens are immutable (read-only).
The only thing left to do is the means by which to draw our lines. Fortunately, the Graphics object offers us a
DrawLine method (among many). The DrawLine method expects 5 parameters: the Pen object and 4 values; the
X1 and Y1 offsets for the left end of the line, and the X2 and Y2 offsets for the right end of the line. In using
the Location and Size properties noted earlier for our non-visible PictureBox, we can specify the first X/Y
values quite easily: X1=19, Y1=121. The second are also easy; with the Width of the object, 513, we will declare
X2=19+513-1 (531), and Y2 will be set to the same as Y1; 121 (we will eliminate all note-taking, and we will
understand why there is no need for math reduction soon!). We next enter these lines to our form paint event:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
Dim pn As New Pen(Color.DarkGray, 1) 'create new pen set to Dark Gray (we could have just used 'Dim Pn As Pen = Pens.DarkGray' here).
e.Graphics.DrawLine(pn, 19, 121, 19+513-1, 121) 'draw the first line at Left, Top, Left+Width-1, Top. Compiler replaces 19+513-1 with 531!
pn.Color = Color.White 'set pen to the second color; White (we could have just used 'Pn = Pens.White' here).
e.Graphics.DrawLine(pn, 19, 122, 19+513-1, 122) 'draw the second line at same X coordinates, but 1 pixel down in Y (Y1+1).
pn.Dispose() 'dispose of resource-allocated Pen object (do not do this if you referenced the built-in Pens).
End Sub

NOTE: We can specify 19+513-1 rather than 531. The compiler will optimize constants to an integer value.
The result speaks for itself. Although the white, lower
line is actually rather subtle, its loss would be quite
apparent if it was missing, not to mention that the
“line” would no longer have an engraved appearance
as it does in the image to the right:
NOTE: Some developers prefer LightLightGray to White.
 Page 327

NOTE: As indicated in earlier examples, we did not really need to specify a new pen object; we could have used system-
provided pens, either by themselves (Pens.ForestGreen), or as variables (Dim pn As Pen = Pens.ForestGreen), which creates
a pointer to the read-only object. An advantage to using them is that we will not need to invoke a Dispose method to release
their resources. Yet, the advantage of using created Pen objects ( Dim pn As New Pen(Color.ForestGreen)) is that we can
alter its color, width, and DashStyle as needed. For the most part, if we are going to use 1-pixel-wide solid lines, the best and
fastest option is to employ the 1-pixel-wide read-only pens provided by the system, referenced in the Pens collection.
One of the overloads to the DrawLine method allows us to alternatively use a 1-pixel-wide Pens object
that is set to a color. Taking advantage of this, we can shorten the above code to just 2 lines.
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
e.Graphics.DrawLine(Pens.DarkGray, 19, 121, 19+513-1, 121) 'draw first line at X1, Y1, X2, Y2.
e.Graphics.DrawLine(Pens.White, 19, 122, 19+513-1, 122) 'draw second line at same X coordinates, but 1 pixel down in Y.
End Sub

Drawing vertical lines is just as easy. Just use the left or right vertical side of our temporary PictureBox
object as a target instead of a top or bottom horizontal line. To reiterate an earlier note, we can also leave
the offset math intact and the compiler will optimize it to an integer value for run-time execution.
Drawing Rectangles from the Paint Event.
Rectangles are even easier to draw than rendering individual lines. In fact, we can take full advantage of
the rectangle that our hidden temporary PictureBox, picTemp, occupies to make our work almost too
easy. We can benefit immensely from monitoring the values stored in this control’s Location (Top,left)
and Size (Width,Height) properties, because these contents are exactly what we must provide to a
rectangle to establish its top-left location and size.
For example, suppose we took our development picTemp PictureBox control (again, with its visibility
set to False) and set it up as a rectangle in the middle of a form. In my experiment, I noted that its
Location properties reported X=89 and Y=83. Its Size properties report Width=89 and Height=82. We
can jump right into our paint event and use these exact same values without change.
To draw a simple rectangle only requires a Pen and the above four values as parameters to the
DrawRectangle graphics method provided by e.Graphics. Consider the following code to draw a rectangle
on the form with a 1-pixel-wide border:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
e.Graphics.DrawRectangle(Pens.Red, 89, 83, 89, 82) 'use built-in Red pen, and the Location and Size parameter values.
End Sub

Now, was that easy, or what? But we are getting ready to make it a whole lot easier!
If we want a thicker border, we will of course have to declare a Pen along with a thickness value, which
we must be sure to Dispose of afterward. We can also declare the bounds of the target rectangle within a
Rectangle structure and then use that structure within a DrawRectangle overloaded method:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
Dim pn As New Pen(Color.Red, 3) 'declare a pen that is three pixels wide.
Dim rec As New Rectangle(89, 83, 89, 82) 'set up bounding rectangle.
e.Graphics.DrawRectangle(pn, rec) 'draw a rectangle using a Pen object and a Rectangle structure.
pn.Dispose() 'dispose of used object.
End Sub

What is more, we can greatly simplify this entire process by inserting temporary code that will take full advantage
of our hidden temporary picTemp PictureBox control, and it will even place the “finalized” declaration for the
rectangle (rec) structure in the IDE’s Debug Output:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
Dim pn As New Pen(Color.Red, 3) 'declare a pen that is three pixels wide.
'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G).
Debug.Print("'Prototype for target Rectangle declaration:")
Debug.Print("Dim rec As New Rectangle({0}, {1}, {2}, {3})", Me.picTemp.Location.X, Me.picTemp.Location.Y,
Me.picTemp.Size.Width, Me.picTemp.Size.Height)
Debug.Print(Nothing) 'add a blank line to the Immediate Window display.
Dim rec As New Rectangle(89, 83, 89, 82) 'set up bounding rectangle (replace this as needed by the debug result).
e.Graphics.DrawRectangle(pn, rec) 'draw a rectangle using a Pen object and a Rectangle structure.
pn.Dispose() 'dispose of created object.
End Sub
 Page 328

This will display the following helpful declaration in the Debug Output, which we can select, copy, and then paste
right into our own code, instead of manually transcribing it all the time as we had done up until now:
'Prototype for target Rectangle declaration:
Dim Rec As New Rectangle(89, 83, 89, 82)

What if we want a filled rectangle? That is as easy as spilling red wine on an expensive white linen tablecloth that
belongs to somebody else. For that, we simply invoke the FillRectangle method after invoking DrawRectangle:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
Dim pn As New Pen(Color.Red, 3) 'declare a pen that is three pixels wide.
'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G).
Debug.Print("'Prototype for target Rectangle declaration:")
Debug.Print("Dim rec As New Rectangle({0}, {1}, {2}, {3})", Me.picTemp.Location.X, Me.picTemp.Location.Y,
Me.picTemp.Size.Width, Me.picTemp.Size.Height)
Debug.Print(vbNullString) 'add a blank line to the Immediate Window display.
Dim rec As New Rectangle(89, 83, 89, 82) 'set up bounding rectangle (replace this as needed by the debug result)
e.Graphics.DrawRectangle(pn, rec) 'draw a rectangle using a Pen object and a Rectangle structure.
e.Graphics.FillRectangle(Brushes.Yellow, rec) 'fill the rectangle with yellow.
pn.Dispose() 'dispose of used object.
End Sub

Another advantage of using a Rectangle to specify bounds is we can make development easier and reduce the
time needed to test sizing the rectangle! One of the overloaded Rectangle methods allows us to specify a
Point and Size structure to indicate the start location and the size of the rectangle. Because the Location and
Size properties of our picTemp control are also Point and Size structures, we can specify them like so:
Dim rec As New Rectangle(Me.picTemp.Location, Me.picTemp.Size) 'set up testing bounding rectangle based on picTemp.

Using temporary code like this (replace “Dim rec” in the above code with it), we can fine-tune the shaping of
the rectangle and test the results immediately. Because I always name my hidden temporary PictureBox
“picTemp”, I keep the following block of code in a text file, which I paste into any form Paint event I am
working on when developing them for rendering user-drawn lines and shapes:
Dim rec As New Rectangle(Me.picTemp.Location, Me.picTemp.Size) 'set up bounding rectangle based on picTemp; 'disable in release code
Dim X1, Y1, X2, Y2 As Integer : X1 = rec.Left : X2 = X2 + rec.Width - 1 : Y1 = rec.Top : Y2 = Y1 + rec.Height – 1 'disable in release code
Debug.Print("'Prototype for Rectangle X1, Y1, X2, and Y2 declarations:") 'disable in release code
Debug.Print("Dim X1, Y1, X2, Y2 As Integer”) 'disable in release code
Debug.Print(”X1={0}: Y1={1}: X2={2}: Y2={3}", X1, Y1, X2, Y2) 'disable in release code
'-------------------------------------------------------------------------------------------------------------
Dim pn As New Pen(Color.Black, 1) 'adjust Pen color and width as needed.
'*************************************************************************************************************
'Disable the lines below you do not require for release code Paint event (disable ALL during initial testing)
'*************************************************************************************************************
'Dim rec As New Rectangle(X, Y, Width, Height) 'Runtime: manually modify run-time values (X, Y, Width, Height).
'e.Graphics.DrawLine(pn, rec.Location, CType(rec.Size, Point)) 'Runtime: rec using rectangle.
'e.Graphics.DrawLine(pn, X1, Y1, X2, Y2) 'Runtime: using absolutes (manually modify X1, Y1, X2, Y2).
'e.Graphics.DrawRectangle(pn, rec) 'Runtime: draw a rectangle.
'e.Graphics.DrawEllipse(pn, rec) 'Runtime: draw ellipse.
'-------------------------------------------------------------------------------------------------------------
' TESTING. Disable whole WITH block for Release code.
'-------------------------------------------------------------------------------------------------------------
With Me.picTemp
'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G).
Debug.Print("'Prototype for target Rectangle declaration:")
Debug.Print("Dim rec As New Rectangle({0}, {1}, {2}, {3})", .Location.X, .Location.Y, .Size.Width, .Size.Height)
Debug.Print("'Or... by specifying Location (X,Y) and Size (Width,Height):")
Debug.Print("Dim lc As New Point({0}, {1})", .Location.X, .Location.Y)
Debug.Print("Dim sz As New Size({0}, {1})", .Size.Width, .Size.Height)
Debug.Print("Dim rec As New Rectangle(lc, sz)")
Debug.Print(Nothing) 'add a blank line to the debug output display.
Dim ptH As New Point(.Location.X + .Size.Width - 1, .Location.Y) 'end location (right) of horizontal line.
Dim ptV As New Point(.Location.X, .Location.Y + .Size.Height - 1) 'end location (bottom) of vertical line.
'disable the following lines that are not needed for testing
e.Graphics.DrawLine(pn, .Location, ptH) 'Horizontal line (top line of picTemp).
e.Graphics.DrawLine(pn, .Location, ptV) 'Vertical line (left side of picTemp).
e.Graphics.DrawRectangle(pn, rec) 'Rectangle.
e.Graphics.DrawEllipse(pn, rec) 'Ellipse (circle).
End With
pn.Dispose() 'remove created resource.

The above produces the following Debug output:


'Prototype for Rectangle X1, Y1, X2, and Y2 declarations:
Dim X1, Y1, X2, Y2 As Integer
X1=12: Y1=129: X2=259: Y2=188
'Prototype for target Rectangle declaration:
Dim rec As New Rectangle(12, 129, 248, 60)
'Or... by specifying Location (X,Y) and Size (Width,Height):
Dim lc As New Point(12, 129)
Dim sz As New Size(248, 60)
Dim rec As New Rectangle(lc, sz)
 Page 329

Drawing Ellipses (Circular Shapes) from the Paint Event.


Drawing ellipses is just as easy, as we may have ascertained by examining the above code, noting the
obvious references to Ellipses. Now that we have played around with Rectangle structures to support
drawing rectangles, we can also take advantage of the rectangle structure to support drawing ellipses.
To draw an ellipse, we use the DrawEllipse method. And to fill it, we use the FillEllipse method. In fact,
once we understand drawing rectangles with the DrawRectangle method, using DrawEllipse to draw
ellipses is just as easy (a perfect circle is drawn when the width and height parameters are identical).
In fact, I can take the previous Paint event and change DrawRectangle to DrawEllipse, change FillRectangle
to FillElipse, and we will have just as suddenly mastered ellipses:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
Dim pn As New Pen(Color.Red, 3) 'declare a pen that is three pixels wide.
'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G).
Debug.Print("'Prototype for target Rectangle declaration:")
Debug.Print("Dim Rec As New Rectangle({0}, {1}, {2}, {3})", picTemp.Location.X, picTemp.Location.Y,
picTemp.Size.Width, picTemp.Size.Height)
Debug.Print(vbNullString) 'add a blank line to the Immediate Window ddisplay.
Dim rec As New Rectangle(89, 83, 89, 82) 'set up bounding rectangle.
e.Graphics.DrawEllipse(pn, rec) 'draw an elipse that will use the rectangle for its bounds
e.Graphics.FillEllipse(Brushes.Yellow, rec) 'fill the ellipse.
pn.Dispose() 'dispose of used object.
End Sub

Drawing Rounded Rectangles from the Paint Event.


For all the amazing power we have at hand under GDI+, and wished that we had under GDI32, we are
not able to draw straightforward Rounded Rectangles (some people in the machining industry like to
refer to these as Obrounds). Even the Visual Basic Power Packs features a CornerRadius property in its
RectangleShape that will enable rectangles to feature rounded corners; a corner radius value of 10
pixels is sufficient, though some prefer a larger value of up to 30. However, even this powerful package
has to submit the Rounded Rectangle shape by rendering 8 individual parts (4 sides plus 4 corners).
Although we might think Microsoft cockamamy to ignore Rounded Rectangles in GDI+, we should be
mindful that 1) GDI+ exploits the computer's graphics hardware code, which do not support Rounded
Rectangles, and 2) Rounded Rectangles eat time because such methods must render 4 lines and 4 90-
degree arcs to each corner. As such, we should use them sparingly, especially in process-heavy code.
Many programmers have made valiant attempts to solve this problem in VB-only code. For example,
consider the following typical online solution, with a tip of the hat to http://stackoverflow.com:
Public Sub DrawRoundedRectangle(ByVal g As Drawing.Graphics, ByVal r As Rectangle, ByVal d As Integer, ByVal p As Pen)
g.DrawArc(p, r.X, r.Y, d, d, 180, 90)
g.DrawLine(p, CInt(r.X + d / 2), r.Y, CInt(r.X + r.Width - d / 2), r.Y)
g.DrawArc(p, r.X + r.Width - d, r.Y, d, d, 270, 90)
g.DrawLine(p, r.X, CInt(r.Y + d / 2), r.X, CInt(r.Y + r.Height - d / 2))
g.DrawLine(p, CInt(r.X + r.Width), CInt(r.Y + d / 2), CInt(r.X + r.Width), CInt(r.Y + r.Height - d / 2))
g.DrawLine(p, CInt(r.X + d / 2), CInt(r.Y + r.Height), CInt(r.X + r.Width - d / 2), CInt(r.Y + r.Height))
g.DrawArc(p, r.X, r.Y + r.Height - d, d, d, 90, 90)
g.DrawArc(p, r.X + r.Width - d, r.Y + r.Height - d, d, d, 0, 90)
End Sub

And this is not even getting into the versions I have seen that fill-paint a rounded rectangle.
The above cryptic code, as most others, is not documented or commented (Sinners! Repent!). We are
often not even told how to use it, though it is not multi-stage booster rocket science to figure that out.
But the important point is – it works. I quickly slapped together the following Paint event solution that
employs the above method and my hidden picTemp control as a template for its shape.
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
Dim pn As New Pen(Color.Black, 3) 'adjust Pen color and width as needed.
With Me.picTemp
'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G).
Debug.Print("'Prototype for target Rectangle declaration:")
Debug.Print("Dim Rec As New Rectangle({0}, {1}, {2}, {3})", .Location.X, .Location.Y, .Size.Width, .Size.Height)
Debug.Print(vbNullString) 'add a blank line to the Immediate Window ddisplay.
Dim rec As New Rectangle(.Left, .Top, .Width, .Height) 'set up bounding rectangle using my picTemp helper frame.
 Page 330

End With
DrawRoundedRectangle(e.Graphics, rec, 15, pn) 'render rounded rectangle with a 15-pixel corner radius.
pn.Dispose() 'dispose of used object.
End Sub

Though it works, this solution, along with many others I found online, ran relatively slow in comparison
to the other solutions we have so far explored, but this is because there is so much drawing going on in
the background to produce the desired shape. All the solutions I found online did the same thing: they
drew, in all, 4 individual lines and 4 individual 90-degree arcs; cobbling together a rounded rectangle.
My own little adventure into VB-based rounded rectangle drawing uses less drawing instructions, taking
advantage of the GraphicsPath collection defined in the System.Drawing.Drawing2D Namespace. This
collection has become popular because it is very easy to add drawing instructions to it. Another great
thing about it is that in defining our drawing path, we do not need to add connecting lines between
plotting instructions. For example, if we add an arc for the top-left corner and next define an arc for the
top-right corner, the plotting function will draw a connecting line from the last drawing point to the start
of the next. If anyone is familiar with HP/GL (Hewlett-Packard Graphics Language), it is much like
creating plotter source code files, used for drawing blueprints and driving sheet metal cutting machines.
Having written drivers for graphic plotters and cutting machines, I know that most examples on the web
demonstrating the GraphicsPath collection typically use far too many instructions. It is not a disaster,
but it wastes more time. What follows is my VB.NET version of a DrawRoundedRectangle method:
'******************************************************************************
' Method : DrawRoundedRectangle
' Purpose : Quickly draw a rounded rectangle using a GraphicsPath.
'******************************************************************************
Public Sub DrawRoundedRectangle(ByRef g As Graphics,
ByRef Pen As Pen,
ByVal BaseRect As Rectangle,
ByVal CornerRadius As Integer)

Dim gPath As New Drawing2D.GraphicsPath 'create graphics path object.


With gPath
Dim CornerX2 As Int32 = CornerRadius << 2 'CornerRadius * 2 = diameter. Shift << is faster.
Dim CornerRect As New Rectangle(BaseRect.Location,
New Size(CornerX2, CornerX2)) 'Bounds for corner arcs.
.AddArc(CornerRect, 180, 90) 'add top-left arc and draw to...
CornerRect.X = BaseRect.Right - CornerRadius '(specify top-right quadrant).
.AddArc(CornerRect, 270, 90) 'top-right arc and draw to...
CornerRect.Y = BaseRect.Bottom - CornerRadius '(specify bottom-right quadrant).
.AddArc(CornerRect, 0, 90) 'bottom-right arc and draw to...
CornerRect.X = BaseRect.Left '(specify bottom-left quadrant).
.AddArc(CornerRect, 90, 90) 'bottom-left arc and draw to...
.CloseFigure() 'close the drawing from end to start.
End With
g.DrawPath(Pen, gPath) 'draw path using supplied pen.
gPath.Dispose() 'dispose of created object.
End Sub

To use it, we supply it with the graphics interface to the surface we are drawing to, a Pen, whether a
default pen or one such as “Dim Pen As New Pen(Color.Blue, 5)”, the rectangle definition, and lastly we
provide it with the radius of our corners, such as 10 pixels.
When we draw within a form's Paint event, we draw to the form's Graphics interface, not to its
BackgroundImage, and so the art will be non-persistent (a refresh would clear it), which is actually what
we want with form and control drawing within Paint events, because event processing will make those
drawings appear persistent. Drawing instead to Image objects, like the form's BackgroundImage, will be
persistent. We will demonstrate this in code very shortly, where we will be instantiating new Graphics
objects to perform persistent drawings from outside Paint events, thus freeing us from being stuck
rendering art from the safety net of their environs, allowing us to spread our wings and draw anywhere!
NOTE: Container controls, like Forms and PictureBoxes, seem to be non-persistent because their surfaces can change a lot,
and so they have a Paint event bound to them. This event fires when the message queue is idle, refreshing any invalidated
(changed) portions. This is why a refresh clears our drawing – we “invalidated” its device context and it will redraw over it!
 Page 331

'******************************************************************************
' Method : Form_Paint
' Purpose : Initialize background image and draw a rectangle on the form device context (display surface) only.
'******************************************************************************
Private Sub Form_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
With Me
Dim Pen As New Pen(Brushes.Blue, 5) 'pen to render with.
With .ClientRectangle 'draw within the form's client area.
Dim rec As New Rectangle(.X + 20, .Y + 20, .Width - 50, .Height – 50)
DrawRoundedRectangle(e.Graphics, Pen, rec, 10) 'draw rounded rectangle to form client area.
End With 'we are NOT drawing to the BackgroundImage.
Pen.Dispose() 'release created resources.
End With
End Sub

The above painting is non-persistent, as it should be for normal Paint event drawing services. For
persistence, we draw directly to an image object itself. Here is code demonstrating this from a form’s
Load event. Notice we point to the image's graphics interface rather than to its container's surface. We
normally do not want owner-drawn objects to be so persistent, but CAD utilities would require this,
which may need to save this image to a file or to be stored as a customized image in an application.
''********************************************************************************
' Method : Form_Load
' Purpose : Initialize background image and draw a persistent rectangle on it.
'******************************************************************************
Private Sub Form_Load(sender As Object, e As EventArgs) Handles Me.Load
With Me 'we will draw to the BackgroundImage, so...
Dim gi As Graphics 'graphics reference for BackgroundImage.
If .BackgroundImage Is Nothing Then 'if no image assigned to background...
.BackgroundImage = New Bitmap(.ClientRectangle.Width, .ClientRectangle.Height) 'create canvas to draw onto.
gi = Graphics.FromImage(.BackgroundImage) 'create Graphics interface to BackgroundImage.
gi.Clear(.BackColor) 'fill canvas with background color (init image).
Else
gi = Graphics.FromImage(.BackgroundImage) 'create Graphics interface to BackgroundImage.
End If
Dim Pen As New Pen(Brushes.Blue, 5) 'pen to render with.
With .ClientRectangle 'draw to image in the form's client area.
Dim rec As New Rectangle(.X + 20, .Y + 20, .Width - 50, .Height - 50)
DrawRoundedRectangle(gi, Pen, rec, 10) 'draw rounded rectangle to BackgroundImage.
End With
Pen.Dispose() 'release created resources.
gi.Dispose() 'ALWAYS remember to release CREATED, but not borrowed Graphics objects!
End With
End Sub

However, the Windows Operating System also delivers filled Rounded Rectangle support through a
RoundRect Pinvoke, found in GDI32.DLL, and it works several times faster than other solutions, such as
the above DrawRoundedRectangle methods. Consider the following root declaration:
' Draw a rounded Rectangle. Return non-zero for success. (Background filled with current system brush color)
Private Declare Function RoundRect Lib "gdi32.dll" Alias "RoundRect" (ByVal hDC As IntPtr,
ByVal X1 As Integer,
ByVal Y1 As Integer,
ByVal X2 As Integer,
ByVal Y2 As Integer,
ByVal CornerWidth As Integer,
ByVal CornerHeight As Integer) As Boolean

NOTE: GDI32.DLL is the base Graphical Display Interface for Win32 that has served us so faithfully since Microsoft
Windows began Win32 support. VB.NET uses a brand new GDI; GDIplus.DLL, which also provides support for Win64 .
GDI.DLL, the first incarnation, provided GDI support for Win16 operating systems up through Windows 3.x.
The hDC integer is a handle to the Device Context of a drawing surface. X1,Y1 is the top-left corner of
the rectangle on the surface, and X2,Y2 is the bottom-right corner. The CornerWidth and CornerHeight
are the dimensions of a bounding rectangle for rendering all four corners of the Rounded Rectangle.
In a Paint event, we get an hDC (a handle, or system reference, to the DC) to the drawing surface using
e.Graphics.GetHdc. If we are drawing this somewhere else, like a subroutine, we get it by creating a
temporary Graphics interface for the image, draw to it, and then dispose of that created object.
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
Dim hDC As IntPtr = Graphics.FromImage(Me.BackgroundImage).GetHdc 'Get an instance of the image's hDC.
'Dim hDC As IntPtr = Me.Graphics.GetHdc 'OR, get an instance of the form's hDC.
'...Drawing code goes here...
Graphics.ReleaseHdc(hDC) 'release local instance of device context (IMPORTANT!!!)
End Sub
 Page 332

Of course, to use the above Pinvoke, we need to know about a Device Context, which is an interface to a
drawing surface, and the process of selecting and resetting Pens and Brushes (a Pen draws the border of
a shape and a Brush paint-fills its background). Also, the Pen and Brush are selected and reset by
invoking the SelectObject Pinvoke. We may require the use of the GetStockObject Pinvoke if we want to
select a transparent Brush (NULL_BRUSH constant), and we will also need the CreatePen and
CreateSolidBrush Pinvokes to obtain the Windows Handles of the needed Pen and Brush objects, which in
turn require a PS_SOLID constant. And since we are creating (allocating) resources, we will also need the
DeleteObject Pinvoke to responsibly release them. Moreover, as we can also see in the above RoundRect
Pinvoke, there is no CornerRadius parameter featured, although there are two others that seem to be
related to it: CornerWidth and CornerHeight. These two parameters define a bounding box for
rendering each of its rounded corners. If we make them both of equal value, then this effectively
becomes a Corner Radius (but x2) value. As such, most developers who use this method generally set
both of these parameters to the same value; which are actually twice the horizontal and vertical radius
(the bounding rectangle must encompass the width and hight of a full ellipse/circle, of which only one
quarter of it will actually be painted to display a needed rounded corner image).
Although it does not seem apparent because the Pinvoke requires starting and end points (X1,Y1 and
X2,Y2), we can in fact easily use the Location and Size members used by a Rectangle structure. This is
because the end point actually points one pixel beyond the drawing when we are working at the Pinvoke
Level, establishing a “draw-until-encountered” limit. As such, we will be able to add the Location.X and
Size.Width values together, and the Location.Y and Size.Height values together in order to compute the
needed “draw-until” endpoint without having to additionally subtract a 1 from each.
Fortunately, we need only think our way through this whole process just once. We will write a reusable
method that we can invoke any other time we need it in any of our future projects.
At the brass tacks level, we really do not need to know very much about the required Device Context,
but we do need access to its Handle. Fortunately, all Graphics objects sport a GetHdc property, providing
an IntPtr to the Device Context; a .NET Int32 Integer that is typically used as a system pointer.
The additional Pinvoke requirements can be neatly collected into a block like the following (we will
ignore the NULL_BRUSH stock object, because Color.Transparent can actually render that for us):
Private Const PS_SOLID As Integer = 0 'Pen Style for solid pen (used by CreatePen())
' create a solid pen. Returns its handle.
Private Declare Function CreatePen Lib "gdi32.dll" Alias "CreatePen" (ByVal nPenStyle As Integer,
ByVal nPixelWidth As Integer,
ByVal crColor As Integer) As Integer
' create a solid brush object. Returns its handle.
Private Declare Function CreateSolidBrush Lib "gdi32.dll" Alias "CreateSolidBrush" (ByVal crColor As Integer) As Integer
' Select/replace an object through its handle. Return the old handle for the type of object.
Private Declare Function SelectObject Lib "gdi32.dll" Alias "SelectObject" (ByVal hDC As IntPtr,
ByVal hObject As Integer) As Integer
'release the resources of a created object through their handle.
Private Declare Function DeleteObject Lib "gdi32.dll" Alias "DeleteObject" (ByVal hObject As Integer) As Integer

The only thing that may look tricky to someone first looking at the above declarations is the color
values, which are declared as Integers (Int32, or DWORD in C parlance). A .NET Color value, although
32-bit, is not directly compatible to an Integer (we cannot cast it either through Cint, CType or
DirectCast). A Color value is a special ARGB value. However, a Color object fortunately has a member
property called ToArgb that will return a 32-bit Integer to us.
NOTE: The Alpha component of an ARGB value is an UInt8 (Unsigned Byte) Alpha Blend value. Typically it has a value of
255, meaning that the color is fully opaque (solid). A value of 0 indicates this color is fully transparent.
Unfortunately, the color values expected by our Pinvokes require colors formatted to RGB (note that
this has nothing to do with VB.NET’s Argb method). ARGB and RGB might appear similar, but they are
not. Internally, ARGB is formatted AARRGGBB; (Alpha, Red, Green, Blue), where each letter
represents a 4-bit hexadecimal (0-15; 0-F) value, and each two hex values form an 8-bit BYTE value,
 Page 333

able to store values from 0 through 255 (00-FF hex). Yet, RGB colors are formatted 00BBGGRR;
(00&H, Blue, Green, Red), so RGB is stored in reverse order from the way it is ‘advertised’.
Luckily, this is old hat conversion that I have been doing since my original Assembly Language days,
and is very easy to resolve with a little helper function I name ARGBtoRGB:
Private Function ARGBtoRGB(ByVal clr As Color) As Integer
Dim vARGB As Integer = clr.ToArgb 'convert color value to AARRGGBB.
Return RGB((vARGB >> 16) And &HFF, (vARGB >> 8) And &HFF, vARGB And &HFF) 'return RGB color (00BBGGRR).
End Function

With this, we now have all the tools we need to render super-fast Rounded Rectangles.
When we specify a pen or a brush, we must be mindful that we are replacing a pen or a brush that the
current device context may be using. As such, we need to store their handles away so we can restore
them (if we will be in fact replacing them, then we should delete the old ones using the DeleteObject
method, but we should never just ignore them or casually toss them away – this is how memory leaks
start). Thus, we will set aside two variables to store these Integer handles. We then must declare our own
Pen and Brush. The next thing is to select them and store the old Pen and Brush handles away for later
recovery. Finally, we actually draw our Rounded Rectangle. After that, we need to clean up, so we
restore the old Pen and Brush, and then we release the resources of our locally created Pen and Brush.
After all this theory, the code is hardly worth all the trouble I just went through to describe it. What
follows is the complete module, which I named modRoundRect :
Module modRoundRect
'*************************************************************************************************************
'RoundRectangle (hDC, Pn, BackColor, Rect, CornerRadius):
' Draw a Rounded Rectangle, provided:
' hDC : The Device Context handle from the graphics object or device to render to.
' Pn : The pen used to draw the border of the shape.
' BackColor : The color to draw the background of the shape (You can use Color.Transparent for NO FILL).
' Rect : Rectangle structure containing the start location (X,Y), Width, and Height of the shape in pixels.
' CornerRadius: The radius of the rounded corner in pixels.
'*************************************************************************************************************
' Pinvoke Stuff
'*************************************************************************************************************
' solid pen constant.
Private Const PS_SOLID As Integer = 0 'Pen Style for solid pen (used by CreatePen()).
' create a solid pen. Returns its handle.
Private Declare Function CreatePen Lib "gdi32.dll" Alias "CreatePen" (ByVal nPenStyle As Integer,
ByVal nPixelWidth As Integer,
ByVal crColor As Integer) As Integer
' create a solid brush object. Returns its handle.
Private Declare Function CreateSolidBrush Lib "gdi32.dll" Alias "CreateSolidBrush" (ByVal crColor As Integer) As Integer
' Select/replace an object through its handle. Return the old handle for the type of object. The integers are REALLY IntPtrs, but...
Private Declare Function SelectObject Lib "gdi32.dll" Alias "SelectObject" (ByVal hDC As IntPtr,
ByVal hObject As Integer) As Integer
' release the resources of a created object through their handle.
Private Declare Function DeleteObject Lib "gdi32" Alias "DeleteObject" (ByVal hObject As Integer) As Integer
' Draw a rounded Rectangle. Return non-zero for success.
Private Declare Function RoundRect Lib "gdi32.dll" Alias "RoundRect" (ByVal hDC As IntPtr,
ByVal X1 As Integer,
ByVal Y1 As Integer,
ByVal X2 As Integer,
ByVal Y2 As Integer,
ByVal CornerWidth As Integer,
ByVal CornerHeight As Integer) As Boolean

'*************************************************************************************************************
'RoundRectangle Method
'*************************************************************************************************************
Public Sub RoundRectangle(ByVal hDC As IntPtr,
ByVal Pn As Pen,
ByVal BackColor As Color,
ByVal rect As Rectangle,
ByVal CornerRadius As Integer)
Dim oldhPen As Integer 'store old Pen handle 'NOTE: we can remove the code from HERE to End Sub and invoke an overload,
Dim oldhBrush As Integer 'store old Brush handle 'listed soon, replacing all the code in this method with:
'RoundRectangle(hDC, Pn.Color, CInt(Pn.Width), BackColor, rect, CornerRadius)
' Declare our local Pen and Brush
Dim hPen As Integer = CreatePen(PS_SOLID, CInt(Pn.Width), ARGBtoRGB(Pn.Color)) 'create our Pen and save its handle.
Dim hBrush As Integer = CreateSolidBrush(ARGBtoRGB(BackColor)) 'create our color brush and save its handle.

' select our Pen and Brush into current use and save the ones we are replacing.
oldhPen = SelectObject(hDC, hPen) 'select new Pen and return and save old Pen.
oldhBrush = SelectObject(hDC, hBrush) 'select new Brush and return and save old Brush.

' draw rounded rectangle


RoundRect(hDC,
 Page 334

rect.Location.X,
rect.Location.Y,
rect.Location.X + rect.Size.Width,
rect.Location.Y + rect.Size.Height,
CornerRadius * 2, CornerRadius * 2) 'Corner bound rectangle is twice the radius on both sides.

' next, restore the old Pen and Brush and release our returned Pen and Brush resources.
DeleteObject(SelectObject(hDC, oldhPen)) 'reselect old Pen and delete our pen object.
DeleteObject(SelectObject(hDC, oldhBrush)} 'reselect old Brush and delete our brush object.
End Sub 'do NOT delete hDC because we borrowed it, not created it!

'*************************************************************************************************************
'ARGBtoRGB Method
'Helper function to covert Color ARGB value (AARRGGBB) to RGB (00BBGGRR)
'*************************************************************************************************************
Private Function ARGBtoRGB(ByVal clr As Color) As Integer
Dim vARGB As Integer = clr.ToArgb 'convert color value to AARRGGBB.
Return RGB((vARGB >> 16) And &HFF, (vARGB >> 8) And &HFF, vARGB And &HFF) 'return RGB color (00BBGGRR).
End Function

End Module

We can invoke our new Win32 GDI RoundRect method like this:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
Dim pn As New Pen(Color.Red, 1) 'adjust Pen color and width as needed.
With Me.ClientRectangle 'draw within the form's client area.
Dim rec As New Rectangle(.X + 20, .Y + 20, .Width - 50, .Height – 50) 'set up bounding rectangle.
RoundRectangle(e.Graphics.GetHdc, pn, Color.White, rec, 15) 'render Rounded Rectangle w/CR of 15 pixels.
End With 'we are NOT drawing to the BackgroundImage.
pn.Dispose() 'remove used resources.
End Sub

NOTE: Remember that persistence all depends on where we draw to, either to a surface, such as the Form or a PictureBox,
or to the Form BackgroundImage or the PictureBox Image.
We may want to create an additional overloaded version of the RoundRectangle method. A suggested
method would allow Color and Width parameters in place of the Pen object: In fact, we could gut the
previous method and just turn around and invoke this method with its Pen Color and Width values,
Public Sub RoundRectangle(ByVal hDC As IntPtr,
ByValPn As Color,
BtValPnWidth As Integer.
ByValBackColor As Color,
ByValrect As Rectangle,
ByValCornerRadius As Integer) 'NOTE: You could take the other overload and simply invoke this method, passing
Dim oldhPen As Integer 'store old Pen handle ' this method the color value from its Pen parameter, making that method
Dim oldhBrush As Integer 'store old Brush handle ' VERY SMALL because it would consume the code from this method.
' A GREAT example of how overloads can save a LOT of compiled code space!
'Declare our local Pen and Brush
Dim hPen As Integer = CreatePen(PS_SOLID, PnWidth, ARGBtoRGB(Pn)) 'create our Pen and save its handle.
Dim hBrush As Integer = CreateSolidBrush(ARGBtoRGB(BackColor)) 'create our color brush.

'select our Pen and Brush into current use and save the ones we are replacing.
oldhPen = SelectObject(hDC, hPen) 'select new Pen and return and save old Pen.
oldhBrush = SelectObject(hDC, hBrush) 'select new Brush and return and save old Brush.

'draw rounded rectangle.


RoundRect(hDC,
rect.Location.X,
rect.Location.Y,
rect.Location.X + rect.Size.Width,
rect.Location.Y + rect.Size.Height,
CornerRadius * 2, CornerRadius * 2) 'Corner bound rectangle is twice the radius on both sides.

'next, restore the old Pen and Brush and release our returned Pen and Brush resources.
DeleteObject(SelectObject(hDC, oldhPen)) 'reselect old Pen and delete our pen object.
DeleteObject(SelectObject(hDC, oldhBrush)} 'reselect old Brush and delete our brush object.
End Sub 'do NOT delete hDC because we borrowed it, not created it!

Drawing Other Shapes from the Paint Event.


By exploring the other features offered by the Graphics object, such as DrawPie, DrawArc, DrawBeziers, DrawCurve,
DrawPolygon, and the impressive number of other drawing features offered, and noting the provided tool tips, the rest
of these functions are child’s play. Pay attention to all the tooltips that pop up. I hear people constantly complaining
about them... as they ask me how to do something. The reason I know how to do all these things is because I paid
attention to the tooltips.
 Page 335

Painting Backgrounds with HatchBrush Patterns from the Paint Event.


We can paint (fill) our background with more than just plain-vanilla solid-color brushes. We can
alternatively use special patterns through a HatchBrush, in place of a solid brush, which is an enhanced,
but fully Brush-compatible object. We can even specify them, or regular brushes, with Pinvoke paint
methods. All GDI+ 2D (flat) VB.NET Brushes can be found within the System.Drawing.Drawing2d
namespace. A HatchBrush features a ForeColor property, which specifies the Pen color to draw a
selected pattern onto the background, and a BackColor property, which is the actual Brush color to paint
the background field. The most intriguing property of a HatchBrush is probably its HatchStyle, through
which we can select its fill pattern. Consider the following table of possible GDI+ HatchBrush patterns.
NOTE: GDI+ HatchBrush styles differ slightly from Win32 HatchBrush Styles, with GDI+ offering more styles.
Hatchstyle Pattern Description
Horizontal A pattern of horizontal lines.
Vertical A pattern of vertical lines.
ForwardDiagonal A pattern of lines on a diagonal from upper left to lower right.
BackwardDiagonal A pattern of lines on a diagonal from upper right to lower left.
Cross Specifies horizontal and vertical lines that cross.
DiagonalCross A pattern of crisscross diagonal lines.
Percent05 Specifies a 5-percent hatch. The ratio of foreground color to background color is 5:100.
Percent10 Specifies a 10-percent hatch. The ratio of foreground color to background color is 10:100.
Percent20 Specifies a 20-percent hatch. The ratio of foreground color to background color is 20:100.
Percent25 Specifies a 25-percent hatch. The ratio of foreground color to background color is 25:100.
Percent30 Specifies a 30-percent hatch. The ratio of foreground color to background color is 30:100.
Percent40 Specifies a 40-percent hatch. The ratio of foreground color to background color is 40:100.
Percent50 Specifies a 50-percent hatch. The ratio of foreground color to background color is 50:100.
Percent60 Specifies a 60-percent hatch. The ratio of foreground color to background color is 60:100.
Percent75 Specifies a 75-percent hatch. The ratio of foreground color to background color is 75:100.
Percent80 Specifies a 80-percent hatch. The ratio of foreground color to background color is 80:100.
Percent90 Specifies a 90-percent hatch. The ratio of foreground color to background color is 90:100.
LightDownwardDiagonal Specifies diagonal lines that slant to the right from top points to bottom points and are spaced 50
percent closer together than ForwardDiagonal, but are not antialiased.
DarkDownwardDiagonal Specifies diagonal lines that slant to the right from top points to bottom points, are spaced 50 percent
closer together than, and are twice the width of ForwardDiagonal. This hatch pattern is not antialiased.
DarkUpwardDiagonal Specifies diagonal lines that slant to the left from top points to bottom points, are spaced 50 percent
closer together than BackwardDiagonal, and are twice its width, but the lines are not antialiased.
WideDownwardDiagonal Specifies diagonal lines that slant to the right from top points to bottom points, have the same spacing
as hatch style ForwardDiagonal, and are triple its width, but are not antialiased.
WideUpwardDiagonal Specifies diagonal lines that slant to the left from top points to bottom points, have the same spacing
as hatch style BackwardDiagonal, and are triple its width, but are not antialiased.
LightVertical Specifies vertical lines that are spaced 50 percent closer together than Vertical.
LightHorizontal Specifies horizontal lines that are spaced 50 percent closer together than Horizontal.
NarrowVertical Specifies vertical lines that are spaced 75 percent closer together than hatch style Vertical (or 25
percent closer together than LightVertical).
NarrowHorizontal Specifies horizontal lines that are spaced 75 percent closer together than hatch style Horizontal (or 25
percent closer together than LightHorizontal).
DarkVertical Specifies vertical lines that are spaced 50 percent closer together than Vertical and are twice its width.
DarkHorizontal Specifies horizontal lines that are spaced 50 percent closer together than Horizontal and are twice the
width of Horizontal.
DashedDownwardDiagonal Specifies dashed diagonal lines, that slant to the right from top points to bottom points.
DashedUpwardDiagonal Specifies dashed diagonal lines, that slant to the left from top points to bottom points.
DashedHorizontal Specifies dashed horizontal lines.
DashedVertical Specifies dashed vertical lines.
SmallConfetti Specifies a hatch that has the appearance of confetti.
LargeConfetti Specifies a hatch that has the appearance of confetti, and is composed of larger pieces than
SmallConfetti.
ZigZag Specifies horizontal lines that are composed of zigzags.
Wave Specifies horizontal lines that are composed of tildes.
DiagonalBrick Specifies a hatch that has the appearance of layered bricks that slant to the left from top points to
bottom points.
HorizontalBrick Specifies a hatch that has the appearance of horizontally layered bricks.
Weave Specifies a hatch that has the appearance of a woven material.
 Page 336

Hatchstyle Pattern Description


Plaid Specifies a hatch that has the appearance of a plaid material.
Divot Specifies a hatch that has the appearance of divots.
DottedGrid Specifies horizontal and vertical lines, each of which is composed of dots, that cross.
DottedDiamond Specifies forward diagonal and backward diagonal lines, each of which is composed of dots, that
cross.
Shingle Specifies a hatch that has the appearance of diagonally layered shingles that slant to the right from top
points to bottom points.
Trellis Specifies a hatch that has the appearance of a trellis.
Sphere Specifies a hatch that has the appearance of spheres laid adjacent to one another.
SmallGrid Specifies horizontal and vertical lines that cross and are spaced 50 percent closer together than hatch
style Cross.
SmallCheckerBoard Specifies a hatch that has the appearance of a checkerboard.
LargeCheckerBoard Specifies a hatch that has the appearance of a checkerboard with squares that are twice the size of
SmallCheckerBoard.
OutlinedDiamond Specifies forward diagonal and backward diagonal lines that cross but are not antialiased.
SolidDiamond Specifies a hatch that has the appearance of a checkerboard placed diagonally.
LargeGrid Specifies the hatch style Cross.
Min Specifies hatch style Horizontal.
Max Specifies hatch style SolidDiamond.

To take advantage of these many styles, we need to declare them as a new HatchBrush, which we can
use in place of any standard brush, even for GDI+ Pinvokes! (We can also create a Win32 Hatchbrush.
See Restoring Raster Graphics and Rubber Bands to .NET on page 345 to see how).
Dim Brsh As Brush = New Drawing2D.HatchBrush(Drawing2D.HatchStyle.DiagonalBrick, Color.Black, Color.Yellow)

In this example, we have declared a new HatchBrush that is using a diagonal brick style, which slants to
the left from top points to bottom points. The pattern is drawn in Black over a Yellow background.
To use the new brush, we use it in place of the standard brush within our Fill method, such as
FillRectangle or FillEllipses. For example:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
'Declare the HatchBrush using a DiagonalBrick pattern, drawn with black over a yellow background.
Dim Brsh As Brush = New Drawing2D.HatchBrush(Drawing2D.HatchStyle.DiagonalBrick, Color.Black, Color.Yellow)
'flood fill the form background. Do not bother with drawing a rectangle. No need for it here.
e.Graphics.FillRectangle(Brsh, Me.ClientRectangle)
Brsh.Dispose()'dispose of created HatchBrush.
End Sub

Painting Flood Fills from the Paint Event.


Sometimes we have to render shapes, such as stars, curves, triangles, and other such non-rectangular
imaging, and we need to paint their backgrounds with a color, or we simply want to fill in the border that
surrounds a number of shapes with a particular color, or we want to fill the shape itself.
There are two principle conditions we face where we need to paint a portion of a surface with a color:
1. We want to cover all colors except for a color delimiter that is referred to as a Border Color. The border color will
stop painting from spilling over this border as long as the border is sealed (unbroken).
2. We want to paint over a specific colored area, but we want to leave all other colors alone. For example, we want to
replace an area filled with one color with another. All other colors will be treated as though they are border colors.

GDI FloodFill.
Although VB6 and VB.NET do not have a built-in solution to this issue, they do have easy access to the
Operating System. The above two criteria can be easily met by the Win32 ExtFloodFill Pinvoke found in
GDI32.DLL. This GDI method is able to paint to any device that has a Windows Handle and a graphical
surface; a Device Context (anything a Paint event can draw to has a DC; a Graphics object). VB.NET
controls, like a Button, PictureBox, Form, printer, and even the screen meet muster here.
 Page 337

Here is the declaration for GDI’s ExtFloodFill method:


Private Declare Function ExtFloodFill Lib "gdi32.dll" (ByVal hDC As IntPtr,
ByVal X As Integer,
ByVal Y As Integer,
ByVal rgbColor As Integer,
ByVal wFillType As Integer) As Boolean
Private Const FLOODFILLBORDER As Integer = 0 'wFillType constant to fill to border (and to DC limits).
Private Const FLOODFILLSURFACE As Integer = 1 'wFillType constant to flood the surface of a specific color with another color.

When hDC holds the handle to the Device Context of an object, X and Y point to a relative pixel location
on that object, rgbColor is set to an RGB color value, and wFillType is set to a fill type constant, this
method can instantly paint-fill a large swath of territory using a Brush set to the RGB color.
When the FLOODFILLBORDER constant is used as a fill type, then the border color is set to the
rgbColor parameter, and the surface begins being painted by the color set in the current system Brush
(which we will also have to set), from the X,Y coordinate outward in all directions, expansion being
contained by the border color being encountered, or the extent of the Device Context is reached.
When the FLOODFILLSURFACE constant is used, then the color to be over-painted is set to the
rgbColor parameter and the surface begins being painted by the color set to the current system Brush,
until that color is blocked by any other color, or the extent of the Device Context is reached. We must
also think about a Brush to paint the background with, to fill it. We can create it using the CreateSolidBrush
Pinvoke, which returns its integer (technically IntPtr) handle. We also need to select the Brush into the
system so we can paint with it. This is solved by the SelectObject Pinvoke. This Pinvoke also returns the old
Brush handle, which we will want to save, because we need to restore it when we are finished. Finally, we
will need to delete the resources we created our Brush with. This is solved by the DeleteObject Pinvoke. All
these additionally required references can be handled by this small block of declaration code:
' Get the RGB color stored at the specified point in the selected Device Context.
Private Declare Function GetPixel Lib "gdi32.dll" (ByVal hDC As IntPtr, ByVal X As Integer, ByVal Y As Integer) As Integer
' Create a solid-color Brush using an RGB color. Return its handle.
Private Declare Function CreateSolidBrush Lib "gdi32.dll" (ByVal crColor As Integer) As IntPtr
' Select a new object, return the old one.
Private Declare Function SelectObject Lib "gdi32.dll" (ByVal hDC As IntPtr, ByVal hObject As IntPtr) As IntPtr
' Delete resources of an object.
Private Declare Function DeleteObject Lib "gdi32.dll" (ByVal hObject As IntPtr) As Boolean

Following is my module to perform GDI Flood Fills. It operates extremely fast and is easy to use:
Module modGdiFloodFill
'Paint a region of a picturebox, form, or printer with a color.
'******************************************************************************
' modGdiFloodfill - The GdiFloodfill() function will paint a region of any object with a DC with a selected color.
' X and Y coordinates define the starting location. If the border Color is Transparent, The color
' selected will be painted, flooding out in all directions as long as the color originally found at
' X and Y is found. If the BorderClr value is set, then the Clr value is filled on the surface
' until a border of BorderClr is encountered, or the DC limits are met.
'EXAMPLE:
''do a flood fill at the point of the mouse click, but only if the Cntrl key is ALSO held down.
'Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
' If e.Button = Windows.Forms.MouseButtons.Left AndAlso Control.ModifierKeys = Keys.Control Then
' GdiFloodFill(Me.Handle, e.X, e.Y, Color.White)
' End If
'End Sub

'******************************************************************************
' Pinvoke stuff
'******************************************************************************
' Releases a device context (DC), freeing it for use by other applications.
Private Declare Function ReleaseDC Lib "user32.DLL" Alias "ReleaseDC" (ByVal hwnd As IntPtr, ByVal hDC As IntPtr) As Boolean
' Get the RGB color stored at the specified point in the selected Device Context.
Private Declare Function GetPixel Lib "gdi32.dll" (ByVal hDC As IntPtr, ByVal X As Integer, ByVal Y As Integer) As Integer
' Create a solid-color Brush using an RGB color. Return its handle.
Private Declare Function CreateSolidBrush Lib "gdi32.dll" (ByVal crColor As Integer) As IntPtr
' Select a new object, return the old one.
Private Declare Function SelectObject Lib "gdi32.dll" (ByVal hDC As IntPtr, ByVal hObject As IntPtr) As IntPtr
' Delete resources of an object.
Private Declare Function DeleteObject Lib "gdi32.dll" (ByVal hObject As IntPtr) As Boolean
' Perform Flood Fill.
Private Declare Function ExtFloodFill Lib "gdi32.dll" (ByVal hDC As IntPtr,
ByVal X As Integer,
ByVal Y As Integer,
ByVal rgbColor As Integer,
 Page 338

ByVal wFillType As Integer) As Boolean


' wFillType constant to fill to border (and to DC limits).
Private Const FLOODFILLBORDER As Integer = 0
' wFillType constant to flood the surface of a specific color with another color.
Private Const FLOODFILLSURFACE As Integer = 1

'******************************************************************************
'GdiFloodFill
'******************************************************************************
Public Function GdiFloodFill(ByVal g As Graphics,
ByVal X As Integer,
ByVal Y As Integer,
ByVal BrushClr As Color,
Optional ByVal BorderClr As Color = Nothing) As Boolean
Dim FillStyle As Integer 'storage for fillstyle.
Dim iClr As Integer 'color to send to ExtFloodFill().

Dim hDC As IntPtr = g.GetHdc() 'get DC handle for window.


If hDC = IntPtr.Zero Then 'does the object have a Device Context?
Return False 'no, so we cannot do anything.
End If
Dim OrgClr As Integer = GetPixel(hDC, X, Y) 'get RGB color at X/Y point on DC.
Dim Brsh As Integer = ARGBtoRGB(BrushClr) 'get RGB rendering of new brush color from its ARGB format.
Dim Brdr As Integer = ARGBtoRGB(BorderClr) 'get RGB border color from its ARGB format.
If BorderClr = Nothing OrElse
BorderClr = Color.Transparent Then 'if border is Nothing or else it is Transparent...
If OrgClr = Brsh Then 'if the X,Y point's color is already the brush's color...
Return False 'then there is no need to continue.
End If
iClr = OrgClr 'else set target color to the original Color value.
FillStyle = FLOODFILLSURFACE 'and fill with new color while OrgClr found or DC limits.
Else
If OrgClr = Brdr Then 'X,Y point color is the same as border color?
Return False 'yes, so nothing to do because we are on a border.
End If
iClr = Brdr 'else set border color.
FillStyle = FLOODFILLBORDER 'fill any color with Clr up to the border or DC limits.
End If
Dim NewBrush As IntPtr = CreateSolidBrush(Brsh) 'define a color brush.
Dim OldBrush As IntPtr = SelectObject(hDC, NewBrush) 'set new brush, save old.
ExtFloodFill(hDC, X, Y, iClr, FillStyle) 'flood fill surface with brush color, starting at X,Y.
SelectObject(hDC, OldBrush) 'restore original brush.
DeleteObject(NewBrush) 'destroy resourced foor now-detached new brush.
g.ReleaseHdc(hDC) 'release device context.
Return True 'report success.
End Function

'*************************************************************************************************************
'ARGBtoRGB Method
' Helper function to convert Color ARGB value (AARRGGBB) to RGB (00BBGGRR).
'*************************************************************************************************************
Private Function ARGBtoRGB(ByVal clr As Color) As Integer
Dim vARGB As Integer = clr.ToArgb 'convert color value to AARRGGBB.
Return RGB((vARGB >> 16) And &HFF, (vARGB >> 8) And &HFF, vARGB And &HFF) 'return RGB color (00BBGGRR).
End Function

End Module

To use this GDI-based FloodFill method, we need to have the handle of an object. For the current form,
this would be “Me.Handle”, or the handle of the PictureBox that we are drawing to. We will also need the
local X and Y coordinates of where we want the flood fill to begin, we will need the color for the brush
to use for the FloodFill, such as Color.Orchid, and lastly, we may need to specify a border color. This last
option controls how things are going to be painted. If it is not provided or set to Nothing, then it is clear
that we do not want to paint to a border color, and so it will instead paint only to the color that is
presently at the X,Y coordinate and outward until any other color is encountered. If the border color is
set to a color, it is assumed that we want to begin painting over any color, starting at the X,Y coordinate
and fanning out from there, until we find the border color or the image boundary.
 Page 339

For example, suppose we have the following MouseDown event:


'do a flood fill at the point of the mouse click
Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
If e.Button = MouseButtons.Left Then
GdiFloodFill(Me.Graphics, e.X, e.Y, Color.Orchid) 'draw to the form's display surface, not the image's.
End If
End Sub

This event will perform a flood fill anywhere on the surface of Form1 we
click, painting the location and outward the color Orchid, as long as the
color we clicked on is available and is not blocked by any other color
(this is because we used the default parameter, setting the border color to
Nothing. Thus, the image shown to the left will become the image to the
right if we click the mouse anywhere on the white background.
This GDI FloodFill works great, but it does not seem to be persistent, at least as we used it. If we did a
Me.Refresh, the image would redisplay in its original state, but this is only because we used the Graphics
interface of its container, the form's DC, which is the form's display surface our image had previously been
drawn to. Because the image itself was not altered, when its container refreshes, the image data will over-
write anything written to the display area of its container. In most cases this behavior is exactly what we
want; for this is how Paint events expect things to be drawn. But, if we want real persistence because we are
using this in an art or drafting program, then we will have to provide the Graphics interface of the actual
Image (which is itself an instance of the design-time image, which will not be altered). This way the image
will endure in the run-time image, not just painted on it’s container’s display surface. Thus, instead of
painting on the Form. paint to its Background image using something like " Dim g As Graphics =
Graphics.FromImage(Me.BackgroundImage) ". Having drawn to the image, we must then refresh its container, such as
the form ("Me.Refresh"), so the its container's display surface will refresh itself with its updated Image object.

GDI+ FloodFill.
GDI+, for as powerful as it is, being tied directly to our video graphics hardware controller, it still does
not provide FloodFill support, because, unlike the ExtFloodFill method provided by GDI32.DLL,
GDIPlus.DLL does not contain a FloodFill method. This is for the simple reason that the video
controller hardware itself does not support FloodFill. This might seem like a silly idea, but most
rendering to games have no use for a single-color fill, but rather a progressive gradient fill that supports
shading, distance perspective, etc, which the OpenGL or DirectX software drivers provide support for.
So, it is left up to us to write our own. Ours will duplicate exactly what I did when writing in Assembler.
NOTE: Most of my fellow C++ developers will scoff at writing such code in VB, declaring that it is too slow compared to
C++. Their words are hollow, spoken from a position of uninformed ignorance. They just got used to VB6 being a slower
language, because it actually ran as ActiveX VBC p-code, but called native by marketing departments because its runtime
parser incessantly invoked native DLL methods. Even so, it actually gave compiled C++ code a run for its money! This is
also why ActiveX DLLs required special handling and processing – because ActiveX does not run as native code. Every time
a VB6 user boasts to me that their code compiles to native and VB.NET is MSIL (but instantly compiles to native), I have to
make my lip bleed to hold back a loud guffaw.
NOTE: Most C++ developers may not realize it, but VB.NET runs just as fast as C++ code, because VB.NET is modeled
on top of Managed C++ using VB syntax, plus many added optional protections and much easier (saner and less cryptic)
formatting. Therefore, when we are writing VB.NET code, we are actually writing Managed C++ code (so tell all those
holuer-than-thou C++ hacks to stuff it where the sun doesn't shine). VB.NET also adds optional overflow and bounds
checking, all of which we can actually turn off in the compiler options. Indeed, overflow errors are among the most common,
and most frustrating problems facing C-based development – this is one of many reasons why one can develop code in 2
hours using VB.NET that would take 2 weeks to write with equal robustness in C++.
The best solution to this issue is usually to write the code using a recursive method. This is because
when the target point is painted, the method should invoke itself 4 times so to check each adjacent point
that is left, right, above, and below it. Each of those invocations will check a saved target point to see if
it needs to paint it, if it is still within the bounds of the image, and if the current point has already been
 Page 340

painted the new color. If it has already been painted or if it breaches boundaries, it simply returns.
Otherwise, it will in turn check 4 other locations relative to that position, and so on.
The problem with this solution is that a bitmap contains an absolutely collosal amount of pixel positions,
where even a tiny 256x256-pixel image contains 65,536 pixel locations. A large image can fill .NET’s
stack space, which is quite sizable to begin with, and so our code will suffer a general protection fault,
which is to say that it will crash (this is like saying “dying” is “exceeding one’s physiological limits”).
As such, and because the recursion method is the absolutely best and shortest means for resolving this
issue, we will need to write our own stack code that will not be limited by the default .NET stack
restrictions (granted, we are able to adjust this stack space allocation, but doing this is just one more
consideration to trudge through when we develop code). Also, the advantage of using our own stack is
that we do not need to put a big furry data-ball full of objects onto consecutive segments of the stack,
but rather we need only push the positions themselves. If we were to use a Point structure, this would
simply shove two 32-bit integers onto that stack (only a Point’s data, not its methods or properties, will
be added to the stack). With that, we can simply test to see if we need to save each point on the stack.
This can be performed non-recursively by placing the target point on this stack, and then, within a loop,
pop the stack, check a point, and if we set it, we check neighboring points in the 4 primary directions. If
any meet muster, those are in turn shoved onto the stack. The looper then checks to see if anything is
still on the stack, and if so, it loops back, where it pops the last-saved point off the stack, and so on.
After developing my solution, I checked the web, because there were quite a number of people looking
for a solution to this issue. All solutions offered used the GDI ExtFloodFill Pinvoke, save one by Bob
Powell at www.BobPowell.net (specifically, at www.BobPowell.net/floodfill.aspx). He implemented a similar
solution to what I came up with. He did offer a more memory-efficient means to get and set points (so
simple and small they are absolute genius, and I was quick to snag them both up). His solution
performed only color replacement, which emulates the FLOODFILLSURFACE option offered by the
GDI ExtFloodFill Pinvoke. Testing for a border to emulate the FLOODFILLBORDER option is much
trickier, because it can easily keep testing back and forth until even our computer’s memory runs out of
space, because it normally ignores the color of the target point, unless it was the specified border color.
My solution to that show-stopper issue, however, after hours of testing, turned out to be brain-dead
simple, and only required a moment to implement – I simply checked to see if the target point is set
either to the Border Color or the Flood Fill color. If so, we need not test it further. Dummy me!
This method will draw to an Image object. As such, we must make sure the Image object actually exists!
The reason for this is that when we drop a PictureBox on a form, its embedded Image control is actually
set to Nothing, but anyone who has ever tried to draw to an Image control will come to this realization in
an instant (it might also have something to do with the exception error the application will slap into our
faces). Therefore, at our program startup, we may need to make sure that the image contains a sized
bitmap. Fortunately, this is almost too easy to do, using code as simple as “Img = New Bitmap(NewWidth,
NewHeight)”. With a PictureBox, we could use something like this:
With Me.PictureBox1
.Image = New Bitmap(.Width, .Height) 'create blank image (of background color) as a background to draw onto.
End With

And since we are on the subject of processing bitmap data, I have often been asked how to easily clear a
PictureBox or a Form (a form would use its BackgroundImage property instead of Image). That is also
very easy. Just use “Graphics.FromImage(Img).Clear(Color.White)". For example:
With Me.PictureBox1
Graphics.FromImage(.Image).Clear(.BackColor) 'clear image using the background color of PictureBox1.
End With

NOTE: We normally need to clear a freshly-created image so that drawing on it will not have issues.
Following is my modGdipFloodFill module code that performs persistent FloodFills on Image objects:
 Page 341

Option Strict On
Option Explicit On
'******************************************************************************
'Paint a region of a picturebox, form, or printer with a color
'******************************************************************************
' modGdipFloodfill - The GdipFloodfill() function will paint a region of any Image
' object with a slected color. X and Y coordinates define the
' starting location. If the border Color is Transparent, The
' color selected will be painted, flooding out in all directions
' as long as the color originally found at X and Y color is
' found. If the BorderClr value is set, then the Clr value is
' filled on the surface until a border of BorderClr is
' encountered, or the DC limits are met.
'EXAMPLE:
''Do a flood fill at the point of the mouse click, but only if the Cntrl key is ALSO held down.
'Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
' If e.Button = MouseButtons.Left AndAlso Control.ModifierKeys = Keys.Control Then
' FloodFill(Me.PictureBox1.Image, e.X, e.Y, Color.White)
' Me.PictureBox1.Refresh 'display updated image to user
' End If
'End Sub
'******************************************************************************
' This is a total rewrite. My original FloodFill exploited ExtFloodFill and was
' GDI-based, but did not support persistent imaging via GDI+. Sadly, GDI+
' does not support FloodFill.
'******************************************************************************
Module modGdipFloodFill
Private stack As New Stack(Of Point) 'stack to hold pixel test locations
Private imgBounds As RectangleF 'Image bounds for testing location being within range

'******************************************************************************
' Method : GdipFloodFill
' Purpose : Floodfill an image at a specified point with a specified color.
' PicBox : PictureBox containing the image to floodfill.
' X : Start X position in image.
' Y : Start Y position in image.
' NewClr : Color to floodfill with.
' BrdrClr : Optional terminating Border. If Nothing, we replace the starting point's color.
'******************************************************************************
Friend Sub GdipFloodFill(ByRef Img As Image,
ByVal X As Int32,
ByVal Y As Int32,
ByVal NewClr As Color,
Optional ByVal BrdrClr As Color = Nothing)

Dim Bmp As Bitmap = DirectCast(Img, Bitmap) 'Ensure the bitmap is in the right format.
imgBounds = Bmp.GetBounds(GraphicsUnit.Pixel) 'eliminate need for complicated TestBounds() test.
Dim Pos As New Point(X, Y) 'init testing position.
Dim TestClr As Color = GetPixel(Pos, Bmp) 'get the color under the point. This is the original.
Dim ReplaceClr As Boolean = BrdrClr = Nothing OrElse
BrdrClr = Color.Transparent 'True if we will replace a color...

If ReplaceClr Then 'if we will replace a color...


If TestClr.Equals(NewClr) Then 'if it is already that color...
Return 'then we have nothing to do.
End If
Else 'otherwise, we will fill to a border color or boundaries.
If TestClr.Equals(BrdrClr) Then 'if we are already at the border...
Return 'then we have nothing to do.
End If
TestClr = BrdrClr 'set color to test for.
End If
FillPixel(Pos, Bmp, NewClr, TestClr, ReplaceClr) 'Fill the first pixel and recursively fill all it's neighbors.
End Sub

'******************************************************************************
' Portions of the following is based on original work by Bob Powell at
' BobPowell.net http://www.bobpowell.net/floodfill.aspx. I enhanced it to:
' process Bitmaps instead of BitMapData objects.
' allow floodfill to borders, not just color-replacement.
' reduce his complicated TestBounds position testing to a simple in-code test (If imgBounds.Contains(Pos) Then).
' speed it up using a strongly-typed stack rather than a generic stack.
'******************************************************************************

'******************************************************************************
' Method : GetPixel
' Purpose : Returns the color at a specific pixel.
' pos : The pixel position on the bitmap.
' bmp : The bitmap.
'******************************************************************************
Private Function GetPixel(ByVal Pos As Point,
ByRef Bmp As Bitmap) As Color
If imgBounds.Contains(Pos) Then 'if the position is within bounds...
Return Bmp.GetPixel(Pos.X, Pos.Y) 'then return the color at the pixel position.
Else
 Page 342

Return Nothing 'if we failed.


End If
End Function

'******************************************************************************
' Method : SetPixel
' Purpose : Sets a pixel at a nominated point to a specified color.
' pos : The pixel position on the bitmap.
' bmp : The bitmap.
' Clr : The color to set.
'******************************************************************************
Private Sub SetPixel(ByVal Pos As Point, ByRef Bmp As Bitmap, ByVal Clr As Color)
If imgBounds.Contains(Pos) Then 'if the position is within bounds...
Bmp.SetPixel(Pos.X, Pos.Y, Clr) 'set the pixel.
End If
End Sub

'******************************************************************************
' Method : FillPixel
' Purpose : Fills a pixel and its un-filled neighbors with a specified color.
' pos : The start position in the bitmap.
' bmp : The bitmap.
' NewClr : The color to fill with.
' TeStClr : The testing color, being the color to replace, or the border to stop at.
' ReplaceClr : True if we are replacing a specific color, otherwise filling to...
' : a border color or to the the bitmap boundary, whichever comes first.
'******************************************************************************
Private Sub FillPixel(ByVal Pos As Point,
ByRef Bmp As Bitmap,
ByVal NewClr As Color,
ByVal TestClr As Color,
ReplaceClr As Boolean)

Dim OrgClr As Int32 = GetPixel(Pos, Bmp).ToArgb 'get the color at the specified pixel position.
If OrgClr = Nothing Then 'if we went out of bounds...
Return 'then we have nothing to do.
End If
'-----------------------------------------------
Dim iTestClr As Int32 = TestClr.ToArgb 'grab integer version of test color.
If ReplaceClr Then 'are we replacing a specific color?
If OrgClr = NewClr.ToArgb Then 'yes, but do the colors already match?
Return 'yes, so we are done without really starting.
End If
ElseIf OrgClr = iTestClr Then 'otherwise, have we met the border or the bitmap boundaries?
Return 'yes, so we are done.
End If
stack.Push(Pos) 'push current position on the stack.
Dim INewClr As Int32 = NewClr.ToArgb 'init fill color value.
'-----------------------------------------------
Do
Pos = stack.Pop() 'get a position to test.
SetPixel(Pos, Bmp, NewClr) 'set pixel to the color.
'now check pixels left, right, up, and down
TestPixel(New Point(Pos.X + 1, Pos.Y), Bmp, INewClr, iTestClr, ReplaceClr) 'look right.
TestPixel(New Point(Pos.X - 1, Pos.Y), Bmp, INewClr, iTestClr, ReplaceClr) 'look left.
TestPixel(New Point(Pos.X, Pos.Y + 1), Bmp, INewClr, iTestClr, ReplaceClr) 'look up.
TestPixel(New Point(Pos.X, Pos.Y - 1), Bmp, INewClr, iTestClr, ReplaceClr) 'look down.
Loop While Cbool(stack.Count) 'keep testing while points exist on the stack 'look at my thumb...
End Sub

'******************************************************************************
' Method : TestPixel
' Purpose : See if a pixel should be saved for replacement.
' pos : The start position in the bitmap.
' bmp : The bitmap.
' TeStClr : The testing color, being the color to replace, or the border to stop at.
' ReplaceClr : True if we are replacing a specific color, otherwise filling to
' : a border color or to the the bitmap boundary, whichever comes first.
'******************************************************************************
Private Sub TestPixel(ByVal Pos As Point,
ByRef Bmp As Bitmap,
ByVal NewClr As Int32,
ByVal TestClr As Int32,
ByVal ReplaceClr As Boolean)
Dim iOrgClr As Int32 = GetPixel(Pos, Bmp).ToArgb 'grab color at pixel position.
If Not iOrgClr = Nothing Then 'if not out of bounds.
If ReplaceClr Then 'are we replacing a color?
If Not iOrgClr = TestClr Then 'if they do not match, then do nothing.
Return
End If
Else 'else we are filling to borders.
If iOrgClr = TestClr OrElse
iOrgClr = NewClr Then 'if they match or we are at the border...
Return 'we are done.
End If
 Page 343

End If
stack.Push(Pos) 'else save point for later scrutiny.
End If
End Sub

End Module

In order to use this GDI+ FloodFill method, we have to provide a target Image control for whatever object we are
writing to, whether it is a form’s BackgroundImage or a PictureBox’s Image. For example: “Me.BackgroundImage”
or “Me.PictureBox1.Image”. We will also need to have the local X and Y pixel coordinates of where we want the
flood fill to begin within the image, such as e.X and e.Y from a MouseDown event. We will also need the color
we want to use for the fill, such as Color.Orchid. Lastly, we need to specify a border color IF we are not going to
do a color-replacement. This last parameter controls how things are going to be painted. If it is not provided, set to
Nothing, or set to Color.Transparent, then it is clear that we do not want to paint to a border color, and so it will
instead paint only to the color that is presently at the X,Y coordinate and outward, and any other color is treated as
a border. If the border color is set to any other color, then it is assumed that we want to begin painting over any
color, starting at the X,Y coordinates and fan out from there until we encounter the selected border color or the
image border.
NOTE: Be sure to refresh the image’s container after a GDI+ flood fill, such as “ Me.PictureBox1.Refresh” to display the
update, because the object was updated, not its container's display surface (device context)..

Drawing Graphics Outside the Paint Event.


Most drawing examples we see are drawn from the Paint Event of a Form or a Control. However, we
can paint and draw text and shapes just as easily outside these events.
Some new programmers are so stuck in trying to figure out how to do these things that they give up,
usually after several long, valiant, yet arduous and ultimately utterly failed attempts. However, all we
really need is a Graphics object that is associated with the form or control we want to draw to, and for
that we simply need to use that object’s CreateGraphics method. For example:
' Generate a graphical interface to a picture box (same as e.Graphics provided by a PaintEventArgs object)
Dim eg As Graphics = Me.PictureBox1.CreateGraphics

And that is all there is to it (Note that System.Drawing might also need to be loaded).
But that is not the end of it. Using the above graphics interface is non-persistent. What this really means
is that what is drawn is to the PictureBox surface. But what if we want to draw to the Image object of the
PictureBox? Doing this will make the drawing persistent, because our drawings will actually be recorded
directly into the image object. How do we get the graphics object for that? EASY:
Dim gi As Graphics = Graphics.FromImage(Me.PictureBox1.Image)

However, there is one more important thing to remember, and that is that a Graphics object is actually a
reference to what system developers might refer to as a Device Context. What this really means to
ordinary humans is that we cannot mix different device contexts with each other without them stepping
on each other’s toes, which causes each of them to re-invoke paint events constantly as they draw their
light sabers and do battle for control over what is drawn, eventually calling out their armies and bringing
about Armageddon on our computer’s memory, locking the system up and stalling everything as they do
battle in endless cycles. The moral of this is that if we are using the Graphics object in a Paint event, or
invoking a method that uses the Graphics object from a Paint event, that we should not also create a
local copy of a graphics object for that same control, such as a PictureBox. Granted, we would think that
they are the same, and the fact is their identities might look to be the same, but they are instantiated as
separate objects, and as such they will maintain their own local copies of the rules and what they claim
control over. What we should do in this case is determine if a Graphics object is passed to the function,
and create a local instance only if one is not provided, and being sure to dispose of the local instance if it
is created, but remembering not to dispose of the graphics object if it is provided to us, such as through a
parameter from a paint event (we borrowed it).
 Page 344

Conclusion.
There is much that we can do with roll-your-own lines and shapes, and developing them is not the great
burden that many may shock us into believing. Although the line and shape controls in the Visual Basic
Power Packs are fantastic, they do, just like VB6 line and shape controls, take much more processing
time. By rendering such simple lines and shapes from a form or control’s Paint event, we keep the code
extremely lean and extremely fast. By whichever route we choose to take, I hope that by exploring
graphical rendering here in much more detail, our implementation decision to draw our own graphics or
use line and shape controls will now be based upon understanding rather than hearsay.
NOTE: All that said, if we would prefer to design our forms using the quick and easy Power Packs Shape controls, but
would afterward like to translate them to much faster and less memory-consuming Paint event drawing instructions, this is
really very easy to do. It is brain-dead simple to do if we use my modShapeConvert module described in Black Book Tip #
31: Easily Replace Power Pack Shape Controls with Paint Event code on page 559 of this document, which exposes a
ConvertShapes method that will generate the Paint event equivalent code needed to duplicate Power Pack LineShape,
RectangleShape, and OvalShape controls, but rendering them many times faster than these Power Pack controls.
 Page 345

Restoring Raster Graphics and Rubber Bands to .NET.


We should be quick to notice that VB.NET does not support general drawing commands as a part of its
intrinsic language repertoire, as had been the case under VB6. I say thank goodness! I still cannot
believe that the very primitive DOS-BASIC-style drawing commands had managed to make their way
all the way into VB6, and especially as a part of its core language. I had been very fearful that the broad
and largely non-professional, albeit fiercely devoted user-base would force VB.NET to end up suffering
such a fate during its development cycle. Just look at the damage hoards of die-hard fans had forced on
bitwise and logical operations, changing them from innovative and more clearly defined, back to just
archaic and ambiguous (“Mongo fears change,” to quote Mongo from a cut scene from the theatrical
release of Mel Brook’s hit comedy, Blazing Saddles).
Under .NET, any specialized features, such as drawing, are grouped under a separate namespace. This is
a very good thing. By placing them as members in a separate static class, we can use the methods in that
class to draw on graphical objects, or, later, when more advanced graphical capabilities are added to
.NET, we can instead use the members of the namespace devoted to that paradigm. In the meantime we
are not cluttering up the language as was sadly done to VB6. For example, suppose someone had
developed a class supporting OpenGL or Direct3D drawing that had used intrinsic drawing commands.
If we forget to specify or import their namespace, we would find ourselves drawing using intrinsic
features instead of enhanced features and we would then have to track down the bugs by finding where
we forgot to reference our graphical class. By keeping all these things in different namespaces, the
language is simplified and allowes for easy and greater expansion of its capabilities, and there is also
less likelihood for method name collisions.
However, as easy and as better as all this is, drawing is largely different under VB.NET than under VB6.
Granted, some changes are easy, like shifting the VB6 Cls command to Clear under VB.NET. Others are
simple to adapt to, such as using the System.Drawing.Graphics object to draw shapes, lines, arcs, circles,
etc. to a PictureBox. Indeed, these are now much easier to use and are also a lot faster.
Some others do not exist anymore because it is much easier and more efficient to provide and maintain
these features within our own code, no longer eating up valuable resource space if we do not employ
them. Cases in point are the VB6 CurrentX and CurrentY last drawing location properties. These
properties are easy enough to compute and update within local variables without eating control resources
to store them within every single drawing surface, especially when virtually none of them will actually
ever be used.
Others are not obvious. For instance, the VB6 Point command is now GetPixel, to get a pixel point on
an image, and must be accessed through a Bitmap object, which, logically, is where such a command
should be.
Regarding Bitmap objects – most examples tell us to create a new Bitmap object and fill it with a from
PictureBox image, but that is a waste of resources if we just need to access the image or make a quick
alteration. Indeed, the only time we should really need to make a copy of it is when we will be making
many changes to the image, but want to update the actual displayed image only once, to eliminate
flicker. Otherwise, instead of chewing up resources and valuable time doing something like this:
Dim Bmp As New Bitmap(Me.PictureBox1.Image) 'instantiate a new copy of the image as a bitmap (useful for many changes).

We can instead simply do the following (note that a Bitmap and an Image are identical in structure):
Dim Bmp As Bitmap = DirectCast(Me.PictureBox1.Image, Bitmap) 'define a reference to the image without instantiating new image.

NOTE: Using the DirectCast compiler directive is very efficient, because it generates no actual compiled code.
The only times we would really need to create a separate bitmap is when, as stated previously, we need
to make many changes or if the target image might actually be set to Nothing, which a blank PictureBox
or a form with no background image may be. In such a case, we can alternatively declare it so:
 Page 346

Dim Bmp As New Bitmap(Me.Width, Me.Height, Me.CreateGraphics) 'create a blank bitmap (also create a graphical interface for it).
'or...
With Me.PictureBox1
Dim Bmp As New Bitmap(.Width, .Height, .CreateGraphics) 'create a blank PictureBox image.
End With

Regardless of how we define the bitmap interface, we can then get the color at a specified point on it using:
Dim Clr As Color = Bmp.GetPixel(intX, intY) 'Get an ARGB color value at a specified X,Y point (duplicates the VB6 Point).

To set a single point, we may often be advised to draw a circle with a radius of 1, like so:
e.Graphics.DrawEllipse(Pens.Black, 100, 100, 1, 1) 'using Ellipse to draw a point at 100x, 100y with width and height of 1.

NOTE: Rather than using e.Graphics, as shown here, which is provided by a Paint event, we can instead easily create our
own graphical interface to the object, as will be demonstrated shortly, and use it outside of Paint events.
Though slothful, it is not as wasteful as it might first appear because .NET will see that the width and
height are set to 1, and so it will just draw a dot. Even so, the SetPixel Pinvoke under gdiplus.dll is much
faster. And because of this, I find the above ellipse advice odd, especially when VB.NET already
supports a much faster SetPixel method under the Bitmap object, complementing its GetPixel method,
providing an interface to the fore-mentioned SetPixel Pinvoke, as shown here:
Bmp.SetPixel(100, 100, Color.Black) 'set a single point on the image (duplicates the VB6 graphical Set command).

Getting an Image Graphics Object Without Going Through the Paint Event.
The main problem many new users to VB.NET run into is when they try to draw shapes at will to
something like a PictureBox. It may be easy to draw within its Paint event (as demonstrated earlier in
this document – see the article, E-Z Ways to Draw Lines and Shapes, and to Paint in VB.NET, on page 325, for
instance), where the PaintEventArgs “e” parameter provides us with access to a convenient Graphics
object that is linked to the PictureBox. However, they seem to get a bit stuck when they want to simply
draw a unique line, shape, or text to the PictureBox, such as a drawing program might use. In fact, I have
seen code where, out of frustration, people have placed specialized hooks into a Paint event that passes
back the event argument object, or more often, saving it off somewhere, implementing it in a customized
method so that they could apply those non-typical drawing features. But this normally leads only to
exception errors.
However, accessing this graphical interface is really not complicated. What we need to understand is
that the graphical interface provided by the Paint event is just a simple System.Drawing.Graphics object
that is tied to the PictureBox, and this is something that is quite easy to generate within our code.
For example, suppose we want to draw to PictureBox1. To access a graphical interface to it using an
object named eg (reminding us of e.graphics), we could use code something like this:
' generate a graphical interface to a picture box (same as e.Graphics provided by a PaintEventArgs object)
Dim eg As Graphics = Me.PictureBox1.CreateGraphics

We may also occasionally notice such objects defined like the following, which is also valid:
Dim egh As Graphics = Graphics.FromHwnd(Me.PictureBox1.Handle) 'new Graphics object for PictureBox1.
Dim egi As Graphics = Graphics.FromImage(Me.PictureBox1.Image) 'new Graphics object for PictureBox1.Image.

NOTE: When drawing shapes, be sure to refresh the image if we use egi so any changes using it will be displayed. Using egh
is non-persistent and alters only the displayed image. Using egi is more persistent because it alters the stored image.
Suppose we want to draw a non-persistent white “X” across an image if the user clicks on it. Try this:
'react to user clicking on PictureBox1.
Private Sub PictureBox1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles PictureBox1.Click
Dim egh As Graphics = Me.PictureBox1.CreateGraphics 'generate a non-persistent PictureBox Graphics object.
With Me.PictureBox1 'drawing to PictureBox, not to its Image object, so PictureBox1.Refresh will erase below art.
egh.DrawLine(Pens.White, 0, 0, .Width, .Height) 'draw a white line from top-left to bottom-right.
egh.DrawLine(Pens.White, 0, .Height, .Width, 0) 'draw a white line from bottom-left to top-right.
End With
End Sub

NOTE: Objects with bound Paint events are considered non-persistant because their surfaces often change, which is why
they have bound Paint events. Also, attached graphical objects update their surfaces, like an attached Image object.
 Page 347

Restoring Raster Operations to .NET.


Other graphical features seem to be lost in limbo, such as the VB6 DrawMode property that allowed us
to specify how a pen object is to be drawn on the background. It could be normal ( R2_CopyPen),
R2_XOrPen, R2_NotXOrPen, R2_MergePen, R2_MergePenNot, or whatever raster drawing operation
we would ever want to perform. The reason for this is that Raster operations are not supported by GDI+
under .NET, regardless of the fact that these are simple processes.
GDI+, introduced in 2001 with Windows XP, is faster and more powerful than standard GDI, and this is
because GDI+ is linked directly to the operations available to graphics cards through their firmware
(software burned into a board-based read-only memory chip (ROM)). It thus naturally lacks support for
Raster Operations (memory-mapped operations) simply because the card’s firmware lacks it.
What this means is that the raster-generated selection rectangle that is used so often in Windows is not
supported by GDI+ under .NET. Indeed, GDI+ was originally meant to be used by C/C++ developers,
but it was quite naturally incorporated into .NET, and so it was naturally exposed to VB.NET and C#.
For example, under GDI+, the System.Drawing.Graphics.CompositingMode property, which is the
closest thing it has to the GDI Raster Operations property, has only 2 meager enumerated settings:
Member name Description
SourceOver Specifies that when a color is rendered, it is blended with the background color. The blend is
determined by the alpha component of the color being rendered. GDI has no similar command
because GDI does not support Alpha blending.
SourceCopy Specifies that when a color is rendered, it overwrites the background color (Default). This is just like
the GDI Raster R2_CopyPen command.

Conversely, the older GDI Raster operations implement 17 useful settings, as defined below:
Member name Description
R2_Black Specifies black pen color.
R2_CopyPen Specifies the pen color. The pen over-writes anything it draws over (Default). Most Used.
R2_MakePenNot Specifies a combination of the colors are common to both the pen and the inverse of the display.
R2_MaskNotPen Specifies a combination of the colors are common to the background color and the inverse of the
pen.
R2_MaskPen Specifies a combination of the colors common to both the pen and the display.
R2_MergeNotPen Specifies a combination of the display color and the inverse of the pen color.
R2_MergePen Specifies a combination of the pen color and the display color.
R2_MergePenNot Specifies a combination of the pen color and the inverse of the display color.
R2_NoOperation Specifies no operation; the output remains unchanged.
R2_Not Specifies the inverse of the display color.
R2_NotCopyPen Specifies the inverse of R2_CopyPen.
R2_NotMaskPen Specifies the inverse of R2_MaskPen.
R2_NotMergePen Specifies the inverse of R2_MergePen.
R2_NotXOrPen Specifies an inverse of R2_XOrPen.
R2_White Specifies a white pen color.
R2_XOrPen Specifies a combination of the colors in the pen and in the display color, but not in both. Most-used
next to R2_CopyPen.

As we can see, even with all this functionality, there is no alpha-channel-blending support provided by
GDI, such as the GDI+ SourceOver setting offers. The other GDI+ setting, SourceCopy, is duplicated by
the GDI R2_CopyPen setting. No other correlations between these two platforms are yet supported.

Getting One’s Hands Dirty.


So, if we want to implement GDI Raster operations under .NET, we need to get our hands dirty and perform
GDI operations in much the same way as I do them under C/C++. This involves obtaining the Device
Context (DC) of the object we want to draw to, creating pens and brushes to draw with, select them into the
DC, perform the drawing operation, and finally release these created resources and the DC.
 Page 348

Of course, even though this appears to be a rather complicated and involved process, it all boils down to
a uniform series of simple, repeatable steps that can be performed for us in an automated ptocess, so we
can instead focus entirely on the actual drawing tasks. What this automated process would involve are
two support methods that can be invoked unminded by us, “behind the curtain”, out of sight. The first
will automatically set up for a drawing task we want to perform, and the second will automatically close
down from it after our selected drawing task.
The first method, which we will name InitPenAndBrush, will get the Graphical interface and the Device
context of the target object, create the foreground drawing pen with a selected color and width, create
the background brush (the fill) with its color and pattern, select each of them into the object we want to
draw on, save the pen and brush that were previously assigned, and select the type of Raster Operation
we need.
Next, just like we would do under GDI+, we perform the actual drawing task, such as drawing a line.
After we perform the drawing task, the other method is invoked, which we will name DisposeResources,
which will select back in the previous pen and brush to the target and finally release the resources of our
new pen, brush, graphical interface, and device context.
Of course, to make this even easier, we can create a class that performs all this added work for each of
the drawing functions for us, so all we have to do is concern ourselves with invoking its public methods
and properties. We will simply define the pen we want to draw with, as needed, the background brush, as
needed, the type of Raster Operation and, naturally, to invoke any required drawing operations.
The GDI32 class, listed on the next page, is derived from various C++/C# sources, and also from a lot of
my own current and previous work (actually, one of the anonymous C++/C# postings in fact credited a
sadly unremembered VB.NET author who had designed an ordered structure to the class and defined its
naming conventions – if anyone recognizes it; please let me know so I can render credit). Note that any
additional features we would personally need can also be added to this class quite easily.
Once we instantiate an instance of this class, we can set the drawing pen’s color and width, the brush’s
color and hatch style (hatch style HS_SOLID is the default), if we need to change them (they are saved
within the class properties so we do not need to set them with every drawing command), and invoke any
of the required drawing operations: SetPoint, GetPoint, DrawLine, DrawCircle, DrawEllipse, DrawArc,
DrawRectangle, DrawRoundRect, and DrawObround.

To implement this class into our own application is very simple. Somewhere within our code, perhaps as
publicly as possible to our application, we would declare an instance of the GDI32 class:
Friend m_GDI As New GDI32 'instantiate an instace of the GDI32 class

Then, when we are ready to draw, be sure our Pen color (foreground), Brush color (background), and the
Raster operation are as we need them to be. By default, the Pen color is White, the Pen style is
PS_SOLID, the Brush color is Transparent, the Brush hatch pattern is HS_SOLID (no pattern), and the
Raster operation is R2_CopyPen. This will draw using a solid white pen with an invisible brush (no
background color under the pen will be painted on the surface; typical for ellipses and rectangles).
NOTE: If the Pen color is Transparent, then the drawing operation will use a stock Null Pen (invisible). If the Brush color
is Transparent, then the drawing operation will use a stock Null Brush.
We set the properties for the drawing operation by changing the properties of the class. For example:
m_GDI.PenColor = Color.LightBlue 'set pen color to Light Blue (default is Color.White)
m_GDI.PenStyle = PenStyles.PS_DASH 'alternate dashes with dots (default is PenStyles.PS_SOLID)
m_GDI.PenWidth = 3 'Set the pen width to 3 pixels (Default is 1)
m_GDI.BrushHatch = HatchStyle.HS_CROSS 'set the brush style to Crosses (default is HatchStyle.HS_SOLID)
m_GDI.BrushColor = Color.Blue 'set the brush color to Blue (default is Color.Transparent)
m_GDI.RasterOp = RasterOps.R2_XOrPen 'merge the pen and background colors (default is RasterOps.R2_CopyPen)
 Page 349

NOTE: GDI requires RGB colors, not the ARGB colors that are part of the System Color Palette and GDI+. As such, we
must convert them from ARGB to RGB. Most gurus will tell us to simply perform a Clr.ToArgb And &HFFFFFF operation, but
this is not enough. ARGB is stored internally as AARRGGBB, for Alpha, Red, Green, and Blue, where each letter
represents a Hexadecimal digit. However, RGB is stored as 00BBGGRR. As such, the Red and Blue color values must also
be swapped. The class performs this for us automatically using the ARGBtoRGB function, defined below:
'*************************************************************************************************************
' Function: ARGBtoRGB
' Helper function to convert Alpha Color ARGB value (AARRGGBB) to RGB (00BBGGRR)
'*************************************************************************************************************
Private Function ARGBtoRGB(ByVal clr As Color) As Integer
Dim vARGB As Integer = clr.ToArgb 'convert color value to AARRGGBB.
Return RGB((vARGB >> 16) And &HFF, (vARGB >> 8) And &HFF, vARGB And &HFF) 'return RGB color (00BBGGRR).
End Function

We then perform the desired drawing operation. Because several of the methods, such as DrawArc,
DrawEllipse, DrawRectangle, DrawRoundRect, and DrawObround have overloaded methods, we can also specify
them by providing Point objects to define their top-left and bottom-right bounds, or by providing a
bounding Rectangle object. Also, we must provide a disposable graphical interface to the target object
we want to draw upon (the method will Dispose of it for us). For example:
' draw a rectangle from PictureBox coordinates 25,25 (top-left) to 100,100 (bottom-right)
m_GDI.DrawRectangle(Me.PictureBox1.CreateGraphics, New Point(25, 25), New Point(100, 100)) 'Send a disposable Graphics object.

Now draw as we see fit. We need to change colors and Raster operations only as needed, not every time.

The GDI32 Class.


What follows is the GDI32 class:
Option Strict On
Option Explicit On

'**************************************************************************************
' GDI32 - GDI Support for .NET
'**************************************************************************************

#Region "GDI32 Enumerations"


'*****************************************************************************************************************************
' Enumerations.
'*****************************************************************************************************************************

' Pen Styles (how lines are drawn)


Public Enum PenStyles As Integer
PS_SOLID = &H0 'A pen style that is a solid color. ──────
PS_DASH = &H1 'A pen style that is dashed. ------
PS_DOT = &H2 'A pen style that is dotted. ●●●●●●
PS_DASHDOT = &H3 'A pen style that consists of alternating dashes and dots. -●-●-●
PS_DASHDOTDOT = &H4 'A pen style that consists of dashes and double dots. -●●-●●
PS_NULL = &H5 'A pen style that is invisible.
PS_INSIDEFRAME = &H6 'A pen style that is a solid color. When this style is specified in a drawing record that takes a
bounding rectangle,
' 'the dimensions of the figure are shrunk so that it fits entirely in the bounding rectangle,
' 'taking into account the width of the pen.
End Enum

'Type of Raster operation (how drawing interacts with the background).


Public Enum RasterOps As Integer
R2_Black = 1 'Specifies black pen color.
R2_NotMergePen = 2 'Specifies the inverse of MergePen.
R2_MaskNotPen = 3 'Specifies a combination of the colors common to the background color and the inverse of the pen.
R2_NotCopyPen = 4 'Specifies the inverse of CopyPen.
R2_MakePenNot = 5 'Specifies a combination of the colors are common to both the pen and the inverse of the display.
R2_Not = 6 'Specifies the inverse of the display color.
R2_XOrPen = 7 'Specifies a combination of the colors in the pen and in the display color, but not in both.
R2_NotMaskPen = 8 'Specifies the inverse of MaskPen.
R2_MaskPen = 9 'Specifies a combination of the colors common to both the pen and the display.
R2_NotXOrPen = 10 'Specifies an inverse of XOrPen.
R2_NoOperation = 11 'Specifies no operation; the output remains unchanged.
R2_MergeNotPen = 12 'Specifies a combination of the display color and the inverse of the pen color.
R2_CopyPen = 13 'Specifies the pen color.
R2_MergePenNot = 14 'Specifies a combination of the pen color and the inverse of the display color.
R2_MergePen = 15 'Specifies a combination of the pen color and the display color.
R2_White = 16 'Specifies a white pen color.
End Enum

'Hatch Styles
Public Enum HatchStyle As Integer
HS_HORIZONTAL = 0 '----- A horizontal hatch.
 Page 350

HS_VERTICAL = 1 '||||| A vertical hatch.


HS_FDIAGONAL = 2 '\\\\\ A 45-degree downward, left-to-right hatch.
HS_BDIAGONAL = 3 '///// A 45-degree upward, left-to-right hatch.
HS_CROSS = 4 '+++++ A horizontal and vertical cross-hatch.
HS_DIAGCROSS = 5 'xxxxx A 45-degree crosshatch.
HS_FDIAGONAL1 = 6 '
HS_BDIAGONAL1 = 7 '
HS_SOLID = 8 '
HS_DENSE1 = 9 '
HS_DENSE2 = 10 '
HS_DENSE3 = 11 '
HS_DENSE4 = 12 '
HS_DENSE5 = 13 '
HS_DENSE6 = 14 '
HS_DENSE7 = 15 '
HS_DENSE8 = 16 '
HS_NOSHADE = 17 '
HS_HALFTONE = 18 '
HS_SOLIDCLR = 19 '
HS_DITHEREDCLR = 20 '
HS_SOLIDTEXTCLR = 21 '
HS_DITHEREDTEXTCLR = 22 '
HS_SOLIDBKCLR = 23 '
HS_DITHEREDBKCLR = 24 '
HS_API_MAX = 25 '
End Enum

'Stock Objects
Enum StockObjects As Integer
WHITE_BRUSH = 0 'White brush.
LTGRAY_BRUSH = 1 'Light gray brush.
GRAY_BRUSH = 2 'Gray brush.
DKGRAY_BRUSH = 3 'Dark gray brush.
BLACK_BRUSH = 4 'Black brush.
NULL_BRUSH = 5 'Null Brush (equivalen to HOLLOW_BRUSH).
HOLLOW_BRUSH = NULL_BRUSH 'Hollow brush (equivalent to NULL_BRUSH)
WHITE_PEN = 6 'White pen.
BLACK_PEN = 7 'Black pen.
NULL_PEN = 8 'Null Pen. The null pen draws nothing.
OEM_FIXED_FONT = 10 'Original Equiptment Manufacturer (OEM) dependent fixed-pitch (monospace) font.
ANSI_FIXED_FONT = 11 'Windows fixed-pitch (monospace) system font.
ANSI_VAR_FONT = 12 'Windows variable-pitch (proportional space) system font.
SYSTEM_FONT = 13 'System font. By default, the system uses system font to draw menus, dialog box controls, and text.
DEVICE_DEFAULT_FONT = 14 'WinNT/Win2K/XP: Device-dependent font.
DEFAULT_PALETTE = 15 'Default palette. This palette consists of the static colors in the system palette.
SYSTEM_FIXED_FONT = 16 'Fixed-pitch (monospace) system font. This object provided for compatibility to 16-bit Windows.
DEFAULT_GUI_FONT = 17 'Default font for UI objects, Menus, dialog boxes. This is MS Sans Serif. Compare to SYSTEM_FONT.
DC_BRUSH = 18 'Win2K/XP: Solid brush. Default White. Color can be changed using SetDCBrushColor() function.
DC_PEN = 19 'Win2K/XP: Solid pen color. Default White. Color can be changed using SetDCPenColor() function.
End Enum

#End Region

'**************************************************************************************
' GDI Class to support GDI operations not supported by GDI+
' GDI+ (gdiplus.dll), a Graphical Design Interface used to take advantage of Graphic
' card hardware and software. It was introduced with Windows XP. It was designed for
' use by C/C++ users, but naturally was incorporated into the .NET platform.
' GDI+ offers faster operations that those provided by GDI, yet Raster operations, such
' as displaying rubberband lines was lost.
'**************************************************************************************
Friend Class GDI32
#Region "GDI32 Protected Fields"
'*****************************************************************************************************************************
' Protected Fields
'*****************************************************************************************************************************
Protected m_hdc As IntPtr 'handle to drawing context

Protected m_gdiPen As IntPtr 'handle to pen.


Protected m_oldPen As IntPtr 'hold original pen.
Protected m_penColor As Color = Color.White 'Default drawing color.
Protected m_penStyle As PenStyles = PenStyles.PS_SOLID 'init pen to solid.
Protected m_penWidth As Integer = 1 'pixel width of pen (line).

Protected m_rasterOp As RasterOps = RasterOps.R2_CopyPen 'Init raster operation to normal.

Protected m_gdiBrush As IntPtr 'handle to new brush.


Protected m_oldBrush As IntPtr 'hold original brush.
Protected m_brushColor As Color = Color.Transparent 'default brush color (fill color).
Protected m_brushHatch As HatchStyle = HatchStyle.HS_SOLID 'hatch style. None = use Solid Brush.
#End Region

#Region "GDI32 Pinvoke Declarations"


'*****************************************************************************************************************************
' INTEROP Pinvoke DECLARATIONS.
 Page 351

'*****************************************************************************************************************************

'-----------------------------------------------------------------------------------------------------------------------------
' Function: CreateSolidBrush
' The CreateSolidBrush function creates a logical brush that has the specified solid color.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function CreateSolidBrush Lib "gdi32.DLL" Alias "CreateSolidBrush" (
ByVal crColor As Integer) As IntPtr

'-----------------------------------------------------------------------------------------------------------------------------
' Function: CreateHatchBrush
' The CreateHatchBrush function creates a logical brush that has the specified hatch pattern and color.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function CreateHatchBrush Lib "gdi32.DLL" Alias "CreateHatchBrush" (
ByVal Style As HatchStyle,
ByVal crColor As Integer) As IntPtr

'-----------------------------------------------------------------------------------------------------------------------------
' Function: CreatePatternBrush
' The CreatePatternBrush function creates a logical brush with the specified
' bitmap pattern. The bitmap can be a DIB section bitmap, which is created
' by the CreateDIBSection function, or it can be a device-dependent bitmap.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function CreatePatternBrush Lib "gdi32.DLL" Alias "CreatePatternBrush" (
ByVal hBitmap As IntPtr) As IntPtr

'-----------------------------------------------------------------------------------------------------------------------------
' Function: GetStockObject
' The GetStockObject function retrieves a handle to one of the stock pens, brushes, fonts, or palettes.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function GetStockObject Lib "gdi32.DLL" Alias "GetStockObject" (
ByVal nIndex As StockObjects) As IntPtr

'-----------------------------------------------------------------------------------------------------------------------------
' Function: CreatePen
' The CreatePen function creates a logical pen that has the specified style,
' width, and color. The pen can subsequently be selected into a device context
' and used to draw lines and curves.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function CreatePen Lib "gdi32.DLL" Alias "CreatePen" (
ByVal nPenStyle As PenStyles,
ByVal nWidth As Integer,
ByVal crColor As Integer) As IntPtr

'-----------------------------------------------------------------------------------------------------------------------------
' Function: SelectObject
' The SelectObject function selects an object into the specified device context
' (DC). The new object replaces the previous object of the same type.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function SelectObject Lib "gdi32.DLL" Alias "SelectObject" (
ByVal hdc As IntPtr,
ByVal hObject As IntPtr) As IntPtr

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DeleteObject
' The DeleteObject function deletes a logical pen, brush, font, bitmap, region,
' or palette, freeing all system resources associated with the object. After
' the object is deleted, the specified handle is no longer valid.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function DeleteObject Lib "gdi32.DLL" Alias "DeleteObject" (
ByVal hObject As IntPtr) As IntPtr

'-----------------------------------------------------------------------------------------------------------------------------
' Function: SetROP2
' The SetROP2 function sets the current foreground mix mode. GDI uses the
' foreground mix mode to combine pens and interiors of filled objects with
' the colors already on the screen. The foreground mix mode defines how colors
' from the brush or pen and the colors in the existing image are to be combined.
' If the function succeeds, the return value specifies the previous mix mode.
' If the function fails, the return value is zero.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function SetROP2 Lib "gdi32.DLL" Alias "SetROP2" (
ByVal hdc As IntPtr,
ByVal nDrawMode As RasterOps) As RasterOps

'-----------------------------------------------------------------------------------------------------------------------------
' Function: GetROP2
' The GetROP2 function retrieves the foreground mix mode of the specified
' device context. The mix mode specifies how the pen or interior color and
' the color already on the screen are combined to yield a new color.
' If the function succeeds, the return value specifies the previous mix mode.
' If the function fails, the return value is zero.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function GetROP2 Lib "gdi32.DLL" Alias "GetROP2" (
ByVal hdc As IntPtr) As Integer
 Page 352

' structure used by MoveToEx. It will contain the previous current position
Public Structure POINTAPI
Dim x As Integer
Dim y As Integer
End Structure

'-----------------------------------------------------------------------------------------------------------------------------
' Function: MoveToEx
' The MoveToEx function updates the current position to the specified point
' and optionally returns the previous position.
'-----------------------------------------------------------------------------------------------------------------------------
'hdc: Handle to a device context.
'X: Specifies the x-coordinate, in logical units, of the new position, in logical units.
'Y: Specifies the y-coordinate, in logical units, of the new position, in logical units.
'lpPoint: Ptr to POINT that receives the prev current posn. If parameter is a NULL ptr, then prev pos not returned.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function MoveToEx Lib "gdi32.DLL" Alias "MoveToEx" (
ByVal hdc As IntPtr,
ByVal x As Integer,
ByVal y As Integer,
ByVal lpPoint As POINTAPI) As Boolean

'-----------------------------------------------------------------------------------------------------------------------------
' Function: MoveToEx
' Description: The MoveToEx function updates the current position to the specified point
' and optionally returns the previous position.
'-----------------------------------------------------------------------------------------------------------------------------
'hdc: Handle to a device context.
'X: Specifies the x-coordinate, in logical units, of the new position, in logical units.
'Y: Specifies the y-coordinate, in logical units, of the new position, in logical units.
'lpPoint: Ptr to POINT that receives the prev current posn. If parameter is a NULL ptr, then prev pos not returned.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function MoveToEx Lib "gdi32.DLL" Alias "MoveToEx" (
ByVal hdc As IntPtr,
ByVal x As Integer,
ByVal y As Integer,
ByVal lpPoint As IntPtr) As Boolean

'-----------------------------------------------------------------------------------------------------------------------------
' Function: LineTo
' The LineTo function draws a line from the current position up to, but not
' including, the specified point.
'-----------------------------------------------------------------------------------------------------------------------------
'hdc: Handle to a device context.
'nXEnd: Specifies the x-coordinate, in logical units, of the line's ending point.
'nYEnd: Specifies the y-coordinate, in logical units, of the line's ending point.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function LineTo Lib "gdi32.DLL" Alias "LineTo" (
ByVal hdc As IntPtr,
ByVal nXEnd As Integer,
ByVal nYEnd As Integer) As Boolean

'-----------------------------------------------------------------------------------------------------------------------------
' Function: Ellipse
' The Ellipse function draws an ellipse. The center of the ellipse is the
' center of the specified bounding rectangle. The ellipse is outlined by using
' the current pen and is filled by using the current brush.
'-----------------------------------------------------------------------------------------------------------------------------
'hdc: A handle to the device context.
'nLeftRect: The x-coordinate, in logical coordinates, of the upper-left corner of the bounding rectangle.
'nTopRect: The y-coordinate, in logical coordinates, of the upper-left corner of the bounding rectangle.
'nRightRect: The x-coordinate, in logical coordinates, of the lower-right corner of the bounding rectangle.
'nBottomRect: The y-coordinate, in logical coordinates, of the lower-right corner of the bounding rectangle.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function Ellipse Lib "gdi32.DLL" Alias "Ellipse" (
ByVal hdc As IntPtr,
ByVal nLeftRect As Integer,
ByVal nTopRect As Integer,
ByVal nRightRect As Integer,
ByVal nBottomRect As Integer) As Boolean

'-----------------------------------------------------------------------------------------------------------------------------
' Function: Rectangle
' The Rectangle function draws a rectangle. The rectangle is outlined by using
' the current pen and filled by using the current brush.
'-----------------------------------------------------------------------------------------------------------------------------
'hdc: A handle to the device context.
'nLeftRect: The x-coordinate, in logical coordinates, of the upper-left corner of the rectangle.
'nTopRect: The y-coordinate, in logical coordinates, of the upper-left corner of the rectangle.
'nRightRect: The x-coordinate, in logical coordinates, of the lower-right corner of the rectangle.
'nBottomRect: The y-coordinate, in logical coordinates, of the lower-right corner of the rectangle.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function Rectangle Lib "gdi32.DLL" Alias "Rectangle" (
ByVal hdc As IntPtr,
 Page 353

ByVal nLeftRect As Integer,


ByVal nTopRect As Integer,
ByVal nRightRect As Integer,
ByVal nBottomRect As Integer) As Boolean

'-----------------------------------------------------------------------------------------------------------------------------
' Function: RoundRect
' The RoundRect function draws a rectangle with rounded corners. The rectangle
' is outlined by using the current pen and filled by using the current brush.
'-----------------------------------------------------------------------------------------------------------------------------
'hdc: A handle to the device context.
'nLeftRect: The x-coordinate, in logical coordinates, of the upper-left corner of the rectangle.
'nTopRect: The y-coordinate, in logical coordinates, of the upper-left corner of the rectangle.
'nRightRect: The x-coordinate, in logical coordinates, of the lower-right corner of the rectangle.
'nBottomRect: The y-coordinate, in logical coordinates, of the lower-right corner of the rectangle.
'nWidth: The width, in logical coordinates, of the ellipse used to draw the rounded corners.
'nHeight: The height, in logical coordinates, of the ellipse used to draw the rounded corners.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function RoundRect Lib "gdi32.DLL" Alias "RoundRect" (
ByVal hdc As IntPtr,
ByVal nLeftRect As Integer,
ByVal nTopRect As Integer,
ByVal nRightRect As Integer,
ByVal nBottomRect As Integer,
ByVal nWidth As Integer,
ByVal nHeight As Integer) As Boolean

'-----------------------------------------------------------------------------------------------------------------------------
' Function: Arc
' The Arc function draws an elliptical arc.
'-----------------------------------------------------------------------------------------------------------------------------
' hdc: A handle to the device context where drawing takes place.
' nLeftRect: The x-coordinate, in logical units, of the upper-left corner of the bounding rectangle.
' nTopRect: The y-coordinate, in logical units, of the upper-left corner of the bounding rectangle.
' nRightRect: The x-coordinate, in logical units, of the lower-right corner of the bounding rectangle.
' nBottomRect: The y-coordinate, in logical units, of the lower-right corner of the bounding rectangle.
' nXStartArc: The x-coord, in logical units, of the ending point of the radial line defining the starting point of the arc.
' nYStartArc: The y-coord, in logical units, of the ending point of the radial line defining the starting point of the arc.
' nXEndArc: The x-coord, in logical units, of the ending point of the radial line defining the ending point of the arc.
' nYEndArc: The y-coord, in logical units, of the ending point of the radial line defining the ending point of the arc.
'-----------------------------------------------------------------------------------------------------------------------------
'The points (nLeftRect, nTopRect) and (nRightRect, nBottomRect) specify bounding rect. An ellipse formed by the specified
' bounding rectangle defines the curve of the arc. The arc extends in the current drawing direction from the point where it
' intersects the radial from center of the bounding rectangle to the (nXStartArc, nYStartArc) point. The arc ends where it
' intersects the radial from center of the bounding rectangle to the (nXEndArc, nYEndArc) point. If the starting point and
' ending point are the same, a complete ellipse is drawn.
'The arc is drawn using the current pen; it is not filled.
'-----------------------------------------------------------------------------------------------------------------------------
Private Declare Function Arc Lib "gdi32.DLL" Alias "Arc" (
ByVal hdc As IntPtr, ByVal nLeftRect As Integer,
ByVal nTopRect As Integer, ByVal nRightRect As Integer,
ByVal nBottomRect As Integer, ByVal nXStartArc As Integer,
ByVal nYStartArc As Integer, ByVal nXEndArc As Integer,
ByVal nYEndArc As Integer) As Boolean
#End Region

#Region "GDI32 Properties"


'*****************************************************************************************************************************
' Properties
' BrushColor: Get/Set brush color. Default Black. Be sure is black for XOR operations if you want a "Rubberband" effect.
' BrushHatch: Get/Set optional Brush Hatch Style. Default is HS_SOLID, which specifies a solid brush.
' PenColor: Get/Set pen color. Default is black.
' PenStyle: Get/Set pen style. Default is PS_SOLID.
' PenWidth: Get/Set pen pixel width. Default is 1.
' RasterOp: Get/Set type of raster operation to perform. Default is R2_CopyPen (overwrite)
'*****************************************************************************************************************************

' Get/Set brush color


Public Property BrushColor() As Color
Get
Return Me.m_brushColor
End Get
Set(ByVal value As Color)
Me.m_brushColor = value
End Set
End Property

' Get/Set Brush Hatch Style


Public Property BrushHatch() As HatchStyle
Get
Return Me.m_brushHatch
End Get
Set(ByVal value As HatchStyle)
Me.m_brushHatch = value
End Set
 Page 354

End Property

' Get/Set pen color


Public Property PenColor() As Color
Get
Return Me.m_penColor
End Get
Set(ByVal value As Color)
Me.m_penColor = value
End Set
End Property

' Get/Set pen style (pattern)


Public Property PenStyle() As PenStyles
Get
Return Me.m_penStyle
End Get
Set(ByVal value As PenStyles)
Me.m_penStyle = value
End Set
End Property

' Get/Set pen width in pixels


Public Property PenWidth() As Integer
Get
Return Me.m_penWidth
End Get
Set(ByVal value As Integer)
Me.m_penWidth = value
End Set
End Property

' Get/Set Raster Operation


Public Property RasterOp() As RasterOps
Get
Return m_rasterOp
End Get
Set(ByVal value As RasterOps)
m_rasterOp = value
End Set
End Property
#End Region

#Region "GDI32 Protected Support Methods"


'*****************************************************************************************************************************
' Protected Support Methods
'*****************************************************************************************************************************
'*************************************************************************************************************
' Function: ARGBtoRGB
' Helper function to convert Alpha Color ARGB value (AARRGGBB) to RGB (00BBGGRR)
'*************************************************************************************************************
Private Function ARGBtoRGB(ByVal clr As Color) As Integer
Dim vARGB As Integer = clr.ToArgb 'convert color value to AARRGGBB.
Return RGB((vARGB >> 16) And &HFF, (vARGB >> 8) And &HFF, vARGB And &HFF) 'return RGB color (00BBGGRR).
End Function

'-----------------------------------------------------------------------------------------------------------------------------
' Subrouine: InitPenAndBrush
' Initialize pen annd brush
'-----------------------------------------------------------------------------------------------------------------------------
Protected Sub InitPenAndBrush(ByVal g As Graphics)
Me.m_hdc = g.GetHdc 'save handle to DC.
' process brush options
If Me.m_brushColor = Color.Transparent Then
Me.m_gdiBrush = GetStockObject(StockObjects.NULL_BRUSH) 'hide if brush transparent.
ElseIf Me.m_brushHatch = HatchStyle.HS_SOLID Then
Me.m_gdiBrush = GDI32.CreateSolidBrush(Me.ARGBtoRGB(Me.m_brushColor)) 'set solid brush style and fill color.
Else
Me.m_gdiBrush = GDI32.CreateHatchBrush(Me.m_brushHatch, Me.ARGBtoRGB(Me.m_brushColor)) 'create hatch brush style.
End If
' process pen options
If Me.m_penColor = Color.Transparent Then
Me.m_gdiPen = GetStockObject(StockObjects.NULL_PEN) 'hide pen if transparent.
Else
Me.m_gdiPen = GDI32.CreatePen(Me.PenStyle, Me.m_penWidth, Me.ARGBtoRGB(Me.PenColor)) 'set pen pattern/style/width.
End If
GDI32.SetROP2(Me.m_hdc, m_rasterOp) 'set raster operation.
Me.m_oldPen = GDI32.SelectObject(Me.m_hdc, Me.m_gdiPen) 'set new pen, save old.
Me.m_oldBrush = GDI32.SelectObject(Me.m_hdc, Me.m_gdiBrush) 'set new bg brush save old.
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Subrouine: DisposeResources
' Dispose of created data and reset old data.
'-----------------------------------------------------------------------------------------------------------------------------
 Page 355

Protected Sub DisposeResources(ByVal g As Graphics)


GDI32.DeleteObject(GDI32.SelectObject(Me.m_hdc, Me.m_oldBrush)) 'reset old brush, delete replaced one.
GDI32.DeleteObject(GDI32.SelectObject(Me.m_hdc, Me.m_oldPen)) 'reset old pen, delete replaced one.
g.ReleaseHdc(Me.m_hdc) 'release device context.
g.Dispose()
End Sub
#End Region

#Region "GDI32 Public Methods"


'*****************************************************************************************************************************
' Public Methods
'*****************************************************************************************************************************
'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawCircle
' draw a circle with a uniform radius from a center point
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawCircle(ByVal g As Graphics, ByVal p1 As Point, ByVal Radius As Integer) 'p1 is center of circle.
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.Ellipse(Me.m_hdc, p1.X - Radius, p1.Y - Radius, p1.X + Radius, p1.Y + Radius) 'draw circle.
Me.DisposeResources(g) 'disposes of resources.
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawEllipse
' Draw circles and ellipses from a bounds defined by a top-left point and a bottom-right point
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawEllipse(ByVal g As Graphics, ByVal p1 As Point, ByVal p2 As Point) 'p1=top-left of bounds, p2=bottom-right.
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.Ellipse(Me.m_hdc, p1.X, p1.Y, p2.X, p2.Y) 'draw ellipse.
Me.DisposeResources(g) 'disposes of resources.
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawEllipse
' Draw ellipses defined within a bounding rectangle
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawEllipse(ByVal g As Graphics, ByVal Bnds As Rectangle) 'p1=top-left of bounds, p2=bottom-right.
Me.InitPenAndBrush(g) 'init pen and brush.
With Bnds
GDI32.Ellipse(Me.m_hdc, .Left, .Top, .Right, .Bottom) 'draw ellipse within the rectangle.
End With
Me.DisposeResources(g) 'disposes of resources.
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawArc
' Draw an arc within bounding rect. Point ArcStart defines pnt from center along start line, ArcEnd is pnt along end line.
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawArc(ByVal g As Graphics, ByVal Bnds As Rectangle, ByVal ArcStart As Point, ByVal ArcEnd As Point)
Me.InitPenAndBrush(g) 'init pen and brush
With Bnds
GDI32.Arc(Me.m_hdc, .Left, .Top, .Right, .Bottom, ArcStart.X, ArcStart.Y, ArcEnd.X, ArcEnd.Y) 'draw arc
End With
Me.DisposeResources(g) 'disposes of resources
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawArc
' Draw an arc within a rectangle specifyed by p1 (top-left) and p2(bottom-right).
' Point ArcStart defines a point from center along start line, ArcEnd is a point along end line
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawArc(ByVal g As Graphics, ByVal p1 As Point, ByVal p2 As Point, ByVal ArcStart As Point, ByVal ArcEnd As Point)
Me.InitPenAndBrush(g) 'init pen and brush
GDI32.Arc(Me.m_hdc, p1.X, p1.Y, p2.X, p2.Y, ArcStart.X, ArcStart.Y, ArcEnd.X, ArcEnd.Y) 'draw arc
Me.DisposeResources(g) 'disposes of resources
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawLine
' Draw a line from point p1 to point p2
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawLine(ByVal g As Graphics, ByVal p1 As Point, ByVal p2 As Point)
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.MoveToEx(Me.m_hdc, p1.X, p1.Y, IntPtr.Zero) 'move to starting point.
GDI32.LineTo(Me.m_hdc, p2.X, p2.Y) 'draw line from start point to end point.
Me.DisposeResources(g) 'disposes of resources.
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawPolygon
' Draw a polygon from an array of points
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawPolygon(ByVal g As Graphics, ByRef PointsArray() As Point)
If PointsArray Is Nothing Then
Return 'if nothing to process.
 Page 356

End If
Dim NumPoints As Integer = UBound(PointsArray) 'get upper bounds of array.
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.MoveToEx(Me.m_hdc, PointsArray(0).X, PointsArray(0).Y, IntPtr.Zero) 'move to starting point.
If CBool(NumPoints) Then 'if more than 1 point.
For Idx As Integer = 1 To NumPoints 'process each point as a sequence in chain.
GDI32.LineTo(Me.m_hdc, PointsArray(Idx).X, PointsArray(Idx).Y) 'draw line from previous point to current.
Next
If Not PointsArray(0).Equals(PointsArray(NumPoints)) Then 'close polygon if start & last not same.
GDI32.LineTo(Me.m_hdc, PointsArray(NumPoints).X, PointsArray(NumPoints).Y) 'draw a line from prev pnt.
End If
End If
Me.DisposeResources(g) 'disposes of resources
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawRectangle
' Draw a rectangle or square from a top-left point to a bottom-right point
' P1 is the top-left corner, P2 is the bottom Right corner.
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawRectangle(ByVal g As Graphics, ByVal p1 As Point, ByVal p2 As Point)
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.Rectangle(Me.m_hdc, p1.X, p1.Y, p2.X, p2.Y) 'draw rectangle from point1 to point2.
Me.DisposeResources(g) 'disposes of resources.
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawRectangle
' Draw a rectangle or square from a bounding rectangle
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawRectangle(ByVal g As Graphics, ByVal Bnds As Rectangle)
Me.InitPenAndBrush(g) 'init pen and brush.
With Bnds
GDI32.Rectangle(Me.m_hdc, .Left, .Top, .Right, .Bottom) 'draw rectangle within bounds.
End With
Me.DisposeResources(g) 'disposes of resources.
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawRoundRect
' Draw a rounded rectangle (obround). Make CornerWidth and CornerHeight equal for symetrical corners
' P1 is the top-left corner, P2 is the bottom Right corner.
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawRoundRect(ByVal g As Graphics, ByVal p1 As Point, ByVal p2 As Point,
ByVal CornerWidth As Integer, ByVal CornerHeight As Integer)
'if corner radius out of range
If CornerWidth <= 0 OrElse
CornerHeight <= 0 OrElse
CornerWidth >= Math.Abs(p1.X - p2.X) OrElse
CornerHeight >= Math.Abs(p1.Y - p2.Y) Then
DrawRectangle(g, p1, p2) 'just draw a rectangle.
Else
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.RoundRect(Me.m_hdc, p1.X, p1.Y, p2.X, p2.Y, CornerWidth, CornerHeight) 'rounded rectangle point1 to point2.
Me.DisposeResources(g) 'disposes of resources.
End If
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawRoundRect
' Draw a rounded rectangle from a bounding rectangle. Make CornerWidth and CornerHeight equal for symetrical corners.
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawRoundRect(ByVal g As Graphics, ByVal Bnds As Rectangle,
ByVal CornerWidth As Integer, ByVal CornerHeight As Integer)
' if corner radius out of range
With Bnds
If CornerWidth <= 0 OrElse
CornerHeight <= 0 OrElse
CornerWidth >= Math.Abs(.Left - .Right) OrElse
CornerHeight >= Math.Abs(.Top - .Bottom) Then
DrawRectangle(g, Bnds) 'just draw a rectangle.
Else
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.RoundRect(Me.m_hdc, .Left, .Top, .Right, .Bottom, CornerWidth, CornerHeight) 'draw point1 to point2.
Me.DisposeResources(g) 'disposes of resources.
End If
End With
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawObRound
' Draw an ObRound, allowing one to specify a uniform corner radius by specifying just a single corner radius property
' P1 is the top-left corner, P2 is the bottom Right corner.
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawObRound(ByVal g As Graphics, ByVal p1 As Point, ByVal p2 As Point, ByVal CornerRadius As Integer)
 Page 357

'if corner radius out of range


If CornerRadius <= 0 OrElse
CornerRadius >= Math.Abs(p1.X - p2.X) OrElse
CornerRadius >= Math.Abs(p1.Y - p2.Y) Then
DrawRectangle(g, p1, p2) 'just draw a rectangle.
Else
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.RoundRect(Me.m_hdc, p1.X, p1.Y, p2.X, p2.Y, CornerRadius, CornerRadius) 'draw rnd rect point1 to point2.
Me.DisposeResources(g) 'disposes of resources.
End If
End Sub

'-----------------------------------------------------------------------------------------------------------------------------
' Function: DrawObRound
' Draw an ObRound from a bounding rectangle, allowing one to specify a
' uniform corner radius by specifying just a single corner radius property.
'-----------------------------------------------------------------------------------------------------------------------------
Public Sub DrawObRound(ByVal g As Graphics, ByVal Bnds As Rectangle, ByVal CornerRadius As Integer)
'if corner radius out of range
With Bnds
If CornerRadius <= 0 OrElse
CornerRadius >= Math.Abs(.Left - .Right) OrElse
CornerRadius >= Math.Abs(.Top - .Bottom) Then
DrawRectangle(g, Bnds) 'just draw a rectangle.
Else
Me.InitPenAndBrush(g) 'init pen and brush.
GDI32.RoundRect(Me.m_hdc, .Left, .Top, .Right, .Bottom, CornerRadius, CornerRadius) 'draw point1 to point2.
Me.DisposeResources(g) 'disposes of resources.
End If
End With
End Sub
#End Region
End Class

Emulating a Selection Rubber Band Under GDI.


Suppose we wanted to define a Selection Rubber Band on an image, such as a background image on a
form. We can do this quite easily with the previously defined GDI32 class. The code required to support
this is very simple (I keep forcing myself to avoid the pun of saying it is very basic).
Create a new VB Windows project. Add a background image to it, if we wish. Also, if we choose to, add
some controls to dress it up (unlike all other rubber band examples we may have seen, this one actually
works exactly as we would expect of normal rubber band operations, where the rubber band will also
cover any controls it passes over on the form). Then, in the Form1 code, add the following:
Public Class Form1
Friend m_GDI As New GDI32 'instantiate an instace of the GDI class.
Friend m_StartPoint As Point 'keep track of starting point.
Friend m_LastPoint As Point 'keep track of last mouse location.

'**************************************************************************************
' Subroutine: Form1_MouseDown
' When the mouse select button is down, init the rectangle start and end points.
'**************************************************************************************
Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
m_GDI.PenColor = Color.White 'set color to white (black will be invisible in an XOR operation).
m_GDI.BrushColor = Color.Transparent 'hide the brush.
m_GDI.RasterOp = RasterOps.R2_XOrPen 'use XOR to emulate rubberbanding.
m_StartPoint = e.Location 'set the start and end locations to the current mouse position.
m_LastPoint = e.Location
End Sub

'**************************************************************************************
' Subroutine: Form1_MouseMove
' If the mouse button is down, erase the old rubberband, update the location,
' then draw the new rubberband.
'**************************************************************************************
Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseMove
If e.Button = MouseButtons.Left Then 'if the mouse select button is down.
DrawRubberBand() 'erase the old rectangle at the old location.
m_LastPoint = e.Location 'update the location.
DrawRubberBand() 'draw the new rectange to the new location.
End If
End Sub

'**************************************************************************************
' Subroutine: Form1_MouseUp
' Erase the selection rubber band, report selection data.
'**************************************************************************************
Private Sub Form1_MouseUp(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseUp
 Page 358

DrawRubberBand() 'erase the old rectangle.


Dim Ctls As New Collections.Generic.List(Of Control) 'list to collect controls selected.

Dim Msg As String = "The selection rectangle was from (" &
m_StartPoint.X.ToString & "," &
m_StartPoint.Y.ToString & ") to (" &
m_LastPoint.X.ToString & "," &
m_LastPoint.Y.ToString & ")" & vbCrLf & vbCrLf

' define a new rectangle to compare against any controls on the form.
Dim Rect As Rectangle = NormalizeSelectRectangle()
' find which controls were selected.
For Each Ctl As Control In Controls
If Ctl.Visible AndAlso Ctl.Bounds.IntersectsWith(Rect) Then 'a control intersection was found.
Ctls.Add(Ctl)
End If
Next

If Ctls.Count = 0 Then
Msg &= "There were no controls selected" 'if no controls were selected.
Else
Msg &= "The follows controls were also selected:" & vbCrLf 'else list the controls selected.
For Each Ctl As Control In Ctls
Msg &= " " & Ctl.Name & vbCrLf
Next
End If
'report results
MsgBox(Msg, MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "Selection Result")
End Sub

'**************************************************************************************
' Function: NormalizeSelectRectangle
' Defind the selection rectangle and normalize its definition, where the start point is
' always upper-left, and the ending point is always lower-right.
'**************************************************************************************
Private Function NormalizeSelectRectangle() As Rectangle
' define the final selection rectangle.
Dim X As Integer = m_StartPoint.X 'get start point.
Dim Y As Integer = m_StartPoint.Y
If X > m_LastPoint.X Then X = m_LastPoint.X 'NORMALIZE COORDINATES if inverted in any way.
If Y > m_LastPoint.Y Then Y = m_LastPoint.Y
' define a new rectangle to draw our current rectangle, and compare against any controls on the form.
Return New Rectangle(X, Y, Math.Abs(m_LastPoint.X - m_StartPoint.X), Math.Abs(m_LastPoint.Y - m_StartPoint.Y))
End Function

'**************************************************************************************
' Subroutine: DrawRubberBand
' Draw or erase the rubberband, taking advantage of XOR Raster method. Also draw/erase
' over controls that may be placed on the form.
'**************************************************************************************
Private Sub DrawRubberBand()
' define a new rectangle to draw our current rectangle, and compare against any controls on the form
Dim Rect As Rectangle = NormalizeSelectRectangle()
m_GDI.DrawRectangle(Me.CreateGraphics, Rect) 'erase the old rectangle or draw the new one.
For Each Ctl As Control In Controls 'also process any controls it intersects with
If Ctl.Visible AndAlso Ctl.Bounds.IntersectsWith(Rect) Then 'a control intersection was found.
' erase the old rectangle or draw the new one over control, offsetting the rubberband as needed
m_GDI.DrawRectangle(Ctl.CreateGraphics, New Rectangle(Rect.Left - Ctl.Left,
Rect.Top - Ctl.Top,
Rect.Width, Rect.Height))
End If
Next
End Sub
End Class

NOTE: Unlike any other GDI example on the web, this is the very first one to demonstrate a feature that many gurus
have typically declared to be too difficult – the above DrawRubberBand method draws the selection rubber band over
any controls it crosses. Most gurus considered this control-inclusive technique to be too complicated and deemed it an
advanced topic that cannot be covered in a single article. But, as we can see, my solution was unbelievably simple
(actually, the solution became simple due to techniques I had already developed to easily emulate VB6 image controls
with transparent backgrounds on a form – see the article, Emulating VB6 Image Control Features Under VB.NET on page
243).
 Page 359

Now, once we have added the previously defined GDI32 class to it, run
the project. It emulates the rubber band function very smoothly. Because
it is drawing to the form’s surface, any controls we place on the form will
be in front of the rubber band, unless we reflect it to the surface of each
control as well. The NormalizeSelectRectangle method normalizes
coordinates so that no matter how we make our selection, the coordinate
addressing will be standardized. The highlighted code in the
DrawRubberBand method shows us how simple it is to actually reflect the
rubber band to the surface of each control that it intersects with.

Emulating a Selection Rubber Band Under GDI+.


Suppose we wanted to simply emulate a Selection Rubber Band under the graphics support that is
already built into .NET, using GDI+. We can do that as well, though if we are not very careful in the way
we write it, I think it can also look much less professional than the GDI method, and I have seen some
very bad examples on the web at some blog and support sites – but these folks are just trying to get
people pointed in the right direction to solving their own problems. For instance, with some examples, I
often see the selection rectangle disappear after about one second if we held it still. But if it is rendered
and handled properly, as I will demonstrate below, it will work great and it will look clean.

Using the GDI+ DrawReversibleFrame Method.


The typical technique developers advise us to use for this task under
GDI+ (and the technique I actually like the least) is to implement the
ControlPaint.DrawReversibleFrame method. Unlike the GDI method, this
method draws to the screen over the top of everything, which is useful for
also conveniently covering form controls without the need for additional
code. However, because it does draw to the whole screen, we must in turn
translate our rectangle’s start coordinates to coordinates relative to the
screen, which we can do using the PointToScreen method as we will see in
the new DrawRubberBand method, below. Unlike the previous example, this
GDI+ example does not require our GDI32 class.
NOTE: This method can look goofy and loses its luster if we move the selection outside the target form, where it will leave
graphical artifacts on the screen, though no real damage is caused and it cleans easily. As such, I will check for this within
the form code and automatically correct for that. But regardless, it can also at times leave tiny graphical artifacts on certain
controls on the form as well. I do not like this method for this reason and consider it to be the least professional method to
use. But we will cover it here because so many online examples feature it, and people are constantly asking me about it.
Like the GDI Rubber Band example, create a new VB Windows project. Add a background image to it if
we wish. Also add some controls to dress it up if we choose. And, to avoid window flashing, be sure to
also set the form’s DoubleBuffered parameter to True. Then, in the Form1 code, add the following:
Public Class Form1
Friend m_StartPoint As Point 'keep track of starting point.
Friend m_LastPoint As Point 'keep track of last mouse location.

'**************************************************************************************
' Subroutine: Form1_MouseDown
' When the mouse select button is down, init the rectangle start and end points.
'**************************************************************************************
Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseDown
m_StartPoint = e.Location 'set the start and end locations to the current mouse position.
m_LastPoint = e.Location
End Sub

'**************************************************************************************
' Subroutine: Form1_MouseMove
' If the mouse button is down, erase the old rubberband, update the location,
' then draw the new rubberband.
'**************************************************************************************
Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseMove
If e.Button = Windows.Forms.MouseButtons.Left Then 'if the mouse select button is down.
Dim Rect As Rectangle = NormalizeSelectRectangle() 'get current rectangle.
 Page 360

Rect.Width += 1 'bump 1 for w/h because drawing edge is 1 higher than rect.
Rect.Height += 1
Me.Invalidate(Rect) 'invalidate that region.
Me.Update() 'update display of that region.
m_LastPoint = e.Location 'update the location.
DrawRubberBand() 'draw the new rectange to the new location.
End If
End Sub

'**************************************************************************************
' Subroutine: Form1_MouseUp
' Erase the selection rubber band, report selection data.
'**************************************************************************************
Private Sub Form1_MouseUp(ByVal sender As Object, ByVal e As MouseEventArgs) Handles Me.MouseUp
Me.Refresh() 'refresh form and controls, erasing rectangles.
Dim Ctls As New Collections.Generic.List(Of Control) 'list to collect controls selected.

Dim Msg As String = "The selection rectangle was from (" &
m_StartPoint.X.ToString & "," &
m_StartPoint.Y.ToString & ") to (" &
m_LastPoint.X.ToString & "," &
m_LastPoint.Y.ToString & ")" & vbCrLf & vbCrLf

' define a new rectangle to compare against any controls on the form.
Dim Rect As Rectangle = NormalizeSelectRectangle()
' find which controls were selected.
For Each Ctl As Control In Controls
If Ctl.Visible AndAlso Ctl.Bounds.IntersectsWith(Rect) Then 'a control intersection was found.
Ctls.Add(Ctl)
End If
Next
If Ctls.Count = 0 Then
Msg &= "There were no controls selected" 'if no controls were selected.
Else
Msg &= "The following controls were also selected:" & vbCrLf 'else list the controls selected.
For Each Ctl As Control In Ctls
Msg &= " " & Ctl.Name & vbCrLf
Next
End If
' report results.
MsgBox(Msg, MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "Selection Result")
End Sub

'**************************************************************************************
' Function: NormalizeSelectRectangle
' Define the selection rectangle and normalize its definition, where the start point is
' always upper-left, and the ending point is always lower-right.
'**************************************************************************************
Private Function NormalizeSelectRectangle() As Rectangle
' define the final selection rectangle
Dim X As Integer = m_StartPoint.X 'get start point.
Dim Y As Integer = m_StartPoint.Y
If m_LastPoint.X < 0 Then m_LastPoint.X = -1 'prevent selection from going outside top-left of frame.
If m_LastPoint.Y < 0 Then m_LastPoint.Y = -1
If X > m_LastPoint.X Then X = m_LastPoint.X 'NORMALIZE COORDINATES if inverted in any way.
If Y > m_LastPoint.Y Then Y = m_LastPoint.Y
' define a new rectangle to draw our current rectangle, and compare against any controls on the form.
Return New Rectangle(X, Y, Math.Abs(m_LastPoint.X - m_StartPoint.X), Math.Abs(m_LastPoint.Y - m_StartPoint.Y))
End Function

'**************************************************************************************
' Subroutine: DrawRubberBand
' Draw or erase the rubberband, taking advantage of XOR Raster method. Also draw/erase
' over controls that may be placed on the form.
'**************************************************************************************
Private Sub DrawRubberBand()
' define a new rectangle to draw our current rectangle.
Dim Rect As Rectangle = NormalizeSelectRectangle()
With Me.ClientRectangle
If Rect.Left + Rect.Width > .Width Then 'make sure selection, which is actually drawn to the screen, not
Rect.Width = .Width - Rect.Left + 1 'to the form, will not exceed the bounds of the form's client
End If 'area, otherwise the selection rectangle will be reflected to the
If Rect.Top + Rect.Height > .Height Then 'screen area not covered by the form, and will look very
Rect.Height = .Height - Rect.Top + 1 'unprofessional. These two checks will eliminate that problem.
End If
End With
' draw the selection rectangle to the actual screen, not to the form and its controls.
ControlPaint.DrawReversibleFrame(New Rectangle(PointToScreen(Rect.Location), Rect.Size), Color.White, FrameStyle.Dashed)
End Sub
End Class
 Page 361

Using the GDI+ DrawRectangle Method and a Translucent Brush.


If we want to emulate the selection rectangle that Windows Vista and
later uses, this is also very easy to do. In fact, I was not able to invent this
method until I figured out how to make the rectangle for the
DrawReversibleFrame method more stable. Only then did I begin to
wonder about how Vista and Windows 7/8 made their translucent
selection rectangle. Granted, I could have simply used the GDI+
DrawRectangle method to emulate the previous examples, once I
discovered that the Invalidate, Refresh, and Update methods could be
used to make the GDI+ DrawReversibleFrame method appear more
professional-looking. However, because it was now so stable, especially with the form’s DoubleBuffered
property set to True, I felt that I had to figure it out.
NOTE: Setting the DoubleBuffered property for a form to True is important to cut down on screen flicker. With this property
set to True, instead of updating the screen as the user draws, the .NET support system will wait until it is between display
refresh cycles to update the screen so that display updates, and flicker, while drawing is going on, will be minimized.
The only real problem I was running into was finding out how to create a translucent brush (actually a
no-brainer), and no one seemed to have ready answers on the internet – only questions about how to
create one. Granted, I fully understood how one was defined within a 32-bit integer block as ARGB,
where the 0-255 alpha-channel member determined the color’s opacity or transparency, but being at the
time new to VB.NET, I was not yet aware of the language features that supported its actual construction.
That is, until I happened upon an obscure example for using .NET’s SolidBrush method while studying
brush creation on MSDN.
Creating a translucent brush simply involves assigning a color value to a brush. The color value is one of
the common ARGB colors that are typical to the WinXP+ operating systems. Unfortunately, most people
seem to be at a loss as to how to do that with an alpha blended color value. Worse, most examples of the
SolidBrush method specify just a single color parameter and leave it at that.
The trick, though, is to realize that a color can be multiply defined. Consider the following examples:
Dim Clr As Color = Color.Navy 'assign Clr the color Navy.
Clr = Color.FromName("Navy") 'assign Clr the color Navy.
Clr = RGB(0, 0, 80) 'assign Clr the color Navy (Red, Green, Blue).
Clr = Color.FromArgb(Color.Navy.ToArgb) 'assign Clr the integer value of Navy.
Clr = Color.FromArgb(128, 0, 0, 80) 'Assign Clr an alpha blend value of 128 (1/2 opaque) for the color Green (A,R,G,B).
Clr = Color.FromArgb(0, 0, 80) 'Assign Clr the color Navy ([Alpha=255; fully opaque], Red=0, Green=0, Blue=80).
Clr = Color.FromArgb(64, Color.Navy) 'Assign Clr an alpha blend value of 64 (1/4 opaque, 3/4 transparent) for the color Navy.

With this knowledge, we can create a brush with a ½ transparent Light Blue color using the following:
Dim Brsh As New SolidBrush(Color.FromArgb(128, Color.LightBlue)) 'define translucent brush with 50% translucency

To implement it, we need only fill a rectangle definition with the brush:
Me.CreateGraphics.FillRectangle(Brsh, Rect) 'do fill with translucient brush (we can also define the new brush here).
 Page 362

Hence, replace the DrawRubberBand method in the previous example with the following two methods:
'**************************************************************************************
' Subroutine: DrawRubberBand
' Draw the rubberband. Also draw over controls that may be placed on the form.
'**************************************************************************************
Private Sub DrawRubberBand()
' define a new rectangle to draw our current rectangle, and compare against any controls on the form.
Dim Rect As Rectangle = NormalizeSelectRectangle()
DrawBand(Me.CreateGraphics(), Rect) 'draw selection rectangle for form.
For Each Ctl As Control In Controls 'also process any controls it intersects with.
If Ctl.Visible Then 'if control can be seen.
Ctl.Refresh() 'always refresh control surface, to deal with artifacts left behind.
If Ctl.Bounds.IntersectsWith(Rect) Then 'a control intersection was found.
' erase the old rectangle or draw the new one over control, offsetting the rubberband as needed.
DrawBand(Ctl.CreateGraphics, New Rectangle(Rect.Left - Ctl.Left, Rect.Top - Ctl.Top, Rect.Width, Rect.Height))
End If
End If
Next
End Sub

'**************************************************************************************
' Subroutine: DrawBand
' Draw using a Vista/Win7-style selection rubberband
'**************************************************************************************
Private Sub DrawBand(ByVal eg As System.Drawing.Graphics, ByVal Rect As Rectangle)
Dim VistaClr As Color = Color.FromArgb(255, 51, 153, 255) 'use Vista+ Sel color (Alpha=255: fully opaque).
eg.DrawRectangle(New Pen(VistaClr), Rect) 'draw outer edge of rectangle.
eg.FillRectangle(New SolidBrush(Color.FromArgb(64, VistaClr)), Rect) 'fill w/translucient brush (A=64; 1/4 opaque).
End Sub

We now have Vista-style rubber band selection.


NOTE: Some people have reported that they have this type of selection rectangle under Windows XP. My XP platform uses
just a rectangle outline. Perhaps they have an enhancement installed, or have an option set that I have disabled, because I
thought that this translucent selection rectangle was introduced with Windows 6.0 (Vista. By the way, Windows 7 is actually
Windows 6.1, and Windows 8 is actually Windows 6.2, Windows 8.1 is actually Windows 6.3, and Windows 10 is actually
(finally) Windows 10 – there goes the Microsoft marketing department, again).
 Page 363

Extending VB.NET Control Functionality.


VB.NET seems to include the VB6 controls we may have grown accustomed to, but these seemingly familiar
controls are now enveloped in incredibly enhanced class wrappers beyond VB6 implementations, and many
VB6 complaints about VB,NET differences stem from these powerful .NET control extensions.
When we begin using these controls under VB.NET they might first seem to simply exhibit properties that
were part of their VB6 predecessors, with the exception being that we may have noticed that the ComboBox,
Collection, and ListBox now store Objects as data, not just simple strings, and they now all sport a uniform
Items collection. But under deeper scrutiny we are going to discover that the VB.NET implementation of
these objects are even more profoundly different, loaded not only with the properties that we may have
become accustomed to, but also with a generous plethora of new properties. Further, these newer objects also
feature a generous repertoire of built-in functions – something that VB6 controls totally lacked. All these
enhancements expand them into much more potent and much more useful tools.
Consider the VB6 properties offered for the ComboBox control, as shown to the
right. This was pretty much all the power that the VB6 interface granted us. If we
wanted to do anything beyond what these properties offered, we were forced to
extend it using system Messages through the Win32 API.
Messages, essentially, are instructions that are added to an application’s message
queue, and are in turn processed by a Window Processor function (a WndProc in
programmer lingo). Each message is an integer flag value that is associated with
a specific purpose, such as a task, that is understood by its target object. Further,
each message is encapsulated within a bundle that includes a unique identifier for
the target object the message is specifically directed at, plus two additional
values that can contain or point to any ancillary data.
The unique identifier of the target object is often referred to as a Window
Handle. Under VB6, reflecting how they were referred to in the C++ developer
community, they were called hWnd, but .NET simply refers to them by the more
meaningful term of Handle, which clearly describes exactly what they are.
NOTE: All form controls are actually individual windows that simply identify a form (yet another window) as their parent.
If we want to extend the functionality of a control, we have to make one of three choices: 1) we can
write our own code to extend it, 2) we can transmit a meaningful Win32 message to the control to extend
it, or 3) we can mix our code and Win32 messages to perform a marriage of tasks to extend it.

Demonstrating Control Extension Using the VB.NET ComboBox (and ListBox).


For example, under VB6, if we wanted a ComboBox to display its drop down list through code alone, we
had to use a Pinvoke to do so. Consider the following VB6 method that can be used to do just that:
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _
ByVal hWnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Any) As Long

Private Const CB_SHOWDROPDOWN = &H14F 'Message code used to display or hide the dropdown list of a ComboBox

'Set Cmd to 1 to ensure the Dropdown is displayed, or to 0 to ensure it is hidden


Public Sub ShowCboDropDown(ByVal hWnd As Long, Optional ByVal Cmd As Long = 1)
Call SendMessage(hWnd, CB_SHOWDROPDOWN, Cmd, 0&)
End Sub

With the above code, we could issue a command, such as ShowCboDropDown(Me.Combo1.hwnd, 1), to ensure
that the dropdown list for the ComboBox control, Combo1, was displayed.
When we moved on to VB. NET, we may have gone through all the trouble of upgrading this code (as I
had done) because we had thought that we would simply have to do so, so that we would not lose this
feature’s functionality, especially if this functionality was critical to the operation of our application.
 Page 364

For example, consider the following VB. NET fully-functional rendering of the above VB6 code:
' Sends the specified message to a window or windows. The SendMessage function invokes the window procedure
' for the specified window and does not return until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As Integer) As Boolean
' The CB_SHOWDROPDOWN message will tell a ComboBox to either show or hide its dropdown list.
' The wParam parameter is set to 1 to ensure the listbox is shown. 0 will ensure that it is hidden.
' The lParam parameter is not used.
Private Const CB_SHOWDROPDOWN As Integer = &H14F
'***************************************************************
' ShowCboDropDown(): Force the listbox of a ComboBox to be displayed or hidden through software.
' Cmd=1 to show the list, if not shown, and Cmd=0 will hide it, if it is not already hidden.
'***************************************************************
Public Function ShowCboDropDown(ByVal sender As Object, Optional ByVal Cmd As Integer = 1) As Boolean
If TypeOf sender Is ComboBox Then 'ensure the object is a ComboBox.
Return SendMessage(DirectCast(sender, ComboBox).Handle, CB_SHOWDROPDOWN, Cmd, 0) 'tell the list to be shown (Cmd=1) or hidden (Cmd=0).
End If
Return False 'failure.
End Function

Yet, once we explore the rich list of features for the VB.NET ComboBox, we will
discover that most Pinvokes we had depended so heavily upon under VB6 is no
longer required. In apparent counterpoint, were we to examine the reported VB.
NET ComboBox properties, as shown to the right, we might not see anything that
might preclude the need for the above code, but we must recognize that a Control
under VB.NET, as an object, has an extensive battery of additional features that are
only available for runtime operation that cannot be set through properties; that it
has a superfluity of built-in methods that are designed to support the control.
Being mindful of that, were we to scan the list under ComboBox Class,
encompassing methods, properties, and events, such as we will find on MSDN (see
http://msdn.microsoft.com/en-us/library/system.windows.forms.combobox.aspx), we would find that this huge
ComboBox member catalog is almost overwhelming.
And as we wade through it, we will discover that most-all things that we may have
previously had to, or still do perform Pinvokes for can now be processed through
simple properties or methods under VB.NET (I constantly rediscover this).
For example, under that extensive list of properties, we will find a runtime-only
property named DroppedDown. Were we to set this Boolean property to True, then the
dropdown list for the ComboBox will immediately display. If we set it to False, the
dropdown list will be removed. Hence, the above ShowCboDropDown method can now be
removed and replaced by merely issuing a simple command, such as
Me.ComboBox1.DroppedDown = True. We can also check this property, instead, if we
simply want to discover if its current state is opened or closed.

Win32 ComboBox Messages.


By now I may have perked your interest. You may be wondering: what is this mysterious list of control
message incantations David has been babbling about?
If we again visit MSDN and look at ComboBox Control Messages (see http://msdn.microsoft.com/en-
us/library/ff485901(VS.85).aspx), we will find a list of 39 entries, each of which we can individually explore by clicking
on them. It lists the following Win32 messages (we will examine highlighted entries in detail):
CB_ADDSTRING = &H143 CB_GETDROPPEDSTATE = &H157 CB_GETMINVISIBLE = &H1702 CB_SETEDITSEL = &H142
CB_DELETESTRING = &H144 CB_GETDROPPEDWIDTH = &H15F CB_GETTOPINDEX = &H15B CB_SETEXTENDEDUI = &H155
CB_DIR = &H145 CB_GETEDITSEL = &H140 CB_INITSTORAGE = &H161 CB_SETHORIZONTALEXTENT = &H15E
CB_FINDSTRING = &H14C CB_GETEXTENDEDUI = &H156 CB_INSERTSTRING = &H14A CB_SETITEMDATA = &H151
CB_FINDSTRINGEXACT = &H158 CB_GETHORIZONTALEXTENT = &H15D CB_LIMITTEXT = &H141 CB_SETITEMHEIGHT = &H153
CB_GETCOMBOBOXINFO = &H164 CB_GETITEMDATA = &H150 CB_RESETCONTENT = &H14B CB_SETLOCALE = &H159
CB_GETCOUNT = &H146 CB_GETITEMHEIGHT = &H154 CB_SELECTSTRING = &H14D CB_SETMINVISIBLE = &H1701
CB_GETCUEBANNER = &H1704 CB_GETLBTEXT = &H148 CB_SETCUEBANNER = &H1703 CB_SETTOPINDEX = &H15C
CB_GETCURSEL = &H147 CB_GETLBTEXTLEN = &H149 CB_SETCURSEL = &H14E CB_SHOWDROPDOWN = &H14F
CB_GETDROPPEDCONTROLRECT = &H152 CB_GETLOCALE = &H15A CB_SETDROPPEDWIDTH = &H160
 Page 365

Of this list of ComboBox messages, VB6's ComboBox control wrapper implements functionality for only a
few of them. Most, though, lack any direct interface via VB6. Conversely, under VB.NET, almost all of
them are supported either through properties or methods. Of those it does not, we will find CB_DIR,
CB_GETTOPINDEX, CB_SETTOPINDEX, CB_GETEXTENDEDUI, CB_SETEXTENDEDUI, and CB_SETCURSEL.

The CB_DIR Message (and how to make it work under VB.NET).


The CB_DIR message adds names, such as drives, directories, and files to the dropdown list of a
ComboBox. The names must match a specified mask string and a set of file attribute flags.
Under VB6, the CB_DIR message worked great, but after I upgraded it to VB.NET, I tested it. At first it
seemed to work. As expected, it duly filled the dropdown list. The dropdown list even properly displayed
when dropped down. But I quickly discovered that it did only half the job. Were we to afterward click on any
entry within that dropdown list, an exception error would result, telling me that I had selected an invalid
index. This resulted from the fact that the Items collection within the ComboBox was still empty. Huh? It was
time to put on my thinking cap...
First, consider the following code portion of my original VB.NET translation of my working VB6 code:
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As String) As Integer

Private Const CB_DIR As Integer = &H145 'dir functions for a ComboBox.


Private Const LB_DIR As Integer = &H18D 'dir functions for a ListBox.

Public Enum DirSpecTypes As Integer


PS_NORMAL = &H0 'all normal files.
PS_READONLY = &H1 'is locked against alteration.
PS_HIDDEN = &H2 'hidden (file or directory).
PS_SYSTEM = &H4 'system type files or directories.
PS_DIRECTORY = &H10 'include directory type entries.
PS_ARCHIVE = &H20 'archive flag set (has been altered since last backup).
PS_DRIVES = &H4000 'include drive type entries.
PS_EXCLUSIVE = &H8000& 'display files that are exclusively of a selected non-PS_NORMAL types.
End Enum

'******************************************************************************
' CBLBDirFill(): Fills a ComboBox or a ListBox control a list of Drives, directories
' or files, with optional attributes, as specified in the DirSpecTypes Enumerated list.
' Enumerated list. Change to an APPEND function by removing the .xxxItems.Clear lines.
'******************************************************************************
Public Function CBLBDirFill(ByVal obj As Object, ByVal PathSpec As String, ByVal SpecType As DirSpecTypes) As Integer
Dim pSpec As String = Trim(PathSpec) 'grab pathspec
If Len(pSpec) = 0 Then pSpec = "*.*" 'all if nothing
If Right(pSpec, 1) = "\" Then pSpec &= "*.*" 'if it ends in a backslash, add *.*

Select Case obj.GetType.Name 'check type of control


Case "ComboBox"
Dim Cbo As ComboBox = DirectCast(obj, ComboBox)
Cbo.Items.Clear() 'clear the ComboBox
Return SendMessage(Cbo.Handle, CB_DIR, SpecType, S) + 1 'NOTE: THIS FILLS THE DROPDOWN LIST, BUT NOT THE ITEMS COLLECTION.
Case "ListBox"
Dim Lst As ListBox = DirectCast(obj, ListBox)
Lst.Items.Clear() 'clear the ListBox
Return SendMessage(Lst.Handle, LB_DIR, SpecType, S) + 1 'NOTE: THIS FILLS THE DISPLAY LIST, BUT NOT THE ITEMS COLLECTION.
Case Else
Return -2 'failure, not ComboBox or ListBox.
End Select
End Function

The reason for the seeming paradox of being able to fill the dropdown list with viewable strings ( actually an
Array of Strings) and yet the Items collection still being empty is all due to the ways that a VB6 and a
VB.NET ComboBox are handled. Under VB6, the Items list directly accessed the string array entries stored
within the dropdown list itself. Under VB.NET, this functionality has changed. Even though its dropdown list
object will still display the string array in its list, as was quite apparent (this string array still exists in
VB.NET, still being the same list used by the VB6 control), its Items list is now completely separate, no
longer being a property mapped directly to that internal string array, but it is now a disconnected collection.
The reason for this change is that under VB.NET the Items list is now a collection of objects, which makes it
tremendously more powerful and extensible. The dropdown list for a ComboBox, however, just like VB6,
still stores an internal string array, but it is now relegated the mundane task of storing the text representations
of the Items collection objects, rather than being the main storage medium as it was under VB6.
NOTE: This is why it is important for the objects stored within a ComboBox or ListBox to have a meaningful ToString
method, so that the string versions of the objects can be represented as we would expect within their displayed lists.
 Page 366

Hence, the CB_DIR/LB_DIR message only fills the dropdown with a list of strings, but it ignores
completely the Items collection because it was never designed for that, being ignorant of .NET objects.
Microsoft chose not to provide support for CB_DIR under .NET, perhaps because they thought it so
little-used. Besides, they could argue that other methods are available that perform similar jobs. So much
so, even the VB6 DriveListBox, DirListBox, and FileListBox controls were not carried directly over
into VB.NET, mainly due to VB.NET featuring very powerful file and folder browsers, even though
there was still plenty of potential application for each of these controls (the end of this article will show
us how to easily add the VB6 DriveListBox, DirListBox, and FileListBox back into our Toolbox).
There is still practical use for the CB_DIR message when developers want system lists, but it appears
that Microsoft felt that, with just a bit of code, the developer could sufficiently emulate any needed
functionality. I will admit that it is do-able using only a few lines of code if all we wanted to do was to
display a list of files that match a certain mask, but the much greater code brevity available using the
CB_DIR message is sorely missed in cases where we would need to fully emulate the CB_DIR
functionality. In this case, it requires more than just a bit of code, but rather a boatload of those bits.
However, the partial result obtained after we pass the CB_DIR message to a ComboBox under VB.NET is
fully salvageable if we add just a few lines of code. Even so, this will still be significantly less code than
that which would be required if we were to resort to emulating the full CB_DIR (or LB_DIR) functionality.
Because the dropdown list will contain a directory list after a CB_DIR message, we can augment this
partial .NET result using the Win32 CB_GETLBTEXT message, because it will extract entries from the
dropdown list, not from the Items collection, and with that we can assign those entries to the Items collection.
Consider the following VB.NET version, which now works perfectly (new/updated code is highlighted):
Option Strict On
Option Explicit On

Module modCBLBDirFill
'Fill ComboBox or ListBox with directory list of drives, dir's, files.
'******************************************************************************
' modCBLBDirFill - The CBLBDirFill() function will fill a ComboBox or a ListBox
' control with a list of Drives, directories, or files, with optional
' attributes, as specified in the DirSpecTypes Enumerated list.
' DirSpecTypes List:
' PS_NORMAL Default. All files that are read/write are included in the list.
' PS_READONLY Readonly files are included (most useful if used with PS_EXCLUSIVE,
' otherwise not of much use).
' PS_HIDDEN Hidden files are included in the list.
' PS_SYSTEM Include system files in the list.
' PS_DIRECTORY includes subdirectories in the list. Subdirectories are enclosed
' in square brackets ([]).
' PS_ARCHIVE Files with the archive flag set are included (most useful if used
' with PS_EXCLUSIVE, otherwise not of much use).
' PS_DRIVES includes drives in the list. Drives are listed in the form "-X-",
' where X is the drive letter.
' PS_EXCLUSIVE Only files with the specified attributes are put in the list. By default,
' normal files are listed, even if PS_NORMAL is not specified. For example,
' included with PS_DRIVES, then only drives will be in the list. Included
' with PS_DIRECTORY, only subdirectories will be in the list.
'
' NOTE: The function returns the number of items inserted in the list. If -1, then there
' was not enough space to fill the list. If -2, the supplied object was not a
' ComboBox or ListBox.
'EXAMPLE:
' Dim BasePath As String = "C:\Program Files" 'base path to scan
' MyListBox.Items.Clear 'make fresh list
' Dim I As Integer = CBLBDirFill(MyListBox, BasePath & "\*.*", PS_DIRECTORY Or PS_EXCLUSIVE)
' Select Case I
' Case -2
' MsgBox "Object not a ComboBox or ListBox"
' Case -1
' MsgBox "Not enough space to fill the list"
' Case 0
' MsgBox "There was nothing to read"
' Case Else
' MsgBox "There were " & CStr(I) & " subdirectories under " & BasePath
' End Select
'******************************************************************************
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
 Page 367

ByVal wMsg As Integer,


ByVal wParam As Integer,
ByVal lParam As String) As Integer

Private Const CB_DIR As Integer = &H145 'dir functions for a ComboBox (must acceess its attached ListBox),
Private Const LB_DIR As Integer = &H18D 'dir functions for a ListBox,
Private Const CB_GETLBTEXT As Integer = &H148 'gets a string from the list of a ComboBox.
Private Const CB_GETLBTEXTLEN As Integer = &H149 'gets the length, in chars, of a string in the list of a ComboBox.
Private Const LB_GETTEXT As Integer = &H189 'gets a string from the index of a ListBox.
Private Const LB_GETTEXTLEN As Integer = &H18A 'gets the length, in chars, of a string from the index of a ListBox.

Public Enum DirSpecTypes As Integer


PS_NORMAL = &H0 'all normal files.
PS_READONLY = &H1 'is locked against alteration.
PS_HIDDEN = &H2 'hidden (file or directory).
PS_SYSTEM = &H4 'system type files or directories.
PS_DIRECTORY = &H10 'include directory type entries.
PS_ARCHIVE = &H20 'archive flag set (has been altered since last backup).
PS_DRIVES = &H4000 'include drive type entries.
PS_EXCLUSIVE = &H8000& 'display files exclusive of selected non-PS_NORMAL types.
End Enum

'******************************************************************************
' CBLBDirFill(): Fills a ComboBox or a ListBox control a list of Drives, directories
' or files, with optional attributes, as specified in the DirSpecTypes Enumerated list.
' Enumerated list.
'******************************************************************************
Public Function CBLBDirFill(ByVal obj As Object, ByVal PathSpec As String, ByVal SpecType As DirSpecTypes) As Integer
Dim pSpec As String = Trim(PathSpec) 'grab pathspec
If Len(pSpec) = 0 Then pSpec = "*.*" 'all if nothing
If Right(pSpec, 1) = "\" Then pSpec &= "*.*" 'if it ends in a backslash, add *.*

Select Case obj.GetType.Name 'check type of control.


Case "ComboBox"
Dim Cbo As ComboBox = DirectCast(obj, ComboBox) 'set aside a ComboBox reference pointer.
Cbo.Items.Clear() 'clear the ComboBox.
Dim Cnt As Integer = SendMessage(Cbo.Handle, CB_DIR, SpecType, pSpec) 'fill list with listing (return ubound).
Dim Ary(Cnt) As String 'set aside storage for strings.
For Idx As Integer = 0 To Cnt 'gather each entry in the dropdown list.
Dim Tmp As New String(" "c, SendMessage(Cbo.Handle, CB_GETLBTEXTLEN, Idx, vbNullString)) 'set entry space.
SendMessage(Cbo.Handle, CB_GETLBTEXT, Idx, Tmp) 'gather the entry.
Ary(Idx) = Tmp 'add to the array.
Next
Cbo.Items.Clear() 'clear the ComboBox dropdown list.
For idx As Integer = 0 To Cnt 'fill both items collection and list.
Cbo.Items.Add(Ary(idx)) 'stuff each entry.
Next
Return Cnt + 1 'return the count of items added.)
Case "ListBox"
Dim Lst As ListBox = DirectCast(obj, ListBox) 'set aside a ListBox reference pointer
Lst.Items.Clear() 'clear the ListBox
Dim Cnt As Integer = SendMessage(Lst.Handle, LB_DIR, SpecType, pSpec) 'fill list with listing (return ubound.
Dim Ary(Cnt) As String 'set aside storage for strings.
For Idx As Integer = 0 To Cnt 'gather each entry in the dropdown list.
Dim Tmp As New String(" "c, SendMessage(Lst.Handle, LB_GETTEXTLEN, Idx, vbNullString)) 'set entry space.
SendMessage(Lst.Handle, LB_GETTEXT, Idx, Tmp) 'gather the entry.
Ary(Idx) = Tmp 'add to the array.
Next
Lst.Items.Clear() 'clear the ListBox display list.
For idx As Integer = 0 To Cnt 'now fill items collection and list.
Lst.Items.Add(Ary(idx)) 'stuff each entry.
Next
Return Cnt + 1 'return the count of items added.
Case Else
Return -2 'failure, not ComboBox or ListBox
End Select
End Function
End Module

The CB_SETTOPINDEX and CB_GETTOPINDEX Messages.


Sometimes it is important to ensure that a certain entry in a dropdown ComboBox list is visible,
especially in a list that must implement a vertical scroll bar. Typically, we would want to place that
selected item as the topmost displayed item in the list, or at least near the top if it is physically very near
the top, where several entries above it are therefore also forced to be displayed.
At other times, we need to know what is currently displayed as the top item in the ComboBox dropdown.
 Page 368

The ComboBox messages used to establish these two features are CB_SETTOPINDEX and
CB_GETTOPINDEX, which are used to set or get the index of the item that is displayed at the top of the
dropdown list. They are so easy to use and implement that I find it odd that .NET does not include them
as a GET/SET property item for the ComboBox class.
Option Strict On
Option Explicit On

Module modCBLBTopIndex
'***************************************************************
' modCBLBTopIndex - Set or Get the index of the item that is
' visible at the top of a partial ComboBox or ListBox list.
'***************************************************************
'The CB_SETTOPINDEX message is used to ensure that a particular item is visible in the list box of a ComboBox. The system
' scrolls the list box contents so that either the specified item appears at the top of the list box or the maximum scroll
' range has been reached. The wParam parameter specifies the zero-based index of the list item. The lParam parameter is not
' used. If the message is successful, the return value is zero. If the message fails, the return value is CB_ERR (-1).
Private Const CB_SETTOPINDEX As Integer = &H15C

'The CB_GETTOPINDEX message is used to retrieve the zero-based index of the first visible item in the list box portion of a
' ComboBox. Initially, the item with index 0 is at the top of the list box, but if the list box contents have been scrolled,
' another item may be at the top. Parameters wParam and lParam are not used and must be set to zero. If the message is
' successful, the return value is the index of the first visible item in the list box of the ComboBox. If the message fails,
' the return value is CB_ERR (-1).
Private Const CB_GETTOPINDEX As Integer = &H15B

' Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified
' window and does not return until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As Integer) As Integer

'***************************************************************
' CboSetTopIndex(): Ensure that a particular item is visible in the list box of a ComboBox.
' It will be displayed as the top item in the list, unless scrolling and list limits from
' being displayed at the top of the list, though it will be visible.
'
'EXAMPLE:
' CboSetTopIndex(Me.ComboBox1, 25)
'***************************************************************
Public Function CboSetTopIndex(ByVal sender As Object, Optional ByVal Index As Integer = 0) As Boolean
If TypeOf sender Is ComboBox Then 'allow only ComboBox controls.
Dim cbo As ComboBox = DirectCast(sender, ComboBox) 'get the sender as a ComboBox control.
If Index > -1 AndAlso Index < cbo.Items.Count Then 'if the index is within the list bounds...
SendMessage(cbo.Handle, CB_SETTOPINDEX, Index, 0) 'try to set the item as the top-displayed item.
Return True
End If
End If
Return False 'error.
End Function

'***************************************************************
' CboGetTopIndex(): Get the line index of the item that is at the top of the current displayed list.
' A return value of -1 indicates failure. Any other value is the index of the item displayed at the
' top of the list.
'
'EXAMPLE:
' Dim Index as Integer = CboGetTopIndex(Me.ComboBox1)
'***************************************************************
Public Function CboGetTopIndex(ByVal sender As Object) As Integer
If TypeOf sender Is ComboBox Then 'allow only ComboBox controls.
Return SendMessage(DirectCast(sender, ComboBox).Handle, CB_GETTOPINDEX, 0, 0) 'get the item that is the top-item.
End If
Return -1 'error.
End Function
End Module

NOTE: Because most examples also demonstrate their usage with a VB.NET ListBox, I will point out that the ListBox
control has a TopIndex property that already performs the above tasks, so the LB_SETTOPINDEX and LB_GETTOPINDEX
messages are not needed. The ComboBox DOES NOT have an exposed TopIndex property, though I wish that it did.
 Page 369

The CB_SETEXTENDEDUI and CB_GETEXTENDEDUI Messages.


Normally, when a ComboBox has focus, we can hit the F4 key to force its dropdown list to open without
having to click the Dropdown button on the control. However, this is not always something easy for
users to remember, and they will typically, as if by instinct, go for the Down Arrow key.
If we would prefer that our users be able to open the dropdown list using the Down Arrow instead of the
F4 key, this is easy to do using the CB_SETEXTENDEDUI message (ComboBox Set Extended User
Interface). Like the other messages, it is very easy to use and implement, as shown in this module:
Option Strict On
Option Explicit On

Module modExtendedComboDrop
'Toggle Combobox to drop with F4 or DownArrow.
'***************************************************************************************
' modExtendedComboDrop - the ExtendedComboDrop() will tell a combobox to drop when the
' user hits the F4 key (default) or the Down-Arrow.
'EXAMPLES:
' SetExtendedComboDrop(Me.ComboBox1, True) 'force dropping with DownArrow instead of F4.
' SetExtendedComboDrop(Me.ComboBox1, False) 'force F4 for drop (default mode).
'
' To get the current extended user interface status of the ComboBox, issue a command like this:
' Dim Result As Boolean = GetExtendedComboDrop(Me.ComboBox1)
' ' If Result = TRUE, then the extended UI is set. If Result = FALSE, then the extended UI is NOT set.
'***************************************************************************************

' Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified
' window and does not return until the window procedure has processed the message. By default, the F4 key opens or closes
' the list and the DOWN ARROW changes the current selection.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Boolean,
ByVal lParam As Integer) As Integer
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As Integer) As Boolean

' An application sends a CB_SETEXTENDEDUI message to select either the default UI or the extended UI for a ComboBox that
' has the CBS_DROPDOWN or CBS_DROPDOWNLIST style. The wParam parameter contains a boolean value that specifies whether
' the ComboBox uses the extended UI (TRUE) or the default UI (FALSE). The lParam parameter is not used. If the operation
' succeeds, the return value is CB_OKAY (0). If an error occurs, it is CB_ERR (-1). In the extended UI, the F4 key is
' disabled and the DOWN ARROW key opens the drop-down list. The mouse wheel, which normally scrolls through the items in
' the list, has no effect when the extended UI is set.
Private Const CB_SETEXTENDEDUI As Integer = &H155

' Determines whether a ComboBox has the default user interface or the extended user interface. The wParam and the lParam
' are not used and must be zero. If the ComboBox has the extended user interface, the return value is TRUE; otherwise,
' it is FALSE. By default, the F4 key opens or closes the list and the DOWN ARROW changes the current selection. In a
' ComboBox with the extended user interface, the F4 key is disabled and pressing the DOWN ARROW key opens the drop-down list.
Private Const CB_GETEXTENDEDUI As Integer = &H156

Public Sub SetExtendedComboDrop(ByVal sender As Object, Optional ByVal DropWithDownArrow As Boolean = False)
If TypeOf sender Is ComboBox Then
SendMessage(DirectCast(sender, ComboBox).Handle, CB_SETEXTENDEDUI, DropWithDownArrow, 0)
End If
End Sub

Public Function GetExtendedComboDrop(ByVal sender As Object) As Boolean


If TypeOf sender Is ComboBox Then
Return SendMessage(DirectCast(sender, ComboBox).Handle, CB_GETEXTENDEDUI, 0, 0)
End If
End Function
End Module

As shown in the above examples, to tell the system that the specified ComboBox control, such as
ComboBox1, should use the Down Arrow key to drop its list when the control has focus, issue the
command SetExtendedComboDrop(Me.ComboBox1, True). To reset it to where the F4 key (the default) is used
to drop its list, issue the command SetExtendedComboDrop(Me.ComboBox1, False).
If we want to check the state of the Extended User Interface of a specific ComboBox, such as
ComboBox1, assign the result of GetExtendedComboDrop(Me.ComboBox1) to a Boolean variable. If the result
is True, then the Extended User Interface is applied to this control, otherwise it is not.
 Page 370

For example:
If GetExtendedComboDrop(Me.ComboBox1) Then
MsgBox("ComboBox has the Extended User Interface set")
Else
MsgBox("ComboBox DOES NOT have the Extended User Interface set")
End If

NOTE: The ListBox does not have this Extended User Interface simply because it does not feature a dropdown list.

The CB_SETCURSEL Message.


If we set the Index property of a ComboBox or ListBox under VB6, it will trigger a Changed event for that
control. If we set the SelectedIndex or SelectedItem property on a ComboBox or ListBox under
VB.NET, it will trigger a SelectedIndexChanged event for that control, which directly corresponds to the
VB6 Changed event.
Sometimes, however, we might not want the reactive event to fire; we simply want to change the
selection index and keep on processing. Under both VB6 and VB.NET, we cannot normally do this.
Enter the CB_SETCURSEL message (or the LB_SETCURSEL message for a ListBox), which will in fact allow
we to do just that – to change the selection index without triggering a SelectedIndexChanged event.
Its companion message, CB_GETCURSEL (or LB_GETCURSEL for a ListBox), is the same as reading the
Index property of a VB6 ComboBox or ListBox, or the SelectedIndex property of a VB.NET control,
and so is not normally required because the control already directly supports it.
To implement the CB_SETCURSEL (or LB_SETCURSEL) message is as easy as all the other messages that we
have implemented so far. Consider this module:
Option Strict On
Option Explicit On

Module modSetListIndex
'Set the ListIndex of a ListBox or ComboBox without triggering a click event.
'*******************************************************************************
' modSetListIndex - The SetListIndex() function Set the ListIndex of a ListBox
' or ComboBox without triggering a click event, which is what
' will normally happen when the control's listindex is set to
' anything but -1. This function returns TRUE if the control
' afterwards reflects the desired listindex, and FALSE if it
' does not, which might be due to an out of range value.
'*******************************************************************************

' Sends the specified message to a window or windows. The SendMessage function calls the window procedure
' for the specified window and does not return until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Integer,
ByVsl lParam As Integer) As Integer

' An application sends a CB_SETCURSEL message to select a string in the list of a ComboBox. If necessary, the list scrolls
' the string into view. The text in the edit control of the combo box changes to reflect the new selection, and any previous
' selection in the list is removed. The wParam parameter specifies the zero-based index of the string to select. If this
' parameter is –1, any current selection in the list is removed and the edit control is cleared. The lParam parameter is not
' used. If the message is successful, the return value is the index of the item selected. If wParam is greater than the
' number of items in the list or if wParam is –1, the return value is CB_ERR(-1) and the selection is cleared.
Private Const CB_SETCURSEL As Integer = &H14E

' An application sends a CB_GETCURSEL message to retrieve the index of the currently selected item, if any, in the list box
' of a ComboBox. The wParam and lParam parameters are not used and must be zero. The return value is the zero-based index
' of the currently selected item. If no item is selected, it is CB_ERR (-1).
Private Const CB_GETCURSEL As Integer = &H147

' An application sends a LB_SETCURSEL message to select a string in the list of a ListBox. If necessary, the list scrolls
' the string into view. The text in the edit control of the combo box changes to reflect the new selection, and any previous
' selection in the list is removed. The wParam parameter specifies the zero-based index of the string to select. If this
' parameter is –1, any current selection in the list is removed and the edit control is cleared. The lParam parameter is not
' used. If an error occurs, the return value is LB_ERR(-1). If the wParam parameter is –1, the return value is LB_ERR even
' though no error occurred. NOTE: Use this message only with single-selection ListBoxes. You cannot use it to set or remove
' a selection in a multiple-selection list box.
Private Const LB_SETCURSEL As Integer = &H186

' An application sends a LB_GETCURSEL message to retrieve the index of the currently selected item, if any, in the list box
' of a ListBox. The wParam and lParam parameters are not used and must be zero. The return value is the zero-based index
 Page 371

' of the currently selected item. If no item is selected, it is LB_ERR (-1).


Private Const LB_GETCURSEL As Integer = &H188

Public Function SetListIndex(ByVal Obj As Object, ByRef NewIndex As Integer) As Boolean


Select Case Obj.GetType.Name 'check which object to process.
Case "ComboBox"
Dim Cbo As ComboBox = DirectCast(Obj, ComboBox) 'set aside ComboBox reference.
SendMessage(Cbo.Handle, CB_SETCURSEL, NewIndex, 0) 'set it 'set the desired index.
Return (NewIndex = SendMessage(Cbo.Handle, CB_GETCURSEL, 0, 0)) 'return true if it succeeded.
Case "ListBox"
Dim Lst As ListBox = DirectCast(Obj, ListBox) 'set aside ListBox reference.
SendMessage(Lst.Handle, LB_SETCURSEL, NewIndex, 0) 'set it 'set the desired index.
Return (NewIndex = SendMessage(Lst.Handle, LB_GETCURSEL, 0, 0)) 'return true if it succeeded.
End Select
Return False 'fail if not ComboBox or ListBox.
End Function
End Module

For example, to set ComboBox1 to a SelectionIndex of 4 without triggering a SelectedIndexChanged


event, use this command: SetListIndex(Me.ComboBox1, 4).

Adding a CheckBox to the Edit Field of a ComboBox.


Sometime we need to alter the way in which the ComboBox is displayed. An example of this was in
adding icons to the dropdown list entries, as had been demonstrated in an earlier article, Adding Icons to
ListBoxes and ComboBoxes with Ease under VB.NET, on page 377.
Another often-requested alteration is adding a CheckBox to the edit field of a
ComboBox. Something like this would be extremely useful in an options list:
when the user selects an option from the dropdown list, the CheckBox would be
automatically checked, but the user can uncheck it to clearly indicate that they
have decided against that field of options. This also looks much better than
having an entry simply named “None”, and it is also more readily accessible,
especially on long lists where, if the user decided against using one of the
selected options, they had to scroll it up or down to locate the “None” entry.
Although the typical solution to this problem is to just place a CheckBox with no Text property data to
the immediate left of the ComboBox, one must admit that a CheckBox actually embedded within the text
field, and if any text within that field would be displayed to the right of it, is a very cool idea.
Fortunately this is also a simple thing to do. Further, because there are Win32 Pinvokes that allow us to
easily set the text margins within an edit control, plus another Pinvoke that will allow us to retrieve the
handle of the embedded edit control within a ComboBox so that we can change its margins, the ability to
add a CheckBox to the ComboBox turns out to be a quick and straightforward thing to do.
To place a CheckBox on the TextBox within the ComboBox is the really easy part (the hard part is saying
that, especially 3 times really fast). We need to set the Text property of the CheckBox to blank
(Me.CheckBox1.Text = Nothing), we need to set its back color to the same as the ComboBox
(Me.CheckBox1.BackColor = Me.ComboBox1.BackColor), we need to size the CheckBox to its minimum
dimensions (Me.CheckBox1.Width = Me.CheckBox1.Height), we need to neatly place the CheckBox within the
TextBox in such a way as to make it look like it is a part of the ComboBox control (2 pixels from the left
and vertically centered within the ComboBox looks good), and we need to ensure that the CheckBox is
displayed in front of the ComboBox (Me.CheckBox1.BringToFront).
NOTE: The CheckBox is still its own entity, and we would write code for it just as we would for any other CheckBox.
Pinvokes are needed to obtain the Window Handle of the edit field control that is embedded within the
ComboBox, and to use that Handle to adjust the left margin of this edit field rightward so that any text
within the ComboBox will not be hidden behind the CheckBox.
 Page 372

The FindWindowEx Pinvoke is used to search for named objects (windows) embedded within a specified
window object (meaning that it will search for named child windows of a specified window), returning
the handle of the found control. All classes have a name. The TextBox control that is embedded in a
ComboBox has a class name of “EDIT”. If we are looking for a particular class item with a specific
window name, we can optionally supply the window name as an additional parameter.
Child windows are ordered as in a list. We can scan for like-named objects using an IntPtr value to
indicate which handle to scan after. Using a handle of zero (null) will start from the beginning of this
‘list’. However, because a ComboBox has only one Edit control, we can quickly find it by specifying the
parent handle, that we should start at the start of the list, that the class name is “EDIT”, and we will
accept any window matching that class name by not supplying a window name, using a command like
this: Dim lhWnd As IntPtr = FindWindowEx(Me.ComboBox1.Handle, IntPtr.Zero, "EDIT", vbNullString).
The EM_SETMARGINS message (&H3) is a general message used with edit and rich text controls. We
supply it along with the handle of the control we want to modify. The wParam parameter contains a
constant value of EC_LEFTMARGIN (&H1) and/or EC_RIGHTMARGIN (&H2). If EC_LEFTMARGIN is
selected, the lower 16-bits of the 32-bit integer lParam parameter will be set to the left margin to set to
the text control in pixels. If EC_RIGHTMARGIN is selected, then the upper 16-bits (RightMargin << 16) of
the 32-bit integer lParam parameter will be set to the right margin to set to the text control in pixels.
Because we are only concerned with the left margin, we will not have to worry about shifting any values
16 bits, though clearly, this is easy enough to do (lParam >> 16).
Since we want to display the text to the right of the location where we are placing the CheckBox, setting
the left margin to CheckBox.Width + 2 looks good. Some people prefer adding 3, but to me this adds too
much of a gap (even though a single pixel normally appears to be next to nothing, anyway).
Consider the following code that will allow us to do all this:
'---------------------------------------------------------------
' INTEROP Constants.
'---------------------------------------------------------------
'Sets the widths of the left and right margins for an edit control. The message redraws the control to reflect the new margins.
' You can send this message to either an edit control or a rich edit control.
Private Const EM_SETMARGINS As Integer = &HD3

'Sets the widths of the left and right margins for an edit control. The message redraws the control to reflect the new margins.
' You can send this message to either an edit control or a rich edit control.
Private Const EC_LEFTMARGIN As Integer = &H1

'---------------------------------------------------------------
' INTEROP Pinvokes.
'---------------------------------------------------------------
' Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified
' window and does not return until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As Integer) As Integer

' Retrieves a handle to a window whose class name and window name match the specified strings. The function searches child
' windows, beginning with the one following the specified child window. This function does not perform a case-sensitive search.
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" (
ByVal hWndParent As IntPtr,
ByVal hwndChildAfter As IntPtr,
ByVal lpszClass As String,
ByVal lpszWindow As String) As IntPtr

'***************************************************************
' AddCheckToCombo(): Adds a checkbox to the combobox text field. Provided a combobox and a checkbox, this routine will combine
' them to display at runtime so that the checkbox is displayed within the text area of the combobox on the left side. Tests for
' the checkbox are performed as normal. Note that the width of the checkbox is set to its height, and its caption is erased.
' Hence, at development time, the checkbox can be placed anywhere.
'
'EXAMPLE: Add check1 checkbox to combobox Combo1.
' AddCheckToCombo(Me.CheckBox1, Me.ComboBox1)
'***************************************************************
Public Sub AddCheckToCombo(ByRef chkThis As CheckBox, ByRef cboThis As ComboBox)
Dim lhWnd As IntPtr = FindWindowEx(cboThis.Handle, IntPtr.Zero, "EDIT", vbNullString) 'get handle of textbox in the ComboBox.
If CBool(lhWnd) Then 'safety net, tho it should never fail.
 Page 373

chkThis.Text = Nothing 'remove any text from the CheckBox.


chkThis.BackColor = cboThis.BackColor 'match background colors.
chkThis.Width = chkThis.Height 'match width of CheckBox to height.
Dim SizeDiff As Integer = (cboThis.Height - chkThis.Height) \ 2 + 1 'top/btm CheckBox offsets in ComboBox.
chkThis.SetBounds(cboThis.Left + 3, cboThis.Top + SizeDiff, chkThis.Width, cboThis.Height - SizeDiff) 'set bounds.
chkThis.BringToFront() 'ensure CheckBox above ComboBox.
Dim lMargin As Integer = chkThis.Width + 2 'compute w.2-pixel buft for CheckBox.
SendMessage(lhWnd, EM_SETMARGINS, EC_LEFTMARGIN, lMargin) 'set left margin of ComboBox text.
End If
End Sub

To use the AddCheckToCombo method is easy. Just place a CheckBox and a ComboBox control on a form.
If the CheckBox was named CheckBox1, and the ComboBox was named ComboBox1, we can add
CheckBox1 to ComboBox1 by issuing the command: AddCheckToCombo(Me.CheckBox1, Me.ComboBox1).
NOTE: Like VB6, VB.NET features a CheckListBox control, so adding a CheckBox feature for a ListBox is not needed.
However, adding checkboxes to each entry in a ComboBox would be a neat treat, and can be done using owner-drawn
ComboBoxes, similar to how I demonstrated owner-drawn ComboBox lines in this document on page 377.

Sizing the ComboBox DropDown List Width.


How many times have we added entries to a ComboBox list, but then find that its width does not match
long entries in its contents? It would be nice to be able to adjust the width of the dropdown list based
upon its contents, such as sizing its width to the width of its longest member.
Fortunately, the VB.NET ComboBox now sports a DropDownWidth property where we can check and set
the width of the dropdown’s width in pixels. Hence, if we want to set ComboBox1’s width to 100 pixels,
we would use a command like this: Me.ComboBox1.DropDownWidth = 100. This property replaces the
CB_SETDROPPEDWIDTH and LB_SETDROPPEDWIDTH Win32 Messages.
But, how do we determine the longest member, and how do we determine the pixel width of that text?
That is also easy to do. The task of determining the width of text has traditionally been determined in
VB6 by filling a hidden auto-sizing Label’s text property to the text after setting the Label’s Font
property to that of the target ComboBox, and then examining the label’s updated width.
However, if we were to use a Graphics object that is associated to the ComboBox, we can use its
MeasureString method, which measures a specified string using a specified Font to determine the width
and height of the text, all without actually writing any text data to any control.
As shown in several examples earlier in this document, we can create a Graphics object associated to
ComboBox1 using a simple command like the following:
Dim cboG As System.Drawing.Graphics = Me.ComboBox1.CreateGraphics. 'associate a Graphics object to ComboBox1

Next, we can determine the width of a string of text using the Graphic object’s MeasureString method in a
command like the following:
Dim TxtWidth As Integer = cboG.MeasureString(TextString, Me.ComboBox1.Font).ToSize.Width.

As we can see, the MeasureString method requires two parameters: the text string to measure, and the
font to be used to calculate the resulting dimensions. It actually returns a SizeF structure (floating point
version of a Size structure) that contains the Width and Height of a hypothetical bounding rectangle as
floating-point values. We in turn use the SizeF’s ToSize method to covert the SizeF result to a Size
structure, which is an integer version of SizeF. The Size object contains 2 properties: Width and Height.
We simply use the Width property to obtain the integer width of the text.
NOTE: Personally, I will not set a ComboBox dropdown list width to less than the width of the ComboBox itself, because it
simply looks too goofy to me, so I always establish the minimum width to be that of the ComboBox.
Consider the following method, which will adjust a specified ComboBox to the width of its widest data:
 Page 374

'***************************************************************
' CboDropDownWidthFromContents(): Compute maximum combobox dropdown field width based upon
' width of its list, and assign the with to the dropdown.
' An optional AddPixels parameter allows you to add an additional width value to the width being computed, which would be
' important if you also used the AddCheckToCombo() function, to allow for the added checkbox. An optional lMaxWidth value allows
' you to set a maximum limit. If not provided, the width of the screen is the limit. Note that the width will not be assigned
' below the width of the combobox control's width.
'
'EXAMPLE: Do not exceed form's width. Add width of checkbox
' CboDropDownWidthFromContents(Me.ComboBox1, Me.Width, Me.CheckBox.Width)
'***************************************************************
Public Sub CboDropDownWidthFromContents(ByRef cboThis As ComboBox,
Optional ByVal lMaxWidth As Integer = -1,
Optional ByVal AddPixels As Integer = 0)
Dim cboG As System.Drawing.Graphics = cboThis.CreateGraphics 'cache graphics interface to ComboBox
Dim lWidth As Integer = cboThis.Width ' ComboBox looks odd if dropdown is narrower than ComboBox.
Dim tmpRECT As New RECT 'structure used to check text dimensions.

For i As Integer = 0 To cboThis.Items.Count - 1 'Loop thru ComboBox list & get widths, storing largest.
Dim txtWidth As Integer = cboG.MeasureString(cboThis.Items(i).ToString, cboThis.Font).ToSize.Width + 8 'compute width.
If txtWidth > lWidth Then
lWidth = txtWidth 'update lWidth to wider value.
End If
Next i

lWidth += AddPixels 'add pixels for user-defined buffering if requested.

'Do not allow width to exceed specified max width, or, if not specified, the width of the screen:
If lMaxWidth <= 0 Then lMaxWidth = Screen.PrimaryScreen.Bounds.Width - 16
If lWidth > lMaxWidth Then lWidth = lMaxWidth

cboThis.DropDownWidth = lWidth 'Set the drop down width.


End Sub

To use this method, simply supply it with a ComboBox to


adjust the dropdown list width for. For example:
CboDropDownWidthFromContents(Me.ComboBox1).

NOTE: A ListBox would be adjusted through its Width property.

Sizing the ComboBox DropDown List Height.


When the dropdown list of a ComboBox is displayed, by default its length is computed to expose up to a
maximum of 30 data items. We can change this using the ComboBox’s DropDownHeight property. This
property did not exist under VB6, and we had to resort to using the CB_SETMINVISIBLE message, which
established the minimum number of data items (lines) to display in the dropdown list.
Because the DropDownHeight property sets and returns its value in pixels, we must multiply the number
of items we want displayed times the ItemHeight property of the ComboBox.
However, if we simply want to display the maximum number of entries possible in a dropdown list, it
can get just a little more complicated. For example, it would be nice to always display all items, but
what if the list would scroll off the screen? And how would we calculate it to determine its dimensioning
relationship to its position on the screen? We would also have to check to see if we can invert its location
to display it above the ComboBox, if necessary. Fortunately, this is all easy to calculate.
Consider the following method, which will display the maximum amount of data possible:
'***************************************************************
' CboSetDropDownToMax(): Set a ComboBox dropdown list to display the maximum items possible.
' This must be done prior to displaying the dropdown list after adding items to it.
'
'EXAMPLE:
' CboSetDropDownToMax(Me.ComboBox1)
'***************************************************************
Public Sub CboSetDropDownToMax(ByVal sender As Object)
If TypeOf sender Is ComboBox Then 'allow only ComboBox controls.
Dim cbo As ComboBox = DirectCast(sender, ComboBox) 'get the sender as a ComboBox control.
Dim CboTL As Point = cbo.FindForm.PointToScreen(cbo.Location) 'get screen coordinate for ComboBox location.
Dim CboBtm As Integer = CboTL.Y + cbo.Height 'get screen location of bottom of ComboBox.
Dim nHeight As Integer = (cbo.Items.Count * cbo.ItemHeight) + 2 'Get full height + top and bottom window border.
 Page 375

Dim ScHeight As Integer = Screen.PrimaryScreen.Bounds.Height 'get screen height in pixels.

If CboBtm + nHeight > ScHeight AndAlso CboTL.Y - nHeight < 0 Then 'list will go below screen or above?
cbo.DropDownHeight = ((ScHeight - CboBtm + cbo.ItemHeight - 1) \ cbo.ItemHeight) * cbo.ItemHeight 'yes, so shorten.
Else
cbo.DropDownHeight = nHeight 'set the desired height of the dropdown list.
End If
End If
End Sub

NOTE: This method should be invoked each time we add data to the ComboBox for pristine visual effect.
This method is easy to use. For example, to set the dropdown list height to the maximum possible for the
current contents of ComboBox1, use the following command:
CboSetDropDownToMax(Me.ComboBox1) 'set ComboBox dropdown list height to max contents.

NOTE: A ListBox would be adjusted through its Height property.


NOTE: I stated earlier that the edit control for a ComboBox had an
edit control with a class name of EDIT, which we picked up using the
FindWindowEx Pinvoke. It also has a ListBox control, which
comprises its dropdown list, having a class name of COMBOLBOX.
However, unlike the EDIT field and dropdown button, it is a separate
entity that us simply displayed with it. Even so, I will show how to
easily acquire it in Black Book Tip # 59: Adding a Horizontal
ScrollBar to a ComboBox DropDown List, on page 757. There, we
will use a Win32 Structure that most developers claim is impossible
to use from VB6 and VB.NET due to system incompatibilities. But,
we know what I think about the word IMPOSSIBLE by now...

Adding the DriveListBox, DirListBox, and FileListBox Controls to VB.NET.


For those familiar with VB6, there were three useful
controls named DriveListBox, DirListBox, and
FileListBox. They are supported under VB.NET,
though we will not find them listed in our toolbox.
But, I will show us how to quickly do that, shortly,
after a brief necessary dissertation on these controls.
The DriveListBox is a modified version of a
ComboBox, which contained an embedded imagelist
and performs owner-drawing so that drive icons will
be displayed within its edit box and dropdown list. It also features a Drive property that returns the
string name of the currently selected drive. When we make a new selection from the list, it triggers a
SelectedIndexChanged event for the control. Here, we can set the Path property of a DirListBox control,
for example, to update its display. Typically, I use code similar to the following, assuming I have all
three controls on a form, much as on the above displayed form:
Private Sub DriveListBox1_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs) Handles DriveListBox1.SelectedIndexChanged
Dim Path As String = Me.DriveListBox1.Drive 'get selected drive path.
Dim I As Integer = InStr(Path, ":") 'scan for colon (we will ignore the volume name, if it exists, that follows this).
If I <> 0 Then 'found it?
Path = Path.Substring(0, I).TrimEnd & "\" 'yes, so set path with trailing backslash.
Try
Me.DirListBox1.Path = Path 'set the directory path.
Catch ex As Exception
MsgBox(ex.Message) 'report error.
Path = Me.DirListBox1.Path 'save current directory path.
Me.DriveListBox1.Drive = Path 'reset drive (extracted from path).
Me.DirListBox1.Path = Path 'reset directory, which was reset when drive reset.
End Try
Me.FileListBox1.Path = Path 'set the file path.
End If
End Sub
 Page 376

The DirListBox is a modified version of a TreeView control that lists the current directory path in tree
form. It also features a Path property that, when set, triggers a SelectedIndexChanged event for the
control. Here, we can set the Path property of a FileListBox control, for example, to update its display. I
typically also check its Click event, and so in all I add the following, which will enable a single-click
reaction to user selections and update the FileListBox control as needed:
Private Sub DirListBox1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles DirListBox1.Click
Me.DirListBox1.Path = Me.DirListBox1.DirList(Me.DirListBox1.DirListIndex)
End Sub

Private Sub DirListBox1_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs)


Handles DirListBox1.SelectedIndexChanged
Me.FileListBox1.Path = Me.DirListBox1.Path 'update file list to new path selected in directory list.
End Sub

The FileListBox is a modified ListBox that has some properties hidden and other special properties
added. In addition to the typical ListBox properties, it also contains a Path property, a Pattern property,
and the Boolean properties Archive, Hidden, Normal, ReadOnly, and System.
Property Description
Path Specifies the directory path to list files from.
Pattern Pattern mask for files. For example, “*.*” to list all files, or “*.txt” to list only text files.
Normal Include all (non-hidden) files in the list that match the specified pattern.
Archive Include Archive files in the list that match the specified pattern. If the Normal property is not set to True, then list
only Archive files, unless the Hidden, ReadOnly, or System properties are also set to True, in which case it will
display only files that match all specified properties.
Hidden Include Hidden files in the list that match the specified pattern. If the Normal property is not set to True, then list
only Hidden files, unless the Archive, ReadOnly, or System properties are also set to True, in which case it will
display only files that match all specified properties.
ReadOnly Include Read-Only files in the list that match the specified pattern. If the Normal property is not set to True, then
list only Read-Only files, unless the Archive, Hidden, or System properties are also set to True, in which case it
will display only files that match all specified properties.
System Include System files in the list that match the specified pattern. If the Normal property is not set to True, then
list only System files, unless the Archive, Hidden, or ReadOnly properties are also set to True, in which case it
will display only files that match all specified properties.

To add the DriveListBox, DirListBox, and FileListBox to our toolbox is


easy. With any form up and selected in the IDE, select the toolbox collection
where we want to add them, such as Common Controls. Right-click it and
select the Choose Items... option. Once the Choose ToolBox Items dialog
window finally comes up, from the .NET Framework Components tab,
scroll down until we locate DirListBox under the Name heading and place a
check in the checkbox next to it. Then find DriveListBox, and then
FileListBox, and add a check to their checkboxes as well.
Once we have all three items checked in the list,
select the OK button to lock in the changes.
We will now see the three new controls listed at
the bottom of our selected toolbox group. Add
them to forms as we need them, and use them
just like we would any other form control, and
just like we did under VB6.
NOTE: These three controls are part of the Microsoft.VisualBasic.Compatibility namespace, and they were primarily meant
to be used to support upgraded VB6 applications, but that does not mean in any way that we cannot use them.
 Page 377

Adding Images to ListBoxes and ComboBoxes Under VB.NET with Ease.


I have seen a lot of internet chatter on how to add icons to a ListBox. Most times people are advised to
simply resort to using a ListView control, which has icon support built right in. That was what we
typically did under VB6. It was a cheat, but it worked. How professional did it look? Well... Many others
advise us that this can only be done under C# because it involves derived classes (it does???), owner-
drawn ListBox items and such, and advise them to examine a C# application example somewhere. I
think many of these gurus offer this advice because they remember all too clearly the difficulty we all
had doing owner-drawn graphics under VB6. Yes, I know, we could do owner-drawn graphics in VB6...
But did we really want to? It was a major pain in the caboose. Luckily, if we wanted to add icons to a
VB6 ComboBox, all we had to do was use an ImageCombo control in place of a ComboBox, which was
found in the Microsoft Common Controls Library 6.0 (MSCOMCTL.OCX); this also provided the
ToolBar, StatusBar, ProgressBar, TreeView, ListView, ImageList, and Slider controls.
Under VB.NET this has all changed and in more ways than one. First, the “supposed” bad news:
VB.NET does not, nor does it have plans to provide an ImageCombo control, to the consternation of
legions of programmers. But the good news is: there is no real need for one! That is because of how easy
VB.NET has made doing owner-drawn controls. Even a rank amateur can manage professional-looking
results in a matter of a minute or two. And that is in a matter of seconds for seasoned VB developers.
Indeed, the previously often-dreaded idea of owner-drawn controls under VB6 is now almost brain-dead
simple under VB.NET. In just 3 simple lines of code, we can perform owner-drawn graphics and text on
a ListBox or ComboBox quickly, easily, and painlessly. Well... 4 lines if we want to also draw a selection
rectangle, which is as easy as remembering to say “Yikes!” when we fall into a snake pit.
First, I am going to show what we need to know to draw an icon and some text into a ListBox, which
goes ditto for ComboBoxes, and how to draw a selection rectangle. Next, we will tack together some
easy software tools so we can draw different graphics for different kinds of data on a line from either the
application resources or an ImageList control.

Principle Features of an Owner-Drawn ListBox (and ComboBox).


The idea of performing owner-drawn, or manually-drawn List Items may sound a bit scary to some, but
if we have tried some of the easy features covered earlier in this document, such as drawing icons on a
object’s surface, drawing text directly to a PictureBox, or drawing shapes on a control or form, then we
should already be familiar with owner-drawn graphics. We should also be aware of how easy they are to
actually do. Indeed, you may even be an old hand at it, but you simply do not yet realize it.
To enable owner-drawn graphics on either a ListBox or a ComboBox, all we have to do is change the
control’s DrawMode property from Normal to either OwnerDrawFixed or OwnerDrawVariable. The
OwnerDrawVariable option is a special form that indicates that all the elements in the control are drawn
manually and can differ in size. For our purposes, we will only concern ourselves with the
OwnerDrawFixed option. If we had a ListBox on a form that contains data and we set the DrawMode to
OwnerDrawFixed right now, and then ran the application, it would run normally, but we might notice
that now there is no data displayed within the ListBox. However, though VB.NET diligently processed
that data, it was harmlessly discarded, and simply because we did not yet perform our responsibilities in
the Owner-Drawn part of the application (us, controlling the Owner part).
To support Owner-Drawn ListBoxes and ComboBoxes we have to write some code within the control’s
DrawItem event. This event can be looked upon just like a Paint event for a control, as we have
examined earlier in this tome, except that a DrawItem event addresses each displayed line of a ListBox or
ComboBox, rather than the control as a whole. What it does is offer us a DrawItem event for each
member in a ListBox or ComboBox that it must display or update. But along with it, it also provides us
with easy access to every tool we will need to easily process those items in short, quick lines of code.
 Page 378

Here is the skeleton for a DrawItem event:


Private Sub LstBox1_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Handles ListBox1.DrawItem

End Sub

The generic sender parameter, defined as the source of the event, is the holder, or container object for
the list item. In our case it is the ListBox, or really any ListBox that has a handler assigned to this event
code (we can add more than one by listing them after the Handles verb, or by manually adding them in
the Form Load event, such as “AddHandler Me.ListBox2.DrawItem, AddressOf lstBox1_DrawItem”).
The second parameter, “e”, is an object of type DrawItemEventArgs that is a Graphics interface hooked
to the list item. It also provides the Index within the Items collection of the sender control.
The first thing we will want to do is refresh the item’s background. This is just like the automatic
background clearing that a Paint event performed. And so we first issue the following command:
e.DrawBackground() 'redraw the listitem line's background (blank it out to match the control's background color).

The second thing is to draw an Icon. Although we can get fancy and select an icon from a list in our
application resources or an ImageList, we will just use a single generic icon for now, and in this case a
simple 16x16 Icon, which is a size I strongly recommend we provide to our own ListBox and ComboBox
controls because this size will fit neatly on a ListBox line, and that I will assume is stored in our
resources for now. Also, be aware that the line height typically specified by the control’s ItemHeight
property is set to 14 (pixels). Because we will be drawing 16x16 icons to the line, we will want to
change the control’s ItemHeight property to at least 16. With that, we can now draw the icon:
e.Graphics.DrawIcon(My.Resources.myIcon, e.Bounds.Location) 'draw icon (draw starting at top-left of the bounding rectangle for list item).

Finally, we need to draw the text for the list item being drawn. We will have to point beyond the icon to
avoid over-writing it. I also like to point 4 pixels beyond that, just to give a nice buffer between the icon
and the text. Because our icon is 16x16, we could simply add an offset of 20 (16+4), which is usually
not very professional, as absolute numbers have a strange way of later introducing bugs. Check this out:
Dim Lb As ListBox = DirectCast(sender, ListBox) 'get a reference to the listbox being used.
e.Graphics.DrawString(Lb.Items(e.Index).ToString, Lb.Font, Brushes.Black,
e.Bounds.X + My.Resources.myIcon.Width + 4, e.Bounds.Y) 'We can use Integers instead of Singles here.

Here, the command “Lb.Items(e.Index).ToString” used the index provided by


e.Index to grab the text of the entry ( .ToString). Using the ListBox font and a
black brush, we drew the text to the bounding rectangle, starting at its left
plus 20 (16 pixels for the icon and 4 for a separator space), and from its top.
Also, if the line we are drawing is the currently selected line, then we will need to also highlight it:
If Lb.SelectedIndex = e.Index Then e.DrawFocusRectangle() 'draw focus rectangle if this line is selected.

Of course, it would be REALLY nice if we could display appropriate icons for each of our entries,
allowing us to easily identify them. How to select them will obviously be a matter of personal choice,
but I have seen three very primitive methods that actually work quite well. For example, we may want to
change icons based upon the type of file being listed, basing that decision on a file extension.

Using A Generic List Collection and an ImageList Control.


One method to determine which icon to draw for each line is to maintain a non-visible secondary ListBox or
a Collection. The strongly-typed zero-based Generic Collections work great for this, because we can
maintain a 1-to-1 index offset for ListBox and Collection items, and we can map an icon resource name, or
index into an ImageList control for each entry. For example, use “ Dim colListItem As New List(Of String) ”
for mapping icon names, such as the names of icons we may have placed in the application resources, or “ Dim
colListItem As New List(Of Integer)” for mapping indexes into an image list or into a custom routine that
will return resource icons based upon an integer flag value or index.
 Page 379

Consider this example: A ListBox named lstFiles contains a list of files. A Generic List Collection of
type Integer named glcFiles has indexes into an ImageList control named ilstFiles, which contains a
series of icons, offset from 0, that represent different file types. When a file is added to lstFiles, its type
was checked, and added to glcFiles. For example, in the Form Load event, we might find:
Dim glcFiles As New List(Of Integer) 'new collection for mapping ImageList indexes.

When we are building the list, we parse each file path and load the ListBox and Collection with the data:
Me.lstFiles.Items.Add(path) 'add an item to the file list (NOTE: This code depends on the Boolean Sorted parameter being set to False)
glcFiles.Add(GetIconType(Path)) 'get the icon index for this type file and save to the collection thru a unser-defined GerIconType() method

Finally, in our DrawItem event for LstFiles, we would put this all together:
Private Sub lstFiles_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Handles Me.lstFiles.DrawItem
Dim LB As ListBox = DirectCast(sender, ListBox) 'get the listbox pointed to by this event.
Dim S As String = LB.Items(e.Index).ToString 'get string in listbox.
Dim Img As Image = ilstFiles.Images(glcFiles(e.Index)) 'get the image stored in the ImageList (Icons become Images).

e.DrawBackground() 'redraw listbox line's background (blank it out).


e.Graphics.DrawImage(Img, e.Bounds.Location) 'draw image at start of line.
'draw the text after the bitmap
e.Graphics.DrawString(s, LB.Font, Brushes.Black, CSng(e.Bounds.X + Img.Width + 4), CSng(e.Bounds.Y))
If LB.SelectedIndex = e.Index Then e.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item.
End Sub

Using A Flag at the Start of the List Item Text.


Another method I have seen is where people place a single character code as the first character of the
string (a solution so primitive it is genius), which is trimmed off during the display of the text, but the
character is matched to an index. For example, consider the following possible implementation of that:
Private Sub lstFiles_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Handles Me.lstFiles.DrawItem
Dim LB As ListBox = DirectCast(sender, ListBox) 'get the listbox pointed to by this event.
Dim S As String = LB.Items(e.Index).ToString 'get string in listbox.
Dim Icn As Icon = Nothing 'init icon.

Select Case S.Substring(0, 1) 'check left-most character.


Case "s"
Icn = My.Resources.SystemFile 'SystemFile.ico.
Case "S"
Icn = My.Resources.SystemDir 'SystemDir.ico.
Case "h"
Icn = My.Resources.HiddenFile 'HiddenFile.ico.
Case "H"
Icn = My.Resources.HiddenDir 'HiddenDir.ico.
Case "D"
Icn = My.Resources.Directory 'Normal Directory.ico.
Case Else ' "F"
Icn = My.Resources.File 'Normal File.ico.
End Select

e.DrawBackground() 'redraw listbox line's background (blank it out).


e.Graphics.DrawIcon(Icn, e.Bounds.Location) 'draw icon at start of line.
'draw the text after the icon, but do not display the first character.
e.Graphics.DrawString(S.SubString(1), LB.Font, Brushes.Black, e.Bounds.X + Icn.Width + 4, e.Bounds.Y)
If LB.SelectedIndex = e.Index Then e.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item.
End Sub

A variation on this idea is where the first character is “0” through “9”, where an index value is gathered
by acquiring the integer value of that first character. This might be implemented as the following:
Private Sub lstFiles_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Handles Me.lstFiles.DrawItem
Dim LB As ListBox = DirectCast(sender, ListBox) 'get the listbox pointed to by this event.
Dim S As String = LB.Items(e.Index).ToString 'get string in listbox.
Dim Img As Image = ilstFiles.Images(CInt(S.Substring(0, 1))) 'get the image stored in the ImageList.

e.DrawBackground() 'redraw listbox line's background (blank it out).


e.Graphics.DrawImage(Img, e.Bounds.Location) 'draw image at start of line.
' draw the text after the bitmap
e.Graphics.DrawString(S.SubString(1), LB.Font, Brushes.Black, e.Bounds.X + Img.Width + 4, e.Bounds.Y)
If LB.SelectedIndex = e.Index Then e.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item.
End Sub

NOTE: If we are using a ComboBox then we should do this slightly differently, because in some cases a ComboBox may
have nothing selected or initially displayed. In these cases, the e.Index parameter may sometimes enter the event set to -1. As
such, we should rewrite the above event code like the following:
 Page 380

Private Sub lstFiles_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Handles Me.lstFiles.DrawItem


e.DrawBackground() 'redraw listbox line's background (blank it out).
If e.Index = -1 Then Return 'if no selection made, just blank the line out, and then leave.
Dim LB As ListBox = DirectCast(sender, ListBox) 'get the listbox pointed to by this event.
Dim S As String = LB.Items(e.Index).ToString 'get string in listbox.
Dim Img As Image = ilstFiles.Images(CInt(S.Substring(0, 1))) 'get the image stored in the ImageList.
e.Graphics.DrawImage(Img, e.Bounds.Location) 'draw image at start of line.
' draw the text after the bitmap
e.Graphics.DrawString(S.SubString(1), LB.Font, Brushes.Black, CSng(e.Bounds.X + Img.Width + 4), CSng(e.Bounds.Y))
If LB.SelectedIndex = e.Index Then e.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item.
End Sub

Using a Custom Class as a List Item to Index Graphics.


The methods I prefer to use when performing such primitive file tagging comes in two flavors: a method
providing an index into an ImageList, and a method providing icon names for resource icons.
A VB.NET ListBox and ComboBox store objects as members of their Items collection, unlike VB6,
which used only strings. This is why we have to add “ .ToString” to extract the text data from a ListBox
item to determine the text that is displayed in the list. It easily extracts the known embedded string.
These custom classes do not have to be big or complicated. For example, the following is a no-holds-
barred bare-boned no-error-trapping two-bit son of a cussin’ custom class that we can add as data to a
ListBox or ComboBox to store both a String and an Integer as data members:
'*******************************************************************************
' Class ListItem
' VERY simple class to service a ListBox or ComboBox.
'*******************************************************************************
Public Class ListItem
Public strItem As String 'text data.
Public intItem As Integer 'Index data.

' declare a string and an optional index.


Public Sub New(ByVal Text As String, Optional ByVal Index As Integer = 0)
strItem = Text 'save string.
intItem = Index 'save integer.
End Sub

' used by ListBox and ComboBox to return text to an invoker.


Public Overrides Function ToString() As String
Return strItem
End Function
End Class

With this, we can ignore Generic List Collections and header flags to mark our List Item entries. For
example, to add a File Path and an ImageList index to a single ListBox entry, we can do the following:
Me.lstFiles.Items.Add(New ListItem(Path, GetIconType(Path))) 'add a string for display and an index into the ImageList control.

With this approach, we could render the owner-drawn data using the following DrawItem event code:
Private Sub lstFiles_DrawItem(ByVal sender As Object, ByVal e As DrawItemEventArgs) Handles Me.lstFiles.DrawItem
Dim LB As ListBox = DirectCast(sender, ListBox) 'get the listbox pointed to by this event.
Dim lstItm As ListItem = DirectCast(LB.Items(e.Index), ListItem) 'get ListItem object from ListBox.
Dim Img As Image = ilstFiles.Images(lstItm.intItem) 'get the image stored in the ImageList.

e.DrawBackground() 'redraw listbox line's background (blank it out).


e.Graphics.DrawImage(Img, e.Bounds.X, e.Bounds.Y) 'draw image at start of line.
' draw the text after the bitmap.
e.Graphics.DrawString(lstItm.ToString, LB.Font, Brushes.Black, e.Bounds.X + Img.Width + 4, e.Bounds.Y)
If LB.SelectedIndex = e.Index Then e.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item.
End Sub

NOTE: We can also use “lstItm.strItem” instead of “lstItm.ToString” in the above code. Notice that if we had used a
command like “Dim S As String = LB.Items(e.Index).ToString ”, that the lstFiles ListBox would have obviously
employed the class’s ToString method to acquire the text. However, if we were to use this control in another application that
does not use user-drawn code, that the ListBox or ComboBox will also use the ToString method to acquire the text to
display to the user within its lists.
 Page 381

The final thing I wanted to cover is using Icon text names as references. Although I consider this a bit
slower in execution, it does work and it may be useful. I will show you just a couple examples, but I am
certain that you will be able to see its full potential, and you will be able to adapt it accordingly, if you
require it:
Dim Icn As Icon = DirectCast(My.Resources.ResourceManager.GetObject("myIcon"), Icon) 'You can use a string variable
' in place of the text.

We can also extract from a ImageList control by using the Name property of an Image:
Dim Img As Image = Me.ilstFiles.Images("myIcon.ico") 'You can use a string variable in place of the text,
' which specifies the Name property of the image.

NOTE: I will demonstrate much more complex icon/file associations by showing how to extract icons from files and
associated applications and then display them in a TreeView directory listing by creating a customizable BrowseFolder
dialog box in Black Book Tip # 50: Replace the BrowseFolderDialog Control with a Custom BrowserDialog Form on page 642.
 Page 382

Send (SMTP) and Retrieve (POP3) Email With Ease Under VB.NET.
Brays whisper distantly beneath midnight mists as spent developers, pinned by looming deadlines and
their brain’s threat of total collapse, rasp desperate prayers against an ominous sense of impending
doom, working at a fever’s pitch to hammer out viable code (which, according to Murphy’s Law of
Looming Deadlines, is an absolute impossibility). Suddenly, in cringed horror, they crash into a brick
wall; and this after having assured their skeptical client of how superior VB.NET was over that
antiquated VB6 the client revered. They realize too late that their strategy for the client’s email handler
is unworkable: VB.NET does not provide the MAPI controls they thought it did because Microsoft
Windows 10 defaulted to it. Another soul-torn howl trebles against the muffled parapets of the valley.
Plain and simple, Microsoft’s MAPI (Messaging Application Program Interface), though a fundamental
part of their email Input/Output in their Windows Operating Systems, like Windows 8/8.1 and Windows
10, and most noticed in Windows Mail, is as of yet not a part of .NET technology; it is still COM
(Common Object Model), used by ASP (Active Server Page), IIS (Internet Information Server), and most
other COM applications that accesses email. Being COM-based, we should not expect to see the
VB.NET Toolbox sport controls such as VB6’s MAPI-based MAPISession or MAPMessage.
Under the VB6 implementation of MAPI, it used the MAPISession control to (what else?) manage a
MAPI session. The MAPIMessage control was used to process email messages, both incoming (POP3;
Post Office Protocol – Version 3) and outgoing (SMTP; Simple Mail Transfer Protocol).
NOTE: MAPI is a Microsoft protocol allowing a MS email client to utilize all the features of an Exchange server. POP is the
most common Internet standard for receiving email. SMTP is the most common internet standard for sending email.
Presently, .NET is set up for SMTP outbound email, but it lacks POP3 support for inbound email, even
though these technologies are both simple TCP Clients (Transmission Control Protocol). I think it may
have something to do with many people wanting to read email using “eye-candy” apps, such as Outlook
or Mail. Yet, this does not remove the need for an inbound class in more controlled environments.
Although it is easy to write VB.NET code to support POP3 Inbound Email services, as I will be showing
you, I have found only one other person (I have since found more, but none of them provide robust
solutions), and he works at Microsoft, who has developed any sort of VB.NET code to demonstrate this
ability, albeit his solution was just a simple example with very limited capability. Even so, by the time I
found his article within the catacombs of MSDN, I had already put the finishing touches on my own
full-featured POP3 Inbound Email solution.

Adding the VB6 MAPISession and MAPIMessage Controls to VB.NET.


However, before diving into these VB.NET solutions, let us first take a look at the kind of support that is
presently available to developers who had upgraded VB6 MAPI applications to VB.NET.
When a VB6 MAPI application is upgraded to VB.NET, we may notice the application will
still sport a VB6 MAPISession and MAPIMessage control on forms that had them before.
Their control sources had been copied locally and referenced. Under .NET, a copy of the
COM-based MSMAPI32.DLL is converted into a non-COM version (its DLLRegisterServer
entry is disabled) and saved to a project-local file named Interop.MSMAPI32.DLL. Additionally, because
both VB6 controls actually accessed this DLL provider through the MSMAPI32.OCX ActiveX interface,
another project-local non-COM DLL named AxInterop.MSMAPI32.DLL is internally compiled by .NET
that will duplicate both the ActiveX visual interface construction services for the controls and the
function mapping services to the new Interop.MSMAPI32.DLL.
Having found these controls on their upgraded applications, many developers want to add them to other
VB.NET projects so they can take advantage of them there, but they cannot seem to find a way to easily
access the new DLLs from their newer projects. It is doable, but it requires numerous coding hacks.
 Page 383

But relax. Why not just add these two VB6 controls to our VB.NET Toolbox and access them directly?
1. With any form up on the Visual Studio screen so that the IDE toolbox is active, right-click a toolbox category we
want to add the MAPI controls to. If we want to add them to their own category, such as to one named COM, right-
click any category and select the Add Tab option, then type the name of our category, such as COM, press ENTER,
then right-click that tab.
2. Select the Choose Items... option, and wait for the IDE to build a massive control reference list from the computer.
3. Once the Choose Toolbox Items dialog is finally displayed – select the COM Components tab.
4. Scroll down and put checkmarks in the boxes for Microsoft MAPI Messages Control, Version 6.0, and Microsoft
MAPI Sessions Control, Version 6.0. Both of these are actually linked to MSMAPI32.OCX, which in turn drills
down to MAPI32.DLL, but they will now both link to a new .NET-compiled axInterop.MSMAPI32.DLL, which in
turn drills down to Interop.MSMAPI32.DLL.
5. Click the OK button, and we will find these two controls now in our selected Toolbox category list, and we can
begin using these controls just exactly as we would had been using them under VB6.
NOTE: If we do not find these entries in the Choose Toolbox Items dialog box, then we may not or no longer have the VB6
redistributables on our system, so we will have to minimally install the free Runtime Distribution Pack for Service Pack 6
for Visual Basic 6.0, available from Microsoft (www.microsoft.com/downloads/details.aspx?FamilyId=7B9BA261-7A9C-43E7-9117-
F673077FFB3C&displaylang=en). We are allowed to do this even if we no longer own VB6. If we also have VB6 installed, we
should also install the Microsoft Visual Basic 6.0 Service Pack 6 Cumulative Update to include the latest tweaks
(www.microsoft.com/download/en/details.aspx?amp;displaylang=en&id=7030).

PART ONE.
Sending Email Under VB.NET Using Native Methods.
VB.NET has its own Outbound SMTP Email class that supports sending email, and without a need to
add the more resource-hungry form controls as we had to do with VB6. Because this technique is more
accessible than supporting inbound email under VB.NET, we will first look at sending email out.
Some people think they can just hit a system-linked “Send” button and a message they had just typed is
automatically launched into the labyrinths of the internet with possibly little or no code from us. Were
that it could be so easy. But with this article, we will be able to make our clients think we made it so.
Back in the “old days” of software engineering, say the early 1990s, we processed email through a thing
called a Berkeley Socket (circa 1983). This “socket” simply described the endpoint of a bidirectional
inter-process communication flow across an Internet Protocol-based network. It was sometimes a real
trick to write code for, depending on the platform, but when it functioned correctly, it was a work of art.
In a pinch we launched a TelNet client and manually typed the various
commands to log on to an email server, send, receive, and read email,
and finally disconnect. Those were cryptic and unforgiving days. But
looking back to those times, I have to wonder if I was either a brilliant
genius or a major drool-monkey, because I thought back in those
younger and smarter years that it was all simple child’s play.

Quick and Dirty Email Senders.


Nowadays, we have built-in tools to do all the hard stuff, such as the System.Net Namespace. This class
library provides the .NET SMTP Outbound Mail class. To use it, in the heading of our form or module,
above the class declaration of the file we want to implement it in, I will ask you to add this line:
Imports System.Net, System.Text 'Most of the code in this article REQUIRES this Imports line!

NOTE: Some people import System.NET.Mail just to avoid typing “Mail.” later in their code, but we will also need access
to the System.Net.Mime namespace, and even later, when we download email, the System.Net.Sockets namespace.
NOTE: MIME (or Mime) is an anagram for Multipurpose Internet Mail Extensions.
 Page 384

First, if we want to send a fast note to someone, most servers will allow we to use the following method:
'*******************************************************************************
' Function Name : BrainDeadSimpleEmailSend
' Purpose : Send super simple email message (works with most SMTP servers)
' : This transmits an unsecure email using default port 25.
'*******************************************************************************
'NOTES: strFrom : Full email address of who is sending the email. ie, David Dingus <daviddingus@att.net> or daviddingus@att.net
' strTo : Full email address of who to send the email to. ie, "Bubba Dingus" <bob.dingus@cox.com> or bob.dingus@cox.com
' strSubject: Brief text regarding what the email concerns.
' strBody : text that comprises the message body of the email.
' smtpHost : This is the email host you are using for sending emails, such
' : as "smtp.comcast.net", "authsmtp.juno.com", etc.
'*******************************************************************************
Public Sub BrainDeadSimpleEmailSend(ByVal strFrom As String,
ByVal strTo As String,
ByVal strSubject As String,
ByVal strBody As String,
ByVal smtpHost As String)
Dim smtpEmail As New Mail.SmtpClient(smtpHost) 'create new SMTP client using TCP Port 25.
smtpEmail.Send(strFrom, strTo, strSubject, strBody) 'send email.
End Sub

NOTE: The FROM and TO email addresses can be simple, such as bob.dingus@cox.com, or more “trendy” formats, such as
“Bernard Shaw Fullo” <FulloBS@highschnozez.com> or even Coat Mahatma <mahatmacoat@classydresser.com>. If the
data contains angle brackets, the Mail object will use only the data contained within them. If there are no angle brackets,
then the mail object will surround the data with angle brackets, assuming that the entire text is an email address.
NOTE: Yahoo, Gmail, HotMail, and Juno, among others, are internet-based services providing both internet and
SMTP/POP3 access. Unlike the other three, Yahoo, normally free, requires an additional monthly fee for SMTP/POP3
access. Juno and HotMail provide this service freely to their subscribers. Gmail, a free service, provides it if we set an
option in the POP Download section of the Forwarding and POP/IMAP option within its internet account Settings. We will,
of course, have to set up SMTP/POP3 accounts and access within our favorite local email application for all four, such as
Windows Mail, Outlook Express, Outlook, or other third-party mail applications..
The above method actually works for most SMTP servers. For example, Comcast and Juno both support
this interface. I use this for quick messages, though they can also be major literary works, like most
people post text messages on their cell phones. However, I think it would be a bit difficult to drive down
the road with a desktop PC and keyboard in hand, trying to steer while I thumb a quick message.
NOTE: Texting while driving is illegal here in Florida, as it should be. In 2010 I witnessed 6 accidents and 1 fatality due to
driver Texting, primarily by young people, though I must concede that they could have been more distracted by their stereo
systems blasting so loudly that it made both their eyes bounce from one side of their head to the other, impairing their vision.
The SMTP Host is the address of our email provider’s SMTP server. SMTP is a TCP/IP (Transmission
Control Protocol/Internet Protocol) process used for sending and receiving email. However, because
SMTP is limited in its capability to queue messages at its receiving end, it is typically used with one of
two other protocols, like POP3 or IMAP (Internet Message Access Protocol). But that is a topic we will
cover after we resolve the email sending issues that many thousands of developers are presently having.
For a much more robust method that supports most-all servers, including those that use security layers,
like Gmail, we might try the following method to send a quick plain-text email with no attachments:
'*******************************************************************************
' Function Name : QuickiEMail
' Purpose : Send a simple email message (but packed with a lot of muscle)
'*******************************************************************************
'NOTES: strFrom : Full email address of who is sending the email. ie, David Dingus <daviddingus@att.net> or daviddingus@att.net.
' strTo : Full email address of who to send the email to. ie, "Bubba Dingus" <bob.dingus@cox.com> or bob.dingus@cox.com.
' strSubject: Brief text regarding what the email concerns.
' strBody : text that comprises the message body of the email.
' smtpHost : This is the email host you are using for sending emails, such
' : as "smtp.gmail.com", "smtp.comcast.net", "authsmtp.juno.com", etc.
' smtpPort : TCP Communications Port to use. Most servers default to 25, though 465 (SSL) or 587 (TLS) are becoming popular.
' usesSLL : If this value is TRUE, then use SSL/TLS Authentication protocol for secure communications.
' SSLUsername: If usesSLL is True, this is the username to use for creating a credential. Leave blank if the same as strFrom.
' SSLPassword: If usesSLL is True, this is the password to use for creating a credential. If this field and SSLUsername
' : are blank, then default credentials will be used (only works on local, intranet servers).
' SSLDomain : If creating a credential when a specific domain is required, set this parameter, otherwise, leave it blank.
'*******************************************************************************
 Page 385

Public Function QuickiEMail(ByVal strFrom As String,


ByVal strTo As String,
ByVal strSubject As String,
ByVal strBody As String,
ByVal smtpHost As String,
Optional ByVal smtpPort As Integer = 25,
Optional ByVal usesSSL As Boolean = False,
Optional ByVal SSLUsername As String = Nothing,
Optional ByVal SSLPassword As String = Nothing,
Optional ByVal SSLDomain As String = Nothing) As Boolean
Try
Dim smtpEmail As New Mail.SmtpClient(smtpHost, smtpPort) 'create new SMTP client.
smtpEmail.EnableSsl = usesSSL 'true if SSL Authentication required.
If usesSSL Then ' 'SSL authentication required?
If Len(SSLUsername) = 0 AndAlso Len(SSLPassword) = 0 Then 'if both SSLUsername and SSLPassword are blank...
smtpEmail.UseDefaultCredentials = True 'use default credentials.
Else 'otherwise, we must create a new credential.
If Not CBool(Len(SSLUsername)) Then 'if SSLUsername is blank, use strFrom.
smtpEmail.Credentials = New NetworkCredential(strFrom, SSLPassword, SSLDomain)
Else
smtpEmail.Credentials = New NetworkCredential(SSLUsername, SSLPassword, SSLDomain)
End If
End If
End If
smtpEmail.Send(strFrom, strTo, strSubject, strBody) 'send email using text/plain content type and QuotedPrintable encoding.
Catch e As Exception 'if error, report it.
MsgBox(e.Message, MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation, "Mail Send Error")
Return False 'return a failure flag.
End Try
Return True 'if no error, then return a success flag.
End Function

With the QuickiEMail method, we can send a full, plain-text message to one recipient with no
attachments, hence; quick and dirty. With this method, we supply it with the full email address for whom
the email is from (you, for example), we also provide the full email address of the person we are sending
it to, a subject for the email (what it concerns), the body of the text message, and the SMTP host (such
as smtp.comcast.net, authsmtp.juno.com, or smtp.gmail.com, for example).
If we must use a TCP Port other than Port 25, such as Gmail requires (Secure TCP Port 465), then
include the needed port number. Also, if we will be using a secure TCP Port, then we will also require
SSL Authentication, so include True for the Boolean usesSSL flag. By setting usesSSL to True, this will
in turn mean that we will need to also supply credentials. Do not panic. This is easy. Just look below.

TCP Ports, SSL Authentication, and Creating Credentials


By default, most Outbound Servers use TCP Port 25, and they use TCP Port 110 for their Inbound
Server, providing unencrypted, non-secure email transactions; the kind the NSA likes to read. However,
some may differ, such as Gmail, as mentioned above, which uses secure TCP Port 465 or 587 for their
Outbound Server and TCP Port 995 for their Inbound Server. But this is primarily because Gmail, and
some others, requires an SSL (Secure Socket Layer) to process email. In these cases we will need to set
the optional parameter to that TCP Port, such as in the following hard-coded example:
Dim smtpEmail As New Mail.SmtpClient("smtp.EngulfNDevour.com", 587) 'Create new SMTP client using Secure TCP Port 465 or 587.
smtpEmail.EnableSsl = True 'True if SSL authentication required (also for TLS authentication).
SmtpEmail.UseDefaultCredentials = True 'Typical for NTLM, negotiate, and Kerberos-based authentication.

NOTE: Yes, yes, I know; TCP port 587 uses a newer breed of security called TLS (Transport Layer Security). But first, TLS
is still an SSL. Second, SSL technology provides identical security. And third, we are not going to find an EnableTls property
in the Mail object. Refer to www.sans.org/reading_room/whitepapers/protocols/ssl-tls-beginners-guide_1029.
The above DefaultCredentials property represents the system credentials for the current security context
in which the application is running. For a client application, these are usually the Windows credentials
(username, password, and domain) of the user running the application (for ASP.NET applications, the
default credentials are the user credentials of the logged-in user, or the user being impersonated).
NOTE: To examine our default credentials, access the System.Net.CredentialCache.DefaultNetworkCredentials property.
However, setting the UseDefaultCredentials property to True will apply only to a Microsoft NT LAN
Manager (NTLM) using intranet-based Negotiate authentication and Kerberos-based authentication. All
others will have to manually create a credential. This is no big whoop, as we are about to see.
 Page 386

NOTE: To make sure that IIS supports both the Kerberos protocol and the NTLM protocol, we must confirm that the
Negotiate security header is set in the NTAuthenticationProviders metabase property. For Negotiate authentication to
function correctly, several exchanges must take place on the same connection. Therefore, Negotiate authentication cannot be
used if an intervening proxy does not support keep-alive connections. If this is not understood, it should not concern us.
NOTE: The account management supported by NT Active Directory Services requires a corresponding authentication
protocol for network log-on. Based on RFC 1510 (www.ietf.org/rfc/rfc1510.txt), the Kerberos protocol provides enhanced
authentication for the distributed computing environment and standardization to interact with other operating systems.
FUNNY DIGRESSION: the Term NT stands for New Technology. It was adopted when Microsoft and IBM parted on their
joint OS2 venture. IBM, slow about everything (a self-study showed it took them 9 weeks to ship an empty box), they refused
to adopt the revolutionary, advanced technology Microsoft was quickly developing without it being time-tested (meaning
proven; this is why NASA uses 20-year-old technology), so Microsoft made its own version of OS2 that used it, naming it NT.
Now, think about how many times we have read or heard even Microsoft mentioning the term NT Technology?
In most cases, such as home use, we will not be accessing a local IIS (intranet) SMTP server. In that
case we will need to create an SSL SMTP Client credential through a new NetworkCredential Object,
providing it with our SSL Username and Password, as shown in this hand-coded example:
Dim smtpEmail As New Mail.SmtpClient("smtp.gmail.com", 465) 'create new Gmail SMTP client with SSL for outgoing eMail.
smtpEmail.EnableSsl = True 'True if SSL/TLS authentication is required.
smtpEmail.Credentials = New NetworkCredential("Norio.Nachamichi.Jones@gmail.com","MomBNipp0neze") 'new credential with Username, Password.

NOTE: We cannot use decorated usernames for creating a network credential. We will not be able to use something like
Tukool Firwurds <iBeAnicn@chic.net>. We would have to provide just the actual email address: iBeAnicn@chic.net.
Although the above methods work in most domains, highly secure domains may require more than an
SSL certificate to reach the outside world. For example, if we are a minion at the Engulf & Devour
Credit Corp., we may need to apply code that bypasses massive firewalls and multi-layer proxies, which
any high school youth worthy of their salt can usually break through before Second Period.
As indicated, we need to create a NetworkCredential object if we access a server through an SSL/TLS
layer, such as Gmail or HotMail, because we will not be able to use default credentials. Compare these
examples, demonstrating default access through “plain” and SSL secure Comcast servers:
QuickiEMail("Idjut@comcast.net", "editor@nyt.com", "Letter to the Editor", "Your paper lines my dog cages.", "smtp.comcast.net") 'plain.
QuickiEMail("Bob <Idjut@comcast.net>", "ed@abrv.com", "Ltr 2 Ed", "Yr ppr lns m dg cgz 2.",
"smtp.comcast.net", 465, True, "Idjut", "pSSwd#6") 'secure.

NOTE: Gmail, like a few other SSL/TLS servers, require the user’s FULL email address for their certificates (includes
@gmail.com). Leaving the SSLUsername field blank, the user’s email address will be used as long as it is not decorated.
But even so, just the above QuickiEMail method supports most emails that people need to transmit, and
is, in fact, all the outgoing email support than a great deal of people will ever require.
An Email Sender with Some Muscle.
If we require multiple recipients, multiple optional CC (Carbon Copy) recipients, multiple optional BCC
(Blind Carbon Copy) recipients, multiple attachments, or if we want to send the body text as HTML
format, or send alternate views of the message body, we will want a method with a whole lot more
muscle, like the following SendEmail method:
'*******************************************************************************
' Function Name : SendEMail
' Purpose : Send a more complex email message
'*******************************************************************************
'NOTES: strFrom : Full email address of who is sending the email. ie, David Dingus <daviddingus@att.net> or daviddingus@att.net.
' strTo : Full email address of who to send the email to. ie, "Bubba Dingus" <bob.dingus@cox.com> or bob.dingus@cox.com.
' : If multiple recipients, separate each full email address using a semicolon (;).
' strSubject: Brief text regarding what the email concerns.
' strBody : text that comprises the message body of the email. May be raw text or HTML code.
' IsHTML : True if the strBody data is HTML, or the type of data that would be contained within an HTML Body block.
' smtpHost : This is the email host you are using for sending emails, such
' : as "smtp.gmail.com", "smtp.comcast.net", "authsmtp.juno.com", etc.
' AltView : A System.Net.Mail.AlternateView object containing a formatted message, such as Rich Text or HTML.
' : If need be, set AltView.ContentType.MediaType and AltView.TransferEncoding to properly format the AlternateView.
' : For example: AltView.ContentType.MediaType = Mime.MediaTypeNames.Text.RichText
' : AltView.TransferEncoding = Mime.TransferEncoding.Base64
' StrCC : Send "carbon copies" of email to this or these recipients.
' : If multiple recipients, separate each full email address using a semicolon (;).
' strBcc : Blind Carbon Copy. Hide this or these recipients from view by others.
' : If multiple recipients, separate each full email address using a semicolon (;).
' strAttachments: A single filepath, or a list of filepaths to send to the recipient.
' : If multiple attachments, separate each filepath using a semicolon (;) (C:\my data\win32.txt; c:\jokes.rtf)
 Page 387

' : The contents of the attachments will be encoded and sent.


' : If you wish to send the attachment by specifying content type (MediaType) and content transfer encoding
' : (Encoding), then follow the attachment name with the MediaType and optional encoding (default is
' : application/octet-stream, Base64) by placing them within parentheses and separated by a comma. For example:
' : C:\My Files\API32.txt (text/plain, SevenBit); C:\telnet.exe (application/octet-stream, Base64)
' : Where: The MediaType is determined from the System.Net.Mime.MediaTypeNames class, which
' : can specify Application, Image, or Text lists. For example, the above content type,
' : "text\plain", was defined by acquiring System.Net.Mime.MediaTypeNames.Text.Plain.
' : The second parameter, Encoding, is determined by the following values specified with the
' : System.Net.Mime.TrasperEncoding enumeration:
' : QuotedPrintable (acquired by System.Net.Mime.TransferEncoding.QuotedPrintable.ToString).
' : Base64 (acquired by System.Net.Mime.TransferEncoding.Base64.ToString).
' : SevenBit (acquired by System.Net.Mime.TransferEncoding.SevenBit.ToString).
' smtpPort : TCP Communications Port to use. Most servers default to port 25 for unsecure transmission.
' usesSLL : If this value is TRUE, then use SSL Authentication protocol for secure communications.
' SSLUsername: If usesSLL is True, this is the username to use for creating a credential. Leave blank if the same as strFrom.
' SSLPassword: If usesSLL is True, this is the password to use for creating a credential. If this field and SSLUsername
' : are blank, then default credentials will be used (but this only works on local, intranet servers).
' SSLDomain : If creating a credential with a SPECIFIC domain is required, set this parameter, otherwise, leave it blank.
'*******************************************************************************
Public Function SendEMail(ByVal strFrom As String,
ByVal strTo As String,
ByVal strSubject As String,
ByVal strBody As String,
ByVal IsHTML As Boolean,
ByVal smtpHost As String,
Optional ByVal AltView As Mail.AlternateView = Nothing,
Optional ByVal strCC As String = Nothing,
Optional ByVal strBcc As String = Nothing,
Optional ByVal strAttachments As String = Nothing,
Optional ByVal smtpPort As Integer = 25,
Optional ByVal usesSSL As Boolean = False,
Optional ByVal SSLUsername As String = Nothing,
Optional ByVal SSLPassword As String = Nothing,
Optional ByVal SSLDomain As String = Nothing) As Boolean

Dim Email As New Mail.MailMessage 'create a new mail message.


With Email
.From = New Mail.MailAddress(strFrom) 'add FROM to mail message (must be a Mail Address object).
'-------------------------------------------
Dim Ary() As String = Split(strTo, ";") 'add TO to mail message (possible list of email addresses; separated each with ";").
For Idx As Integer = 0 To UBound(Ary)
If Len(Trim(Ary(Idx))) <> 0 Then .To.Add(Trim(Ary(Idx))) 'add each TO recipient (primary recipients).
Next
'-------------------------------------------
.Subject = strSubject 'add SUBJECT text line to mail message.
'-------------------------------------------
.Body = strBody 'add BODY text of email to mail message.
.IsBodyHtml = IsHTML 'indicate if the message body is actually HTML text.
'-------------------------------------------
If AltView IsNot Nothing Then 'if an alternate view of plain text message is defined...
.AlternateViews.Add(AltView) 'add the alternate view
End If
'-------------------------------------------
If CBool(Len(strCC)) Then 'add CC (Carbon Copy) email addresses to mail message.
Ary = Split(strCC, ";") '(possible list of email addresses, separated each with ";").
For Idx As Integer = 0 To UBound(Ary)
If Len(Trim(Ary(Idx))) <> 0 Then .CC.Add(Trim(Ary(Idx))) 'add each recipient.
Next
End If
'-------------------------------------------
If CBool(Len(strBcc)) Then 'add Bcc (Blind Carbon Copy) email addresses to mail message.
Ary = Split(strBcc, ";") '(possible list of email addresses; separated each with ";").
For Idx As Integer = 0 To UBound(Ary)
If Len(Trim(Ary(Idx))) <> 0 Then .Bcc.Add(Trim(Ary(Idx))) 'add each recipient (hidden recipents).
Next
End If
'-------------------------------------------
If CBool(Len(strAttachments)) Then 'add any attachments to mail message.
Ary = Split(strAttachments, ";") '(possible list of file paths, separated each with ";").
For Idx As Integer = 0 To UBound(Ary) 'process each attachment.
Dim attach As String = Trim(Ary(Idx)) 'get attachment data.
If Len(attach) <> 0 Then 'if an attachment present...
Dim I As Integer = InStr(attach, "(") 'check for formatting instructions.
If CBool(I) Then 'formatting present?
Dim Fmt As String 'yes, so set up format cache.
Fmt = Mid(attach, I + 1, Len(attach) - I - 1) 'get format data.
attach = Trim(VB.Left(attach, I - 1)) 'strip format data from the attachment path.
Dim Atch As New Mail.Attachment(attach) 'create a new attachment.
Dim fmts() As String = Split(Fmt, ",") 'break formatting up.
For I = 0 To UBound(fmts) 'process each format specification.
Fmt = Trim(fmts(I)) 'grab a format instruction.
If CBool(Len(Fmt)) Then 'data defined?
Select Case I 'yes, so determine which type of instruction to process.
Case 0 'index 0 specified MediaType.
Atch.ContentType.MediaType = Fmt 'set media type to attachment.
Case 1 'index 1 specifies Encoding.
Select Case LCase(Fmt) 'check the encoding types and process accordingly.
Case "quotedprintable", "quoted-printable"
Atch.TransferEncoding = Mime.TransferEncoding.QuotedPrintable
Case "sevenbit", "7bit"
Atch.TransferEncoding = Mime.TransferEncoding.SevenBit
Case Else
Atch.TransferEncoding = Mime.TransferEncoding.Base64
End Select
End Select
End If
Next
.Attachments.Add(Atch) 'add attachment to email.
 Page 388

Else
.Attachments.Add(New Mail.Attachment(attach)) 'add filepath (if no format specified, encoded in effiecient Base64).
End If
End If
Next
End If
End With
'-----------------------------------------------------------------------
'now open the email server...
Try
Dim SmtpEmail As New Mail.SmtpClient(smtpHost, smtpPort) 'create new SMTP client on the SMTP server.
SmtpEmail.EnableSsl = usesSSL 'true if SSL Authentication required.
If usesSSL Then 'SSL authentication required?
If Len(SSLUsername) = 0 AndAlso Len(SSLPassword) = 0 Then 'if both SSLUsername and SSLPassword are blank...
SmtpEmail.UseDefaultCredentials = True 'use default credentials.
Else 'otherwise, we must create a new credential.
If Not CBool(Len(SSLUsername)) Then 'if SSLUsername is blank, use strFrom.
SmtpEmail.Credentials = New NetworkCredential(strFrom, SSLPassword, SSLDomain)
Else
SmtpEmail.Credentials = New NetworkCredential(SSLUsername, SSLPassword, SSLDomain)
End If
End If
End If
SmtpEmail.Send(Email) 'finally, send the email...
Catch e As Exception 'if error, report it.
MsgBox(e.Message, MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation, "Mail Error")
Return False 'return failure flag.
End Try
Return True 'return success flag.
End Function

Notice that in this version we have added more recipients. A lot more. There is a TO list, a Carbon Copy
list (CC), and a Blind Carbon Copy list (BCC; not viewable by TO or CC recipients). The CC list is an
archaic vestige, except for business transactions, where it is useful to inform the primary recipient that
these other people also got a copy of it. The BCC was essential for sending copies to other concerned
parties, but it was not essential, or the sender did not want the TO or CC viewers to know that these BCC
recipients were being sent copies. Some claim there is no use for a BCC list, but I beg to differ.
NOTE: Microsoft Mail features BCC, but it does not seem to work (under Windows Vista, anyway).
The important thing to notice about these three recipient fields is that we separate each recipient with a
semicolon (;). Notice also that in my code that I did a check on each after I split them into an array to
ensure that a field was not empty. Most email applications simply slap a semicolon on the tail of each
email address, so splitting them into an array will leave any last array element empty.
The Attachments, strAttachments, we handle just like TO, CC and BCC. The file paths to the
attachments are separated by semicolons. The Mail.Attachments collection object takes care of loading
the actual file data. Attachments are appended to the end of the email. As noted in the comments above
the method, we can also declare the encoding and display formats for an attachment; otherwise they will
default to binary (“application/octet-stream”) and encoded for internet transport using the Base64
method, which, as all, converts them to encoded 7-bit text, which all emails must be formatted to for
internet transfer, so simplifying things by just using the default should be no issue of concern.
An email is actually a series of bytes (in email lingo, these are called octets; 8-bits), formatted as 7-bit
ASCII text (ergo, the 8th bit is never used). As such, even binary attachments are encoded into blocks of
ASCII text, sometimes formatted, especially by older systems, as ASCII Hexadecimal (hex; “0”
through “9”, and “A” through “F”, allowing for a Base16 numbering system, though this also doubles
the data size), where each byte is represented by two 7-bit characters, and each character represents a
nibble, or 4 bits (computer engineers must always be hungry). However, many servers now support
various types of encoding, like Base64, to better transport 8-bit/binary data in the 7-bit-only catacombs
of the internet (at a cost of the data’s footprint being about 25% larger). For example, my API32.txt file,
which, by its extension, is hopefully a text file, looks like gobbledygook at the bottom of my email when
converted using the default Base64 encoding. Here is a sampling of its beginning:
 Page 389

NOTE: My comments in the SendEmail method header regarding attachments, where I discuss formatting the attachment to
different content types and encoding, we will leave for later, when I actually discovered these solutions as I was learning.
From: mercedes_silver@80micro.com
To: david.ross.goben@gmail.com
Date: 21 Feb 2011 21:16:00 -0500
Subject: Test
Content-Type: multipart/mixed; boundary=--boundary_0_5fbcc36e-0097-412e-bf2b-c4dc5bc543d0

----boundary_0_5fbcc36e-0097-412e-bf2b-c4dc5bc543d0
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: quoted-printable

This is just a test


----boundary_0_5fbcc36e-0097-412e-bf2b-c4dc5bc543d0
Content-Type: application/octet-stream; name=API32.txt ▬▬ The Name parameter identifies this as an attachment (and it was handled as binary stream)
Content-Transfer-Encoding: base64 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ How the file is encoded (I would have rather had this be text/plain, with 7bit encoding)

JyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t
LS0tLS0tLS0tLS0tLS0tLS0tLS0NCicNCicgICAgIEFQSTMyLlRYVCAtLSBXaW4zMiBBUEkg
VHlwZSBEZWNsYXJhdGlvbnMgZm9yIFZpc3VhbCBCYXNpYw0KJw0KJyAgICAgICAgICAgICAg
ICAgICAgICAgQ29weXJpZ2h0IChDKSAxOTk2IERlc2F3YXJlDQonDQonICBZb3UgaGF2ZSBh
IHJveWFsdHktZnJlZSByaWdodCB0byB1c2UsIG1vZGlmeSwgcmVwcm9kdWNlIGFuZCBkaXN0
... ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ The encoded definition of several thousand more API declarations, structures, and
constants continues from here.

NOTE: In the back of my mind, when I encountered this during my initial email tests, I was really wondering about this
Base64 Content Transfer Encoding. But, we will return to this when we examine alternate views and attachments, where it
will make more sense to us. We will also learn how we can fully exploit it and very easily decode it (and also how to avoid it).
Two other parameters we may have noticed in the SendEmail method were IsHTML and AltView.
Sending Email Messages as HTML.
The IsHTML parameter in the SendEMail method sets the state of the Mail.MailMessage object’s
Boolean IsBodyHtml flag. If it is set to True, then the SMTP interface will know to set the body text
formatting flag to text/html instead of its usual text/plain. It is actually up to email reader software to use
that information and determine how to present the data. For example, some plain text readers will simply
show the raw data, regardless. However, others will bring up a web interface, such as a WebBrowser
control, envelop the text within an HTML body, if it is not wrapped already, and present that to the user.
This is quite easy to do, but there is a simple test we will need to perform, because some main body
HTML is sent without HTML/BODY tags, though most are. The easiest test is to simply check to see if
“</HTML>” is contained within the message. If not, all we have to do is prepend the text “ <HTML><BODY>” in
front of the message, and append the text “</BODY></HTML>” behind it. And that is all! Consider this test,
where String variable Msg is assumed to contain the HTML-formatted text of the message body:
If Not Msg.Contains("</HTML>") Then 'Msg contains an HTML wrapper?
Msg = "<HTML><BODY>" & Msg & "</BODY></HTML>" 'no so add one to it
End If

Suppose I sent the following urgent code red email message (note the True for the IsHTML parameter):
Dim Msg As String = "<b>This is bold text</b><p><u>This should be underlined</u><p>" 'some simple HTML text.
SendEMail("mercedes_silver@80micro.com", "david.ross.goben@gmail.com", "Test", Msg, True, "smtp.80micro.com")

My Gmail account will receive an email with the following at the bottom of the data (less my notes):
From: mercedes_silver@80micro.com
To: david.ross.goben@gmail.com
Date: 21 Feb 2011 21:39:04 -0500
Subject: Test
Content-Type: text/html; charset=us-ascii ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Used second to determine how to display or process the data.
Content-Transfer-Encoding: quoted-printable ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Used first to determine how to decode the data.
<b>This is bold text</b><p><u>This should be underlined</u><p> ▬▬▬▬ The HTML formatted text without HTML or BODY tags.

Once I strip out the header data (I will show this later on), I end up with a String variable I named Msg
that contains the text “<b>This is bold text</b><p><u>This should be underlined</u><p>”. I would expose it within
my Web Browser interface control, WebBrowser1, using code similar to the following:
Me.WebBrowser1.Bounds = Me.RichTextBox1.Bounds 'set the web browser to the same location and dimensions as my usual text display.
control
Me.RichTextBox1.Visible = False 'hide my plain text/Richtext textbox.
If Not Msg.Contains("</HTML>") Then 'Does my Msg contain an HTML wrapper?
Msg = "<HTML><BODY>" & Msg & "</BODY></HTML>" 'no so add one to it.
End If
Me.WebBrowser1.DocumentText = Msg 'set web browser contents to that of my email message body and display it.
Me.WebBrowser1.Visible = True 'expose the web browser.
 Page 390

The two lines of text are dutifully displayed using the HTML formatting I assigned to
them. The first line was Bold, and the second line was underlined. The urgent code
red message was processed and read in time! The world is once again safe.
There is so much more to explore if we truly want to create a full-featured email
processor. The Internet Message Format document, RFC 2822 (www.ietf.org/rfc/rfc2822.txt),
outlines all the gory details of email formatting.
I should close this sub-section by saying that the message data on multi-line
documents should be further processed to ensure that the data is properly formatted
for our web browser, rich text box, or simple text box, such as by decoding special tags that may have
been added to represent Unicode text within the simple 7-bit ASCII text format required for email
transactions, for example. These considerations will also be covered in this article.
Data in an email is a series of lines, each terminated by a vbCrLf (Carriage Return and Linefeed – codes
13 and 10, respectively). Often these line terminators are not a part of the original text, but are required
to limit the line width of the email data. In these cases they are tagged, such as by using a “soft return”
flag, like the equals sign “=” used by Quoted-Printable encoding. These must be decoded and removed
before displaying the text. Also with Quoted-Printable-encoded text, if a space character precedes a
vbCrLf, then that space is converted to a special hexadecimal format, “ =20”, which I habitually call
Hex-Tags, because they are formatted in Base16 (Hexadecimal). Also, if there are any 8-bit characters
embedded in the message, then we should convert them to Hex-Tags, otherwise the system will
automatically encode the data to Base64, regardless of what we really want, such as Quoted-Printable,
which it may be forced to do simply to ensure 100% original data integrity, which specifications require.
Fortunately, I will later present very simple functions to allow Quoted-Printable encoding of 8-bit text
that will convert any 8-bit codes to 7-bit without losing integrity, as well as decode Quoted-Printable-
encoded and Base64-encoded data back to its original form with ease.

Sending Alternate Message Views.


Another thing I want to explore with you is one of the more interesting parameters I have listed for the
SendEMail method, and that is AltView.

The AltView parameter is a System.Net.Mail.AlternativeView object. Setting up an alternate view is


easy. For example, to create an alternate view from a RichTextBox control named myRTC, we can use:
Dim AltView As Mail.AlternateView = Mail.AlternateView.CreateAlternateViewFromString(myRTC.Rtf)
AltView.ContentType.MediaType = Mime.MediaTypeNames.Text.RichText 'how the alternate view should be handled, once it is decoded.
AltView.TransferEncoding = Mime.TransferEncoding.Base64 'how it should be encoded for transport across the internet.

Even though my SendEMail method presently allows for only one alternative view, which is usually all
we ever really need, the .NET SMTP processor will actually allow for as many as we want, in
compliance to RFC 2822, as though we would want to spend the rest of our miserable lives toiling over
the various formatting of a single email to say “Thanks for the $1 on my birthday” to Great Aunt Ethel.
However, the real reason for this is more mundane: some people simply want to send both a “pretty”
version of their email and a plain text version for those who may want to view them on a cell phone.
Technically, most email processors will, by default, display the alternate view, and typically the first
alternate view they are able to support, leaving the Plain Text version as the “last ditch” option.
Typically, most of us tend to send plain text emails, even if they are in fact formatted by a Rich Text or
HTML editor, failing to add emphasis, bolding, italics, or underlining. Phooey! I still remember the thrill
I got when I hit a button on my Selectric II Typewriter and it bolded whem I typed something, or got my
first TypeBall – we called them golf balls – that supported italics. I did not even blink an eye when I had
to go through the time and effort of changing the TypeBall just to change fonts, even to just affect a
single word. For me, at least, plain text is so monotonous and passé.
 Page 391

Most people prefer their emails to be formatted as HTML or as Rich Text, but they also want the option
to process their text as Plain Text for those who want to read their email on a cell phone, or who are
vision-impaired (“Can’t see out of one eye, and blind as a bat in the other,” as Grandpa often said).
Most of us are aware that in a VB-written Rich Text editor using a RichTextBox control, we have easy
access to the Plain Text version and the Rich Text version of a document. Its Text property provides the
Plain Text version, and its Rtf property provides the Rich Text version.
But did you know that accessing plain text from HTML formatting can also be easy?
An HTML version can be provided by accessing the DocumentText property of their WebBrowser
control, as most of us are already aware. But a Plain Text rendering has always seemed to be an issue. I
have seen a number of home-spun HTML editors that provide a Plain Text version of their data by
manually stripping out all the HTML Text Tags, plus any special formatting that might be stored within
or between them, on top of going through the often arduous task of interpreting all the special HTML
Entities in order to provide that “simple” plain-text version. This adds up to a whole lot of work.
NOTE: An HTML Text Tag is a thing starting with “<” and ending with “>”, such as “<HTML>” or “</BODY>”.
NOTE: HTML Entities start with an ampersand (&) and end with a semicolon (;), such as “&lt;” to represent an
intentional “<”,“&gt;” for an intentional “>”, “&nbsp;” (non-breaking space) for a blank space “ ” where a space might
normally be ignored, plus “;;” for an intentional “;”. Following is a list of the HTML Reserved Entities:
Characrer Entity Number Entity Name Description
" &#34; &quot; quotation mark (?)
' &#39; &apos; apostrophe (’)
& &#38; &amp; ampersand (&)
< &#60; &lt; less-than (<)
> &#62; &gt; greater-than (>)

As we can see, there are two versions of tags; one that includes the decimal ASCII code, and one that includes the typical or
“classic” representation. The Entity Number also allows 8- or 16-bit extended characters to be displayed by a 7-bit source.
Even though I have seen a number of utilities, both commercial and shareware, that offer this kind of
service, we can in fact bypass them and remove better than 75% of that work with this 1-line function:
'*******************************************************************************
' Function Name : QConvertHTML2Text
' Purpose : Short-Form Convert HTML formatted text to plain text
' :
' Returns : Provided a simple HTML source string, it will return a Plain Text.
' : string with HTML code removed.
'*******************************************************************************
Public Function QConvertHTML2Text(ByVal HTMLText As String) As String
Return RegularExpressions.Regex.Replace(HTMLText.Replace("&nbsp;", " ").Replace("&quot;", """").Replace("&apos;",
"'"), "<[^>]*>", "").Replace("&lt;", "<").Replace("&gt;", ">").Replace("&amp;", "&").Replace(";;", ";")
End Function

The above function will also remove all the extra data within the HTML tags. Just invoke it like this:
“Dim PlainText As String = QConvertHTML2Text(HtmlText)”.
NOTE: In a pinch, copy from the HTML display of a WebBrowser control, paste it to Notepad, then copy it as plain text, but
this does not provide for line formatting, and can sometimes look almost as disorganized as a college dorm room.
However, the following enhanced method does 99.99% of the work that any commercial package offers:
'*******************************************************************************
' Function Name : ConvertHTML2Text
' Purpose : Convert HTML formatted text to plain text.
' :
' Returns : Provided a complex HTML string, it will return a Plain Text string
' : with all HTML codes and formatting removed from it.
' :
' NOTE : Numerous of these conversions will convert the text to 8-bit,
' : though most of these symbols will not be encountered in most
' : HTML documents we produce. But regardless of that, if you wish
' : to make this conversion the main body message of an email, you
' : may have to further convert this using ForceQuotedPrintable()
' : to maintain Quoted-Printable encoding and avoid Base64, even
' : though this is typically not an issue. However, some few really
' : primitive email readers, typically those that simply allow you
' : to preview email messages, without fully loading them, will not
' ' know how to support Base64, or will not bother with it, but simply
' : display the raw data. RFC 2045 requires email handlers to support it.
'*******************************************************************************
Public Function ConvertHTML2Text(ByVal HTMLText As String) As String
' instantiate an initially blank StringBuilder object.
Dim Sb As New StringBuilder()
 Page 392

' first remove leading whitespace of each line and append the result to the StringBuilder.
Dim ary() As String = Split(HTMLText, vbCrLf)
For Each S As String In ary
Sb.Append(S.TrimStart(Chr(9), " "c))
Next

' replace reserved entities (except <, >, and &).


Sb.Replace("&quot;", """").Replace("&apos;", "'").Replace("&nbsp;", " ")

' replace HTML paragraph, line breaks, and table entry terminators with vbCrLf
Sb.Replace("<p>", vbCrLf).Replace("<P>", vbCrLf).Replace("</p>", vbCrLf).Replace("</P>", vbCrLf).Replace("<br>",
vbCrLf).Replace("<BR>", vbCrLf).Replace("</td>", vbCrLf).Replace("</TD>", vbCrLf)

' replace ISO 8859-1 Symbols (160-255). Note that any matches will make the text 8-bit
Sb.Replace("&iexcl;", "¡").Replace("&cent;", "¢").Replace("&pound;", "£").Replace("&curren;",
"¤").Replace("&yen;", "¥").Replace("&brvbar;", "¦").Replace("&sect;", "§").Replace("&uml;",
"¨").Replace("&copy;", "©").Replace("&ordf;", "ª").Replace("&laquo;", "«").Replace("&not;",
"¬").Replace("&shy;", "-").Replace("&reg;", "®").Replace("&macr;", "¯").Replace("&deg;",
"°").Replace("&plusmn;", "±").Replace("&sup2;", "²").Replace("&sup3;", "³").Replace("&acute;",
"´").Replace("&micro;", "µ").Replace("&para;", "¶").Replace("&middot;", "•").Replace("&cedil;",
"¸").Replace("&sup1;", "¹").Replace("&ordm;", "º").Replace("&raquo;", "»").Replace("&frac14;",
"¼").Replace("&frac12;", "½").Replace("&frac34;", "¾").Replace("&iquest;", "¿").Replace("&times;",
"×").Replace("&divide;", "÷")

' replace ISO 8859-1 characters. Note that any matches will make the text 8-bit
Sb.Replace("&Agrave;", "À").Replace("&Aacute;", "Á").Replace("&Acirc;", "Â").Replace("&Atilde;", "Ã").Replace("&Auml;",
"Ä").Replace("&Aring;", "Å").Replace("&AElig;", "Æ").Replace("&Ccedil;", "Ç").Replace("&Egrave;",
"È").Replace("&Eacute;", "É").Replace("&Ecirc;", "Ê").Replace("&Euml;", "Ë").Replace("&Igrave;",
"Ì").Replace("&Iacute;", "Í").Replace("&Icirc;", "Î").Replace("&Iuml;", "Ï").Replace("&ETH;",
"Ð").Replace("&Ntilde;", "Ñ").Replace("&Ograve;", "Ò").Replace("&Oacute;", "Ó").Replace("&Ocirc;",
"Ô").Replace("&Otilde;", "Õ").Replace("&Ouml;", "Ö").Replace("&Oslash;", "Ø").Replace("&Ugrave;",
"Ù").Replace("&Uacute;", "Ú").Replace("&Ucirc;", "Û").Replace("&Uuml;", "Ü").Replace("&Yacute;",
"Ý").Replace("&THORN;", "Þ").Replace("&szlig;", "ß").Replace("&agrave;", "à").Replace("&aacute;",
"á").Replace("&acirc;", "â").Replace("&atilde;", "ã").Replace("&auml;", "ä").Replace("&aring;",
"å").Replace("&aelig;", "æ").Replace("&ccedil;", "ç").Replace("&egrave;", "è").Replace("&eacute;",
"é").Replace("&ecirc;", "ê").Replace("&euml;", "ë").Replace("&igrave;", "ì").Replace("&iacute;",
"í").Replace("&icirc;", "î").Replace("&iuml;", "ï").Replace("&eth;", "ð").Replace("&ntilde;",
"ñ").Replace("&ograve;", "ò").Replace("&oacute;", "ó").Replace("&ocirc;", "ô").Replace("&otilde;",
"õ").Replace("&ouml;", "ö").Replace("&oslash;", "ø").Replace("&ugrave;", "ù").Replace("&uacute;",
"ú").Replace("&ucirc;", "û").Replace("&uuml;", "ü").Replace("&yacute;", "ý").Replace("&thorn;",
"þ").Replace("&yuml;", "ÿ")

' replace Math Symbols Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("&forall;", "∀").Replace("&part;", "∂").Replace("&exist;", "∃").Replace("&empty;", "∅").Replace("&nabla;",
"∇").Replace("&isin;", "∈").Replace("&notin;", "∉").Replace("&ni;", "∋").Replace("&prod;",
"∏").Replace("&sum;", "∑").Replace("&minus;", "−").Replace("&lowast;", "∗").Replace("&radic;",
"√").Replace("&prop;", "∝").Replace("&infin;", "∞").Replace("&ang;", "∠").Replace("&and;",
"∧").Replace("&or;", "∨").Replace("&cap;", "∩").Replace("&cup;", "∪").Replace("&int;",
"∫").Replace("&there4;", "∴").Replace("&sim;", "∼").Replace("&cong;", "≅").Replace("&asymp;",
"≈").Replace("&ne;", "≠").Replace("&equiv;", "≡").Replace("&le;", "≤").Replace("&ge;",
"≥").Replace("&sub;", "⊂").Replace("&sup;", "⊃").Replace("&nsub;", "⊄").Replace("&sube;",
"⊆").Replace("&supe;", "⊇").Replace("&oplus;", "⊕").Replace("&otimes;", "⊗").Replace("&perp;",
"⊥").Replace("&sdot;", "⋅") 'NOTE: certain characters have tall characteristics

' replace Greek Letters Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("&Alpha;", "Α").Replace("&Beta;", "Β").Replace("&Gamma;", "Γ").Replace("&Delta;", "Δ").Replace("&Epsilon;",
"Ε").Replace("&Zeta;", "Ζ").Replace("&Eta;", "Η").Replace("&Theta;", "Θ").Replace("&Iota;",
"Ι").Replace("&Kappa;", "Κ").Replace("&Lambda;", "Λ").Replace("&Mu;", "Μ").Replace("&Nu;",
"Ν").Replace("&Xi;", "Ξ").Replace("&Omicron;", "Ο").Replace("&Pi;", "Π").Replace("&Rho;",
"Ρ").Replace("&Sigma;", "Σ").Replace("&Tau;", "Τ").Replace("&Upsilon;", "Υ").Replace("&Phi;",
"Φ").Replace("&Chi;", "Χ").Replace("&Psi;", "Ψ").Replace("&Omega;", "Ω").Replace("&alpha;",
"α").Replace("&beta;", "β").Replace("&gamma;", "γ").Replace("&delta;", "δ").Replace("&epsilon;",
"ε").Replace("&zeta;", "ζ").Replace("&eta;", "η").Replace("&theta;", "θ").Replace("&iota;",
"ι").Replace("&kappa;", "κ").Replace("&lambda;", "λ").Replace("&mu;", "μ").Replace("&nu;",
"ν").Replace("&xi;", "ξ").Replace("&omicron;", "ο").Replace("&pi;", "π").Replace("&rho;",
"ρ").Replace("&sigmaf;", "ς").Replace("&sigma;", "σ").Replace("&tau;", "τ").Replace("&upsilon;",
"υ").Replace("&phi;", "φ").Replace("&chi;", "χ").Replace("&psi;", "ψ").Replace("&omega;",
"ω").Replace("&thetasym;", "ϑ").Replace("&upsih;", "ϒ").Replace("&piv;", "ϖ")

' replace Other Entities Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("&OElig;", "Œ").Replace("&oelig;", "œ").Replace("&Scaron;", "Š").Replace("&scaron;", "š").Replace("&Yuml;",
"Ÿ").Replace("&fnof;", "ƒ").Replace("&circ;", "ˆ").Replace("&tilde;", "˜").Replace("&ensp;",
" ").Replace("&emsp;", " ").Replace("&thinsp;", " ").Replace("&ndash;", "–").Replace("&mdash;",
"—").Replace("&lsquo;", "‘").Replace("&rsquo;", "’").Replace("&sbquo;", "‚").Replace("&ldquo;",
" ").Replace("&rdquo;", " ").Replace("&bdquo;", "„").Replace("&dagger;", "†").Replace("&Dagger;",
"‡").Replace("&bull;", "•").Replace("&hellip;", "...").Replace("&permil;", "‰").Replace("&prime;",
"′").Replace("&Prime;", "″").Replace("&lsaquo;", "‹").Replace("&rsaquo;", "›").Replace("&oline;",
"‾").Replace("&euro;", "€").Replace("&trade;", "™").Replace("&larr;", "←").Replace("&uarr;",
"↑").Replace("&rarr;", "→").Replace("&darr;", "↓").Replace("&harr;", "↔").Replace("&crarr;",
"↵").Replace("&lceil;", "⌈").Replace("&rceil;", "⌉").Replace("&lfloor;", "⌊").Replace("&rfloor;",
"⌋").Replace("&loz;", "◊").Replace("&spades;", "♠").Replace("&clubs;", "♣").Replace("&hearts;",
"♥").Replace("&diams;", "♦") 'NOTE: certain characters have tall characteristics

' replace special ASCII coding entities that were not captured by the above. Note that values > 127 will make the text 8-bit.
For Idx As Integer = 1 To 255 'See www.w3schools.com/tags/ref_entities.asp.
Sb.Replace("&#" & Idx.ToString & ";", Chr(Idx)) 'replace most common numeric entities.
Next

' Ensure header definitions are followed by vbCrLf


Dim NewText As String = RegularExpressions.Regex.Replace(Sb.ToString(), "</H[^>]*>", vbCrLf)

' Also seek out other Unicode encoded number entities not covered by the above and individually update them.
Dim Idy As Integer = InStr(NewText, "&#") 'check for a numeric entity.
Do While Idy <> 0 'loop as long as we find one.
Dim Idz As Integer = InStr(Idy, NewText, ";") 'find terminating semicolon.
Dim S As String = Mid(NewText, Idy, Idz - Idy + 1) 'grab expression.
RegularExpressions.Regex.Replace(NewText, S, Chr(CInt(Mid(S, 3, Len(S) - 3)))) 'replace expression.
InStr(Idy + 1, NewText, "&#")
Loop
 Page 393

' strip remaining HTML text tags, replace < and > placeholders, convert ampersand, replace ;; with ;, then return result.
Return RegularExpressions.Regex.Replace(NewText, "<[^>]*>", "").Replace("&lt;",
"<").Replace("&gt;", ">").Replace("&amp;", "&").Replace(";;", ";")
End Function

Sending a Plain Text Message Body and a RichText AlternateView.


Suppose we wanted to send both a Plain Text and a Rich Text version of an email to someone, so if they
were trendy and wanted to view the chic version of our email as Rich Text, they could. Or, if they were
viewing our email on their trendy Droid, iPhone, HRC's supposedly secure throw-away Blackberry
(well, this is just something 17 told us), or other small Email-enabled portable device, they can read our
email in plain text, which reads much better on small screens.
We typically pass the Plain Text version as the strBody parameter of the SendEMail method. The Rich
Text or HTML version would be sent as an alternate view.
Email is progressing to the point to where most emails are in HTML format, which is why we have the
IsBodyHtml parameter on a MailMessage object, allowing us to transmit the main message body as
HTML. However, Rich Text is becoming so common that I expect one day to see an industry-recognized
IsBodyRtf parameter, though I would really hope that the main body would be forced to Plain Text, and
IsHtmlBody be revoked. Or, to be diplomatic, simply allow the user to optionally specify the Content-
Type and Content-Transfer-Encoding of the message body they choose to send, though in this case email
readers would have to be able to convert the RichText or HTML main message to plain text if required.
Consider the following, where early on in my initial trials I sent an email with a RTF alternate view:
'create an alternate view of the RTF data from our RichTextBox Control.
Dim rtfView As Mail.AlternateView = Mail.AlternateView.CreateAlternateViewFromString(Me.RichTextBox1.Rtf)
'send our email as plain text and with a rich text alternater view.
SendEMail("mercedes_silver@80micro.com", "david.ross.goben@gmail.com", "Test", Me.RichTextBox1.Text, False, "smtp.80micro.com", rtfView)

As we can see from this example, creating an AlternateView object is almost too easy. We can see it
using the Rich Text data for the alternate view, and its plain text for the message body parameter.
 Page 394

Although the results are valid and perfectly usable, I was disappointed not to see actual RTF text:
From: mercedes_silver@80micro.com
To: david.ross.goben@gmail.com
Date: 22 Feb 2011 17:19:08 -0500
Subject: Test
Content-Type: multipart/alternative; ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This email is multi-part (either alternate view or attachments added)
boundary=--boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Declare unique boundary: will be used to mark the boundaries of each part

----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ First REAL boundary marker. Main body of email follows (usually plain
text)
Content-Type: text/plain; charset=us-ascii ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Used as clue toward how to process data after it is decoded
Content-Transfer-Encoding: quoted-printable ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Encoded as Quoted-Printable. All controls codes rendered visible (=xx)
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This first blank line is NOT part of the message body (MUST BE BLANK)
This is a test of Rich Text data.=0A ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Note the decoration (=0A); a necessary result of email. Easy to address.
----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ End boundary for main body, as well as start boundary for next part
Content-Type: text/plain; charset=utf-8 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This reports plain text using UTF-8 (8-bit extended ASCII)
Content-Transfer-Encoding: base64 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Encoded using Base64 method for safe internet transfer of data
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This blank line is NOT part of the attachment (MUST BE BLANK)
e1xydGYxXGFuc2lcYW5zaWNwZzEyNTJcZGVmZjBcZGVmbGFuZzEwMzN7XGZvbnR0Ymx7XGYw ▬▬ Base64-encoded data (is this rich text?.......Yes, it is, but encoded.
XGZuaWxcZmNoYXJzZXQwIFRpbWVzIE5ldyBSb21hbjt9e1xmMVxmbmlsXGZjaGFyc2V0MCBN When it is decoded, it will be able to be processed as rich text)
aWNyb3NvZnQgU2FucyBTZXJpZjt9fQ0Ke1xjb2xvcnRibCA7XHJlZDBcZ3JlZW4wXGJsdWUw
O30NClx2aWV3a2luZDRcdWMxXHBhcmRcY2YxXGYwXGZzMjQgVGhpcyBpcyBhIFxiIHRlc3Qg
XGIwIG9mIFxpIFJpY2ggVGV4dCBkYXRhXGkwIC5ccGFyDQpccGFyZFxjZjBcZjFcZnMxN1xw
YXINCn0NCg==
----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321-- ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ End boundary for second part. Side note: note the extra "--" on both sides

What the–? (Well, that is what I first said. The notes tend to indicate garnered knowledge since then.)
NOTE: Even though I did not realize it at the time I first saw this, I can in fact fully use and properly translate the above
alternate view data with unbelievable ease. But that is getting ahead of what I had thought I was trying to do...

Sending Alternate Message Views with Different Context Types and Transfer Encoding.
There’s that Base64 thing again. Also, my RichText version of the message looks like the cat was
dancing on the keyboard chasing the fish on the computer’s screen saver. But for this implementation,
the MSDN documentation states in its remarks: “The default media type is plain text and the default
encoding is ASCII.” Well, that’s kinda true, except that UTF-8 (8-bit Unicode Transformation Format) is
not actually ASCII (Standard ASCII, also known as simply ASCII, is strictly 7-bit code). Microsoft’s
version of “ASCII” is a transformation format for translating between 16-bit Unicode and 8-bit
Extended-ASCII code (UTF-8). And there is also that Base64 part, which I was unfamiliar with at the
time. Something smells fishy, and it’s likely not the fish on the screen saver.
In my more rational mind, I acknowledged that the first part, the plain text version, looks fine. But the Rich
Text version is tagged, according to the Content-Type (how the reader should handle the decoded data), as
plain text, UTF-8. Yet the Content-Transfer-Encoding (how the data is encoded for internet transport)
indicates Base64. I had seen several other Rich Text emails, but most of them typically showed the Content-
Type as “text/richtext”, but with those I could also see the HTML-like Rich Text encoding of the message
body perfectly fine. So instead of shaking my fists at the heavens in forlorn, unquenchable yearning for
success, I decided that it was time to hit the books (GASP!) and find out what I was not yet doing.
Scanning the internet yielded no useful results. I did find myriad requests for help from people in the
same boat as me (well – 438,000 anyway; a crowded boat), but the knowledgeable respondents, always
trying to be helpful (except for the non-social jerks who just want to make everyone else as miserable as
they are), replied with some resigned candor that the hapless inquisitors were stuck with Base64
encoding, and we are presently incapable of changing that harsh truth any time soon under VB.NET.
After more time searching MSDN resources, I found another overload (which, dummy me, I should
have found in the first place), specifying another parameter for the CreateAlternateViewFromString
method. With it, I could add a second parameter that was a System.Net.Mime.ContentType object.
Hmm. Maybe it was not fish I smelled, after all. Is it maybe bovine scat? And mine?
If we look at this ContentType object, we can specify the text that Content-Type reports, such as
“text/plain”, “text/richtext”, or whatever I choose. This would be handy for encrypted emails, where we
could specify text indicating which decryption method for our custom email reader to use.
 Page 395

Constructing a ContentType object is easy. For example:


Dim ContentType As New Mime.ContentType(Mime.MediaTypeNames.Text.RichText)'create a ContentType object for the RTF data from our RichTextBox

NOTE: All the code in this article will assume that we have at least imported System.NET. and System.Text.
Now suppose we were to try sending that email again...
' create a custom ContentType object to "specify text/richtext".
Dim ContentType As New Mime.ContentType(Mime.MediaTypeNames.Text.RichText)'create an alternate view of the RTF data from our RichTextBox.
Dim rtfView As Mail.AlternateView = Mail.AlternateView.CreateAlternateViewFromString(Me.RichTextBox1.Rtf, ContentType)
' send our email as plain text and with a rich text alternate view.
SendEMail("mercedes_silver@80micro.com", "david.ross.goben@gmail.com", "Test", Me.RichTextBox1.Text, False, "smtp.80micro.com", rtfView)

D’OH! The results are the same! The only difference between the current version and the previous
version is that instead of the Content-Type declaring “text/plain”, it states “text/richtext”, but the
Content-Transfer-Encoding was still set to Base64 (obviously, I have to also change the encoding).
NOTE: Unbeknownst to me at the time, but this just gave me all the information I needed to easily use that data!
I did some more checking and I realized that there were actually 3 overloads to the
CreateAlternateViewFromString method (I guess I tend to go blind at 2 o’clock in the morning, even
though I have plenty of time to sleep and still get up at 5 AM). I searched really hard to find MSDN
examples that demonstrated them all, which took some real doing. By the way, all the examples I did
find on MSDN were for C# and C++, as if VB code was totally incapable of doing this (untrue!).
The third overload did not expect just a Mime parameter, but it expected a System.Text.Encoding class and
then the System.Net.Mime.MediaTypeNames string. I can already sense a move into the right direction...
Encoding specifies a series of conversion classes. Usually, we use these classes to convert one type of
text to another. But here, by specifying a method, like System.Text.Encoding.ASCII, we do not use the
method, but rather we supply that method to SMTP, which will in turn use it to convert our data.
Based on the kind of conversion we specify from the Encoding class, the Content-Transfer-Encoding
mechanism (which I had been having issues with) will be internally set to one of 3 values: quoted-
printable, where all control codes are marked with 7-bit Hex-Tags (“=” followed by a 2-character
hexadecimal value, such as “=0D” for a vbCr), base64, where binary or 8-bit code, or text containing
8-bit code will be compressed into a string of 7-bit text tokens, or 7bit, which is un-encoded 7-bit data.
Therefore, if we are passing ASCII data, such as Plain Text, HTML, or Rich Text, it will be encoded as
Quoted-Printable as long as we pass a text-based method for Encoding. If we leave this field blank, or
the data is not text, or it contains 8-bit text, it can be expected to be processed as Base64. And that is
why we have been getting Base64 as the Content-Transfer-Encoding type all this time on attachments.
NOTE: An AlternateView object is actually embedded within an email as an Attachment. The only difference between the
two is that an AlternateView will not specify a filename parameter in the Content-Type field. For example, “Content-Type:
text/plain; charset=us-ascii” versus “Content-Type: text/plain; name="Baby Names.txt" ”. Note further that
the specified default filename in an Attachment will not be wrapped in quotes if there are no spaces embedded within it.
MediaTypeNames specifies 3 subclass types of interest, which are most notably useful for regular
attachments: Application, Image, and Text. Each of these in turn has subtypes, which is why we see the
Context-Type field indicate “text/plain” or “application/octet-stream”. The first part is the Type, and the
second part, after the slash, is the Subtype. All this will, again, be discussed in the next section.
Presently, we are more interested in the Text type. But we must remember that this field is only text and
informational to the email application. It does nothing to the data; it simply specifies how the decoded
data should be processed by the email application. In fact, if we specify “text/plain”, but the data
contains 8-bit characters, we will find the encoding method will be forced to Base64 to ensure data
security, which is valid and legal because internet regulations require that our data cannot be altered in a
way that it is not 100% reversible.
 Page 396

NOTE: Rather than depend on using the System.Net.Mime.MediaTypeNames class to determine this Content-Type data, we
can simply supply our own string, as long as our recipient’s email software can use that information.
With the above gained knowledge, we will try transmitting our email again:
'create an alternate view of the RTF data from our RichTextBox Control
Dim rtfView As Mail.AlternateView = Mail.AlternateView.CreateAlternateViewFromString(Me.RichTextBox1.Rtf,
System.Text.Encoding.ASCII,
System.Net.Mime.MediaTypeNames.Text.RichText)
'send our email as plain text and with a rich text alternate view
SendEMail("mercedes_silver@80micro.com", "david.ross.goben@gmail.com", "Test", Me.RichTextBox1.Text, False, "smtp.80micro.com", rtfView)

And now, as we can see, our email result is creeping much closer to what I had been expecting to see:
From: mercedes_silver@80micro.com
To: david.ross.goben@gmail.com
Date: 22 Feb 2011 20:56:39 -0500
Subject: Test
Content-Type: multipart/alternative;
boundary=--boundary_0_12390f25-c489-470a-a36a-c3eb5dc04ad9
----boundary_0_12390f25-c489-470a-a36a-c3eb5dc04ad9
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: quoted-printable

This is a test of Rich Text data.=0A


----boundary_0_12390f25-c489-470a-a36a-c3eb5dc04ad9 ▬▬▬▬▬ End of message body, start of an attachment block.
Content-Type: text/richtext; charset=us-ascii ▬▬▬▬▬▬▬▬▬▬ This attachment is an alternate view, because a NAME parameter is not specified.
Content-Transfer-Encoding: quoted-printable ▬▬▬▬▬▬▬▬▬▬ Because data is encoded Quoted-Printable, you will see the (=) Hex-Tags
decorating the following RTF message block.
{\rtf1\ansi\ansicpg1252\deff0\deflang1033{\fonttbl{\f0\fnil\fcharset0 Times= ▬▬▬▬▬ Because the text is encoded Quoted-Printable, Hex-Tags were added
New Roman;}{\f1\fnil\fcharset0 Microsoft Sans Serif;}}=0D=0A{\colortbl= to control codes to ensure total original media content recovery.
;\red0\green0\blue0;}=0D=0A\viewkind4\uc1\pard\cf1\f0\fs24 This is a \b=
test \b0 of \i Rich Text data\i0 .\par=0D=0A\pard\cf0\f1\fs17\par=0D=0A}=
=0D=0A
----boundary_0_12390f25-c489-470a-a36a-c3eb5dc04ad9—

In the above Rich Text data, an equals sign “=” precedes each physical end of line, which must be
removed along with the vbCrLf following it. They are used to mark a ‘soft’ line return, dictated by its
requirements, not by the original text. This equals sign also precedes document special codes (having a
value less than 32) that are rendered to Hex-Tags by being converted to 7-bit Hex format (for example,
“=0D” represents Hex(&HD), or a Carriage Return, and “=0A” represents Hex(&HA), or a Linefeed).
This, and any other hex digits that are formatted as a Hex-Tag, will have to be translated back to their
original codes (the fact that the encoding reported quoted-printable should clue us into that). We may
also see a lot of “=20” tags, which represents a non-breaking space, as opposed to normal spaces
between words, which are inserted in places where it would otherwise be removed or ignored. Note also
that an actual ‘=’ in the text will be, ironically, encoded to “=3D”; the hex code for “=”.
NOTE: Though Quoted-Printable encoding will change conversion to Base64 if it finds byte codes greater than 127 (8-bit
data), we will later provide a method to encode those 8-bit codes to Hex-Tags, so we can send them Quoted-Printable, or
even as 7bit.
For example, if a string variable named Msg contains the above lines of RTF data, we could filter it to a
more perfect form using the following general “quick-conversion” code:
RichTextBox1.Rtf = Msg.Replace("=" & vbCrLf, Nothing).Replace("=0D", vbCr).Replace("=0A", vbLf).Replace("=20", " "c).Replace("=3D", "="c)

Hence, the above RTF data becomes perfectly formatted to its original form:
{\rtf1\ansi\ansicpg1252\deff0\deflang1033{\fonttbl{\f0\fnil\fcharset0 Times New Roman;}{\f1\fnil\fcharset0 Microsoft Sans Serif;}}
{\colortbl ;\red0\green0\blue0;}
\viewkind4\uc1\pard\cf1\f0\fs24 This is a \b test \b0 of \i Rich Text data\i0 .\par
\pard\cf0\f1\fs17\par
}

But one thing it does not do is take care of other possible hex codes, which we will inevitably encounter.
So we end up where we were, unless we choose to implement code to slog through the data that could
easily chew up a lot of resources while it processes. A rough example of an interim solution follows:
Msg = Msg.Replace("=" & vbCrLf, Nothing) 'clean up line termination tags.
For idx As Integer = &H1 To &HFF 'process the whole ASCII gambit (=00 never used).
Dim Hx As String = Hex(idx) 'convert to hex value.
If idx < 16 Then Hx = "0" & Hx ' 'leading zero if less than 16 (1-9, A-F).
Msg = Msg.Replace("=" & Hx, Chr(idx)) 'replace hex data with single character code.
Next
Me.RichTextBox1.Rtf = Msg 'stuff result to rich text box.
 Page 397

But wait a tick! If we recall from an earlier article, I mentioned something about the StringBuilder
object that was 200 times faster at string manipulation. This manipulation uses little resources, and what
resources they do use will get flushed away in a flash when we leave a method. We will explore such
support in our next section, where we will cover E-Z decoding of Quoted-Printable and Base64 data.
But let us stop what we are doing and simply think about ASCII-type encoding for a moment.
ASCII refers to 7-bit code (128 character codes), even though now we often think of it in terms of 8-bits
(256 codes). But that is all because of the immense presence of the Microsoft Windows environment. On
the internet, though, where server standards must conform to the least-common-denominator, Unix is
King, and many (ancient) versions of Unix use only standard ASCII. If we look under .NET, the ASCII
encoding class actually does support 7-bit ASCII, so some of us may be wondering why our control
codes are being translated to Hex-Tags when they are supposed to be a part of standard ASCII?
...Because the text is being encoded as Quoted-Printable, and this specification always renders all of its
non-displayable ASCII tokens as displayable Hex-Tags, so not one character of that data is lost.
NOTE: One of the cardinal rules of email processing is that not one byte of a transmitted message can ever be allowed to be
lost or altered in such a way that they cannot be fully restored to their original format. Hence, even if we send a plain text
message, but the “plain text” contains some 8-bit codes, such as Chr(149), “·”, the text will be forced to being encoded to
Base64, so that the data can be transmitted over the internet as 7-bit ASCII data and can afterward be fully restored.
So what I really need is un-encoded 7-bit US-ASCII support. Hmm. I first thought about UTF-7, which
is a true 7-bit ACII, but then I realized that the same result would follow, due to Quoted-Printable
encoding. However, according to my references, the un-encoded 7bit option is exactly what I need.
But how do I get there? It seemed that I was getting no closer to attaining my actual goal of trying to
send the rich text alternate view as unencoded rich text data. It was like getting directions from someone
in the beautiful State of Maine: “You can’t get there from here; you have to go somewhere else first.”
I would have to think outside the box I had worked myself into. That was when I considered that an
AlternateView and an Attachment, like everything else in .NET, is an object, and objects seem to always
have a lot of properties. So I took a look at Attachment and AlternateView properties. Eureka! I instantly
discovered two members that were perfect to my purpose: The first was ContextType, and the other was
TransferEncoding (the little hairs on the back of my neck are starting to rise). And even better, they
were both malleable properties (that is, they are properties that we can both read from and write to). I
did not expect that they would be so easy to find, considering the countless programmers who have been
complaining about this on the internet. But the important thing was that I now had exactly what I need.
After a little exploring, I figured out that all I had to do was set the MediaType string property of
ContextType. I could use System.Net.Mime.MediaTypeNames again to set it. TransferEncoding was an
integer enumeration, offering me a choice of QuotedPrintable (0), Base64 (1), or SevenBit (2). A fourth
option, Unknown, spits only venomous messages of non-support into our eyes if we try to play with it.
But the good news is – I can use the SevenBit option! So I try again, this time using the following code:
' create an alternate view of the RTF data from our RichTextBox Control.
Dim rtfView As Mail.AlternateView = Mail.AlternateView.CreateAlternateViewFromString(Me.RichTextBox1.Rtf)
rtfView.ContentType.MediaType = Mime.MediaTypeNames.Text.RichText 'format our new view as rich text...
rtfView.TransferEncoding = Mime.TransferEncoding.SevenBit 'and send unencoded as 7-bit ASCII.
' send our email as plain text and with a rich text alternate view.
SendEMail("mercedes_silver@80micro.com", "david.ross.goben@gmail.com", "Test", Me.RichTextBox1.Text, False, "smtp.80micro.com", rtfView)

The results were spectacular. We may also want to apply SevenBit encoding to HTML and Plain Text
data, as long as they do not contain any embedded 8-bit coding. Consider my final result:
From: mercedes_silver@80micro.com
To: david.ross.goben@gmail.com
Date: 25 Feb 2011 14:22:46 -0500
Subject: Test
Content-Type: multipart/alternative;
boundary=--boundary_0_f3de9980-d92b-423b-84a7-cd77f3a35def

----boundary_0_f3de9980-d92b-423b-84a7-cd77f3a35def
 Page 398

Content-Type: text/plain; charset=us-ascii


Content-Transfer-Encoding: quoted-printable

This is a test of Rich Text data.=0A ▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Note the vbLf terminator here in the plain text
----boundary_0_f3de9980-d92b-423b-84a7-cd77f3a35def
Content-Type: text/richtext; charset=utf-8 ▬▬▬▬▬▬▬▬ Note text/richtext
Content-Transfer-Encoding: 7bit ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Note 7bit encoding (or rather, 7bit indicating NO ENCODING)

{\rtf1\ansi\ansicpg1252\deff0\deflang1033{\fonttbl{\f0\fnil\fcharset0 Times New Roman;}{\f1\fnil\fcharset0 Microsoft Sans Serif;}}


{\colortbl ;\red0\green0\blue0;}
\viewkind4\uc1\pard\cf1\f0\fs24 This is a \b test \b0 of \i Rich Text data\i0 .\par
\cf0\f1\fs17\par
}
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Blank line is due to an additional embedded vbCrLf code in the Rich Text data
----boundary_0_f3de9980-d92b-423b-84a7-cd77f3a35def—

Were we to plug the above raw Rich Text data without modification into the Rtf
property of a RichTextBox, we would see the result shown on the right.
All things considered, after all this work, though it is very helpful to have alternate views that are
readable, it is probably best to let the system take care of the encoding details, making less work for us.
But even so, it was most fascinating learning a great deal about formatting email data in different ways.

Typical Email Server Specifications:


Comcast SMTP/POP3 Info: COX SMTP/POP3 Info:
POP server: mail.comcast.net (Port 995) POP server: pop.cox.net (Port 995)
POP SSL required? Yes (If No, use POP3 Port 110 and pop3.comcast.net server) POP SSL/TLS required? Yes
User name: Your email name. For example yourname from yourname@comcast.net User name: Your email name. For example yourname from yourname@cox.net
Password: The password you usually use to sign in to Comcast Password: The password you usually use to sign in to Cox
SMTP server: smtp.comcast.net (Port 465 for SSL, or 587 for TLS) SMTP server: smtp.juno.net (Port 465 for SSL, or 587 for TLS)
Authentication required? Yes (this matches your POP username and password) Authentication required? Yes (this matches your POP username and password)
SMTP TLS/SSL required? Yes (If No, use SMTP Port 25) SMTP TLS/SSL required? Yes

GMAIL SMTP/POP3 Info: NOTE: Some Cox servers use pop.west.cox.net, plus “east” and “central”. Refer to the
POP server: pop.gmail.com (Port 995) link in the note, below.
POP SSL required? Yes
User name: Your Gmail address. For example yourname@gmail.com Juno SMTP/POP3 Info:
Password: The password you usually use to sign in to Google and Gmail POP server: pop.juno.com (Port 995)
SMTP server: smtp.gmail.com (Port 465 for SSL, or 587 for TLS) POP SSL required? Yes
Authentication required? Yes (this matches your POP username and password) User name: Your email name. For example yourname from yourname@Juno.com
SMTP TLS/SSL required? Yes Password: The password you usually use to sign in to Juno
SMTP server: authsmtp.juno.com (Port 465)
HOTMAIL SMTP/POP3 Info: Authentication required? Yes (this matches your POP username and password)
POP server: pop3.live.com (Port 995) SMTP TLS/SSL required? Yes
POP SSL required? Yes
User name: Your Hotmail name. For example yourname from yourname@hotmail.com Most other servers SMTP/POP3 Info (Verizon, NetZero, etc.):
Password: The password you usually use to sign in to Hotmail POP server: pop.xxx.com (Port 110 (default))
SMTP server: smtp.live.com (Port 587) POP SSL required? No
Authentication required? Yes (this matches your POP username and password) User name: Your email name. For example yourname from yourname@xxx.com
SMTP TLS/SSL required? Yes Password: The password you usually use to sign in to your server
SMTP server: smtp.xxx.com (Port 25 (default))
Yahoo! SMTP/POP3 Info (paid service): Authentication required? Yes (this matches your POP username and password)
POP server: pop.mail.yahoo.com (Port 995) SMTP TLS/SSL required? No
POP SSL/TLS required? Yes
User name: Your email name. For example yourname from yourname@yahoo.net NOTE: The above settings will also work with Comcast. The above is the typical
Password: The password you usually use to sign in to Yahoo settings for “Plain” (unsecured) email. SSL/TLS email, on the other hand is encrypted
SMTP server: smtp.mail.yahoo.com (Port 465 for SSL, or 587 for TLS) for security.
Authentication required? Yes (this matches your POP username and password)
SMTP TLS/SSL required? Yes NOTE: Look to www.defcon-5.com/support/index.cfm?docid=95 for others. Such as
ATT, East, Central, and West COX servers, AOL, EarthLink, etc.

NOTE: Always check and verify port numbers, SSL settings, and provider addresses with our provider, and to verify our
selected, or how to select a User Name, and how to set up a Password (veteran email users can often do this on their own
without pause). Use a web search for something like “pop3 xxx settings”, though using our provider in place of xxx
(obviously, we would have to do that on a computer that already has active internet access).
NOTE: If we change our email settings, it may be a good idea to also re-logon to our computer, to ensure the new settings
will work with the .NET Mail interface, because sometimes these things are set up only during the Logon process.
NOTE: TLS is usually an option available for smart phones, mobile PCs, etc., though using TLS ports with SSL usually
works, and many new servers will even insist on using TLS ports for even desktop computer subscribers.
.
 Page 399

PART TWO.
Encoding and Decoding Email Data.
When we send and receive email, the parameters noted for the Content-Type and Content-Transfer-
Encoding fields are of paramount importance! If the wrong information is provided, the user’s email
application can get confused. When there is confusion, it is required to default the Content Type of
“application/octet-stream” (a binary stream) and “Base64” for Content-Transfer-Encoding for safety.
The goal is never to lose original content. For example, if we specify a Quoted-Printable-class encoder,
but the data is actually 8-bit, the transmitter will supersede our option and specify Base64 encoding.
But where does all this come from? Where and how can we tell VB.NET how to transmit these
instructions? What if I can barely understand “application/octet-stream,” let alone spell it consistently?
And what in blue blazes is Base64 encoding, anyway?
NOTE: Blue Blazes refers to very hot Hell fire, coming from the searing blue flame given off by petroleum gasses.
The .NET MIME processor has two built-in members that will help us to easily resolve the Content-
Type and Content-Transfer-Encoding issues. The first helper is the System.Net.Mime.TrasferEncoding
enumeration, and the second helper is the System.Net.Mime.ContentType class.
Based on the kind of conversion we specify for encoding, the Content-Transfer-Encoding mechanism
will reflect one of three values. A fourth, Unknown, is presently defined, but should never be used:
Member: Description
QuotedPrintable (0) Encodes data that consists of printable characters in the US-ASCII character set. See RFC 2045 Section 6.7. All Text encoders default to this (US-ASCII,
UTF7, UTF8, etc.). Maximum line length is 75 characters. ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬► Stored in email as “quoted-printable”.
Base64 (1) Encodes stream-based data. See RFC 2045 Section 6.8. Best for Binary, or any code you do not want to deal with control-code conversion Hex-Tags.
Maximum line length is 75 characters. ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬► Stored in email as “base64”.
SevenBit (2) Used for data that is not encoded. The data is in 7-bit US-ASCII characters with a total line length of no longer than 1000 characters. See RFC 2045
Section 2.7. ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬► Stored in email as “7bit”.
Unknown (-1) This is currently not supported and will never be selected by the SMTP server.

NOTE: RFC 2045 can be found at www.ietf.org/rfc/rfc2045.txt. Also, the MSDN documentation actually erroneously references
RFC 2406, which in fact documents the IP Encapsulating Security Payload (ESP). RFC 2045 refers correctly to
Multipurpose Internet Mail Extensions, (MIME) Part One: Format of Internet Message Bodies.
Therefore, if we are passing ASCII data, such as Plain Text, HTML, or Rich Text, it will be encoded as
QuotedPrintable as long as we specify a text-based method from Encoding. If we leave this field blank,
or the data is not 7-bit text, it can be expected to be processed as Base64, and that is why we tend to get
Base64 as the Content-Transfer-Encoding type a lot on Attachments and Alternate Views.
For specifying the data for the Content-Type field, we look to System.Net.Mime.MediaTypeNames.
MediaTypeNames specifies 3 types of interest, which are most notably useful for regular attachments:
Member: Description
Application Specifies the kind of application data in an email message attachment or AlternateView.
Image Specifies the type of image data in an email message attachment or AlternateView.
Text Specifies the type of text data in an email message attachment or AlternateView.

The MediaTypeNames.Application type exposes the following default members:


Member: Context-Type Field Reports: Description
Octet application/octet-stream Specifies that the Application data is not interpreted (an Octet-stream (Byte-stream))..
Pdf application/pdf Specifies that the Application data is in Portable Document Format (PDF)..
Rtf application/rtf Specifies that the Application data is in Rich Text Format (RTF)..
Soap application/soap+xml Specifies that the Application data is a Simple Object Access Protocol document (SOAP)..
Zip application/zip Specifies that the Application data is compressed.

If we want to specify other sub-types, we can, but it is likely another email processor except our own
will support it. The .NET MediaTypeNames class strictly follows the RFC 2045 specification.
The MediaTypeNames.Image type exposes the following default members:
Member: Context-Type Field Reports: Description
Gif image/gif Specifies that the Image data is in Graphics Interchange Format (GIF).
Jpeg image/jpeg Specifies that the Image data is in Joint Photographic Experts Group (JPEG) format.
Tiff image/tiff Specifies that the Image data is in Tagged Image File Format (TIFF).

NOTE: I hope they add Png to the specification; it is more efficient and has much less data loss than Jpeg.
 Page 400

The MediaTypeNames.Text type exposes the following members:


Member: Context-Type Field Reports: Description
Html text/html Specifies that the Text data is in HTML format.
Plain text/plain Specifies that the Text data is in Plain Text format.
RichText text/richtext Specifies that the Text data is in Rich Text Format (RTF).
Xml text/xml Specifies that the Text data is in XML format.

Allowing Users to Specify Content-Type and Content-Transfer-Encoding Options


Of course, the options we offer our email application users can differ, or can even be similar to the
previously mentioned tables. However, we will need to have a mechanism in place for either interpreting
their Attachment/Alternate View choices, or for allowing them to explicitly specify them. The easier we
make it for them, the more trust and loyalty they will have for our products. Just do not make it so
brain-dead-simple that we begin to restrict them in their options.
If we refer to the SendEmail method comments, we will notice that the specification for an Alternate View
showed we how to stipulate a different Content-Type and Content-Transfer-Encoding type from the
default by using VB programming methods. Sure, this is all well and good when we are programming
that code, but it is not very convenient for our users if they wish to specify these things, nor is adding
parameterized declarations for an attachment. For example, consider having to hand-type “C:\My
Files\API32.txt (text/plain, SevenBit)”, and even more so for a binary file, like “ C:\telnet.exe
(application/octet-stream, Base64)”. Ack!

It would of course be more convenient to allow them to select the files from a browser and for us to
either interpret their attachment and determine how best to transmit them (this is not as difficult as we
might think), or allow them, through an optional advanced user option, to specify both parts themselves
from dropdown ComboBox controls, and then we would simply wrap them up in a manner recognizable
by the SendEmail method, though I would imagine that we would customize the code to our needs, such
as adding trendy features like specifying email recipients by name, rather than just their full email
addresses. This makes selecting recipients more convenient, such as using a current naming format that
is popular and is already supported by the .NET Mail object, like the following: "David Ross Goben"
<david.ross.goben@gmail.com>.

Consider the following structure and code to simplify defining Content-Type text:
'*******************************************************************************
' Enum MediaTypes: Enumeration used by GetMediaType
'*******************************************************************************
Public Enum MediaTypes As Integer
ApplicationOctet ' 0 = Integer Value
ApplicationPdf ' 1
ApplicationRtf ' 2
ApplicationSoap ' 3
ApplicationZip ' 4
ImageGif ' 5
ImageJpeg ' 6
ImageTiff ' 7
TextHtml ' 8
TextPlain ' 9
TextRich '10
TextXml '11
End Enum
'*******************************************************************************
' Function Name : GetMediaType
' Purpose : Provide easy access to System.Net.Mime.MediaTypes text.
' :
' Returns : provided a MediaTypes enumeration value, a string representing
' : the selected type will be returned.
'*******************************************************************************
Public Function GetMediaType(ByVal MediaType As MediaTypes) As String
Select Case MediaType
Case MediaTypes.ApplicationPdf
Return "application/pdf"
Case MediaTypes.ApplicationRtf
Return "application/rtf"
Case MediaTypes.ApplicationSoap
Return "application/soap+xml"
Case MediaTypes.ApplicationZip
 Page 401

Return "application/zip"
Case MediaTypes.ImageGif
Return "image/gif"
Case MediaTypes.ImageJpeg
Return "image/jpeg"
Case MediaTypes.ImageTiff
Return "image/tiff"
Case MediaTypes.TextHtml
Return "text/html"
Case MediaTypes.TextPlain
Return "text/plain"
Case MediaTypes.TextRich
Return "text/richtext"
Case MediaTypes.TextXml
Return "text/xml"
Case Else
Return "application/octet-stream"
End Select
End Function

For user options, we can present them however we wish (or they wish), such as the text displayed within
the enumeration in a ListBox or ComboBox control, presented to them in prettier or simpler text, such as
“Application/Octet” or even “Binary”, or by first separating the three types, Application, Image, and
Text, and then present secondary subtype lists for each. The methods for how to do this are many.
Simply ensure that the final values match those listed in the comments for their integer values. We can
then supply this integer value (or first cast it, using something like “DirectCast(Value, MediaTypes)”) to
the GetMediaType function, which will return the appropriate, email-friendly Content-Type text.
For simplifying Content-Transfer-Encoding, consider the following small structure and method:
'*******************************************************************************
' Enum TransferEncodings: Enumeration used by GetTransferEncoding
'*******************************************************************************
Public Enum TransferEncodings As Integer
QuotedPrintable ' 0 = Integer value
Base64 ' 1
SevenBit ' 2
End Enum

'*******************************************************************************
' Function Name : GetTransferEncoding
' Purpose : Provide easy access to System.Net.Mime.TransferEncoding data
' :
' Returns : Provided a TransferEncodings value, a TransferEncoding value
' : is returned.
'*******************************************************************************
Public Function GetTransferEncoding(ByVal TransferEncoding As TransferEncodings) As System.Net.Mime.TransferEncoding
Return DirectCast(TransferEncoding, System.Net.Mime.TransferEncoding)
End Function

Using techniques similar to those already outlined for GetMediaType, we can do the same thing for our users,
allowing them in optional advanced options to specify how they want their data to be encoded.
Ultimately, we are the one who must specifically inform the SMTP server of the type and encoding.
Since an Alternate View object’s MediaType parameter (MyAlternateView.ContentType.MediaType) expects
a string, setting it is easy – we just supply it with the string we got from the GetMediaType function. Also,
because the Alternate View’s Transfer Encoding ( MyAlternateView.TrasferEncoding) expects an integer
cast to System.Net.Mime.TrasferEncoding, we can simply provide it with the value returned by the
GetTransferEncoding method, or, just use the DirectCast method to cast their ComboBox selection index
(0-2) to System.Net.Mime.TransferEncoding.

Determining if Text can be Sent Encoded As Quoted-Printable, Base64, or 7Bit.


When we are preparing to specify coding, or the lack thereof for text, we need to ensure that if we are
going to submit it as using as Quoted-Printable or 7Bit, that it will not be forced to Base64, if it ends up
containing any 8-bit data, unless that is what we want to happen. If not, we can first scan it with the
following little function, which will quickly determine if the code contains any 8-bit data:
 Page 402

'*******************************************************************************
' Function Name : TextNeedsEncoding
' Purpose : Determine if HTML text, Rich Text, or Plain Text requires
' : 8-bit code translation to 7-bit Quoted-Printable tags.
' :
' Returns : Provided a source string, it returns a boolen flag.
' : If the returned value is true, the source contains 8-bit data
' : and will be encoded by server.
' :
' NOTES : If text data contains 8-bit values, the default .NET
' : SMTP processor will force this code to be encoded to Base64,
' : even if only a single byte is 8-bit.
' :
' : To avoid this, the Force7BitHtml() method can be invoked on
' : HTML text to ensure that it is 7-bit encoded so that it can
' : be processed as Quoted-Printable or as 7Bit. The ForceQuotedPrintable()
' : method performs essential conversions for non-HTML text, but this
' : would be best served in Attachments and Alternate Views.
'*******************************************************************************
Public Function TextNeedsEncoding(ByVal Message As String) As Boolean
Dim Byt() As Byte = Encoding.UTF8.GetBytes(Message) 'convert message to byte array
For Each B As Byte In Byt
If CBool(B And &H80) Then Return True
Next
Return False
End Function

If the TextNeedsEncoding method returns True and we are processing HTML text, we can pass the HTML
text through the Force7BitHtml function, listed below, which will ensure, upon return, that all code in the
HTML text is 7-bit, and that any 8-bit or 16-bit code that was in it is dutifully converted to 7-bit HTML
Entity Numbers. If we are passing the function Rich Text or Plaint Text, we can specifically Hex-Tag its
8-bit characters (something that the default Quoted-Printable encoders will not do, even though it is
permitted) using the ForceQuotedPrintable function, listed further below.

Converting 8-Bit HTML Data to 7-Bit for Sending without Loss of Integrity.
Consider the following simple HTML upgrade function that will transparently convert any HTML text that
contains 8-bit or 16-bit data to fully compatible 7-bit HTML text:
'*******************************************************************************
' Function Name : Force7BitHtml
' Purpose : Method to convert 8-bit code in an HTML message to 7-bit.
' :
' Returns : Provided a string containing HTML code, it will return a string
' : containing HTML code that does not have any 8-bit data embedded.
' :
' NOTES : If any characters in an HTML text string are 8-bit (values
' : greater than 127), then they are converted into a special
' : 7-bit HTML Entity Number, For example, code 149 (•) is an 8-bit
' : value that can be changed to HTML "&#149;", which will ensure
' : that it will still be displayed on the HTML page, but the HTML
' : souece code will no longer carry an actual 8-bit value. If such
' : code had not been corrected, the encoding of the data would be
' : forced to change from quoted-printable to Base64, because that
' : would be the only way the email processor could guarantee that
' : the email text was fully intact.
'*******************************************************************************
Public Function Force7BitHtml(ByVal HtmlSource As String) As String
Dim Sb As New StringBuilder 'set up string builder for appending data
For Idx As Integer = 1 To Len(HtmlSource)
Dim C As Integer = AscW(Mid(HtmlSource, Idx, 1)) 'get a single character from the source
Select Case C 'check each character
Case Is > &H7F, Is < 0 'if 8-bit or unicode code
Sb.Append("&#" & C.ToString & ";") 'convert to 7-bit HTML ecoder
Case Else
Sb.Append(ChrW(C)) 'else save text regardless
End Select
Next
Return Sb.ToString
End Function

Converting 8-Bit Text Data to 7-Bit for S ending without Data Loss.
If we test a text message with the TextNeedsEncoding function and it comes back True, we can either
encode it using Base64, or, if we want it to remain readable in raw format, we can convert it to a Hex-
Tagged 7-bit text format by passing it through the ForceQuotedPrintable function, listed here:
 Page 403

'*******************************************************************************
' Function Name : ForceQuotedPrintable
' Purpose : Force 8-bit code in a text message to 7-bit, without data loss.
' :
' Returns : Provided a source string that contains 8-bit data, the 8-bit
' : data is converted to Hex-Tags, and the returned string is 7-bit.
' :
' NOTES : if any characters in a text string are 8-bit (values greater
' : than 127), then they are converted into special 7-bit tags.
' : For example, code 149 (•) is an 8-bit value that can be changed
' : to hex "=95", which will ensure that it will still be displayed
' : in the text, but the text data will no longer carry actual 8bit
' : that would force DATA to change from quoted-printable or 7bit
' : to Base64, because that would be the only way the email processor
' : Base64, because that would be the only way the email processor
' : could guarantee that the email text was fully intact. However,
' : you will have to use the DecodeQuotedPrintable() method to convert
' : it back to its original text form.
' :
' : The Encoded text will begin with "=00". Because unencoded null codes
' : are not permitted in email data, you can use this to instantly
' : determine on the receiving end that this code will need to be
' : processed by DecodeQuotedPrintable() a second time (if initially
' : encoded as Quoted-Printable). A second pass would be required,
' : because if this translated code was afterward encoded as Quoted-
' : Printable, and all the "=xx" byte-translations, would be
' : reinterpreted as "=3Dxx", which DecodeQuotedPrintable() would
' : convert back to "=xx", so passing through a second time would
' : properly convert the additional encoding. Further, by checking the
' : text startiing with "=00", you would know that you would need to
' : double-decode the text. Also, you would want to initially skip this
' : initial tag when passing it the second time to DecodeQuotedPrintable():
' :
' : Dim Result As String = DecodeQuotedPrintable(Message) 'initially decode Quoted-Printable text
' : If Result.SubString(0, 3) "=00" Then 'tagged as pre-encoded?
' : Return DecodeQuotedPrintable(Result.SubString(3)) 'yes, so decode again and return, less initial null byte
' : Else
' : Return Result 'otherwise, return result of decoding
' : End If
'*******************************************************************************
Public Function ForceQuotedPrintable(ByVal Message As String) As String
Dim Byt() As Byte = Encoding.UTF8.GetBytes(Message) 'convert message to byte array
Dim Sb As New StringBuilder("=00") 'set up string builder for appending data. Init with "=00"
For Each B As Byte In Byt
Select Case B 'check each byte
Case Is > &H7F 'if 8-bit code
Sb.Append("=" & Hex(B)) 'convert to 7-bit tag
Case Else
Sb.Append(Chr(B)) 'else save text regardless
End Select
Next
Return Sb.ToString
End Function

The ForceQuotedPrintable method uses the same encoding tags for 8-bit code that are used by Quoted-
Printable encoding for control codes. However, if we also pass it as Quoted-Printable, this means that
we will have to decode it twice; the first time to decode the “=” tags added by the internal SMTP
processor, and a second time to decode the tags that we had added using the above method prior to
SMTP encoding the data as Quoted-Printable. This is discussed in more detail in the next topic.
We may notice that the ForceQuotedPrintable method leads the result text with “=00”. We can simply
check for this in our email processor, and if found, as shown in the above comments, we can decode it a
second time. This can be done because the above encoder may add Hex-Tags for 8-bit values, but the
internal SMTP processor will not know that these are Hex-Tags, because it would not expect them, so it
would encode “=95” to “=3D95”, where the “=3D” is the equals sign encoded (this symbol has a hex
ASCII value of &H3D). This will decode to “=95”, and then the “=95” will in turn have to be decoded
in a second pass (&H95 is 149 decimal, and represents the “•” symbol).
Of course, we can also ‘encode’ this data in an attachment or alternate text as 7Bit, but we should still
check for the leading “=00” tag, to see if we will still need to decode it back to 8-bit text.
NOTE: Remember that other email processors will most-likely not have this kind of arcane knowledge for functionality, and
so their users will still see any Hex-Tag encoding that the ForceQuotedPrintable method had placed in there.
 Page 404

Decoding Quoted-Printable Text.


Quoted-Printable text has a line length limit set at about 75 characters, including the line terminator. I
see it more as a “when we find out that we are at 72-75 characters, then do a forced soft line wrap”. All
text encoded using the Quoted-Printable method will have Hex-Tag encoding for any control codes,
except maybe extremely simple 1-line notes. Regardless, run all Quoted-Printable encoded data through
the following DecodeQuotedPrintable method to render them into their original format.
'*******************************************************************************
' Function Name : DecodeQuotedPrintable
' Purpose : Method to clean typical control translations, or all of them.
' : This should be invoked for all data coded Quoted-Printable.
' :
' Returns : Provided a raw message string block, it returns a decoded string.
' :
' NOTES : Typical cleaning involves changing "=0D" to vbCr, "=0A" to vbLf,
' : "=20" to a space, and "=3D" to "=", plus any line wrap
' : terminators at the end of lines to vbNullstring.
' :
' : A StringBuilder object will be used, which will very quickly
' : do a replacement of all control code translations using fewer
' : resources, and what resources that are used will be instantly
' : flushed when the method exits.
'*******************************************************************************
Public Function DecodeQuotedPrintable(ByVal Message As String, Optional ByVal QuickClean As Boolean = False) As String
'set up StringBuilder object with data stripped of any line continuation tags
Dim Msg As New StringBuilder(Message.Replace("=" & vbCrLf, vbNullString))

If QuickClean Then 'perform a quick clean (clean up common basics)


Return Msg.Replace("=" & vbCrLf, vbNullString).Replace("=0D", vbCr).Replace("=0A",
vbLf).Replace("=20", " ").Replace("=3D", "=").ToString
Else 'perform total cleaning
'store 2-character hex values that require a leading "0"
Dim HxData As String = "X0102030405060708090A0B0C0D0E0F"
For Idx As Integer = 1 To &HF 'initially process codes 1-15, which require a leading 0
Msg.Replace("=" & Mid(HxData, Idx << 1, 2), Chr(Idx)) 'replace hex data with character code (SHIFT is faster)
Next
For idx As Integer = &H10 To &HFF 'process the whole 8-bit extended ASCII gambit
Msg.Replace("=" & Hex(idx), Chr(idx)) 'replace hex data with single character code
Next
Return Msg.ToString 'return result string
End If
End Function

The above function will execute much faster than the conversion method demonstrated a much earlier,
plus it can perform an optional “quick clean” for minor jobs, or a detailed cleaning in just about an
instant, but I wish a StringBuilder could do blind replacements (MID-like operations) so I could make it
even faster, plus a Find method to emulate Instr-type functionality would also be nice.
Basically, what this method does is convert the Hex-Tags with their 2-character hexadecimal digits (0-9,
A-F) back to their original 8-bit character codes (> &H7F), or their original control codes (< &H20). A
special case was spaces “=20”, which, if at the end of a line, we often lost, so they were encoded. On top
of that, because there was an imposed 75-character line limit, soft line-wraps were added by placing a
‘=’ at the end of a forced returned line. The DecodeQuotedPrintable method reverses that, providing us
with pristine text data that was formatted exactly as it had been provided to the server.

Translating Base64 Data Back to Its Original Format.


For all the trouble I had earlier with Base64, converting it to Text or a binary format is not difficult at all.
Base64, for those of us who have been waiting with bated breath for a definition, is really simple
enough, and is laid out much like a Lucky Charms™ secret decoder ring, but with some Umph! (And
why does their breath smell like fish bait? Or was that restrained, or anticipating breath?)
Base64 represents binary (or 8-bit) data in an ASCII string by translating the data into a radix-64 format.
Various forms of this are commonly used in applications when there is a need to encode binary data that
must be stored and transferred over media that are designed to deal with strictly 7-bit text data. This is to
guarantee that the data remains fully intact without modification during transport, and can be decoded to
 Page 405

100% of its orginal form. This type of encoding was needed to initially support dialup communication
between systems running different operating systems. For example, Base64-type ecoding for an
uppercase-only, symbol-rich character set was implemented as uuencode by UNIX, and BinHex by the
TRS-80 (this TRS-80 format was later adapted for the Macintosh). Because of their compatability, a
messaging application could make better assumptions about what characters were safe to use.
Base64, as used by MIME, has a 64-character set
Char Index Char Index Char Index Char Index
defined in the table shown to the right, where a plain A 00 Q 16 g 32 w 48
ASCII character is used to represent a numeric index: B 01 R 17 h 33 x 49
C 02 S 18 i 34 y 50
Each byte of text is composed of 8 bits. Were we to look at D 03 T 19 j 35 z 51
any bytes in radix-2 (Base2, or Binary); it would look like E 04 U 20 k 36 0 52
a string of 8 ones and/or zeros. F 05 V 21 l 37 1 53
G 06 W 22 m 38 2 54
For example, each letter of my name, David, is represented H 07 X 23 n 39 3 55
by the decimal ASCII codes 68, 97, 118, 105, and 100. In I 08 Y 24 o 40 4 56
hexadecimal, which can be computed using the Hex J 09 Z 25 p 41 5 57
function, it would be &H44, &H61, &H76, &H69, and K 10 a 26 q 42 6 58
L 11 b 27 r 43 7 59
&H64. These are represented in binary streams 01000100,
M 12 c 28 s 44 8 60
01100001, 01110110, 01101001, and 01100100. Together, N 13 d 29 t 45 9 61
we get 0100010001100001011101100110100101100100. O 14 e 30 u 46 + 62
Rather confusing, I know, but strangely, it is actually quite P 15 f 31 v 47 / 63
easy to translate in our head... once we know hexadecimal.
NOTE: Each letter of a hex value – 0 to F (15 decimal) –
represents 0000, 0001, 0010, 0011, 0100, 0101, 0110, 0111, 1000,
1001, 1010, 1011, 1100, 1101, 1110, and 1111 in Binary, where the position values from left to right are 8, 4, 2, and 1. Thus
1000 = 8, 0100 = 4, 0010 = 2, and 0001 = 1.
If we look at this stream as a table that is sectioned off at 8 bits, it is less confusing, and less at 4 binary
bits. But we are more interested in breaking this up into Base64, which uses only 6 bits (values 0-63, or
00-3F hex, or 00|0000 to 11|1111 binary). So examine this long stream of bits in that light:

We can compare the bit patterNs to the upper, 8-bit blocks for Base256, and the lower, 6-bit blocks for
Base64. The Hex index takes 6 consecutive bits and converts them to Base16, or hexadecimal. That is, the
first 2 bits comprise the first hex value (0-3), and the remaining 4 bits comprise the second hex value (0-F).
So if we take the first 6 bits of the 8-bit value for the letter “D” (0100|0100), which is 01|0001, we get 11
Hex, or 1 x 16 + 1, or 17. The value 17 corresponds to the ASCII letter “R” in the previous Base64 table.
Explaining this makes it seem complicated, but doing it is easy (even easier if we let the system do it!)
We might notice other things. First, we padded the 8-bit data out 2 extra bits, so that we could
accommodate the 6-bits required for the last Base64 value. Second, we may notice that 3 8-bit values fit
nicely into 4 6-bit values (24-bits total). Because of this, what Base64 conversion does is process the
original 8-bit values in groups of 3 bytes at a time, which yields 4 Base64 values for each 3-byte group.
However, it is clear that the original data cannot, and should not be always divisible by 3, so the
conversion routine implements padding to fill it out. It pads the data by adding 1 to 3 ‘ =’ symbols at the
end of the encoding run (this is why we will frequently see them trailing Base64-encoded data).
 Page 406

What these pad characters are used for is to pad out missing codes from the final 4-character Base64
grouping. They are treated as 0 values and tossed away, but the decoder, when grabbing values, will first
check if it is the end of the data, and if not, it will simply slog in the next set of 4 characters from the
feeder stream. So throwing in some padding is like throwing some old clodhoppers into the bottom of
the last bushel of pears – it makes it look as full as the others. I actually did this on my grandparent’s
farm, and the accentuated and prolonged stinging on my hindquarters informed me that this was an
inappropriate course of action, warranting further personal review within the acetic seclusion of the TV-
less, toy-less, book-less confines of Grandma’s sewing room, unless I wanted to flip on the radio and
catch up on pork-belly futures – but the good news was, I did figure out how to sew.
NOTE: Clodhoppers are sturdy ankle boots farmers use to protect their feet and ankles while walking through tilled fields.
Suppose we took the Base64-encoded data that was in one of my earlier, and at the time presumed
failed, AlternateView attempts for my Rich Text data:
From: mercedes_silver@80micro.com
To: david.ross.goben@gmail.com
Date: 22 Feb 2011 17:19:08 -0500
Subject: Test
Content-Type: multipart/alternative;
boundary=--boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321

----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321
Content-Type: text/plain; charset=us-ascii
Content-Transfer-Encoding: quoted-printable

This is a test of Rich Text data.=0A


----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This blank line after the Content-Transfer-Encoding line is required, and is not part of the message
e1xydGYxXGFuc2lcYW5zaWNwZzEyNTJcZGVmZjBcZGVmbGFuZzEwMzN7XGZvbnR0Ymx7XGYw
XGZuaWxcZmNoYXJzZXQwIFRpbWVzIE5ldyBSb21hbjt9e1xmMVxmbmlsXGZjaGFyc2V0MCBN
aWNyb3NvZnQgU2FucyBTZXJpZjt9fQ0Ke1xjb2xvcnRibCA7XHJlZDBcZ3JlZW4wXGJsdWUw
O30NClx2aWV3a2luZDRcdWMxXHBhcmRcY2YxXGYwXGZzMjQgVGhpcyBpcyBhIFxiIHRlc3Qg
XGIwIG9mIFxpIFJpY2ggVGV4dCBkYXRhXGkwIC5ccGFyDQpccGFyZFxjZjBcZjFcZnMxN1xw
YXINCn0NCg==
----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321--

If this data (highlighted in the above block) was stored in a string variable named b64Data, we could
then stuff it into our RichTextBox1 control using the following single line of code:
Me.RichTextBox1.Rtf = Encoding.UTF8.GetChars(System.Convert.FromBase64String(b64Data.Replace(vbCrLf, vbNullString)))

And that is it! But I think we might like to also look at what I did a little less cryptically:
b64Data = b64Data.Replace(vbCrLf, vbNullString) 'remove artificial email line terminators from b64Data string
Dim Byt() As Byte = System.Convert.FromBase64String(b64Data) 'convert b64Data string from Base64 to a byte array of ASCII values
Me.RichTextBox1.Rtf = Encoding.UTF8.GetChars(Byt) 'convert 8-bit ASCII values in Byte array to a 16-bit Unicode String Array

But what about translating binary data? Actually, the first two lines of code will properly decrypt binary
code from Base64 format to a binary array of bytes, which means we are already halfway there!
If we want to save this array of bytes to a file, and it was a JPEG file, which the Content-Type file may
have informed us of by stating “Content-Type: image/jpeg; name=NewImage.JPG”, we could do this:
b64Data = b64Data.Replace(vbCrLf, vbNullString) 'remove artificial email line terminators from b64Data string
Dim Byt() As Byte = System.Convert.FromBase64String(b64Data) 'convert b64Data string from Base64 to a binary byte array
Dim sw As New System.IO.FileStream("C:\MiscIO\NewImage.JPG", IO.FileMode.OpenOrCreate) 'create destination file stream
sw.Write(Byt, 0, UBound(Byt) + 1) 'write file from Byte array. Stream all bytes
sw.Close() 'close file (automatically invoke Dispose for sw object)

But suppose we want to display it within a PictureBox control without first saving it to a file? What
would we do then? The easy and fast way is to instead simply take advantage of a MemoryStream
object, which is just like a file stream, except that the stream remains entirely in-memory:
b64Data = b64Data.Replace(vbCrLf, vbNullString) 'remove artificial email line terminators from b64Data string
Dim Byt() As Byte = System.Convert.FromBase64String(b64Data) 'convert b64Data string from Base64 to a byte array of ASCII values
Dim ImgStrm As New System.IO.MemoryStream(Byt) 'convert byte array to a memory stream
Me.PictureBox1.Image = Image.FromStream(ImgStrm) 'import the memory stream to an image object
ImgStrm.Close() 'close the memory stream (automatically invoke Dispose)
 Page 407

Or, how about this one-liner that will do exactly the same thing:
Me.PictureBox1.Image = Image.FromStream(New System.IO.MemoryStream(System.Convert.FromBase64String(b64Data.Replace(vbCrLf, vbNullString))))

NOTE: Do not worry about closing the memory stream, as we did in the previous example. It will automatically close when
it falls out of scope, which happens after the picture box received the image. This is the real beauty with VB.NET being a
fully-functional object-oriented programming language, and finally having real, and thankfully strict scoping rules.
Consider the following two functions, which will decode raw Base64-encoded message string data:
'*******************************************************************************
' Function Name : DecodeBase64ToStr
' Purpose : Decode a provided raw email message string that is encoded to Base64.
' :
' Returns : Decoded String
' :
' NOTES : note that the lone vbCrLf at the end of lines is filtered out.
'*******************************************************************************
Public Function DecodeBase64ToStr(ByVal strData As String) As String
Return Encoding.UTF8.GetChars(DecodeBase64ToBytes(strData))
End Function

'*******************************************************************************
' Function Name : DecodeBase64ToBytes
' Purpose : Decode a provided raw email message string that is encoded to Base64.
' :
' Returns : Decoded binary Byte Array
' :
' NOTES : note that the lone vbCrLf at the end of lines is filtered out.
'*******************************************************************************
'this modification returns a Byte Array of the Base64 encoded source data
Public Function DecodeBase64ToBytes(ByVal strData As String) As Byte()
Return System.Convert.FromBase64String(strData.Replace(vbCrLf, vbNullString))
End Function

The first function, DecodeBase64ToStr is meant to process text data that has been encoded to Base64, but is
known to have a text result (perhaps by inspecting the parameter in the Content-Type field, for example). The
second method, DecodeBase64ToBytes, is meant to process a binary data result, returning a Byte array. We can
afterward process the byte array as we see fit, such as saving it to a file, or rendering it if it is a renderable
object, such as an image, using the Memory Stream technique shown above. Notice, finally that the
DecodeBase64ToStr method takes full advantage of the DecodeBase64ToBytes method, because the data must
always be first rendered to a Byte array.

Translating BinHex Data Back to its Original Format.


BinHex was first written by Tim Mann for the TRS-80. It is actually an inefficient format, converting
every byte into two, but it was essential back in the mid-1980s to transfer binary data over the internet
by services like CompuServe, which for some time did not support 8-bit data transfer. BinHex was a fix
that just stuck, and would have come along even if Tim did not write it. Even the MacIntosh adopted it.
NOTE: Anyone who knocks the TRS-80 computer, calling it by such derogatory terms as “Trash-80”, is speaking from a position of
total ignorance. It was an essential technological platform that enabled us be sitting here today with our PCs and our iMacs, our
smart phones, tablet PCs, laptops, PDAs, and most other gadgets. Had the relatively inexpensive TRS-80 failed and had not become
a leader in the PC market and business, we would be looking at a much different and more primitive world right now.

For those who discover that they have such data, they can decode it quite easily. Just supply the
DecodeBinHex function with a raw hex string, such as the data provided in an email, and it will return a
Byte array with the converted data. Consider the following method:
'*******************************************************************************
' Function Name : DecodeBinHex
' Purpose : Decode a provided raw email message string that is encoded to BinHex.
' :
' Returns : Decoded String
' :
' NOTES : note that the lone vbCrLf at the end of lines is filtered out.
'*******************************************************************************
Public Function DecodeBinHex(ByVal StrData As String) As Byte()
Dim Src() As Byte = Encoding.UTF8.GetBytes(StrData.Replace(vbCrLf, vbNullString).ToUpper)
Dim Result() As Byte 'init output buffer
ReDim Result((UBound(Src) + 2) \ 2) 'set initial dimension to bytes defined in file
Dim Index As Integer = 0 'init index for Result() array
For Idx As Integer = 0 To UBound(Src) Step 2 'scan the string, 2 hex characters at a time
Dim CL As Integer = Src(Idx) - 48 'Convert "0" - "F" to 0-F
If CL > 10 Then CL -= 7
 Page 408

Dim CR As Integer = Src(Idx + 1) - 48 'do the same for the right hex digit
If CR > 10 Then CR -= 7
Result(Index) = CByte(CL * 16 + CR) 'stuff byte value
Index += 1 'bump index
Next
ReDim Preserve Result(Index - 1) 'set array to final size
Return Result 'return the final result
End Function

Like with the DecodeBase64ToBytes method, mentioned earlier, we can use the returned array to save to a
file, or render it to an image, if that is its purpose. However, if we would like to convert to text, if that
was its origin, then we can convert the returned Byte array like this:
Dim NewStr As String = Encoding.UTF8.GetChars(DecodeBinHex(strData))

PART THREE.

Receiving Email Under VB.NET Using Native Methods.


The third part of the article will focus on receiving email into VB.NET, and we are going to do it with
ease, because none of the methods I present here are either long or complicated. Once we examine all
the methods that we will use to define our email reading class, POP3, we may really start to wonder
why Microsoft did not choose to make POP3 inbound mail processing as easily accessible as they did
SMTP outbound mail. Perhaps they figured, and maybe rightly so, that if anyone can write an email
processor that supports the RFC 2045 specification, then they likely had sufficient knowledge to easily
handle TCP Clients without an easier back end. But imagine the apps that would come out if they did?
There are hundreds of thousands of programmer queries on the internet asking how to access POP3 from
VB.NET (and those are just the ones who took time out of their day to say they needed POP3 front end
code). Most “solutions” simply direct us to a few samples of comment-challenged C# or C++ code.
Most VB.NET “gurus” will simply say that POP3 is impossible to do under VB.NET.
But because it was impossible, it took me all afternoon to come up with my own VB.NET solution, and
it works better than any C# or C++ version I have yet seen. I must admit that after having fully
developed this solution, I finally did find a solitary VB.NET article, last updated on 26 March, 2003, on
MSDN by Duncan Mackenzie (see “Checking My E-mail” at http://msdn.microsoft.com/en-us/library/ms973213.aspx),
who was the Microsoft Visual Basic .NET Content Strategist for MSDN. It implemented a very simple
sample solution. His example approach was very rudimentary (I almost said basic – no pun intended),
slow, it did not provide for SSL certificates, it featured little functionality, and it used a lot of code where
I needed only a little just to accomplish the same thing. But I am not knocking him; he was writing this
for VB2002, and he was just throwing out an example, not a full implementation. Plus, working with the
.NET Framework is much easier now than it was back in the wild and wooly days of ‘ot 3 or even ‘ot 1.
My solution was initially inspired by C# code pieced together by Randy Charles Morin, author of the
KBCafe.com blog site (see “How to POP3 in C#” www.kbcafe.com/articles/HowTo.POP3.CSharp.pdf, circa May 2002,
but my current link is to a recently updated version of the article). Randy sifted various C# solutions and
tacked together his own. Bill Dean also made a 5 August 2003 C# submission to The Code Project
(www.codeproject.com/KB/IP/pop3client.aspx), which was inspired by Agus Kurniawan’s article on using C# to
communicate with a POP3 server, submitted to The Code Project on 19 January, 2002
(www.codeproject.com/KB/IP/popapp.aspx).
For all those listed, their code interfaces are very simplistic, and even rougher around the edges than that of
Mackenzie’s work. For example, not one of them could access SSL/TLS accounts, and interpreting email
was not addressed (yep, we are going to find a utility here to easily break emails up into its parts).
 Page 409

My initial goal was simply to see why those internet gurus, some of whom, based on their writing skills,
I can easily imagine sitting there in 3-day-old boxers, wolfing down a bag of M&Ms and flushing the
sugar away with Diet Pepsi, were saying that this kind of solution was not possible under VB.NET.
After building a VB.NET version familiarly modeled after Randy’s approach, I then took great pains to
significantly optimize the code for both size and speed, and to fully address all the limitations he pointed out
(this in turn resulted in two complete rewrites of my code), and further enhancing it to accommodate many
more functions to conform more fully to the POP3 specifications (see “Post Office Protocol – Version 3” at
www.ietf.org/rfc/rfc1939.txt), to include optional TCP Ports and SSL/TLS support. I finally fully commented the code.
By the way, what is it with most developers and their inability to comment their code? Do comments have
cooties?
NOTE: Cooties was a term first used in a 1917 Service Manual, and is likely to have come from the Malayan word kutu,
meaning lice. The trenches of World War I were a veritable breeding ground for every kind of parasitic pest imaginable.
We will create a single file, but we will add 3 classes to it. The file will be a class module. So go ahead
and create a class file named POP3.vb. This shall result in the following shell code being generated:
Public Class POP3

End Class

We will leave it alone for now and create two other classes, following it immediately in the same file.
The second class, named POP3Message, shall define a message storage class, which will store a single
email message. It will only hold fields, much like its abstract class cousin, a Structure, but we will
define it as an object to avoid Boxing as we work with it (Boxing is the process of wrapping a structure
within a class shell so that it can be operated on much more quickly as a reference object, keeping its
data contained without the possibility of cloning itself whenever it is manipulated). It is declared below:
'***********************************************************************************
'***********************************************************************************
' Class Name : POP3Message
' Purpose : POP3 message data
'***********************************************************************************
'***********************************************************************************
Public Class POP3Message
Public MailID As Integer = 0 'message number
Public ByteCount As Integer = 0 'length of message in bytes
Public Retrieved As Boolean = False 'flag indicating if the message has been retrieved
Public Message As String = Nothing 'the text of the message

Public Overrides Function ToString() As String


Return Message
End Function
End Class

NOTE: Do not be nervous about adding more than one class to a file. This is perfectly acceptable. In fact, if these other
classes were used only within the POP3 class, we could have embedded them within its body, at the same level as its
methods. Further, we might be able to see how a .NET Namespace is structured from this. We will see this concept much more
clearly once we put these classes together to create a class library, which we will be able to import into an application.
The above class object will store the message number as an integer. This is the reference number that the
email server will have assigned to each message, which is normally an incremental index, but we should not
make any assumptions about it. It also has a field storing the message's size in bytes. A Boolean flag acts to
indicate if we have actually retrieved the message text, or just its references, and then we store the message
itself as a string. Because email is transmitted as text, even HTML and Rich Text format, and binary
attachments are tacked on as encoded 7-bit ASCII strings, text is usually formatted to certain line widths, and
each line is terminated by a Carriage Return and Linefeed (vbCrLf). The email program usually translates
this encoding and displays any formatted representation within a text box, rich text box, or web control.
Our third class, POP3Exception, is an error class used to differentiate our POP3 errors from normal
coding errors:
 Page 410

'***********************************************************************************
'***********************************************************************************
' Class Name : POP3Exception
' Purpose : process exception
' NOTE : This is a normal exception, but we wrap it to give it an identity
' : that can be associated with our POP3 class
'***********************************************************************************
'***********************************************************************************
Public Class POP3Exception
Inherits ApplicationException

Public Sub New(ByVal str As String)


MyBase.New(str)
End Sub
End Class

It may not look like much, but the Inherits instruction provides it with as much capability and
functionality as any other Exception object, plus we can trap errors that we will know were issued by it.
That is the real beauty of inheritance.
With this exception class, within a Try...Catch block, we can capture exceptions issued by it as Catch ex
As POP3Exception, before more general catches, if any.

Now we are ready to construct our main and presently empty POP3 class.
To do everything I want to do in this class, I already know that I will need to import 2 namespaces:
● I will first import System.Net, from which I can access the Sockets and Security namespaces. The Sockets
namespace is important because through it I can access a POP3 server though a TC/IP Client class. The Security
namespace is important if we will require SSL/TLS authentication, such as Gmail and many others servers require.
● The second namespace to import is System.Text, because I will need to translate between Unicode strings and Byte
arrays. Although it is possible to long-path these namespaces in the code without a compilation cost of even 1 byte, I
like to list the namespaces used at the top for self-documentation purposes, informing reviewers of use of resources.
I will also need to have local fields that will live as long as the class instance. Among those, I will need a
Boolean flag that will indicate if we will require SSL/TLS authentication or not. We will also need to
store two stream objects; one for a standard non-SSL Network Stream, and a possible second SSL
Stream. Both of these streams shall be used for POP3 mail server communication. Regardless, the
Network Stream is essential for both types, because an SSL piggybacks itself onto a Network Stream.
Finally, I want to store the last line read from the server in case we ever want to check it further.
Therefore, the main body of my file, with all class bodies, becomes the following:
Option Strict On
Option Explicit On
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' POP3 - Copyright © 2011 – 2021 by David Ross Goben.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'This VB.NET Code was inspired by C# code originally written by Randy Charles Morin,
'author of KBCafe.com.
'
' I have optimized the heck out of the code to speed I/O and program execution,
' forcing a complete rewrite, I have added MANY language and POP3 enhancements,
' cleaned up a lot of clutter, and added Port and SSL support.
' Oh! And I include REAL comments.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imports System.Net, System.Text
'-------------------------------------------------------------------------------
' Class Name : POP3
' Purpose : POP3 Interface Class
'-------------------------------------------------------------------------------
Public Class POP3
Inherits Sockets.TcpClient 'this class shall inherit all the functionality of a TC/IP Client.

Private Stream As Sockets.NetworkStream 'non-SSL stream object.


Private UsesSSL As Boolean = False 'True if SLL authentication required.
Private SslStream As Security.SslStream 'set to SSL stream supporting SSL authentication if UsesSSL is True.
Private SslStreamDisposed As Boolean = False 'true if we disposed of the SSL Stream object.
Public LastLineRead As String = Nothing 'copy of the last response line read from the TCP server.

'>>>>>>>>>ANY MORE CODE WILL BE INSERTED HERE HERE<<<<<<<<<


End Class

'***********************************************************************************
 Page 411

'***********************************************************************************
'***********************************************************************************
'-------------------------------------------------------------------------------
' Class Name : POP3Message
' Purpose : POP3 message data
'-------------------------------------------------------------------------------
Public Class POP3Message
Public number As Integer = 0 'message number.
Public bytes As Integer = 0 'length of message in bytes.
Public retrieved As Boolean = False 'flag indicating if the message has be retrieved.
Public message As String = Nothing 'the text of the message.
End Class

'***********************************************************************************
'***********************************************************************************
'***********************************************************************************
'-------------------------------------------------------------------------------
' Class Name : POP3Exception
' Purpose : process exception
' NOTE : This is a normal exception, but we wrap it to give it an identity
' : that can be associated with clsPOP3.
'-------------------------------------------------------------------------------
Public Class POP3Exception
Inherits ApplicationException

Public Sub New(ByVal str As String)


MyBase.New(str)
End Sub
End Class

The first and last thing we need to do after defining this is to connect to the POP3 server, and of course,
to disconnect from it when we are finished. Plus, we might as well find out if we have any email.

Connecting to a POP3 Server.


Connecting to a server involves making a connection using a port, typically through generic Port 110,
but we must keep it open for other ports in case they are defined (note that email is now trending toward
SSL/TLS-encrypted ports). We then have to submit our email Username and Password to it. We will also
want to save the server name off in case we will require later SSL authentication.
Connect is a method that is built into the socket library, so we will have to overload it in order to apply
our own customizations on top of it. This is not anywhere near as scary as it might sound, because we
can still access the original Connect method declared for the TCPClient base class that our POP3 class
inherited from, which is a good thing, because we will in fact need to invoke it.
Connecting to our POP3 Host Server is always the first thing we do. Once we have done that, we will be
able to issue all other POP3 commands until we either remove our connection to the POP3 class, which
breaks our connection to the server, or we can explicitly disconnect from the server. We must have this
choice in terminating POP3 server communication, as we will learn, in order to provide us with the
ability to remove or not removed downloaded emails from the Host Server.
We will also require a method to submit queries to the server, which will also mean that we need a
resource to check responses from the server (its replies to our queries). Communication with the server
is just a long series of our text-based requests and its text-based responses. We will examine each of
these parts in their turn.
NOTE: When we connect to a server using an eMail application, such as OutLook, or connect to a web eMail service,
most of them feature extended functionality. Apart from the bells and whistles of enhanced viewing of our eMail on our
system or on the server, most of them now feature subfolders, where we can store eMails we have sent or read and want
to save. However, if we think that we are going to be able to connect to our server and simply access those subfolders,
bear in mind that features such as server subfolders are extended features not covered, but allowed by the eMail
specifications. As such, these custom extensions will not be covered here. Even so, online articles do exist that detail
these customizations, such as Outlook, that we can adapt our code to. Being that these extensions are becoming
uniform, I expect that some day they will be standardized and be incorporated as a part of the specifications.
 Page 412

Following is our new Connect method. Note that it references other methods that we will address as we
continue:
'*******************************************************************************
' Sub Name : Connect (This is the first thing we do with a POP3 object)
' Purpose : Connect to the server using the Server, User Name, Password, and a if SSL authentication is required.
' :
' Returns : Nothing
' :
' Typical TelNet I/O:
'telnet mail.domain.net 110 (submit)
'+OK POP3 mail.domain.net v2011.83 server ready
'USER myusername (submit)
'+OK User name accepted, password please
'PASS mysecretpassword (submit)
'+OK Mailbox open, 3 messages (the server locks and opens the appropriate maildrop)
'*******************************************************************************
Public Overloads Sub Connect(ByVal Server As String,
ByVal Username As String,
ByVal Password As String,
Optional ByVal InPort As Integer = 110,
Optional ByVal UseSSL As Boolean = False)

If Connected Then Disconnect() ' 'check BaseClass Boolean flag to see if we are presently connected,
' 'and if so, disconnect that session.
UsesSSL = UseSSL 'set flag True or False for SSL authentication.
Try
Connect(Server, InPort) 'connect to server via base class (signature differs from our method).
Catch
Throw New POP3Exception("Cannot connect. Check Server, Port, Username or Password for errors")
Return
End Try
Stream = GetStream() 'before we check for response, we first must set up non-SSL stream.
If UsesSSL Then 'do we also need to use SSL authentication?
SslStream = New Security.SslStream(Stream) 'yes, so build an SSL stream object on top of the non-SSL Network Stream.
SslStream.AuthenticateAsClient(Server) 'add authentication as a client to the server.
End If

If Not Me.CheckResponse() Then Exit Sub ' 'exit if an error was encountered.

If CBool(Len(Username)) Then 'if the username is defined (some servers will reject submissions).
Me.Submit("USER " & Username & vbCrLf) 'submit user name.
If Not Me.CheckResponse() Then Exit Sub ' 'exit if an error was encountered.
End If

If CBool(Len(Password)) Then 'if the password is defined (some servers will reject submissions).
Me.Submit("PASS " & Password & vbCrLf) 'submit password.
If Not Me.CheckResponse() Then Exit Sub ' 'exit if an error was encountered.
End If
End Sub

As we can see, we begin our adventure into the world of POP3 by initiating communication with our
POP3 server. We do this by providing the Connect method with the host server address, our email
username (or our whole email address, if our particular server, such as Gmail, requires it), our email
password, a port number if it differs from TCP Port 110, which is becoming common, and True if our
POP3 server will require a Secure Socket Layer.
The first thing that Connect does is check to see if we have a valid POP3 connection. After that, we stow
away important references, such as our SSL choice. If we will use SSL, we set our UsesSSL flag to True.
Next, we access the base class’s Connect method and submit the server name we wish to access, such as
“mail.comcast.net” or “pop.gmail.com”. By default, we submit Port 110 for communication. This is the
port most often used by mail servers, though encrypted SSL/TLS connections are growing in popularity.
Some servers use a different port, but this is usually because they also use SSL Authentication. Gmail
does this. For example, it uses a secure TCP Port 995 for POP3.
NOTE: Some servers let anyone and everyone on. In such cases, some few servers sometimes do not even accept usernames
and/or passwords and will reject them, or even to accept any and all responses, or they might allow us to use a generic sign-
on, such as “guest” or “user” and “password”. In those cases, we have to allow for those situations.
 Page 413

Checking for a POP3 Server Response.


We next invoke CheckResponse to see if a request was accepted. If so, a returned string begins with
“+OK”. Because this test runs rampant throughout a TCP client session, I broke it out into a separate
function that returns True if it was successful (its full response, stored in our public string,
LastLineRead, begins with “+OK”), and False if it was not (an error begins with “-ERR”). This test
follows most-all submissions.
Here is the CheckResponse method:
'*******************************************************************************
' Function Name : CheckResponse
' Purpose : Check the response to a POP3 command
' :
' Returns : Boolean flag. Return True for Success. Throw an error and return False if not.
' :
' NOTE : All status responses from the server begin with:
' : +OK (OK; Success, or request granted)
' or : -ERR (NEGATIVE; error)
'*******************************************************************************
Public Function CheckResponse() As Boolean
If Not Me.IsConnected() Then Return False ' 'exit if not in TRANSACTION mode.
LastLineRead = Me.Response() 'check response (and save response line).
If (LastLineRead.Substring(0, 3) <> "+OK") Then 'OK?
Throw New POP3Exception(LastLineRead) 'no, so throw an exception.
Return False 'return failure flag.
End If
Return True 'else return success flag.
End Function

This method in turn invokes our Response method. But what is important here is that we get our first
look at returned data from the POP3 server, and if we encounter an error, such as the first 3 letters of the
response line not being “+OK”, then we throw our POP3Exception error, transmitting the returned text
line to the message pump hidden with the exception object. We may even want to preview what the full
responses are by examining the LastLineRead text; the rest of the “+OK” message can be informative
(as are error messages, which all start with “-ERR”), such as “+OK Gpop ready for requests from
12.34.567.89 fahrvergnugen8.0”.

Checking for Being Connected to a POP3 Server.


The IsConnected method consolidates a lot of checks to the underlying base class’s Boolean Connected
property. It throws a POP3ExceptionError if the server is not connected, indicating that it is not in a
Transaction state:
'*******************************************************************************
' Function Name : IsConnected
' Purpose : Return True if we are connected to Server in TRANSACTION state. Throw an error and return False if not.
' :
' Returns : Boolean Flag. True if connected to server
'*******************************************************************************
Public Function IsConnected() As Boolean
If Not Connected Then 'if not connected, throw an exception.
Throw New POP3Exception("Not Connected to a POP3 Server.")
Return False 'return failure flag.
End If
Return True 'Indicate that we are in the TRANSACTION state).
End Function
 Page 414

Getting a Response from the POP3 Server.


The Response method is where all the really interesting things happen. Here we will finally implement
our communication streams to receive “real” information from the POP3 server.
'*******************************************************************************
' Function Name : Response
' Purpose : get a response from the server (read from the mail stream into a buffer)
' :
' Returns : string of data from the server
' :
' NOTE : If a dataSize value > 1 is supplied, then those number of bytes will be streamed in, otherwise, the data will
' : be read in a line at a time, and end with the line end code (Linefeed (vbLf) 10 decimal).
'*******************************************************************************
Public Function Response(Optional ByVal dataSize As Integer = 1) As String
Dim ServerBufr() As Byte 'establish buffer.
Dim Index As Integer = 0 'init server buffer index and character counter.
If dataSize > 1 Then 'did invoker specify a data length to read?
'-------------------------------------------------------
ReDim ServerBufr(dataSize - 1) 'size to dataSize to read as single block (allow for 0 index)
Dim dtsz As Integer = dataSize 'set aside updating countdown.
Dim sz As Integer 'variable to store actual number of bytes read from the stream.
Do While Index < dataSize 'while we have not read the entire message...
If UsesSSL Then 'process through SSL Stream if secure stream.
sz = SslStream.Read(ServerBufr, Index, dtsz) 'read a server-defined block of data from SSLstream.
Else 'else process through general TCP Stream.
sz = Stream.Read(ServerBufr, Index, dtsz) 'read a server-defined block of data from Network Stream.
End If
If sz = 0 Then Return Nothing ' 'we lost data, so we could not read the string.
Index += sz 'bump index for data count actually read.
dtsz -= sz 'drop amount left in buffer.
Loop
Else '------------------------------------------------------
ReDim ServerBufr(255) 'initially dimension buffer to 256 bytes (including 0 offset).
Dim byteRead As Int32 'capture result of Stream.ReadByte() (function returns Int32).
Do
If UsesSSL Then 'process through SSL Stream if secure stream.
byteRead = SslStream.ReadByte 'read a byte from SSLstream .
Else 'else process through general TCP Stream.
byteRead = Stream.ReadByte 'read a byte from Network stream.
End If
If byteRead = -1 Then Exit Do ' 'end of stream if -1 encountered.
ServerBufr(Index) = CByte(byteRead) 'otherwise, capture the byte read in our array.
Index += 1 'bump our offset index and counter.
If byteRead = 10 Then Exit Do ' 'done with line if Newline code (10; Linefeed code) read in.
If Index > UBound(ServerBufr) Then 'if the index points past the end of the buffer...
ReDim Preserve ServerBufr(Index + 255) 'then bump buffer for another 256 bytes, but keep existing data.
End If
Loop 'loop until a line is read in or the end of data is encountered.
End If
Dim enc As New ASCIIEncoding 'medium for ASCII representation of Unicode characters.
Return enc.GetString(ServerBufr, 0, Index) 'decode from the byte array to a string and return the string.
End Function

Here we read from either the Network Stream (Stream) or the SSL Stream (SslStream), depending on
whether we require SSL authentication to communicate with the TCP Server or not. If we do not specify
a buffer size, we read the port one byte at a time until we encounter an end of line code (vbLf ; 10).
Every C#/C++ example I have ever seen first reads a stream byte into a single-element intermediate
buffer array using the stream.Read method, which requires a destination byte array. However, I have
noticed that this can hang until an eventual timeout (usually 30-60 seconds) if the signal drops off, so we
end up trying to read beyond the end of the provided stream, which is a more concerning issue when
reading one byte at a time. Unlike everyone else, I chose the ReadByte method instead, which can read a
Byte value (the docs do state that a temp 1-byte internal array is created to read the byte, so the user
should find a more efficient means of reading more than one byte, but I think reading one byte at a time
is still efficient enough, and still quite fast). More importantly, ReadByte will return a -1 immediately if
we moved beyond the end of the stream or, more importantly, if the connection dropped off.
If the Stream.ReadByte method or the SslStream.ReadByte methods do not report back that they failed
to read from their stream, then the index/counter is incremented.
 Page 415

We loop until we get an end of a line code (a value of 10, representing a Linefeed character; vbLf,
typically following a Carriage Return; vbCr), or if no data was received (it could mean that the signal
dropped off). Once it is done reading bytes, the byte array is then translated to a Unicode string (the
default string format for VB.NET) and it is returned to the invoker.
If a buffer size was supplied, the ServerBufr is immediately dimensioned to that size (-1 is applied for
the 0 offset). We then continuously try to fill the buffer full. However, the server actually returns data to
us in blocks, not in one big blobby mess of data. For example, the “header” of an email actually consists
of a number of headers, and the body of an email is considered another block, as are attachments and
alternate views. And even then, if the data block is longer than the server’s stream buffer, it will break it
up into a sequence of sub-blocks. Documentation says that the default maximum stream buffer size is
998 bytes, though it is not uncommon to find buffers that are larger, such as 1400 bytes. This size is
implementation-defined. Even though our Stream and SslStream objects do have a SetLength property
that is supposed to allow us to set the stream buffer size, this is not yet supported: a sad fact that
MSDN’s documentation also states.
Submitting a Request to the POP3 Server.
The one thing left that we must look at, which is so far unresolved, is the Submit method:
'*******************************************************************************
' Sub Name : Submit
' Purpose : Submit a request to the server
' :
' Returns : Nothing
' :
' NOTE : Command name must be in UPPERCASE, such as "PASS pw1Smorf". "pass pw1Smorf" would not be acceptable.
' : Though some servers do allow for this, we should NEVER assume it.
'*******************************************************************************
Public Sub Submit(ByVal message As String)
Dim enc As New ASCIIEncoding 'medium for ASCII representation of Unicode characters
Dim WriteBuffer() As Byte = enc.GetBytes(message) 'converts the submitted string into to a sequence of bytes.
If UsesSSL Then 'using SSL authentication?
SslStream.Write(WriteBuffer, 0, WriteBuffer.Length) 'yes, so write SSL buffer using the SslStream object.
Else
Stream.Write(WriteBuffer, 0, WriteBuffer.Length) 'else write to Network buffer using the non-SSL object.
End If
End Sub

We convert our Unicode request to a Byte array and then send it to the appropriate stream, depending on
whether we use SSL authentication or not. One thing we may not realize is that we can directly access
this method and submit a command directly. Just be sure that the command word is uppercase.
Disconnecting from the POP3 Server.
A final primary thing that we may need to do, if we want the server to enter its usual UPDATE state, is
to disconnect from the POP3 Server. That is accomplished through the Disconnect method:
'*******************************************************************************
' Sub Name : Disconnect (This is the last thing we do with a POP3 object)
' Purpose : Disconnect from the server and have it enter the UPDATE mode.
' :
' Returns : Nothing
' :
' Typical telNet I/O:
'QUIT (submit)
'+OK Sayonara
'
' NOTE: When the client issues the QUIT command from the TRANSACTION state, the POP3 session enters the UPDATE state.
' (Note that if the client issues the QUIT command from the AUTHORIZATION state, the POP3 session terminates
' but does NOT enter the UPDATE state.)
'
' If a session terminates for some reason other than a client-issued QUIT command, the POP3 session does NOT
' enter the UPDATE state and MUST NOT remove any messages from the maildrop.
'
' The POP3 server removes all messages marked as deleted from the maildrop and replies as to the status of this
' operation. If there is an error, such as a resource shortage, encountered while removing messages, the
' maildrop may result in having some or none of the messages marked as deleted be removed. In no case may the
' server remove any messages not marked as deleted.
'
' Whether the removal was successful or not, the server then releases any exclusive-access lock on the maildrop
' and closes the TCP connection.
 Page 416

'*******************************************************************************
Public Sub Disconnect()
Me.Submit("QUIT" & vbCrLf) 'submit quit request.
CheckResponse() 'check response.
If UsesSSL Then 'SSL authentication used?
SslStream.Dispose() 'dispose of created SSL stream object if so.
SslStreamDisposed = True
End If
End Sub

We check for a response, but we throw it away, at least at this level, because we are leaving, regardless.
Also, because we had to instantiate our SSL Stream (but only if we require SSL authentication), we
dispose of this resource. We do not have to concern ourselves with the Network Stream object, because
we only maintained a pointer to it. The actual Stream object is taken care of by the underlying
TCPClient base class, which will dispose of that object for us (we did not instantiate the Network
Stream object with “New”, because it was already instantiated by the underlying code of the base class).
Of course, now that we can connect, log on, and disconnect from the POP3 server, it would also be a
really good idea to be able to read some data from it, such as an email maybe, before we disconnect.
Getting Email Statistics from the POP3 Server.
The first thing we might want to do after connecting to our POP3 server is to get some statistics, such as
how many emails we have in our mail drop on the server and how many bytes this data occupies on that
server. These can both be accessed using our Statistics method:
'*******************************************************************************
' Function Name : Statistics
' Purpose : Get the number of email messages and the total size as any integer array.
' :
' Returns : 2-element integer array.
' : Element(0) is the number of user email messages on the server.
' : Element(1) is the total bytes of all messages taken up on the server.
' :
' Typical telNet I/O:
'STAT (submit)
'+OK 3 16487 (3 records (emails/messages) totaling 16487 bytes (octets))
'*******************************************************************************
Public Function Statistics() As Integer()
If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode.
Me.Submit("STAT" & vbCrLf) 'submit Statistics request.
LastLineRead = Me.Response 'check response.
If (LastLineRead.Substring(0, 3) <> "+OK") Then 'OK?
Throw New POP3Exception(LastLineRead) 'no, so throw an exception.
Return Nothing 'return failure flag.
End If
Dim msgInfo() As String = Split(LastLineRead, " "c) 'separate by spaces, which divide its fields
Dim Result(1) As Integer.
Result(0) = Integer.Parse(msgInfo(1)) 'get the number of emails.
Result(1) = Integer.Parse(msgInfo(2)) 'get the size of the email messages.
Return Result
End Function

This returns an integer array. Result(0) will contain the number of email messages residing in our mail
drop, and Result(1) contains the total number of bytes the messages occupy on the server.
NOTE: Most online servers are Unix, and store line terminators as vbLf, rather than vbCrLf, so we should expect a slight
discrepancy when we load files, because our TCP connection will read and convert a lone vbLf to vbCrLf.

Getting an Email Reference List from the POP3 Server.


The next thing we might want to do after connecting and finding out that we have email in our drop box,
is to get a list of those emails from the server. Our List method takes care of this, constructing a
ListArray of POP3Message objects from the server, and then returning them to the invoker:
 Page 417
.

'*******************************************************************************
' Function Name : List
' Purpose : Get the drop listing from the maildrop
' :
' Returns : An Arraylist of POP3Message objects
' :
' Typical telNet I/O:
'LIST (submit)
'+OK Mailbox scan listing follows
'1 2532 (record index and each size on the server in bytes)
'2 1610
'3 12345
'. (end of records terminator)
'*******************************************************************************
Public Function List() As ArrayList
If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode.

Me.Submit("LIST" & vbCrLf) 'submit List request.


If Not CheckResponse() Then Return Nothing ' 'check for a response, but if an error, return nothing.
'
'get a list of emails waiting on the server for the authenticated user.
'
Dim retval As New ArrayList 'set aside message list storage.
Do
Dim response As String = Me.Response 'check response.
If (response = "." & vbCrLf) Then 'done with list?
Exit Do 'yes.
End If
Dim msg As New POP3Message 'establish a new message
Dim msgInfo() As String = Split(response, " "c) 'separate by spaces, which divide its fields.
msg.MailID = Integer.Parse(msgInfo(0)) 'get the list item number.
msg.ByteCount = Integer.Parse(msgInfo(1)) 'get the size of the email message.
msg.Retrieved = False 'indicate its message body is not yet retreived.
retval.Add(msg) 'add a new entry into the retrieval list.
Loop
Return retval 'return the list.
End Function

The List method constructs a list of POP3Message objects, but we may notice that we do not fill their
Message field with anything, just their server reference number and the number of bytes that the
message occupies. That is because when we submit a “List” request, the server returns a simple string
for each email that it has pending for the Username. This simple string consists of two numeric text
values, separated by a space, such as “ 1 2532”. The first is the reference index for each item in its mail
drop for the Username, and the other is the number of bytes the message occupies on the server.
Get an Email Header from the POP3 Server.
There are also a number of peripheral POP3 commands that we can exploit to round out our email
retriever, such as retrieving the actual email message text (gee, what a unique idea), deleting an email
from the server, resetting any deletions (great for OOPS! situations), and we can even grab just the
header of the email and optionally a specified number of additional lines from the body of the email
(handy for previews). These peripheral methods will be listed one at a time.
The following method gets the header and an optional number of message lines from an email.
'*******************************************************************************
' Function Name : GetHeader
' Purpose : Grab the email header and optionally a number of lines from the body
' :
' Returns : Gets the Email header of the selected email. If an integer value is provided, that number of body lines will
' : be returned. The returned object is the submitted POP3Message.
' :
' Typical telNet I/O:
'TOP 1 0 (submit request for record 1's message header only, 0=no lines of body)
'+OK Top of message follows
' xxxxx (header for current record is transmitted)
'. (end of record terminator)
'
'TOP 1 10 (submit request for record 1's message header plus up to 10 lines of body data)
'+OK Top of message follows
' xxxxx (header for current record is transmitted)
' xxxxx (first 10 lines of body)
'. (end of record terminator)
'*******************************************************************************
Public Function GetHeader(ByRef msg As POP3Message, Optional ByVal BodyLines As Integer = 0) As POP3Message
If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode.
Me.Submit("TOP " & msg.MailID.ToString & " " & BodyLines.ToString & vbCrLf)
 Page 418

If Not CheckResponse() Then Return Nothing ''check for a response, but if an error, return nothing.
msg.Message = Nothing 'erase current contents of the message, if any.
'
'now process message data by binding the lines into a single string.
'
Do
Dim response As String = Me.Response 'grab message line.
If response = "." & vbCrLf Then 'end of data?
Exit Do 'yes, done with the loop if so.
End If
msg.Message &= response 'else build message by appending the new line.
Loop
Return msg 'return new filled Message object.
End Function

The GetHeader method allows us to grab only the header of the message, or the header plus a specified
number of body text lines. By submitting a POP3Message to the GetHeader function, this method will
return a pointer to the POP3Message object that we had submitted, containing the header of the
message. If it returns Nothing, then there was an error in trying to retrieve the header.
The Header of email is interesting, except to mere mortals, who have no use for it, most thinking it
contains nothing but routing data. However, though they may not realize it, the last few lines of the
header actually do have some interest to them. Consider the following trailing end of an email header:
From: mercedes_silver@80micro.com
To: david.ross.goben@gmail.com
Date: 25 Feb 2011 17:59:01 -0500
Subject: Test
Content-Type: multipart/alternative;
boundary=--boundary_0_570b636e-3a77-40a7-bf02-5b178a2cff5b

The lines beginning with the fields “From:”, “To:”, and “Subject:” are easily extractable. The “Date:”
field displays the Date and Time as local, with an offset value to GMT (Greenwich Mean Time;
originally referring to mean solar time at the Royal Observatory, Greenwich in England. It is now often
used to refer to Coordinated Universal Time (UTC)). But regardless, the above time means that now was
this time 5 hours ago (-0500) in Greenwich, England (I am tempted to make a SpaceBalls reference).
We can also grab the Content-Type and, if multipart/alternative, which it will always report if there are
alternative views or attachments included, it will also provide a “boundary=” parameter, which informs
us of what the boundary between the various blocks will consist of. If no alternative views or
attachments, then the Content-Type will contain the type of data, such as text/plain, and maybe a
parameter containing the character set used for the message. Alternative views and attachments, after a
boundary marker, also have a Content-Type field, which will inform us of the format the data is
formatted as, after decoding, and if an attachment, a name parameter that will tell us the default filename
(name) to store the data under, if the user wants to save the attachment to a file.

Retrieve an Email from the POP3 Server.


'*******************************************************************************
' Function Name : Retrieve
' Purpose : Retrieve email from POP3 server for the provided POP3Message object
' :
' Returns : The submitted POP3 Message object with its Message property filled, and its ByteCount property properly
' : fitted to the message size.
' :
' NOTE: Some email servers are set up to automatically delete an email once it is retrieved from the server. Outlook,
' Outlook Express, and Windows Mail do this. It is an option under Juno and Gmail. So, if we do not submit a POP3
' QUIT (the Disconnect() method), but just close out the POP3 object, the messages will not be deleted. Even so,
' most Windows-based server-processors will add an additional CR for each LF, but the reported email size does not
' account for them. So we must retreive more data to account for this.
'
' Typical telNet I/O:
'RETR 1 (submit request to retrieve record index 1 (cannot be an index marked for deletion))
'+OK 2532 octets (octet is geek for an old-school byte. Even geeks go for “chic”, but they think it refers to girls).
' xxxx (entire email is retreived as one long string)
'. (end of record terminator)
'*******************************************************************************
Public Function Retrieve(ByRef msg As POP3Message) As POP3Message
If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode.
Me.Submit("RETR " & msg.MailID.ToString & vbCrLf) 'issue request for indicated message number.
If Not CheckResponse() Then Return Nothing ' 'check for a response, but if an error, return nothing.
 Page 419

msg.Message = Me.Response(msg.ByteCount) 'grab message line.


'the stream reader automatically converts the NewLine code, vbLf, to vbCrLf, so the file is not yet
'fully read. For example, a file that was 233 lines will therefore also have 233 more characters not
'yet read from the file when it has reached its reported data size. So we will scan these in.
'But even if this was not the case, the trailing "." & vbCrLf is still pending...
Do
Dim strData As String = Response() 'grab more data.
If strData = "." & vbCrLf Then 'end of data?
Exit Do 'If so, then exit loop.
End If
msg.Message &= strData 'else tack data to end of message.
Loop 'keep trying.
msg.ByteCount = Len(msg.Message) 'ensure full size updated to actual size (server size may differ).
Return msg 'return new message object.
End Function

By submitting a POP3Message to the Retrieve function, presumably its present message string being
empty or ignorable, it will fill its Message property with the entire email, including both the full header
and body of the message. It will also return a pointer to the message, except if there was a failure.

Deleting an Email from the POP3 Server.


'*******************************************************************************
' Sub Name : Delete
' Purpose : Delete an email
' :
' Returns : Nothing
' :
' NOTE: Some email servers are set up to automatically delete an email once it is retrieved from the server. Outlook,
' Outlook Express, and Windows Mail do this. It is an option under Juno and Gmail.
'
' Typical telNet I/O:
'DELE 1 (submit request to delete record index 1)
'+OK Message deleted
'*******************************************************************************
Public Sub Delete(ByVal msgHdr As POP3Message)
If Not IsConnected() Then Exit Sub 'exit if not in TRANSACTION mode.
Me.Submit("DELE " & msgHdr.MailID.ToString & vbCrLf) 'submit Delete request.
CheckResponse() 'check response.
End Sub

The Delete method will delete a specified message. Just provide the selected POP3Message object.

Reset (Undo) All Deletes from the POP3 Server.


'*******************************************************************************
' Sub Name : Reset
' Purpose : Reset any deletion (automatic or manual) of all email from the current session.
' :
' Returns : Nothing
' :
' Typical telNet I/O:
'RSET (submit)
'+OK Reset state
'*******************************************************************************
Public Sub Reset()
If Not IsConnected() Then Exit Sub 'exit if not in TRANSACTION mode.
Me.Submit("RSET" & vbCrLf) 'submit Reset request.
CheckResponse() 'check response.
End Sub

The Reset method will undelete emails that we selected with Delete during the current session.
 Page 420

Send a ‘Keep-Alive’ NOOP Command to the POP3 Server.


'*******************************************************************************
' Function Name : NOOP (No Operation)
' Purpose : Does nothing. Juts gets a position response from the server
' :
' Returns : Boolean flag. False if disconnected, else True if connected.
' :
' NOTE: This NO OPERATION command is useful when you have a server that automatically disconnects after a certain idle
' period of activity. This command can be issued by a timer that also monitors users inactivity, and issues a
' NOOP to reset the server timer.
'
' Typical telNet I/O:
'NOOP (submit)
'+OK
'*******************************************************************************
Public Function NOOP() As Boolean
If Not IsConnected() Then Return False 'exit if not in TRANSACTION mode.
Me.Submit("NOOP")
Return CheckResponse()
End Function

The NOOP method will do nothing but obtain a response from the server. As noted, above, this method
can be used just to tickle the server. Some servers will disconnect from a user after a certain period of
time. But obtaining a response from the server, this timer is reset to the beginning of its counter, thus
keeping the connection alive. If we have such a server, we could maintain our own timer that times out
before the server time, and automatically submits a NOOP request to the server. Of course, we will want
to reset our own timer (Me.myTimer.Enabled=False: Me.myTimer.Enabled=True ) each time we issue a command to
the server, to reset our own timer (Duh!).

Disposing of Resources.
'*******************************************************************************
' Function Name : Finalize
' Purpose : remove SSL Stream object if not removed
'*******************************************************************************
Protected Overrides Sub Finalize()
If SslStream IsNot Nothing AndAlso Not SslStreamDisposed Then 'SSL Stream object Disposed?
SslStream.Dispose() 'no, so do it.
End If
MyBase.Finalize() 'then do normal finalization.
End Sub

Normally, when we issue a Disconnect command, the resources are released. But we may not want to
always do that. Many email providers will delete the message from the server once we have retrieved it,
or they make it an option. However, if the server does not receive a QUIT command (issued by
Disconnect), then it will not enter into its UPDATE state, where servers often delete retrieved messages.
If it does not enter the UPDATE state, then the emails will remain on the server.
However, if we do not issue a Disconnect command, the created resources will not be removed. This is
not really a big issue, because the .NET Garbage Collector will detect that the resources are no longer
referenced and remove them, but it is always a good idea to take care of that. Hence, we added a
Finalize method that will be issued by the Garbage Collector before destroying the POP3 object.
NOTE: Weshould never invoke the Finalize method ourselves. Let the system automatically take care of that for us.

Using the Completed POP3 Class.


Now that we have defined all the parts of our POP3 class (the complete class listing is at the end of this
article), we need to be able to use it.
Even though the following is a short-cut, consider invoking the following sample intermediary method,
during testing anyway, which will connect to the POP3 host server, grab all the emails, and disconnect.
 Page 421

However, during testing, as we prod and modify it to implement our own test designs, it might be a good
idea to leave our email on the server, to use for later testing. In that case, we should skip invoking the
Disconnect method. No worries; once our objects go out of scope, or are set to Nothing, the connection
will be lost. If we are concerned about waiting for the Garbage Collector – do not waste even a bead of
sweat. It runs as a separate thread in the background, and it runs much more often than most people
would imagine. Were it up to me, I would have it running constantly in the background (well, as of
VB2010, it does!).
The following sample method, SampleReadPOP3, returns all the available online emails from the
server to the invoker as an ArrayList filled with all our Email Messages, each member of the array stored
as individual POP3Message objects:
'*******************************************************************************
' Function Name : SampleReadPOP3
' Purpose : Sample method to Read a POP3 account maildrop
' :
' Returns : ArrayList containing a list of POP3Message objects, or Nothing if no emails
' :
' NOTE : This method should be modified to suit your application. For
' : example, the text may be HTML or Rich Text format. The raw text
' : should be plugged into the appropriate medium, such as
' : "Me.TextBox1.Text = msg2.Message" for proper viewing.
'*******************************************************************************
Public Function SampleReadPOP3(ByVal Server As String,
ByVal Username As String,
ByVal Password As String,
Optional ByVal InPort As Integer = 110,
Optional ByVal UseSSL As Boolean = False) As ArrayList
Try
Dim InMail As New POP3 'create a new POP3 connection
InMail.Connect(Server, Username, Password, InPort, UseSSL) 'Connect to user account
'-----------------------------------------------------------
Dim Stats() As Integer = InMail.Statistics() 'get maildrop statistics (# of message, total byte size)
If Stats(0) = 0 Then 'check number of messages for being 0 (none)
Return Nothing 'no email found in the maildrop, so nothing to do
End If
Dim localList As New ArrayList 'set up list of emails that will contain message text
For Each msg As POP3Message In InMail.List 'parse each header object (contains only index and size)
localList.Add(InMail.Retrieve(msg)) 'add a message object with message text
Next 'process all messages
'-----------------------------------------------------------
InMail.Disconnect() 'disconnect from server (DISABLE THIS LINE TO KEEP EMAILS ON SERVER FOR TESTING)
Return localList 'return list of filled POP3Messages to invoker

Catch e As POP3Exception 'POP3-side error


MsgBox(e.Message,
MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation,
"Error Encountered")

Catch e As Exception 'general programming error


MsgBox(e.Message,
MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation,
"Error Encountered")
End Try
Return Nothing
End Function

The first three parameters are mandatory, though in the rare cases of an all-are-welcome server, we may
need to provide a blank Username and/or Password. The Server is always mandatory, such as
“pop.gmail.com” or “authpop.juno.com”. Also, though on most log-ins, the Username is the part of the
user’s email address that comes before the “@” (At) sign, such as “Im1Idjut”, some require the entire
email address; most notably Gmail, which needs to have the entire email address, such as
“Im1Idjut@gmail.com”.
The last two parameters are required only if our POP3 TCP Port is different or our server needs SSL
authentication. For example, the parameter list for a Verizon account would be ("incoming.verizon.net",
"Im1Idjut", "CantRecall1"), specifying Server, Username, and Password. But on Comcast, the parameter
list would be ("mail.comcast.net", "Im1Idjut", "CantRecall1", 995, True) . However, typically, this list
should not be hard-coded as shown, but rather values retrieved from previously saved user preferences.
 Page 422

It first instantiates a POP3 instance and connects it to the server. It then builds an ArrayList named localList
that will contain all retrieved emails as POP3Message objects. It then disconnects from the server (only do
this when we want the server to enter the UPDATE mode and delete its list of email files ), and then it returns
this list of raw emails, each of which we must afterward break up into their individual component parts, such
as From, To, Subject, Date, Message Body, plus any possible attachments and/or alternate views.
What follows is SampleReadEmail, a sample method, that invokes the above SampleReadPOP3 method
and loop through its returned array list, displaying each raw message in a MsgBox (rather than using
absolute user account parameters, our application should process them as variable components):
'*******************************************************************************
' Sub Name : SampleReadEmail
' Purpose : Sample method to Read a POP3 account maildrop (using SampleReadPOP3 function)
' NOTE : and display each unprocessed message in a Message Box.
'*******************************************************************************
Friend Sub SampleReadEmail()
Dim EmailBag As ArrayList = SampleReadPOP3("pop.gmail.com", "bob.the.builder@gmail.com", "YudLk2Knw", 995, True)
If EmailBag IsNot Nothing AndAlso EmailBag.Count <> 0 Then 'if data was found
For Each msg As POP3Message In EmailBag 'display each email
MsgBox(msg.Message,
MsgBoxStyle.OkOnly Or MsgBoxStyle.Information,
"Message # " & msg.MailID.ToString & " of " & EmailBag.Count.ToString) 'display each raw email
Next 'process all messages
Else 'transfer here if no one loves you
MsgBox("No email found on server.", MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "No Email")
End If
End Sub

This is all. we may want to design an interactive email program. In such cases we would want to keep a
connection open and allow the user to fiddle with their email to their heart’s content, such as giving them
time to read them, reply to them, forward the same jokes for the twenty-third time, though we would want to
plagiarize the really good ones. Using the methods provided in the POP3 class, we can do quite a bit with it.

PART FOUR.
Email Data Blocks Made Easy.
There is a lot of information we can gather from just a little bit of data when it comes to an email.
Let’s first just look at a simple email and extract its important parts, and what we can do with them:
From: mercedes_silver@80micro.com ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬ FROM Field (FROM, TO, SUBJECT, and DATE Fields can be placed in any order within their 4-field zone).
To: david.ross.goben@gmail.com ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ TO Field.
Date: 25 Feb 2011 19:07:02 -0500 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ DATE Field (always d mmm, yyyy HH:mm:ss zzz).
Subject: Test ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ SUBJECT Field.
Content-Type: text/html; charset=us-ascii ◄▬▬▬▬▬▬ CONTENT-TYPE Field. The Message body data is Text and is formatted as HTML.
Content-Transfer-Encoding: quoted-printable◄▬▬▬▬▬ Content-Transfer-Encoding Field. Quoted-Printable means data is Hex-Tag encoded.
◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This line, following Content-Transfer-Encoding, is ALWAYS blank. It is NOT part of the message.
This is a test=0D=0AThis is a second test=0D=0A◄▬ Data is here, until the end of the data, or until a boundary marker is encountered.

The above is just a simple email; the type that my QuickiEmail or BrainDeadSimpleEmailSend would
send out. There are of course other routine fields located above it. But we may be wondering how the
data is formatted, or how to separate Alternate Views from Attachments, or how to decode them, or what
the hell did we do to make the boss ‘volunteer’ us to develop an email interface?
Relax. It is all easy. To solve all these issues, we are going to add a single method to our library that will
break all this information out for us so we can afterwards access each of its component parts.
If we refer to RFC 2045 (www.ietf.org/rfc/rfc2045.txt), “Multipurpose Internet Mail Extensions, (MIME) Part
One: Format of Internet Message Bodies”, we will find all the answers, though we will have to wade
through a lot of cryptic descriptions and lots of jargon, making some parts read as though it was a
foreign language. Or, we could let me read them, and then simply let me show how to use them.
From our example, above, we can see 6 fields defined: “From”, “To”, “Date”, Subject”, “Content-
Type” and “Content-Transfer-Encoding”. Each is followed by a colon and a space “: ” to delimit the
end of the fields, separating each from its data. From, To, and Subject are easy enough. The Date field
stores the date and time as “d mmm, yyyy HH:mm:ss zzz”. That is, day, month name, year, then hours,
minutes, seconds, followed by the Zulu-time offset (number of hours against Greenwich, England time).
 Page 423

The data for a field can consist of one or more parameters. If there is more than one, they are separated
from each other by a semicolon “;”. A parameter ending in vbCrLf marks the end of the parameters.
Between parameters, intervening white space (non-displayable characters), such as spaces, tabs,
Carriage Returns, Linefeeds, and such, are not counted as data. If the line ends in a semicolon “;”, then
the next line is considered a continuation of the previous line. For example, we may often see this:
Content-Type: text/plan; ◄▬▬▬▬▬▬ ";" indicates more parameters defined, so this is not the end of the field parameters
Name=API32.TXT ◄▬▬▬▬▬▬ continuations of previous lines are led by white space (non-printable characters: spaces, tabs, etc.)

The above is considered one line of data. It is clear that the next line is a continuation because the
previous line ended with a semicolon, indicating at lease one more parameter is pending.
We may also notice in message bodies and the like, something like this (part of my signature block):
This is a test ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line ended normally
◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line consisted only of vbCrLf
=20 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line ended with blank space, so it is encoded
◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line consisted only of vbCrLf
=20 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line ended with blank space, so it is encoded
◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line consisted only of vbCrLf
David Ross Goben
◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line consisted only of vbCrLf
\|/
~ ~
(@ @)=20 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ a space preceded end of line, so it is encoded
--oOO-{_}-OOo------------------------------------------------------------= ◄▬▬▬▬▬▬▬▬▬▬▬▬ non-breaking line continuation tag (=)
------- ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ continuation of previous line
--- ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ data simply chopped off due to my 80-char plain text limit
Author of "A Gnostic Cycle: Exploring the Origin of Christianity"

Notice the “=20” Hex-Tags. These represent spaces prior to an end of line. This means that between the
text and my signature are 4 lines, two of which have a space in them. Notice that there was a space down
by “@)”, as well. This can all be easily cleaned up by the DecodeQuotedPrintable method.
Notice on the long, dashed line that at 73 characters there was an email-impose end of line, tagged by
“=”, and followed by a vbCrLf. What this means is that when this message is processed, the line will
actually continue on the next line, where we find 7 more dashes. This equals an 80-character line once
we strip off the “=” and vbCrLf. The final 3 dashes are lost in the translation, being that I have set the
line limit of plain text data at 80 characters in Outlook, so we have to live with this overflow.
Now, consider the following abbreviated email (it was actually quite long, even with a small image
(302x244), but it contains some interesting differences from previous raw emails we have examined):
From: "Mercedes Silver" Mercedes_silver@80micro.com ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ From Field. (these 4 fields can be in any order)
To: <david.ross.goben@gmail.com> ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ To Field. (these 4 fields can be in any order)
Subject: Check out this scary thing! ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Subject Field. (these 4 fields can be in any order)
Date: Sun, 27 Feb 2011 12:31:07 -0500 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Date Field (d mm, yyyy HH:MM:SS zzz). (these 4 fields can be in any order)
Message-ID: <8883802E7DC3402AA08372C008416D95@David> ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Obvious email extension as allowed by RFC 2045. Can be ignored.
MIME-Version: 1.0
Content-Type: multipart/mixed; ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ If a semicolon ends a line, the next line will be a continuation.
boundary="----=_NextPart_000_0000_01CBD67A.365E6060" ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Notice we have a boundary declaration set within quotation marks.
X-Priority: 3 (Normal) ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Obvious email extension as allowed by RFC 2045. Can be ignored.
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook, Build 1234.5.6789
Importance: Normal

------=_NextPart_000_0000_01CBD67A.365E6060 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Begin next block (note the 2 extra dashes at the beginning).
Content-Type: multipart/alternative;
boundary="----=_NextPart_001_0001_01CBD67A.365E8770" ◄▬▬▬▬▬ Notice we have ANOTHER boundary declaration, which is used here to wrap message
types, both the main (plain), and alternate (HTML).

------=_NextPart_001_0001_01CBD67A.365E8770 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Beginning of main body message area.


Content-Type: text/plain; ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Main body is plain text
charset="us-ascii"
Content-Transfer-Encoding: 7bit ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Encoding, or rather, the lack thereof in the case of 7bit.
◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Notice that a Content-Transfer-Encoding line is followed by a REQUIRED blank line.
Picture image is attached. ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Beginning of plain text message.

Mercedes

------=_NextPart_001_0001_01CBD67A.365E8770 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ End of plain text message, start of alternate view/attachment.


Content-Type: text/html; ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ alternate view/attachment data is HTML.
charset="us-ascii" ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ no filename, so what follows is an alternate view.
Content-Transfer-Encoding: quoted-printable ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ indicates text could have control conversions (though HTML usually covers most).
The email is so short, there will not be any actual control code translation.
<html>

<body>
 Page 424

Picture image is attached.</p>

<p>
<p>
&nbsp; Mercedes<p> ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ HTML forcing a leading space by inserting a non-breaking space.

</body>

</html>

------=_NextPart_001_0001_01CBD67A.365E8770-- ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ End of HTML alternate view.

------=_NextPart_000_0000_01CBD67A.365E6060 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Beginning of second boundary data.


Content-Type: image/jpeg; ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ data is a binary image formatted as Jpeg.
name="David Goben.JPG" ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ filename for data (the filename indicates it is an attachment).
Content-Transfer-Encoding: base64
Content-Disposition: attachment; ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ email extension as allowed by RFC 2045. Can be ignored.
filename="David Goben.JPG" ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ optional disposition field, which specifically indicates an attachment, plus a
specific filename (some emails do not have a disposition, so depend on name, above)
/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a
HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy ◄▬▬▬▬ Base64 Binary image data.
MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAD0AS4DASIA
◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This blank zone is ACTUALLY filed with about a page and a half of encoding...
+4v9XWWZt7COWIKD7Dy6p/8ADPPhL/oI63/3/i/+N0UV3x2RxS3D/hnnwkP+Yjrf/f6L/wCN0f8A
DPPhL/oI63/3/i/+N0UVRI5P2e/CKNk32ssPQzx4P5R11s/w+0mbw8dESe8htTH5W6JkD4xjqVI/
Siis6m6Kich/wzz4S/6COt/9/ov/AI3R/wAM8+Ev+gjrf/f+L/43RRWgjbtPhDoFnpI01LzU2gD7
8tLHuz/3xVm/+F2iaikazXWoAR9Nkif1SiivMlGPtL26np0JyUVZn//Z

------=_NextPart_000_0000_01CBD67A.365E6060-- ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ End of second boundary data (note the 2 extra dashes at the beginning and end).

Notice that an email can have multiple boundaries defined. Though allowed, I do
not understand why, because one would have served this email, but for some
reason Microsoft Outlook chose to separate the main body and the alternate view
from the attachments with different boundaries, which are grouped by the outer
blocking. This is why some simple email readers have trouble parsing Outlook email, and the fact that
they pack them full of self-defined extensions that are not specified by the RFC 2045 document, though
they may actually be useful and help define a future specification, which were last updated in 1996.
Further, notice that boundary definitions can be optionally enclosed within quotation marks, and that a
boundary will be unique to each email, being unlike any other line in all of its other text.
Another thing to notice is the Content-Disposition line. This simply identifies the data block as an
attachment. But we already know that from the previous Content-Type field. The only thing that RFC
2045 has to say about this field is that it should be ignored. So we will ignore it.
Issues that we may encounter regarding the Content-Type field will be in interpreting its fields. The first
part is simple enough, if we refer back to our tables, but notice that the first one is different if we have
attachments (attachments and/or alternate views). When the email has attachments of any sort, the first
parameter for Content-Type is supposed to be “multipart/alternative”, which indicates that we have
attachments and/or alternate views. However, some email processors, such as Outlook, also can specify
“multipart/mixed” if they feature, say, alternate views and attachments. This is an allowed user-defined
extension. But we can still use it. We just need to check its left side for “multipart/”. If “multipart/”
leads, then we can be sure that the second parameter will specify a boundary definition string (which
Microsoft Outlook, but few others, embraces within quotation marks).
Because some email applications, such as Outlook, use multiple boundaries, we simply need to be sure
to absorb each one, and to always test for each one, but we can treat any as though they were the same
boundary, simplifying our processing. That is, when any of the defined boundaries are encountered, then
we will simply assume we have hit a boundary and think nothing of it beyond that. However, as we
check for boundaries, we simply need to check each line for containing the boundary using an Instr
function, because we really do not need to concern ourselves with the auxiliary “--” leaders or trailers
that we will often find decorating or enhancing a boundary specification within the body of the email.
 Page 425

If the first part of the Content-Type specifies a data type, such as “application/rtf” or “text/plain” or
“image/jpeg”, the second parameter will either be “name=”, or it will be a display format, such as
“charset=us-ascii”, or something similar, depending on the global region. We will only concern
ourselves when this second parameter begins with “name=”, which will only happen when we are in an
attachment, and the data can be saved to a file, using the specified filename as a default, but only if the
user chooses to save it. Otherwise, the data is either the main message, or it is an alternate view.
Issues that we may encounter with regard to reading the Content-Transfer-Encoding field will be how
their parameter tags are actually embedded in the emails, as opposed to how we specified them. This is
because they cannot be declared in an enumeration the same way the RFC 2045 document dictates that
they must be provided. Consider the following table:
How WE Declare Content-Transfer-Encoding Value How RFC 2045 specifies they are Shipped/Received with Email
QuotedPrintable quoted-printable
SevenBit 7bit
Base64 base64

When we read email, look to the data in the right column for what we will find. If it is not of these
values, we are instructed to assume the encoding is “base64”, regardless of what the Content-Transfer-
Encoding field specifies (of course, in your company, we may have a proprietary encryption format that
we do not want competitors to read. In that case, we could specify that encryption specification here and
say “so, what?” if someone else’s email reader cannot make heads or tails of it).
Easily Extracting the Component Parts from an Email File.
The Retreive method of POP3 takes a POP3Message object that already has its MailID member set to
the ID number of the mail that we want retrieved, which had previously been initiated by the POP3 class
List method, and the ByteCount member of POP3Message is set to the length of the desired email,
Retreive will return this POP3Message object with its Message member filled with an entire raw email;
header, attachments, et al. It is the complete message as it is stored on the server (though, as previously
noted, any lone vbLf codes contained within it will have been replaced by vbCrLf).
Of course, a header contains many more fields than those shown in the above email sample, but the
important header fields, to us at least, are the To, From, Date, and Subject fields. To extract this
important information, the main message body, plus all attachments and alternate views, simply provide
the Message parameter of a POP3Message object to the GetEmailInfo method, defined below.
Provided this POP3Message.Message member, the GetEmailInfo method will break that message data
up and distribute it in an EmailInfo class object, which in turn will contain one, or several EmailItem
class object members, depending on the number of Alternate Views and/or Attachments included in the
message. Both the Alternate Views and the Attachments are maintained within ArrayLists of type
EmailItem that allow the arrays to store multiples of each. We can check these ArrayList objects, named
AlternateViews and Attachments within the EmailInfo object, and if their Count member is non-zero, we
can extract each EmailItem object from their Items collection and inspect them as we see fit. We will
then have to decode each EmailItem ContentBody member based upon what is set to its
ContentEncoding member; quoted-printable, 7bit, or base64. If it is quoted-printable, we will need
to run the ContentBody member through the DecodeQuotedPrintable method. Remember, if the text
data begins with =00, we will have to run it through twice. If it is 7bit, we will need do nothing extra. If
it is base64, we will need to decode the ContentBody member to a Byte array or to a string through
System.Convert.FromBase64String as needed, as outlined earlier, depending on what the ContentType
member specifies, such as image/jpeg, text/rtf, text/html, or whatever.
There is one EmailItem object, named MessageBody, that is reserved for the main body of the email.
The Remainder of the EmailInfo object stores the e-mail’s From, To, Date, and Subject members.
 Page 426

What follows is the GetEmailInfo method, which returns an EmailInfo class instance containing FromData,
ToData, DateData, SubjectData, MessageBody, AlternateView and Attachment members. The listing also
include its two required support classes, EmailItem and EmailInfo:
'*******************************************************************************
' Function Name : GetEmailInfo
' Purpose : Break email down into its component parts.
' :
' Returns : EmailInfo object with component parts of email broken down.
' :
' NOTES: This method uses classes EmailItems and EmailInfo. The Message Body, and each AlternnateView or Attachment
' are contained within EmailItem objects within the EmailInfo object.
'
' An EmailItem contains fields for FROM, TO, SUBJECT, Content-Type, a flag indicating if the ContentTypeData is
' a filename or if it is text formatting, content-transfer-encoding data, and the raw encoded, data, whether it
' is a message or binary information. If the content-transfer encoding is set to "base64", the data should be
' decoded using the DecodeBase64() method. If it is "quoted-printable", the data should be decoded using
' DecodeQuotedPrintable(). If it is "7bit", it is 7-bit data and does not need to be decoded.
'*******************************************************************************
Public Shared Function GetEmailInfo(ByVal MailMessage As String) As EmailInfo
Dim Info As New EmailInfo 'structure to hold breakdown of email
Dim Ary() As String = Split(MailMessage, vbCrLf) 'break full email into lines
Dim Idx As Integer = 0 'index into Ary()
Dim MX As Integer = UBound(Ary) + 1 'find end if list+1
Dim Boundaries As New List(Of String) 'boundary definitions

Dim IsMultiPart As Boolean = False 'true if we have multiple parts


Dim SeekingEncoding As Boolean = False 'true if we are looking for encoding
Dim BuildingDataBlock As Boolean = False 'true if we are building a data block
Dim HaveMessageBody As Boolean = False 'true if we have the message body defined

Dim ContentType As String = Nothing 'hold last-defined Content Type


Dim ContentTypeIsName As Boolean = False 'true of Content Type specified a file
Dim ContentTypeData As String = Nothing 'if block is an attachment
Dim ContentEncoding As String = Nothing 'hold last-defined Content Transfer Encoding
Dim ContentBody As String = Nothing 'block data accumulator
'-----------------------------------------------------------
Dim Inheader As Integer = 4 'flag for gathering To, From, Date, Subject
Do
Dim S As String = Ary(Idx) 'grab a line of data from the email
'
' check for important header items
'
If CBool(Len(S)) AndAlso CBool(Inheader) Then 'if we are currently in the header...
Dim I As Integer = InStr(S, ": ") 'find field delimiter
If CBool(I) Then 'found one?
If Right(S, 1) = ";" Then 'line continues?
Idx += 1 'yes, so bump index
S &= Ary(Idx).Trim(Chr(9), " "c) 'append next line but remove tabs and spaces
End If
Select Case LCase(Left(S, I + 1)) 'yes, check for one of 4 fields
Case "from: " 'Found FROM field
Info.FromData = Trim(Mid(S, I + 1)) 'stuff to structure
Inheader -= 1 'drop 1 from flag
Case "to: " 'Found TO field
Info.ToData = Trim(Mid(S, I + 1)) 'stuff to structure
Inheader -= 1 'drop 1 from flag
Case "date: " 'Found DATE field
Info.DateData = Trim(Mid(S, I + 1)) 'stuff to structure
Inheader -= 1 'drop 1 from flag
Case "subject: " 'Found SUBJECT field
Info.SubjectData = Trim(Mid(S, I + 1)) 'stuff to structure
Inheader -= 1 'drop 1 from flag
End Select
End If
If Not CBool(Inheader) Then 'if InHeader flag is now zero...
SeekingEncoding = True 'start looking for a Content-Transfer-Encoding field
S = Nothing 'purge current data
End If
End If
'-------------------------------------------------------
' check for boundaries
'-------------------------------------------------------
If CBool(Len(S)) AndAlso CBool(Boundaries.Count) Then 'check any defined boundaries
For Idy As Integer = 0 To Boundaries.Count - 1
If CBool(InStr(S, Boundaries.Item(Idy), CompareMethod.Text)) Then
If BuildingDataBlock Then
Dim Itm As New EmailItem 'create a new item
Itm.ContentType = ContentType 'store content type
Itm.ContentTypeData = ContentTypeData 'save filename or character set
Itm.ContentTypeDataIsFilename = ContentTypeIsName 'save flag indicating if Attachment
Itm.ContentEncoding = ContentEncoding 'store encoding
Itm.ContentBody = ContentBody 'store data
 Page 427

ContentBody = Nothing 'reset accumulator


If HaveMessageBody Then 'already have a message body?
If ContentTypeIsName Then 'if an attachment
Info.Attachments.Add(Itm) 'add an attachment
Else 'otherwise an alternate view
Info.AlternateViews.Add(Itm)
End If
Else
Info.MessageBody = Itm 'else stuff new item to message body
HaveMessageBody = True 'indicate we now have a message body
End If
ContentTypeData = Nothing 'reset filename/charset
BuildingDataBlock = False 'turn off building flag
End If
SeekingEncoding = True 'turn block seeing on again
S = Nothing 'purge current data
Exit For
End If
Next
End If
'-------------------------------------------------------
' build data block
'-------------------------------------------------------
If BuildingDataBlock Then
ContentBody &= S & vbCrLf 'add a line to content data
End If
'-------------------------------------------------------
' if seeking encoding
'-------------------------------------------------------
If CBool(Len(S)) AndAlso SeekingEncoding Then 'are we seeking TCE?
Dim I As Integer = InStr(S, ": ") 'yes, check for field delimiter
If CBool(I) Then 'did we find one?
Select Case LCase(Left(S, I + 1)) 'yes, check for types
'*******************************************************
Case "content-type: " 'Content type?
ContentType = Mid(S, I + 1).Trim(Chr(9), " "c) 'yes, so grab data and trim tabs and spaces
If Right(S, 1) = ";" Then 'more to add?
Idx += 1 'yes, so bump index
ContentType &= Ary(Idx).Trim(Chr(9), " "c) 'grab next line and trim tabs and spaces
End If
ContentTypeIsName = False 'init flag specifying a file as false
Dim sbAry() As String = Split(ContentType, ";") 'now check the content type data
ContentType = sbAry(0) 'keep first part for ContentType
If StrComp(Left(sbAry(0), 10), "multipart/", CompareMethod.Text) = 0 Then
'multipart, so grab second parameter (boundary definition), and strip any quotes
Dim Bnd As String = Trim(Mid(sbAry(1), InStr(sbAry(1), "=") + 1)).Replace("""", Nothing)
Boundaries.Add(Bnd) 'and add a boundary
ElseIf StrComp(Left(sbAry(1), 5), "name=", CompareMethod.Text) = 0 Then
ContentTypeIsName = True 'attachment if a filename spec'd (otherwise a view)
sbAry = Split(sbAry(1), "=") 'multipart, so grab second parameter
'get second part of second parameter (filename definition)
ContentTypeData = sbAry(1).Trim().Replace("""", Nothing) 'strip any quotes
Else
ContentTypeData = sbAry(1) 'AlternateView, so stuff display character set
If Len(Trim(Ary(Idx + 1))) = 0 Then 'if next line blank, assume ContentEncoding=base64
ContentEncoding = "base64" 'is blank, so assume base64
SeekingEncoding = False 'turn off seeking flag
BuildingDataBlock = True 'turn on building data block flag
Idx += 1 'bump to skip the blank line
End If
End If
'*******************************************************
Case "content-transfer-encoding: "
ContentEncoding = Mid(S, I + 1).Trim(Chr(9), " "c) 'yes, so grab data
SeekingEncoding = False 'turn off seeking flag
BuildingDataBlock = True 'turn on building data block flag
Idx += 1 'bump to skip required following blank line
End Select
End If
End If
Idx += 1 'bump array index
Loop While Idx < MX
'-----------------------------------------------------------
'some emails do not define borders, so a message body is not created, so force it!
'-----------------------------------------------------------
If Info.MessageBody Is Nothing Then 'if the email lacks a border for message data...
Dim Itm As New EmailItem 'we will create it now, so create a new EmailItem...
Itm.ContentType = ContentType 'store content type...
Itm.ContentTypeData = ContentTypeData 'save filename or character set...
Itm.ContentTypeDataIsFilename = ContentTypeIsName 'save flag indicating if Attachment...
Itm.ContentEncoding = ContentEncoding 'store encoding...
Itm.ContentBody = ContentBody 'store data...
Info.MessageBody = Itm 'and finally set the new item to the message body
End If
Return Info 'return with filled data block
 Page 428

End Function

'***********************************************************************************
'***********************************************************************************
'***********************************************************************************

'*******************************************************************************
' Class Name : EmailItem (used by EmailInfo class)
' Purpose : Stores structure of an email block
'*******************************************************************************
Public Class EmailItem
Public ContentType As String = Nothing 'CONTENT-TYPE data
Public ContentTypeData As String = Nothing 'filename or text encoding
Public ContentTypeDataIsFilename As Boolean = False 'True if ContentTypeData specifies a filename
Public ContentEncoding As String = Nothing 'CONTENT-TRANSFER-ENCODING data
Public ContentBody As String = Nothing 'raw data of block
End Class

'***********************************************************************************
'***********************************************************************************
'***********************************************************************************

'*******************************************************************************
' Class Name : EmailInfo (used by GetEmailInfo method)
' Purpose : Store component parts of an Email
'*******************************************************************************
Public Class EmailInfo
Public FromData As String = Nothing 'FROM:
Public ToData As String = Nothing 'TO:
Public DateData As String = Nothing 'DATE:
Public SubjectData As String = Nothing 'SUBJECT:
Public MessageBody As EmailItem 'contents of message body
Public AlternateViews As New List(Of EmailItem) 'list of alternate views
Public Attachments As New List(Of EmailItem) 'list of attachments
End Class

Compiling Everything into a Class Library.


My email sending methods, BrainDeadSimpleEmailSend, QuickiEMail, and
SendEMail, I embed in a class wrapper named SMTP. To do this is really
easy (see the full listing later, if you want to just copy it).
I just select the menu options Project / Add Class..., and type SMTP into the name field. Do not worry
about it maintaining the default “.vb” file extension. It will be re-added if we do not type it in. This
creates the class shell:
Public Class SMTP

End Class

I then decorate its heading with essential references and identification, being sure to include the imports
I had needed for the three methods, like so:
Option Strict On
Option Explicit On
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' SMTP - Copyright © 2011 - 2021 by David Ross Goben.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imports System.Net
'-------------------------------------------------------------------------------
' Class Name : SMTP
' Purpose : SMTP Static Interface Class
'-------------------------------------------------------------------------------
Public NotInheritable Class SMTP
░░░░░░░░░░░░░░░░░ ◄▬▬▬▬▬▬▬▬▬▬ Class Body is here
End Class

I finally copy my three methods, BrainDeadSimpleEmailSend, QuickiEMail, and SendEMail, within the
SMTP class body.
Next, we modify the three method’s declarations by adding Shared after Public. For example, “Public
Shared Sub BraindeadSimpleEmailSend(” and “Public Shared Function SendEmail(”.
And that is all there is to creating the class. Easy, breezy, nice ’n’ easy!
 Page 429

Notice that we do not need to, and cannot instantiate an instance of the SMTP class. First of all, because
it is a Non-inheritable Class (note the NotInheritable verb in the class declaration). What this means is
that we can just select the class name, add a dot, and then specify the method we want to invoke.
For example, to send an email, I can do so simply by using code similar to the following:
'build a message and send an email
Dim msg As String = "This is a test" & vbCrLf & "This is a second test" & vbCrLf
SMTP.SendEMail("mercedes_silver@80micro.com", "david.ross.goben@gmail.com", "Test", msg, False, "smtp.80micro.com")

I will also take all my utilities and add them to a new class file named Utilities.vb. And like with the
SMTP class utilities, we will not have to instantiate a Utilities class (this class will be covered, shortly).
When we build our class library, reference it from our application, and then import the class library into
our project file, we can still access a method within the SMTP class by simply specifying
SMTP.methodname, or Utilities.methodname without having to instantiate either class. As previously
stated, and to reiterate, we will also have to change all the Public methods defined within the SMTP and
Utilities classes to Public Shared methods.
It is not quite as easy for our POP3 class. This is because our POP3 class inherits from
System.Net.Sockets.TcpClient, so we will need to in turn instantiate an instance of our POP3 class. But that
is no big deal, because so far, that is exactly what we have been doing. The only difference is, with the class
library we are about to build, that we would no longer be referencing a locally compiled class file.
But how would we reference this class library?
No problem. It is as easy as stepping on an upturned rake and rearranging our face with its handle.
But before we get to those simple steps, how about we actually build this class library, so we can quickly
prove that these steps are as simple as I claim them to be?
We have already defined our SMTP and POP3 class files (their complete listings are at the end of this
article). But we also want to create a Utilities class. After all, we have already been talking about the poor
thing as though it already exists.
We would create the Utilities class exactly the same way as we did the SMTP class. We would add a new
class and name it Utilities. We would then initially tattoo its new class shell like this:
Option Strict On
Option Explicit On
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' Utilities - Copyright © 2011 - 2021 by David Ross Goben.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imports System.Text

Public NotInheritable Class Utilities


░░░░░░░░░░░░░░░░░ ◄▬▬▬▬▬▬▬▬▬▬ Class Body is here
End Class

Next, we would add the following items to this Class file, all of which we have explored earlier, during the
course of this article/novella (it is also fully listed at the end of this article):
Method/Enum/Class Item Description
DecodeBase64ToStr() Decode Base64-encoded files to their original format and return a string.
DecodeBase64ToBytes() Decode Base64-encoded files to their original format and return a Byte Array.
DecodeQuotedPrintable() Method to clean typical control translations, or all of them. This should be invoked for all data coded Quoted-Printable.
DecodeHexDec() Decode Base16-encoded files to their original format, where each two characters prepresents a binary Byte.
TextNeedsEncoding() Determine if HTML text, Rich Text, or Plain Text requires 8-bit code translation to 7-bit Quoted-Printable tags.
Force7BitHtml() Method to convert 8-bit code in an HTML message to 7-bit.
ForceQuotedPrintable() Method to force 8-bit code in a text message to 7-bit, without losing any data.
QConvertHTML2Text() Short-Form Convert HTML formatted text to plain text.
ConvertHTML2Text() Convert HTML formatted text to plain text.
Enum MediaTypes Enumeration used by GetMediaType.
GetMediaType() Provide easy access to System.Net.Mime.MediaTypes text.
Enum TransferEncodings Structure used by GetTransferEncoding
GetTransferEncoding() Provide easy access to System.Net.Mime.TransferEncoding data.
GetEmailInfo() Break email down into its component parts.
Class EmailItem (used by GetEmailInfo)
Class EmailInfo (used by GetEmailInfo)
 Page 430

Once we have all three classes fully defined (and there are no errors laughing at us), we should further wrap
these three classes within a Class Library project named VBNetEmail, and build them to a DLL for easy
access by our application. This is incredibly easy to do.

Building the VBNetMail Class Library.


To incorporate our three classes into a Class Library project named VBNetEmail is really easy. The first
thing to do is to save off and close our project that contains the classes SMTP, POP3, and Utilities, and
remember where we saved them! Copy them from the listings at the end of this article, if we need to.
● Now, create a new Class Library project named VBNetEmail.
● Once created, in Project Explorer, we will see a Class
File, Class1.vb. Simply right-click it and choose Delete.
We do not need it; we already have the class files we
require: SMTP.vb, POP3.vb, and Utilities.vb.
● Next, select the menu options Project / Add Existing Item... Browse
to the location where we saved our POP3, SMTP, and Utilities class
files, select them, then click the Add button. The three class files
should then appear in our Project Explorer.
(We now have all we need to compile our class library.)
● Select the menu options Build / Build VBNetEmail (be sure that we
are building a Release version). We will end up with a DLL file named
VBNetEmail.dll (look within our Projects folder,
Projects\VBNetEmail\VBNetEmail\obj\Release, for it). I tend to test
the class files within test projects, though later, once proven, I copy
them to a class library. I even do this for my related utilities, adding
them to a Utilities class, such as those used within this article.

Accessing Your New VBNetEmail Class Library DLL From Another Project
Once we have built the VBNetEmail.dll file, we can add it to other projects in just a few easy steps.
● In the Project Properties for our new application that will use the VBNetEmail
library, make sure the References tab is selected, and choose the ADD button.
● Select the Browse Tab, and then browse to our VBNetEmail Class Library project
(we should find ourselves initially within our current project). Choose the Up
One Level button to back out of any current project; it is the folder icon with a
curved green up arrow:
● Drill down into the VBNetEmail project to find the Obj folder, and open it.
● Within the Obj folder, select and open the Release folder.
● Within the Release folder, click the VBNetEmail.dll file and then select the OK
button (if we have one-click enabled, OK may be auto-selected).
● We will now find a reference to VBNetEmail listed in our References list. For
subsequent projects, we can simply choose the Recent Tab and very quickly
select the VBNetEmail.dll file.
● Finally, in the project file that we will need to access the classes in, place the line
“Imports VBNetEmail” above the first class declaration at the top of the file.
We can now access our classes as though they were included in our project (well, actually, they are).
Remember, the methods for the SMTP and Utilities classes can be accessed by simply specifying SMTP or
Utilities, hitting the dot, and selecting the method we want to use. To use the Inbound POP3 email class, we
must instantiate an instance of the class, and then access its methods through that instance.
We are now ready for some serious email processing.
 Page 431

The Complete SMTP Class File.


Option Strict On
Option Explicit On
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' SMTP - Copyright © 2011 - 2021 by David Ross Goben.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imports System.Net
'***********************************************************************************
'***********************************************************************************
' Class Name : SMTP
' Purpose : SMTP Static Interface Class
'***********************************************************************************
'***********************************************************************************
Public NotInheritable Class SMTP
'*******************************************************************************
' Function Name : BrainDeadSimpleEmailSend
' Purpose : Send super simple email message (works with most SMTP servers)
'-------------------------------------------------------------------------------
'NOTES: strFrom : Full email address of who is sending the email. ie, David Dingus <daviddingus@att.net> or daviddingus@att.net
' strTo : Full email address of who to send the email to. ie, "Bubba Dingus" <bob.dingus@cox.com> or bob.dingus@cox.com
' strSubject: Brief text regarding what the email concerns.
' strBody : text that comprises the message body of the email.
' smtpHost : This is the email host you are using for sending emails, such
' : as "smtp.comcast.net", "authsmtp.juno.com", etc.
'*******************************************************************************
Public Shared Sub BrainDeadSimpleEmailSend(ByVal strFrom As String,
ByVal strTo As String,
ByVal strSubject As String,
ByVal strBody As String,
ByVal smtpHost As String)
Dim smtpEmail As New Mail.SmtpClient(smtpHost) 'create new SMTP client using TCP Port 25
smtpEmail.Send(strFrom, strTo, strSubject, strBody) 'send email
End Sub

'*******************************************************************************
' Function Name : QuickiEMail
' Purpose : Send a simple email message (but packed with a lot of muscle)
'-------------------------------------------------------------------------------
'NOTES: strFrom : Full email address of who is sending the email. ie, David Dingus <daviddingus@att.net> or daviddingus@att.net
' strTo : Full email address of who to send the email to. ie, "Bubba Dingus" <bob.dingus@cox.com> or bob.dingus@cox.com
' strSubject: Brief text regarding what the email concerns.
' strBody : text that comprises the message body of the email.
' smtpHost : This is the email host you are using for sending emails, such
' : as "smtp.gmail.com", "smtp.comcast.net", "authsmtp.juno.com", etc.
' smtpPort : TCP Communications Port to use. Most servers default to 25, though 465 (SSL) or 587 (TLS) are becoming popular.
' usesSLL : If this value is TRUE, then use SSL/TLS Authentication protocol for secure communications.
' SSLUsername: If usesSLL is True, this is the username to use for creating a credential. Leave blank if the same as strFrom.
' SSLPassword: If usesSLL is True, this is the password to use for creating a credential. If this field and SSLUsername
' : are blank, then default credentials will be used (but this only works on local, intranet servers).
' SSLDomain : If creating a credential when a specific domain is required, set this parameter, otherwise, leave it blank.
'*******************************************************************************
Public Shared Function QuickiEMail(ByVal strFrom As String,
ByVal strTo As String,
ByVal strSubject As String,
ByVal strBody As String,
ByVal smtpHost As String,
Optional ByVal smtpPort As Integer = 25,
Optional ByVal usesSSL As Boolean = False,
Optional ByVal SSLUsername As String = Nothing,
Optional ByVal SSLPassword As String = Nothing,
Optional ByVal SSLDomain As String = Nothing) As Boolean
Try
Dim smtpEmail As New Mail.SmtpClient(smtpHost, smtpPort) 'create new SMTP client
smtpEmail.EnableSsl = usesSSL 'true if SSL Authentication required
If usesSSL Then 'SSL authentication required?
If Len(SSLUsername) = 0 AndAlso Len(SSLPassword) = 0 Then 'if both SSLUsername and SSLPassword are blank...
smtpEmail.UseDefaultCredentials = True 'use default credentials
Else 'otherwise, we must create a new credential
If Not CBool(Len(SSLUsername)) Then 'if SSLUsername is blank, use strFrom
smtpEmail.Credentials = New NetworkCredential(strFrom, SSLPassword, SSLDomain)
Else
smtpEmail.Credentials = New NetworkCredential(SSLUsername, SSLPassword, SSLDomain)
End If
End If
End If
smtpEmail.Send(strFrom, strTo, strSubject, strBody) 'send email using text/plain content type and QuotedPrintable encoding
Catch e As Exception 'if error, report it
MsgBox(e.Message, MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation, "Mail Send Error")
Return False 'return a failure flag
End Try
Return True 'if no error, then return a success flag
End Function
 Page 432

'*******************************************************************************
' Function Name : SendEMail
' Purpose : Send a more complex email message
'-------------------------------------------------------------------------------
'NOTES: strFrom : Full email address of who is sending the email. ie, David Dingus <daviddingus@att.net>
' strTo : Full email address of who to send the email to. ie, "Bubba Dingus" <bob.dingus@cox.com>
' : If multiple recipients, separate each full email address using a semicolon (;)
' strSubject: Brief text regarding what the email concerns.
' strBody : text that comprises the message body of the email. May be raw text or HTML code.
' IsHTML : True if the strBody data is HTML, or the type of data that would be contained within an HTML Body block.
' smtpHost : This is the email host you are using for sending emails, such
' : as "smtp.gmail.com", "smtp.comcast.net", "authsmtp.juno.com", etc.
' AltView : A System.Net.Mail.AlternateView object, such as Rich Text or HTML.
' : If need be, set AltView.ContentType.MediaType and AltView.TransferEncoding to properly format the AlternateView.
' : For example: AltView.ContentType.MediaType = Mime.MediaTypeNames.Text.RichText
' : AltView.TransferEncoding = Mime.TransferEncoding.Base64
' StrCC : Send "carbon copies" of email to this or these recipients.
' : If multiple recipients, separate each full email address using a semicolon (;)
' strBcc : Blind Carbon Copy. Hide this or these recipients from view by others.
' : If multiple recipients, separate each full email address using a semicolon (;)
' strAttachments: A single filepath, or a list of filepaths to send to the recipient.
' : If multiple attachments, separate each filepath using a semicolon (;) (C:\my data\win32.txt; c:\jokes.rtf)
' : The contents of the attachments will be encoded and sent.
' : If you wish to send the attachment by specifying content type (MediaType) and content transfer encoding
' : (Encoding), then follow the attachment name with the MediaType and optional encoding (default is
' : application/octet-stream,Base64) by placing them within parentheses, and separated by a comma. For example:
' : C:\My Files\API32.txt (text/plain, SevenBit); C:\telnet.exe (application/octet-stream, Base64)
' : Where: The MediaType is determined from the System.Net.Mime.MediaTypeNames class, which
' : can specify Application, Image, or Text lists. For example, the above content type,
' : "text\plain", was defined by acquiring System.Net.Mime.MediaTypeNames.Text.Plain.
' : The second parameter, Encoding, is determined by the following the values specified with the
' : System.Net.Mime.TrasperEncoding enumeration:
' : QuotedPrintable (acquired by System.Net.Mime.TransferEncoding.QuotedPrintable.ToString)
' : Base64 (acquired by System.Net.Mime.TransferEncoding.Base64.ToString)
' : SevenBit (acquired by System.Net.Mime.TransferEncoding.SevenBit.ToString)
' smtpPort : TCP Communications Port to use. Most servers default to 25.
' usesSLL : If this value is TRUE, then use SSL/TLS Authentication protocol for secure communications.
' SSLUsername: If usesSLL is True, this is the username to use for creating a credential. Leave blank if the same as strFrom.
' SSLPassword: If usesSLL is True, this is the password to use for creating a credential. If this field and SSLUsername
' : are blank, then default credentials will be used (but this only works on local, intranet servers).
' SSLDomain : If creating a credential when a specific domain is required, set this parameter, otherwise, leave it blank.
'*******************************************************************************
Public Shared Function SendEMail(ByVal strFrom As String,
ByVal strTo As String,
ByVal strSubject As String,
ByVal strBody As String,
ByVal IsHTML As Boolean,
ByVal smtpHost As String,
Optional ByVal AltView As Mail.AlternateView = Nothing,
Optional ByVal strCC As String = Nothing,
Optional ByVal strBcc As String = Nothing,
Optional ByVal strAttachments As String = Nothing,
Optional ByVal smtpPort As Integer = 25,
Optional ByVal usesSSL As Boolean = False,
Optional ByVal SSLUsername As String = Nothing,
Optional ByVal SSLPassword As String = Nothing,
Optional ByVal SSLDomain As String = Nothing) As Boolean

Dim Email As New Mail.MailMessage 'create a new mail message


With Email
.From = New Mail.MailAddress(strFrom) 'add FROM to mail message (must be a Mail Address object)
'-------------------------------------------
Dim Ary() As String = Split(strTo, ";") 'add TO to mail message (possible list of email addresses; separated each with ";")
For Idx As Integer = 0 To UBound(Ary)
If Len(Trim(Ary(Idx))) <> 0 Then .To.Add(Trim(Ary(Idx))) 'add each TO recipent (primary recipients)
Next
'-------------------------------------------
.Subject = strSubject 'add SUBJECT text line to mail message
'-------------------------------------------
.Body = strBody 'add BODY text of email to mail message.
.IsBodyHtml = IsHTML 'indicate if the message body is actually HTML text.
'.IsBodyHtml = True
'-------------------------------------------
If AltView IsNot Nothing Then 'if an alternate view of plaint text message is defined...
.AlternateViews.Add(AltView) 'add the alternate view
End If
'-------------------------------------------
If CBool(Len(strCC)) Then 'add CC (Carbon Copy) email addresses to mail message
Ary = Split(strCC, ";") '(possible list of email addresses, separated each with ";")
For Idx As Integer = 0 To UBound(Ary)
If Len(Trim(Ary(Idx))) <> 0 Then .CC.Add(Trim(Ary(Idx))) 'add each recipient
Next
End If
'-------------------------------------------
If CBool(Len(strBcc)) Then 'add Bcc (Blind Carbon Copy) email addresses to mail message
Ary = Split(strBcc, ";") '(possible list of email addresses; separated each with ";")
For Idx As Integer = 0 To UBound(Ary)
If Len(Trim(Ary(Idx))) <> 0 Then .Bcc.Add(Trim(Ary(Idx))) 'add each recipient (hidden recipients)
Next
End If
 Page 433

'-------------------------------------------
If CBool(Len(strAttachments)) Then 'add any attachments to mail message
Ary = Split(strAttachments, ";") '(possible list of file paths, separated each with ";")
For Idx As Integer = 0 To UBound(Ary) 'process each attachment
Dim attach As String = Trim(Ary(Idx)) 'get attachment data
If Len(attach) <> 0 Then 'if an attachment present...
Dim I As Integer = InStr(attach, "(") 'check for formatting instructions
If CBool(I) Then 'formatting present?
Dim Fmt As String 'yes, so set up format cache
Fmt = Mid(attach, I + 1, Len(attach) - I - 1) 'get format data
attach = Trim(attach.Substring(0, I - 1)) 'strip format data from the attachment path
Dim Atch As New Mail.Attachment(attach) 'create a new attachment
Dim fmts() As String = Split(Fmt, ",") 'break formatting up
For I = 0 To UBound(fmts) 'process each format specification
Fmt = Trim(fmts(I)) 'grab a format instruction
If CBool(Len(Fmt)) Then 'data defined?
Select Case I 'yes, so determine which type of instruction to process
Case 0 'index 0 specified MediaType
Atch.ContentType.MediaType = Fmt 'set media type to attachment
Case 1 'index 1 specifies Encoding
Select Case LCase(Fmt) 'check the encoding types and process accordingly
Case "quotedprintable", "quoted-printable"
Atch.TransferEncoding = Mime.TransferEncoding.QuotedPrintable
Case "sevenbit", "7bit"
Atch.TransferEncoding = Mime.TransferEncoding.SevenBit
Case Else
Atch.TransferEncoding = Mime.TransferEncoding.Base64
End Select
End Select
End If
Next
.Attachments.Add(Atch) 'add attachment to email
Else
'.Attachments.Add(New Mail.Attachment(attach)) 'add filepath (if no format specified, encoded in effiecient Base64)
Dim Atch As New Mail.Attachment(attach) 'add attachment to email
Atch.ContentType.MediaType = Mime.MediaTypeNames.Application.Octet 'returns string constant "application/octet-stream"
Atch.TransferEncoding = Mime.TransferEncoding.Base64 'set encoding to base64
.Attachments.Add(Atch) 'add attachment to email
End If
End If
Next
End If
End With
'-----------------------------------------------------------------------
'now open the email server...
Try
Dim SmtpEmail As New Mail.SmtpClient(smtpHost, smtpPort) 'create new SMTP client on the SMTP server
SmtpEmail.EnableSsl = usesSSL 'true if SSL Authentication required
If usesSSL Then 'SSL authentication required?
If Len(SSLUsername) = 0 AndAlso Len(SSLPassword) = 0 Then 'if both SSLUsername and SSLPassword are blank...
SmtpEmail.UseDefaultCredentials = True 'use default credentials
Else 'otherwise, we must create a new credential
If Not CBool(Len(SSLUsername)) Then 'if SSLUsername is blank, use strFrom
SmtpEmail.Credentials = New NetworkCredential(strFrom, SSLPassword, SSLDomain)
Else
SmtpEmail.Credentials = New NetworkCredential(SSLUsername, SSLPassword, SSLDomain)
End If
End If
End If
SmtpEmail.Send(Email) 'finally, send the email...
Catch e As Exception 'if error, report it
MsgBox(e.Message, MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation, "Mail Error")
Return False 'return failure flag
End Try
Return True 'return success flag
End Function
End Class

The Complete POP3 Class File.


Option Strict On
Option Explicit On
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' POP3 - Copyright © 2011 – 2021 by David Ross Goben.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'***********************************************************************************
'***********************************************************************************
' Class POP3
' POP3 Inbound Email Support
'
'This VB.NET Code was inspired by C# code originally written by Randy Charles Morin,
'author of KBCafe.com.
'
' I have optimized the heck out of the code to speed I/O and program execution,
' forcing a complete rewrite, I have added MANY language and POP3 enhancements,
' cleaned up a lot of clutter, and added Port and SSL support.
' Oh! And I include REAL comments.
'***********************************************************************************
'***********************************************************************************
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imports System.Net, System.Text
 Page 434

'-------------------------------------------------------------------------------
' Class Name : POP3
' Purpose : POP3 TCP Client Interface Class
'-------------------------------------------------------------------------------
Public Class POP3
Inherits Sockets.TcpClient 'this class shall inherit all the functionality of a TC/IP Client
'*******************************************************************************
Private Stream As Sockets.NetworkStream 'non-SSL stream object used for streaming data from source
Private UsesSSL As Boolean = False 'True if SLL authentication ia required
Private SslStream As Security.SslStream 'set to SSL stream supporting SSL authentication if UsesSSL is True
Private SslStreamDisposed As Boolean = False 'True if we disposed of SSL Stream object (if created)
Public LastLineRead As String = Nothing 'copy of the last response line read from the TCP server stream
'*******************************************************************************

'*******************************************************************************
' Sub Name : Connect (This is the first thing we do with a POP3 object)
' Purpose : Connect to the server using the Server, User Name, Password, and a if SSL authentication is required
' :
' Returns : Nothing
' :
' Typical TelNet I/O:
'telnet mail.domain.net 110 (submit)
'+OK POP3 mail.domain.net v2011.83 server ready
'USER myusername (submit)
'+OK User name accepted, password please
'PASS mysecretpassword (submit)
'+OK Mailbox open, 3 messages (the server locks and opens the appropriate maildrop)
'*******************************************************************************
Public Overloads Sub Connect(ByVal Server As String,
ByVal Username As String,
ByVal Password As String,
Optional ByVal InPort As Integer = 110,
Optional ByVal UseSSL As Boolean = False)

If Connected Then Disconnect() ' 'check underlying Boolean flag to see if we are presently connected,
' 'and if so, disconnect that session
UsesSSL = UseSSL 'set flag True or False for SSL authentication
Try
Connect(Server, InPort) 'now connect to the server via our base class (signature differs from our method)
Catch
Throw New POP3Exception("Cannot connect. Check Server, Port, Username or Password for errors")
Return
End Try
Stream = GetStream() 'before we can check for a response, we first have to set up a non-SSL stream
If UsesSSL Then 'do we also need to use SSL authentication?
SslStream = New Security.SslStream(Stream) 'yes, so build an SSL stream object on top of the non-SSL Network Stream
SslStream.AuthenticateAsClient(Server) 'add authentication as a client to the server
End If

If Not Me.CheckResponse() Then Exit Sub ' 'exit if an error was encountered

If CBool(Len(Username)) Then 'if the username is defined (some servers will reject submissions)
Me.Submit("USER " & Username & vbCrLf) 'submit user name
If Not Me.CheckResponse() Then Exit Sub 'exit if an error was encountered
End If

If CBool(Len(Password)) Then 'if the password is defined (some servers will reject submissions)
Me.Submit("PASS " & Password & vbCrLf) 'submit password
If Not Me.CheckResponse() Then Exit Sub ' 'exit if an error was encountered
End If
End Sub

'*******************************************************************************
' Function Name : CheckResponse
' Purpose : Check the response to a POP3 command
' :
' Returns : Boolean flag. Return True for Success. Throw an error and return False if not.
' :
' NOTE : All status responses from the server begin with:
' : +OK (OK; Success, or request granted)
' or : -ERR (NEGATIVE; error)
'*******************************************************************************
Public Function CheckResponse() As Boolean
If Not Me.IsConnected() Then Return False ' 'exit if not in TRANSACTION mode
LastLineRead = Me.Response() 'check response (and save response line)
If (LastLineRead.Substring(0, 3) <> "+OK") Then 'OK?
Throw New POP3Exception(LastLineRead) 'no, so throw an exception
Return False 'return failure flag
End If
Return True 'else return success flag
End Function

'*******************************************************************************
' Function Name : IsConnected
' Purpose : Return True if we are connected to Server in TRANSACTION state. Throw an error and return False if not.
' :
' Returns : Boolean Flag. True if connected to server
'*******************************************************************************
Public Function IsConnected() As Boolean
If Not Connected Then 'if not connected, throw an exception
Throw New POP3Exception("Not Connected to an POP3 Server.")
Return False 'return failure flag
End If
Return True 'Indicate that we are in the TRANSACTION state)
End Function

'*******************************************************************************
' Function Name : Response
' Purpose : get a response from the server (read from the mail stream into a buffer)
 Page 435

' :
' Returns : string of data from the server
' :
' NOTE : If a dataSize value > 1 is supplied, then those number of bytes will be streamed in, otherwise, the data will be
' : read in a line at a time, and end with the line end code (Linefeed (vbLf) 10 decimal)
'*******************************************************************************
Public Function Response(Optional ByVal dataSize As Integer = 1) As String
Dim ServerBufr() As Byte 'establish buffer
Dim Index As Integer = 0 'init server buffer index and character counter
If dataSize > 1 Then 'did invoker specify a data length to read?
'-------------------------------------------------------
ReDim ServerBufr(dataSize - 1) 'size to dataSize to read as a single stream block (allow for 0 index)
Dim dtsz As Integer = dataSize 'set aside updating countdown
Dim sz As Integer 'variable to store actual number of bytes read from the stream
Do While Index < dataSize 'while we have not read the entire message...
If UsesSSL Then 'process through SSL Stream if secure stream
sz = SslStream.Read(ServerBufr, Index, dtsz) 'read a server-defined block of data from SSLstream
Else 'else process through general TCP Stream
sz = Stream.Read(ServerBufr, Index, dtsz) 'read a server-defined block of data from Network Stream
End If
If sz = 0 Then Return Nothing ' 'we lost data, so we could not read the string
Index += sz 'bump index for data count actually read
dtsz -= sz 'drop amount left in buffer
Loop
Else '------------------------------------------------------
ReDim ServerBufr(255) 'initially dimension buffer to 256 bytes (including 0 offset)
Dim byteRead As Int32 'capture result of Stream.ReadByte() (function returns Int32)
Do
If UsesSSL Then 'process through SSL Stream if secure stream
byteRead = SslStream.ReadByte 'read a byte from SSLstream
Else 'else process through general TCP Stream
byteRead = Stream.ReadByte 'read a byte from Network stream
End If
If byteRead = -1 Then Exit Do 'end of stream if -1 encountered
ServerBufr(Index) = CByte(byteRead) 'otherwise, capture the byte read in our array
Index += 1 'bump our offset index and counter
If byteRead = 10 Then Exit Do 'done with line if Newline code (10; Linefeed code) read in
If Index > UBound(ServerBufr) Then 'if the index points past the end of the buffer...
ReDim Preserve ServerBufr(Index + 255) 'then bump buffer for another 256 bytes, but keep existing data
End If
Loop 'loop until a line is read in or the end of data is encountered
End If
Dim enc As New ASCIIEncoding 'medium for ASCII representation of Unicode characters
Return enc.GetString(ServerBufr, 0, Index) 'decode from the byte array to a string and return the string
End Function

'*******************************************************************************
' Sub Name : Submit
' Purpose : Submit a request to the server
' :
' Returns : Nothing
' :
' NOTE : Command name must be in UPPERCASE, such as "PASS pw1Smorf". "pass pw1Smorf" would not be acceptable.
' : Though some servers do allow for this, we should NEVER assume it.
'*******************************************************************************
Public Sub Submit(ByVal message As String)
Dim enc As New ASCIIEncoding 'medium for ASCII representation of Unicode characters
Dim WriteBuffer() As Byte = enc.GetBytes(message) 'converts the submitted string into a sequence of bytes
If UsesSSL Then 'using SSL authentication?
SslStream.Write(WriteBuffer, 0, WriteBuffer.Length) 'yes, so write SSL buffer using the SslStream object
Else
Stream.Write(WriteBuffer, 0, WriteBuffer.Length) 'else write to Network buffer using the non-SSL object
End If
End Sub

'*******************************************************************************
' Sub Name : Disconnect (This is the last the we do with a POP3 object)
' Purpose : Disconnect from the server and have it enter the UPDATE mode
' :
' Returns : Nothing
' :
' Typical telNet I/O:
'QUIT (submit)
'+OK Sayonara
'
' NOTE: When the client issues the QUIT command from the TRANSACTION state, the POP3 session enters the UPDATE state.
' (Note that if the client issues the QUIT command from the AUTHORIZATION state, the POP3 session terminates
' but does NOT enter the UPDATE state.)
'
' If a session terminates for some reason other than a client-issued QUIT command, the POP3 session does NOT
' enter the UPDATE state and MUST NOT remove any messages from the maildrop.
'
' The POP3 server removes all messages marked as deleted from the maildrop and replies as to the status of this
' operation. If there is an error, such as a resource shortage, encountered while removing messages, the
' maildrop may result in having some or none of the messages marked as deleted be removed. In no case may the
' server remove any messages not marked as deleted.
'
' Whether the removal was successful or not, the server then releases any exclusive-access lock on the maildrop
' and closes the TCP connection.
'*******************************************************************************
 Page 436

Public Sub Disconnect()


Me.Submit("QUIT" & vbCrLf) 'submit quit request
CheckResponse() 'check response
If UsesSSL Then 'SSL authentication used?
SslStream.Dispose() 'dispose of created SSL stream object if so
SslStreamDisposed = True
End If
End Sub

'*******************************************************************************
' Function Name : Statistics
' Purpose : Get the number of email messages and the total size as any integer array
' :
' Returns : 2-element interger array.
' : Element(0) is the number of user email messages on the server
' : Element(1) is the total bytes of all messages taken up on the server
' :
' Typical telNet I/O:
'STAT (submit)
'+OK 3 16487 (3 records (emails/messages) totaling 16487 bytes (octets))
'*******************************************************************************
Public Function Statistics() As Integer()
If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode
Me.Submit("STAT" & vbCrLf) 'submit Statistics request
LastLineRead = Me.Response 'check response
If (LastLineRead.Substring(0, 3) <> "+OK") Then 'OK?
Throw New POP3Exception(LastLineRead) 'no, so throw an exception
Return Nothing 'return failure flag
End If
Dim msgInfo() As String = Split(LastLineRead, " "c) 'separate by spaces, which divide its fields
Dim Result(1) As Integer
Result(0) = Integer.Parse(msgInfo(1)) 'get the number of emails
Result(1) = Integer.Parse(msgInfo(2)) 'get the size of the email messages
Return Result
End Function

'*******************************************************************************
' Function Name : List
' Purpose : Get the drop listing from the maildrop
' :
' Returns : An Arraylist of POP3Message objects
' :
' Typical telNet I/O:
'LIST (submit)
'+OK Mailbox scan listing follows
'1 2532 (record index and each size on the server in bytes)
'2 1610
'3 12345
'. (end of records terminator)
'*******************************************************************************
Public Function List() As ArrayList
If Not IsConnected() Then Return Nothing 'exit if not in TRANSACTION mode

Me.Submit("LIST" & vbCrLf) 'submit List request


If Not CheckResponse() Then Return Nothing 'check for a response, but if an error, return nothing
'
'get a list of emails waiting on the server for the authenticated user
'
Dim retval As New ArrayList 'set aside message list storage
Do
Dim response As String = Me.Response 'check response
If (response = "." & vbCrLf) Then 'done with list?
Exit Do 'yes
End If
Dim msg As New POP3Message 'establish a new message
Dim msgInfo() As String = Split(response, " "c) 'separate by spaces, which divide its fields
msg.MailID = Integer.Parse(msgInfo(0)) 'get the list item number
msg.ByteCount = Integer.Parse(msgInfo(1)) 'get the size of the email message
msg.Retrieved = False 'indicate its message body is not yet retreived
retval.Add(msg) 'add a new entry into the retrieval list
Loop
Return retval 'return the list
End Function

'*******************************************************************************
' Function Name : GetHeader
' Purpose : Grab the email header and optionally a number of lines from the body
' :
' Returns : Gets the Email header of the selected email. If an integer value is provided, that number of body lines will
' : be returned. The returned object is the submitted POP3Message.
' :
' Typical telNet I/O:
'TOP 1 0 (submit request for record 1's message header only, 0=no lines of body)
'+OK Top of message follows
' xxxxx (header for current record is transmitted)
'. (end of record terminator)
'
'TOP 1 10 (submit request for record 1's message header plus 10 lines of body data)
'+OK Top of message follows
' xxxxx (header for current record is transmitted)
' xxxxx (first 10 lines of body)
'. (end of record terminator)
'*******************************************************************************
 Page 437

Public Function GetHeader(ByRef msg As POP3Message, Optional ByVal BodyLines As Integer = 0) As POP3Message
If Not IsConnected() Then Return Nothing 'exit if not in TRANSACTION mode
Me.Submit("TOP " & msg.MailID.ToString & " " & BodyLines.ToString & vbCrLf)
If Not CheckResponse() Then Return Nothing 'check for a response, but if an error, return nothing
msg.Message = Nothing 'erase current contents of the message, if any
'
'now process message data by binding the lines into a single string
'
Do
Dim response As String = Me.Response 'grab message line
If response = "." & vbCrLf Then 'end of data?
Exit Do 'yes, done with the loop if so
End If
msg.Message &= response 'else build message by appending the new line
Loop
Return msg 'return new filled Message object
End Function

'*******************************************************************************
' Function Name : Retrieve
' Purpose : Retrieve email from POP3 server for the provided POP3Message object
' :
' Returns : The submitted POP3 Message object with its Message property filled, and its ByteCount property properly
' : fitted to the message size.
' :
' NOTE: Some email servers are set up to automatically delete an email once it is retrieved from the server. Outlook,
' Outlook Express, and Windows Mail do this. It is an option under Juno and Gmail. So, if we do not submit a POP3
' QUIT (the Disconnect() method), but just close out the POP3 object, the messages will not be deleted. Even so,
' most Windows-based server-processors will add an additional CR for each LF, but the reported email size does not
' account for them. So we must retreive more data to account for this.
'
' Typical telNet I/O:
'RETR 1 (submit request to retrieve record index 1 (cannot be an index marked for deletion))
'+OK 2532 octets (an octet is a fancy term for a 8-bit byte)
' xxxx (entire email is retrieved as one long string)
'. (end of record terminator)
'*******************************************************************************
Public Function Retrieve(ByRef msg As POP3Message) As POP3Message
If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode
Me.Submit("RETR " & msg.MailID.ToString & vbCrLf) 'issue request for indicated message number
If Not CheckResponse() Then Return Nothing ' 'check for a response, but if an error, return nothing
msg.Message = Me.Response(msg.ByteCount) 'grab message line
'the stream reader automatically converts the NewLine code, vbLf, to vbCrLf, so the file is not yet
'fully read. For example, a file that was 233 lines will therefore have 233 extra characters not
'yet read from the file when it has reached its reported data size. So we will scan these in.
'But even if this was not the case, the trailing "." & vbCrLf is still pending...
Do
Dim strData As String = Response() 'grab more data
If strData = "." & vbCrLf Then 'end of data?
Exit Do 'If so, then exit loop
End If
msg.Message &= strData 'else tack data to end of message
Loop 'keep trying
msg.ByteCount = Len(msg.Message) 'ensure full size updated to actual size (server size may differ)
Return msg 'return new message object
End Function

'*******************************************************************************
' Sub Name : Delete
' Purpose : Delete an email
' :
' Returns : Nothing
' :
' NOTE: Some email servers are set up to automatically delete an email once it is retrieved from the server. Outlook,
' Outlook Express, and Windows Mail do this. It is an option under Juno and Gmail.
'
' Typical telNet I/O:
'DELE 1 (submit request to delete record index 1)
'+OK Message deleted
'*******************************************************************************
Public Sub Delete(ByVal msgHdr As POP3Message)
If Not IsConnected() Then Exit Sub 'exit if not in TRANSACTION mode
Me.Submit("DELE " & msgHdr.MailID.ToString & vbCrLf) 'submit Delete request
CheckResponse() 'check response
End Sub

'*******************************************************************************
' Sub Name : Reset
' Purpose : Reset any deletion (automatic or manual) of all email from the current session.
' :
' Returns : Nothing
' :
' Typical telNet I/O:
'RSET (submit)
'+OK Reset state
'*******************************************************************************
Public Sub Reset()
If Not IsConnected() Then Exit Sub 'exit if not in TRANSACTION mode
Me.Submit("RSET" & vbCrLf) 'submit Reset request
CheckResponse() 'check response
End Sub
 Page 438

'*******************************************************************************
' Function Name : NOOP (No Operation)
' Purpose : Does nothing. Juts gets a position response from the server
' :
' Returns : Boolean flag. False if disconnected, else True if connected.
' :
' NOTE: This NO OPERATION command is useful when you have a server that automatically disconnects after a certain idle
' period of activity. This command can be issued by a timer that also monitors users inactivity, and issues a
' NOOP to reset the server timer.
'
' Typical telNet I/O:
'NOOP (submit)
'+OK
'*******************************************************************************
Public Function NOOP() As Boolean
If Not IsConnected() Then Return False 'exit if not in TRANSACTION mode
Me.Submit("NOOP")
Return CheckResponse()
End Function

'*******************************************************************************
' Function Name : Finalize
' Purpose : remove SSL Stream object if not removed
'*******************************************************************************
Protected Overrides Sub Finalize()
If SslStream IsNot Nothing AndAlso Not SslStreamDisposed Then 'SSL Stream object Disposed?
SslStream.Dispose() 'no, so do it
End If
MyBase.Finalize() 'then do normal finalization
End Sub
End Class

'***********************************************************************************
'***********************************************************************************
'***********************************************************************************

'***********************************************************************************
'***********************************************************************************
' Class Name : POP3Message
' Purpose : POP3 message data
'***********************************************************************************
'***********************************************************************************
Public Class POP3Message
Public MailID As Integer = 0 'message number
Public ByteCount As Integer = 0 'length of message in bytes
Public Retrieved As Boolean = False 'flag indicating if the message has been retrieved
Public Message As String = Nothing 'the text of the message

Public Overrides Function ToString() As String


Return Message
End Function
End Class

'***********************************************************************************
'***********************************************************************************
'***********************************************************************************

'***********************************************************************************
'***********************************************************************************
' Class Name : POP3Exception
' Purpose : process exception
' NOTE : This is a normal exception, but we wrap it to give it an identity
' : that can be associated with our POP3 class
'***********************************************************************************
'***********************************************************************************
Public Class POP3Exception
Inherits ApplicationException

Public Sub New(ByVal str As String)


MyBase.New(str)
End Sub
End Class
 Page 439

The Complete Utilities Class File.


Option Strict On
Option Explicit On
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' Utilities - Copyright © 2011 - 2021 by David Ross Goben.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imports System.Text

Public NotInheritable Class Utilities

'*******************************************************************************
' Function Name : DecodeBase64ToStr
' Purpose : Decode a provided raw email message string that is encoded to Base64.
' :
' Returns : Decoded String
' :
' NOTES : note that the lone vbCrLf at the end of lines is filtered out.
'*******************************************************************************
Public Shared Function DecodeBase64ToStr(ByVal strData As String) As String
Return Encoding.UTF8.GetChars(DecodeBase64ToBytes(strData))
End Function

'*******************************************************************************
' Function Name : DecodeBase64ToBytes
' Purpose : Decode a provided raw email message string that is encoded to Base64.
' :
' Returns : Decoded binary Byte Array
' :
' NOTES : note that the lone vbCrLf at the end of lines is filtered out.
'*******************************************************************************
'this modification returns a Byte Array of the Base64 encoded source data
Public Shared Function DecodeBase64ToBytes(ByVal strData As String) As Byte()
Return Convert.FromBase64String(strData.Replace(vbCrLf, Nothing))
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
' Function Name : DecodeQuotedPrintable
' Purpose : Method to clean typical control translations, or all of them.
' : This should be invoked for all data coded Quoted-Printable.
' :
' Returns : Provided a raw message string block, it returns a decoded string.
' :
' NOTES : Typical cleaning involves changing "=0D" to vbCr, "=0A" to vbLf,
' : "=20" to a space, and "=3D" to "=", plus any line wrap
' : terminators at the end of lines to Nothing.
' :
' : A StringBuilder object will be used, which will very quickly
' : do a replacement of all control code translations using fewer
' : resources, and what resources that are used will be instantly
' : flushed when the method exits.
'*******************************************************************************
Public Shared Function DecodeQuotedPrintable(ByVal Message As String, Optional ByVal QuickClean As Boolean = False) As String
'set up StringBuilder object with data stripped of any line continuation tags
Dim Msg As New StringBuilder(Message.Replace("=" & vbCrLf, Nothing))

If QuickClean Then 'perform a quick clean (clean up common basics)


Return Msg.Replace("=" & vbCrLf, Nothing).Replace("=0D", vbCr).Replace("=0A",
vbLf).Replace("=20", " ").Replace("=3D", "=").ToString
Else 'perform total cleaning
'store 2-character hex values that require a leading "0"
Dim HxData As String = "X0102030405060708090A0B0C0D0E0F"
For Idx As Integer = 1 To &HF 'initially process codes 1-15, which require a leading zero
Msg.Replace("=" & Mid(HxData, Idx << 1, 2), Chr(Idx)) 'replace hex data with single character code (SHIFT is faster)
Next
For idx As Integer = &H10 To &HFF 'process the whole 8-bit extended ASCII gambit
Msg.Replace("=" & Hex(idx), Chr(idx)) 'replace hex data with single character code
Next
Return Msg.ToString 'return result string
End If
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Page 440

'*******************************************************************************
' Function Name : DecodeBinHex
' Purpose : Decode a provided raw email message string that is encoded to BinHex.
' :
' Returns : Decoded String
' :
' NOTES : note that the lone vbCrLf at the end of lines is filtered out.
'*******************************************************************************
Public Shared Function DecodeBinHex(ByVal StrData As String) As Byte()
Dim Src() As Byte = Encoding.UTF8.GetBytes(StrData.Replace(vbCrLf, Nothing).ToUpper)
Dim Result() As Byte 'init output buffer
ReDim Result((UBound(Src) + 1) \ 2) 'set initial dimension bytes defined in file
Dim Index As Integer = 0 'init index for Result() array

For Idx As Integer = 0 To UBound(Src) Step 2 'scan the string, 2 hex characters at a time
Dim CL As Integer = Src(Idx) - 48 'Convert "0" - "F" to 0-F
If CL > 10 Then CL -= 7
Dim CR As Integer = Src(Idx + 1) - 48 'do the same for the right hex digit
If CR > 10 Then CR -= 7
Result(Index) = CByte(CL * 16 + CR) 'stuff byte value
Index += 1 'bump index
Next
ReDim Preserve Result(Index - 1) 'set array to final size
Return Result 'return the final result
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
' Function Name : TextNeedsEncoding
' Purpose : Determine if HTML text, Rich Text, or Plain Text requires
' : 8-bit code translation to 7-bit Quoted-Printable tags.
' :
' Returns : Provided a source string, it returns a boolena flag.
' : If the returned value is true, the source contains 8-bit data
' : and will be encoded by server.
' :
' NOTES : If text data contains 8-bit values, the default .NET
' : SMTP processor will force this code to be encoded to Base64,
' : even if only a single byte is 8-bit.
' :
' : To avoid this, the Force7BitHtml() method can be invoked on
' : HTML text to ensure that it is 7-bit encoded so that it can
' : be processed as Quoted-Printable or as 7Bit. The ForceQuotedPrintable()
' : method performs essential conversions for non-HTML text, but this
' : would be best served in Attachments and Alternate Views.
'*******************************************************************************
Public Shared Function TextNeedsEncoding(ByVal Message As String) As Boolean
Dim Byt() As Byte = Encoding.UTF8.GetBytes(Message) 'convert message to byte array
For Each B As Byte In Byt
If CBool(B And &H80) Then Return True
Next
Return False
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
' Function Name : Force7BitHtml
' Purpose : Method to convert 8-bit code in an HTML message to 7-bit.
' :
' Returns : Provided a string containing HTML code, it will return a string
' : containing HTML code that does not have any 8-bit data embedded.
' :
' NOTES : If any characters in an HTML text string are 8-bit (values
' : greater than 127), then they are converted into a special
' : 7-bit HTML Entity Number, For Example, code 149 (•) is an 8-bit
' : value that can be changed to HTML "&#149;", which will ensure
' : that it will still be displayed on the HTML page, but the HTML
' : source code will no longer carry an actual 8-bit value. If such
' : code had not been corrected, the encoding of the data would be
' : forced to change from quoted-printable to Base64, because that
' : would be the only way the email processor could guarantee that
' : the email text was fully intact.
'*******************************************************************************
Public Shared Function Force7BitHtml(ByVal HtmlSource As String) As String
Dim Sb As New StringBuilder 'set up string builder for appending data
For Idx As Integer = 1 To Len(HtmlSource)
Dim C As Integer = AscW(Mid(HtmlSource, Idx, 1)) 'get a single character from the source
Select Case C 'check each character
Case Is > &H7F, Is < 0 'if 8-bit or unicode code
Sb.Append("&#" & C.ToString & ";") 'convert to 7-bit HTML ecoder
Case Else
Sb.Append(ChrW(C)) 'else save text regardless
End Select
Next
Return Sb.ToString
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Page 441

'*******************************************************************************
' Function Name : ForceQuotedPrintable
' Purpose : Force 8-bit code in a text message to 7-bit, without data loss.
' :
' Returns : Provided a source string that contains 8-bit data, the 8-bit
' : data is converted to Hex-Tags, and the returned string is 7-bit.
' :
' NOTES : if any characters in a text string are 8-bit (values greater
' : than 127), then they are converted into special 7-bit tags.
' : For Example, code 149 (•) is an 8-bit value that can be changed
' : to hex "=95", which will ensure that it will still be displayed
' : in the text, but the text data will no longer carry actual 8bit
' : that would force data to change from quoted-printable or 7bit
' : to Base64, because that would be the only way the email processor
' : Base64, because that would be the only way the email processor
' : could guarantee that the email text was fully intact. However,
' : you will have to use the DecodeQuotedPrintable() method to convert
' : it back to its original text form.
' :
' : The Encoded text will begin with "=00". Because unencoded null codes
' : are not permitted in email data, you can use this to instantly
' : determine on the receiving end that this code will need to be
' : processed by DecodeQuotedPrintable() a second time (if initially
' : encoded as Quoted-Printable). A second pass would be required,
' : because if this translated code was afterward encoded as Quoted-
' : Printable, and all the "=xx" byte-translations, would be
' : reinterpreted as "=3Dxx", which DecodeQuotedPrintable() would
' : convert back to "=xx", so passing through a second time would
' : properly convert the additional encoding. Further, by checking the
' : text startiing with "=00", you would know that you would need to
' : double-decode the text. Also, you would want to initially skip this
' : initial tag when passing it the second time to DecodeQuotedPrintable():
' :
' : Dim Result As String = DecodeQuotedPrintable(Message) 'initially decode Quoted-Printable text
' : If Result.SubString(0, 3) "=00" Then 'tagged as pre-encoded?
' : Return DecodeQuotedPrintable(Result.SubString(3)) 'yes, so decode again and return, less initial null byte
' : Else
' : Return Result 'otherwise, return result of decoding
' : End If
'*******************************************************************************
Public Shared Function ForceQuotedPrintable(ByVal Message As String) As String
Dim Byt() As Byte = Encoding.UTF8.GetBytes(Message) 'convert message to byte array
Dim Sb As New StringBuilder("=00") 'set up string builder for appending data
For Each B As Byte In Byt
Select Case B 'check each byte
Case Is > &H7F 'if 8-bit code
Sb.Append("=" & Hex(B)) 'convert to 7-bit tag
Case Else
Sb.Append(Chr(B)) 'else save text regardless
End Select
Next
Return Sb.ToString
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
' Function Name : QConvertHTML2Text
' Purpose : Short-Form Convert HTML formatted text to plain text
' :
' Returns : Provided a simple HTML source string, it will return a Plain Text
' : string with HTML code removed.
'*******************************************************************************
Public Shared Function QConvertHTML2Text(ByVal HTMLText As String) As String
Return RegularExpressions.Regex.Replace(HTMLText.Replace("&nbsp;", " ").Replace("&quot;", """").Replace("&apos;",
"'"), "<[^>]*>", "").Replace("&lt;", "<").Replace("&gt;", ">").Replace("&amp;", "&").Replace(";;", ";")
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
' Function Name : ConvertHTML2Text
' Purpose : Convert HTML formatted text to plain text
' :
' Returns : Provided a complex HTML string, it will return a Plain Text string
' : with all HTML codes and formatting removed from it.
' :
' NOTE : Numerous of these conversions will convert the text to 8-bit,
' : though most of these symbols will not be encountered in most
' : HTML documents we produce. But regardless of that, if you wish
' : to make this conversion the main body message of an email, you
' : may have to further convert this using ForceQuotedPrintable()
' : to maintain Quoted-Printable encoding and avoid Base64, even
' : though this is typically not an issue. However, some few really
' : primitive email readers, typically those that simply allow you
' : to preview email messages, without fully loading them, will not
' ' know how to support Base64, or will not bother with it, but simply
' : display the raw data. RFC 2045 requires email handlers to support it.
'*******************************************************************************
 Page 442

Public Shared Function ConvertHTML2Text(ByVal HTMLText As String) As String


'instantiate an initially blank StringBuilder object
Dim Sb As New StringBuilder()

'first remove leading whitespace of each line and append the result to the StringBuilder
Dim ary() As String = Split(HTMLText, vbCrLf)
For Each S As String In ary
Sb.Append(S.TrimStart(Chr(9), " "c))
Next

'replace reserved entities (except <, >, and &)


Sb.Replace("&quot;", """").Replace("&apos;", "'").Replace("&nbsp;", " ")

'replace HTML paragraph, line breaks, and table entry terminators with vbCrLf
Sb.Replace("<p>", vbCrLf).Replace("<P>", vbCrLf).Replace("</p>", vbCrLf).Replace("</P>", vbCrLf).Replace("<br>",
vbCrLf).Replace("<BR>", vbCrLf).Replace("</td>", vbCrLf).Replace("</TD>", vbCrLf)

'replace ISO 8859-1 Symbols (160-255). Note that any matches will make the text 8-bit
Sb.Replace("&iexcl;", "¡").Replace("&cent;", "¢").Replace("&pound;", "£").Replace("&curren;",
"¤").Replace("&yen;", "¥").Replace("&brvbar;", "¦").Replace("&sect;", "§").Replace("&uml;",
"¨").Replace("&copy;", "©").Replace("&ordf;", "ª").Replace("&laquo;", "«").Replace("&not;",
"¬").Replace("&shy;", "-").Replace("&reg;", "®").Replace("&macr;", "¯").Replace("&deg;",
"°").Replace("&plusmn;", "±").Replace("&sup2;", "²").Replace("&sup3;", "³").Replace("&acute;",
"´").Replace("&micro;", "µ").Replace("&para;", "¶").Replace("&middot;", "•").Replace("&cedil;",
"¸").Replace("&sup1;", "¹").Replace("&ordm;", "º").Replace("&raquo;", "»").Replace("&frac14;",
"¼").Replace("&frac12;", "½").Replace("&frac34;", "¾").Replace("&iquest;", "¿").Replace("&times;",
"×").Replace("&divide;", "÷")

'replace ISO 8859-1 characters. Note that any matches will make the text 8-bit
Sb.Replace("&Agrave;", "À").Replace("&Aacute;", "Á").Replace("&Acirc;", "Â").Replace("&Atilde;", "Ã").Replace("&Auml;",
"Ä").Replace("&Aring;", "Å").Replace("&AElig;", "Æ").Replace("&Ccedil;", "Ç").Replace("&Egrave;",
"È").Replace("&Eacute;", "É").Replace("&Ecirc;", "Ê").Replace("&Euml;", "Ë").Replace("&Igrave;",
"Ì").Replace("&Iacute;", "Í").Replace("&Icirc;", "Î").Replace("&Iuml;", "Ï").Replace("&ETH;",
"Ð").Replace("&Ntilde;", "Ñ").Replace("&Ograve;", "Ò").Replace("&Oacute;", "Ó").Replace("&Ocirc;",
"Ô").Replace("&Otilde;", "Õ").Replace("&Ouml;", "Ö").Replace("&Oslash;", "Ø").Replace("&Ugrave;",
"Ù").Replace("&Uacute;", "Ú").Replace("&Ucirc;", "Û").Replace("&Uuml;", "Ü").Replace("&Yacute;",
"Ý").Replace("&THORN;", "Þ").Replace("&szlig;", "ß").Replace("&agrave;", "à").Replace("&aacute;",
"á").Replace("&acirc;", "â").Replace("&atilde;", "ã").Replace("&auml;", "ä").Replace("&aring;",
"å").Replace("&aelig;", "æ").Replace("&ccedil;", "ç").Replace("&egrave;", "è").Replace("&eacute;",
"é").Replace("&ecirc;", "ê").Replace("&euml;", "ë").Replace("&igrave;", "ì").Replace("&iacute;",
"í").Replace("&icirc;", "î").Replace("&iuml;", "ï").Replace("&eth;", "ð").Replace("&ntilde;",
"ñ").Replace("&ograve;", "ò").Replace("&oacute;", "ó").Replace("&ocirc;", "ô").Replace("&otilde;",
"õ").Replace("&ouml;", "ö").Replace("&oslash;", "ø").Replace("&ugrave;", "ù").Replace("&uacute;",
"ú").Replace("&ucirc;", "û").Replace("&uuml;", "ü").Replace("&yacute;", "ý").Replace("&thorn;",
"þ").Replace("&yuml;", "ÿ")

'replace Math Symbols Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("&forall;", "∀").Replace("&part;", "∂").Replace("&exist;", "∃").Replace("&empty;", "∅").Replace("&nabla;",
"∇").Replace("&isin;", "∈").Replace("&notin;", "∉").Replace("&ni;", "∋").Replace("&prod;",
"∏").Replace("&sum;", "∑").Replace("&minus;", "−").Replace("&lowast;", "∗").Replace("&radic;",
"√").Replace("&prop;", "∝").Replace("&infin;", "∞").Replace("&ang;", "∠").Replace("&and;",
"∧").Replace("&or;", "∨").Replace("&cap;", "∩").Replace("&cup;", "∪").Replace("&int;",
"∫").Replace("&there4;", "∴").Replace("&sim;", "∼").Replace("&cong;", "≅").Replace("&asymp;",
"≈").Replace("&ne;", "≠").Replace("&equiv;", "≡").Replace("&le;", "≤").Replace("&ge;",
"≥").Replace("&sub;", "⊂").Replace("&sup;", "⊃").Replace("&nsub;", "⊄").Replace("&sube;",
"⊆").Replace("&supe;", "⊇").Replace("&oplus;", "⊕").Replace("&otimes;", "⊗").Replace("&perp;",
"⊥").Replace("&sdot;", "⋅")'NOTE: certain characters have tall characteristics

'replace Greek Letters Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("&Alpha;", "Α").Replace("&Beta;", "Β").Replace("&Gamma;", "Γ").Replace("&Delta;", "Δ").Replace("&Epsilon;",
"Ε").Replace("&Zeta;", "Ζ").Replace("&Eta;", "Η").Replace("&Theta;", "Θ").Replace("&Iota;",
"Ι").Replace("&Kappa;", "Κ").Replace("&Lambda;", "Λ").Replace("&Mu;", "Μ").Replace("&Nu;",
"Ν").Replace("&Xi;", "Ξ").Replace("&Omicron;", "Ο").Replace("&Pi;", "Π").Replace("&Rho;",
"Ρ").Replace("&Sigma;", "Σ").Replace("&Tau;", "Τ").Replace("&Upsilon;", "Υ").Replace("&Phi;",
"Φ").Replace("&Chi;", "Χ").Replace("&Psi;", "Ψ").Replace("&Omega;", "Ω").Replace("&alpha;",
"α").Replace("&beta;", "β").Replace("&gamma;", "γ").Replace("&delta;", "δ").Replace("&epsilon;",
"ε").Replace("&zeta;", "ζ").Replace("&eta;", "η").Replace("&theta;", "θ").Replace("&iota;",
"ι").Replace("&kappa;", "κ").Replace("&lambda;", "λ").Replace("&mu;", "μ").Replace("&nu;",
"ν").Replace("&xi;", "ξ").Replace("&omicron;", "ο").Replace("&pi;", "π").Replace("&rho;",
"ρ").Replace("&sigmaf;", "ς").Replace("&sigma;", "σ").Replace("&tau;", "τ").Replace("&upsilon;",
"υ").Replace("&phi;", "φ").Replace("&chi;", "χ").Replace("&psi;", "ψ").Replace("&omega;",
"ω").Replace("&thetasym;", "ϑ").Replace("&upsih;", "ϒ").Replace("&piv;", "ϖ")

'replace Other Entities Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("&OElig;", "Œ").Replace("&oelig;", "œ").Replace("&Scaron;", "Š").Replace("&scaron;", "š").Replace("&Yuml;",
"Ÿ").Replace("&fnof;", "ƒ").Replace("&circ;", "ˆ").Replace("&tilde;", "˜").Replace("&ensp;",
" ").Replace("&emsp;", " ").Replace("&thinsp;", " ").Replace("&ndash;", "–").Replace("&mdash;",
"—").Replace("&lsquo;", "‘").Replace("&rsquo;", "’").Replace("&sbquo;", " ‚").Replace("&ldquo;",
" ").Replace("&rdquo;", " ").Replace("&bdquo;", "„").Replace("&dagger;", "†").Replace("&Dagger;",
"‡").Replace("&bull;", "•").Replace("&hellip;", "...").Replace("&permil;", "‰").Replace("&prime;",
"′").Replace("&Prime;", "″").Replace("&lsaquo;", "‹").Replace("&rsaquo;", "›").Replace("&oline;",
"‾").Replace("&euro;", "€").Replace("&trade;", "™").Replace("&larr;", "←").Replace("&uarr;",
"↑").Replace("&rarr;", "→").Replace("&darr;", "↓").Replace("&harr;", "↔").Replace("&crarr;",
"↵").Replace("&lceil;", "⌈").Replace("&rceil;", "⌉").Replace("&lfloor;", "⌊").Replace("&rfloor;",
"⌋").Replace("&loz;", "◊").Replace("&spades;", "♠").Replace("&clubs;", "♣").Replace("&hearts;",
"♥").Replace("&diams;", "♦") 'NOTE: certain characters have tall characteristics
'replace special ASCII coding entities that were not captured by the above. Note that values > 127 will make the text 8-bit
For Idx As Integer = 1 To 255 'See www.w3schools.com/tags/ref_entities.asp
Sb.Replace("&#" & Idx.ToString & ";", Chr(Idx)) 'replace most common numeric entities
Next

'Ensure header definitions are followed by vbCrLf


Dim NewText As String = RegularExpressions.Regex.Replace(Sb.ToString(), "</H[^>]*>", vbCrLf)

'Also seek out other Unicode encoded number entities not covered by the above and individually update them
Dim Idy As Integer = InStr(NewText, "&#") 'check for a numeric entity
Do While Idy <> 0 'loop as long as we find one
 Page 443

Dim Idz As Integer = InStr(Idy, NewText, ";") 'find terminating semicolon


Dim S As String = Mid(NewText, Idy, Idz - Idy + 1) 'grab expression
RegularExpressions.Regex.Replace(NewText, S, Chr(CInt(Mid(S, 3, Len(S) - 3)))) 'replace expression
InStr(Idy + 1, NewText, "&#")
Loop

'strip remaining HTML text tags, replace < and > placeholders, convert ampersand, replace ;; with ;, then return result
Return RegularExpressions.Regex.Replace(NewText, "<[^>]*>", "").Replace("&lt;",
"<").Replace("&gt;", ">").Replace("&amp;", "&").Replace(";;", ";")
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
' Enum MediaTypes: Structure used by GetMediaType
'*******************************************************************************
Public Enum MediaTypes As Integer
ApplicationOctet ' 0 = Integer Value
ApplicationPdf ' 1
ApplicationRtf ' 2
ApplicationSoap ' 3
ApplicationZip ' 4
ImageGif ' 5
ImageJpeg ' 6
ImageTiff ' 7
TextHtml ' 8
TextPlain ' 9
TextRich '10
TextXml '11
End Enum

'*******************************************************************************
' Function Name : GetMediaType
' Purpose : Provide easy access to System.Net.Mime.MediaTypes text
' :
' Returns : provided a MediaTypes enumeration value, a string representing
' : the selected type will be returned.
'*******************************************************************************
Public Shared Function GetMediaType(ByVal MediaType As MediaTypes) As String
Select Case MediaType
Case MediaTypes.ApplicationPdf
Return "application/pdf"
Case MediaTypes.ApplicationRtf
Return "application/rtf"
Case MediaTypes.ApplicationSoap
Return "application/soap+xml"
Case MediaTypes.ApplicationZip
Return "application/zip"
Case MediaTypes.ImageGif
Return "image/gif"
Case MediaTypes.ImageJpeg
Return "image/jpeg"
Case MediaTypes.ImageTiff
Return "image/tiff"
Case MediaTypes.TextHtml
Return "text/html"
Case MediaTypes.TextPlain
Return "text/plain"
Case MediaTypes.TextRich
Return "text/richtext"
Case MediaTypes.TextXml
Return "text/xml"
Case Else
Return "application/octet-stream"
End Select
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
' Enum TransferEncodings: Structure used by GetTransferEncoding
'*******************************************************************************
Public Enum TransferEncodings As Integer
QuotedPrintable ' 0 = Integer value
Base64 ' 1
SevenBit ' 2
End Enum
 Page 444

'*******************************************************************************
' Function Name : GetTransferEncoding
' Purpose : Provide easy access to System.Net.Mime.TransferEncoding data
' :
' Returns : Provided a TransferEncodings value, a TransferEncoding value
' : is returned.
'*******************************************************************************
Public Shared Function GetTransferEncoding(ByVal TransferEncoding As TransferEncodings) As System.Net.Mime.TransferEncoding
Return DirectCast(TransferEncoding, System.Net.Mime.TransferEncoding)
End Function

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
' Function Name : GetEmailInfo
' Purpose : Break email down into its component parts.
' :
' Returns : EmailInfo object with component parts of email broken down.
' :
' NOTES: This method uses classes EmailItems and EmailInfo. The Message Body, and each AlternnateView or Attachment
' are contained within EmailItem objects within the EmailIngo object.
'
' An EmailItem contains fields for FROM, TO, SUBJECT, Content-Type, a flag indicating if the ContentTypeData is
' a filename or if it is text formatting, content-transfer-encoding data, and the raw encoded, data, whether it
' is a message or binary information. If the content-transfer encoding is set to "base64", the data should be
' decoded using the DecodeBase64() method. If it is "quoted-printable", the data should be decoded using
' DecodeQuotedPrintable(). If it is "7bit", it is 7-bit data and does not need to be decoded.
'*******************************************************************************
Public Shared Function GetEmailInfo(ByVal MailMessage As String) As EmailInfo
Dim Info As New EmailInfo 'structure to hold breakdown of email
Dim Ary() As String = Split(MailMessage, vbCrLf) 'break full email into lines
Dim Idx As Integer = 0 'index into Ary()
Dim MX As Integer = UBound(Ary) + 1 'find end if list+1
Dim Boundaries As New List(Of String) 'boundary definitions

Dim IsMultiPart As Boolean = False 'true if we have multiple parts


Dim SeekingEncoding As Boolean = False 'true if we are looking for encoding
Dim BuildingDataBlock As Boolean = False 'true if we are building a data block
Dim HaveMessageBody As Boolean = False 'true if we have the message body defined

Dim ContentType As String = Nothing 'hold last-defined Content Type


Dim ContentTypeIsName As Boolean = False 'true of Content Type specified a file
Dim ContentTypeData As String = Nothing 'if block is an attachment
Dim ContentEncoding As String = Nothing 'hold last-defined Content Transfer Encoding
Dim ContentBody As String = Nothing 'block data accumulator
'-----------------------------------------------------------
Dim Inheader As Integer = 4 'flag for gathering To, From, Date, Subject
Do
Dim S As String = Ary(Idx) 'grab a line of data from the email
'
' check for important header items
'
If CBool(Len(S)) AndAlso CBool(Inheader) Then 'if we are currently in the header...
Dim I As Integer = InStr(S, ": ") 'find field delimiter
If CBool(I) Then 'found one?
If Right(S, 1) = ";" Then 'line continues?
Idx += 1 'yes, so bump index
S &= Ary(Idx).Trim(Chr(9), " "c) 'append next line but remove tabs and spaces
End If
Select Case LCase(Left(S, I + 1)) 'yes, check for one of 4 fields
Case "from: " 'Found FROM field
Info.FromData = Trim(Mid(S, I + 1)) 'stuff to structure
Inheader -= 1 'drop 1 from flag
Case "to: " 'Found TO field
Info.ToData = Trim(Mid(S, I + 1)) 'stuff to structure
Inheader -= 1 'drop 1 from flag
Case "date: " 'Found DATE field
Info.DateData = Trim(Mid(S, I + 1)) 'stuff to structure
Inheader -= 1 'drop 1 from flag
Case "subject: " 'Found SUBJECT field
Info.SubjectData = Trim(Mid(S, I + 1)) 'stuff to structure
Inheader -= 1 'drop 1 from flag
End Select
End If
If Not CBool(Inheader) Then 'if InHeader flag is now zero...
SeekingEncoding = True 'start looking for a Content-Transfer-Encoding field
S = Nothing 'purge current data
End If
End If
'-------------------------------------------------------
' check for boundaries
'-------------------------------------------------------
If CBool(Len(S)) AndAlso CBool(Boundaries.Count) Then 'check any defined boundaries
For Idy As Integer = 0 To Boundaries.Count - 1
If CBool(InStr(S, Boundaries.Item(Idy), CompareMethod.Text)) Then
If BuildingDataBlock Then
Dim Itm As New EmailItem 'create a new item
Itm.ContentType = ContentType 'store content type
Itm.ContentTypeData = ContentTypeData 'save filename or character set
Itm.ContentTypeDataIsFilename = ContentTypeIsName 'save flag indicating if Attachment
Itm.ContentEncoding = ContentEncoding 'store encoding
Itm.ContentBody = ContentBody 'store data
ContentBody = Nothing 'reset accumulator
 Page 445

If HaveMessageBody Then 'already have a message body?


If ContentTypeIsName Then 'if an attachment
Info.Attachments.Add(Itm) 'add an attachment
Else 'otherwise an alternate view
Info.AlternateViews.Add(Itm)
End If
Else
Info.MessageBody = Itm 'else stuff new item to message body
HaveMessageBody = True 'indicate we now have a message body
End If
ContentTypeData = Nothing 'reset filename/charset
BuildingDataBlock = False 'turn off building flag
End If
SeekingEncoding = True 'turn block seeing on again
S = Nothing 'purge current data
Exit For
End If
Next
End If
'-------------------------------------------------------
' build data block
'-------------------------------------------------------
If BuildingDataBlock Then
ContentBody &= S & vbCrLf 'add a line to content data
End If
'-------------------------------------------------------
' if seeking encoding
'-------------------------------------------------------
If CBool(Len(S)) AndAlso SeekingEncoding Then 'are we seeking TCE?
Dim I As Integer = InStr(S, ": ") 'yes, check for field delimiter
If CBool(I) Then 'did we find one?
Select Case LCase(Left(S, I + 1)) 'yes, check for types
'=======================================================
Case "content-type: " 'Content type?
ContentType = Mid(S, I + 1).Trim(Chr(9), " "c) 'yes, so grab data and trim tabs and spaces
If Right(S, 1) = ";" Then 'more to add?
Idx += 1 'yes, so bump index
ContentType &= Ary(Idx).Trim(Chr(9), " "c) 'grab next line and trim tabs and spaces
End If
ContentTypeIsName = False 'init flag specifying a file as false
Dim sbAry() As String = Split(ContentType, ";") 'now check the content type data
ContentType = sbAry(0) 'keep first part for ContentType
If StrComp(Left(sbAry(0), 10), "multipart/", CompareMethod.Text) = 0 Then
'multipart, so grab second parameter (boundary definition), and strip any quotes
Dim Bnd As String = Trim(Mid(sbAry(1), InStr(sbAry(1), "=") + 1)).Replace("""", Nothing)
Boundaries.Add(Bnd) 'and add a boundary
ElseIf StrComp(Left(sbAry(1), 5), "name=", CompareMethod.Text) = 0 Then
ContentTypeIsName = True 'attachment if a filename specified (otherwise a view)
sbAry = Split(sbAry(1), "=") 'multipart, so grab second parameter
'get second part of second parameter (filename definition)
ContentTypeData = sbAry(1).Trim().Replace("""", Nothing) 'strip any quotes
Else
ContentTypeData = sbAry(1) 'AlternateView, so stuff display character set
If Len(Trim(Ary(Idx + 1))) = 0 Then 'if next line blank, assume ContentEncoding=base64
ContentEncoding = "base64" 'is blank, so assume base64
SeekingEncoding = False 'turn off seeking flag
BuildingDataBlock = True 'turn on building data block flag
Idx += 1 'bump to skip the blank line
End If
End If
'===================================================
Case "content-transfer-encoding: "
ContentEncoding = Mid(S, I + 1).Trim(Chr(9), " "c) 'yes, so grab data
SeekingEncoding = False 'turn off seeking flag
BuildingDataBlock = True 'turn on building data block flag
Idx += 1 'bump to skip required following blank line
End Select
End If
End If
Idx += 1 'bump array index
Loop While Idx < MX
'-----------------------------------------------------------
'some emails do not define borders, so a message body is not created, so force it!
'-----------------------------------------------------------
If Info.MessageBody Is Nothing Then 'if the email lacks a border for message data...
Dim Itm As New EmailItem 'we will create it now, so create a new EmailItem...
Itm.ContentType = ContentType 'store content type...
Itm.ContentTypeData = ContentTypeData 'save filename or character set...
Itm.ContentTypeDataIsFilename = ContentTypeIsName 'save flag indicating if Attachment...
Itm.ContentEncoding = ContentEncoding 'store encoding...
Itm.ContentBody = ContentBody 'store data...
Info.MessageBody = Itm 'and finally set the new item to the message body
End If
Return Info 'return with filled data block
End Function
 Page 446

'-------------------------------------------------------------------------------
' Sample Code
'-------------------------------------------------------------------------------
'*******************************************************************************
' Function Name : SampleReadPOP3
' Purpose : Sample method to Read a POP3 account maildrop
' :
' Returns : ArrayList containing a list of POP3Message objects, or Nothing if no emails
' :
' NOTE : This method should be modified to suit your application. For
' : example, the text may be HTML or Rich Text format. The raw text
' : should be plugged into the appropriate medium, such as
' : "Me.TextBox1.Text = msg2.Message" for proper viewing.
'*******************************************************************************
Public Function SampleReadPOP3(ByVal Server As String,
ByVal Username As String,
ByVal Password As String,
Optional ByVal InPort As Integer = 110,
Optional ByVal UseSSL As Boolean = False) As ArrayList
Try
Dim InMail As New POP3 'create a new POP3 connection
InMail.Connect(Server, Username, Password, InPort, UseSSL) 'Connect to user account
'-----------------------------------------------------------
Dim Stats() As Integer = InMail.Statistics() 'get maildrop statistics (number of message, total byte size)
If Stats(0) = 0 Then 'check number of messages for being 0 (none)
Return Nothing 'no email found in the maildrop, so nothing to do
End If
Dim localList As New ArrayList 'set up list of emails that will contain message text
For Each msg As POP3Message In InMail.List 'parse each header object (contains only index and size)
localList.Add(InMail.Retrieve(msg)) 'add a message object with message text
Next 'process all messages
'-----------------------------------------------------------
InMail.Disconnect() 'disconnect from server (DISABLE THIS LINE TO KEEP EMAILS ON SERVER FOR TESTING)
Return localList 'return list of filled POP3Messages to invoker
Catch e As POP3Exception 'POP3-side error
MsgBox(e.Message,
MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation,
"Error Encountered")
Catch e As Exception 'general programming error
MsgBox(e.Message,
MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation,
"Error Encountered")
End Try
Return Nothing
End Function
'-------------------------------------------------------------------------------
' Sample Code
'-------------------------------------------------------------------------------
'*******************************************************************************
' Sub Name : SampleReadEmail
' Purpose : Samples method to Read a POP3 account maildrop (using above SampleReadPOP3 function)
' NOTE : and display each unprocessed message in a Message Box.
'*******************************************************************************
Friend Sub SampleReadEmail()
Dim EmailBag As ArrayList = SampleReadPOP3("pop.gmail.com", "bob.the.builder@gmail.com", "YudLk2Knw", 995, True)
If EmailBag IsNot Nothing AndAlso EmailBag.Count <> 0 Then 'if data was found
For Each msg As POP3Message In EmailBag 'display each email
MsgBox(msg.Message,
MsgBoxStyle.OkOnly Or MsgBoxStyle.Information,
"Message # " & msg.MailID.ToString & " of " & EmailBag.Count.ToString)
Next 'process all messages
Else 'transfer here if no one loves you
MsgBox("No email found on server.", MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "No Email")
End If
End Sub
End Class
'*******************************************************************************
'*******************************************************************************
' Class Name : EmailItem (used by EmailInfo class)
' Purpose : Stores structure of an email block
'*******************************************************************************
Public Class EmailItem
Public ContentType As String = Nothing 'CONTENT-TYPE data
Public ContentTypeData As String = Nothing 'filename or text encoding
Public ContentTypeDataIsFilename As Boolean = False 'True if ContentTypeData specifies a filename
Public ContentEncoding As String = Nothing 'CONTENT-TRANSFER-ENCODING data
Public ContentBody As String = Nothing 'raw data of block
End Class
'*******************************************************************************

'*******************************************************************************
' Class Name : EmailInfo (used by GetEmailInfo method)
' Purpose : Store component parts of an Email
'*******************************************************************************
Public Class EmailInfo
Public FromData As String = Nothing 'FROM:
Public ToData As String = Nothing 'TO:
Public DateData As String = Nothing 'DATE:
Public SubjectData As String = Nothing 'SUBJECT:
Public MessageBody As EmailItem 'contents of message body
Public AlternateViews As New List(Of EmailItem) 'list of alternate views
Public Attachments As New List(Of EmailItem) 'list of attachments
End Class
 Page 447

Conclusion.
This concludes this article on Internet SMTP and POP3 Email processing, but it is certainly not the end
of what can be done. For example, SMTP and POP3 both inherit from System.Net.Socket.TcpClient.
What does this mean to us, you may ask? It means that we can create our own SMTP Mail object, and
send email the way WE want to send it. For instance, with the .NET Mail class we cannot send the main
body of an email as Rich Text (remember that IsBodyRtf option I mused about? You can add it in our
own version of the class), or we cannot send a message body coded 7bit. With our own Mail class, we
could, just as we can in Outlook or Windows Mail. We can even create a proprietary email format that
can only be properly read by our email application, to include encryption and other protections to keep
our or our company’s information safe.
Actually, the underpinning of such a class is already wrapped up within our POP3 class.
NOTE: Vetorri Massimo had informed me that in using the GetEmailInfo function in the Utilities class
on some emails did not return a message body. After investigating, I discovered that some emails do not
specify Boundary strings, and because of this the gathered data is never collected into the EmailInfo
object’s MessageBody member. I managed to resolve this by checking at the end of the function to see if
the MessageBody member is not yet set to an object. If not, a new EmailItem object is instantiated,
filled, and then assigned to the MessageBody member. The listed GetEmailInfo method in this article
includes this fix.
 Page 448

Black Book Tips.


Tricks and Tips for Extending VB.NET Beyond Our Expectations.
 Page 449

Additional Black Book Tips.


This entire document was originally my personal “Black Book”. A Black Book is typically one’s Go-To
Guide that stores our personal, preferred solutions to typically complicated or hard to remember things,
being a ready reference so that we do not have to cram so much into our brains (or things we inevitably
forget, but, usually, just before we need to use them). Indeed, I was originally going to release this entire
document as “My VB6 to VB.NET Black Book”, but because numerous other black books have already
been released, and this term, once marketing departments got their fists wrapped around it, have tended
to over-use it to the point that it does not have the same powerful meaning that it once had, I chose to
give the document an alternative, though meaningful title, first as “Visual Basic .NET Compared to
Visual Basic 6.0”, and finally as “Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0”.
Since I first put this treatise to e-book form in September of 2009 as a 23-page personal collection of
notes, and finally publishing it online in July of 2010 with 140 pages, it had gone through a steady
evolution all the way through early 2012. After that, it has sat relatively idle, getting a minor tweak now
and then as I found better solutions than the ones I previously shared, but there were no major updates.
However, being that I am knee-deep in a mega project that has required me, almost on a daily basis, to
invent sometimes very unique, and some tell me quite patentable solutions to problems, I decided that
instead of concealing many of those solutions, I would share them in an additional section, here.
Some of what I share here are sometimes quite mundane, and sometimes even brain-dead simple, though
it seems that there are still a great many users out there who are in desperate need of finding a solution
to such problems, whether it is because they cannot see the forest due to the trees because they are so
close to the problem, or they just do not have enough experience in a particular arena of knowledge (I
have been there plenty of times). Such tips include adding a file to the Recent Folder, or placing a link to
the application in their SendTo folder, or how to create shortcut files, how to get the file path from a link
file, sizing a TextBox to fully contain and expose provided text, how to display TextBox data as justified
text (great for About Boxes), how to associate unique file extensions to our application, how to get
instances of our app, ascending or descending sorts on any column in a ListView control, easily re-
dimensioning leftward dimensions in multi-dimensional arrays, dithering a form’s background, easily
recovering crashed menus, bypassing the default TextBox context menus, locking out or hiding tab
control pages, easily tracking ComboBox items under the mouse pointer, easily cleaning up image
backgrounds on menus, toolbars, and context menus on the fly, how to do very easy owner drawing on
TabPages, TreeViews, and other controls, making printing brain-dead simple, even from a RichTextBox
and with full support, plus many, many other tinker-toys I keep within my personal bag of tricks.
A tip recently added shows us how to enable Full-Text-Justification in a RichTextBox control; a feature
that has actually been built right into these controls ever since Windows XP was released, but for some
reason it had been locked away (See Black Book Tip # 30). For all the fuss people have made about it
over the years on so-called expert forums, it is actually almost brain-dead simple to enable. But this
really confuses me, because for all those online gurus and forum monitors expounding about it and
boasting “this is how you do it”, every one of them had got it wrong! They kept forgetting to perform the
one simple step that would have made their solutions work: the one puny line of code required to enable
the Advanced Typography Options in a Rich Text control. Interestingly, reading through Microsoft’s
online documentation on this control just once clearly tells anyone bothering to read it exactly how to
enable it. It seems few of these gurus actually read documentation, unless perhaps forced to at gun-point.
But justification does not end there. In Black Book Tip # 9 on page 470, I show how to display justified
text in a TextBox, and in Black Book Tip # 46 on page 614, I show a very simple method of displaying
justified text not just in a TextBox or Label, but even in our MsgBox dialogs.
So, what follows is a sampling of the additional tricks beyond what I have already drawn up for us...
 Page 450

Black Book Tip # 1


Bypassing Click Events When a Usual Context Menu is Not to be Displayed on a Right-Click.
Suppose we throw up a Context Menu when we right-click a PictureBox by monitoring the right mouse
button in a MouseDown event, except when conditions exist precluding a need for that menu. Normally,
control simply returns without displaying the menu. But what happens if we must also monitor the
control’s Click, MouseClick, or MouseUp events to perform other tasks, such as when we left-click the
control to pop up a properties dialog or to deselect another selection? For example, this can happen in a
spreadsheet where we need to click a mapped grid cell to deselect a selected cell range if that cell was
not also within the selected range, or to hide a floating properties box for a previously selected cell.
If a right-click on this PictureBox is determined not to require a Context Menu, then the pending mouse
event messages, normally consumed by the now-missing Context Menu, will still exist, and so they will
funnel on down through the Message Queue and through any Click, MouseClick, or MouseUp events
before they are ultimately discarded from the queue. Because, left or right, we still clicked on the
PictureBox. And, as long as we support other events like Click, MouseClick, and MouseUp in our code
for that control, those orphaned messages from MouseDown will find their way through them.
NOTE: PictureBox controls, like Forms, but unlike Button controls, will fire a Click event on both left- and right-clicks.
NOTE: Mouse button events fire in the following order: MouseDown, MouseClick, MouseUp, and finally Click.

Some developers try checking for the right mouse button being used during these events, leaving via a
Return instruction if it is detected. The MouseDown, MouseClick, and MouseUp events do in fact expose
the needed MouseEventArgs parameter to test for this, but not in a Click event. The MouseClick is
normally a good alternative to a Click event when we need to determine which button was used, but they
differ enough so that this is not always applicable. For example, pressing the ENTER key will fire a
Click event, yet it does not also fire a MouseClick event. Some have resorted to the GetKeyState Pinvoke
to check the state of Keys.RButton, but this is useless because the Click event only fires after a mouse
button is already released. Truth is, all these solutions are useless because our goal here is to ignore all
these system messages when a Context Menu is not to be displayed until after MouseUp is triggered.
Some developers have done lots of hair-pulling in order to surmount this issue, and all they end up doing
is getting frustrated and bald, because the program is simply doing exactly what it was designed to do.
The ideal solution would have been for the MouseEventArgs object to feature a “SupressMousePress”
flag that would act much like the SupressKeyPress flag exposed by a KeyEventArgs object during
keyboard handling. This way we could stop further processing of the mouse button from the
MouseDown event so that the Click, MouseClick and MouseUp events would never be issued a message
in the system message queue (this was also the suggestion I submitted to Microsoft).
Until such a time, my current solution is to declare a local flag, such as Private pSupressMousePress As
Boolean = False, and set it to True when my application determines that it will not be displaying its usual
Context Menu within the MouseDown event. Thus, if my application also monitors MouseClick, or
MouseUp or Click events, then at the start of my Click or MouseClick event, which are handled before a
MouseUp event, check the pSupressMousePress flag, and if it is set, then simply return to the invoker:
If pSupressMousePress Then 'Skip this click event because we right-clicked but no context menu was displayed?
Return 'Yes, so leave (let MouseUp finish up and actually reset the pSuppressMousePress flag)
End If
'...Any other code you require in this event should follow if pSupressMousePress was not set...

Finally, in a required MouseUp event, even if the following example comprises its only code, likewise
checks the flag, but if it is set, then reset the flag and return without doing anything else:
If pSupressMousePress Then 'Skip this event because we came from a right-click with no context menu?
pSupressMousePress = False 'Yes, so first reset the flag...
Return 'and then leave without doing anything else.
End If
'...Any other code you require in this event should follow if pSupressMousePress was not set...
 Page 451

Using this simple technique, we can completely eliminate the click-dribbling headaches that typically
come about with such feature-rich applications that support conditional context menus along with
though other mouse event tasks.

BONUS TIP: Regarding the mentioned GetKeyState Pinvoke (Private Declare Function GetKeyState Lib "user32.DLL"
(ByVal nVirtKey As Int32) As Int16), this Pinvoke is also really useful when we are monitoring a Scroll control’s Scroll
event. The Scroll event fires whenever we click on the Scroll control, press the left mouse button down on it, and when we
release the left mouse button over it. All these reactive events typically result in redundant code processing and sometimes
even to a jerky display if there are a whole lot of things we have to do in that code. Using the GetKeyState Pinvoke, we can
quickly check the key state of the left mouse button ( Keys.LButton) at the top of our event code and see if it is greater than or
equal to zero, which indicates that it is not pressed. If so, we can actually ignore further processing of our Scroll event code
and just return to our invoker, thus totally eliminating the jerkiness and redundant invocations. This positive state will be the
case during a MouseUp, MouseClick, and Click events, all of which do not fire until the mouse button is actually released.
Also, in case we were not aware of it, the e.KeyCode property of the KeyEventArgs employs the Keys enumerated list of
Virtual Keys, which are Int32 constants mapped to the keyboard and mouse, exactly mirroring the Virtual Key Codes defined
for use by Visual Studio C++ and VB6 development (usually via the VB6 API Viewer Add-In), though .NET developers using
Pinvokes often employ them as well, not realizing they can use the Keys enumeration instead of a declared constant. All we
really have to do is type “keys”, followed by a Dot, to see a list of all these enumerated Virtual Keys (see MSDN for
additional details on virtual keys).

NOTE: I wrote the DotNET API Viewer application, available for free from Planet Source code ( http://www.planet-source-
code.com/vb/scripts/ShowCode.asp?txtCodeId=8293&lngWId=10&txtForceRefresh=213201411542115354 ), in source code form only, which provides
Pinvoke support for both VB.NET and C#.
The DotNET API Viewer for VB.NET and C# is similar to the VB6 API Viewer, but on steroids. The stand-alone DotNET API
Viewer application, written in VB.NET, makes pasting new API declarations into our VB and C# .NET applications a breeze.
 You can dynamically declare constants and Enumerators as Integer in the selected
local copy of a constant or enumerator. This allows the user to keep program speed
optimal by not slowing down for object (variant) conversions. Although constants
can be of types other than Integer, the .NET interop API interface uses only Integer
values for its constants.
 You can create new Constants right within the viewer. Assigned values are
expected to be numeric or numeric constants, as was required by the VB6 API
interface, but numeric values can be declared as hexadecimal, octal, or binary. You
can also apply + or - offsets. Constants are created as all-capitals. No complex
checks are performed on the value. It simply assumes that you know what you are
doing, because such checks can involve complex offsets and naming of other
constants. The viewer will also check to ensure that the newly entered constant
does not already exist.
 You can create new API method Declarations right within the viewer and add them
to your API library list.
 You can create Overloaded Declarations of methods right within the Viewer and
add them to your API library list.
 You can create new Structures and Enumerations right within the viewer and add them to your API list.
 You can Delete entries from the API library list.
 You can edit Declared Subroutine and Function parameter lists, and apply these changes to new subroutine or function names. The
built-in Declaration Editor makes such changes a breeze with just a few clicks of the mouse.
 It automatically checks for new parameter dependencies. If an added declaration or structure requires another structure or constant not
included in the selection list, you can view the requested types in a dialog and select them or reject them for inclusion in your selection
list, or set and option to automatically include them. This can make resolving declaration headaches such as with the complicated
AccessCheck Function, which requires the additional inclusion of the GENERIC_MAPPING, PRIVILEGE_SET, and
SECURITY_DESCRIPTOR structures. These additional structures in turn require the ACL and LUID_AND_ATTRIBUTES
structures and the ANYSIZE_ARRAY constant. These newer structures in turn also require the LUID structure. The New API Viewer
makes farming these additional types a breeze with a few quick clicks of the mouse.
 Additions created within the New API Viewer can be optionally saved for later re-use in the API library file. New entries are appended
to the API file with a date- and time-stamp marker.
 Plus many more features.
 Page 452

Black Book Tip # 2


Creating an Association Between a Unique File Extension and Our Application.
When we select most files in File Explorer, the executable program they are associated with (one that
supports the file’s data format) is launched and it will load or run that file. For example, text files with
the file extension “txt” are usually associated with a text editor, such as Notepad, unless we have
changed that association. If the file is not associated with any particular executable, then we are
prompted to select or browse for a compatible application that we want to process that file with.
In many cases I have seen on the web, the user is instructed to right-click the uniquely-extended file if the
Open With dialog does not come up, select the Open With... context menu option, and then select or browse
to the appropriate application so that afterward that application will be associated with that type of file.
This is all well and good, but it is also entirely unnecessary within our own applications, and is also a bit
amateurish if we expect the user to perform this task in order to use the files our application creates. This
is because we can easily force this association between a unique file extension and the desired
executable with just a few simple lines of code, all neatly wrapped within a function, and our end-user
need do nothing but take advantage of the fact that we have created this association for them, though, of
course, we should offer that to them as an option, in case the extension is also used by another program.
If we are one to explore the Registry using RegEdit or some other Registry viewer, then if we look in the
HKEY_CLASSES_ROOT hive, or at its reflection in HKEY_CURRENT_USER\Software\Classes, we will
see a lot of entries that are typically 3 text characters preceded by a period (a dot). These are file
extensions known to the operating system. Their default entry contains the registry entry that files of that
extension are to be associated with.
We have sole interest in its Default registry entry.
We need not concern ourselves with any other
sub-keys or sub-folders defined within any given
extension, as we might see here, to the right.
If we examine this referenced entry, in this case
txtfile, we will notice a subfolder named shell.
This is the only path we need to be interested in
right now. It has a sub-folder named open, and it
in turn has a sub-folder named command.
Here, we will see that the default entry for the shell\open\command path is the Notepad executable with
at least one parameter list placeholder. Applications that can use multiple files usually have a list of
numbered parameters (for safety, when I define such entries, I wrap the executable and each individual
optional parameter extension within quotation marks because any one of them could potentially contain
spaces that can sometimes cause confusion about the delineation of a file if they were not present).
Unlike VB6, under VB.NET the process of creating such registry entries is almost brain-dead simple.
For example, suppose we need to associate the file extension “xyz” to our application. To do this, we
need only three pieces of information:
1. The lower-case file extension that we want to have associated with our executable.
2. The full path to our executable that the files with the target extension are to be associated with.
3. The name for a registry entry that will link the extension to the executable file.
By default, we can define the linking entry by simply using the text of the extension and tacking “ file”
onto its end, such as “xyzfile” for the “.xyz” extension. The executable file, if we are running this code
from the application startup (it is OK to run it each time we run our application, in case the executable
location is changed by us or our user), such as within the main form’s Load event. We can grab the full
path to our application’s executable with the simple command, “Application.ExecutablePath”.
 Page 453

We need execute only a couple lines of code to create all of the required registry entries. The first line
will create the file extension entry. For it, if we are using a file extension of “. xyz” and linking it to
“xyzfile”, we could first construct the preliminary command line to add this “.xyz” entry.:
'Add a registry entry that will notify the operating system of the file extension we are going to associate
My.Computer.Registry.CurrentUser.CreateSubKey(
"Software\Classes\.xyz").SetValue(Nothing, "xyzfile", Microsoft.Win32.RegistryValueKind.String)

Notice that we are not providing a registry key name as our first parameter, but rather we give it a value
of Nothing. Doing so will tell the system that we actually want to write this data to the key’s Default
entry (also known as the @ entry). Further, notice that we actually perform two separate tasks at the very
same time. The first half creates the Sub-Key “.xyz” (notice also that we cheated by defining it within
the HKEY_CURRENT_USER hive, which actually sets the Classes data within HKEY_CLASSES_ROOT
and simply reflects the data back to this location, all without having to set a ton of security features if we
had wanted to access HKEY_CLASSES_ROOT directly. The operating system simply ensures that what
is being set is safe). The second part (starting with “.SetValue”) is setting the actual default value for the
now-created “.xyz” entry that will in turn reference our yet-to-be-created “xyzfile” entry.
The second line of code does basically the exact same thing, but it creates the full
“xyzfile\shell\open\command” registry path and also sets the default value within the command folder to
our executable. Notice, below, that I am actually taking an additional step by setting up our executable
path and embracing its optional parameter list of place-holders within the safety of quotation marks, just
so the line does not run off the end of the page (even though this still could have been done in one line):
' set up a command path that includes a parameter substitution list (note that 2 double-quotes defines a single double quote)
Dim CmdPath As String = """" & Application.ExecutablePath & """ ""%l"" ""%2"" ""%3"" ""%4"" ""%5"" ""%6"" ""%7"" ""%8"" ""%9"""
' create the application reference and also the registry shell command to open Extension-type files using the Exe.
My.Computer.Registry.CurrentUser.CreateSubKey(
"Software\Classes\xyzfile\shell\open\command").SetValue(Nothing, CmdPath, Microsoft.Win32.RegistryValueKind.String)

And that is all there is to creating a file extension association with our application.
NOTE: The “shell” part of the path refers to the Windows Shell, such as Shell32.DLL or Shell64.DLL. This Shell is the
Windows Kernel; the heart of the Windows Operating System. A Kernel is a geek term for a seed; an origin point. This
comprises what had once been the DOS Shell that MS-DOS originally spawned from. Indeed, this is why we have a Shell
command in VB to execute DOS Shell instructions. On top of that, Windows originally ran on top of DOS, and if we are old
enough, we may remember typing “win” from a DOS Prompt to launch Windows 1.0. through Windows 3.1. It was not until
the Network version, Windows 3.11, that Windows began absorbing the DOS Shell and automatically firing up in Windows. I
am only 65, but suddenly I want to look for a walker or cane, and see if I can leave a left turn signal on for a week or two.
NOTE: As noted in the code comments, 2 double-quotes forms 1 double-quote, though some might be rightly confused to see
me using 4 double-quotes (““””) in the code. This is because the 2 outer double-quotes are used to embrace text, and the 2
inner double-quotes comprise that embraced text. This also had me confused the first time I ever saw them etched on a cave
wall, next to some bison with spears sticking out of them, chased by little stick figures dressed like Tarzan.
NOTE: We will only want to add the number of parameters that our executable can handle. For example, if it can process
only one submitted file, use: """" & Application.ExecutablePath & """ ""%l"""
Of course, we may want to use this in other applications that we may have, so we would probably want
to define a method within a reusable Module file. I use a function that returns several flags in case I goof
the works up in the parameters I provide when I am first stitching the code together. The following will
define a function named SetFileAssociation that expects two parameters with an optional third, to define
the linking entry within the registry. We can supply the file extension with or without a leading Dot, and
the intermediate registry entry defaults to the text of the provided file extension plus “file”. Hence,
SetFileAssociation(Application.ExecutablePath, "XYZ") will create a registry entry named “.xyz” (note the
lowercase) that will reference “xyzfile” (also note the lowercase), and xyzfile will in its turn reference
our target executable, complete with 9 optional parameters:
 Page 454

'*******************************************************************************
' Method Name : SetFileAssociation
' Purpose : Associate QWS file extension with an application program that processes files with a specified extension
' Parameters :
' :AppExePath: The full filepath, including the full name, of the executable that is to be invoked when a file
' with the provided extension is selected.
' Extension: The file extensions, such as "txt" or "myExt", that should be associated with AppExePath.
' AppReference: Optional parameter to specify the File Association within the registry. By default, it is the text
' of the Extension plus "file". For example, if Extension is set to "txt", then AppReference
' would become "txtfile".
' NumParams: Number of file parameters to add (1-9).
'
' Return Values: 0 = Success.
' -1 = No AppExePath provided.
' -2 = AppExePath is invalid.
' -3 = No extension provided.
'*******************************************************************************
Friend Function SetFileAssociation(ByVal AppExePath As String,
ByVal Extension As String,
Optional ByVal AppReference As String = Nothing,
Optional ByVal NumParams As Int32 = 1) As Int32
If NumParams < 1 OrElse NumParams > 9 Then 'keep number of parameters in 1-9 range.
NumParams = 1
End If
If Len(AppExePath) = 0 Then 'if the Executable was not provided...
Return -1 'indicate that this was so and do no more.
End If
If Not IO.File.Exists(AppExePath) Then 'if the selected executable cannot be found...
Return -2 'indicate that this was so and do no more.
End If
If Len(Extension) = 0 Then 'if the desired file extension was not provided...
Return -3 'indicate that this was so and do no more.
End If
Extension = LCase(Extension) 'ensure we are lower case!
If Extension.Substring(0, 1) <> "." Then 'ensure it has a leading Dot.
Extension = "." & Extension
End If

If AppReference Is Nothing Then 'if the registry application reference was not provided...
AppReference = Extension.Substring(1) & "file" 'create our own (also lower-case)
End If

If InStr(AppExePath, " ") <> 0 Then 'does the executable path contain any spaces?
AppExePath = """" & AppExePath & """" 'embrace app path with quotation marks if it contains spaces.
End If
Try
' Add a registry entry that will notify the operating system of the file extension we are going to associate.
' Nothing references the Default.
My.Computer.Registry.CurrentUser.CreateSubKey(
"Software\Classes\" & Extension).SetValue(Nothing, AppReference, Microsoft.Win32.RegistryValueKind.String)
' set up a command path that includes a parameter substitution list.
Dim CmdPath As String = AppExePath & " ""%l"""
For Idx As Int32 = 2 To NumParams 'add selected number of parameters beyond 1
CmdPath &= " ""%" & Idx.ToString & """"
Next
' Now create the application reference and also create the registry
' shell command to open Extension-type files using the AppExePath.
My.Computer.Registry.CurrentUser.CreateSubKey(
"Software\Classes\" & AppReference & "\shell\open\command").SetValue(Nothing,
CmdPath,
Microsoft.Win32.RegistryValueKind.String)
Return 0 'return success.
Catch
Return -4 'return failure to create the registry entries.
End Try
End Function

NOTE: We should actually make associating our file types an option to our user, to let them decide if they want to associate
the files with our application, because that file extension might also be associated with another application. In that case, if
they say NO, we should leave the door open to them under our File or Option menu for them later to bind the association if
they change their minds.
 Page 455

Black Book Tip # 3


Get the Linked File Path From a Shortcut File.
Sometimes within certain applications we need to know the actual file that a Shortcut file (a Link file) is
linking to. A Shortcut file, of course, is simply a file that we might find on our desktop or elsewhere that
has a hidden “.lnk” file extension (so hidden, in fact, that the only way we are usually able to actually
see that its extension is in fact “.lnk” is through a DOS shell or through directory parsing).
Back in the days of VB6, one common cheat (which, technically, we can still use, even though it is so
brilliantly crude) was to parse backward through the Link file and searching for the character sequence
“:\”. Once we encountered this, we knew we had found the drive path to our link-to file. We then parsed
backward until we encounter a Null to pick up either the drive letter or the network root drive name, and
then parsed forward to find a Null at the end of the path. Once we had that, we had the full target path.
However, a much cleaner, more direct, and much more robust method is to implement the COM-based
Shell32.DLL, which is a DLL that is as old as Microsoft Windows itself (well..., maybe back then it was
just called Shell.DLL, just as now Shell64.DLL is the new normal on 64-bit systems), and is the
operating system’s primary workhorse, performing most any task required of the Windows interface
shell, which covers just about anything we can imagine regarding files, folders, the registry, memory,
and maybe the kitchen sink.
But what is important to us at present is its ShellLinkObject class. A ShellLinkObject is a class that
simply describes a shortcut file, exposing read/write properties for its icon information, plus the
properties Arguments, Description, WorkingDirectory, Hotkey, and Path. This last property is the path
that the shortcut links to (another property, Target, is simply another ShellLinkObject reference used for
re-direction). Of all these properties, we are interested only in Path.
With the above knowledge, the process of extracting the Path property from the shortcut is relatively simple.
We first need to set a COM reference to Shell32.DLL in our application. Go to our Project Properties,
select the References tab, select the “Add...” button, select the COM tab on the Add Reference dialog, hit
the M key to skip down to the M’s in the list, and scroll to the “Microsoft Shell Control and Automation”,
click on the entry to select it and place a check in its checkbox if it has one, and finally click the OK
button to add it. The IDE will create a non-COM version of the DLL named interop.shell32.dll. It will
also recreate it during any build if we ever delete it, as it is in our references.
But to reference Shell32.DLL is not enough. We also have to set a special parameter to prevent the
Interop Types defined for Shell32 in our application’s assembly file from being embedded within our
application. This step is important, as otherwise we will not be able to assign its all-important
ShellClass object to anything. For example, after adding a Shell32 reference to our application, if we
were to enter the following line into our code (as we will shortly need to):
Dim Shell As Shell32.Shell = New Shell32.ShellClass 'define our shell class.

We will be quick to notice that the .NET cross-language compiler will flag this line to be in error.
Specifically, it will have an issue with Shell32.ShellClass by underscoring it with a red tag and tell us in
the Error List panel that “Interop type 'ShellClass' cannot be embedded. Use the applicable interface instead. ”
If we are left scratching our head, we are not alone. It is a really crappy error message that, though it
may well state the problem (that ShellClass cannot be embedded within the application), it is likewise
too pithy to really clue us into what we need to do to fix it. Specifically, what does it mean: use the
applicable interface instead? However, what this does mean is that we should only use these objects as
delivered by the operating system instead of trying to embed these core operating system Interop Types.
Technically, it needs to use the GUIDs (Global Unique Identifiers) as delivered by the provider, not
automatic GUIDs generated by the .NET root Common Language Runtine for embedded objects.
 Page 456

When we add a COM reference to our application, it embeds its Interop Types in
our application by default. When interfacing with the core operating system, this
is an instance where we do not want to do that (we might notice other auto-
added references in our project also do this). We actually want it to derive this
class from outside our code, from the operating system itself, and so we must not
embed the interface within our application code. So what we need to do is set
Shell32’s Embed Interop Types parameter to False from its default True state in
the properties for the Microsoft Shell Controls and Automation reference, as
shown to the right (we can view this property from the Reference Tab on the
Application Properties page when we click on the added Shell32 reference entry).
In our code, we next define a string variable to hold the path to the link file we want to read, to include
its “.lnk” extension. This simply consists of the folder path where we are accessing the link file, such as
the Desktop (Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)), the name of the
shortcut file to access, plus “.lnk” at the end. For example, if we want to access a Desktop shortcut
named BetterSlicedBread, we could set up a string variable to acquire the full path using the following:
' build full path to the Link file (note that I keep the addition of .lnk separate to highlight the fact that we need to add it)
Dim ShortcutFilepath As String =
Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) & "\BetterSlicedBread" & ".lnk"

What we need to do next is to define a ShellClass object, derive from that a shell Folder object that is
associated with the folder path to the shortcut, and derive from that a shell FolderItem object that refers to
the shortcut file. We can do all these seemingly complex tasks by using the following three lines of code:
Dim Shell As Shell32.Shell = New Shell32.ShellClass 'define our shell class (ensure its Embed Interop Types parameter = False)
Dim Folder As Shell32.Folder =
Shell.NameSpace(System.IO.Path.GetDirectoryName(ShortcutFilePath)) 'define folder object to link file's directory folder.
Dim FolderItem As Shell32.FolderItem =
Folder.ParseName(System.IO.Path.GetFileName(ShortcutFilePath)) 'define link to shortcut file object from folder object.

All that is left to do is to pick up the actual path from the FolderItem object, which is the point of this
whole exercise. We can pick up this path property using the following single line of code:
Dim LinkToPath As String = DirectCast(FolderItem.GetLink, Shell32.ShellLinkObject).Path

Like with anything useful, we may want to reuse this code, so let us place it inside a function:
'*******************************************************************************
' Method Name : GetShortcutLinkToPath
' Purpose : Retrieve the command path a shortcut file links to
'
'This method requires COM references to:
' Microsoft Shell Controls and Automation (Shell32.dll), and ensure its Embed Interop Types parameter = False.
'*******************************************************************************
Private Function GetShortcutLinkToPath(ByVal ShortcutFilePath As String) As String
Try
Dim Shell As Shell32.Shell = New Shell32.ShellClass 'define shell class obj as link to OS shell.
Dim Folder As Shell32.Folder =
Shell.NameSpace(System.IO.Path.GetDirectoryName(ShortcutFilePath)) 'define folder obj to link file's dir folder.
Dim FolderItem As Shell32.FolderItem =
Folder.ParseName(System.IO.Path.GetFileName(ShortcutFilePath)) 'define link to shortcut obj from folder obj.
If FolderItem IsNot Nothing Then 'if it exists...
Return DirectCast(FolderItem.GetLink, Shell32.ShellLinkObject).Path.Trim 'then return link's command path.
End If
Catch
End Try
Return Nothing 'otherwise failure, so indicate so.
End Function

BONUS TIP: After creating a shell FolderItem, we can persist access to the shortcut’s properties by using something like
“Dim ShortCutObj As Shell32.ShellLinkObject = DirectCast(FolderItem.GetLink, Shell32.ShellLinkObject)”. With this
object we can examine or alter the other properties exposed by it, which are all text properties, such as its arguments,
description, working directory, icon (the path to the icon, plus an optional index, such as myApp.exe, 0, which is the default
embedded icon index), path, and HotKey (like “Alt-F11”). To update the shortcut in case we made changes that we want to
keep, simply remember to invoke the object’s Save method before leaving, such as “ShortCutObj.Save”.
 Page 457

Black Book Tip # 4


Create a Shortcut File Within Our Code.
If we have ever needed to create a shortcut that the user can select to launch our application or an
auxiliary file, the process to do so is actually quite simple. We can accomplish this using a simple COM
Reference to the Windows Scripting Host Object Model (WSHOM; IWShRuntimeLibrary.DLL). Refer to
the last tip to see how to add a COM reference to our application.
NOTE: Also, just like with Shell32.DLL, actually operates entirely from within the operating system and so it’s Interop Types
cannot be embedded within our application, in the properties for the Windows Scripting Host Object Model reference; we must
ensure that its Embed Interop Types parameter is set to False from its default True state.

A shortcut file actually consists of a number of properties, all but one of which is optional:
Link-To Path....................(Required) A text string holding the full path, including the folder, filename, and extension, for the
file or executable we want the shortcut to re-direct to.
ShortCut Name................(Optional) A text string holding the name we want to be displayed on the shortcut icon. This
comprises the actual name of the shortcut, plus a hidden “.lnk” extension. If not supplied, we can
extract a default name from the Link-To Path’s filename and extension.
Shortcut Folder...............(Optional) A text string holding the destination folder path where the shortcut will be created, such
as the path to our Desktop. If none is provided, the Desktop is assumed.
Shortcut Description.......(Optional) A text string holding text that would simply describe to users what the purpose of this
shortcut is in the link file’s properties page.
Working Directory..........(Optional) A text string holding the folder path we want the linked application or file to treat as its
local folder. By default, this is the folder holding the linked file or executable.
Shortcut Icon...................(Optional) A text string holding the full path to an icon file, icon library, executable, or DLL. This
will tell us which icon to display for this shortcut. If the file has multiple icons, choose the one we
want to use by appending a comma and then the icon index to use. For example,
“C:\MyFolder\MyApp.exe, 1” to select the second icon in the icon list (default is “, 0”). The
space after the comma is optional. By default, if no icon is provided, it will extract the main icon
from an executable or from the executable associated with the file if it is a non-executing file.
Shortcut Arguments........(Optional) A text string holding an optional argument list we want to supply the shortcut with.
Typically, this is not necessary because parameters, such as files to process, will append just fine,
but such parameters would typically be special manual instructions, such as application-
meaningful instructions to do something, like setting up a required special screen resolution.
Window Style...................(Optional) An integer value indicating how the target should be displayed (if it is displayable),
such as Normal (0; default), Minimized (1), or Maximized (2).
The above seems like a lot to consider, but if we think about it, it all makes perfect sense. The path to the
shortcut that is to be created (or over-written) is constructed by combining the Shortcut Folder with the
Shortcut Name, and adding “.lnk” to the end. If our application has parameters, then we will want to include
those with the Shortcut Arguments. If the user is curious about what that icon is that they just found on their
desktop, in examining its properties, it will give them a Description. If our executable operates from one
directory, but it should treat another as its local working directory, which might contain additional support
files, we will need the Working Directory parameter. If we want the application to start up minimized or
maximized, then we will need the Window Style parameter.
With a reference made to Windows Scripting Host Object Model, and its Embed Interop Types parameter
set to False, we are ready to begin.
The first thing we would want to do is set up the actual path to the shortcut file we are going to create ( or
over-write). We want to also check to see if the Shortcut Folder is supplied. If not, we need to pick up the
path to the Desktop. Suppose we had two string fields, one named ShortcutName that holds the name for the
shortcut to create, such as “MyApp”, and the other named ShortcutFolder, which holds the path to the
directory where we want to create the shortcut. With that, we can prepare for creating the shortcut with the
following lines of code:
 Page 458

If ShortcutFolder = vbNullString Then 'is ShortCutFolder not defined?


ShortcutFolder = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) 'no, so default to the desktop.
End If
Dim ShortcutFilepath As String = ShortcutFolder & "\" & ShortcutName & ".lnk" 'build full path to Link file.

With that, we are ready to create the shortcut object. This only takes two lines of code:
Dim wshShell As New IWshRuntimeLibrary.WshShellClass 'define our shell class and our Shortcut object.
Dim MyShortcut As IWshRuntimeLibrary.IWshShortcut =
DirectCast(wshShell.CreateShortcut(ShortcutFilepath), IWshRuntimeLibrary.IWshShortcut)

Now, we are ready to define its properties, such as applying the Link-To Path. For this example, we will
supply only the Link-To path, which we have stored in a string variable named LinkToPath. We will let
the system assume the working directory is the execution folder, the icon is the main icon for the file
specified in LinkToPath, it will have no description, no arguments, and the program will start up
displayed normally. To do all this and save the shortcut file, we issue the following two lines of code:
MyShortcut.TargetPath = LinkToPath 'then set the linking path.
MyShortcut.Save() 'finally, save the shortcut file and close the object.

As we can see, there is nothing at all complicated about this process.


For re-use, let us create a method that can be invoked to create as many shortcuts as we require, and with
as many parameters as we need. Consider the following method that can be placed in a module:
'*******************************************************************************
' Method Name : CreateShortcut
' Purpose : Create Windows Shortcut
'NOTES:
' LinkToPath = Full path, including executable, to the target of the shortcut.
' ShortcutName = Optional Shortcut Name to display for link file.
' ShortcutFolder = Optional Destination folder for shortcut (default=Desktop).
' ShortcutDescr = Optional description for the shortcut.
' WorkingDir = Optional working directory path for the target file.
' ShortcutIcon = Optional path to an Icon file (this can also be the file, itself).
' ShortcutArgs = optional Argument list for the target file.
' WinStyle = App display style (default is Normal Focus).
'
'This method requires COM references to:
' System Windows Object Model (IWShRuntineLibrary.dll), and ensure its Embed Interop Types parameter = False.
'*******************************************************************************
Friend Sub CreateShortcut(ByVal LinkToPath As String,
Optional ByVal ShortcutName As String = Nothing,
Optional ByVal ShortcutFolder As String = Nothing,
Optional ByVal ShortcutDescr As String = Nothing,
Optional ByVal WorkingDir As String = Nothing,
Optional ByVal ShortcutIcon As String = Nothing,
Optional ByVal ShortcutArgs As String = Nothing,
Optional ByRef WinStyle As Windows.Forms.FormWindowState = FormWindowState.Normal)
'-------------------------------------------------------------------------------------------
'get path where to place the shortcut. If it is Nothing, then default to the Desktop.
'-------------------------------------------------------------------------------------------
If ShortcutFolder = vbNullString Then 'is ShortCutPath not defined by the user?
ShortcutFolder = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) 'no, so default to the desktop.
End If
If ShortcutName = vbNullString Then 'If the shortcut name is not defined...
ShortcutName = System.IO.Path.GetFileNameWithoutExtension(LinkToPath) 'grab the filename of the file from the full path.
End If
Dim ShortcutFilePath As String = ShortcutFolder & "\" & ShortcutName & ".lnk" 'build the full path to the Link file.
'-------------------------------------------------------------------------------------------
Dim wshShell As New IWshRuntimeLibrary.WshShellClass 'define our shell class and our Shortcut object.
Dim MyShortcut As IWshRuntimeLibrary.IWshShortcut = DirectCast(wshShell.CreateShortcut(ShortcutFilePath), IWshRuntimeLibrary.IWshShortcut)
MyShortcut.TargetPath = LinkToPath 'then set the target path.
MyShortcut.Arguments = ShortcutArgs 'then apply them to the target path.
If Len(WorkingDir) <> 0 Then 'did we define a working directory?
MyShortcut.WorkingDirectory = Environment.ExpandEnvironmentVariables(WorkingDir) 'yes, so assign it to the shortcut object.
End If
If Len(ShortcutIcon) <> 0 Then 'if an icon path exists...
MyShortcut.IconLocation = Environment.ExpandEnvironmentVariables(ShortcutIcon) & ", 0" 'specify its first icon (offset 0).
End If
MyShortcut.Description = ShortcutDescr 'apply a file description if required.
MyShortcut.WindowStyle = WinStyle 'apply a windows style.
MyShortcut.Save() 'finally, save the shortcut file and close the object.
End Sub

BONUS TIP: If our application has files associated with it (see Black Book Tip # 2 on page 452), we can right-click them in File
Explorer and select the Send To menu to the select our application by adding a shortcut to our application in our system’s SendTo
folder. With the above method present, doing this is almost too easy. Consider the following two lines of code:
Dim SendTo As String = Environment.GetFolderPath(Environment.SpecialFolder.SendTo) 'get path to system’s SENDTO folder.
CreateShortcut(Application.ExecutablePath, My.Application.Info.ProductName, SendTo, "Open File using SuperWizBang").
 Page 459

Black Book Tip # 5


Adding a File to the Recent Documents Folder.
Some applications save a shortcut to the files they open in the system’s Recent Documents folder. With
file associations in place as shown in Bonus Tip # 2 on page 452, the user can bring up the Recent File
list by selecting Start / Recent Files, and selecting the file, which will be opened by the application it is
associated with.
To accomplish this task is so simple it is almost stupid, were it not for the fact that this information
seems to be difficult to locate. I missed it plenty of times.
For some time, I typically had saved a shortcut to the file I am opening by creating it in the system’s
Recent folder (Environment.GetFolderPath(Environment.SpecialFolder.Recent)), and go through the usual
shortcut creation process as outlined in the last tip, but prepping it by extracting the name of the file, etc.
Basically, it looks like this (here, we are using the CreateShortcut method used in the previous tip):
CreateShortcut(LinkToPath, System.IO.Path.GetFileName(LinkToPath), Environment.GetFolderPath(Environment.SpecialFolder.Recent))

This works well enough and has never failed me, but even this is more complicated than it needs to be.
Hidden deep within the catacombs of the system workhorse, Shell32.DLL, is an innocent-looking
Pinvoke named SHAddToRecentDocs. This little method requires only two parameters; an Integer and a
text string. Because we are using .NET, I will declare the Unicode version so that the processor will not
have to first convert this string to ANSI, as we often did under VB6, mostly unknowingly:
' Notifies the system that an item has been accessed, for the purposes of tracking those items used
' most recently and most frequently. This function can also be used to clear all usage data.
Friend Declare Unicode Sub SHAddToRecentDocs Lib "shell32.DLL" (ByVal uFlags As Int32, ByVal FilePath As String)

Up through Windows Vista (Windows 6.0), this method had just 3 integer values for its uFlags parameter,
the last two of which hold interest to us. A value of 2 indicated that FilePath points to a null-terminated
ANSI string (we would need to change the “Unicode” in the above definition to “Ansi” in order to use
that value), which is fine and I am perfectly comfortable with it, being that I used it constantly while
developing C++ code. However, because .NET strings are Unicode, I am opting for a value of 3, which
indicates the FilePath points to a null-terminated Unicode string. Other, higher values can be found on
MSDN’s website, and they are defined for Windows 7 (actual Windows 6.1), Windows 8 (Windows 6.2),
Windows 8.1 (Windows 6.3), and Windows 10 (Windows 10, oddly enough).
NOTE: For some bizarre reason, Microsoft has complicated the determination of Windows 8.1 and Windows 10, so the usual
version information is returned as 6.2 (Windows 8), unless the application is specifically manifested for those operating
systems (see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724832(v=vs.85).aspx). I can see why they did this, but
they are not so good at explaining as much. To put some icing on it, it is because Windows 8.1 became optimized to running
Win64 and shelling Win32 apps, or running as Win32 more fluidly and efficiently, This is why some few apps stopped
working when migrating from 8.0 to 8.1, where Win32 apps tried accessing Win64 drivers. This also happened with Windows
10's first big update, where a few apps using unapproved shortcuts stopped working, like Office 2002 Outlook.
The great thing about this method is that I do not need to set anything up. It will automatically extract
the filename to name the shortcut, apply the icon that is associated with it via its associated executable
(which it will find in order to extract it), and everything else. All we need to do is supply a fully
qualified path to the file we want to add a link to in the Recent folder.
For example, suppose that a string named ProcessedFilePath contains the full path to the file that our
application is processing. To save a shortcut to it in the Recent Documents folder, we simply execute the
following line of code:
SHAddToRecentDocs(3, ProcessedFilePath) 'add the file to the Recent Documents Folder

And that is all there is to it.


 Page 460

Black Book Tip # 6


Sorting Any Column in a ListView in Ascending or Descending Order, Even by Date.
We have probably played around with the Details-View headers in File Explorer to sort the items based on
the column selected, where it toggles between ascending and descending sorted order each time we click it. A
number of users on the web have expressed seething exasperation in trying to either get any selected column
under a VB.NET ListView control to sort, or more often to get the ListView’s Sorting property to work with
the objects they assigned to the ListView. For example, if we set the Sorting property to
SortOrder.Descending, it might still sort on the first column in ascending order, or even not at all if we fed
objects to the ListView that did not define their own ToString function to provide displayable content.
When I first ran into this issue, I thought perhaps the ListView control also required the invocation of the
Sort method to resort it, but that, as I fully expected, did nothing. Afterward, I verified that it does
automatically invoke its Sort method whenever we change the Sorting property’s state, as logically it
should (it likely had to do with actually reading some documentation – in truth, reading documentation
is a pass-time that I do even when I am not up against a deadline or am in desperate need of a solution).
Consider the following simple event code that is supposed to toggle sorting on the ListView when the
user clicks on any of its column headers (and it will on simple string data):
Private Sub lvFileList_ColumnClick(sender As Object, e As ColumnClickEventArgs) Handles lvFileList.ColumnClick
With DirectCast(sender, ListView)
If .Sorting = SortOrder.Descending Then 'if we are presently sorting in Descending order...
.Sorting = SortOrder.Ascending 'change it to Ascending order.
Else
.Sorting = SortOrder.Descending 'otherwise, select Descending Order.
End If
End With
End Sub

The frustrating thing here is that ascending and descending sorts work fine for simple string-only data.
Using custom objects or even structures featuring a ToString function to deliver display content, or
trying to sort off of multi-columns, on the other hand, sometimes throws gasoline on a fire, and we end
up scrambling madly about like stray cats in a dog pound as we try to figure out how to make multi-
column sorts work properly, sweat spraying off our heads like nervous cartoon characters.
In this Black Book Tip I will address both of these issues with a rather simple comparison class that can
be plugged right into a ListView’s ListViewItemSorter property. This method is not the actual function
used to sort the list. We do not need to do that. It is instead simply the comparator used by the QuickSort
method that the ListView and other .NET controls use to sort their lists. QuickSort claims an advantage
of being small in size and offers relatively quick speed. I argue the Shell-Metzner algorithm is many
times faster, especially in long lists, and is also shorter... but QuickSort is already resident.
If we look to MSDN’s documentation on the ListViewItemSorter property, they tell us that we can sort
on a particular column using the following small class, which I extracted directly from their example on
MSDN (note the sad lack of comments that should fully document developer reasoning):
' Implements the manual sorting of items by columns.
Class ListViewItemComparer
Implements IComparer

Private col As Integer

Public Sub New()


col = 0
End Sub

Public Sub New(ByVal column As Integer)


col = column
End Sub

Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements IComparer.Compare


Return [String].Compare(CType(x, ListViewItem).SubItems(col).Text, CType(y, ListViewItem).SubItems(col).Text)
End Function
End Class
 Page 461

The problem with this simple class, however, is that toggling the Sorting property between Ascending
and Descending no longer does anything but sort in Ascending Order! Grrrr...
The reason for this is that the default comparer class that supported descending sorts that also recognized the
ListView’s Sorting property was replaced by the above class, which sadly performs only ascending
comparisons. Therefore, if we want our class to also support descending sorts on any column, then we will
have to also provide descending comparison program code (do not worry, it is unbelievably easy to do!).
But first, assuming we already have a working comparison class for the ListView, how can we use it?
Suppose we defined a class named, as in Microsoft’s example above, ListViewItemComparer, and featuring
similar operation, but, because changing the Sorting property does not work with custom objects that are
unaware of the associated ListView’s Sorting property, we will also tell the sorting class that we want to sort
in Ascending or Descending order. Thus, we will be invoking it like this: New ListViewItemComparer(iColumn,
SortingOrder). We would normally initially plug our class object into the ListView within the form’s Load
event, or before we load the ListView to avoid any delays (in the Bonus Tip at the end of this article I will
show us how easy it is to in fact key its sorting order directly from the ListView’s Sorting property).
For example, if we have a ListView control named lvFileList, we would intially assign our class to it
within the Form’s Load event by entering “Me.lvFileList.ListViewItemSorter = New ListViewItemComparer(0,
SortOrder.Ascending)”, where 0 indicates the first column and we are starting with an Ascending Sort. It
would also be a good idea, whenever we set this property, to also set the Sorting property appropriately
to reflect our chosen sorting order. When we use objects that do not feature built-in awareness of the
ListView’s Sorting property, it basically ends up being just a flag exposed for the user code to inspect.
During ColumnClick events, we want to determine which column is being clicked and apply the sorting class
to it. The way I like to do it is to always sort in Ascending Order if we clicked on a different column, and
only toggle between Ascending and Descending Order when we click on the same column as the last time.
With that in mind, consider the following example ColumnClick event (this event will only fire when we are
in the Details View mode, but not in any of the other modes).
'*********************************************************************************
' Method Name : lvFileList_ColumnClick
' Purpose : User clicked on a column to sort on
'*********************************************************************************
Private _LastColumn As Int32 = -1 'keep track of current sorting column.
(initialize to no column)

Private Sub lvFileList_ColumnClick(sender As Object, e As ColumnClickEventArgs) Handles lvFileList.ColumnClick


With DirectCast(sender, ListView)
If _LastColumn = e.Column AndAlso .Sorting = SortOrder.Ascending Then 'same column and ascending?
.ListViewItemSorter = New ListViewItemComparer(e.Column, SortOrder.Descending) 'sort on new column, Descending.
.Sorting = SortOrder.Descending 'reflect to user interface flag.
Else
_LastColumn = e.Column 'keep track of last col selected.
.ListViewItemSorter = New ListViewItemComparer(e.Column, SortOrder.Ascending) 'sort on new column, Ascending.
.Sorting = SortOrder.Ascending 'force Ascending if Descending or different col.
End If
End With
End Sub

What is left is to simply define a comparator class that will work for both Ascending and Descending
comparisons. We can do that by keeping track of the column, as demonstrated in the previous Microsoft
example, and also keeping track of Ascending or Descending sort order. This way, we can tell if we
should return True if the left string is greater than the right string, but False if it is Descending Order:
Option Explicit On
Option Strict On
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
' ListViewItemComparer Class Module (VB.NET version)
' Allow Comparing SubItems in a ListView (used by Sorting method)
'
'NOTE: Each ListView control uses a QuickSort algorythm that by default performs an ascending on the first or
' only column. It exposes a ListViewItemSorter property that allows developers to assign to it a comparison
' object that implements iComparer criteria. This comparer is provided two strings and is expected to determine
' if the first string is less than, equal to, or greater than the second string.
'NOTE: The ListView Sorting property does not seem to actually work for most user-defined classes because they
' are not keyed to its ListView object. However, it is still useful as a reporting mechanism. Because of this,
 Page 462

' I have enhanced the comparer to support both ascending or descending comparisons.
'-------------------------------------------------------------------------------------
'Assign this Class to the ListView object using something like the following:
' Me.listView1.ListViewItemSorter = New ListViewItemComparer() 'Sort Coumn 0 (first or only column), Ascending
'
'To sort on a particular Column, simply provide the column index as a parameter.
'
' NOTE: The following example assumes the user is clicking on a Column header in a Multi-column ListView, and this
' method will sort the lists based upon the column clicked. Clicking the same column again toggles the sort order
' between Ascending and Descending.
'
' Private _LastColumn As Int32 = -1 'init last column clicked. This will be used to enhance operation so that if the same
' 'column is clicked, the sort order is toggled, but if a different Column is clicked, then
' 'the sort order begins as Ascending for that new column.
'
' Private Sub lvFileList_ColumnClick(sender As Object, e As ColumnClickEventArgs) Handles lvFileList.ColumnClick
' If _LastColumn = e.Column AndAlso Me.lvFileList.Sorting = SortOrder.Ascending Then 'same column and ascending?
' Me.lvFileList.ListViewItemSorter = New ListViewItemComparer(e.Column, SortOrder.Descending) 'sort Desc on new col.
' Me.lvFileList.Sorting = SortOrder.Descending
' Else
' _LastColumn = e.Column 'keep track of the last column selected.
' Me.lvFileList.ListViewItemSorter = New ListViewItemComparer(e.Column, SortOrder.Ascending) 'sort Asc on new col.
' Me.lvFileList.Sorting = SortOrder.Ascending 'force Ascending if Descending or different col.
' End If
'End Sub
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
Class ListViewItemComparer
Implements IComparer
'-------------------------------------------------------------------------------
Private _Column As Integer 'column to sort on.
Private _SortingOrder As SortOrder 'True if Sort is in Ascending Order.
Private _CheckDates As Boolean 'True if we will check dates.
'-------------------------------------------------------------------------------

'*******************************************************************************
'*******************************************************************************
' Method Name : New
' Purpose : Initialize a new sorting method using the default column 0 and Ascending Sort Order.
'*******************************************************************************
'*******************************************************************************
Public Sub New()
_Column = 0 'if no column specified, use column 0 (the first column).
_SortingOrder = SortOrder.Ascending 'default to Ascending sort.
_CheckDates = False 'default to not checking date formats (checking dates is slower).
End Sub

'*******************************************************************************
'*******************************************************************************
' Method Name : New
' Purpose : Initialize a new sorting method using a specified column and Sort Order
'*******************************************************************************
'*******************************************************************************
Public Sub New(ByVal Column As Integer,
Optional ByVal SortingOrder As SortOrder = SortOrder.Ascending,
Optional ByVal CheckDates As Boolean = False)
Me._Column = Column 'creating class with a specified column number to sort on.
Me._SortingOrder = SortingOrder 'store Ascending/Descending sort flag.
Me._CheckDates = CheckDates 'if we will check dates or not.
End Sub

'*******************************************************************************
' Method Name : Compare
' Purpose : String comparison using by the ListView.ListViewItemSorter interface.
'*******************************************************************************
' comparison method used by the Listview (can sort Ascending or Descending)
Friend Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements IComparer.Compare
Dim Obj1 As ListViewItem = DirectCast(x, ListViewItem) 'get local objects.
Dim Obj2 As ListViewItem = DirectCast(y, ListViewItem) 'get local objects.
Dim AscDecFlag As Int32 'define Ascending/Decending flag
If Me._SortingOrder = SortOrder.Ascending Then 'ascending?
AscDecFlag = 1 'treat result normally if Ascending.
Else
AscDecFlag = -1 'otherwise, invert the result for Descending.
End If
'---------------------------------------------------------------
If _CheckDates Then 'if we will be checking dates (slower).
Try 'first, try to treat the items as dates.
Dim firstDate As Date = DateTime.Parse(Obj1.SubItems(_Column).Text)
Dim secondDate As Date = DateTime.Parse(Obj2.SubItems(_Column).Text)
Return DateTime.Compare(firstDate, secondDate) * AscDecFlag 'if here, they ARE dates. Return result.
Catch 'if either are NOT dates, then fall below.
End Try
End If
'---------------------------------------------------------------
' because equal compares check only for the length of the shortest member, compare their lengths if the result is zero.
 Page 463

Select Case String.Compare(Obj1.SubItems(_Column).Text, Obj2.SubItems(_Column).Text) * AscDecFlag


Case 1
Return 1
Case -1
Return -1
Case Else
Return Obj1.SubItems(_Column).Text.Length.CompareTo(Obj2.SubItems(_Column).Text.Length) * AscDecFlag
End Select
End Function
End Class

NOTE: The above class also takes into consideration Date entries, which means that this method can also sort columns containing
date fields that are formatted for any global locale. This small part of the code is a result of taking my original example class and
enhancing it to reflect an idea that Shannon Dunn at Microsoft offered on MSDN in July, 2002 on how to sort either text or dates
accurately. I found Shannon’s example long after I had developed my first example class, which had sorted strictly based upon text.
See http://msdn.microsoft.com/en-us/library/ms996467.aspx. Notice also that I have made it an optional parameter,
defaulting to False because checking dates is a bit slower than simply comparing strings, but can have a noticeable lag on really
long lists
BONUS TIP: We can add slight modifications to the above ListViewItemComparer class so that we can inspect the
associated ListView’s own Sorting property to determine ascending or descending sorts.
Making these modifications requires very little code. For example, to start, simply add a new private field in the heading of
our above class. Name it _lvParent of type ListView:
'*******************************************************************************
Private _Column As Integer 'column to sort on.
Private _lvParent As ListView 'ListView using this class.
Private _SortingOrder As SortOrder 'True if Sort is in Ascending Order.
Private _CheckDates As Boolean 'True if we will check dates.
'*******************************************************************************

Next, insert the following additional New constructor to the above class below the other two:
'*******************************************************************************
'*******************************************************************************
' Method Name : New
' Purpose : Initialize a new sorting method using a specified column and ListView.
' : This will allow us to access the state of the ListView's Sorting property.
'*******************************************************************************
'*******************************************************************************
Public Sub New(ByVal Column As Integer,
ByRef lvParent As ListView,
Optional ByVal CheckDates As Boolean = False)
Me._Column = Column 'creating class with a specified column number to sort on.
Me._lvParent = lvParent 'set listview parent so we can access its Sorting property.
Me._SortingOrder = SortOrder.Ascending 'default to Ascending in case user provided lvParent as Nothing.
Me._CheckDates = CheckDates 'set to True if we will check dates (default is False, for faster sorts).
End Sub

Here, instead of supplying the class constructor with a sorting order as we did with our other parameterized constructor, we
instead deliver it with the ListView control that we are in fact assigning this comparer class to.
To make the above small changes actually work, in the Compare function, immediately after we declare our Integer
AscDecFlag variable, we would add 3 simple lines of code:
Friend Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements IComparer.Compare
Dim Obj1 As ListViewItem = DirectCast(x, ListViewItem) 'get local objects
Dim Obj2 As ListViewItem = DirectCast(y, ListViewItem) 'get local objects
Dim AscDecFlag As Int32 'define Ascending/Decending flag
If Me._lvParent IsNot Nothing Then 'if we have a ListView control defined...
Me._SortingOrder = Me._lvParent.Sorting 'set our sort order from it
End If
If Me._SortingOrder = SortOrder.Ascending Then 'ascending?
AscDecFlag = 1 'treat result normally if Ascending
Else
AscDecFlag = -1 'otherwise, invert the result for Descending
End If
'---------------------------------------------------------------

And we should also remember to update our initial comparer-class assignment we make in our Form’s Load event with
“Me.lvFileList.ListViewItemSorter = New ListViewItemComparer(0, Me.lvFileList)”.
And that is all there is to it! Now, consider this much simpler update to our example ColumnClick event:
 Page 464

'*********************************************************************************
' Method Name : lvFileList_ColumnClick
' Purpose : User clicked on a column to sort on.
'*********************************************************************************
Private _LastColumn As Int32 = -1 'keep track of current sorting column (init to col 0).

Private Sub lvFileList_ColumnClick(sender As Object, e As ColumnClickEventArgs) Handles lvFileList.ColumnClick


With DirectCast(sender, ListView)
If _LastColumn = e.Column Then 'if we are sorting on the same column...
If .Sorting = SortOrder.Ascending Then 'and we are presently Ascending...
.Sorting = SortOrder.Descending 'flip to Descending (no need for new Comparer instance).
Else
.Sorting = SortOrder.Ascending 'else flip back to Ascending.
End If
Else 'we clicked a different column...
_LastColumn = e.Column 'so keep track of the last column selected.
.ListViewItemSorter =
New ListViewItemComparer(e.Column, DirectCast(sender, ListView)) 'sort on the new column.
.Sorting = SortOrder.Ascending 'force Ascending to start.
End If
End With
End Sub

Although we are doing much the same as before, which is flipping sort order when the same column is clicked, with this
change we can now just change the Sorting property of our ListView control and it will automatically initiate a resort in the
opposite order on the same column without any need to assign a new comparer class with new parameters.
 Page 465

Black Book Tip # 7


Sizing a Label or TextBox to Fully Contain a String for Display.
Often we have sometimes quite lengthy text that we want to display within a Label or a TextBox control,
but we cannot determine beforehand how large the Label or TextBox should be sized, such as a data file,
the results of a diagnostic report, or some text of a length that cannot be predetermined. Although with a
TextBox we can apply a scroll bar (with its Multiline parameter set to True, of course), this is not
feasible with a Label (with its AutoSize parameter set to False, of course). However, the solution is so
easy that I almost did not include it in these tips, being almost embarrassed that I have it in my bag of
tricks. However, after a number of conversations with developers who were having trouble trying to
figure out how to properly resize their dialogs to fit such data, I realized that this trick I constantly used
was not as widely known as I had at first thought it was.
If our text has built-in line breaks, namely vbCrLf codes to terminate each line, it is incredibly easy to
calculate the display size for the text data. For example, suppose we have a message string in a variable
named strMessage and we want to display it in a TextBox named txtMessage. Using just the Font
information defined in the label, we can assign a Size-type structure with the required width and height
of the text data using the built-in TextRenderer class’s MeasureText method, like so:
Dim TextSize As Size = TextRenderer.MeasureText(strMessage, Me.txtMessage.Font) 'get the calculated text size of the data.
Me.txtMessage.Width = TextSize.Width + 4 'set the label width to match it (+4 pixels for appearance).
Me.txtMessage.Height = TextSize.Height 'set the label height to match it.
Me.txtMessage.Text = strMessage 'stuff the message.

However, usually, we also have to resize our form to accommodate this re-dimensioned control, which is
still easy enough to do, though we would actually want to do is resize the form immediately after we
have acquired the new size, but before we actually resize the Label or TextBox itself, like so:
Dim TextSize As Size = TextRenderer.MeasureText(strMessage, Me.txtMessage.Font) 'get the calculated text size of the data.
Me.Width = Me.Width - Me.txtMessage.Width + TextSize.Width + 4 'resize form width to Match (+4 pixels for appearance).
Me.Height = Me.Height - Me.txtMessage.Height + TextSize.Height 'resize the form height to match.
Me.txtMessage.Width = TextSize.Width + 4 'set label width to match (+4 pixels for appearance).
Me.txtMessage.Height = TextSize.Height 'set the label height to match it.
Me.txtMessage.Text = strMessage 'stuff the message.

NOTE: If the control is anchored to all four sides so it will automatically resize according to the resizing of the form, we
would not require the two lines that specifically resize the control, txtMessage, because resizing the form would, in that case,
automatically properly resize it without the need for additional assistance from us. Even so, leaving such code intact will not
hurt it, either – it would simply show no effect on the control.
NOTE: When using a Label, it is important to set its AutoSize property to False and to have positioned it in the form to a
size that is initially scaled to look nice in the form’s startup dimensions, for appearances sake.
But were it that our needs could be so simple. We run into trouble when we want to limit the width of the
string, which is the most common scenario, such as keeping our message dialog down to a manageable
size, or the Label or TextBox must be restricted to a particular or maximum width and it is anchored to
the left and right of its parent control or form, docked, but we do not want to rescale the form’s width, or
the text of the message is simply one long line of text without any line breaks, and a single line simply
would not accommodate it unless we were using the Times Square Jumbotron as a computer monitor.
Although setting the WordWrap parameter of a TextBox to True, or turning the AutoSize parameter of a
Label to False will provide auto-word-wrap if we stuff long text into them, TextRenderer.MeasureText is
not able to provide us with the actual dimensions we require of such long, control-wrapped text to
properly resize the form height because it assumes that each physical line of text is not wrapped. If the
MeasureText method did allow for auto-wrapping, the following exercise would be pointless.
Unfortunately, its only overload allowing size restrictions features a ProposedSize parameter of type
Size, but that includes height restrictions, which will not work for us. Thus, we must adjust the text by
manually “wrapping” it so the MeasureText method will work as needed. Too bad MeasureText does not
have a MaxWidth parameter.
 Page 466

What we will need to do is set up the message so that the above simple example can be used to calculate
the final size, adding line breaks as needed to properly wrap the text within a specified width.
One way I like doing this is to break the string up on its current line breaks, if any, and make sure that
each of those lines is still within the width limit established for the text display. For example, suppose
that our lblMessage control can be of variable size, though it is initially set to its minimum size of, say,
200 pixels, but we want it to have only a maximum width of 550 pixels (a MsgBox control has a
maximum text width of 375 pixels). Also, if we are indeed using a Label control, please make sure its
AutoSize property is set to False so that we are in fact able to use its initial width. It would also be a
good idea to set an initial height that is compatible with the control layout on our form, but also consider
that the controls should be set up and anchored in such a way that they will accommodate a variable-size
label. Proper anchoring to a form corner or top or bottom usually works best. In such cases, the only
resizing we should then have to worry about is the sizing of the form.
To properly adjust each line of the text, we will need three pieces of information:
1. The string of text to check.
2. The Font that we will use to measure the text (this should always be the target control’s font, of course).
3. The maximum allowed width for the target control.
If the control is anchored or docked and the width will not change, this maximum width value is simply the
width of the control. If the form will resize with the resizing of the control, we will need to supply it with a
maximum width. On custom message boxes, I have found that 200 pixels is optimal for a minimum label
width size, and 550 pixels is optimal for a maximum size in a message area, because dialogs containing
message areas beyond these bounds tend to make a dialog form look a bit disturbing, to say the least.
We will also need to set up a support function to break the message up and return the modified message
that will fit within the maximum defined width value for the control. We will also require the same
parameters we supplied the MeasureText method to process each individual line. Let us call our user
interface function SizeMessage, and our private support function (which only services SizeMessage),
SizeMsgLine. With that, we can replace the first two lines of the above example code, yielding:
Dim TextSize As Size = SizeMessage(strMessage, Me.lblMessage.Font, 550) 'get the calculated text size of the data.
Dim ShowWidth As Int32 = Me.Width - Me. lblMessage.Width + TextSize.Width + 4 'compute new form width (+4 for appearance).
If ShowWidth > Me.Width Then 'greater than the minimum (current) size?
Me.Width = ShowWidth 'yes, so adjust the form width.
End If
Me.Height = Me.Height - Me.lblPrompt.Height + TextSize.Height 'resize the form height to match.
Me.txtMessage.Width = TextSize.Width + 4 'set label width to match (+4 for looks).
Me.txtMessage.Height = TextSize.Height 'set the label height to match it.
Me.txtMessage.Text = strMessage 'stuff the message.

Now, of course, we will need to write these support methods (there is always a catch...).
The SizeMessage method is the simplest. All it needs to do is break the message down into individual
physical lines as they are currently defined and pass each line to its support method, SizeMsgLine. It
will also recombine all the resulting lines to be the message passed back to the invoker. And because it
will no doubt alter the message string sent to it, we will have to pass it to SizeMessage By Reference.
This way, when the method returns, the return value will be a Size structure that will provide the
suggested width and height for the Label or TextBox, and the message text passed to it is also properly
delineated, if required, for proper display. Hence, the following describes our SizeMessage function:
'*********************************************************************************
' Method : SizeMessage
' Purpose : Compute the display size of the text.
'*********************************************************************************
Friend Function SizeMessage(ByRef Message As String, ByRef Fnt As Font, ByVal MaxWidth As Int32) As Size
If InStr(Message, vbCrLf) = 0 Then 'if there are no CR/LF’s embedded...
Message = SizeMsgLine(Message, Fnt, MaxWidth) 'process the single line.
Else 'otherwise...
Dim Ary() As String = Split(Message, vbCrLf) 'split out each line of the prompt,
For Idy As Int32 = 0 To UBound(Ary) 'then check the width of each one...
Ary(Idy) = SizeMsgLine(Ary(Idy), Fnt, MaxWidth) 'to make sure the prompt will wrap properly.
Next
Message = Join(Ary, vbCrLf) 'reconstruct array, in case changes in above loop.
 Page 467

End If
Return TextRenderer.MeasureText(Message, Fnt) 'return the computed size of the final message.
End Function

The SizeMsgLine function will take each line provided to it and ensure that it will fit within the
MaxWidth bounds that is also provided to it. If the line text extends beyond those bounds, then that line
is broken up at a point in the line where it will be acceptable, at least for the start of the line, and then
each individual split line is then passed on to SizeMsgLine (this is a recursive process) until any line
within the original line does not require further splitting. Once that point is reached, the updated
message string is returned to the invoker.
The interesting thing about this method is that it will take advantage of the MeasureText function to
determine if the line is too long, and it is used again to find an acceptable length for the first part of the
message if it does not. It then splits the line at that point by inserting a line-break and passes the data
beyond that line-break back to itself (recursion) until there is no more splitting to do. Afterward, any
recursion is wound back down and the final string is constructed and returned to the ultimate invoker,
which accumulates the updates to each line, and then recombines them as shown above in the
SizeMessage method. Consider the recursive SizeMsgLine function, below:
'*********************************************************************************
' Method : SizeMsgLine (support method)
' Purpose : Ensure the string does not exceed the maximum Width value
'*********************************************************************************
Private Function SizeMsgLine(ByVal TextLine As String, ByRef Fnt As Font, ByVal MaxWidth As Int32) As String
Dim TextSize As Size = TextRenderer.MeasureText(TextLine, Fnt) 'get dynamics of individual line of text,
Dim Idx As Int32 = TextLine.TrimEnd.Length 'grab length of entire text line,
Dim Ofst As Int32 'offset is 1 if space ends line; 0 if dash.
Do While Idx <> 0 AndAlso TextSize.Width > MaxWidth 'is present width > than max allowed?
Dim Idy As Int32 = TextLine.LastIndexOf("-"c, Idx - 1) 'track backward for a dash.
Idx = TextLine.LastIndexOf(" "c, Idx - 1) 'also track backward for a space.
If Idy > Idx Then 'if we found a dash before a space...
Idx = Idy 'set index to dash.
Ofst = 0 'allow dash to be included in text data.
Else
Ofst = 1 'skip over space.
End If
TextSize = TextRenderer.MeasureText(TextLine.Substring(0, Idx).TrimEnd, Fnt) 'recalculate size based on dash found.
Loop
If Idx > 0 AndAlso Idx < TextLine.TrimEnd.Length Then 'if text len to be adjusted...
TextLine = TextLine.Substring(0, Idx + 1).TrimEnd & vbCrLf &
TextLine.Substring(Idx + 1 + Ofst).Trim 'break text up w/vbCrLf & break itdown.
Dim Ary() As String = Split(TextLine, vbCrLf) 'now break text up to array.
For Idx = 1 To UBound(Ary) 'test each higher line. Index 0 is correct.
Ary(Idx) = SizeMsgLine(Ary(Idx), Fnt, MaxWidth) 'break up each part as needed (recursion).
Next
TextLine = Join(Ary, vbCrLf) 'convert array to single string w/breaks.
End If
Return TextLine 'return the text to the invoker.
End Function

NOTE: We can also use the SizeMessage function to prepare text for display in a MsgBox. In this case we will not need its
return Size value and we can instead invoke it as if it were a subroutine. However, we should set its MaxWidth parameter to
no higher than 375 pixels, because a MsgBox will itself wrap text that is longer than this. Also, when using a MsgBox, we
should use the SystemFonts.MessageBoxFont font to measure the text.
NOTE: Later in this document, in Black Book Tip # 46, “Quick and Easy Text-Justification for Text Boxes, Labels, and
Dialog Boxes” on page 614, we will modify these methods even further in order to support simple text justification (just
adjusting space counts between words) that can be provided to a TextBox, Label, or even a MsgBox with a cleaner, better-
formatted appearance of its text.
 Page 468

Black Book Tip # 8


Set a New SelectedIndex for a ListBox or ComboBox Without Firing a Click Event.
Typically, when we set the ListIndex property of either a ListBox control or a ComboBox control, the
respective control’s Click event and its SelectedIndexChanged events, if defined, also fires. We would
clearly expect this of the SelectedIndexChanged event because we are in fact changing that index, but
the Click event is fired because it is considered to be a selection that simply makes the assumption that it
was clicked on. However, often we just want to set this index without causing any special processing we
might have developed for the control’s Click event to be passed through.
A way to do this is to set the index for the control by sending it a message. For a ListBox control, this
message is LB_SETCURSEL (&H186), and for a ComboBox control it is CB_SETCURSEL (&H14E).
We accompany this message with the handle of the selected control and the index to set.
We send messages through the SendMessage Pinvoke. For example, following is the definition of
SendMessage that we want to use:
' Sends the specified message to a window or windows. The SendMessage function calls the window procedure
' for the specified window and does not return until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As Integer) As Boolean

To send a message is easy. For example, to set the SelectedIndex of a ListBox control named lstContacts
to 17, we would issue the command:
SendMessage(lstContacts.Handle, LB_SETCURSEL, 17, 0)

NOTE: Be aware that this message only works with Single-Selection lists. This cannot be used to support selecting multiple
items in the list. After all, this method was originally designed to change the highlighted selection during mouse auto-
tracking in the ListBox portion of a ComboBox dropdown list.
As would be expected, the ListBox or ComboBox will still scroll the selected item into view within its
displayed list and mark the item as being selected, and also fire a SelectedIndexChanged event, just as
though we had set its SelectedIndex property. However, it will NOT broadcast a Click event.
A wrapper for this, so that we can invoke this functionality for any and all ListBoxes and ComboBoxes
we have can be accomplished by placing the following code within a module file. I named mine
modSetListIndex:
Option Strict On
Option Explicit On

Module modSetListIndex
'Set the SelectedIndex of a ListBox or ComboBox without triggering a click event
'*******************************************************************************
' modSetListIndex - The SetListIndex() function Sets the ListIndex of a ListBox
' or ComboBox without triggering a click event, which is what
' will normally happen when the control's listindex is set to
' anything but -1. This function returns TRUE if the control
' afterwards reflects the desired listindex, and FALSE if it
' does not, which might be due to an out of range value.
'*******************************************************************************

' Sends the specified message to a window or windows. The SendMessage function calls the window procedure
' for the specified window and does not return until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal wMsg As Integer,
ByVal wParam As Integer,
ByVal lParam As Integer) As Boolean

' An application sends a CB_SETCURSEL message to select a string in the list of a ComboBox. If necessary, the list scrolls
' the string into view. The text in the edit control of the combo box changes to reflect the new selection, and any previous
' selection in the list is removed. The wParam parameter specifies the zero-based index of the string to select. If this
' parameter is –1, any current selection in the list is removed and the edit control is cleared. The lParam parameter is not
' used. If the message is successful, the return value is the index of the item selected. If wParam is greater than the
' number of items in the list or if wParam is –1, the return value is CB_ERR(-1) and the selection is cleared.
Private Const CB_SETCURSEL As Integer = &H14E
 Page 469

' An application sends a LB_SETCURSEL message to select a string in the list of a ListBox. If necessary, the list scrolls
' the string into view. The text in the edit control of the combo box changes to reflect the new selection, and any previous
' selection in the list is removed. The wParam parameter specifies the zero-based index of the string to select. If this
' parameter is –1, any current selection in the list is removed and the edit control is cleared. The lParam parameter is not
' used. If an error occurs, the return value is LB_ERR(-1). If the wParam parameter is –1, the return value is LB_ERR even
' though no error occurred. NOTE: Use this message only with single-selection ListBoxes. You cannot use it to set or remove
' a selection in a multiple-selection list box.
Private Const LB_SETCURSEL As Integer = &H186

Friend Function SetListIndex(ByRef LstOrCboBox As Object, ByVal NewIndex As Integer) As Boolean


Select Case LstOrCboBox.GetType.Name 'check which object to process.
Case "ComboBox"
Dim Cbo As ComboBox = DirectCast(LstOrCboBox, ComboBox) 'set aside ComboBox reference.
SendMessage(Cbo.Handle, CB_SETCURSEL, NewIndex, 0) 'set the desired index.
Return (NewIndex = Cbo.SelectedIndex) 'return true if it succeeded.
Case "ListBox"
Dim Lst As ListBox = DirectCast(LstOrCboBox, ListBox) 'set aside ListBox reference.
SendMessage(Lst.Handle, LB_SETCURSEL, NewIndex, 0) 'set the desired index.
Return (NewIndex = Lst.SelectedIndex) 'return true if it succeeded.
End Select
Return False 'fail if not ComboBox or ListBox.
End Function

End Module

NOTE: Another way around this issue is to simply abandon using the Click event altogether and use instead the MouseClick
event. The MouseClick event does not fire when the index is changed, as the Click event will, but fires only when the mouse
performs a click, as its name implies. However, to be fair, the reason the Click event fires on a normal selection change was
to accommodate using the keyboard to move the selection up and down the list.
TRIVIA NOTE: Where did Geek Speak originate? It started, oddly enough, with the thing most modern geeks do the least,
often avoiding it like it was Bella Lugosi – documenting source code. "Back in the day" when printers and video interfaces
were first introduced, maximum line widths were very narrow, being usually 40 characters wide (any old Apple ][, TRS-80
Model 1, or other early PC users remember those days?). Especially when writing code in cryptic Assembly Language, the
code was much harder to follow, but there was not much room for comments in the right margin following a code line, so
developers, especially those working on teams, needed to communicate intent to their team members as they checked each
others code for errors. This is why leading comments above the code became so popular, because they could use the whole
line. Such practices even became rote at places as IBM. But in order to fit meaningful comments in the narrow margins when
we had to, we had to break text down to its most basic parts, so using words like "the" were taboo. For example, to say
"remove all of the leading spaces from the currently selected line and then test the resulting line's length against the
maximum allowed for lines on the displayed text box", we had to cram it down into something like "del lead sp fm line & test
len fm max", or even "trm, ch 2 mx". This is another reason jargon became so cryptic, favorably short and especially
abbreviated. Coding that way every day, year in and out, and days were up to 12 and sometimes 72 hours straight during
crunch time before a new code release was due, left many programmers stuck speaking this cryptic jargon as common
language, and it was so ingrained that they could not understand why “normal” people could not understand them, because
they thought they were speaking a common tongue, making them wonder if “Norms” were retarded (no “PC” spoken here,
because PC is nothing more than a device by which a lie can be made to sound like the truth – whatever happened to
honesty?), which caused non-programmers "out in the world" giving them odd looks and distancing themselves because
there "seems to be something wrong with that one". Keep the kids close. Know your exits.
This even filtered down to the military, where anagrams are used for everything, but this was largely due to their impossible
to understand inventory lists being literary works crammed into endless streams of anagrams. which could render Tolstoy's
War and Peace down to just 4 pages. I left the Army never knowing what some anagrams for critical components of my
assigned duties actually stood for. For example, GI stood for “General Issue, 1 each, OD Green in color” (OD was Olive
Drab), “20” was a term borrowed from the FCC to indicate current location, RP was a Rally Point, FNG for “Fracking”
New Guy, or more kindly as Newbie or 'Cruit, TC was Top Commander, referring to the guy stuck sticking their faces in the
cold wind from the top hatch of a tank or APC (Armoured Personnel Carrier), CP for Command Post, or even BBMFIC, for
Big Bad Mudder Fudder In Charge.
 Page 470

Black Book Tip # 9


Display TextBox Text Format-Justified at Runtime.
When I am displaying certain information on the monitor, such as in an About Box, or I am displaying a
dialog to explain some technical information, sometimes I think that the text just looks better when its
display format is Justified, not simply Ragged-Right or Centered. Consider a comparison of the same
text, first formatted Left-Aligned on the left, and then Justified-Aligned on the right, below:

The method behind accomplishing text justification is actually rather simple. The technique I used is
very similar to the techniques I had developed while designing quite a number of commercial printer
drivers, to enhance their functionality, and among those many features, to also support justified text:
 We start by first computing the size of a single Space character. This is very important, because this value is key towards attaining
optimal alignment of text between the left and right margins. All spacing widths should be minimally this value.
 Next, we scan each line of text as it is currently defined for each line within the TextBox. Actually, we will only compute the length of
each line of text that should be justified, ignoring lines that are terminated by a line-break, which can naturally be ignored, because we
would certainly not want to justify them.
 Next, we compute how many pixels each line is from filling out to the TextBox width. We do this using the
TextRenderer.MeasureText method. This returns the width and height of the line in Pixels. We can simply subtract the pixel width
from the width of the TextBox (and we also subtract a computed buffer offset, which we will cover shortly) and we will have the exact
number of pixels we need to insert within the line in order to fully justify that line of text.
 Next, for lines that do require justification, we replace all the Space characters in the line with the ASCII code 128, which is a code
that is easy to detect, and adding to it the current pixel width of a single Space character.
 With this result we will increment each of the special space codes we had inserted with enough pixels to fill each line to the full width
of the TextBox, updating all these special codes except for those that lead a line, which might comprise an indent; something that we
would not want to expand further. We increment these special codes by repeatedly looping through each line from beginning to end,
each time adding a single pixel to each of the special codes. We will repeat this loop as necessary until we have exhausted the number
of required pixels to insert.
 Once fully processed, we terminate each individual line with a line-break code ( 13 & 10, or vbCrLf), just so my Print event code,
which will be used to render the text, will know when each line ends, and append it to a stored data buffer.
 Finally, the accumulated string and a reference to the PictureBox control is saved to a tiny automatically instantiated class object, used
by a Print event handler to display the formatted text. We will also clean up by making sure that the actual TextBox is invisible, we
instantiate a new PictureBox, duplicate the TextBox display characteristics to it by setting it to the same location, size, border style,
etc., add a reference to the TextBox in the PictureBox’s Tag property, and add a Print event handler to the PictureBox that will be used
to properly interpret the specially formatted Text data.

The Print event computes the size of a space and the vertical size of a printed line and reserves them for
handling still-existing spaces and line-break codes when parsing the modified text data. It then sets up a
graphical X and Y offset to keep track of our printing location within the PictureBox. The X offset will
be incremented by the size of the text or of the space code. When a line-break code is encountered, if
any accumulated text data exists, it is painted to the current X and Y coordinates within the PictureBox
and then the X offset is reset to the left edge and the Y offset is incremented by the computed line height.
Whenever it encounters a special space replacement code (128+) or an actual Space character, if we
have accumulated text data, the text is painted to the PictureBox, the X offset is incremented by the
width of the painted text, and then the space size is added. If it is a Space character, the computed size of
a space is added to the X offset. Otherwise, we remove the 128 value from the found special code and
add that result to the X offset.
As stated previously, we can easily compute the size of a text string using the MeasureText function of
the TextRenderer class. However, unlike any other text painting examples that we may have ever seen,
here we will also use the DrawText method from the TextRender class to actually paint the text onto the
PictureBox as well, because if we use the “traditional” DrawText method from the Graphics object,
 Page 471

which most every other example that I have ever found in books and on the web demonstrate, the text
will in fact not render correctly. It will be close, but it will most certainly be shy of perfect, falling short
of what it could actually be. The reason for the functional discrepancy between these two DrawText
methods is that we must also apply special Text Format Flags in order to perfectly render this text to the
PictureBox and precisely emulate a TextBox. The TextRenderer version of DrawText fully supports
these flags, but the Graphics version sadly does not. And these Text Format Flags are critically
important to properly emulate a TextBox display, but with the additional benefit of text justification.
This little difference drove me a bit crazy at first, until I finally realized that these two methods, though
sharing the same name, did not work exactly alike (actually, I believe that they do in fact share common
code, but the Graphics version does not expose all the possible parameters that the TextRenderer class
exposes). When I had initially tried to implement my justification technique, I had tried to draw the text
to the PictureBox, just as I had done for many years, using the tried and true e.Graphics.DrawText
method. The problem was that using this version of DrawText will cause the displayed text to appear to
lose several spaces between words, especially following words of 8 or greater characters. For example,
the text “also fantasizing that” would be displayed as “also fantasizingthat”.
The TextRenderer.DrawText method avoids all this and renders the text impeccably.
Even so, as hinted at earlier, the TextRenderer.MeasureText method will always add an offset to its
reported text width that is consistently equal to the Font’s Point Size minus 0.5 and minus the width of a
space of that point size. Hence, we can easily compute it using “ Dim MeasureAdjust As Int32 =
CInt(TxtBox.Font.Size - 0.5!) - SpcSize”. Therefore, on an 8.25-point font that has a single space width of 2
pixels, this added offset is 6 (we must consider rounding). For a 12-point Font that has a space width of
3, it adds a value of 9. This happens even if we tell it via flags not to apply TextBox margins, and even to
format it for a TextBox display (which causes it to render text more tightly), but this consistent offset that
is based off the point size is a value that is ALWAYS applied to any measured text, whether it be a full
line of text or even a single character. However, because we can accurately and easily compute this
special offset value, I can most certainly live with it.
I wrote a module named modJustifyTextBoxDisplay.vb in order to to support all these requirements.
This module exposes only a single method, JustifyTextBoxDisplay, which will provide support for as
many text boxes as we may have need of in our application. We simply invoke it in the Form’s Load
event with our TextBox object as a parameter, such as JustifyTextBoxDisplay(myTextBox). This method
will format the text for use by a special print event processor, and it will also automatically instantiate a
PictureBox for each TextBox processed through this method, attaching that PictureBox to the private
JustifyText_Paint event also included in the module, which will display the pre-processed text as fully
justified text.
We may notice two other unexposed event handlers included in the module. The PictureBox_Resize
event handler allows the TextBox to be resized if its anchoring allows it to do so and if the user is able to
resize the form. For example, if we have our TextBox(es) anchored to all 4 sides of the form, then that
anchoring will be copied to the PictureBox control that is created within the JustifyTextBoxDisplay
method. This way, when the PictureBox in turn resizes, it will invoke an unexposed
RefreshPicTextData method that is also included within the module that will reformat the TextBox text
for the new size and then force the PictureBox to repaint itself. The other unexposed event handler
named TextBox_FontChanged is used to support font property changes the user might make to the
associated TextBox. This way if we enable our user to change Fonts or Point Sizes in order to allow them
to read our information more clearly, in case they are vision impaired, then this event handler will
automatically reformat the text again for that new font information and again force a repaint.
The code to do all these things is also fully commented, so feel free to explore its slick, though actually
rather simple techniques. Sample code for an example form is also included within its comments:
 Page 472

Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modJustifyTextBoxDisplay Class Module
' Display the contents of a hidden TextBox, justified within a PictureBox.
'
' Copyright (c) 2013 by David Ross Goben. All rights reserved. Feel free to use it in
' your own apps. I just do not want to see it
' posted somewhere by someone, touted as a
' creature of their own invention. This has happened already.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modJustifyTextBoxDisplay
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Const Flags As TextFormatFlags = TextFormatFlags.NoClipping Or
TextFormatFlags.NoPadding Or
TextFormatFlags.TextBoxControl Or
TextFormatFlags.SingleLine 'flags to properly render text to PictureBox.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Friend Class TextDataReference 'this class is used so we can store more than 1 item in TextBox Tag property.
Friend PictBox As PictureBox 'reference to PictureBox. Will be updated by JustifyTextBoxDisplay() and stored here.
Friend FmtText As String 'formatted Text Data that will be used by the Paint Event.

Friend Sub New(Optional ByRef PicBox As PictureBox = Nothing)


Me.PictBox = PicBox
Me.FmtText = Nothing
End Sub

Public Overrides Function ToString() As String 'simplify access to formatted text data of this little class.
Return FmtText
End Function
End Class
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method Name : JustifyTextBoxDisplay
' Purpose : Reformat spacing in a textbox to allow for variable spacing, exactly
' : as a printer driver will process spacing for justification.
' : Having written many dozens of printer drivers over the years, this
' : is old hat. By the way, though you can do this with a RichTextBox,
' : you really should consider interpreting the RTF code to properly
' : render everything else, otherwise, if you were to simply replace
' : the TextBox casting to a RichTextBox, it would only process the
' : data from its Text property, so pretty text and coloring and font
' : typeface, size, and enhancements will not display in the PictureBox.
' : It is doable, but it requires a heck of a lot more work.
'*******************************************************************************
' Set up your code to use the JustifyTextBoxDisplay() method and the JustifyText_Paint()
' event like this, placing this code within your form's Load() event code.
'
' JustifyTextBoxDisplay(TextBoxControlToJustify) 'create a PictureBox control to justify the text and hide this TextBox.
' 'Will also link new PictureBox Paint event to JustifyText_Paint.
'-------------------------------------------------------------------------------
' NOTE: You can duplicate the above code for as many text box controls as you require.
' Also, there is no need to render them ReadOnly, because the user will not
' be able to access them through the user interface.
'*******************************************************************************
Friend Sub JustifyTextBoxDisplay(ByRef TxtBox As TextBox) 'NORMAL USER-ACCESSABLE METHOD.
If Len(Trim(TxtBox.Text)) = 0 Then 'do nothing if the TextBox is blank.
Return
End If
TxtBox.Visible = False 'make TextBox invisible (at runtime, see only PictureBox).
Dim picImage As New PictureBox 'create PictureBox (Text will be painted to it).
With picImage
.Location = TxtBox.Location 'locate the PictureBox to the provided TextBox control.
.Size = TxtBox.Size 'size it to the textbox boundaries.
.Parent = TxtBox.Parent 'set parent so it will display when the parent is displayed.
.BackColor = TxtBox.BackColor 'duplicate the TextBox's background color to the PictureBox.
.BorderStyle = TxtBox.BorderStyle 'duplicate the TextBox border style.
.Anchor = TxtBox.Anchor 'reflect the TextBox anchoring to the PictureBox.
.Tag = TxtBox 'save a reference to the textbox for use during painting.
AddHandler picImage.Paint, AddressOf JustifyText_Paint 'attach an event handler to the picture box's paint event.
AddHandler picImage.Resize, AddressOf PictureBox_Resize 'attach an event handler to the picture box's resize event.
End With

TxtBox.Tag = New TextDataReference(picImage) 'object to allow a Tag to contain multiple items.


AddHandler TxtBox.FontChanged, AddressOf TextBox_FontChanged 'attach event handler to TextBox font changed event.
RefreshPicTextData(TxtBox) 'then refresh the TextBox data for display data.
End Sub

'*******************************************************************************
' Method Name : RefreshPicTextData
 Page 473

'Purpose : Support JustifyTextBoxDisplay(), PictureBox_Resize, and TextBox_FontChanged events


'
'Consider the following complete sample form class code that features a single TextBox.
'This code assumes a TextBox filled with text data named TextBox1, a button control named
'BtnOK, and another button control named btnSelectFont.
'<----------------------------- BEGIN COPY ------------------------------------>
'Public Class Form1
' '*******************************************************************************
' ' Method Name : Form1_Load
' 'Purpose : Set up justified TextBox data, then set the FormLoaded flag.
' '*******************************************************************************
' Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
' JustifyTextBoxDisplay(Me.TextBox1) 'display this textbox with justified text.
' End Sub
'
' '*******************************************************************************
' ' Method Name : btnOK_Click
' 'Purpose : Close the sample form.
' '*******************************************************************************
' Private Sub btnOK_Click(sender As System.Object, e As System.EventArgs) Handles btnOK.Click
' Me.Close() 'OK button.
' End Sub
'
' '*******************************************************************************
' ' Method Name : btnSelectFont_Click
' 'Purpose : Change the Font and/or Point Size used to render the textbox text.
' '*******************************************************************************
' Private Sub btnSelectFont_Click(sender As Object, e As EventArgs) Handles btnSelectFont.Click
' With New FontDialog 'with a new instance of a Font Dialog form...
' .FontMustExist = True 'make sure the fonts specified exists.
' .ShowEffects = False 'show no effects. We are not Stan Winston.
' .Font = Me.TextBox1.Font 'start with current font & point size.
' If .ShowDialog(Me) = DialogResult.OK Then 'user selected OK?
' Me.TextBox1.Font = .Font 'yes, change font via event handler.
' End If ' applied to TextBox will do housekeeping.
' .Dispose() 'release the created resource.
' End With
' End Sub
' End Class
'<------------------------------ END COPY ------------------------------------->
'*******************************************************************************
Private Sub RefreshPicTextData(ByRef TxtBox As TextBox)
With TxtBox
Dim TextSize As Size = TextRenderer.MeasureText("W y", .Font, .Size, Flags) 'get pixel width of "W y".
Dim SpcSize As Int32 = TextSize.Width 'save measurement (will adjust soon).
TextSize = TextRenderer.MeasureText("Wy", .Font, .Size, Flags) 'get pixel Wd (+ Ht) of "Wy", w/o space.
SpcSize -= (TextSize.Width + 1) 'compute space Wd (-1 for perfect render).
Dim SpcInit As Char = Chr(128 + SpcSize) 'encoded space start size (pixels + 128).
Dim MeasureAdjust As Int32 = CInt(.Font.Size - 0.5!) - SpcSize 'spacing adjustment we must subtract.
Dim Result As String = Nothing 'init encoded result.
Dim LastLine As Int32 = .GetLineFromCharIndex(Len(.Text)) 'get last line index of TextBox, from 0.
Dim SpcAry(15) As Int32 'array for spaces in line. Bump as needed.
'-----------------------------------------------------------------------
For Idx As Int32 = 0 To LastLine 'process each line of TextBox text.
Dim Tmptext As String 'copy of line's Text that will be updated.
Dim SkipJustify As Boolean = False 'init flag used to skip justification.
Dim CurlineIdx As Int32 = .GetFirstCharIndexFromLine(Idx) 'get index to start of TextBox line text.
If Idx = LastLine Then 'are we at the last line of text?
Tmptext = RTrim(.Text.Substring(CurlineIdx)) 'grab text from line start index to end.
SkipJustify = True 'indicate no reason to justify.
Else
Dim NxtLineIdx As Int32 = .GetFirstCharIndexFromLine(Idx + 1) 'not last; get index to start of next.
Tmptext = RTrim(.Text.Substring(CurlineIdx, NxtLineIdx - CurlineIdx)) 'grab text of line & trim spaces.
End If

If Len(Tmptext) <> 0 AndAlso Right(Tmptext, 2) = vbCrLf Then 'does the line terminate with a break?
Tmptext = RTrim(Left(Tmptext, Len(Tmptext) - 2)) 'yes, so strip CR/LF & leading spaces .
SkipJustify = True 'indicate no reason to justify.
End If
'-------------------------------------------------------------------
If Len(Tmptext) <> 0 AndAlso Not SkipJustify Then 'If no skip justify & line needs it...
TextSize = TextRenderer.MeasureText(Tmptext, .Font, .Size, Flags) 'get its current size
Dim SpcNeeded As Int32 = .Width - (TextSize.Width - MeasureAdjust) 'compute # of pixels to add in line.
Tmptext = Tmptext.Replace(" "c, SpcInit) 'then replace spaces with special code.

Dim Idy As Int32 = 1 'init col start (used to skip LEAD spaces).
Do While Asc(Mid(Tmptext, Idy, 1)) > 127
Idy += 1 'skip past leading spaces (indent).
Loop
'---------------------------------------------------------------
If SpcNeeded > 0 Then 'if space insertions needed for line...
'-----------------------------------------------------------
'Do an initial pass through the string and locate all space position to update.
'Update the required spaces at the same time to speed things along. If more
'spacing is required, subsequent passes will only process space positions without
 Page 474

'needing to scan every character. This greatly speeds up processing.


'-----------------------------------------------------------
Dim SpcIdx As Int32 = -1 'init index to space location array.
For Idz As Int32 = Idy To Len(Tmptext) 'scan through line data to find spaces.
Dim cAsc As Int32 = Asc(Mid(Tmptext, Idz, 1)) 'get the currently indexed character code.
If cAsc > 127 Then 'special spacing character found?
Mid(Tmptext, Idz, 1) = Chr(cAsc + 1) 'yes, so re-stuff it with 1 incr.
SpcNeeded -= 1 'drop 1 from # of padding pixels needed.
If SpcNeeded = 0 Then 'have we added all spaces line needs?
Exit For 'yes, so do not add any more.
End If
SpcIdx += 1 'else bump space location array index.
If SpcIdx > UBound(SpcAry) Then 'if we need more array space...
ReDim Preserve SpcAry(SpcIdx + 16) 'bump it up...
End If
SpcAry(SpcIdx) = Idz 'then stuff a space location into it.
End If
Next
'-----------------------------------------------------------
'If more space insertion is required, speed things up by processing only
'spacing positions within the line using the space location array.
'-----------------------------------------------------------
Dim AddedSpacing As Boolean = False 'set flag to detect if we added spacing.
Do While SpcNeeded > 0 'while we still need space.
For Idz As Int32 = 0 To SpcIdx 'loop through the space index.
Mid(Tmptext, SpcAry(Idz), 1) = Chr(Asc(Mid(Tmptext, SpcAry(Idz), 1)) + 1) 'increment space code.
AddedSpacing = True 'indicate at least 1 space was changed.
SpcNeeded -= 1 'drop 1 from # of padding pixels needed.
If SpcNeeded = 0 Then 'have we added all need for line?
Exit For 'yes, so do not add any more.
End If
Next
'-----------------------
If Not AddedSpacing Then 'if we could not add any space...
Exit Do 'we exit loop to avoid wndless loop.
Else
AddedSpacing = False 'wlse reset flag for another pass.
End If
Loop
End If
End If
'-------------------------------------------------------------------
Result &= vbCrLf & Tmptext 'add TmpText data to accumulator.
Next 'then process the next line of text.
'-----------------------------------------------------------------------
DirectCast(TxtBox.Tag, TextDataReference).FmtText = Mid(Result, 3) 'finally, save result, less leading vbCrLf.
End With '(NOTE: The Paint event will finish job).
End Sub

'*******************************************************************************
' Method Name : JustifyText_Paint
' Purpose : Draw text from a TextBox onto a PictureBox control (created in the
' : JustifyTextBoxDisplay() method), using specially inserted spacing codes.
' :
' NOTE : A reference to the associated Textbox is stored in the PictureBox's Tag property.
'*******************************************************************************
Private Sub JustifyText_Paint(sender As Object, e As PaintEventArgs)
With DirectCast(DirectCast(sender, PictureBox).Tag, TextBox) 'use TextBox PictureBox is associated...
Dim TxtData As String = .Tag.ToString 'grab formatted text.
Dim TextSize As Size = TextRenderer.MeasureText("X y", .Font, .Size, Flags) 'get pixel W and H of sample text.
Dim Yinc As Int32 = TextSize.Height 'save height for inc lines in PicBox.
Dim SpcSize As Int32 = TextSize.Width 'save the length result.
TextSize = TextRenderer.MeasureText("Xy", .Font, .Size, Flags) 'grab same text, but w/o space.
SpcSize -= (TextSize.Width + 1) 'compute space width (-1 for rendering).
Dim MeasureAdjust As Int32 = CInt(.Font.Size - 0.5!) - SpcSize 'spacing adjustment we must subtract.

Dim PosnX As Int32 = e.ClipRectangle.Left 'start pos for drawing in PictureBox.


Dim PosnY As Int32 = e.ClipRectangle.Top
Dim TmpTxt As New System.Text.StringBuilder(256) 'init temporary text holder.
'---------------------------------------------------------------------------
For Each Ch As Char In TxtData 'scan each char in master text.
Dim iChar As Int32 = Asc(Ch) 'grab ASCII code for indexed char.
Select Case iChar
Case Asc(vbCr), Asc(" "c), Is > 127 'break, Space, or special spacing char?
If TmpTxt.Length <> 0 Then 'so dump temp text if accumulated data.
TextRenderer.DrawText(e.Graphics, TmpTxt.ToString, .Font,
New Point(PosnX, PosnY), .ForeColor, .BackColor, Flags)
TextSize = TextRenderer.MeasureText(TmpTxt.ToString, .Font, .Size, Flags) ' grab size of text.
PosnX += (TextSize.Width - MeasureAdjust) 'bump x offset less hard-coded margins.
TmpTxt.Clear() 'clear the temp text buffer.
End If
Select Case iChar
Case Asc(vbCr) 'line-break?
PosnX = e.ClipRectangle.Left 'reset X offset to left side...
PosnY += Yinc 'bump line index to next line position.
 Page 475

Case Asc(" "c) 'a space char?


PosnX += SpcSize 'yes, bump X offset by 1 space size.
Case Else 'otherwise, it special spacing char...
PosnX += (iChar And 127) 'bump X offset by spacing code.
End Select
Case Asc(vbLf) 'Linefeed? If so, ignore it.
Case Else 'normal char?
TmpTxt.Append(Ch) 'yes, add char to temp text buffer.
End Select
Next
'---------------------------------------------------------------------------
If TmpTxt.Length <> 0 Then 'done; dump remaining temp text data.
TextRenderer.DrawText(e.Graphics, TmpTxt.ToString, .Font,
New Point(PosnX, PosnY), .ForeColor, .BackColor, Flags)
End If
End With
End Sub

'*******************************************************************************
' Method Name : PictureBox_Resize
'Purpose : Resize a PictureBox that was instantiated to display Justified Text.
' : This might happen because the JustifyTextBoxDisplay() method will
' : echo any anchoring defined for the TextBox to the PictureBox that
' : will display the justifyed text. As such, in addition to the Paint()
' : event handler being applied to the PictureBox, this ReSize() event
' : handler is also applied.
'*******************************************************************************
Private Sub PictureBox_Resize(sender As Object, e As System.EventArgs)
Try
With DirectCast(sender, PictureBox)
Dim txtBox As TextBox = DirectCast(.Tag, TextBox) 'grab TextBox.
txtBox.Width = .Width 'make TextBox size of PictureBox (TextBox may not resize).
txtBox.Height = .Height
RefreshPicTextData(txtBox) 'reformat the TextBox Text for new characteristics.
.Refresh() 'repaint the associated PictureBox.
End With
Catch
End Try
End Sub

'*******************************************************************************
' Method Name : TextBox_FontChanged
'Purpose : When the font or font point size changes, the data needs to be
' : updated, just like when the TextBox resizes. Therefore, within the
' : JustifyTextBoxDisplay() method, the TextBox has this event handler
' : applied to it.
'*******************************************************************************
Private Sub TextBox_FontChanged(sender As Object, e As System.EventArgs)
' reformat the text using the original text
RefreshPicTextData(DirectCast(sender, TextBox))
' force a repaint of the PictureBox data
DirectCast(DirectCast(sender, TextBox).Tag, TextDataReference).PictBox.Refresh()
End Sub

End Module

BONUS TIP 1: If we take advantage of the SizeMessage method demonstrated in Black Book Tip # 7 (page 465, “Sizing a
Label or TextBox to Fully Contain a String For Display”), but use it with the TextBox we want to display justified, we can
Invoke the SizeMessage method first, such as Dim TextSize As Size = SizeMessage(Me.myTextBox.Text, Me.myTextBox.Font,
550) to set a maximum width of 550, then size the TextBox from the dimensions returned in TextSize, and finally invoke
JustifyTextBoxDisplay(Me.myTextBox). This way we can be assured that the TextBox will fully contain our message, especially if
we do not know ahead of time the exact size of our message.
For example:

'*********************************************************************************
' Method : AdjustTextBoxForJustify
' Purpose : Size a TextBox for a Message of unknown size and then Justify its text.
'
' NOTE: This method assumes the Message is held in the TextBox Text property.
' Note also that this will not shrink the TextBox any smaller than its
' size defined at Development Time; it will only grow it.
'*********************************************************************************
Friend Sub AdjustTextBoxForJustify(ByRef TxtBox As TextBox, ByVal MaxWidth As Int32)
With TxtBox
Dim TextSize As Size = SizeMessage(.Text, .Font, MaxWidth) 'first, get proposed size for TextBox container.
.Parent.Width = .Parent.Width - TxtBox.Width + TextSize.Width 'next adjust parent form to contain TextBox.
 Page 476

.Parent.Height = .Parent.Height - TxtBox.Height + TextSize.Height


.Width = TextSize.Width 'resize TextBox. If anchored to Form sides...
.Height = TextSize.Height 'then these 2 lines will do nothing new.
JustifyTextBoxDisplay(TxtBox) 'finally, justify the display.
End With
End Sub

BONUS TIP 2: If we want or need to perform full text justification on a RichTextBox, we can actually get the RichTextBox
to perform full-Justification on its own! Sadly, VB.NET accesses the default state of a RichTextBox control as it was defined
for RichEdit 1.0 and RichEdit 2.0. Since then, as of Windows XP Service Pack 1, RichEdit 3.0 and MSFEdit 4.1 have been
released. .NET uses at least RichEdit 3.0. But these newer versions actually support Full Text Justification. See Black Book
Tip # 30 on page 555 to see how we can very easily do just that!

BONUS TIP 3: If we would like to use a much simpler function that will very decently justify text on-the-fly and off-the-cuff,
which we can then display in a TextBox, Label, or even in a MsgBox dialog, see Black Book Tip # 46, “Quick and Easy
Text-Justification for Text Boxes, Labels, and Dialog Boxes” on page 614.
 Page 477

Black Book Tip # 10


Get The ListIndex of a ListBox Row Under the Cursor Without a Mouse Click.
Often we need to know the item we are moving the Cursor over or right-clicking on in a ListBox or a
CheckedListBox. This is useful for updating the control’s tooltip or allowing us to construct a Context
Menu that is tailored for the data the cursor is currently pointing at.
As we may already be painfully aware, the selection index for a ListBox does not update when we move
over or right-click one of its items, nor should it. Even so, there is no GetItemAt method for a ListBox to
tell us which item line the Cursor is located, as there is with a ListView control.
Even so, this index is very easy to compute. As it happens, both the MouseMove and MouseDown event
handlers for a ListBox also provides us with a X and Y pixel coordinate within the ListBox, telling us
where the Mouse Cursor is located relative to the ListBox client area. The Y (vertical) offset is the only
part of this information that we will need in order to compute the relative line the cursor is over.
If we divide this Y value by the Item Height of a line (LstBox.ItemHeight) and then add the index of the
line that is currently displayed at the top of the ListBox (LstBox.TopIndex), we have in fact computed the
relative line within the ListBox where the Mouse Cursor is located. All that we have left to do is to
ensure that this computed line does not exceed the number of actual lines in the ListBox, in case the
mouse might be over the blank field below a list of items, which can happen on short lists. We can do
this by comparing the computed index against the number of items in the ListBox (LstBox.Items.Count).
Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modListItemByCoordinate Static Class Module
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modListItemByCoordinate
'*******************************************************************************
'Get a ListIndex in a ListBox by coordinates. Useful on Right-Clicks
'*******************************************************************************
' modListItemByCoordinate - The ListItemByCoordinate() function returns the ListIndex of an item in a ListBox.
' Although this is normally possible with a left-click, where you can obtain it by
' the obvious ListBox.ListIndex property, the item is not selected on a rightpick or
' a mousemove, where you might want to display information on the item in a ToolTip.
' If the returned index is -1, then the mouse is not over a valid item in the list.
'EXAMPLE:
' Private Sub MyListBox_MouseDown(sender As Object, e As MouseEventArgs) Handles MyListBox.MouseDown, ItsLstBox.MouseDown
' If e.Button = MouseButtons.Right Then 'Right pick?
' Dim LstBx As ListBox = DirectCast(sender, ListBox) 'yes, so Get the listbox we are to work with.
' Dim Itm As Int32 = ListItemByCoordinate(LstBx, e.Y) 'get list item the mouse is over.
' If Itm <> -1 Then 'if it is a valid index...
' Me.ToolTip1.SetToolTip(LstBx, "Right-Clicked on " & LstBx.Items(Itm).ToString)
' End If
' End If
' End Sub
'*******************************************************************************

'*******************************************************************************
' Method Name : ListItemByCoordinate
' Purpose : Get a ListIndex in a ListBox by coordinates
'*******************************************************************************
Friend Function ListItemByCoordinate(ByRef LstBox As ListBox, ByVal Y As Int32) As Int32
'Get the vertical mouse position and divide it by the height of a row to get the relative row number, then
' add the TopIndex value, which is the index to the row at the top of the ListBox, to determine the actual
' index of the row selected.
Dim Idx As Int32 = Y \ LstBox.ItemHeight + LstBox.TopIndex
' If the computed index is higher than the number of items in the list, then return -1.
If Idx >= LstBox.Items.Count Then Idx = -1
Return Idx
End Function

'*******************************************************************************
' Method Name : ListItemByCoordinate
' Purpose : Get a ListIndex in a CheckedListBox by coordinates
'*******************************************************************************
Friend Function ListItemByCoordinate(ByRef LstBox As CheckedListBox, ByVal Y As Int32) As Int32
'Get the vertical mouse position and divide it by the height of a row to get the relative row number, then
' add the TopIndex value, which is the index to the row at the top of the ListBox, to determine the actual
' index of the row selected.
Dim Idx As Int32 = Y \ LstBox.GetItemHeight(0) + LstBox.TopIndex
' If the computed index is higher than the number of items in the list, then return -1.
If Idx >= LstBox.Items.Count Then Idx = -1
Return Idx
End Function
End Module
 Page 478

Black Book Tip # 11


Open File Explorer With a Target File Selected.
Have we ever wondered how some applications will open File Explorer and already have the target file
selected in the window? This is actually so easy to do that this tip was almost not included, yet I am
constantly asked how to do it. But, once I show it to you, you will see exactly how simple it really is.
First, most seasoned developers take advantage of File Explorer through the ShellExecuteEx Pinvoke.
Through this, we can issue specific commands like “open”, “explore”, “Print”, “properties”, “edit”, and
“find”. This is all well and good, and I use it all the time to take advantage of its many features, though
it does implement a complicated-looking, though actually quite easy-to-use Structure named
SHELLEXECUTEINFO.
But we do not actually need any of that. All we need to do is directly access Explorer.exe, the executable
for File Explorer, and provide it with some special parameters.
For this, we will need only 4 pieces of information:
1. The path to our Target File (too easy... NEXT!).
2. Where to find Explorer.exe (so easy it is almost stupid).
3. What the special parameters are (easy to find).
4. How to launch all this (almost too easy to do).
We should already have the path to our Target File. We can directly access Explorer.exe without locating
it through our launcher, Shell(). Its command line options can be quickly found by searching
http://support.microsoft.com for “Windows Explorer Command-Line Options” or “KB152457”.
The Shell command is often derided by developers as amateur access to the operating system. Forgive them,
O Lord, for they know not what they do. Granted, it requires little effort to use, which most developers see as
simplistic, but its complexity is just hidden by the primary workhorse of the Windows Operating System,
Shell32.DLL. One of its best features is, if our executable is in the system path, we do not need to provide the
folder path to the Shell command. Hence, “Shell("explorer.exe")” is all we need to launch File Explorer,
and we can open it to folders on our drive, such as our MyDocuments folder, with “Shell("explorer.exe " &
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments))”. Actually, we do not even need the
“.exe” extension, but I am fickle about that because I want my code reviewers to know my exact intent, and
introducing ambiguity into code is an invitation for bugs that tend to easily get overlooked.
If we look to Microsoft’s Knowledge Base at Explorer [/n] [/e] [(,)/root,<object>] [/select,<object>]

/n Opens a new single-pane window for the default


http://support.microsoft.com/kb/152457, we find a table selection. This is usually the root of the drive
Windows is installed on. If the window is already
describing and exemplifying the four optional Explorer.exe open, a duplicate opens.

parameters, as shown to the right: /e Opens Windows Explorer in its default view.

/root,<object> Opens a window view of the specified object.

As we can see, we certainly need the “/select,”


/select,<object> Opens a window view with the specified folder, file or
parameter to select our target file or folder, but we application selected.

should also not forget about the “/e,” parameter as well, Examples:

which will open File Explorer using its default view; Example 1: Explorer /select,C:\TestDir\TestApp.exe

Opens a window view with TestApp selected.


this being the view we may have specified at some point Example 2: Explorer /e,/root,C:\TestDir\TestApp.exe
when we opened Explorer (we can also easily adjust This opens Explorer with C: expanded and TestApp selected.

this by using Explorer’s View menu). Example 3: Explorer /root,\\TestSvr\TestShare

Opens a window view of the specified share.


So, for our use, we will combine these two commands Example 4: Explorer /root,\\TestSvr\TestShare,select,TestApp.exe

into the primary string, “explorer.exe /e,/select,”, and Opens a window view of the specified share with TestApp selected.

then append the path to our target file or folder.


For example: “Shell("explorer.exe /e,/select," & MyFilePath)”. And that is all there is to it!
 Page 479

Black Book Tip # 12


Determining if an Array is Dimensioned.
I have seen a barrage of code on the web explaining to .NET users how to detect if an array is
dimensioned, some getting quite elaborate, thanks to the more complex formatting of arrays. For
example, apart from Arrays now being objects, which are immensely useful, we must also take into
consideration ranking, which is the number of dimensions. Ranking, in simple terms, is the number of
commas we have in our array declarations. For example, my1DArray(10) is a 1-dimensional array,
my2DArray(10, 20) is 2-dimensional, and my3DArray(10, 20, 30) is obviously a 3-dimensional array.
If we have an array and know its ranking, we can quickly pick up how deep each rank of the array is by
using the optional Ranking parameter of a UBound function. For example, Dim aDim As Int32 =
UBound(my3DArray, 1) to get the upper bounds of the first dimension of any array (the 1-parameter is the
default), or use Dim aDim As Int32 = UBound(my3DArray, 2) to get the UBound of the second
dimension, or use Dim aDim As Int32 = UBound(my3DArray, 3) to get the UBound of the third dimension.
But what if we want to know if the array, regardless of dimensioning, is dimensioned to begin with?
This would be important when we might declare an empty array to start, and later redimension it to fit
the data that our program will need to load into it.
Under VB6, this was amazingly simple to do. We might define a simple function that returned TRUE if
the array was dimensioned, or FALSE if it was not. For example, consider this simple VB6 code I had
written to determine if any type of array was dimensioned:
'Determine if an array is dimensioned under VB6
'********************************************************************************
' modIsDimmed - The IsDimmed() function returns True if the specified array is
' dimensioned.
'EXAMPLE:
' Dim Test() As String
'
' Print IsDimmed(Test) 'prints False
' ReDim Test(5 To 6)
' Print IsDimmed(Test) 'prints True
' ReDim Test(0 To 5)
' Print IsDimmed(Test) 'prints True
' Erase Test
' Print IsDimmed(Test) 'Prints False
'********************************************************************************
Public Function IsDimmed(vArray As Variant) As Boolean
On Error Resume Next
IsDimmed = IsNumeric(UBound(vArray))
On Error GoTo 0
End Function

That was all there was to it, regardless of how many dimensions there were defined. To this day, I hear
die hard VB6 “aficionados” claiming how this makes VB6 superior to VB.NET. This one thing versus
everything else VB.NET has to offer? (Are they about to be disappointed...)
Unfortunately, under VB.NET this does not play well with arrays, because they are class objects, and so
trying to pass their array name as an object will give the compiler fits. I have seen some quite ingenious
solutions to get around this under .NET, to include writing a slough of overloaded methods of each type
and, because we must include the type of the array, we must therefore also include its ranking, and so we
would see 3 versions of each test, each addressing 1, 2, or 3 dimensions.
However, one thing I think we are all missing in the heat to solve, or over-engineer the solution to this
dilemma is to determine the root of what we are actually testing. For example, if I declare the following:
Dim my3DArray(,,) As Double

It is actually the very same as declaring it as the following:


Dim my3DArray(,,) As Double = Nothing

So, from this, how do we determine if an array of any ranking is dimensioned? Simply like this:
If my3DArray IsNot Nothing Then 'True if the array is dimensioned.
 Page 480

Black Book Tip # 13


Customizing the Display of TabControl Tabs..
As wonderful and as easy as the TabControl is to use, one of its less glamorous
aspects is in its seeming inability to distinctly highlight which tab is active.
Granted, it is slightly obvious in that the selected tab is shown as white and the
other tabs are a light-light-gray, unless we are visually impaired like me.
However, apart from selecting its appearance to alternatively be normal or as flat
buttons (refer to the control’s Appearance property), I like to make the normal appearance of these tabs
much more distinct, which I usually do by displaying the selected tab’s text in bold and sometimes also
as a different color, and making the other tabs a bit darker, but always a shade of gray, although a dark
version of a brighter color assigned to the selected tab also works quite well.
Fortunately, the control’s default display can be
changed because it does afford us the ability to
customize each tab’s appearance, even to the point
that we can make them another shape, such as a
manila folder tab or like the tab formats shown in
the various evolving versions of the Visual Studio Integrated Development Environment (IDE). If we
choose to, we can even draw images or icons on them. I will show you where we can do it, but it will be
left entirely up to you to actually implement such dramatic, creative enhancements.
The Tab Control offers a hook into its tab display process through its DrawItem event handler, allowing
us to customize each tab after it has drawn helpful tab shadows on the TabControl’s background through
its MyBase.Paint event, which we can erase or over-draw within the DrawItem event (or even by
intercepting the Paint or OnPaint events. if we are– hopefully–feeling an urge to conduct research).
The DrawItem event handler is activated by setting the Tab Control’s DrawMode
property. This property is set to Normal, meaning that the .NET Framework’s
Common Language Runtime (CLR) will take care of drawing our tabs. But if we
want to take care of that instead, we would set this property to OwnerDrawFixed.
Setting this will also initially render the object a bit differently, because it is now
expecting us to perform all the detail work, as shown to the right. This is because the
drawing responsibility for each tab, and even within the IDE, is now squarely in our hands. This includes
rendering the tabs to look distinctly different, which, as we can see from the example, the CLR no longer
handles, making the distinction between tabs, save for a shadowed, prior-drawn edge, difficult to discern.
Some developers are not aware that the Tab Control even fires events because if we double-click on the
control during development, a default event handler is not generated. However, we can quickly generate
this event body by going to the form’s code page (click the ViewCode icon in the Solution Explorer’s
menu tab), and from the left dropdown at the top of the code page (the Controls dropdown), select our
TabControl object, and then from the right dropdown, select DrawItem from the Declarations
dropdown; this will construct for us an event body like the one shown below:
Private Sub TabControl1_DrawItem(sender As Object, e As DrawItemEventArgs) Handles TabControl1.DrawItem

End Sub

The “sender” object provides us with a connection to the TabControl object we are working with. This is
immensely handy if we are wanting to handle more than one TabControl object with this event, where we
would simply add handlers to other Tab Controls by appending the other controls and their selected event
process (always DrawItem in this case) in the header of this event code as a list, such as “ ) Handles
TabControl1.DrawItem, TabControl2.DrawItem”, or, as I usually do, I hook them in during the Form’s Load
event with something like “AddHandler TabControl2.DrawItem, AddressOf TabControl1_DrawItem”, though I
will also usually rename the event handler method to a more generic title, such as simply “DrawOnTabs”.
 Page 481

Regardless, we now have our DrawItem event interface. Just like other item drawing events, such as the
DrawItem event for ListBox, CheckedListBox, or ComboBox controls, it will not draw anything if we do not
provide it with the code to do so (Duh!). Within this event, we will be responsible for drawing the contents of
the tab and controlling its distinctiveness.
The first things we will want to do is get a hold on the actual tab control object being worked with, especially
if we are handling more than one with this event. Fortunately, this is very easy to do with something like this:
Dim TabControl As TabControl = DirectCast(sender, TabControl) 'get the tab control being processed

NOTE: Some bloggers make the point that naming an object reference to be the same as its type is too confusing. If the code using
it is not clearly written, then that might well be the case. However, from a software standpoint, it is clearly not true, and actually
helps to prevent a lot of bugs from creeping into our code. Nothing is worse than naming an object of type TabControl as
TabControl1, and then in the rush to write the code and taking a little too much advantage of Intelli-sense, we accidentally
reference TabControl instead of TabControl1, and then sit there wondering why a bug is being reported. Besides, if we actually do
want to clearly differentiate the divide between our reference names and their object types, we can mark an object type by
surrounding it with square brackets with absolutely no cost in code generation, such as in “ Dim TabControl As [TabControl]”. This
use of square brackets around a command is also a useful tool when we are over-riding a base object command, such as ToString,
but we might still need to use the base command version of ToString within our over-riding code.

Of course, we will also want to grab the actual Tab Page itself, which the DrawItemEventArgs variable,
e, provides us access to through its Index property, which we can gather in this form:
Dim TabPage As TabPage = TabControl.TabPages(e.Index) 'get the tab page being processed.

The next thing we will want to do is paint the tab’s background. This involves optionally painting a
rectangle around the edges of the tab and then filling the background with a color. We will start simple,
so we will first draw a Silver rectangle along the tab’s border, and then fill it with a White background.
We can grab the tab’s bounding rectangle through the Tab Control’s GetTabRect method:
Dim tabRect As Rectangle = TabControl.GetTabRect(e.Index) 'get the tab rectangle for the current tab page.
e.Graphics.DrawRectangle(Pens.Silver, tabRect) 'draw tab rectangle (I personally do not do this).
e.Graphics.FillRectangle(Brushes.White, tabRect) 'now, fill the tab with our selected background color...

The final thing we must do is to draw the tab’s text on the tab so that each tab will have a more apparent
and meaningful purpose. For the font, we can simply use the Tab Control’s font:
e.Graphics.DrawString(TabPage.Text, TabPage.Font, Brushes.Black, tabRect) 'draw tab contents using selected font.

All this stitched together defines our preliminary experiment into drawing our own tabs:
Private Sub DrawOnTab (sender As Object, e As DrawItemEventArgs) Handles TabControl1.DrawItem
Dim TabControl As [TabControl] = DirectCast(sender, [TabControl]) 'get tab control to process ([] not really needed).
Dim TabPage As [TabPage] = TabControl.TabPages(e.Index) 'get tab page being processed
Dim tabRect As Rectangle = TabControl.GetTabRect(e.Index) 'get tab rectangle for current tab
e.Graphics.DrawRectangle(Pens.Silver, tabRect) 'draw tab rectangle
e.Graphics.FillRectangle(Brushes.White, tabRect) 'now, fill tab with selected background color...
e.Graphics.DrawString(TabPage.Text, TabPage.Font, Brushes.Black, tabRect) 'draw tab contents using selected font.
End Sub

However, so far, what we will see from this is what is shown on the right:
We have so far emulated much of what the Tab Control’s built-in support
was providing, though we might notice that their unselected tabs are a very
light gray, whereas ours are by default white, and their text is centered,
whereas ours is not, but originate in the top-left corner of each tab.
Ignoring text centering for now, let us first focus on customizing the color appearance of our tabs. I am
all for customizing the background color of the selected tab. It often makes immediately knowing which
tab is active, especially when we have multiple rows of tabs, easy to discern by the user after they have
become “trained” to recognize a particular color from several uses of the interface. However, I still
prefer to gray-out the background of all unselected tabs, though I really like using a slightly darker, more
contrasting shade than the SystemColor.Control color that is used by default.
 Page 482

To do this, however, we must also know which tab is actually selected. This is very easy to pick up from
the Tab Control’s SelectedIndex property, which provides the index number of the selected tab that we
can also compare against the current tab being drawn, which is provided by e.Index. For example:
Dim SelectedIdx As Int32 = TabControl.SelectedIndex 'get user-selected tab.

For now, let us keep the selected tab’s background color white, but set the unselected tab background colors
to Light Gray. We would modify the last 3 command lines of our DrawOnTab event handler like so:
e.Graphics.DrawRectangle(Pens.Silver, tabRect) 'draw tab rectangle.
If TabControl.SelectedIndex = e.Index Then 'is current tab to paint also active tab?
e.Graphics.FillRectangle(Brushes.White, tabRect) 'yes; fill tab with active color...
Else
e.Graphics.FillRectangle(Brushes.LightGray, tabRect) 'else fill tab with inactive color...
End If
e.Graphics.DrawString(TabPage.Text, TabPage.Font, Brushes.Black, tabRect) 'draw tab text.

This renders the Tab Control more distinctly, as shown to the right:
Personally, I like to distinguish the active Tab even more by displaying its
text as Bold. Hence, I would further enhance the above code as follows:
e.Graphics.DrawRectangle(Pens.Silver, tabRect) 'draw the tab rectangle.
If TabControl.SelectedIndex = e.Index Then 'is the current tab to paint also the active tab?
e.Graphics.FillRectangle(Brushes.White, tabRect) 'yes; fill the tab with our active color...
Dim txtFont As Font = New Font(TabPage.Font, FontStyle.Bold) 'draw selected tab's font as bold.
e.Graphics.DrawString(TabPage.Text, txtFont, Brushes.Black, tabRect) 'draw the tab text.
txtFont.Dispose() 'and finally dispose of the created resource.
Else
e.Graphics.FillRectangle(Brushes.LightGray, tabRect) 'else fill the tab with our inactive color...
e.Graphics.DrawString(TabPage.Text, TabPage.Font, Brushes.Black, tabRect) 'draw the tab text.
End If

This change has the effect shown to the right:


One thing that I dislike about this so far, and this is just of the Tab Control in
general, is the presence of the selection rectangle on the selected tab. There is
really not much we can do about it, unless we set the focus to a control on
the tab when the tab is entered, which is where focus is normally, if we
actually had controls on each tab page. For example, suppose I added a button
to each tab (for those new to Tab Controls, simply click the Tab Control, then
select the displayed tab to select in the actual tab – sometimes we must do this
twice in order to roll object focus inward to what we actually want to select –
and then select the body of the tab page to select the actual TabPage control
for the desired tab). Then, I would add an Enter event for each tab page, and
set the focus for this button. For example:
Private Sub TabPage1_Enter(sender As Object, e As EventArgs) Handles TabPage1.Enter.
Me.Button1.Focus() 'emulate focus going to controls on tab page, where it normally is.
End Sub

Private Sub TabPage2_Enter(sender As Object, e As EventArgs) Handles TabPage2.Enter


Me.Button2.Focus()
End Sub

Private Sub TabPage3_Enter(sender As Object, e As EventArgs) Handles TabPage3.Enter


Me.Button3.Focus()
End Sub

This provides a cleaner appearance to the tab, as shown on the right:


We can of course do other things here, like set the selected color not just to White, but to any color we
choose. We can even use the DrawImage Graphics method to actually draw small images to the tab,
which might be more useful in an interface employed in perhaps a child’s learning game. However, this
will be left for us to fully explore on our own.
What I want to conclude this tip with is displaying the text centered on the tab, not just drawing to its
top-left corner. To do this is quite easy, though it will require a different target rectangle. This rectangle,
like the previous rectangle that we had gathered from the GetTabRect method of the Tab Control, will
tell the DrawText method where to draw the text on the Tab. All we need to do is compute a new top-left
corner for the text that will center it, both horizontally and vertically within the tab.
 Page 483

The math to do this is all very simple. For vertical centering, we add half the difference between the Tab
rectangle’s height and the height of the text, and we will do likewise for horizontal centering. We can get
the dimensions of the text using the TextRenderer.MeasureText method. For example:
Dim txtSize As Size = TextRenderer.MeasureText(TabPage.Text, txtFont) 'get the width and height of the text to render in pixels.
Dim Y As Int32 = tabRect.Y + (tabRect.Height - txtSize.Height) \ 2 'compute vertical centering start location.
Dim X As Int32 = tabRect.X + (tabRect.Width - txtSize.Width) \ 2 'compute horizontal centering start location.
Dim cntrRect As New Rectangle(New Point(X, Y), txtSize) 'compute new centering rectangle with text width/height limits.

And finally, in the DrawText method, we would use cntrRect in place of tabRect. We would also have to
apply the bold font instead for the selected tab, rendering the following, now overly busy code:
If TabControl.SelectedIndex = e.Index Then 'is the current tab to paint the active tab?
e.Graphics.FillRectangle(Brushes.White, tabRect) 'yes; fill the tab with our active color...
Dim txtFont As Font = New Font(TabPage.Font, FontStyle.Bold) 'draw selected tab's font as bold.
Dim txtSize As Size = TextRenderer.MeasureText(TabPage.Text, txtFont) 'get the width and height of the text to render in pixels.
Dim Y As Int32 = tabRect.Y + (tabRect.Height - txtSize.Height) \ 2 'compute vertical centering start location.
Dim X As Int32 = tabRect.X + (tabRect.Width - txtSize.Width) \ 2 'compute horizontal centering start location.
Dim cntrRect As New Rectangle(New Point(X, Y), txtSize) 'compute new centering rectangle with text width/height limits.
e.Graphics.DrawString(TabPage.Text, txtFont, Brushes.Black, cntrRect) 'draw the tab text.
txtFont.Dispose() 'and finally dispose of the created resource.
Else
e.Graphics.FillRectangle(Brushes.LightGray, tabRect) 'else fill the tab with our inactive color...
Dim txtSize As Size = TextRenderer.MeasureText(TabPage.Text, TabPage.Font) 'get the width and height of the text to render in pixels.
Dim Y As Int32 = tabRect.Y + (tabRect.Height - txtSize.Height) \ 2 'compute vertical centering start location.
Dim X As Int32 = tabRect.X + (tabRect.Width - txtSize.Width) \ 2 'compute horizontal centering start location.
Dim cntrRect As New Rectangle(New Point(X, Y), txtSize) 'compute new centering rectangle with text width/height limits.
e.Graphics.DrawString(TabPage.Text, TabPage.Font, Brushes.Black, cntrRect) 'draw the tab text.
End If

Though the above new code actually works, for me it is rather cumbersome, as most first-draft code is.
So let us try optimizing it so that it does not duplicate a lot of method invocations, being that the code is
almost identical. I like the following optimization, though yours might actually be quite different (that is
the beauty of code development – it is so personal). Also, just for the sake of experimentally changing
things up a bit, notice that I have additionally changed the selected tab’s font color to Navy:
'*********************************************************************************
' Method : DrawOnTab
' Purpose : Owner draw each individual tab on a TabControl.
' : This is actually a TabControl object method, but
' : it is used to render its tabs and their text.
' : Be sure that [TabControl].DrawMode = TabDrawMode.OwnerDrawFixed
'*********************************************************************************
Private Sub DrawOnTab(sender As Object, e As DrawItemEventArgs) Handles TabControl1.DrawItem
Dim TabControl As TabControl = DirectCast(sender, TabControl) 'get the tab control being processed.
Dim TabPage As TabPage = TabControl.TabPages(e.Index) 'get the tab page being processed.
Dim tabRect As Rectangle = TabControl.GetTabRect(e.Index) 'get the tab rectangle for the current tab.
e.Graphics.DrawRectangle(Pens.Silver, tabRect) 'draw the tab rectangle.
Dim bgBrush As Brush 'store to desired tab background color.
Dim txtBrush As Brush 'store to desired font color.
Dim txtFont As Font 'store the draw font as bold or normal.
If TabControl.SelectedIndex = e.Index Then 'is the current tab to paint also the active tab?
bgBrush = Brushes.White 'yes, so use White for the tab background...
txtBrush = Brushes.Navy 'and use Navy for its text...
txtFont = New Font(TabPage.Font, FontStyle.Bold) 'then define the Bold font...
Else
bgBrush = Brushes.LightGray 'otherwise, use a grayed background color...
txtBrush = Brushes.Black 'use our default text color...
txtFont = New Font(TabPage.Font, FontStyle.Regular) 'and ensure that the tab font is normal (in case it is not).
End If
e.Graphics.FillRectangle(bgBrush, tabRect) 'now, fill the tab with our selected background color...
Dim txtSize As Size = TextRenderer.MeasureText(TabPage.Text, txtFont) 'get the width and height of the text to render in pixels.
Dim Y As Int32 = tabRect.Y + (tabRect.Height - txtSize.Height) \ 2 'compute vertical centering start location.
Dim X As Int32 = tabRect.X + (tabRect.Width - txtSize.Width) \ 2 'compute horizontal centering start location.
Dim cntrRect As New Rectangle(New Point(X, Y), txtSize) 'compute new centering rectangle with text wd/ht limits.
e.Graphics.DrawString(TabPage.Text, txtFont, txtBrush, CntrRect) 'draw the tab text
txtFont.Dispose() 'and finally dispose of the created resource.
End Sub
 Page 484

BONUS TIP: If we want to eliminate the line drawn beneath the Selected Tab so that this tab and its Tab Page blend more
evenly, we can actually draw the left, top, and right border lines around the tab and not bother with the bottom border, or, we
can simply draw the rectangle, and then draw a bottom border that is the same color as the Tab Page background. I prefer
the latter method because even if we did just draw the 3 other borders, we will still see a border along the bottom because
the system will have already drawn a line below it, but 1 pixel outside the range of the tab’s bounding rectangle, so we will
still have to hide that one as well. We can do that in one shot by simply drawing a line that is 2 pixels wide.
We should keep in mind two things: 1) if the Tab Page background color is set to
Transparent, the darker background will simply wash through it, thus defeating
our intent, and 2) as indicated above, the line we draw along the bottom will need
to be 2 pixels wide; 1 for the tab border that we had just drawn, and another for
the line drawn by the system for the Tab Page border, which had been drawn
before any of our tabs are rendered.
For example, if we had set all our tab pages to have their BackColor properties
set to White, we could add the following lines immediately below the test line “If
TabControl.SelectedIndex = e.Index Then” in the above example:
With tabRect 'for the line below to work, make sure TabPage.Backcolor is not set to Transparent, or it will not seem to work.
Dim tPen As New Pen(TabPage.BackColor, 2.0!) 'create a 2-pixel pen to cover the bottom border.
e.Graphics.DrawLine(tPen, .X, .Y + .Height + 1, .X + .Width, .Y + .Height + 1) 'draw a thick, covering white line beneath the tab.
tPen.Dispose() 'dispose of the created pen resources.
End With

Of course, we will have to adjust the X and Y offsets if we have the Tabs displayed on a different side of the page (see the Tab
Control’s Alignment property). For example, if we had the Tab Control’s Alignment property set to Bottom instead of the
default Top, the above additional code should instead be rendered:
With tabRect 'for the line below to work, make sure TabPage.Backcolor is not set to Transparent, or it will not seem to work.
Dim tPen As New Pen(TabPage.BackColor, 2.0!) 'create a 2-pixel pen to cover the bottom border.
e.Graphics.DrawLine(tPen, .X, .Y - 1, .X + .Width, .Y - 1) 'draw a thick covering line above the tab.
tPen.Dispose() 'dispose of the created pen resources.
End With

NOTE: If we are doing the math, we will notice that we are actually drawing outside
the tab’s bounding rectangle, by 1 pixel anyway, but the rectangle provided to us by
the GetTabRect method is just a guideline, not a limit, and we can draw anywhere on
the tab page, though, generally, we should still not stray too far from it if we are not
looking to totally redesign the tab page, such as giving the tabs a complete new
makeover, like rendering manila folder tabs, especially if we are feeling adventurous
and extend further out from the border rectangle and draw overlapped manila tabs,
That being said, we can actually draw our tab shapes to whatever we want, though
we may choose to do part of this within the Paint event of the tab control itself to get
around its drawing of the default shadows for the tabs, which is performed before the
DrawItem Event fires. we can even draw images of pre-defined tabs over them, or even overlap, to speed processing, In this
case, we would want to display all other tabs first, and the selected tab last, so it will overlap all others. Even though the
system sends the list of tabs in sequential ascending index order, we can save the selected tab's index and rectangle data, and
then render it last, drawing it when, but after rendering the last tab, Tricky, but it will make we feel like a Grand Master
afterward.
NOTE: Technically, the tabs are actually Button Controls that have been rendered to just look like tabs.
 Page 485

Black Book Tip # 14


Detecting a TabControl Tab on a Right MouseDown Event.
Have we ever wanted to display a context menu for a Tab Control tab, but we are foiled in that we
cannot seem to find out how to detect which tab the mouse cursor is over?
This is actually easy to do, but it requires that we use a Pinvoke that I believe really should have been
made accessible from the .NET Framework’s Tab Control, much as we are able to detect which List
View Item we are over in a List View control by invoking its GetItemAt method.
Granted, it is possible to roll through each tab’s bounding rectangle and find out if the mouse cursor is
located within any of them. For example, consider the following code I had written to do just that:
Private Sub TabControl1_MouseDown(sender As Object, e As MouseEventArgs) Handles TabControl1.MouseDown
If e.Button = MouseButtons.Right Then 'right mouse button pressed down?
Dim SelectedIndex As Int32 = -1 'init to no tab selected
With DirectCast(sender, TabControl)
For Idx As Int32 = 0 To .TabPages.Count - 1 'process each tab page
If .GetTabRect(Idx).Contains(e.Location) Then 'mouse within the tab of this tab page?
SelectedIndex = Idx 'yes, so save that index...
Exit For 'and we are done parsing
End If
Next 'parse all tabs on the control

If SelectedIndex <> -1 Then 'a tab was found?


MsgBox("User Right-Clicked on tab " &
.TabPages(SelectedIndex).Text) 'yes, so report which Tab the user right-clicked on
End If
End With
End If
End Sub

However, this is time consuming. It is especially a bother if we in fact have functionality built right into
the operating system that performs all the work for us and simply returns the index of the desired tab,
and all in one line of code.
Like so many Pinvokes, this one is different from most versions of the SendMessage command in that
its Long Pointer parameter, lParam, actually does send a Long Pointer to a memory address (a Long
Pointer, unlike its original meaning, typically refers to a value that is passed By Reference, though any
more, most instances of its use often use this parameter to simply pass a simple 32-bit Handle).
Because this application of the SendMessage Pinvoke is different from most uses of it, I have also
renamed it in order to specialize its use. Consider the following new declaration of the SendMessage
Pinvoke, and of the TCM_HITTEST constant that we will use with it:
' Sends the specified message to a window or windows. The SendMessage function
' calls the window procedure for the specified window and does not return
' until the window procedure has processed the message. Even tho renamed, the Aluas tells Win32 where to look.
Friend Declare Function SendHitTestMessage Lib "user32.DLL" Alias "SendMessageA" (
ByVal hwnd As IntPtr,
ByVal msg As Int32,
ByVal wParam As Int32,
ByRef lParam As Point) As Int32
Friend Const TCM_HITTEST As Int32 = &H130D 'used as a Hit Test Tab Control Message

With the above, we can immediately grab the index of a tab page without bothering with any testing
ourselves. So, we can replace the preceding example with this much shorter code:
Private Sub TabControl1_MouseDown(sender As Object, e As MouseEventArgs) Handles TabControl1.MouseDown
If e.Button = MouseButtons.Right Then 'right mouse button pressed down?
Dim SelectedIndex As Int32 = SendHitTestMessage(Me.TabControl1.Handle, TCM_HITTEST, 0, e.Location)
MsgBox("User Clicked on tab " & Me.TabControl1.TabPages(SelectedIndex).Text) 'report tab clicked if so.
End If
End Sub

Now that we have the index of the Tab we right-clicked on, we can go further and perhaps display a
context menu at the mouse cursor location to present the user with choices regarding that Tab Page.
 Page 486

Black Book Tip # 15


Prevent the User From Selecting a TabControl Tab..
Now that we have covered displaying and clicking on a Tab of a Tab Control, I have seen a number of
developers wondering how they can PREVENT a user from clicking on a tab. Ideally, they want to
disable a tab because its functionality is not available for the particular application that the dialog is set
up for. For example, some people want to disable a tab if the user is not authorized to access its
functionality.
The general solution that I use for my own projects is to simply delete the tab entirely, but unlike most
developers, I never use the pre-defined instance of my forms in my code. I always create a new instance,
such as Dim Frm As New frmTicTacToe, and use that instance, and also remember to dispose of its resources
after I have closed it if I am using it as a Dialog. As such, I seldom have instancing issues.
Even so, for those people who want to prevent a tab from being selected, there is a Tab Page event
handler named Selecting that is just what the doctor ordered. This event will fire before a Tab is actually
selected (at that time the system already knows that the user has clicked on it, but the system has not yet
initiated the actual tab selection process).
Within this event, if we determine that the user should not be able to access the selected tab, set the
Cancel parameter of the TabControlEventArgs parameter e to True (e.Cancel = True).
For example, suppose we have a Boolean flag named bUserAuthorized that is initialized to False, and is
set to True somewhere in the code if the user is in fact authorized to access restricted tabs on the Tab
Control by way of authentication through passwords or whatnot. We could use the following simple
code to restrict access to the second tab (Index =1, from zero):
Private bUserAuthorized As Boolean = False 'our code must set this to TRUE if the user has authorization.

Private Sub TabControl1_Selecting(sender As Object, e As TabControlCancelEventArgs) Handles TabControl1.Selecting


If e.TabPageIndex = 1 Then 'do not show the 2nd tab page if the user is not authorized.
e.Cancel = Not bUserAuthorized 'set the Cancel to True if the user is not authorized.
End If
End Sub

If this flag is set to False each time the user clicks on it, the user can click on that second tab to their
heart’s content, but it will not select in.
NOTE: It also might be useful, in cases where the tab is disabled, to also display the tab text with a dark gray or even as
white text (such as the unselected tabs are painted gray, for example), taking advantage of Black Book Tip # 13 on page 480.
NOTE: Also refer to Black Book Tip # 16 on page 487 (the next page) to see how to simply hide a tab page from view
without disposing of its resources, so that form code that is dependant upon objects assigned to that tab page existing will
not broadcast an exception error.
 Page 487

Black Book Tip # 16


Hiding Tab Pages Without Destroying the Tab Pages or Their Resources.
Previously in Black Book Tip # 15 on page 486, we were shown how to prevent a user from selecting a
tab page by using the Tab Control’s Selecting event handler and setting e.Cancel = True if we do not
want the user to actually select in that tab. The reason we might have to do this instead of removing the
tab would be if objects on that tab are accessed throughout the run of the code of its containing form.
For example, text fields are updated or images are changed, and the code will broadcast an exception
error as soon as we remove the tab.
Ideally, if it were possible that this would not affect the operation of anything else, the absolute best
solution would have been to simply delete the tab page. For example, to remove the second tab from our
Tab Control, we could simply use a command like this: Me.TabControl1.TabPages.RemoveAt(1).
Unfortunately, in the real world this seldom seems to be feasible without errors being reported. Yet,
having said that, as long as we maintain a reference to the Tab Page object we want to hide from the
user, we can in fact remove the Tab Page from the Tab Control and not lose any of its resources.
All objects are data that occupy storage space. Any variable assigned an object is just a reference to that
object; it does not house the object itself, unlike scalar variables, which are in fact Structures that
encapsulates its data. Thus, if we copy a reference variable to another reference variable, we end up with
two reference variables that point to the very same object.
NOTE: Structures are actually Classes, but they are abstracted by being able to readily auto-clone themselves to virtually
anything touching them. and they also store their data in the more transient (volatile in Geek) space of the System Stack.
For example:
Dim Pen1 As New Pen(Color.FromArgb(64, Color.Blue)) 'define a Blue pen that is 3/4 transparent.
Dim Pen2 As Pen = Pen1 'point Pen2 to the very same pen object as Pen1.

Thus, if we maintain a reference variable of type TabPage assigned to the Tab Page we want to remove,
then if we remove it from the Tab Control, it will still exist and any code that might access it within the
form will not crash because the Garbage Collector will detect that the object is still referenced and so it
will not dispose of its resources. For example:
Private bHideThumbnailTab As Boolean = True 'set to False if user is authorized to access secure tab page.
Private pThumbPageHold As TabPage = Nothing 'declare this in the heading of our form.
'...other processing and setup code goes here...
' check for displaying our 'Thumb' Tab Page (index 1) within the Form's Load() event handler.
If bHideThumbnailTab = True Then 'if we will need to hide our 'Thumbnail' tab...
pThumbPageHold = tpThumbnail 'reference the tab page to remove so it will not be disposed of...
Me.tcImageOptions.TabPages.RemoveAt(1) 'and remove the tab from the display.
End If

Here, we declared a reference variable named pThumbnailPageHold and initialized it to Nothing. Then,
within our form’s Load event code, we checked to see if we should hide our Thumbnail Tab Page. If so,
we assign a reference to the Thumbnail tab page to pThumbnailTabHold and then remove that tab page
from the Tab Control, which was the second tab (index 1).
Finally, when we are closing the form, or simply within our FormClosing event handler, we would make
sure that this Tab Page’s resources are released:
Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
'other form closing code goes above this, in case the closing is to be canceled
If pThumbPageHold IsNot Nothing Then 'was the Thumb tab page hidden?
pThumbPageHold.Dispose() 'yes, so release its resources
End If
End Sub

This was all fairly simple and straightforward code. However, if our code does things like checking
which tab page is selected by inspecting the Tab Control’s SelectedIndex property; we might find
ourselves in a little trouble index-wise, though nothing we cannot easily program ourselves out of.
 Page 488

Assume that we have 4 tabs on our Tab Control, the second tab page, at index 1, is our Thumbnail tab.
Now, suppose we remove it from the Tab Control as outlined above. What happens to the other tabs that
had been indexed at 2 and 3? What happens is the tab at index offset 2 becomes 1, and the tab at index
offset 3 becomes 2, and so on. Problems arise if we are using SelectedIndex to check for which tab is
active. If we have code that must check for specific indexes, we could run into trouble. For example:
Select Case Me.tcImageOptions.SelectedIndex
Case 0
'do setup stuff specific to the "General" tab.
Case 1
'do setup stuff specific to your "Thumbnail" tab.
Case 2
'do setup stuff specific to the "Cropping" tab.
Case 3
'do setup stuff specific to the "Watermark" tab.
End Select

Oops.
But actually, this is easy to program around. Solutions abound, but my favorite is to maintain an integer
array that stores the actual indexes that an original index pointed to, so we can look to the original index
offset and grab its current (actual) index value from there. Consider this:
Private OrgIndexes() As Int32 = {0, 1, 2, 3} 'set up original tab indexes in header of form (we will improve on this later).
...
'check for displaying the Thumb Tab Page (index 1) within the Form's Load() event handler.
If bHideThumbnailTab = True Then 'if we will need to hide the Thumbnail tab...
pThumbPageHold = tpThumbResizing 'reference the tab page to remove...
Me.tcImageOptions.TabPages.RemoveAt(1) 'and remove the tab from display without destroying it.
OrgIndexes(1) = -1 'flag Thumbnail for non-display (we will automate all of this code later).
OrgIndexes(2) = 1 'adjust Cropping TabPage reference index down.
OrgIndexes(3) = 2 'adjust Watermark TabPage reference index down.
End If

Here, we set up an Integer array (yeah, I know, I habitually use Int32) named OrgIndex and define the
original index offsets when the form loads.
Then, within the form load event, if we find that we are going to hide our Thumbnail page, we adjust the
offset indexes for index 2 and 3 down to their new offsets. Notice that Index 1 is also set to -1, for
safety’s sake.
Finally, in our testing code, we modify it as follows:
Select Case Me.tcImageOptions.SelectedIndex
Case -1
'capture hidden pages. Do nothing else here.
Case OrgIndexes(0)
'do setup stuff specific to the "General" tab.
Case OrgIndexes(1)
'do setup stuff specific to your "Thumbnail" tab.
Case OrgIndexes(2)
'do setup stuff specific to the "Cropping" tab.
Case OrgIndexes(3)
'do setup stuff specific to the "Watermark" tab.
End Select

Here, we simply ignore indexes that are adjusted to -1. Actually, this should never happen, but this is
one of those things I like to call a “safety net”, in case I suffer a brain fart and do something really goofy,
like delete all the tab pages.
Alternatively, we can simply check the tab’s Name property. For example:
Select Case Me.tcImageOptions.SelectedTab.Name
Case "General"
'do setup stuff specific to the "General" tab.
Case "Thumbnail"
'do setup stuff specific to your "Thumbnail" tab.
Case "Cropping"
'do setup stuff specific to the "Cropping" tab.
Case "Watermark"
'do setup stuff specific to the "Watermark" tab.
End Select
 Page 489

One should exercise caution here, though, because in a flash of artistic brilliance we might decide to
rename a tab or two, but not remember that we have the above code present... Oops. I strongly
recommend the index array method, or displaying a warning message in a “Case Else” statement.
We can also hide multiple tabs. We could initialize multiple reference variables to tab pages to Nothing
and then check in form closing if they are assigned to dispose of them, or we could store them in a
strongly-typed list object, such as “Private HiddenTabs As New List(Of TabPage)”.
Of course, in the removal process we should want to remove tabs by starting from the last, highest-
indexed tab in the chain and move toward the lowest-indexed tab, just so the indexes we know that it
started with will still be intact when we remove them. For example, if we were to remove both tabs
indexed at 1 and 3, I would first remove the tab at index 3 prior to removing the tab at index 1. The reason I
would do it this way is so that if I removed the tab at index 1, I would not try afterward to remove the tab at
index 3, which will fail, because it is now at index 2.
However, we can also spiff up our code so that we can actually remove the tabs in any order, backward
for forward in sequence. For example, consider the following start-up code:
Private OrgIndexes() As Int32 'set up the original tab indexes in the header of our form.
Private HiddenTabs As New List(Of TabPage) 'storage for hidden tabs.
'...Place the following lines in your Form's Load() Event:
Private Sub Form_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ReDim OrgIndexes(Me.tcImageOptions.TabPages.Count - 1) 'set aside space for the original indexes.
For Idx As Int32 = 0 To Ubound(OrgIndexes)
OrgIndexes(Idx) = Idx 'initialize the original indexes.
Next
End Sub

Here, we did not pre-initialize the OrgIndex() array, but declared it empty, along with a List object
named HiddenTabs of type TabPage to hold any hidden tab pages. This way, if we add another tab later
during development, things will not come crashing down about our ears.
Next, in the form Load event, we get the maximum index of the tabs initially defined for the tab control,
re-dimension the index array, and then fill it with the original index sequence.
Now, we can actually hide our tab pages in any order as long as we do so by invoking a method such as the
following, named HideTab, where the index we provide is the original index value (this way we can hide tab
index 1 and later hide tab index 3 without worrying about tab index 3 being at actual tab index 2):
'*********************************************************************************
' Method : HideTab
' Purpose : Hide a tab, based on its original, startup index offset.
'*********************************************************************************
Private Sub HideTab(ByVal OriginalIndex As Int32)
Dim NewIndex As Int32 'storage for our adjusted index.
Try
NewIndex = OrgIndexes(OriginalIndex) 'pick up adjusted Index.
Catch
Return 'if we suffered a brain fart and went out of original bounds.
End Try

If NewIndex = -1 Then 'if the item has already been hidden...


Return 'then there is nothing to do.
End If

HiddenTabs.Add(Me.tcImageOptions.TabPages(NewIndex)) 'save a reference to the tab page to remove.


Me.tcImageOptions.TabPages.RemoveAt(NewIndex) 'remove tab page from the tab control.
OrgIndexes(OriginalIndex) = -1 'mark the item removed.
For Idx As Int32 = OriginalIndex + 1 To UBound(OrgIndexes) 'now update the upper tabs, if any.
Dim Updt As Int32 = OrgIndexes(Idx) 'pick up an item to update.
If Updt <> -1 Then 'if the item is not already hidden...
OrgIndexes(Idx) = Updt - 1 'drop the actual index for this item down.
End If
Next 'process the next.
End Sub

Using the above method, we can remove tab 1, our Thumbnail Tab, using HideTab(1), and then Tab 3, our
Watermark tab, using HideTab(3), and in that order, and the code will not crash.
 Page 490

The only thing left to do is release our resources within our FormClosing event code:
Do While HiddenTabs.Count <> 0 'while the list contains tab page references...
HiddenTabs(0).Dispose() 'dispose of a tab page's resources
HiddenTabs.RemoveAt(0) 'then remove it from the list
Loop

And there we have it – code to easily perform a task for which developers have been wailing and
gnashing their teeth about all the way back to the days of VB1.
Restoring tabs is also simple, where we would reinsert them at the appropriate offsets on the tab control
using its TabPages collection’s InsertAt method. But this is only required if we do not use new instances
of our forms. As such, this is a very easy exercise that I will leave up to your resourcefulness.
 Page 491

Black Book Tip # 17


Passing a Parameter ByVal When the Invoking Function Specifies a ByRef Parameter.
This subject was actually covered in passing very early in this manual (see “A Note on Passing
Parameters ByVal” on page 50), though that particular point was just made in passing at its tail end and
was very likely overlooked by most readers as they tried to stay awake through my bloviating (I always
found it a fascinating paradox that when users get pithy details, they seethingly demand much more
bloviated details, but if they get much more bloviated details, they seethingly demand pithy details).
Sometimes we need to pass an object or structure to a pre-defined method that requires that the
parameter be passed By Reference, but 1) we do not want a particular scalar object to actually be altered,
and 2) we will not need the modification to the item when the invoked method returns. This is very
much like invoking a function as a subroutine and ignoring the returned value. For example, recently I
had a function that took a structure as a parameter and did things with the data supplied by the structure,
but also altered the data in the structure to format it for display. But all I wanted was for the data to be
gathered and stored for later reference within the class the method was encapsulated within, and I did
not want this structure to be altered in that particular instance.
The typical solution to this problem is to copy the structure, or whatever the object is we want to pass to
the ByRef parameter in the invoked method, and pass the copy or clone to the method instead, and
simply toss the temporary copy away afterward. For example:
Dim Tmp As MyStruct = OriginalStruct 'make copy of original structure (is abstract, so clone is auto-copied to Tmp).
FormatSetup(Tmp) 'format the Tmp structure and store data in the structure.

Here, we made a copy of the original structure and saved it to the Tmp structure (we will have to clone it
ourself if the object is an instantiated (concrete) class) or if the structure contains object references,
Refer to “Cloning Structures Containing Object References Made E-Z” on page 224. We can apply the
same principle to cloning class objects. Also refer to “Adding Run-Time Custom Menus and Cloning
ToolStripMenuItems Under VB.NET” on page 282 to see how to clone a control.
However, we can do the same thing as copying to a variable. but with less code and overhead, yet not
with instantiated (concrete) objects, by simply enclosing the parameter within parentheses, like so:
FormatSetup((OriginalStruct)) 'format a COPY of OriginalStruct – do not alter the original structure itself

What happens here is that an abstract class is actually treated just like it is part of an expression, and so a
temporary copy is instantiated for the “result” of this “expression”, which is a copy of the scalar object.
Yet, with a concrete object, we would still have to perform the following:
FormatSetup(DirectCast(OriginalObject.Clone, OriginalObjectType)) 'format COPY of OriginalObject. Do not alter original object.

NOTE: I have said this several times, but I am fighting bad information from Microsoft Tech Support – DirectCast is a
directive to the compiler to Cast an object to an indicated class. It will verify the data signature to ensure compatibility, and
then treat it as that class. The important point is this directive does not cause even one more byte of code to be generated.
But, if we do specify a wrong type, the compiler will nag us, so it is very safe to use. Microsoft Tech Support suggests using
CType, but it can convert (with additional code) a type to a type that may cause errors down the road, making those bugs
harder to track down. For example, casting an Integer to Short, and then we wonder why we keep getting error reports.

NOTE: Keep in mind that enclosing object reference variables within parentheses will in fact return a separate copy of the
reference variable, but this new copy will still point to the very same object data!
Also, if a Structure contains instantiated objects, simply copying them using the Equals operator will create a separate copy
of the structure, but any contained reference fields will be a copy of a reference to the same object as the source. So how do
we easily get around that? I will repeat this because it is very important, did we know that we can easily add a Clone
method to our structures? It will copy the structure, yet clone any object members. See page 224 for details.
FormatSetup(OriginalStruct.Clone) 'format a Cloned COPY of OriginalStruct – do not alter the original structure itself.
NewStruct = OriginalStruct.Clone 'Use this format whenever you copy a structure containing instantiated objects.
 Page 492

Black Book Tip # 18


Show and Hide Additional Information at the Bottom of the Form.
We have all seen forms that feature a “More Details” button at its bottom, and
the form expands to reveal previously hidden details when we click it?
Although this can be as simple as resizing a form, it can give us headaches if we
do not allow for it being full-screen, or worse, just near to it. And if we want to animate it to expand and
contract, or add interactive button effects, it can turn from a simple idea into a mind-numbing nightmare.
As a veteran of numerous such forms, I have honed this sticky process down to a few simple steps.
First, fill an empty form with a Panel control, except for the bottom part we want to be hidden. Second, fill
the rest of the form, the part to be normally hidden, with another Panel, butted right up against the first.
Nexr, place all main body controls on the first Panel, which I always name pnlMainBody. That is the
selected panel shown below, and place the controls we want displayed in the “expanded” view in the second
Panel below it, always named pnlMoreDetails. This approach makes management E-Z. Notice that OK and
Cancel Buttons, and a Show More Details Label are all placed on the pnlMainBody surface.
Next, set aside a 24x24-pixel flat PictureBox named picShowMore, to act as an interactive button, used to
show when to display more or less details (I will later provide six 24x24 in-code icons to fill it). Accompany
it with the label, named lblMoreDetails, which the user can also click. Place them on the left side of the
form, across from the OK and Cancel buttons, named btnOK and btnCancel.
NOTE: Using a flat PictureBox for a Show More ‘button’ requires 6 images, which will be embedded as string data in our demo
program (so we can copy them to our own programs), used to animate the image when the cursor interacts with it.

So far, such a form might look something like this:


Anchoring can be the tricky part, because we want to be able to
expand the form and the controls to adjust to it, but we will
want to expand and contract the form without moving all the
other controls, and hence, the reason for panels.
To start, I will have pnlMainBody anchored to all four sides. I
also anchor the OK and Cancel Buttons to the bottom-right
corner of pnlMainBody, the more details PictureBox and
Label to its bottom-left corner, and the additional details panel to the form’s bottom, left, and right.
When expanding and contracting the form, we temporarily have to alter the anchoring of PnlMainBody
and pnlMoreDetails to only Top, Left, and Right sides. Once the resizing has completed, we will reset
pnlMainBody back to all four sides and pnlMoreDetails back to the Bottom, Left, and Right sides.
In order to animate the form’s expansion and contraction, we will also require a timer control, which I
always name tmrMoreDetails, and set it to an interval of a paltry 25 (milliseconds).
The last thing we need to do, for setup that is, is to set aside some private variables (fields. actually) that
will determine the pixel increment during each timer tick, the target form size, whether we are resizing
the form or resizing the panels, in case the form is full or near-full screen. Plus, for brevity’s sake, I will
add some common constants that I will be using. I declare them in the header of my form, like so:
Const IncDecAmount As Int32 = 16 'increment resizing the form by 16-pixels each tmr tick.
Const AnchorLR As AnchorStyles = AnchorStyles.Left Or AnchorStyles.Right 'Anchor left, right (used by following constants).
Const AnchorLRT As AnchorStyles = AnchorLR Or AnchorStyles.Top 'Anchor Left, Right, and Top (used during resizing).
Const AnchorLRB As AnchorStyles = AnchorLR Or AnchorStyles.Bottom 'Anchor Left, Right, and Bottom (def for pnlMoreDetails).
Const AnchorLRTB As AnchorStyles = AnchorLRT Or AnchorStyles.Bottom 'Anchor in all directions (default for pnlMainBody).

Private pIncrement As Int32 'when resizing the form, this holds the IncDecAmount value as + or -.
Private pNewTarget As Int32 'the target height when the resizing is finished.
Private pSizeByForm As Boolean = False 'True if we resize the form, not move panels.
Private pCanShowMore As Boolean = False 'True when we can show more details. False for Less.
 Page 493

Within my Form’s Load event, I initially conceal pnlMoreDetails like this:


Me.pnlMainBody.Anchor = AnchorLRT 'anchor main panel to left, right, and top sides.
Me.pnlMoreDetails.Anchor = AnchorLRT 'anchor bottom panel to left, right, and top sides.
Me.Height -= Me.pnlMoreDetails.Height 'Initially size form to hide lower panel.
Me.pnlMainBody.Anchor = AnchorLRTB 'Now anchor main panel to all 4 sides.
Me.pCanShowMore = True 'indicate we can show more details.
InitializeImageList(Me.imgList) 'initialize image list.

As we can see, we expand the form to hide pnlMoreDetails.


We need to write a method to handle expansion and contraction, which I call FlipMoreLess, and write the
Timer’s Tick event method. In all, the code for the form, which just supports the expansion and contraction
of the form, to include moving the details panel in and out if the form already fills the screen, is as follows:
Public Class frmDemoMoreLess
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Const incdecAmount As Int32 = 16 'incr resizing form by 16-pixels each timer tick.
Const anchorLR As AnchorStyles = AnchorStyles.Left Or AnchorStyles.Right 'Anchor left and right (used by following).
Const anchorLRT As AnchorStyles = anchorLR Or AnchorStyles.Top 'Anchor Left, Right, and Top (used during resize).
Const anchorLRB As AnchorStyles = anchorLR Or AnchorStyles.Bottom 'Anchor Left, Right, and Bottom.
Const anchorLRTB As AnchorStyles = anchorLRT Or AnchorStyles.Bottom 'Anchoring in all directions.

Private pIncrement As Int32 'when resizing form, this holds the IncDecAmount value as + or -.
Private pNewTarget As Int32 'the target height when the resizing is finished.
Private pSizeByForm As Boolean = False 'True if we resize the form, not move items.
Private pCanShowMore As Boolean = False 'True when we can show more details. False for Less.
Private imgList As New ImageList 'image list to hold More-Less images (no need to drop it on form).

Private Enum Images As Int32 'image index for button states in ImageList.
LessUP
LessUPover
LessDOWN
MoreUP
MoreUPover
MoreDOWN
End Enum
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
' Method Name : Form_Load
' Purpose : Initialize the form.
'*********************************************************************************
Private Sub Form_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.pnlMainBody.Anchor = AnchorLRT 'anchor main panel to left, right, and top sides.
Me.pnlMoreDetails.Anchor = AnchorLRT 'anchor bottom panel to left, right, and top sides.
Me.Height -= Me.pnlMoreDetails.Height 'Initially size form to hide lower panel.
Me.pnlMainBody.Anchor = AnchorLRTB 'Now anchor main panel to all 4 sides.
Me.pCanShowMore = True 'indicate we can show more details.
InitializeImageList(Me.imgList) 'initialize image list.
Me.picMoreDetails.Image = Me.imgList.Images(Images.MoreUP) 'initially start with being able to show more details.
End Sub

'*********************************************************************************
' Method Name : MoreDetails_Click (Button and Label)
' Purpose : Flip details in or out (This is the Show More Details button).
'*********************************************************************************
Private Sub MoreDetails_Click(sender As Object, e As EventArgs) Handles picMoreDetails.Click, lblMoreDetails.Click
Me.FlipMoreLess()
End Sub

'*********************************************************************************
' Method Name : picMoreLess_MouseEnter
' Purpose : Set image highlight for mouse-over
'*********************************************************************************
Private Sub picMoreLess_MouseEnter(sender As Object, e As EventArgs) Handles picMoreDetails.MouseEnter
If pCanShowMore Then
Me.picMoreDetails.Image = Me.imgList.Images(Images.MoreUPover)
Else
Me.picMoreDetails.Image = Me.imgList.Images(Images.LessUPover)
End If
End Sub

'*********************************************************************************
' Method Name : picMoreLess_MouseLeave
' Purpose : Remove image highlight for no longer mouse-over.
'*********************************************************************************
Private Sub picMoreLess_MouseLeave(sender As Object, e As EventArgs) Handles picMoreDetails.MouseLeave
If pCanShowMore Then
Me.picMoreDetails.Image = Me.imgList.Images(Images.MoreUP)
Else
Me.picMoreDetails.Image = Me.imgList.Images(Images.LessUP)
 Page 494

End If
End Sub

'*********************************************************************************
' Method Name : picMoreLess_MouseDown
' Purpose : Mouse popped down over fake button.
'*********************************************************************************
Private Sub picMoreLess_MouseDown(sender As Object, e As MouseEventArgs) Handles picMoreDetails.MouseDown
If pCanShowMore Then
Me.picMoreDetails.Image = Me.imgList.Images(Images.MoreDOWN)
Else
Me.picMoreDetails.Image = Me.imgList.Images(Images.LessDOWN)
End If
End Sub

'*********************************************************************************
' Method Name : picMoreLess_MouseUp
' Purpose : Mouse popped up over fake button.
'*********************************************************************************
Private Sub picMoreLess_MouseUp(sender As Object, e As MouseEventArgs) Handles picMoreDetails.MouseUp
If pCanShowMore Then
Me.picMoreDetails.Image = Me.imgList.Images(Images.MoreUP)
Else
Me.picMoreDetails.Image = Me.imgList.Images(Images.LessUP)
End If
End Sub

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' Method Name : FlipMoreLess
' Purpose : Flip form between showing more and showing less.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Sub FlipMoreLess()
Me.pnlMainBody.Anchor = anchorLRT 'remove Anchor Style Bottom from panel.
Me.pnlMoreDetails.Anchor = anchorLRT 'remove Anchor Style Bottom from panel and set Top.
If Me.pCanShowMore Then 'if it presently hides search options...
Me.pCanShowMore = False 'then set for when already showing addl details.
Me.picMoreDetails.Image = Me.imgList.Images(Images.LessUP) 'image to indicate can select show less details.
Me.lblMoreDetails.Text = "&Show Less Details" 'update option label.
If Me.WindowState = FormWindowState.Normal Then 'resize the form if the window state is normal.
Me.pSizeByForm = True 'we will be resizing the form itself.
Me.pIncrement = incdecAmount 'we will increase the form size.
Me.pNewTarget = Me.Height + Me.pnlMoreDetails.Height 'compute the final form size when finished.
Else 'otherwise, we will move controls instead of form.
pSizeByForm = False 'will not resize maxed form; will move controls.
pIncrement = -incdecAmount 'we will decrease additional panel visibility.
pNewTarget = Me.pnlMainBody.Height - Me.pnlMoreDetails.Height 'compute final form size when finished.
End If
Else
pCanShowMore = True 'otherwise, set up for decreasing the form size.
Me.picMoreDetails.Image = Me.imgList.Images(Images.MoreUP) 'image to indicate we can show more details.
Me.lblMoreDetails.Text = "&Show More Details" 'update option label.
If Me.WindowState = FormWindowState.Normal Then 'resize the form if the window state is normal.
Me.pSizeByForm = True 'we will be resizing the form itself.
Me.pIncrement = -incdecAmount 'we will decrease the form size.
Me.pNewTarget = Me.Height - Me.pnlMoreDetails.Height 'compute the final form size when finished.
Else 'will not resize maxed form; will move controls.
Me.pSizeByForm = False 'indicate sizing by controls.
Me.pIncrement = incdecAmount 'we will increase additional panel visibility.
Me.pNewTarget = Me.pnlMainBody.Height + Me.pnlMoreDetails.Height 'compute final target size when finished.
End If
End If
Me.tmrMoreDetails.Enabled = True 'enable resizing timer and let it move all.
End Sub

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' Method Name : tmrMoreDetails_Tick
' Purpose : Handle slide-resizing the form
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Sub tmrMoreDetails_Tick(sender As Object, e As EventArgs) Handles tmrMoreDetails.Tick
If pSizeByForm Then 'if we are resizing the form...
Dim diff As Int32 = Math.Abs(Me.Height - Me.pNewTarget) 'compute how much is left to adjust.
If diff <= incdecAmount Then 'within 1-shot limits?.
Me.tmrMoreDetails.Enabled = False 'yes, so disable the timer...
Me.Height = Me.pNewTarget 'set to target height...
Me.pnlMainBody.Anchor = anchorLRTB 'and reset anchor values.
Me.pnlMoreDetails.Anchor = anchorLRB
ElseIf Me.pIncrement < 0 AndAlso
Me.Height = Me.MinimumSize.Height Then 'trying to reduce form to less than min size?
Me.pSizeByForm = False 'yes, so switch to moving controls.
Me.pIncrement = incdecAmount
Me.pNewTarget = Me.pnlMainBody.Height + diff 'only move by what is left.
ElseIf Me.pIncrement > 0 AndAlso
(Me.Top + Me.Height + Me.pIncrement) >= Screen.PrimaryScreen.WorkingArea.Height Then
Me.pSizeByForm = False 'form is max size, so switch to moving controls.
Me.pIncrement = -incdecAmount
 Page 495

Me.pNewTarget = Me.pnlMainBody.Height - diff 'only move by what is left.


Else
Me.Height += Me.pIncrement 'adjust form height.
End If
Else
Dim diff As Int32 = Math.Abs(Me.pnlMainBody.Height - Me.pNewTarget) 'compute how much is left to adjust.
If diff <= incdecAmount Then 'within limits?
Me.tmrMoreDetails.Enabled = False 'yes, so disable the timer...
Me.pnlMainBody.Height = Me.pNewTarget 'set to target height...
Me.pnlMainBody.Anchor = anchorLRTB 'and reset anchor values.
Me.pnlMoreDetails.Anchor = anchorLRB
'---------------------------------------------------------------
Else
Me.pnlMainBody.Height += Me.pIncrement 'adjust main panel size.
Me.pnlMoreDetails.Top += Me.pIncrement 'adjust additional details panel top.
End If
End If
End Sub

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' Method : InitializeImageList
' Purpose : Imitialize a provided ImageList and fill it with locally-created images.
' :
' NOTE : If you want to append the images to an existing list, set the Replace
' : parameter to FALSE.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Sub InitializeImageList(ByRef imgList As ImageList, Optional ByVal Replace As Boolean = True)
If Replace Then 'if we are filling, not appending images.
imgList.Images.Clear() 'initialize image list.
imgList.ImageSize = New Size(24, 24) 'define 24x24 pixel images in this list.
End If
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.
'--------
'Image 0 LessUP
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEdSURBVDhPrZEtDoQwEIU5BQrFFbgBd0DjuAIWxRFIcCv2" &
"BJwADx6FwuFwuNn9GqZpoev2JS9pp/Pe/DT6O+SLfd9l2zZZ19WQMzFwpYVxHIfM8yzneQaJGUZXug/E" &
"VNJkjLquk7ZtZRxHG8fgYUJrbuVpmqSqKo+v19u+L8vij4MjHfBItbIspSgK6fv+cQ+aaOskZ1lm2DSN" &
"TXbjjEWMfVgD3AgOwyBpmpq5OSOI49je8zw345BLx9AzUJKcJIkRQ84qVHoGtOOKqYyoruvHXfO8EVgi" &
"QWYl8V5RO4K6G37KGnDgG/kqN8mldsKPUN22r6CL+y5CRIjBJfOBCZ3oSHchBbzZQ9BxmBEB5M5+Hm3/" &
"ginxBQKouJ4dRNEHQYBcEf8gEh8AAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(0).
'--------
'Image 1 LessUPover
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEpSURBVDhPrZK9jYQwEIWpgoiIFuiAHojJaIGUiBKQLttg" &
"K6ACcsiJiMjQJVTg8zfr8dm7vuye9CT/zHsznnH27zAW13WZ4zjMvu9C1pwBF5bGfd8i2Gxgitxh5MJj" &
"ICaTF2ybmabJjONolmX5Pf9+VehkL9hzEWjQuq6m67qIj8fT31MJcPIsw1Evyda2rWmaRir4ssbhPmly" &
"nqcXV1UlHIbBB8/z7M/VhOcCMdDGEViWpbxbRXmeixn7uq7lOWpM3yIDJcFFUYgYsg6FSm8Qdl8zI+r7" &
"/mPvDew0gBhoE2kYge8ZtSKovWFSQAxYMEZGFQaF1EqYCBX78hVU8d6LFInDwMliyFP4UPZ9KTEJovGl" &
"wKWYqBG0a/7IR9l/QVJYIIAKdx0gy34A0+9VZLWdBAkAAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(1).
'--------
'Image 2 LessDOWN
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE3SURBVDhPrZMhsoNAEEQ5BYoLoFFYPBKN4wpYFEegChfx" &
"T8AJ8OBRKBwOh5ufN5/ZQEJF/anqYtnp7u3ZEO/fS561LIv0fS+Px4+C9TzPsm2bHLT7goCgrmupqkrK" &
"snTgvWkaNT/o13oXZ1kmvu9DliRJLiak+VMdRWzENDHI81zFZ8RxrD3Qti2SlwmxzKAoCgmCQEVpml7e" &
"SWIG3IszGcdRDYhpJ0KGCM77jAa367qXAW5sGJERmDcMw493jOGicb+KGRiIGUWRA/NjcuZcDBiBDcA9" &
"EJPZLSrJ2ONpPOBG4BKnaVIBEYkLgRMAPVKZiR3oDFgMw6AGkEy877vCTBCTDAMX32pdVyXxtJPNxNb0" &
"wMeHZEXT3O+AELjod0WTJPbnAdwRI7I+aN9Lj3iWGVgd7VN53i+4CRoX/tNJ6wAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(2).
'--------
'Image 3 MoreUP
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAE1SURBVDhPrZIhsoQwEEQ5BQqF4whYPJIrcAUsiiNQhVux" &
"J+AEePAoFA6Hw+XvGzLZsMv/6nfVVEKmu9OTIvh3mBe2bTPrukotyyIrZ8DS7gFhmiZzHMdtYbbv+70J" &
"Ygh3Qr9IQlnZCb2ZqG3b/mrU973w5nlG8jbBkWjjOJq6rk3TNJdR1Jhe13VydjGBoGQIZVlKYcgZQr5Z" &
"lXcx+IyMSZ7npigKEbJn9Xkkdg+KG4fETtPUjZIkiYnjWIzoY/J4PL8N1JkmAkx4ML6ZfRgGSeGbMbYb" &
"gUfkUG+JokgKE86yLJNvVuWR0hmwIT6RaFZVJQJGII2KdVQSu/gKUiiBIkkYhpAuNyPE4FR5IAUmJNGR" &
"eAP+CYwRsiIGVvYNmphQCCj2PCTGlvY35IoXuFUL2LaHIPgBSdhjS+Vm++gAAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(3).
 Page 496

'--------
'Image 4 MoreUPover
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFASURBVDhPrZIhkoQwEEU5BQqF4whYPJIrcAUsiiNQhRux" &
"J+AEePCYReGoNZwg269JsrCTdfurehLS///uziT6dxjBcRwa27aZdV115RtYWhgQlmUxC2sgMDvPM2wi" &
"ea0UEj7i6+rQyi7I+VVZkn3fa6WQeBxH5ZEHVh5FOEKYpsm0bWu6rjPT5/QjtsbkhmHQs4fJvu+eDKGu" &
"aw0MOUPIN6vjPQx+t4xJWZamqioVsmd98KQrf6E+IfPlee5HybLMpGmqRpAxeb0+vIk3cLdPEgEmXBjf" &
"tXTDni6cGVw0QA3cJRJUSZJEw1UrikK/WR1vnmf5tQZs7g+oaRoVMALdeLHlMPLbg6KL+yXRSRzHkB6V" &
"Cdq/VDfIub4w96AgMgJvwp1R4DF7CCRVIIFAu5I9/8zbE/4LWkLAnC6ATd8QRd80qluIiZXfSAAAAABJ" &
"RU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(4).

'--------
'Image 5 MoreDOWN
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFFSURBVDhPrZMtkoNAEIU5BYoLoFFYPBKN4wpYFEegCheR" &
"E3ACPHgUCofD4XrzdaZnNz+VmO2qrh6m33vzpjMJ/j3kFtu2yTRNMgyDXC5XGcdR1nWlJQ72Po7jUELb" &
"ttI0jdR17ZPvrutk3/f3Ip/IzyI4dLR7YA0yoDAMpaoqBT9nHMdSFIX0ff94HRQRgIhAFEW6xo05gkwv" &
"SRIVYC5eZJ5nFaCRZZkCSRyxZ2Qq32AZsBdAzSZOYhMXEDiRNZX7GxkOc3sQAACYWpalkhDJ81wJuMPV" &
"iwBXYIP7QsIBIHNFj5mkaaoifPNOzvO8CzDEZVkEIfu5ILPPKSZOD4wd6GcACEVeGyQ7gTUJAUEqGKq3" &
"b8ELwwXVBBGyZI8eiYij/QZ2aJo6aVZNFOLX/wRNiHZ/OxlHVAf7HHrELUyAJFz7TwTBD4wjJCWt2pa0" &
"AAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(5).
End Sub

'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image
End Function

End Class
 Page 497

NOTES ON IMAGES: Here are samples of the images I normally use within a
picture box control, acting as my button, which will not display the bounding box we
see in the samples. As shown in the demo, we monitor the MouseEnter, MouseLeave,
Mouse Down, and MouseUp events for the picture box, and adjust the “button”
images accordingly. LessUP and MoreUP are their normal states, and also set when
the MouseLeave event fires. LessUPover and MoreUPover are set when the the
MouseEnter or MouseUp events fire (MouseUp also initiates the “button click”
process). LessDown and MoreDOWN are set when the MouseDown event fires. Also,
the edges of these images are set to transparent, and these images are dimensioned to
24 x 24 pixels. I quickly created them using the incredibly powerful yet rather
inexpensive Axialis IconWorkshop (www.axialis.com).
Notice that the images are embedded within the source code itself. It does this magic by storing the images as Base64 strings and
embedding them right in the source code. We can also take advantage of this slick feature in our own applications by including the
BuildImageListCode method found in Black Book Tip # 49, Embedding Images Within Your Source Code , on page 628, which
will build a full InitializeImageList method customized to our own images. Just supply it with an ImageList control pre-filled with
our selected images. When using the resulting InitializeImageList method in our target applications, supply the method with the
ImageList control to fill. Also, set the optional Replace parameter to False if we want to append, not replace any images in our
ImageList:
 Page 498

Black Book Tip # 19


Easily Recovering Crashed Menus and Toolbars Under VB.NET.
Have you ever worked on a VB.NET project and
“suddenly” a form’s Designer loads and our menu entries
and/or toolbar buttons are dead in the water (well... this is
usually after selecting the Ignore and Continue option
when a form will not display, even though a data not
found warning was reported, telling us the full path to the
data it cannot find, but we tend to forget about lamenting
these annoying facts to our friends)? Most who work on
only simple, straightforward projects may be lucky enough to have never experienced it, but most of us
who have found ourselves needing to reorganize a project as it grows have had it happen at least once. It
typically occurs as we are experimenting with container folder assignments and fine-tuning the naming
of resource objects that were already assigned to our application resources. I first noticed this issue
being reported in VB2005, and it has happened to me personally under VB2008 and VB2010, and I
expect it to face it again in later editions. When reported on blogs, most moderators typically castigate
the user for not backing up their work, or it being their own tough break, so deal with it. A few of them
offer sympathy, but no one had offered any sort of viable after-crash solution.
Granted, all seems, or is assumed to be lost. All those countless hours of work appear to have just gone
gone up in smoke. But even after we finally sigh, suck it up, and accept our fate, as we try to rebuild out
menus from the ash remains of our project, it typically refuses to accept new objects with property
names that we had previously assigned them, such as mnuFile, mnuEdit, and so on. Suddenly, we all
start talking like pirates on September 19th (International Talk-Like-a-Pirate Day); Aaarrrrr!!!

DON’T PANIC
As the late great Douglas Adams reminded us with the big bold-lettered “DON’T PANIC”
message on his book, The Hitchhikers Guide to the Galaxy, there is absolutely no reason
to, well... panic. That is because all of our menu objects are still intact. What is missing,
however, are the instructions that added those menu and toolbar items to our main menu and/or toolbar.
Although it seems like a major inconvenience, it is actually quite easy to restore them in cases where we
had selected Ignore and Continue, but it is even easier to simply back out of the error as the IDE—the
Integrated Development Environment—had tried to suggest to us in their warning, shown above.

WHY IT HAPPENS
The big mystery, or so it has been said, had always been why it happens in the first place. That part is
easy because it is bleeding obvious: Because WE shifted resources around or renamed them after we
added references to these file items in our project resource file. This will consequentially confuse .NET,
which will still expect these resources to have the same names and/or exist at their previous locations.
The application resource file, Resources.ResX, is not a binary file, though most developers assume that
it is (it was under VB6); it is actually an XML file. Right-click a ResX file in Project Explorer and
choose Select With..., and then select Source Code (Text) Editor to view its content. It contains text
reference paths to resource files. The IDE recompiles this data to an internal binary any time it discovers
resources have changed. This makes it more like an auto-compiled VB6 Resource Source File (.rc).
Instead of sending programmers into a black spiraling abyss of despair, my thinking is that the IDE
should have either replaced the unfound image resource with a default and warned us about that, or offer
to provide a browser so we can find its new location and/or name (hint!), and instead of just commenting
out the menu constructor instructions that caused the error to fire (hint!), they actually prevent the code
from working at all by eliminating the chance of a crash by deleting them, and without saying so! It is
nice that the IDE will toss a rather benign warning about it not finding a resource, but after we, pressed
 Page 499

by looming deadlines, stupidly (err... mistakenly) hit the Ignore and Continue option, it appears to
calmly dump a load of refuse emitted from the south side of a north-bound bull all over our project.
Microsoft did this because some software designer there thought it the best way to ensure all form data
was not lost, though I argue there are much less confusing and recoverable ways to disable the problem
(I will refrain from saying out loud that whoever designed that solution suffers Cranial Rectitus!).
Anyone who works on a complex project, especially one that will be commercially available, tends to
change their icons and other resources frequently as they endeavor to provide a uniform look and feel to
the presentation of their masterpiece. This is why we can edit a source object that is also referenced by
the project resources, such as an image, or replace it with another of the same name from File Explorer,
but not need to edit our project resources in order to update them. We usually see a form go blank and
then refresh as it updates to the altered resource data (the IDE keeps an eye on our resource files, and it
will update the affected forms when it detects a resource source has an altered timestamp).

DUPLICATING THIS ISSUE


The easiest way to duplicate this issue, to get some experience in dealing with it
without panic, is to create a form and place a simple menu on it, such as a File and
an Edit entry with some sub-menu items, such as Exit for the File main menu entry,
and perhaps Cut, Copy, and Paste for the Edit menu. Though not necessary, I tend
to name such entries mnuFile, mnuFileExit, mnuEdit, mnuEditCut, mnuEditCopy,
and mnuEditPaste (I do not like their default naming, such as
FileToolStripMenuItem for File, but I must admit that it is accurate and helpful
for most simple projects).
Look at the Designer form (enable access to it in Solution Explorer by clicking
the Show All Files icon) and select the form name with the Designer.vb
extension, such as Form1.Designer.vb. Please do not bother wasting any of our
precious time with an attack of nerves about peering into the dark catacombs of
this file. Previous to VB2005, prior to the introduction of the Partial Class
modifier that enabled us to have multiple classes declared under the same name,
thus allowing multiple files to support the same class, all this data was actually
stored in the main code body of the form’s class structure. Its only ‘protection’
back in those days was simply wrapping this designer-modified code within a
simple, non-protected Region declaration that could be collapsed. Whoopie-do.
If we examine this simple code (well, simple once we understand that what it actually does is simple),
we see within the InitializeComponent method, where each of our objects are first declared, such as
MenuStrip1, which is our main menu object, and all of its items, like mnuFile, mnuEdit, and so on:
Me.MenuStrip1 = New System.Windows.Forms.MenuStrip()
Me.mnuFile = New System.Windows.Forms.ToolStripMenuItem()
Me.mnuFileExit = New System.Windows.Forms.ToolStripMenuItem()
Me.mnuEdit = New System.Windows.Forms.ToolStripMenuItem()
Me.mnuEditCut = New System.Windows.Forms.ToolStripMenuItem()
Me.mnuEditCopy = New System.Windows.Forms.ToolStripMenuItem()
Me.mnuEditPaste = New System.Windows.Forms.ToolStripMenuItem()

Below that, we see where each of these objects are customized and bound together:
'
'MenuStrip1
'
Me.MenuStrip1.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.mnuFile, Me.mnuEdit})
Me.MenuStrip1.Location = New System.Drawing.Point(0, 0)
Me.MenuStrip1.Name = "MenuStrip1"
Me.MenuStrip1.Size = New System.Drawing.Size(284, 24)
Me.MenuStrip1.TabIndex = 0
Me.MenuStrip1.Text = "MenuStrip1"
'
'mnuFile
'
Me.mnuFile.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.mnuFileExit})
Me.mnuFile.Name = "mnuFile"
 Page 500

Me.mnuFile.Size = New System.Drawing.Size(37, 20)


Me.mnuFile.Text = "File"
'
'mnuFileExit
'
Me.ExitToolStripMenuItem.Image = Global.WindowsApplication1.My.Resources.Resources.iexplore_14_5
Me.mnuFileExit.Name = "mnuFileExit"
Me.mnuFileExit.Size = New System.Drawing.Size(152, 22)
Me.mnuFileExit.Text = "Exit"
'
'mnuEdit
'
Me.mnuEdit.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.mnuEditCut, Me.mnuEditCopy, Me.mnuEditPaste})
Me.mnuEdit.Name = "mnuEdit"
Me.mnuEdit.Size = New System.Drawing.Size(39, 20)
Me.mnuEdit.Text = "Edit"
'
'mnuEditCut
'
Me.mnuEditCut.Name = "mnuEditCut"
Me.mnuEditCut.Size = New System.Drawing.Size(152, 22)
Me.mnuEditCut.Text = "Cut"
'
'mnuEditCopy
'
Me.mnuEditCopy.Name = "mnuEditCopy"
Me.mnuEditCopy.Size = New System.Drawing.Size(152, 22)
Me.mnuEditCopy.Text = "Copy"
'
'mnuEditPaste
'
Me.mnuEditPaste.Name = "mnuEditPaste"
Me.mnuEditPaste.Size = New System.Drawing.Size(152, 22)
Me.mnuEditPaste.Text = "Paste"

Notice the three highlighted lines, which bind submenu items to their parent menu. The first binds the
mnuFile and mnuEdit objects to the MenuStrip1 main menu strip. These will become important later
because it is these that tend to get killed off by the IDE.
NOTE: Many have asked how this weird AddRange parameter formatting works, such as New ToolStripItem() {Me.mnuFile,
Me.mnuEdit}, but this is simply creating a new array, like many of us do every day, such as Dim Ary() As String = {"Zero",
"One", "Two"}, but not specifying a target variable, which is not necessary in such cases.

Next, add at least one image or icon to our resources (not directly to the menu from local resources), and
then add those images as images to one or more of our menu entries (be sure to specify Project
Resource File when we are assigning an image to a menu from the Add Image dialog). Now close the
form design view and we will try to make this thing crash. Go into the Solution Explorer and then
rename one of the image objects we had loaded into our project resources (although we can simply
delete it for this example, this is more difficult to recover from). Finally, try to re-open our form. It
throws up a warning like the one shown on the first page of this article, but if we choose Ignore and
Continue... BOOM! Our menu that contained the altered data is missing!
Once such damage is done, any theory and speculation often mean nothing.
Some time ago I had a medium-sized project with almost 100 menu/submenu entries. I had decided the
project was large enough to be better organized. I already had abundant folders, such as Forms to hold
my form files, several class group folders further stored under a general Classes folder, a Modules
folder, and a Resources folder, under which I had subfolders for Cursors, Icons, and Images. The Icons
subfolder was getting a bit stuffed (I wish we could sub-group our project resources), so before starting
work on the code one day, I used File Explorer to build several subfolders for the images to
accommodate each toolbar and menu dropdown, and moved them to the appropriate folders.
Oops! When I re-entered VB2010 and loaded my project, it reported it could not find the location for the
main form’s icon. I closed the warning, fixed it in resources by deleting the missing icon place-holder
and reloading it from its new location, saved the changes, and ran my code. More resource errors were
reported, though my menus and toolbars seemed to come up OK, so I exited the IDE and tried to copy
the offending icons back to their original locations, hoping to fix the problem. However, the damage had
already been done (the application’s text-based Resources.Resx file was already pointing to the wrong
source locations for the moved images). When I went back in my app and brought up the main form, the
menu bar and the toolbars were completely blank. The Titanic only exceeded this in terms of disaster on
account of its greater death toll (not by the fact that its very trouble-prone and damaged sister ship, the
 Page 501

Olympic, had clearly replaced the Titanic for insurance reasons by having their engraved 4-foot bow
names covered at the last minute by large riveted plates with new lettering – notice that two of the plates
that fell off, where the removed sequential letters “AN” (TITANIC) actually revealed the large engraved
letters “MP” (OLYMPIC) underneath. Also, the Olympic had acquired starboard propeller # 401 from
the Titanic when it damaged its own and its replacement was not ready, but Titanic’s was, so it was used
instead. This is the same propeller photographed in 1986).
Having had this happen to me previously, I knew I could fix this in a matter of minutes, though I spent
an afternoon researching how this happened in the first place. It simply all came down to misplaced
resource sources. This is because the Resource.ResX file was assigned source locations for images when
we placed them in the resources, but this does not update if we physically move the image file to another
folder location using File Explorer. For example, consider the following sample XML entry for an image
I loaded into my resources named “iexplorer_14_5”:
<data name="iexplore_14_5" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\iexplore_14_5.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a</value>
</data>

As we can see, the XML Value entry specified the resource data name (“ data name="iexplore_14_5"”),
relative file location from the application folder (“..\Resources\iexplorer_15_4.png”), the type of item
(“System.Drawing.Bitmap”), the Namespace that supports it (“ System.Drawing”), and so on. This value does
not change when we use Explorer or Solution Explorer and move or rename the file, such as renaming it
to “iexplorer.png”, thus inviting a crash when the resources get rebuilt.

QUICK, EASY, AND PAINLESS SOLUTIONS


Problems arise when the editor tried to recompile the resource data (which it does more often than we
might suspect), and it loses track of a referenced resource, and hence, the warning of potential data
corruption. Please avoid exiting the .NET IDE! Even if we choose not to save the changes we may have
made to our files, the actual resource file is automatically updated when we exit the IDE, and so exiting
can mean much more work for we to do later.
By staying within the IDE with the application loaded, the problem is as easy to fix as using File
Explorer to correct the file path or file name to what was expected, or simply use File Explorer to restore
the missing (deleted) object, even if it is with temporary same-named objects of identical type.
The easiest solution, however, and also my personal favorite because it keeps files named and located as
I want them named and located, is to simply edit the offending line in the Resources.ResX file. For
example, consider the previously-shown XML snippet. We can find this by selecting the Resources.ResX
file (located in the My Project Solution Explorer Folder), and edit it by hitting the View Code button on
the Solution Explorer tool bar, or simply doing a FIND search for the offending image path (such as
“\iexplorer_14_5.png”). Edit this text entry as needed. So, if we had renamed the file, simply replace
the old filename shown there with its new name, and/or if the folder path is not where the file is now
located, just edit the folder path to reflect its new path. This fixes the issue.
 Page 502

REVIEWING THE DAMAGE IN CASE WE IGNORED THE WARNING


If we made the mistake of choosing Ignore and Continue, even after a problem was reported, then we
have more work ahead of us, though not any that is very difficult. Were we to look at our designer code
for the damaged form, we might see something like the following, which is from the same code we had
looked at previously:
'
'MenuStrip1
'
Me.MenuStrip1.Location = New System.Drawing.Point(0, 0)  Notice Me.MenuStrip1.AddRange() no longer precedes this line
Me.MenuStrip1.Name = "MenuStrip1"
Me.MenuStrip1.Size = New System.Drawing.Size(284, 24)
Me.MenuStrip1.TabIndex = 0
Me.MenuStrip1.Text = "MenuStrip1"
'
'mnuFile
'
Me.mnuFile.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.mnuFileExit})
Me.mnuFile.Name = "mnuFile"
Me.mnuFile.Size = New System.Drawing.Size(37, 20)
Me.mnuFile.Text = "File"
'
'mnuFileExit
'
Me.mnuFileExit.Image = Global.WindowsApplication1.My.Resources.Resources.iexplore_14_5  Source of error in first place
Me.mnuFileExit.Name = "mnuFileExit"
Me.mnuFileExit.Size = New System.Drawing.Size(152, 22)
Me.mnuFileExit.Text = "Exit"
'
'mnuEdit
'
Me.mnuEdit.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.mnuEditCut, Me.mnuEditCopy, Me.mnuEditPaste})
Me.mnuEdit.Name = "mnuEdit"
Me.mnuEdit.Size = New System.Drawing.Size(39, 20)
Me.mnuEdit.Text = "Edit"
'
'mnuEditCut
'
Me.mnuEditCut.Name = "mnuEditCut"
Me.mnuEditCut.Size = New System.Drawing.Size(152, 22)
Me.mnuEditCut.Text = "Cut"
'
'mnuEditCopy
'
Me.mnuEditCopy.Name = "mnuEditCopy"
Me.mnuEditCopy.Size = New System.Drawing.Size(152, 22)
Me.mnuEditCopy.Text = "Copy"
'
'mnuEditPaste
'
Me.mnuEditPaste.Name = "mnuEditPaste"
Me.mnuEditPaste.Size = New System.Drawing.Size(152, 22)
Me.mnuEditPaste.Text = "Paste"

Notice, however, that in comparing this code to the previous, we see that there is sometimes only a
single line that is missing, which was the first line under the MenuStrip1 comment header:
Me.MenuStrip1.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.mnuFile, Me.mnuEdit})

More often, however, in typically more disastrous situations, the above two highlighted entries under
mnuFile and mnuEdit may also be missing if lost resources were also assigned to any of their child menu
items. It all depends on where the moved/renamed/deleted resources were assigned.
In any case, after correcting our missing resource errors, were we to then simply reinsert the missing
“AddRange” lines (this applies equally to toolbars), even the “trashed” app will suddenly return to full
working condition. However, we might not have the missing source lines, especially if we did not back
our source code up as often as we should (...guilty!). But regardless of even that, they are actually quite
easy to reconstruct, as demonstrated below.
 Page 503

EASILY REPAIRING THE “DOOMSDAY SCENARIO” DAMAGE


NOTICE: For this solution to work, we must still repair the unfound resources in the Resources.ResX first. We can “cheat” by renaming
the offending files back to the expected name, or moving the files to their expected locations, but this will not restore the lines that will be
missing from our designer code after we hit Ignore and Continue, which the text below outlines how to recover.
I typically repair them in Notepad – it is just less tinkering with the interactive nature of the IDE. Do a
Select All (Ctrl-A) of the designer code, copy it (Ctrl-C), bring up Notepad, and paste it into it (Ctrl-V).
Once our edits are done, we will select all of the Notepad data and replace all the designer code we had
previously copied out.
What we want to do is take a look at the very first line of all menu items that in turn should have
submenus. Sometimes, I find errors only in the main menu strip, and but more often also the main menu
items have lost their mojo, because further submenus are in most-all cases still fully intact. This will
happen with every menu item that has at least one submenu item that has an unfound resource. No other
code is ever altered; just the first line of their definitions, which is the one that should add submenu
items to it. I wish they would have simply commented the line out, along with a comment warning,
which would greatly simplify recovery, but this is likely not practical, because the Designer code often
recreates itself, which is why I wish it would either replace missing images with defaults, or remove the
assignment, and warn us, even if such a strategy was a user-selectable option in the IDE Settings.
For example, for our main menu, MenuStrip1, we would insert a blank line under its heading, then start
out by specifying the main menu object, Me.MenuStrip1, and after that add .DropDownItems.AddRange(New
ToolStripItem() { }), resulting in the following line:
Me.MenuStrip1.Items.AddRange(New ToolStripItem() { })

Now, all we need to do is populate this array definition with the appropriate submenu objects, which are
mnuFile and mnuEdit in our example’s case. Notice, in case we are not sure of exactly what we named
them, they are listed in an automatic dropdown option box (if we are editing within the IDE – the big
advantage to using it instead of NotePad), though remember that any submenu items that also have
submenus have their items listed sequentially after them, so there is some intuitive skipping we might
need to do. However, hopefully we are usually aware of what we named our main menu items, and the
IDE was usually kind enough, in cases we did not rename our menu items, to have named them with
non-spaced versions of their Text entries, though such also includes a trailing “ToolStripMenuItem”.
In the example’s case, I simply add “Me.mnuFile, Me.mnuEdit” within the curly braces (“{ }”) above:
Me.MenuStrip1.Items.AddRange(New ToolStripItem() {Me.mnuFile, Me.mnuEdit})

And suddenly all is well.


But, as stated earlier, often the other submenus with dropdowns are also missing their initial
“AddRange” entries, but that would be because one of their submenu items also had a missing resource
assigned to it. In those cases, we just as easily reconstruct them exactly as we had done to reconstruct
our main menu entry, so we could add “ Me.mnuFile.DropDownItems.AddRange(New ToolStripItem() { })”
below the mnuFile heading, and “Me.mnuEdit.DropDownItems.AddRange(New ToolStripItem() { })” under the
mnuEdit heading. We would then add the required items following them. In our example’s case, we
would add mnuFileExit within the curly-braces for the mnuFile entry, and add mnuEditCut,
mnuEditCopy, and mnuEditPaste to the mnuEdit entries, which are all listed following their parent menu
item, as shown above, rendering:
Me.mnuFile.DropDownItems.AddRange(New ToolStripItem() {Me.mnuFileExit})

And...
Me.mnuEdit.DropDownItems.AddRange(New ToolStripItem() {Me.mnuEditCut, Me.mnuEditCopy, Me.mnuEditPaste})
 Page 504

CONCLUSION
Although this was a simple example, this solution is just as simple and applicable for even the most
complex menus and toolbars. I have managed to repair what appeared to be total disaster in very
complex menu and toolbar losses to just a few minutes of work. After we have done it a time or two, it
should become a simple “Oh, well...” moment, followed by a quick repair.
It also reminded me that I should back my work up much more often than I do.
It also reminds me that I should NEVER exit the IDE after such a disaster, to keep repairs fast and easy.
But we must also remember that if we had hit Ignore and Continue and lost some dropdown definitions,
we should always FIRST repair the location and naming of the presumed missing resources, otherwise
any repairs to dropdown items will again be lost. Nothing is worse than taking the time to rebuild the
user-source code definitions and then run into all the same errors again – and worse if we suffer a brain
fart and again hit Ignore and Continue, all because we forgot to first repair the original problem.
Now, if we could just convince Microsoft to simply comment the offending lines out, not deleting them,
or, better, add them to an error log file, in order to make recovery a speedier repair between mouthfuls of
M&M’s and Diet Pepsi (Diet Pepsi immediately flushes the sugars and starches from our mouths, which
is why we often see developers doing such seemingly oxymoronic things, though, ironically, the artificial
sweetener Aspartame is an accumulative toxin that can cause us to gain water weight, not to mention
that Aspartame, suspended in liquid at 80 degrees F turns back into Methyl Alcohol (Wood Grain
Alcohol), which quickly turns into Formaldehyde at body temperature, which starts eating holes in the
brain, causing all sorts of neurological issues. Anyone ever hear of Gulf War Syndrome? Pallets of diet
soda sitting out on pallets in the 125 degree Saudi Arabian air was going to do nothing?)...
 Page 505

Black Book Tip # 20


Tracking ComboBox Items Under the Cursor.
Some get frustrated trying to determine the real-time index of the item the cursor is hovering over in the
dropdown list of a ComboBox control. Unlike with a ListBox control, which has an exposed TopItem
property so that we know which index item is being displayed at the top of the list, we cannot compute
the index from the cursor position (see Black Book Tip # 10 on page 477). Besides, even though we
could send a message to the ComboBox to get its TopItem value, the dropdown list of a ComboBox
control is in fact a separate window (a ListBox, actually), so if we want to track cursor movement for it,
we have to locate the Handle for that Listbox and monitor its cursor movement. It is doable using the
GetComboBoxInfo Pinvoke and a COMBOBOXINFO structure, which is described in Black Book Tip
# 59, Adding a Horizontal ScrollBar to a ComboBox DropDown List on page 757, but that does not
address hooking and unhooking the system message queue and monitoring window messages required
to monitor cursor movement and automatic item selection, which seems like a heck of a lot of work to
make ourself go through just to detect which entry the cursor is currently hovering over.
If we spelunk through the internet for alternative solutions, we will find most people responding to
someone wondering about how to do this with advice to obtain this index from the DrawItem method,
which is in fact the only method that does provide mapped coordinate changes. Although it is relatively
easy to use, being a solution I had discovered on my own, it sure seems like an over-engineered solution
just to get an index for the item within the ComboBox the mouse is hovering over.
However, the SelectedIndex updates as the cursor moves over the list. This is why the selection
rectangle displayed in the ListBox changes automatically with the mouse movement, even though the
item in the TextBox of the ComboBox does not change. This is truly a source of frustration for other
developers who are depending on that SelectionIndex property reflecting the displayed selection, which
it might not be, and it has been reported repeatedly to Microsoft as a bug (even though it actually is not),
especially because the SelectedIndexChanged event does not fire when mouse move changes
SelectedIndex. In fact, that event only fires as a result of a click event on the ComboBox list, a keyboard
selection, or if we change either the SelectionIndex property or the SelectedItem property in-code.
Being aware that the selection index changes with mouse movement offers us the possibility for another
solution... if only we can trap it, because, as I just said, mouse movement changing the SelectedIndex
and SelectedItem does not cause the SelectedIndexChanged event to fire. However, I have found that if I
do an absolutely out-of-the-hat cheap parlor trick using a short-interval timer, I can track and update data
with the mouse movement with absolute ease. Indeed, once we see this simple little solution, we will
probably slap our forehead because it will suddenly appear to be so obvious.
I will provide two separate methods for picking up this hover selection index value, both of which are
relatively easy to implement. The first uses the DrawItem method, and the second uses a simple timer.

Acquiring the Hover Index Using the DrawItem Technique


To implement this method, we have to set the ComboBox’s DrawMode property to OwnerDrawFixed.
Then, of course, we will have to write the DrawItem event handler method so that we will be able to still
view the contents of the ComboBox list, as otherwise it will be rendered blank because the DrawItem
event handler invoked by default will simply be the MyBase object’s version of that method, which is
just a stub. That is to say it does nothing (a stub is a method that simply returns upon invocation, useful
for disabling methods that we do not wish to be invoked). Naturally, it would be incumbent upon us to
over-ride it with our own method that will do something useful. To keep our code robust, we should also
initialize an external variable to the current selection when the ComboBox is opened, using it to detect
when a selection has changed. We can do this with the following methods:
 Page 506

Private comboboxIndex As Int32 'keep track of the item index.

'*********************************************************************************
' Method : ComboBox1_DropDown
' Purpose : Init selection index
'*********************************************************************************
Private Sub ComboBox1_DropDown(sender As Object, e As EventArgs) Handles ComboBox1.DropDown
comboboxIndex = Me. ComboBox1.SelectedIndex 'set to current selection when the combo-dropdown opens.
End Sub

'*********************************************************************************
' Method : ComboBox1_ DrawItem
' Purpose : Draw each individual item in the ComboBox, and detect which item is selected.
'*********************************************************************************
Private Sub ComboBox1_DrawItem(sender As Object, e As DrawItemEventArgs) Handles ComboBox1.DrawItem
e.DrawBackground() 'redraw combobox line's background (blank it out).
If e.Index <> -1 Then 'if something is selected (-1 = none).
With DirectCast(sender, ComboBox)
Dim brsh As New SolidBrush(.ForeColor) 'brush used for painting text.
e.Graphics.DrawString(.Items(e.Index).ToString,
.Font,
brsh,
e.Bounds.Location) 'draw the current item's text to the ComboBox list.
brsh.Dispose() 'dispose of created resource.
If e.State = DrawItemState.Selected Then 'if the current item is the mouse hover selection...
If comboboxIndex <> e.Index Then 'if the stored index does not match...
comboboxIndex = e.Index 'then update the tracking index.
'----------------------------------------------------------------------------
'Here, you can also invoke a method that should react when this index changes.
'Note, however, that although you can use this stored index value in your method, a little-
'known fact is that the ComboBox's SelectedIndex property WILL ALSO REFLECT THIS VALUE!
'----------------------------------------------------------------------------
End If
End If
End With
End If
End Sub

As we can see, when the ComboBox dropdown list opens, we will set our comboboxIndex value to the
current selection index of the ComboBox, which will always highlight first. Within our DrawItem event
handler, we are doing the minimal work necessary, which is simply to draw the contents of the index line
provided, which is 1) Draw the field background, 2) Draw the line’s text, and 3) keep track of the item
that is currently selected by the mouse hovering over it.
Finally, once all this is in place, we would invoke any immediate reaction to changes in the
SelectedIndex value right from within the DrawItem method, or, if we are really sneaky, we can declare
a method WithEvents and fire off that event. But however we do it, within such a reactionary method we
will grab the value stored in comboboxIndex or the SelectedIndex value from the ComboBox when we
want to know which item the mouse is hovering over (this index value changes automatically when we
move the mouse, because the DrawItem event handler will be invoked as the mouse tracks over each
item), which is technically, for all intensive purposes, also an effective SeletedIndex-tracking monitor.
That is the typical solution, although I had strengthened it by initializing the stored variable from within the
DropDown event handler.
If we spelunk around MSDN, we will find a CB_GETTOPINDEX message that will pick up the TopIndex
property of a ComboBox. This was covered earlier on page 367, but this is usually almost useless for tracking
indexes in a ComboBox because we cannot monitor MouseMove events when the mouse is over anything
except the text field of the ComboBox. The dropdown list for a ComboBox is actually a separate ListBox
object and, although we can in fact access this object with a little bit of code gymnastics to acquire its handle
and then monitor its messages, that is a boatload of excessive work to go through just to find out which item
index the mouse is over, when the above technique, and the one below, work quite well.
Acquiring the Hover Index Using a Timer
Another thing – suppose we do not want to bother with the trouble of implementing a DrawItem event
handler for the ComboBox. Were you aware that we can still monitor the hover selection of the mouse? As
indicated above, the SelectionIndex property actually changes as we move the mouse over an item. The trick
is – how do we track it, because we usually want to do this in real time?
 Page 507

My solution is to use a timer that will only be active when the ComboBox’s dropdown is open, which means
we will enable the timer when the dropdown opens through its DropDown event handler, and we will disable
the timer when the dropdown closes through its DropDownClosed event handler.
To demonstrate this simple technique, start a new Windows Form project and place a Label Control named
Label1 on the form. Below that, place a ComboBox control named ComboBox1. Next, add a Timer control
named Timer1 on the form. The timer will default to an interval of 100 milliseconds, being 1/10 th of a
second, which is perfect, although some people prefer to use a longer interval, but a delay much longer than
100 milliseconds can sometimes get to be a bit distracting.
Finally, add the following complete form code to our new Form1:
Option Strict On
Option Explicit On
Public Class Form1
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private ComboIndex As Int32 = -1 'store the selection index monitor.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'*********************************************************************************
'*********************************************************************************
' Method : Form1_Load
' Purpose : Initialize by clearing the label text
'*********************************************************************************
'*********************************************************************************
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
With Me.ComboBox1.Items
For Index As Int32 = 0 To 49 'fill the combobox with some simple data.
.Add("Sample Line " & Index.ToString)
Next
End With
Me.ComboBox1.SelectedIndex = 0 'initialize the data by selecting the first entry.
End Sub
'*********************************************************************************
' Method : ComboBox1_DropDown
' Purpose : Store the current index selection and start the timer
'*********************************************************************************
Private Sub ComboBox1_DropDown(sender As Object, e As EventArgs) Handles ComboBox1.DropDown
Me.ComboIndex = Me.ComboBox1.SelectedIndex 'save the current selection.
Me.Timer1.Enabled = True 'turn the timer on.
End Sub
'*********************************************************************************
' Method : ComboBox1_DropDownClosed
' Purpose : Stop the timer
'*********************************************************************************
Private Sub ComboBox1_DropDownClosed(sender As Object, e As EventArgs) Handles ComboBox1.DropDownClosed
Me.Timer1.Enabled = False 'turn the timer off.
End Sub
'*********************************************************************************
' Method : ComboBox1_SelectedIndexChanged
' Purpose : the selection index changed via a user click
'*********************************************************************************
Private Sub ComboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Handles ComboBox1.SelectedIndexChanged
Me.ComboIndex = Me.ComboBox1.SelectedIndex 'save the selection index.
Me.Label1.Text = Me.ComboBox1.SelectedItem.ToString 'record the selection to the label.
End Sub
'*********************************************************************************
' Method : Timer1_Tick
' Purpose : Init mouse index and reveal demo image when the dropdown opens
'*********************************************************************************
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Me.Timer1.Enabled = False 'disable the timer for now.
With Me.ComboBox1
If .SelectedIndex <> -1 AndAlso
Me.ComboIndex <>.SelectedIndex Then 'if we detected a change.
Me.ComboIndex = .SelectedIndex 'update the test index.
Me.Label1.Text = .SelectedItem.ToString 'report the current indexed item.
End If
End With
Me.Timer1.Enabled = True 'turn the timer back on.
End Sub
End Class

Run this code and we will notice that when we drop the ComboBox list down, the Label1 control will
faithfully reflect the item under the mouse cursor as we move it, even though, if we wanted to or needed
to, we could also reference our saved variable value.
 Page 508

Black Book Tip # 21


Demystifying Pinvoke Definitions of VB6 POINTAPI and RECT Structures.
Many former VB6 developers have expressed frustration when they must resort to a Pinvoke to employ
some arcane capability deep within the bowels of the operating system that requires a Point or Rectangle
structure. They are frustrated because they think they cannot pass a .NET Point or Rectangle structure as
a Pinvoke parameter. They think they must declare separate and cruder Win32 structures, fill them from
existing .NET structures, just to pass the .NET structure data to that Pinvoke.
But that is a myth. The data footprint of a .NET Point and a Win32 POINTAPI are exactly the same, as
are the data footprints of a .NET Rectangle and a Win32 RECT, so we can in fact use them!
What we must embrace is that a Point and Rectangle structure under .NET is just that – a structure. What sets
structures apart from the User-Defined Types of Visual Studio 6 and its predecessors, which we may have
over time grown numbly used to, is that the .NET versions of these constructs are unrestricted abstract
classes, and so they are fully able to support constructors, methods, and properties. Actually, User-Defined
Types were abstract classes, but limited in that they did not support methods and properties.
Internally, a Point, just like POINTAPI, contains two Int32 fields of data, and a Rectangle, just like
RECT, contains four Int32 fields, but with the bonus of various methods and properties to enhance their
use, providing significant code reuse, but also appearing to confuse a lot of their newer users.
In order to understand the general misconceptions that a great many developers have about these
structures, largely because .NET Points and Rectangles do contain constructors, methods and properties,
let us likewise enhance POINTAPI and RECT in a similar fashion. But even with all these new
enhancements added to them, we can still use them exactly as we did before with a Pinvoke.

Getting the Point


Consider the traditional POINTAPI structure that we may have used to death for countless Pinvokes:
Friend Structure POINTAPI
Friend X As Int32 'X field.
Friend Y As Int32 'Y Field.
End Structure

This is typical of how we declare this structure, much as we may have done with mechanical regularity
under Visual Studio 6 and its ancestors. We then assign it to a field name and initialize it, like so:
Dim Pnt As POINTAPI 'declare storage space for a POINTAPI structure.
Pnt.X = 10 'fill the POINTAPI structure with data.
Pnt.Y = 25

This is OK if we do it only a time or two, but if we do it a lot, it can become a major pain in the tin can.
What is more frustrating is when we have a .NET Point structure already defined, but many think they
are forced to assign its data to the POINTAPI structure before being able to use it in a Pinvoke.
Consider the following POINTAPI structure declaration that features two constructors, which we can
use to both create a new copy of the structure and initialize it with a Point structure or two integer
values, and a bonus property that we can use to set and retrieve the data using a .NET Point:
'*********************************************************************
'*********************************************************************
' Structure : POINTAPI
' Purpose : Provide simple .NET interface to traditional POINTAPI.
'*********************************************************************
'*********************************************************************
Friend Structure POINTAPI
Friend X As Int32 'X field.
Friend Y As Int32 'Y Field.

' constructor to create a POINTAPI structure by assigning a .NET Point to it. ie: Dim myPnt As POINTAPI = Me.Location,
Friend Sub New(ByVal Point As Point) ' or: Dim myPnt As New POINTAPI(Me.Location).
Me.Point = Point 'take advantage of our own Point property, declared below.
End Sub

' constructor to create a POINTAPI structure using two integer values. ie: Dim myPnt As New POINTAPI(iX, iY).
 Page 509

Friend Sub New(ByVal X As Int32, ByVal Y As Int32)


Me.X = X
Me.Y = Y
End Sub

' property to get/set the POINTAPI data via a VB.NET Point ie: myPnt.Point = Me.Location, or Me.Location = myPnt.Point.
Friend Property Point As [Point] 'use [] to bypass declaration ambiguity and reference the base .NET Point type.
Get
Return New [Point](Me.X, Me.Y) 'return data as a .NET Point.
End Get
Set(value As [Point]) 'assign data from a .NET Point.
Me.X = value.X
Me.Y = value.Y
End Set
End Property
End Structure

NOTE: Notice how we took advantage of our internal Point property in our structure to define a new POINTAPI object from
a .NET Point. Notice further the use of square brackets so we can inform the compiler that we want these objects to reference
the language’s own Point structure, and not confuse it with our property of the same name. If we did not do this, the compiler
would assume that we are actually referencing our property, because it is declared locally in scope and would otherwise take
precedence over the outer, intrinsic declaration.
With this declared, we can commence declaring our POINTAPI structures on the fly, and even right
within the parameter list of the Pinvoke that we might be executing, if we wanted to. For example:
Dim NetPnt1 As New Point(40, 17) 'declare a new .NET Point structure.
Dim Pnt1 As New POINTAPI(10, 25) 'declare POINTAPI structure with two integer values.
Dim Pnt2 As New POINTAPI(NetPnt1) 'declare POINTAPI structure using a .NET Point.
NetPnt1 = Pnt1.Point 'assign a .NET Point from an existing POINTAPI structure.
Pnt2.Point = NetPnt1 'assign POINTAPI data from an existing .NET Point.
Dim NetPnt2 As Point = Pnt2.Point 'create a new .NET Point from a POINTAPI structure's data.

NOTE: A few programmers new to Object-Oriented Programming might be a bit confused about how we might be able to
declare methods and properties within a Structure and not increase its total size. The data (field values) of the structure is
stored on the stack just the same as any other structure that might be declared without methods or properties. The methods,
however, are always stored separately, in the program space set aside for regular program code. This also helps explain one
of the biggest points of confusion to new programmers, which is how we can reference and use a new Structure that is only
now being declared. The point to remember is that this program code exists when we begin executing our application and so
it is already available for use whenever we create a new POINTAPI structure object. Also, when we begin executing these
methods or properties, prior to the code of a New method even being executed, the compiler had already inserted code that
will set aside space on the program stack to store the data items declared within the new structure. To recap, because data
and code are stored separately, the data, which defines the actual object, not its program code, will still have exactly the
same data footprint size as the data from a structure declared without methods and properties. This also highlights the fact
that Structures are actually Classes, but abstracted by storing their data on the transient Stack rather than the Protected
Heap, and they are also empowered to clone themselves incessantly to virtually anything that tries to peek at them.

Simplifying Pinvoke Rectangle Structures


A traditional Win32 RECT structure declared for Pinvoke use contains 4 integer data elements. Pinvokes
typically expects of rectangles the client coordinates of a region, which specifies Left, Top, Right, and
Bottom pixel coordinates, consecutively. Following is a typical RECT declaration:
Friend Structure RECT
Friend iLeft As Int32
Friend iTop As Int32
Friend iRight As Int32
Friend iBottom As Int32
End Structure

As previously stated, and to reiterate, an advantage of .NET structures being full-featured abstract
classes is that we can introduce methods and properties to these structures that are more specific to how
the data can be used. Apart from declaring such structures and assigning four integer values to them, we
can also assign two .NET Points to them, and assign and retrieve Point values after it has been created.
On top of that, we can also introduce Width, Height, Left, Right, Top, Bottom, X, X1, X2, Y, Y1, and Y2
properties so that we can communicate with these different objects exactly the same, but they will still
store their respective data in the specific way that is expected of them.
 Page 510

Consider my rather long, though feature-rich enhancement of the RECT structure, providing various
constructors to initialize them during declaration, and plenty of other properties to access and set the
rectangle’s data, either as individual values, by Point, or even by Width and Height:
'*********************************************************************
'*********************************************************************
' Structure : RECT
' Purpose : Win32 Rectangle with 4 coordinate indexes.
'*********************************************************************
'*********************************************************************
Friend Structure RECT
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private _Left As Int32 'protect and declare our 4 integer values.
Private _Top As Int32
Private _Right As Int32
Private _Bottom As Int32
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'------------------------------------------------------------------------
'return our left-most location.
'------------------------------------------------------------------------
Friend Property Left As Int32
Get
Return Me._Left
End Get
Set(value As Int32)
Me._Left = value
End Set
End Property

'------------------------------------------------------------------------
'return our top-most location.
'------------------------------------------------------------------------
Friend Property Top As Int32
Get
Return Me._Top
End Get
Set(value As Int32)
Me._Top = value
End Set
End Property

'------------------------------------------------------------------------
'return our right-most location.
'------------------------------------------------------------------------
Friend Property Right As Int32
Get
Return Me._Right
End Get
Set(value As Int32)
Me._Right = value
End Set
End Property

'------------------------------------------------------------------------
'return our bottom-most location.
'------------------------------------------------------------------------
Friend Property Bottom As Int32
Get
Return Me._Bottom
End Get
Set(value As Int32)
Me._Bottom = value
End Set
End Property

'------------------------------------------------------------------------
'get/set our left location as X.
'------------------------------------------------------------------------
Friend Property X As Int32
Get
Return Me._Left
End Get
Set(value As Int32)
Me._Left = value
End Set
End Property

'------------------------------------------------------------------------
'get/set our top location as Y.
'------------------------------------------------------------------------
Friend Property Y As Int32
Get
 Page 511

Return Me._Top
End Get
Set(value As Int32)
Me._Top = value
End Set
End Property

'------------------------------------------------------------------------
'get/set our left location as X1.
'------------------------------------------------------------------------
Friend Property X1 As Int32
Get
Return Me._Left
End Get
Set(value As Int32)
Me._Left = value
End Set
End Property

'------------------------------------------------------------------------
'get/set our top location as Y1.
'------------------------------------------------------------------------
Friend Property Y1 As Int32
Get
Return Me._Top
End Get
Set(value As Int32)
Me._Top = value
End Set
End Property

'------------------------------------------------------------------------
'get/set our right location as X2.
'------------------------------------------------------------------------
Friend Property X2 As Int32
Get
Return Me._Right
End Get
Set(value As Int32)
Me._Right = value
End Set
End Property

'------------------------------------------------------------------------
'get/set our bottom location as Y2.
'------------------------------------------------------------------------
Friend Property Y2 As Int32
Get
Return Me._Bottom
End Get
Set(value As Int32)
Me._Bottom = value
End Set
End Property

'------------------------------------------------------------------------
'return the width of our rectangle.
'------------------------------------------------------------------------
Friend Property Width As Int32
Get
Return Me._Right – Me._Left + 1
End Get
Set(value As Int32)
Me._Right = Me._Left + value - 1
End Set
End Property

'------------------------------------------------------------------------
'return the height of our rectangle.
'------------------------------------------------------------------------
Friend Property Height As Int32
Get
Return Me._Bottom – Me._Top + 1
End Get
Set(value As Int32)
Me._Bottom = Me._Top + value - 1
End Set
End Property
 Page 512

'------------------------------------------------------------------------
'return the Top-left location of our rectangle.
'------------------------------------------------------------------------
Friend Property Location As Point
Get
Return New Point(Me._Left, Me._Top)
End Get
Set(value As Point)
Me._Left = value.X
Me._Top = value.Y
End Set
End Property

'------------------------------------------------------------------------
'return the bottom-right location of our rectangle.
'------------------------------------------------------------------------
Friend Property brLocation As Point
Get
Return New Point(Me._Right, Me._Bottom)
End Get
Set(value As Point)
Me._Right = value.X
Me._Bottom = value.Y
End Set
End Property

'------------------------------------------------------------------------
'return the size of our rectangle.
'------------------------------------------------------------------------
Friend Property Size As [Size]
Get
Return New [Size](Me.Width, Me.Height)
End Get
Set(value As [Size])
Me.Width = value.Width
Me.Height = value.Height
End Set
End Property

'------------------------------------------------------------------------
'declare our rectangle using a .NET Rectangle.
'------------------------------------------------------------------------
Friend Sub New(ByVal NetRect As Rectangle)
Me.Location = NetRect.Location
Me.Size = NetRect.Size
End Sub

'------------------------------------------------------------------------
'declare a RECT using 2 .NET Points.
'------------------------------------------------------------------------
Friend Sub New(ByVal tlLocation As Point, ByVal brLocation As Point)
Me.Location = tlLocation
Me.brLocation = brLocation
End Sub

'------------------------------------------------------------------------
'declare a RECT using .NET Point and Size items.
'------------------------------------------------------------------------
Friend Sub New(ByVal Location As Point, ByVal Size As [Size])
Me.Location = Location
Me.Size = Size
End Sub

'------------------------------------------------------------------------
'declare a RECT using 4 integer location values.
'------------------------------------------------------------------------
Friend Sub New(ByVal X1 As Int32, ByVal Y1 As Int32, ByVal X2 As Int32, ByVal Y2 As Int32)
Me._Left = X1
Me._Top = Y1
Me._Right = X2
Me._Bottom = Y2
End Sub
End Structure

Notice in this example structure that it features many of the same properties as its Rectangle cousin.
Indeed, this structure, like the one I provided for POINTAPI, completely encapsulates its data and the
user communicates with that data only through its methods and properties.
 Page 513

Hopefully we can use these structures as a launching point for our own adventures into declaring
structures that feature constructors, methods, and properties. We could get our feet wet by expanding
these structure’s capabilities and provide them with much more robust methods, and also provide some
of the other features available to the .NET Point and Structure objects.
But simply remember this: we in fact do not need to use the POINTAPI or RECT structure anymore,
even with Pinvokes, but we can in fact use the .NET Point and Rectangle objects in their place. By
understanding the enhancements we have made to the POINTAPI and RECT structures, bringing them
closer to their .NET Point and Rectangle cousins, one can begin to comprehend and embrace the real
power of .NET software development with a sense of greater understanding.
 Page 514

Black Book Tip # 22


Easily Sorting Strongly-Typed Generic Collections Lists.
Many developers get excited when they see that the Collections.Generic List types expose a Sort
method. However, that excitement can often flip to frustration if it throws up error messages because
they are trying to assign complex objects to them, as opposed to simple strings or scalar values. Grrrr...
The solution to this issue is really rather simple. If we examine the error message, we will see that they
are referring to a Comparison function that the internal QuickSort sorting algorithm requires us to
provide it in order for it to perform comparisons on our objects. This is necessary because the sorter has
no knowledge of our object’s structure and what part of it we want to base the sort on.
If we refer back to Black Book Tip # 6 on page 460, we were shown a small class object that provided
the comparison method required to properly sort any column on a ListView control. We need to provide
something similar, but nothing nearly as complex, to Generic Collections Lists.
However, we do not really need to create a whole new class if we do not want to. For example, we could
just define a comparison function. In fact, we actually have 4 different options to choose from:
1. We can simply do nothing and use the List object’s default Sort method right out of the gate if we are storing string
data or scalar (numeric) values.
2. We can declare a simple separate comparison class to provide the required equity services that is designed to access
the appropriate fields in each object to check during the sorting process.
3. We can alternatively declare a comparison function to provide this equity service.
4. The final option simply allows us to perform either limited-range (partial) sorts and binary searches utilizing the
class already suggested by option 2.
All these options make doing sorts with Generic Collections Lists almost too easy. The only thing we
have to decide on is how we want to implement the sorting service.

Option 1: Using the Default Sort


If we are storing strings or any scalar types that VB.NET already recognizes, then we do not need to do
anything special. We can just invoke the Sort method on our list. For example:
Dim MyList As New Collections.Generic.List(Of String)
MyList.Add("Ralph") 'fill the list
MyList.Add("Judy")
MyList.Add("Timmy")
MyList.Add("Bob")

MyList.Sort() 'sort the list using the default comparison methods.

For Each Item As String In MyList


Debug.Print(Item) 'display the sorted contents of the list.
Next

The above code would yield the following result in the Debug Output panel of the IDE:
Bob
Judy
Ralph
Timmy

Option 2: Using Sort(IComparer(Of T))


Suppose we were storing within the list a class object that encapsulate numerous fields as its data, and
we want to perform the sort based upon just one of those fields (we can also perform more complex
sorts involving multiple items within the object, but only if we really wanted to go through all that
trouble). Let us assume for now that we have an object class of type Contact that has a property called
Name that is a string field, and we want the sort to be based on that selected field.
 Page 515

Before we can start with the solution, first consider the simple Contact class we use in our examples:
Friend Class Contact
Friend Name As String 'contact name.
Friend eMail As String 'contact email.

Friend Sub New(ByVal Name As String, ByVal eMail As String) 'we will assume that text data is provided in both fields.
Me.Name = Name.Trim 'assign contact name.
Me.eMail = eMail.Trim 'assign contact email.
End Sub

Public Overrides Function ToString() As String


Return Me.Name & " <" & Me.eMail & ">" 'return the name and email.
End Function
End Class

Now, to get started, we will declare a small comparison class to perform the Sort method’s comparisons
on our object. This is very easy to do. Consider the following small class that I designed to sort the
Contact objects (note that the object stored in the list collection could even be a structure):
Friend Class ContactComparer
Implements IComparer(Of Contact) ' <-- typing this, VB will automatically produce the Compare function heading.

Friend Function Compare(ByVal X As Contact, ByVal Y As Contact)


As Integer Implements System.Collections.Generic.IComparer(Of Contact).Compare
If X Is Nothing Then 'if X is Nothing...
If Y Is Nothing Then 'and Y is ALSO Nothing...
Return 0 'then they are equal.
Else
Return -1 'otherwise, Y > X, because X is Nothing and Y is not.
End If
ElseIf Y Is Nothing Then 'Else X is not Nothing but if Y is Nothing...
Return 1 'X > Y because Y is Nothing and X is not.
Else
Select Case X.Name.CompareTo(Y.Name) 'compare the strings if X and Y are not Nothing...
Case 1
Return 1 'X>Y (reflect string's CompareTo result).
Case -1
Return -1 'X<Y (reflect string's CompareTo result).
Case Else 'X=Y.
Return X.Name.Length.CompareTo(Y.Name.Length) 'return the comparison of lengths if strings seem to match.
End Select
End If
End Function
End Class

That is all there is to this class. It will check to see if either of the objects exists. If both the X and the Y
objects do not exist, 0 for equity is returned. If X does not exist but Y does, then Y is assumed greater
and -1 is returned. If X exists and Y does not, then 1 is returned, indicating that X is greater. If both
exist, a string comparison is performed on their Name properties. We only check further if equity is
detected, because one may in fact be longer than the other, but the check is only performed until the
length of one or the other gives out, so if a 0 result is detected, we compare the lengths of the strings.
To use it, we will load our Contact list with some data. For example:
Dim MyList As New Collections.Generic.List(Of Contact) 'new list of type Contact.
MyList.Add(New Contact("Ralph", "Ralph@Home.com")) 'fill the list (entry 0).
MyList.Add(New Contact("Judy", "Judy@Home.com")) '(entry 1).
MyList.Add(New Contact("Timmy", "Timmy@Home.com")) '(entry 2).
MyList.Add(New Contact("Bob", "Bob@Home.com")) '(entry 3).

Dim Comp As New ContactComparer 'define an instance of our comparison class.


MyList.Sort(Comp) 'sort the list using that instance.

For Each Item As Contact In MyList


Debug.Print(Item.ToString) 'display the sorted contents of the list.
Next

This provides a result of:


Bob <Bob@Home.com>
Judy <Judy@Home.com>
Ralph <Ralph@Home.com>
Timmy <Timmy@Home.com>
 Page 516

Note also that if this is in fact a one-shot deal, that we did not really have to also declare the Comp
object in order to sort the list, but instead we could have simply sorted this list using “ MyList.Sort(New
ContactComparer)”. However, it is safe to declare the sorting object in case we will be using it again.

Try writing a class to sort in reverse, or simply flip the result by multiplying it times a value of -1.

Option 3: Using Sort(Comparison(Of T))


An alternative to defining a comparer class is to instead simply declare a comparison function and
reference it when invoking the Sort method. Notice that the comparison method below simply pulls the
Compare function out of the previously mentioned class, ContactComparer. Here we rename the method
ContactCompare and declared it right within the body of our code:
Friend Function ContactCompare(ByVal X As Contact, ByVal Y As Contact) As Integer
If X Is Nothing Then 'if X is Nothing...
If Y Is Nothing Then 'and Y is ALSO Nothing...
Return 0 'then they are equal.
Else
Return -1 'otherwise, Y > X, because X is Nothing and Y is not.
End If
ElseIf Y Is Nothing Then 'Else X is not Nothing but if Y is Nothing...
Return 1 'X > Y because Y is Nothing and X is not.
Else
Select Case X.Name.CompareTo(Y.Name) 'compare the strings if X and Y are not Nothing...
Case 1
Return 1 'X>Y (reflect string's CompareTo result).
Case -1
Return -1 'X<Y (reflect string's CompareTo result).
Case Else 'X=Y
Return X.Name.Length.CompareTo(Y.Name.Length) 'return the comparison of lengths if strings seem to match.
End Select
End If
End Function

Implementation of this method is almost like using the class except that first, we obviously do not need
to instantiate a comparison class, and second, we simply reference our comparison method through the
AddressOf operator. Consider this example, which produces the exact same results as the last example:
Dim MyList As New Collections.Generic.List(Of Contact) 'new list of type Contact.
MyList.Add(New Contact("Ralph", "Ralph@Home.com")) 'fill the list (entry 0).
MyList.Add(New Contact("Judy", "Judy@Home.com")) '(entry 1).
MyList.Add(New Contact("Timmy", "Timmy@Home.com")) '(entry 2).
MyList.Add(New Contact("Bob", "Bob@Home.com")) '(entry 3).

MyList.Sort(AddressOf ContactCompare) 'sort the list through the referenced comparer function.
'Too bad .NET can auto-gen Delegates for its use, but not of clearly-defined methods for interop. But, safety...
For Each Item As Contact In MyList
Debug.Print(Item.ToString) 'display the sorted contents.
Next

Option 4: Using Sort(Int32, Int32, IComparer(Of T))


I save this method for last, even though it uses our ContactComparer class outlined in Option 2. The use
of this sort method is to simply sort a portion of the list. Instead of doing a full sort, we supply it with an
index into the list where to start the sort, and a count of the consecutive number of items to sort. Hence,
if we specify an index of 3 and a count of 5, it will start sorting on the 4 th item in the list (the index is
offset from zero), and sort that and the following 4 items, leaving everything else alone.
This comparison class is also useful if we want to perform a binary search (an example of a binary
search will be demonstrated in a moment) within a limited window in a list collection and we will be
expected to supply a comparison class to it if we are using non-default types.
To sort just the last two entries on our almost standard list of items, consider this example:
Dim MyList As New Collections.Generic.List(Of Contact) 'new list of type Contact.
MyList.Add(New Contact("Ralph", "Ralph@Home.com")) 'fill the list (entry 0).
MyList.Add(New Contact("Judy", "Judy@Home.com")) '(entry 1).
MyList.Add(New Contact("Timmy", "Timmy@Home.com")) '(entry 2).
MyList.Add(New Contact("Bob", "Bob@Home.com")) '(entry 3).

Dim Index As Int32 = 2 'start sort on Timmy.


Dim Count As Int32 = 2 'include only Timmy and Bob in the sort.
 Page 517

Dim Comp As New ContactComparer 'create a new comparison class.


MyList.Sort(Index, Count, Comp) 'sort the list from Index for Count Items.

For Each Item As Contact In MyList


Debug.Print(Item.ToString) 'display the sorted contents.
Next

This example yields the following result, sorting only the Timmy and Bob entries:
Ralph <Ralph@Home.com>
Judy <Judy@Home.com>
Bob <Bob@Home.com>
Timmy <Timmy@Home.com>

I will leave you to explore the finer points of using selective BinarySearch method on your own, which
is invoked much as we will below for Sort, except that we would also include a search object and it
returns the index of the match found.
Note, however, that if an exact match is not found, then the returned index will be the bitwise
complement of the index of the next element that is larger than the search string. What this really means
in plain English is that if we receive a negative result, this means that a match was not found, but we can
simply flip this returned value (just place a negative sign in front of it) and subtract 1 (all this simply
performs a mathematical 2’s Complement) so we can use it as an index into the list to insert the unfound
value in the proper sorted location. For me, I invoke the sort, and then invoke BinarySearch.
Performing a Sort and a Binary Search
If we want to insert data sequentially into a list, we first have to sort it to get it in order, and then invoke
a built-in binary search to find where to insert it. A binary search finds the position of a target value
within a sorted array incredibly fast. It begins by comparing our value to that at the middle of the array.
If the value is less than it, then the search continues by checking the middle of the array's lower half, or
if it is greater, then the middle of the upper half, eliminating the search range by half each time until it or
the place where it should be inserted is found, and all without the fuss and muss of doing it ourself.
Thus, a search of 1024 items will require a max of 11 checks, though typically less, down to 1.
Consider the following, where we will sort the list, search for a new object using a binary search, and
then insert it into the list if it is not found, placing the new object in the proper sorted location:
Dim MyList As New Collections.Generic.List(Of Contact) 'new list of type Contact.
MyList.Add(New Contact("Ralph", "Ralph@Home.com")) 'fill the list (entry 0).
MyList.Add(New Contact("Judy", "Judy@Home.com")) '(entry 1).
MyList.Add(New Contact("Timmy", "Timmy@Home.com")) '(entry 2).
MyList.Add(New Contact("Bob", "Bob@Home.com")) '(entry 3).

Dim Comp As New ContactComparer 'define an instance of our comparison class.


MyList.Sort(Comp) 'sort the list using that instance.
Dim NewItem As New Contact("David", "david.ross.goben@gmail.com") 'create an object to search for.
Dim Index As Int32 = MyList.BinarySearch(NewItem, Comp) 'find where to insert it, if it does not already exist.
If Index < 0 Then 'if it was not found in the list...
MyList.Insert(-Index - 1, NewItem) 'then insert it into the list: -Index-1 = insert point.
End If

For Each Item As Contact In MyList


Debug.Print(Item.ToString) 'display the sorted contents.
Next

Notice that when a negative value is returned, the negative value is, as stated earlier the bit-wise
complement (AKA 2’s Complement) of the index of the item that is larger than the tested item. Again, to
get the actual index to insert the new item at from this result, we simply have to flip the negative result’s
sign and then subtract 1 from it (-Index – 1). This yields the following result:
Bob <Bob@Home.com>
David <david.ross.goben@gmail.com>
Judy <Judy@Home.com>
Ralph <Ralph@Home.com>
Timmy <Timmy@Home.com>
 Page 518

Black Book Tip # 23


Dithering a Form’s Background.
Have you ever seen forms where their background smoothly transition from a color, such as Blue, down
to black, or near black at the bottom? It is a really cool effect, and even better, it is really easy to do.
In 1998 I wrote a VB6 method to perform this task, and it worked
great, even with its clunky, archaic graphics capabilities. Just
recently I upgraded it to VB.NET. I spiffed it up using enumerators
and computed a more controlled transition on any screen size (the
VB6 scaled the screen as though it was consistently 1600 twips high,
which is 800 pixels, but that looked a bit grainy on the larger and
more modern screen displays, like my 25-inch UHD digital monitor).
Because we invoke this method from the Form’s Paint event handler, where we will draw the form’s
background, we will also need to pass the paint method’s Graphics object (the form’s Device Context) to
our dithering method. Normally we can create our own Graphics object as easily as “ Dim eg As Graphics =
Me.CreateGraphics”, and in most cases this works fine, but if we use a different DC to perform various paint
operations, we will find our Paint method recursively re-invoking itself because we actually used a
different device context to over-write various portions of the display, which will happen if we perform
other paint operations that will draw on top of the form background that was just dithered. What is even
more apparent is if we place labels on the form that are supposed to have a transparent background.
They tend to lose that transparent effect using a different DC during a paint event (DUH!).
However, to fix this, all we need to do is to pass on the Graphics object. Also, if we will be performing
other painting chores aside from dithering, it is also a very good idea to fully obstruct any possible
recursive invocations to our Paint event. For example, consider the following Paint event that I cooked
up, which we should use as a model for our own dithering operations:
'*********************************************************************
' Method : Form1_Paint
' Purpose : Draw background.
'*********************************************************************
Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
Static Processing As Boolean = False 'flag to eliminate recursive invocations.

If Not Processing Then 'if following code is not already running...


Processing = True 'set the processing flag.
Dither(Me, e.Graphics, DitherColors.Blue) 'dither the form background from Blue down to black.
'-------------------------------------------
'Other form-painting methods go here. If you have no other painting tasks,
'then the Processing flag and this If-block wrapper are really not needed.
'-------------------------------------------
Processing = False 'reset the processing flag before leaving.
End If
End Sub

By using the Processing Boolean flag, we can nip recursive invocations right in the bud.
The process of dithering, although it sounds mysterious and complicated, is really quite simple. We just
draw a series of horizontal colors from one color down to black across the screen. Even with just that, it
actually looks amazingly cool, just like some impressive parlor trick.
We must keep in mind that for a 32-bit color, each of the 3 emittive color values (not counting the Alpha
component) has only 256 (0-255) values available, so what we must do is divide the screen up vertically
into 256 segments and draw however many lines are assigned to that segment of that color graduation,
which starts at 255, and each drops 1 down to 0 from the previous. By setting the Red and Green color
values to zero and setting the Blue color value to 255, the display will start at full blue at the top, and by
the time we get to the bottom, it will become black, because Red=0, Green=0, and Blue =0 is black.
 Page 519

Following is my Background Dithering Module:


Option Explicit On
Option Strict On

Module modDitherBackground.
'*********************************************************************
' Dither: The Dither() method dithers a form background, blue
' down to black by default. From your form's Form_Paint
' event, you can do: Dither(Me, e.Graphics).
'
' Optional Intensity 0-8.
' 0 has a good balance from full color down to black.
' greater values dither to less darkness at the bottom of the screen.
'
'---------------------------------------------------------------------
'Use in your own Form_Paint() event handler. For example:
'
'Private Sub Form_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
' Dither(Me, e.Graphics) 'dither the form background from Blue down to black.
'End Sub
'
'Note: Although you can generates a graphics object from the Form using Form.CreateGrraphics(),
'because you may be painting other things to the form within the form's Paint(), event, if you
'are using more than one form graphics object during the same process, they can conflict and
'cause the application to hang.
'*********************************************************************

Friend Enum DitherColors As Int32


Red
Green
Blue
Yellow
Magenta
Cyan
White
End Enum

Friend Sub Dither(ByVal Frm As Form, ByRef eGraphics As Graphics,


Optional ByVal ClrValue As DitherColors = DitherColors.Blue,
Optional ByVal Intensity As Int32 = 1)

Dim tHeight As Int32 = Frm.ClientSize.Height 'height of the form's client area.


Dim tWidth As Int32 = Frm.ClientSize.Width 'width of the form's client area.

Intensity = Math.Abs(Intensity) + 2 'compute the intensity value (set actual minimum to at least 2).
If Intensity > 10 Then
Intensity = 10 'but do not let it go above 10.
End If
'---------------------------------------------------
Dim cRed As Int32 = 0 'init RGB base color flags.
Dim cGreen As Int32 = 0
Dim cBlue As Int32 = 0

Select Case ClrValue 'set used color values to 255 as needed.


Case DitherColors.Red
cRed = 255 'full Red: Red = 255, Green = 0, Blue = 0.
Case DitherColors.Green
cGreen = 255 'full Green: Red = 0, Green = 255, Blue = 0.
Case DitherColors.Blue
cBlue = 255 'full Blue: Red = 0, Green = 0, Blue = 255.
Case DitherColors.Yellow
cRed = 255 'full Yellow: Red = 255, Green = 255, Blue = 0.
cGreen = 255
Case DitherColors.Magenta
cRed = 255 'full Magenta: Red = 255, Green = 0, Blue = 255.
cBlue = 255
Case DitherColors.Cyan
cGreen = 255 'full Cyan: Red = 0, Green = 255, Blue = 255.
cBlue = 255
Case DitherColors.White
cRed = 255 'full white: Red = 255, Green = 255, Blue = 255.
cGreen = 255
cBlue = 255
Case Else 'default to Blue.
cBlue = 255
End Select
'---------------------------------------------------
Dim tRed As Int32 = 0 'init RGB pen creator color indexes.
Dim tGreen As Int32 = 0
Dim tBlue As Int32 = 0

Dim htOffset As Int32 = tHeight \ 256 'determine vertical pixels to assign to each step.
 Page 520

For intLoop As Int32 = 0 To tHeight Step htOffset 'process the form height as a series of 256 steps.
If cRed <> 0 Then 'if Red is defined...
tRed = cRed - intLoop \ Intensity 'compute Red intensity.
If tRed < 0 Then
tRed = 0 'if too low, then default to 0.
End If
End If
If cGreen <> 0 Then 'if Green is defined...
tGreen = cGreen - intLoop \ Intensity 'compute green intensity.
If tGreen < 0 Then
tGreen = 0 'if too low, then default to 0.
End If
End If
If cBlue <> 0 Then 'if Blue is defined...
tBlue = cBlue - intLoop \ Intensity 'compute blue intensity.
If tBlue < 0 Then
tBlue = 0 'if too low, then default to 0.
End If
End If
'create pen to draw with
Dim Pn As New System.Drawing.Pen(System.Drawing.Color.FromArgb(tRed, tGreen, tBlue))
For Lp As Int32 = 0 To htOffset - 1 'process vertical list of line rows to define.
eGraphics.DrawLine(Pn, 0, intLoop + Lp, tWidth, intLoop + Lp)
Next
Pn.Dispose() 'release pen resource.
Next
End Sub
End Module

To use it, within our paint event, as already demonstrated, we provide it with the form that we want to
dither the background for, and the e.Graphics object that is supporting the background paint. Further, we
can specify a primary color to dither from, such as Red or Green, and we can also adjust the intensity.
For example:
Dither(Me, e.Graphics, DitherColors.Cyan, 0) 'dither the form background from Cyan down to black
 Page 521

Black Book Tip # 24


Designing Intelligent Context Menus for TextBox and RichTextBox Controls.
With a TextBox on our form, we are able to bring up a default pop-up menu by
right-clicking the TextBox. This default pop-up context menu was added to
the .NET repertoire for all languages because most developers wanted a simple
auto-provided context-menu interface so they would not have to construct a
simple context menu for every TextBox they added to their forms to provide the
user-expected editing services, such as cut, copy, paste, delete, and select all.
The problem seems to be when we want to display our own context menu. I
have seen grumbling on the web regarding this. I feel their pain; I was once
also foiled, because when I threw up a menu in the MouseDown event and displayed a context menu if
the user selected the right-mouse-button, my menu would pop up, but then that darned default menu
would pop up right over the top of it. Even on Microsoft’s own VB Forum, one moderator made the
suggestion that on a right-click that we disable the TextBox, display our custom menu, and then re-enable
the TextBox. It was very amateurish, but it did work. Fortunately, a RichTextBox did not have this issue.
After some quick research, I determined that the culprit was the TextBox’s ContextMenuStrip property
(both a TextBox and a RichTextBox sport a ContextMenu property to support old-style menus, and a
ContextMenuStrip property to support more advanced menu interfaces). Setting the ContextMenuStrip
property to Nothing does exactly that – nothing, but that is simply because this property was already set
to Nothing by default. If it is not Nothing, however, the interface will assume that a custom menu is in
place and so it will not display its own default menu. And therein lays the solution to our dilemma.
We are able to disable the default context menu by assigning a new ContextMenuStrip object there,
whether a ContextMenuStrip of our own design, or even if it is just a blank, empty one. For example:
Me.txtMessage.ContextMenuStrip = New ContextMenuStrip 'disable the default context menu with an empty one.

By doing just that, all frustration with a default context menu popping is
alleviated. And yes, we are still able to toss up our own context menu by
intercepting right-mouse-button-down, if we wish to continue defining on-
the-fly context menus that way.
NOTE: We can also disable the default context menu by setting the TextBox’s
ShortCutsEnabled property to False, But these keyboard shortcuts are handy enough to
keep active, so I would not normally offer that as a solution (refer to
https://msdn.microsoft.com/en-us/library/system.windows.forms.textboxbase.shortcutsenabled(v=vs.110).aspx).

Typically, we just want to throw up a context menu that minimally features Cut, Copy, and Paste. Or,
because we are creating a new context menu, after all, we might as well go all-out and throw in a little
extra functionality to add some snappy pizzazz to our context menus.
However, one thing we should also consider is the benefit of enabling and disabling context menu items
as needs require. What happens if the TextBox is empty? What happens if it is presently tagged
ReadOnly until some Edit mode we had designed into our application is set? What happens of there is no
text selected? We can test for all these things by defining just a few simple fields:
Dim TB As TextBox = Me.txtMessage 'get a simpler reference to the textbox.
Dim HasText As Boolean = CBool(TB.Text.Length) 'True if text is present.
Dim HasSelect As Boolean = CBool(TB.SelectionLength) 'set TRUE if selection exists.
Dim CanRemove As Boolean = Not TB.ReadOnly AndAlso HasSelect 'true if we can remove existing data.
Dim CanPaste As Boolean = Not TB.ReadOnly AndAlso
Clipboard.ContainsText(TextDataFormat.Text) 'set true if the clipboard contains data.

With these few Boolean fields we can enable or disable the above menu options Cut, Copy, Paste, Delete,
Select All, and Select Line. The additional Undo option can be checked by the CanUndo property of the
TextBox itself, and implement UNDO using its own Undo method.
 Page 522

Granted, there are Pinvoke messages already defined that we can invoke in order to perform these
checks by sending a message to the TextBox control and collecting the result. These messages are:
Private Const WM_CUT As Int32 = &H300 'Copy selected data to the clipboard and remove from the textbox.
Private Const WM_COPY As Int32 = &H301 'Copy selected data to the clipboard.
Private Const WM_PASTE As Int32 = &H302 'Paste data from the clipboard.
Private Const EM_UNDO As Int32 = &HC7 'UNDO an edit.
Private Const EM_CANUNDO As Int32 = &HC6 'returns non-zero if data can be undone.
Private Const EM_REPLACESEL As Int32 = &HC2 'replace selected data with new text.

Even so, it is easier to use what I have defined that does not require invoking these Pinvokes directly.
I like to also feature a Select Line option, but this requires that we save the cursor position when the
mouse is pressed down. This way our Select Line support method can easily compute the line clicked
on, and how to select the entire line when the user either chooses the Select Line context menu option, or
if they double-click the text. We will save this position in a Point structure named ContextLocation.
With just that, we have everything we need to construct and display a context menu when we right-click
a TextBox, and enable or disable our several options as needed. And, just to be fancy, we will disable the
default context menu of a TextBox, if needed, and directly within our MouseDown event code where we
test for the right mouse button. Here is the complete code to support on-the-fly context menus on a
TextBox named txtMessage (we will afterward provide additional code to support the extended
functionality of a RichTextBox, though all of the following code will also be used to support it):
Public Class Form1
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private imgList As New ImageList 'image list to store context menu images.
'---------------------------------------------------------------------------------
'image index within ImageList.
'---------------------------------------------------------------------------------
Private Enum Images As Int32 'we will supply in-code edit menu images
Cut 'at the end of this code as a Base64 String.
Copy 'You will see how to create your own in
Paste 'Black Book Tips #49 and #51.
Delete
SelectAll
SelectLine
Undo
Redo
End Enum
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*********************************************************************************
'*********************************************************************************
' Method : Form_Load
' Purpose : Initialize ImageList for use in form.
'*********************************************************************************
'*********************************************************************************
Private Sub Form_Load(sender As Object, e As System.EventArgs) Handles Me.Load
InitializeImageList(Me.imgList) 'load ImageList with images.
End Sub

'*********************************************************************************
' Method : txtMessage_MouseDown
' Purpose : Pop up edit menu for a TextBox. Can be easily adapted for a RichTextBox.
'*********************************************************************************
Private Sub txtMessage_MouseDown(sender As Object, e As MouseEventArgs) Handles txtMessage.MouseDown
If e.Button = Windows.Forms.MouseButtons.Right Then 'if the mouse-down was with the right button...
Dim TB As TextBox = DirectCast(sender, TextBox) 'Use RichTextBox here if txtMessage is an RTB.
If TB.ContextMenuStrip Is Nothing Then 'if the default context menu can be triggered...
TB.ContextMenuStrip = New ContextMenuStrip 'disable the default context menu.
End If
Dim HasText As Boolean = CBool(TB.Text.Length) 'True if text is present.
Dim HasSelect As Boolean = CBool(TB.SelectionLength) 'set TRUE if selection exists.
Dim CanRemove As Boolean = Not TB.ReadOnly AndAlso HasSelect 'true if we can remove existing data.
Dim CanPaste As Boolean = Not TB.ReadOnly AndAlso
Clipboard.ContainsText(TextDataFormat.Text) 'set true if the clipboard contains data
'The ContextMenuStrip.Items.Add() method returns a reference to the added MenuItem that you could capture and use to
'enhance the item even more. Note: the weird spacing is to adjust text spacing between the command and properly
'aligning the text control’s emulated Cntrl-key shortcut.
Dim cMenu As New ContextMenuStrip 'declare a new context menu.
cMenu.Items.Add("Cut (Cntrl-X)", Me.imgList.Images(Images.Cut),
New EventHandler(AddressOf Cut_Click)).Enabled = CanRemove
cMenu.Items.Add("Copy (Cntrl-C)", Me.imgList.Images(Images.Copy),
New EventHandler(AddressOf Copy_Click)).Enabled = HasSelect
cMenu.Items.Add("Paste (Cntrl-V)", Me.imgList.Images(Images.Paste),
New EventHandler(AddressOf Paste_Click)).Enabled = CanPaste
 Page 523

cMenu.Items.Add("Delete (Del)", Me.imgList.Images(Images.Delete),


New EventHandler(AddressOf Delete_Click)).Enabled = CanRemove
cMenu.Items.Add("-")
cMenu.Items.Add("Undo (Cntrl-Z)", Me.imgList.Images(Images.Undo),
New EventHandler(AddressOf Undo_Click)).Enabled = TB.CanUndo
cMenu.Items.Add("-")
cMenu.Items.Add("Select All (Cntrl-A)", Me.imgList.Images(Images.SelectAll),
New EventHandler(AddressOf SelectAll_Click)).Enabled = HasText
cMenu.Items.Add("Select Line (Dbl-Click)", Me.imgList.Images(Images.SelectLine),
New EventHandler(AddressOf SelectLine_Click)).Enabled = HasText
'AdjustContextMenuItemTransparency(cMenu) 'adjust context menu image transparency (See BlackBook Tip # 25).
cMenu.Show(Cursor.Position) 'show the menu with top-left at the mouse pointer location.
End If
End Sub

'*********************************************************************************
' Method : Paste_Click
' Purpose : Support context menu PASTE in a TextBox or RichTextBox.
'*********************************************************************************
Private Sub Paste_Click(ByVal sender As Object, ByVal e As EventArgs)
Me.txtMessage.Paste() 'paste text or over-write selection.
End Sub

'*********************************************************************************
' Method : Cut_Click
' Purpose : Support context menu CUT in a TextBox or RichTextBox. Later RTB code can extend this.
'*********************************************************************************
Private Sub Cut_Click(ByVal sender As Object, ByVal e As EventArgs)
With Me.txtMessage
If CBool(.SelectionLength) Then 'if there is something to Cut...
.Cut() 'cut the selected data to the clipboard.
End If
End With
End Sub

'*********************************************************************************
' Method : Copy_Click
' Purpose : Support context menu COPY in a TextBox or RichTextBox.
'*********************************************************************************
Private Sub Copy_Click(ByVal sender As Object, ByVal e As EventArgs)
With Me.txtMessage
If CBool(.SelectionLength) Then 'if there is something to Copy...
.Copy() 'copy the selected data to the clipboard.
End If
End With
End Sub

'*********************************************************************************
' Method : Delete_Click
' Purpose : Support context menu DELETE in the TextBox or RichTextBox.
'*********************************************************************************
Private Sub Delete_Click(ByVal sender As Object, ByVal e As EventArgs)
Me.txtMessage.SelectedText = vbNullString 'blank out selection.
End Sub

'*********************************************************************************
' Method : Undo_Click
' Purpose : Undo the last edit made in the TextBox or RichTextBox.
'*********************************************************************************
Private Sub Undo_Click(ByVal sender As Object, ByVal e As EventArgs)
Me.txtMessage.Undo() 'UNDO an edit.
End Sub

'*********************************************************************************
' Method : SelectAll_Click
' Purpose : Support context menu SELECT ALL in a TextBox or RichTextBox.
'*********************************************************************************
Private Sub SelectAll_Click(ByVal sender As Object, ByVal e As EventArgs)
With Me.txtMessage
.SelectionStart = 0 'set the selection to the start...
.SelectionLength = .Text.Length 'and select everything.
.Focus() 'make sure focus stays here.
End With
End Sub

'*********************************************************************************
' Method : SelectLine_Click
' Purpose : Select whole line in a TextBox or RichTextBox.
' NOTE : We do double-duty here by also supporting a Double-Click in the text.
'*********************************************************************************
Private Sub SelectLine_Click(sender As Object, e As EventArgs) Handles txtMessage.MouseDoubleClick
With Me.txtMessage
If CBool(.Text.Length) Then
' get cursor location on textbox
Dim ContextLocation As Point = .Parent.PointToClient(Cursor.Position)
 Page 524

' set select location from mouse-down location.


.SelectionStart = .GetCharIndexFromPosition(ContextLocation)
' get the line the cursor location is on.
Dim lineIndex As Int32 = .GetLineFromCharIndex(.SelectionStart)
' get the start of this line.
Dim lineStartingCharIndex As Integer = .GetFirstCharIndexFromLine(lineIndex)
' get the start of next line.
Dim lineNextStartingCharIndex As Integer = .GetFirstCharIndexFromLine(lineIndex + 1)
' if this value is negative...
If lineNextStartingCharIndex < 0 Then
lineNextStartingCharIndex = .Text.Length + 1 'then this was the last line, so fake it.
End If
' compute line length.
Dim lineLength As Integer = lineNextStartingCharIndex - lineStartingCharIndex - 1
.SelectionStart = lineStartingCharIndex 'set select to the start of the clicked line.
.SelectionLength = lineLength 'select entire line.
.Focus() 'make sure focus stays here.
End If
End With
End Sub

'*********************************************************************************
' Method : InitializeImageList
' Purpose : Imitialize a provided ImageList and fill it with locally-created images.
' :
' NOTE : If you want to append the images to an existing list, set the Replace
' : parameter to FALSE.
'*********************************************************************************
Private Sub InitializeImageList(ByRef imgList As ImageList, Optional ByVal Replace As Boolean = True)
If Replace Then 'if we are filling, not appending images.
imgList.Images.Clear() 'initialize image list.
imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
End If
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.
'------------------------
'ImageStrip for 8 Images: Cut, Copy, Paste, Delete, SelectAll, SelectLine, UnDo, ReDo.
'------------------------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAAIAAAAAQCAYAAADeWHeIAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAATQSURBVGhD7ZixTuY4FIX/p6Ci4hVS0dJTUtPlFWipqLdC" &
"SkexGmmKbXgC6iU9FRUdmmLo6LL+rnOSG8d2MghptLsc6Sh/kmvHvudc23AYvvC/xsoA9/f3w93dn+Nd" &
"xMvLy3BzczPeLfHt75/Gru+NDw8Pdk37+BX89f2PiZeXl+PTMg6Hw0S+XwPvifPw7cUUe2I+G10fqRyL" &
"JaRxal/DahYId3t7uxAcU1xdXY13S+QGRHzbdUPTtOOTMrzYsOto0xn7vhvathmur69tPGKKVJiSCUrv" &
"fVtYwt64z4DEC2k0toFe3BT+HbFqt2WC7CwwgSr49fXVxD86OrL7FFR7ii588fHxMYi3bQJEDy0mygAI" &
"3/etXeEQfg99k10RVNWeJZFz5qi180i/85lALAkr4RCyafsFJXIKPU/jzQyjkRSn3yA7CyqYVaAP4iIk" &
"SS8ZQAnrf8TOuWIeDShlm9jRGwDxqfohEPGNXTRAhyG6Q3FLYBypQBpbep/Cvy/F+XeK93h+fra8bYH2" &
"xFFYAnmRASQYeZqFDIU0XnlPXAq19bFqT1+xz+W3QNUAkOX34uJifLNGOun393ebXG6QfFiDEhYGaFtb" &
"bdpwjYziewOcn59buxK8QJ45UQXeC15oIffMt2G+jOvs7MyMUANb2PHxseX26elpfBoFRyBReUJIez/+" &
"5pmvYGHOLatnzO/SBHPfvgizBpDwiEHF1ZLOcm/XcDHaROJSlYKP81yDAostwKqfqj8UeXp6au1qkGCi" &
"F24PvOD+dwmMiZimaTa/hQGIxQT+PGMVb+JENuFG4gO2UuWtboBAt+2aCUJfU7923TAAYP+OVbgcSAoZ" &
"gMp/e3uzaqAKZAAGBtkabJI2yLnPyQCj+JGH6RyCGTlQGm1MsV0NJDjlljApfHyt7cnJyeI7CMuzEtlK" &
"Feu31Sk3YXpMkXs/1Y8awPdF37yvGiA2iI2mDh1TIBLiq/JFGUPQ1sCZkXe4EiDwLD6HvcBQ6TqECjJX" &
"NOQ8wRReDISD/n4v9hoAqH9WAP2lUiLbhOL9X1brXMfKFWLRRCG9gELMu/px7UIfauspLAwQO5f4uYZz" &
"x4IMQNWzp0EOjzKA+vP0BiAp0QCj+KH6WerNGAHEQs4afAPxOZjmziVKLEwFzD0vQfFc/e8SmAPVjAHI" &
"B7HkIEdWVWK51vpMt9AoZBQ5pGMFnvHOdHLGAbntWEgMEAWS2HsNYMtTQvoRqF62gDB/G8zaAKr+MJxx" &
"r2fJB35bwVycSRCfc4k/D0jgklg1IXku5OJyz3wbxkg1U93kg3+clWBb2ob4OZB/0q8DXg68kwn2YtMA" &
"8fQ53gdh90IGUJ+eeQPM4ssAJNavAKourtF4wTQBiOFZSm5OSODbldr7d4r3oLq7MGbGWTMARiaWue2B" &
"BCVd0qGEqNkcWwmdsDLALHZsPd+3dojZC4xjZwNLzJrqn782qH4JPxkgfNNXPss+yRdLBuBdDWrvsbc9" &
"73ysB4IiPNwr7h54QWviC6kJtrCYxST2KLjta+PvyO0BCIu+rI81AQbg738t7SyjCIsBEF/VnqMMIGFq" &
"4nmksV5UWMLeuM9ESMOUy72IOf/ACjCJNgr+O2Gn/SDwFgUE3WuANDYVNifunph/I/4bs/jCBzEM/wCF" &
"o7p5iqxZ5AAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.AddStrip(Img) 'add this as Images 0 – 7.
End Sub

'*********************************************************************************
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object
'*********************************************************************************
Private Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function
End Class

NOTES: The ContextMenu’s Add function above returns an object of type ToolStripItem. Were we to go to the trouble to cast
it to a ToolStripMenuItem, we could then add a hot key to it instead by assigning values to its ShortCutKeys property, such as
“mItem.ShortcutKeys = Keys.Control Or Keys.X”, and avoid pad spacing, but we also do not want to redefine the
editing shortcuts because they are already defined with viable support methods.
 Page 525

Note also that we are building an ImageStrip and storing it in a local ImageList named imgList, as opposed to adding
individual images, as we demonstrated previously, and also as detailed in Black Book Tip # 49, Embedding Images Within
Your Source Code on page 628. An ImageStrip is just a series of uniformly-sized images arranged shoulder-to-shoulder. This
enhancement to Black Book Tip # 49 is detailed in Black Book Tip # 51, Using, Creating, and Embedding ImageStrips with
Ease on page 659.

Notice that we are enabling/disabling menu options as we add them to the context menu. The context
menu’s Items collection’s Add method constructs a ContextMenuItem object from the information we
provide it, and then returns a reference to a ToolStripItem. We can immediately use that returned
reference by applying our enablement options using its properties, as shown above.

Using a RichTextBox
A RichTextBox is just as easy to work with, plus it features a CanRedo property and a Redo method.
Even more, we can set it up to paste plain text if that was saved in the clipboard, RichText Format text if
that us what was saved, or even an image, if that is what was saved in the clipboard. Because it is so
similar to a TextBox, I will list only the MouseDown event, the ReDo_Click method, and the updated
Paste_Click method, because all the other methods are exactly the same:
'*********************************************************************************
' Method : txtMessage_MouseDown
' Purpose : Bring up a context menu for a RichTextBox.
'*********************************************************************************
Private Sub txtMessage_MouseDown(sender As Object, e As MouseEventArgs) Handles txtMessage.MouseDown
Me.ContextLocation = e.Location 'save selection location if this is not a right-click.
If e.Button = Windows.Forms.MouseButtons.Right Then 'if the mouse-down was with the right button...
Dim TB As RichTextBox = DirectCast(sender, RichTextBox) 'get a simpler reference to the textbox.
Dim HasText As Boolean = CBool(TB.Text.Length) 'True if text is present.
Dim HasSelect As Boolean = CBool(TB.SelectionLength) 'set TRUE if selection exists.
Dim CanRemove As Boolean = Not TB.ReadOnly AndAlso HasSelect 'true if we can remove existing data.
Dim CanPaste As Boolean = Not TB.ReadOnly AndAlso
(Clipboard.ContainsText(TextDataFormat.Rtf) OrElse
Clipboard.ContainsText(TextDataFormat.Text) OrElse
Clipboard.ContainsData(DataFormats.Bitmap)) 'set true if the clipboard contains data.
'The ContextMenuStrip.Items.Add() method returns a reference to the added MenuItem that you could capture and use to
'enhance the item even more. Note: the weird spacing is to adjust text spacing between the command and properly
'aligning the text control’s Cntrl-key shortcut.
Dim cMenu As New ContextMenuStrip 'declare a new context menu.
cMenu.Items.Add("Cut (Cntrl-X)", Me.imgList.Images(Images.Cut),
New EventHandler(AddressOf Cut_Click)).Enabled = CanRemove
cMenu.Items.Add("Copy (Cntrl-C)", Me.imgList.Images(Images.Copy),
New EventHandler(AddressOf Copy_Click)).Enabled = HasSelect
cMenu.Items.Add("Paste (Cntrl-V)", Me.imgList.Images(Images.Paste),
New EventHandler(AddressOf Paste_Click)).Enabled = CanPaste
cMenu.Items.Add("Delete (Del)", Me.imgList.Images(Images.Delete),
New EventHandler(AddressOf Delete_Click)).Enabled = CanRemove
cMenu.Items.Add("-")
cMenu.Items.Add("Undo (Cntrl-Z)", Me.imgList.Images(Images.Undo),
New EventHandler(AddressOf Undo_Click)).Enabled = TB.CanUndo
cMenu.Items.Add("Redo (Cntrl-Y)", Me.imgList.Images(Images.Redo),
New EventHandler(AddressOf Redo_Click)).Enabled = TB.CanRedo
cMenu.Items.Add("-")
cMenu.Items.Add("Select All (Cntrl-A)", Me.imgList.Images(Images.SelectAll),
New EventHandler(AddressOf SelectAll_Click)).Enabled = HasText
cMenu.Items.Add("Select Line (Dbl-Click)", Me.imgList.Images(Images.SelectLine),
New EventHandler(AddressOf SelectLine_Click)).Enabled = HasText
'AdjustContextMenuItemTransparency(cMenu) 'adjust context menu image transparency (See BlackBook Tip # 25).
cMenu.Show(Cursor.Position) 'show the menu with top-left at the mouse pointer location.
End If
End Sub

'*********************************************************************************
' Method : Paste_Click
' Purpose : Support context menu PASTE in a RichTextBox.
'*********************************************************************************
Private Sub Paste_Click(ByVal sender As Object, ByVal e As EventArgs)
With Me.txtMessage
If Clipboard.ContainsText(TextDataFormat.Rtf) Then 'if we can paste RTF data...
.Paste(DataFormats.GetFormat(DataFormats.Rtf)) 'then paste it.
ElseIf Clipboard.ContainsText(TextDataFormat.Text) Then 'otherwise, check for text data...
.Paste(DataFormats.GetFormat(DataFormats.Text)) 'we can, so paste it.
ElseIf Clipboard.ContainsData(DataFormats.Bitmap) Then 'or finally, check for an image.
.Paste(DataFormats.GetFormat(DataFormats.Bitmap)) 'and paste it if we have it.
End If
End With
End Sub
 Page 526

'*********************************************************************************
' Method : Redo_Click
' Purpose : Redo the last edit made in the RichTextBox.
'*********************************************************************************
Private Sub Redo_Click(ByVal sender As Object, ByVal e As EventArgs)
Me.txtMessage.Redo()
End Sub

NOTE: If we look to Black Book Tip # 44 on page 610, its PadTextString function would eliminate the padding we inserted
(BTW, adding an actual ShortCutKey of Del is a can of worms I try to avoid, which was why I was padding the text in the
first place). Thus, we could change the above shaded lines using PadTextString to the following:
Dim cMenu As New ContextMenuStrip 'declare a new context menu.
Dim sysFont As Font = SystemFonts.MenuFont 'simplify common reference.
Dim maxWidth As Int32 = TextRenderer.MeasureText("Select Line", sysFont).Width + 32 'define master length + 32-pixel padding.
cMenu.Items.Add(PadTextString("Cut", maxWidth, sysFont) & "(Cntrl-X)", Me.imgList.Images(Images.Cut),
New EventHandler(AddressOf Cut_Click)).Enabled = CanRemove
cMenu.Items.Add(PadTextString("Copy", maxWidth, sysFont) & "(Cntrl-C)", Me.imgList.Images(Images.Copy),
New EventHandler(AddressOf Copy_Click)).Enabled = HasSelect
cMenu.Items.Add(PadTextString("Paste", maxWidth, sysFont) & "(Cntrl-V)", Me.imgList.Images(Images.Paste),
New EventHandler(AddressOf Paste_Click)).Enabled = CanPaste
cMenu.Items.Add(PadTextString("Delete", maxWidth, sysFont) & "(Del)", My.Resources.delete,
New EventHandler(AddressOf Delete_Click)).Enabled = CanRemove
cMenu.Items.Add("-")
cMenu.Items.Add(PadTextString("Undo", maxWidth, sysFont) & "(Cntrl-Z)", Me.imgList.Images(Images.Undo),
New EventHandler(AddressOf Undo_Click)).Enabled = TB.CanUndo
cMenu.Items.Add(PadTextString("Redo", maxWidth, sysFont) & "(Cntrl-Y)", Me.imgList.Images(Images.Redo),
New EventHandler(AddressOf Redo_Click)).Enabled = TB.CanRedo
cMenu.Items.Add("-")
cMenu.Items.Add(PadTextString("Select All", maxWidth, sysFont) & "(Cntrl-A)", My.Resources.SelectAll,
New EventHandler(AddressOf SelectAll_Click)).Enabled = HasText
cMenu.Items.Add(PadTextString("Select Line", maxWidth, sysFont) & "(Dbl-Click)", My.Resources.SelectLine,
New EventHandler(AddressOf SelectLine_Click)).Enabled = HasText
 Page 527

Black Book Tip # 25


Setting Icon Image Transparency on On-the-Fly Context Menus at Runtime.
Sometimes we add images to ContextMenuStrip objects that we cannot assign a border transparency
color to, such as Color.Transparent. This would be typical for those of us who go on the cheap and
might use MS Paint to build our menu images. This is not a big issue, because when we set them to the
Image property of a Main Menu object we can always set its ImageTransparentColor property to the
image’s background color, which in most cases is a non-used color, such as Magenta, typical for most
16-color images, or Fuchsia, a typical 256-color variation of Magenta that is interchangeable with it.
However, to do so for the images assigned to a ToolStripMenuItem, we will have to tinker with its
ImageTransparentColor property. But, if our Context Menu has a lot of such images, this can also turn
out to be a heck of a lot of work.
For example, suppose I had stored a 16-color, 16-bit by 16-bit image file with a Magenta
background color named Clear.png in my Image Resources, which I want to use in one of my
popup Context Menus. Consider the following typical approach to solving this issue:
'*********************************************************************************
' Method : tvSelectRange_MouseDown
' Purpose : Context menu support for our TreeView control tvSelectRange.
'*********************************************************************************
Private Sub tvSelectRange_MouseDown(sender As Object, e As MouseEventArgs) Handles tvSelectRange.MouseDown
If e.Button = Windows.Forms.MouseButtons.Right Then 'right-mouse?
Dim Nd As TreeNode = Me.tvSelectRange.GetNodeAt(e.Location) 'if so, get the TreeView node the mouse is on, if any.
If Nd IsNot Nothing Then 'is there in fact a node there?
Me.tvSelectRange.SelectedNode = Nd 'yes, so ensure that it is selected.
End If
End If
Dim cMenu As New ContextMenuStrip 'we have a valid node, so construct a Context menu.
' add a menu item, and at the same time assign a handler and set its transparency color to Magenta.
cMenu.Items.Add("Undefine the selected Named Range",
My.Resources.Clear,
New EventHandler(AddressOf btnUndefine_Click)).ImageTransparentColor = Color.Magenta
cMenu.Items.Add("-") 'add a line Separator (ToolStripSeperator object).
'
' Add other context menu entries at this location
'
cMenu.Show(Me.tvSelectRange.PointToScreen(e.Location)) 'display using screen coords at cursor.
' Consider replacing the above line with: cMenu.Show(Me.tvSelectRange, e.Location) 'Easier – let it compute coordinates.
' Consider replacing the above line with: cMenu.Show(Cursor.Position) 'Easiest – simply supply screen coords.
End Sub

As we can see, by itself it is not much of an issue. However, it becomes trickier if we may also have to
set other properties, such as menu item enablement. Granted, enablement might first seem useless and
counter-intuitive for a context menu, but we may want the user to become familiar with all options
available through this context menu, though that particular option might not be available under the
current circumstances.
In this case we will need to store it in an object reference and manage it that way. The thing that
sometimes bothers me about this, and something that has driven some new developers to head-banging-
the-wall distraction is that even though we store ToolStripMenuItem objects in the Items collection of a
Context Menu object, its Add method will instead return to us a reference to it as a ToolStripItem object,
which is a narrower, more ancestral definition of our ToolStripMenuItem objects.
Even so, with that in mind, we can access and manipulate the Menu item using something like this:
Dim Item As ToolStripItem = cMenu.Items.Add("Undefine the selected Named Range",
My.Resources.Clear,
New EventHandler(AddressOf btnUndefine_Click))
Item.ImageTransparentColor = Color.Magenta 'set this entry's transparency color.
Item.Enabled = False 'disable the menu entry.
 Page 528

Some developers have the bright idea to loop through a Context Menu and set each
member’s image transparency color before displaying it. However, some express
exasperation because there is more than just ToolStripItems in a menu, such as combo
boxes, text boxes, and separators, which lack this property, and so throw errors at us.
Another typical issue is that the background color of one or more of the images used in a menu might
actually use a different color as their transparency color. This is often dependent on which colors are
required to properly render each image and what unused colors are still available for use as a
transparency color.
Hence, this simple code can start getting rather complicated. For example, the above assignment to a
ToolStripItem object reference fails if the object is actually a ToolStripSeparator.
Assume, at least to start, that all background images will use Magenta for a transparency color (we can
do this even if we do not assign an image to a menu item, in which case this will do nothing at all to the
display of the object). Consider the following function, which will do three fundamental things: 1) it will
ensure that we are manipulating only ToolStripMenuItem objects, 2) we will set the ToolStripMenuItem
object’s ImageTransparentColor property to Magenta, and 3) we will return the number of items in the
context menu. With this last piece of information we can determine if the Context Menu should actually
be displayed or if we should just dispose of its resources. This is useful when we build menus based on
active situations.
'*********************************************************************************
' Method : AdjustContextMenuItemTransparency
' Purpose : Set the ImageTransparentColor property of Centext menu items to Magenta.
'*********************************************************************************
Friend Function AdjustContextMenuItemTransparency(ByRef cMenu As ContextMenuStrip) As Int32
Dim Count As Int32 = cMenu.Items.Count 'get # of context menu strip items.
If Count <> 0 Then 'anything to do?
For Each item As Object In cMenu.Items 'yes. Process each (treat as Object).
If TypeOf item Is ToolStripMenuItem Then 'is this one a ToolStripMenuItem?
DirectCast(item, ToolStripMenuItem).ImageTransparentColor = Color.Magenta 'set ImageTransparentColor to Magenta.
End If
Next 'process the next.
End If
Return Count 'return context menu strip item count.
End Function

To use it, after we construct our context menu, pass it to the above function as a parameter. For example:
If AdjustContextMenuItemTransparency(cMenu) = 0 Then 'anything defined within the context menu?
cMenu.Dispose() 'no, so simply dispose of its resources.
Else
cMenu.Show(Me.tvSelectRange.PointToScreen(e.Location)) 'otherwise, display context menu (disposal by menu processor).
End If

Now consider the very real possibility that these images might have different color backgrounds that we
will want to render as its transparency color, or, shock of shocks, the transparency color might already be
set within and image (its Alpha component is set to Zero, tested for by something like “If (Clr.ToArgb And
&HFF000000) = 0 Then”), which is typical of PNG image files that sport a built-in transparency color. In that
case we must pile on even more code in order to pick up the image’s current background color and if it
is not Transparent, then set that value as its ImageTransparentColor property. Even so, the above code
will work even if the image has a built-in transparency color, because in those cases its actual
transparency color is used, not the color assigned to its ImageTransparentColor property.
However, even then, some images might use the entire image surface, leaving no outer border for a
transparency field. In most cases the border of the image is black, but that is not a guarantee. As such, we
will have to take this into consideration as we encounter these instances. For example, instead of directly
assigning a transparency color, we can pass the Image property of the menu item image to another method,
such as to the following, to figure out transparency:
 Page 529

'*********************************************************************************
' Method Name : SetImageTransparency
' Purpose : Set image transparency color.
'*********************************************************************************
Friend Sub SetImageTransparency(ByRef Img As Image,
Optional ByVal TransparentColor As Color = Nothing)
If Img IsNot Nothing Then 'if a bitmap object was picked up...
With DirectCast(Img, Bitmap) 'treat the image as a bitmap (identical format).
Dim Clr As Color = .GetPixel(.Width - 1, .Height - 1) 'check b-r pixel – most likely candidate for bkgrnd.
'---------------------------------------------------------------
If (Clr.ToArgb And &HFF000000) = 0 Then 'determine if dealing with transparent color.
Clr = Color.Transparent 'Alpha-Blend value zero, so use Color.Transparent.
End If
'---------------------------------------------------------------
If Clr <> Color.Transparent Then 'if background color not already transparent...
If TransparentColor = Nothing Then 'then if user-defined transparency color Nothing...
TransparentColor = Clr 'then use color grabbed from B-R corner of image.
End If
.MakeTransparent(TransparentColor) 'finally, set the desired transparency color.
End If
End With
End If
End Sub

In this example, if no transparency color is provided, the method will pick it up from the image,
assuming its selected corner contains a background color (our example is testing its bottom-right
corner). As such we can invoke the above method from our AdjustContextMenuItemTransparency method
using the line:
SetImageTransparency(DirectCast(item, ToolStripMenuItem).Image, Color.Magenta)

Also in this example, we are still assigning Magenta as a transparency color, but we are also telling it
that if the image contains a transparency color (it will report an Alpha component of zero if so), to ignore
setting it. We can also tell it to use the color defined in its bottom-right pixel, though still respecting any
image with a built-in transparency color by issuing the command:
SetImageTransparency(DirectCast(item, ToolStripMenuItem).Image)

Another thing to consider is a more complex context menu that itself contains additional or even
cascading dropdown menus to several levels. In this case we can take advantage of one of my favorite
tools in my bag of tricks, which is code recursion. Although some developers immediately fall into a
sweaty panic and break out in hives whenever they encounter this term, I use it so much that I seldom
even think about it. It is one of the absolute greatest tools for walking any branching linked structure.
Here, we can redesign our AdjustContextMenuItemTransparency method, dividing it into two separate
methods, where the second method is used for recursion, but still use it as before, such as the following:
'*********************************************************************************
' Method : AdjustContextMenuItemTransparency
' Purpose : Set the ImageTransparentColor property of Centext menu items.
'*********************************************************************************
Friend Function AdjustContextMenuItemTransparency(ByRef cMenu As ContextMenuStrip,
Optional ByVal TransparentColor As Color = Nothing) As Int32
Dim Count As Int32 = cMenu.Items.Count 'get the number of context menu strip items.
If Count <> 0 Then 'anything to do?
For Each item As Object In cMenu.Items 'yes, process each as Object for diff types.
pAdjustMenuItemTransparency(item, TransparentColor) 'process and adjust each menu item.
Next 'process the next.
End If
Return Count 'return the context menu strip item count.
End Function

'*********************************************************************************
' Method : pAdjustMenuItemTransparency
' Purpose : Recursion menu support to set image transparency.
'*********************************************************************************
Private Sub pAdjustMenuItemTransparency(ByRef mnuItem As Object,
Optional ByVal TransparentColor As Color = Nothing)
If TypeOf item Is ToolStripMenuItem Then 'if we can use the object.
With DirectCast(mnuItem, ToolStripMenuItem)
SetImageTransparency(.Image, TransparentColor) 'set its transparency, if required.
If .DropDownItems.Count <> 0 Then 'does it contain dropdown items?
For Each mnu As Object In .DropDownItems 'yes, so process each item in the dropdown list.
pAdjustMenuItemTransparency(mnu, TransparentColor) 'recurse for however many levels required.
Next
 Page 530

End If
End With
End If
End Sub

With the above, we would still invoke AdjustContextMenuItemTransparency just as we did before, but it will
also take into consideration the possibility of dropdown menus, if they exist, and also, as we might note,
it will allow us to specify a transparency color, if we require it (if we do not supply a color, it will try to
determine one by sampling the image). Otherwise, we will notice no difference in operation.

BONUS TIP # 1: We can take advantage of the above pAdjustMenuItemTransparency method for our main menus as well. This
will totally eliminate the usual task of manually going through our menus and setting the ImageTransparentColor properties
on any entries that have different background colors that are also to be used as transparency keys. Within our Form’s Load
event, just pass our MenuStrip object to the following method:
'*********************************************************************************
' Method : AdjustMainMenuItemTransparency
' Purpose : Set the ImageTransparentColor property of main menu items.
'*********************************************************************************
Friend Function AdjustMainMenuItemTransparency(ByRef mMenu As MenuStrip,
Optional ByVal TransparentColor As Color = Nothing) As Int32
Dim Count As Int32 = mMenu.Items.Count 'get the number of main menu items.
If Count <> 0 Then 'anything to do?
For Each item As Object In mMenu.Items 'yes, so process each item.
pAdjustMenuItemTransparency(item, TransparentColor) 'process and adjust each menu item.
Next 'process the next.
End If
Return Count 'return the menu strip item count.
End Function

BONUS TIP # 2: In the previous example code, we saw a point that was assigned a translation from client coordinates (mouse
coordinates relative to a ListView control) to screen coordinates. Awareness of screen coordinates in our code can be very
important. The reason why we used the PointToScreen method of the Treeview object (most controls also feature this method) is
because context menus are positioned using Screen Coordinates, not relative to the object the mouse cursor position is located over
(Client Coordinates). Some objects, such as forms, context menus, and the Cursor object, do indeed use screen coordinates. For
controls themselves, their MouseEventArgs parameter, if they feature one, always provide Client Coordinates, which are relative to
the object the mouse event is associated with. Hence, for a TreeView control, the cursor position is relative to its top-left corner,
which the MouseEventArgs parameter would report as being coordinate X=0, Y=0. However, we might notice that if we toss up a
context menu based upon those mouse coordinates, it appears off to the upper-left of the mouse pointer because it expects screen
coordinates. Thus, we often figure we must compute several offsets, such as the form’s X and Y locations, the width and height of the
form’s border and banner, the location of the control the TreeView is parented by, and so on. Grrr...
Being a professional C/C++ developer (who finds himself spending more and more time in the blazing Rapid Application
Development (RAD) speed of VB.NET software design), I have developed mountains of C/C++ code under the Microsoft
Foundation Class (MFC), and thus use Pinvokes at a torrential pace. One of those gems was a tiny method that converted
client coordinates to screen coordinates, named ClientToScreen (there is also another one that converts screen coordinates
to client coordinates, named ScreenToClient). It took a simple Point structure as a parameter, storing local-relative X and Y
pixel offsets, and on return it held the screen-relative offsets.
However, .NET users have it easy because they do not need to resort to calling up a Pinvoke. Screen area-consuming controls
feature a PointToScreen and PointToClient method. Both of these methods return a Point object that is a translation of the
Point object provided to it. The PointToScreen method takes the provided Point structure, which we assume contains Client
coordinates, and it returns a Point structure containing Screen coordinates. The PointToClient method, on the other hand,
provided a Point structure that we assume contains screen coordinates, and returns a Point structure containing client
coordinates.
Typically, we tend to use the PointToScreen method the most, especially if we display lots of context menus. So in our case,
using the tvSelectRange TreeView control, we would display its context menu, cMenu, at the cursor position using the
command “cMenu.Show(Me.tvSelectRange.PointToScreen(e.Location))”.

HOT TIP: Did you know that we can in fact provide context menus with local, client coordinates if we also provide it with the
control so that it can compute the screen coordinates for us, such as “ cMenu.Show(Me.tvSelectRange, e.Location)”.
 Page 531

HOT TIP: But we are still not through. Have you thought about my points stated above about displaying the context menu
at the cursor position and the fact that I also stated that the Cursor object reports Screen coordinates? The MouseEventArgs
parameter is simply a translation of the Cursor’s screen-relative coordinates that have been conveniently converted to
control-local coordinates. What this means is that we can forgo any translation at all and simply use
“cMenu.Show(Cursor.Position)”. Indeed, if we replace our computed position in the above examples with this, it will work
exactly as before, and also cuts considerable internal code out of our application. I use it so much that I seldom ever bother
using anything else.

HOT TIP: All that being said, there is a situation where we might need to translate coordinates. Consider that we have a
program that uses a large PictureBox surface to draw a user’s doodles or to format a matrix. Suppose we have a custom
panel that displays comments over certain known locations, such as drawn objects. Because we want to remove this comment
panel when the cursor leaves the drawing surface, we implement the PictureBox’s MouseLeave event to detect when the
cursor leaves the PictureBox so we can remove the comment panel.
But what happens if the user moves the mouse over onto the comment panel? The PictureBox’s MouseLeave event will fire
because the mouse has in fact moved off the PictureBox surface and is now over the comment panel’s surface. So, what
happens is the comment panel will be dutifully removed. Oops.
How do we avoid that? Easy. In our PictureBox’s MouseLeave event, we simply translate the Cursor object’s position to
coordinates relative to the PictureBox and then check to see if the PictureBox’s client rectangle still consumes the cursor’s
coordinate. If so, we do not bother removing the comment panel, like so:
Friend Sub picViewport_MouseLeave(sender As Object, e As EventArgs) 'Handles are added via AddHandler as Viewports are added.
If Me.pnlComment.Visible Then 'if the comment panel is presently visible...
With DirectCast(sender, PictureBox) 'then check the current viewport...
If .Bounds.Contains(.PointToClient(Cursor.Position)) Then 'if cursor is still within the bounds of the viewport...
Return 'then DO NOT remove the comment panel.
End If
End With
CloseplnComment() 'otherwise, close the comment panel.
End If
End Sub
 Page 532

Black Book Tip # 26


Adding Background Transparency to Menu and Toolstrip Images.
Images embedded into ToolStrips, MenuStrips, and ContextMenuStrip
really help sell the functionality of a button, a dropdown button, a split
dropdown button, a menu, or dropdown menu option, along with adding
tool tips to our menu items to visually explain their functionality.
But one thing that detracts from this is if the embedded image has
obvious box-framing around it, especially when it is highlighted. It just
looks less blended into our toolbar or menu when we can see a little
square box behind it. Grrr... If its background was a transparency color,
such as I get when I take the time to edit my images as PNG images with
Transparency, then this is not an issue and its background color adjusts
with the highlighting seamlessly, so we never see it. But if we are
programming on a shoestring, or in a hurry, resources can get pretty tight,
where we may need to hijack images from actual icons that we force to image format by assigning them
with the “.ToBitmap” tag appended to the resource (e.g., “My.Resources.Open.ToBitmap”).
To address this issue, I designed helper methods to parse a MenuStrip, a ToolStrip, dropdown buttons,
menu dropdown items, and even individual images stored anywhere, such as on a form or control. I had
already touched on some of these in Black Book Tip # 25 (see page 479), but this solution is more
universal. Like with the previous solution, with this solution we can also create images that use a non-
used color as a background color, such as Magenta or Fuchsia, and these methods will render those
backgrounds transparent even if we may have selected a different color for various images.
The five methods that we can use to roll through these controls are:
AdjustToolStripTransparency(ToolStripReference)
This method will set the ImageTransparentColor property of all members of a Toolstrip, to include any and all
dropdown members.
AdjustContextMenuItemTransparency(ContextMenuReference) (this method was also shown in
Black Book Tip # 25)
This method will set the ImageTransparentColor property of CentextMenuStrip tree, to include any and all dropdown
members.
AdjustMainMenuItemTransparency(MenuStripReference) (this method was also shown in Black
Book Tip # 25)
This method will set the ImageTransparentColor property of MenuStrip tree and any and all of its dropdown
members.
pAdjustMenuItemTransparency(MenuStripItemReference)
This method is used for Recursion menu support to set image transparency. This method is normally invoked by the
above AdjustToolStripTransparency, AdjustContextMenuItemTransparency, and AdjustMainMenuItemTransparency
methods, but we can invoke it directly by providing it with a ToolStripMenuItem, ToolStripButton, or
ToolStripDropDownButton that may have dropdown lists attached to them. We can also provide it with the
MenustripItem from a ToolStrip, MenuStrip or ContextMenuStrip when we have changed its image and want to
adjusts the new image’s background transparency, though I tend to speed things up by selecting the method below.
SetImageTransparency(ImageReference) (this method was also shown in Black Book Tip # 25)
This method directly sets an image transparency color. This is normally invoked by the above
pAdjustMenuItemTransparency method, but it can also be invoked directly on an image. This is useful when we have
an image in a ToolStrip, MenuStrip, or DropDown menu. This way we can readjust its background transparency after
we have assigned a new image to it.

The first three methods roll through their primary objects, a ToolStrip, a ContextMenuStrip, or a
MainMenuStrip, and invoke the pAdjustMenuItemTransparency method on each member in its primary
Items collection, such as a ToolStripMenuItem, a ToolStripButton, a ToolStripDropDownButton, or a
ToolStripSplitButton. Others, such as ToolStripLabels and ToolStripSeparators, are ignored. Of these
 Page 533

three main types, a ToolStripDropDownButton, a ToolStripSplitButton, and a ToolStripMenuItem are


capable of having a DropDown collection. For example, as previously demonstrated in Black Book Tip #
25, after we have constructed a ContextMenuStrip object named cMenu, we would pass cMenu using
AdjustContextMenuItemTransparency(cMenu) just prior to displaying that pop-up context menu.

The pAdjustMenuItemTransparency method is invoked by each primary method, which is fully recursive
and can walk as many branches as the menu tree holds. When an Image property is found, a reference to
it is passed to the SetImageTransparency method. The pAdjustMenuItemTransparency method might do all the
heavy lifting, but the SetImageTransparency method does all the fine detail work. The first thing it does is
cast the Image object to a Bitmap (Bitmaps and Images have Identical structure, and are
interchangable, but are named differently due to their names making better sense in their use context).
This method then grabs the pixel from its bottom-right corner, which is the location most likely (like,
99.99% of the time) to have a background pixel. Indeed, I specifically design my images to ensure the
bottom-right corner only has the background color, even if it is just that single pixel in the very corner.
Next, it checks if the color is already a transparency color (if its ARGB value, AA RR GG BB, for Alpha
component, Red, Green, Blue, the Alpha 2-digit hexadecimal value (&H00 - &HFF) is zero, or fully
transparent). I have enhanced this version to also check for Black (the least likely background color).
Next, it checks to see if the user supplied a default transparency color, as may be needed if their image
draws to the bottom-right corner in a color other than black, so a transparency key can accurately be
assigned, though this is very rare, and I have only had to do this twice in a 3-year period.
It then checks to see if the color value it picked up is not already defined as being transparent or black. If
it is not, it then checks to see if the invoker supplied a Transparency color. If they did not, then the color
picked up from the image background is used as the transparency color.
What follows is my modAdjustTransparency module, which will provide all these services.
Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modAdjustTransparency Static Class Module
' This module is used to clean up image background transparency on ToolStrips,
' ContextMenuStrips, MenuStrips, and even individual images.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modAdjustTransparency
'*********************************************************************************
' Method Name : AdjustToolStripTransparency
' Purpose : Set the ImageTransparentColor property of a Toolstrip.
' :
' Parameters : mToolstrip = Toolstrip to parse.
' : TransparentColor = optional user-provided color value to assign
' : as transparency color for the image. This is
' : useful if the user used a primitive image editor
' : to create image icons with solid-color backgrounds.
'*********************************************************************************
Friend Function AdjustToolStripTransparency(ByRef mToolstrip As ToolStrip,
Optional ByVal TransparentColor As Color = Nothing) As Int32
Dim Count As Int32 = mToolstrip.Items.Count 'get the number of main menu items.
If Count <> 0 Then 'anything to do?
For Each item As Object In mToolstrip.Items 'yes, so process each item.
pAdjustMenuItemTransparency(item, TransparentColor) 'process and adjust each menu item.
Next 'process the next.
End If
Return Count 'return the menu strip item count.
End Function

'*********************************************************************************
' Method Name : AdjustContextMenuItemTransparency
' Purpose : Set the ImageTransparentColor property of Context menu items.
' :
' Parameters : cMenu = Context Menu to parse
' : TransparentColor = optional user-provided color value to assign
' : as transparency color for the image. This is
' : useful if the user used a primitive image editor
' : to create image icons with solid-color backgrounds.
'*********************************************************************************
 Page 534

Friend Function AdjustContextMenuItemTransparency(ByRef cMenu As ContextMenuStrip,


Optional ByVal TransparentColor As Color = Nothing) As Int32
Dim Count As Int32 = cMenu.Items.Count 'get the number of context menu strip items.
If Count <> 0 Then 'anything to do?
For Each item As Object In cMenu.Items 'yes, process each as object for diff types.
pAdjustMenuItemTransparency(item, TransparentColor) 'process and adjust each menu item.
Next 'process the next.
End If
Return Count 'return the context menu strip item count.
End Function

'*********************************************************************************
' Method Name : AdjustMainMenuItemTransparency
' Purpose : Set the ImageTransparentColor property of main menu items.
' :
' Parameters : mMenu = Main Menu to parse (this will in turn invoke pAdjustMenuItemTransparency(), below).
' : TransparentColor = optional user-provided color value to assign
' : as transparency color for the image. This is
' : useful if the user used a primitive image editor
' : to create image icons with solid-color backgrounds.
'*********************************************************************************
Friend Function AdjustMainMenuItemTransparency(ByRef mMenu As MenuStrip,
Optional ByVal TransparentColor As Color = Nothing) As Int32
Dim Count As Int32 = mMenu.Items.Count 'get the number of main menu items.
If Count <> 0 Then 'anything to do?
For Each item As Object In mMenu.Items 'yes, so process each item.
pAdjustMenuItemTransparency(item, TransparentColor) 'process and adjust each menu item.
Next 'process the next.
End If
Return Count 'return the menu strip item count.
End Function

'*********************************************************************************
' Method Name : pAdjustMenuItemTransparency
' Purpose : Recursion menu support to set image transparency. This method is normally invoked
' : by the above AdjustToolStripTransparency(), AdjustContextMenuItemTransparency(),
' : and AdjustMainMenuItemTransparency() methods, but you can invoke it directly by
' : providing it with a ToolStripMenuItem, ToolStripButton, or ToolStripDropDownButton
' : that may have dropdown lists attached to them. You can also provide it with the
' : MenustripItem from a ToolStrip, MenuStrip or ContextMenuStrip when you have changed
' : its image and want to adjust the image background transparency.
' :
' Parameters : mnuItem = Menu Item to start scanning from
' : TransparentColor = optional user-provided color value to assign
' : as transparency color for the image. This is
' : useful if the user used a primitive image editor
' : to create image icons with solid-color backgrounds.
'*********************************************************************************
Private Sub pAdjustMenuItemTransparency(ByRef mnuItem As Object, Optional ByVal TransparentColor As Color = Nothing)
Select Case mnuItem.GetType.Name
Case "ToolStripMenuItem"
With DirectCast(mnuItem, ToolStripMenuItem)
SetImageTransparency(.Image, TransparentColor) 'set its transparency, if required.
If .DropDownItems.Count <> 0 Then 'does it contain dropdown items?
For Each mnu As Object In .DropDownItems 'yes, so process each item in the dropdown list.
pAdjustMenuItemTransparency(mnu, TransparentColor) 'recurse for however many levels required.
Next
End If
End With
Case "ToolStripButton" 'regular button on a toolstrip.
With DirectCast(mnuItem, ToolStripButton)
SetImageTransparency(.Image, TransparentColor) 'set its transparency, if required.
End With
Case "ToolStripDropDownButton" 'dropdown button on a toolstrip.
With DirectCast(mnuItem, ToolStripDropDownButton)
SetImageTransparency(.Image, TransparentColor) 'set its transparency, if required.
If .DropDownItems.Count <> 0 Then 'does it contain dropdown items?
For Each mnu As Object In .DropDownItems 'yes, so process each item in the dropdown list.
pAdjustMenuItemTransparency(mnu, TransparentColor) 'recurse for however many levels required.
Next
End If
End With
Case "ToolStripSplitButton" 'a split dropdown button on a toolstrip.
With DirectCast(mnuItem, ToolStripSplitButton)
SetImageTransparency(.Image, TransparentColor) 'set its transparency, if required.
If .DropDownItems.Count <> 0 Then 'does it contain dropdown items?
For Each mnu As Object In .DropDownItems 'yes, so process each item in the dropdown list.
pAdjustMenuItemTransparency(mnu, TransparentColor) 'recurse for however many levels required.
Next
End If
End With
End Select
End Sub

'*******************************************************************************
 Page 535

' Method Name : SetImageTransparency


' Purpose : Directly sets an image transparency color. This is normally invoked
' : by the above pAdjustMenuItemTransparency() method, but it can also
' : be invoked directly on an image. This is useful when you have an image
' : in a ToolStrip or MenuStrip or DropDown menu. This way you can readjust
' : its background transparency after you have assigned the new image.
' :
' Parameters : Img = Image object to adjust transparency on
' : TransparentColor = optional user-provided color value to assign
' : as transparency color for the image. This is
' : useful if the user used icons that draw to
' : the bottom-right corner of an image.
' NOTE : This method is used by all of the above, but you can also invoke it
' : directly if you have, for example, changed an image assigned to a
' : toolstrip, menustrip, or ToolStripDropDownButton.
'*******************************************************************************
Friend Sub SetImageTransparency(ByRef Img As Image,
Optional ByVal TransparentColor As Color = Nothing)
If Img IsNot Nothing Then 'if a bitmap object was picked up...
With DirectCast(Img, Bitmap) 'treat the image as a bitmap (identical format).
Dim Clr As Color = .GetPixel(.Width - 1, .Height - 1) 'check B-R pixel – most likely background.
'---------------------------------------------------------------
Dim iClr As Int32 = Clr.ToArgb 'grab integer version of color.
If (iClr And &HFF000000) <> 0 AndAlso (iClr And &HFFFFFF) <> 0 Then 'determine if not Transparent or Black.
If TransparentColor = Nothing Then 'not, so if user-defined color is Nothing...
TransparentColor = Clr 'then use color from B-R corner.
End If
.MakeTransparent(TransparentColor) 'finally, set the desired transparency color.
End If
End With
End If
End Sub
End Module
 Page 536

Black Book Tip # 27


Easily Do a Redim Preserve With Leftward Bounds of Multi-dimensioned Arrays.
We often redimension arrays to add additional data to them dynamically as we gather it. However, the
.NET Cross-Language compiler is going to smack us in the face with a warning if we redimension a
leftward dimension and also try to preserve its data. For example, if we have a 2D integer array declared
“Dim MyData(10, 2) As Int32” and we try to do this: “ReDim Preserve MyData(20, 2)”, we get this:

We are informed that Redim can only


change the rightmost dimension. Grrr...
Interestingly, this took me completely
by surprise. Why? Because first, I
normally redimension arrays without
needing to preserve its old data, and
second, I very seldom did it, because I
almost obsessively use linked lists to store the objects I need as data within them. Now that I have done
my Obama impersonation by blaming anything but me for my oversight, I will make the excuse that I
have designed a number of languages and have always incorporated full and unrestricted multi-
dimensional redimensioning in them, such as my VB Interpreter in 1984, and my C compiler in 1995.
Funny thing, but I did it because it never occurred to me that this was declared impossible. For example,
when I wrote the ReDim function in my tokenized VB interpreter in 1984, I automatically assumed the
existing data should be preserved, and I went about redimensioning multi-dimensional arrays without
even pausing. In my 1995 C Compiler, when I added a Redim method to it, I assumed this functionality
was expected by default; never realizing that the vast hoard of (self-proclaimed) software development
gurus were declaring that I could not do what I was doing, and with ease.
But my philosophy for the past 40-odd years of my working career has always been, “I can program
anything, but if it is impossible, it will take me a couple more hours.”
So why does Microsoft think this is so impossible? Well, let us take a look at how they process array
information. Microsoft, like everyone else, stores the array table in the same memory space as it does its
array data. In fact, for scalar arrays (numeric data), this data is the array table. This is thanks to the fact
that these items are of identical scaling, such as an Int32 (Integer) always using exactly 32-bits, or 4
bytes of memory. An Int64 (Long) always uses 64 bits, or 8 bytes of memory. Thus, we can easily stack
scalar values neatly and uniformly on top of each other, one after the other. This is also the technique
that the procedural language C used, which was in turn borrowed from its predecessor language, B. The
neat thing about this layout in C was that we could declare it as a 2D array, such as Int myData[11, 2]
(VB.NET myData(10,1)), but because it was laid out uniformly and sequentially in memory, we could also
access it as one long single-dimensioned array if we needed to, such as using “ Int Tmp = myArray[13]”.
Internal bookkeeping indexes that were associated with the myData variable, used by the compiler (set
during the array declaration), simply told it how wide and long the data was in the 2D array. To make
this short story a little longer, it is a fact that 99% of the time developers dimension their arrays just once
(I will not count declaring an empty array, such as “ Dim myData(,) As Int32 = Nothing”), but I do not
understand how the redimensioning of preserved data in multi-dimensional arrays is supposed to be such
a difficult task. The fact that they allow data preservation during the redimensioning of the rightmost
dimension has me really confused, because that simple task is actually the lion’s share of the work when
one allows FULL redimensioning of an Array.
As an example, assume we have made the following 2D array declaration:
Dim MyData(,) As Int32 = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {9, 10, 11}, {12, 13, 14}, {15, 16, 17}}
 Page 537

This declares myData to be an array dimensioned to 5,2, which consists of 6 groups of 3 elements each.
I declared it this way so that we can see how the data is stored in physical memory, which starts with the
0, then 1, and so on to 17. An internal index associated with the myData variable informs the compiler
that the left dimension is 5 (internally, this is actually 6), and a secondary index informs it that the right
parameter is 2 (internally, this is 3). Using the second index, a compiler can compute an array index in a
snap. If the user accessed myData(4,1), it multiplies the left dimension times the width of the right
dimension plus the desired right-dimension offset; 5 * 3 + 2 (or (4+1) * (2+1) + (1+1) in VB;
remembering to add 1 to each dimension due to VB6 hobbyists during VB.NET Beta development
whining so resoundingly loud because they were too wiped out by medical marijuana or something to be
able to cognate beyond simplistic upper bounds, as opposed to thinking more logically in terms of
setting aside X number of objects and then applying them in zero-based terms, which requires much less
memory and computer cycles to process). This results in the 18th element, or offset 17 in zero-based
terms, which, interestingly enough, is our stored integer value of 17.
Microsoft copies the entire array to another allocated location in storage space that is initially zeroed and
scaled to the required expanded size, thus very quickly satisfying an expanded rightward dimension. The
old space is then released. But, for some reason they cannot seem to be able to redimension any of the
leftward bounds. In my C compiler, for example, I was slightly divergent from this extremely fast and
dirty solution (this tried and true technique is almost as old as computer programming itself). I likewise
computed the needed space and allocated it in new memory space. I then very quickly rebuilt the entire
array, using the old pointer into the old space to copy the rightward dimensioned data. I knew how many
bytes each rightward dimension occupied (rightward_dim_size * scalar_byte_size), similarly computing
the rightward size of the new dimension, and copied the old data for the old size into the new space in
these groupings, bump the new space pointer to the base of its next grouping, and repeat until I was
done. All of this is a very easy thing to do. I was actually surprised that Microsoft had not adopted this
technique, because the code to do all this is quite small and what they were doing only required just a
tiny amount of additional work to completely support fully redimensioned arrays. Even the new .NET
language I am currently developing does this (but then, it also does a lot of things that are supposed to
be impossible. Guess I am just a rebel at heart. It is a lonely life thinking outside the “box” where
everyone operates in robotic synchronicity, like those drunk people who think line dancing looks cool).
But all this blather is academic. We are probably more interested in how to get around this problem
within our own code. Even from the VB-side, it is still very easy to rebuild these arrays. We simply
reconstruct the multi-dimensioned array and then assign the new array over to the original array pointer.
Remember, arrays in VB.NET are objects. As such, a variable that is declared as an array is really just
a pointer to the actual array data. Therefore, we can construct a broader array, copy the original array’s
data to it, and then assign that new array directly to the original array pointer. There is nothing tricky
being done here. It is just really, really simple stuff.
Suppose that we want to bump the base array size of a 2D array named myData by 10, thus resizing this
Integer 2D array of myData(10,2) up to myData(20,2). All I would have to do is the following:
'Redimension myData(,) to have a left dimension 10 higher than present.
Dim LSize As Int32 = UBound(myData, 1) 'grab rank 1 dimension (default); the left dimension.
Dim RSize As Int32 = UBound(myData, 2) 'grab rank 2 dimension; the right dimension.
Dim Tmp(LSize + 10, RSize) As Int32 'dimension temporary 2D array Tmp to desired new size for myData.
For Idx As Int32 = 0 To LSize 'process left dimensioning of myData.
For Idy As Int32 = 0 To RSize 'process right dimensioning of myData.
Tmp(Idx, Idy) = myData(Idx, Idy) 'copy old existing data from myData to the new Tmp array in matching elements.
Next 'process next right element.
Next 'process next left element.
Erase myData 'release myData's original content to the garbage collector.
myData = Tmp 'point the original 2D array pointer to the new data
Tmp = Nothing 'break Tmp's connection to the new array data (the data itse.lf is not affected).
 Page 538

As we can see, the way I solved it was to create a new 2D array Named Tmp, dimension it to the new
requirement, then copied the data from the original 2D array to the Tmp array. I then erase the original
array to release its resources, and then assign Tmp to the original array. Copying arrays between variable
in VB.NET simply copies the pointer to the Array, and so the original variable now also points to Tmp's
data, and thus the original 2D array is redimensioned without any fuss, muss, or Aces up our sleeves.
If we do a lot of this, we may want to construct helper methods to resize the array as needed. For
example, overload methods that support both 2D or 3D Integer arrays, and redimensioning of any
dimension, to include shrinking them, could be declared much like the following functions:
'*********************************************************************************
' Method : RedimInt32Array
' Purpose : Redimension a 2D or 3D Integer Array to a new size while preserving current data in it.
'
' Parameters : userArray() – 2D or 3D Integer Array to resize (see the 3D overloaded version in the next function).
' : New1Dim ------- New size for first dim. Set to -1 if no change.
' : New2Dim ------- New size for second dim. Set to -1 if no change.
' : New3Dim ------- New size for third dim (see next function). Set to -1 if no change (used in 3D version).
'
' Returns : True if successful. False usually indicates negative parameters when an
' : array is not dimensioned.
'*********************************************************************************
Friend Function RedimInt32Array(ByRef userArray(,) As Int32,
Optional ByVal New1Dim As Int32 = -1,
Optional ByVal New2Dim As Int32 = -1) As Boolean
If userArray Is Nothing Then 'if the array does not exist...
Return False 'do nothing and report failure.
ElseIf New1Dim < 0 AndAlso
New2Dim < 0 Then 'if both user-defined sizes are not defined...
Return True 'then Nothing to do, but no harm, so assume success.
End If

Dim Size1 As Int32 = UBound(userArray, 1) 'get the current upper bounds of the user array.
Dim Size2 As Int32 = UBound(userArray, 2) 'get the current second dimension.
If New1Dim < 0 Then 'if the user did not provide a new first dimension...
New1Dim = Size1 'then absorb the current first dimension.
End If
If New2Dim < 0 Then 'if the user did not provide a new second dimension...
New2Dim = Size2 'then absorb the current second dimension.
End If 'one or the other of these might be < 0, but not both.
'-------------------------------------------------------
If New1Dim = Size1 AndAlso
New2Dim = Size2 Then 'if both dimensions already match...
Return True 'then nothing changed, so nothing to do. Return success.
End If
'-------------------------------------------------------
If New1Dim < Size1 Then
Size1 = New1Dim 'adjust in case user is shrinking the first dimension.
End If
If New2Dim < Size2 Then
Size2 = New2Dim 'adjust in case user is shrinking the second dimension.
End If
'-------------------------------------------------------
Dim Tmp(New1Dim, New2Dim) As Int32 'dimension temporary 2D array to new size.
'-------------------------------------------------------
For Idx As Int32 = 0 To Size1 'process first dimension.
For Idy As Int32 = 0 To Size2 'process second dimension.
Tmp(Idx, Idy) = userArray(Idx, Idy) 'copy data.
Next 'process next second dimension element.
Next 'process next first dimension element.
Erase userArray 'release the original data to the garbage collector.
userArray = Tmp 'point the original 2D array pointer to the new data.
Return True 'return success.
End Function
'-------------------------------------------------------
'-------------------------------------------------------
'version of RedimInt32Array that supports 3D arrays
Friend Function RedimInt32Array(ByRef userArray(,,) As Int32,
Optional ByVal New1Dim As Int32 = -1,
Optional ByVal New2Dim As Int32 = -1,
Optional ByVal New3Dim As Int32 = -1) As Boolean
If userArray Is Nothing Then 'if the array does not exist...
Return False 'do nothing and report failure.
ElseIf New1Dim < 0 AndAlso
New2Dim < 0 AndAlso
New3Dim < 0 Then 'if all 3 user-defined sizes are not defined...
Return True 'then Nothing to do, but no harm, so assume success.
End If

Dim Size1 As Int32 = UBound(userArray, 1) 'get the current upper bounds of the user array.
 Page 539

Dim Size2 As Int32 = UBound(userArray, 2) 'get the current second dimension.


Dim Size3 As Int32 = UBound(userArray, 3) 'get the current third dimension.
If New1Dim < 0 Then 'if the user did not provide a new first dimension...
New1Dim = Size1 'then absorb the current first dimension.
End If
If New2Dim < 0 Then 'if the user did not provide a new second dimension...
New2Dim = Size2 'then absorb the current second dimension.
End If
If New3Dim < 0 Then 'if the user did not provide a new third dimension...
New3Dim = Size3 'then absorb the current third dimension.
End If 'one of these might be < 0, but not both.
'-------------------------------------------------------
If New1Dim = Size1 AndAlso
New2Dim = Size2 AndAlso
New3Dim = Size3 Then 'if all dimensions already match...
Return True 'then nothing changed, so nothing to do. Return success.
End If
'-------------------------------------------------------
If New1Dim < Size1 Then
Size1 = New1Dim 'adjust in case user is shrinking the first dimension.
End If
If New2Dim < Size2 Then
Size2 = New2Dim 'adjust in case user is shrinking the second dimension.
End If
If New3Dim < Size3 Then
Size3 = New3Dim 'adjust in case user is shrinking the third dimension.
End If
'-------------------------------------------------------
Dim Tmp(New1Dim, New2Dim, New3Dim) As Int32 'dimension temporary 3D array to new size.
'-------------------------------------------------------
For Idx As Int32 = 0 To Size1 'process first dimension.
For Idy As Int32 = 0 To Size2 'process second dimension.
For Idz As Int32 = 0 To Size3 'process third dimension.
Tmp(Idx, Idy, Idz) = userArray(Idx, Idy, Idz) 'copy data
Next 'process next third dimension element.
Next 'process next second dimension element.
Next 'process next first dimension element.
Erase userArray 'release the original data to the garbage collector.
userArray = Tmp 'point the original 3D array pointer to the new data.
Return True 'return success.
End Function

To use the above, consider these rough examples:


Dim MyData(,) As Int32 = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}, {9, 10, 11}, {12, 13, 14}, {15, 16, 17}}
Dim Bol As Boolean = RedimInt32Array(MyData, 20, -1) 'expand left dim, keep right the same,
Bol = RedimInt32Array(MyData, -1, -1) 'no actual change,
Bol = RedimInt32Array(MyData, 20, 3) 'expand right dim by 1,
Bol = RedimInt32Array(MyData, 10, 2) 'shrink both dimensions,

Adapting this method for other types, such as String or Doubles is easy and straightforward. Just change
the storage types of the Int32Array and Tmp variables (a name change from Int32Array to reflect our
data type might also be nice). Enhancing it to process 3D arrays is also very easy. Just add a Size3
integer to store the extra dimension, and an Idz loop control to loop through a third dimension, plus an
optional New3Dim function parameter.
To adapt the above methods for Doubles or Strings, simply rename the twin methods appropriately, such
as RedimDblArray or RedimStrArray, and change the storage types defined in the above highlighted
areas from Int32 to either Double or String, as required.
 Page 540

Black Book Tip # 28


Implementing a C-Style Bit Field Class in VB.NET.
Under C and C++, we can declare a thing called a Bit Field, which is sadly lacking in C# and VB.
A Bit Field was often used to pack data very tightly, especially on old Unix systems where memory was
more scarce than target-practice volunteers in medieval times. And back in their youth, memory had an
extremely steep price, where 128K would cost in the thousands of dollars. Even in the hobbyist world, I
remember upgrading a computer from 4K (that’s Kilobytes, kids, or 4 x 1024 bytes; 4096) of memory
and paying Radio Shack $127 so I could rip out those 8 wimpy chips and plug in a massive 16K of
memory, giving me tons of breathing space (well, until I went on a memory gluttony binge by expanding
to a whopping 48K). But, apart from the fact that you now know that I am an old guy (and the older we
get, the less bashful we are about the fact – though my niece informs me that this phase is followed
shortly by bed pans, diapers, and walkers... and that one left turn signal that never switches off), we also
know of another powerful way to massage data.
Bit Fields are already used in a simplistic manner under VB.NET in the Collections.BitArray list, which
represents each sequential bit in the list as a Boolean value of True (1) or False (0). But Bit Fields were
also extremely useful for storing all sorts of integer data. Under C, we could declare a special type of
structure that included a bit reservation count after a colon, such as the following:
typedef struct {
unsigned int flag:1; /* 1-bit space used to enable/disable flag */
unsigned int color:3; /* 3-bit space to store an 8-color value 0-7 */
unsigned int pattern: 8; /* 8-bit space to store a border definition pattern */
} borders;

This allowed me to easily store 3 items of data within the confines of a single word of data ( 16 bits, 32
bits, or 64 bits, depending on our system). But the point is, we could store more than just Boolean data
in the fields. Further, we could use these Bit Fields to translate data between data formats, such as an old
VAX 64-bit Double-precision value to the 64-bit IEEE Double Precision value we use on PC’s and
Macs. Using it, we could quickly take a Double value as stored on a VAX system and translate it to a
Double value that we can use on our PCs.
For example, a 64-bit IEEE Double Precision value is stored in memory like this:
IEEE-754 64-bit double-precision floating point:
Sign/Exp/Fraction: SEEEEEEE EEEEFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
1’s bits: FEDCBA98 76543210 FEDCBA98 76543210 FEDCBA98 76543210 FEDCBA98 76543210
16’s bits: 33333333 33333333 22222222 22222222 11111111 11111111 00000000 00000000
bytes: byte7 byte6 byte5 byte4 byte3 byte2 byte1 byte0
16-bit words: <-------3-------> <-------2-------> <-------1-------> <-------0------->
Exponent Bias: Excess-1023 (this means that an exponent of 0 is stored as 1023)

This is very straight-forward and logical. The Fraction part (also referred to as the Mantissa) consists of 52
consecutive bits (0-51), which can basically be viewed as a 52-bit integer value, but with its value rotated
upward in the field so that numeric data is defined from the upper side. The 11 bits set aside for the Exponent
is also like a 11-bit integer value, used to set how far the decimal place is moved left or right of the left-most
“fraction” digit. The final Sign bit indicates the positive or negative value of the fractional part.
The old VMS-G (Grand Double-Precision, which superseded the error-prone VMS-D format, which we
will discuss later in this Tip) on the VAX computers used a strange, though in its world, probably just as
logical format, as shown here:
VMS-G (Grand) 64-bit Double precision floating point:
Sign/Exp/Fraction: FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF SEEEEEEE EEEEFFFF
1’s bits: FEDCBA98 76543210 FEDCBA98 76543210 FEDCBA98 76543210 FEDCBA98 76543210
16’s bits: 33333333 33333333 22222222 22222222 11111111 11111111 00090000 00000000
bytes: byte7 byte6 byte5 byte4 byte3 byte2 byte1 byte0
16-bit words: <-------3-------> <-------2-------> <-------1-------> <-------0------->
Exponent Bias: Excess-1024

NOTE: VMS stands for Virtual Memory System; a part of the OpenVMS Operating System.
 Page 541

Did we do a double-take? As we can see, VSM-G seems to split up the Fraction portion into 2 parts,
separating the first 4 bits (0-3) from the rest by an 11-bit Exponent and a 1-bit Sign, and then that is
followed by 48 bits for the remainder of the Fraction. This layout is actually a hang-over from the days
of the PDP-11 mini-computers (well, they were still the size of a full-sized chest freezer), because the
PDP-11 had read values from memory in 16-bit words in OPPOSITE order, where, after retrieval, for it
the sign would then be at bit 0, the exponent was bits 1 thru 11, and the Mantissa was from bit 12 thru
bit 63. Perhaps odd now, but this was the source of great confusion (and a cacophony of clenched-teeth
screaming) when Intel introduced the revolutionary, and to many programmers, sacrilegious practice of
retrieving memory words in actual sequential order, grabbing the bytes in lo-hi order from the left, as
they are actually stored in memory. This was profoundly confusing, especially to mainframe
programmers, because we represent binary values on paper from right to left, as I did above, placing bit
0 on the right end, and bit 15 on the left. Well, the PDP-11 actually stored its words of data in memory
in the same order as on paper! This seems really weird now, but it made total sense to computer
scientists back when they were trying to establish what would become computer science, because they
were programming computers entirely by hand, manually computing every bit, byte, and word value. If
the computers stored the data in physical memory in
exactly the same way they represented it on paper,
people transcribing their bit positions from their notes
to switch settings on the programming panel (no
keyboards back then), it all would be much less
confusing and much less prone to transcription errors
creeping in (a study conducted by the US Army
discovered and introduced this practice when they were
computing log tables on the ENIAC computers – Log10
values allowed people to multiply large numbers on a battlefield by looking up the Log10 values of the
numbers in their standard Military Log Books and simply add them together, and then looking up their
antilog, or dividing by simple subtraction – I actually did this in the US Army when I was an Artillery
Surveyor). Paper tape came along, then tape and teletype keyboards. In fact, the IBM Selectric was
designed to be a much friendlier replacement for teletype keyboards that were used to program the paper
and then the magnetic tapes. Card readers came later, which stored up to 32 words of code, which could
be individually “edited” (replaced; using the old ones for bookmarks) to patch transcription errors (or so
the programmers claimed), before magnetic tape became more reliable. Once memory became cheaper,
Intel entered the fray, and to cut corners and simplify its printed circuit designs, they started storing bits
in logical progressive order from left to right, broken up in 4-bit, 8-bit, and 16-bit segments, introducing
the 4-bit CPU in their Intel 4004. The 8-bit Intel 8008 featured 16-bit instruction pointers and frames,
and replaced later by the Intel 8080, still an 8-bit CPU but with more powerful 16-bit framing, and thus
sparking the computer revolution, started humbly in Ed Robert’s MITS Altair 8800, released in 1976.
Once Intel’s 16-bit 8086 CPU came along, nothing was ever the same. Without it, when families got
together for the holidays, they would actually talk to each other, not just sit around the dinner table,
heads bowed in hushed, reverent silence, and staring at their phones.
What is even more confusing, but not to those wacky old programmer hacks, was that the 16-bit words
of the fractional part were likewise stacked in reverse order. Normally the low bits, such as bit 0, would
be considered the lowest value. Nope, that was the highest value. The lowest value is stored at... try and
guess... no, not bit 63, but rather bit 48; the individual 16-bit words were internally stored in proper
order, but the actual words (the 16-bit frames) of memory were in reversed order. No wonder Unix
programmers had such odd behavior, like keeping an army cot next to their consoles, and using an egg
timer as an alarm clock so they could catch 40 winks while a 20-minute batch program ran (our cell
phones can now run those same apps in a fraction of a second); they had gone insane, sporting beards,
pipes, suspenders, ill-fitting rumpled smelly clothes (and wearing those same clothes for days or weeks
on end), and always keeping a punch bowl filled to the brim with M&Ms within easy reach.
 Page 542

But resolving such number translation issues are the types of problems where Bit Fields really shine. For
a first easy example, we could set up one 64-bit Bit Field for VMS-G, another for IEEE, and then, by
defining our fields to line up correctly, we could very quickly swap values between the two and
accurately translate Double-Precision values back and forth between them.
But this is all academic if we had no way of implementing it under VB.NET.
Because a friend of mine was struggling with how to work around the very problem described above, I
created a class named BitField64 that in fact allows us to define named fields along with how many bits
to set aside for each field named within the confines of a 64-bit space. With it, we can fill and grab the
whole data using a Signed or Unsigned Long, or using a Double Precision value. Using this class, we
can create as many 64-bit Bit Fields as we require, including overlapping fields if we need them, and we
can use it to translate between formats, or even use it for data encryption purposes.
Once the fields are defined for a Bit Field, we can set and grab data using a Byte, a Signed or Unsigned
Integer, a Signed or Unsigned Long, or even a Double, to grab however many bits of data we needed.
For example, we could define a Bit Field to address a standard IEEE 64-bit Double-Precision value:
Dim IEEE_Value As New BitField64 'Flat IEEE Double value (no init value, so it is set to zero)
With IEEE_Value
.AddField("Fraction", 52) '52 bit fraction (bits 0 to 52)
.AddField("Exponent", 11) '11 bit exponent (bits 52 to 62)
.AddField("Sign", 1) '1 bit sign (bit 63)
End With

Or Even a 64-bit VMS-G Double-Precision Value:


Dim VMS_G_Value As New BitField64() 'create with a 64-bit VMS-G Value
With VMS_G_Value
.AddField("Fraction1", 4) '4 bit fraction (bits 0 to 3)
.AddField("Exponent", 11 '11 bit exponent (bits 4 to 14)
.AddField("Sign", 1) '1 bit sign (bit 15)
.AddField("Fraction2", 48) '48 bit fraction (bits 16 to 63)
End With

However, because the VAX system stores words of the fraction in quasi-reverse order, and to
accommodate this for translation to and from IEEE format, and if we actually wanted to translate
between them, we would need to in fact define both of these Bit Fields like this:
IEEE and VMS-G formatted for practical inter-translation:
Dim IEEE_Value As New BitField64 'Flat IEEE Double value (no init value, so it is set to zero)
With IEEE_Value
.AddField("Fraction4", 16) '16 bits lowest fraction (bits 0 to 15)
.AddField("Fraction3", 16) '16 bitsnext higher fraction (bit 16 to 31)
.AddField("Fraction2", 16) '16 bitsnext higher fraction (bits 32 to 47)
.AddField("Fraction1", 4) '16 bitsnext highest fraction (bits 48 to 51)
.AddField("Exponent", 11) '11 bits, field bits 52 to 62
.AddField("Sign", 1) '1 bit, field bit 63
End With

Dim VMS_G_Value As New BitField64 'create with a 64-bit VMS-G Value


With VMS_G_Value
.AddField("Fraction1", 4) '4 bits highest Fraction (bits 0 to 3)
.AddField("Exponent", 11) '11 bits exponent (bits 4 to 14)
.AddField("Sign", 1) '1 bit sign (bit 15)
.AddField("Fraction2", 16) '16 bits next lower fraction (bits 12 to 27)
.AddField("Fraction3", 16) '16 bits next lower fraction (bits 28 to 47)
.AddField("Fraction4", 16) '16 bits lowest fraction ( bits 48 to 63)
End With

There is still one more thing to consider, and that is a thing called Exponent Bias or Excess. By default, IEEE
Double-precision stores an offset value of 1022 here (I’ll get to its ‘proclaimed’ Excess-1023 part in a
moment) when the exponent is considered zero. When the decimal value is rightward, the value stored in the
exponent is higher than the bias, and when the decimal place is left, the value is less than the bias. Further,
these values represent binary decimal placement, not base10 decimal placement (yes, I can appreciate the
irony in the terminology used here). Further, because the binary exponent values 11111111111 and
00000000000 have special meaning, the “zero” exponent value stored here is actually 1022, not 1023 (why
those idiots could not just call it what it is, Excess-1022, instead of Excess-1023, is probably a conspiracy to
 Page 543

make us mere mortals envy the programming demigods who claim to actually understand these things, when
it is probably all just an inside joke). VAX programmers, when developing VMS-G, had more sober senses
of humor, because the Excess-1024 Exponent bias they used in their double-precision values actually use
1024 to represent an Exponent of zero. But what all this belly-aching on my part boils down to is that we
must subtract 2 from the VMS-G exponent to correct it for IEEE usage (some programming gurus will
actually tell we to first subtract 1024, next, add 1023, and finally subtract 1, to “properly” convert a VMS-G
exponent to an IEEE exponent, but they are clearly playing us for a fool).
Using my BitField64 class we can translate a 64-bit VMS-G Double-Precision value stored in the
previous VMS_G_Value instance to the IEEE _Value instance using the following BitField64 methods:
IEEE_Value.SetBitsFromByte("Sign", VMS_G_Value.GetBitsToByte("Sign")) '1 bit from VMS Sign to 1 bit IEEE
IEEE_Value.SetBitsFromUInt32("Exponent", VMS_G_Value.GetBitsToUInt32("Exponent") - 2UI) '11 bits fm VMS Exp IEEE (adj down 2 for IEEE)
IEEE_Value.SetBitsFromByte("Fraction1", VMS_G_Value.GetBitsToByte("Fraction1")) '4 bits from VMS Fraction1 to 4 bits IEEE
IEEE_Value.SetBitsFromUInt32("Fraction2", VMS_G_Value.GetBitsToUInt32("Fraction2")) '16 bits from VMS Fraction1 to 16 bits IEEE
IEEE_Value.SetBitsFromUInt32("Fraction3", VMS_G_Value.GetBitsToUInt32("Fraction3")) '16 bits from VMS Fraction1 to 16 bits IEEE
IEEE_Value.SetBitsFromUInt32("Fraction4", VMS_G_Value.GetBitsToUInt32("Fraction4")) '16 bits from VMS Fraction1 to 16 bits IEEE

A much trickier prospect is to process the old VMS-D format, carried over from the PDP-10 (strange
that so many people (and I mean a lot) are still making a living converting all this old data to more
modern computers, before those old clunkers finally flip their last bit). The 64-bit VMS-D (Double
Precision) format is a format that was supposed to be superseded by VMS-G, which rendered its version
of the IEEE format as best it could. Consider VMS-D’s even odder layout:
VMS-D (Double) 64-bit Double precision floating point:
Sign/Exp/Fraction: FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF SEEEEEEE EFFFFFFF
1’s bits: FEDCBA98 76543210 FEDCBA98 76543210 FEDCBA98 76543210 FEDCBA98 76543210
16’s bits: 33333333 33333333 22222222 22222222 11111111 11111111 00000000 00000000
bytes: byte7 byte6 byte5 byte4 byte3 byte2 byte1 byte0
16-bit words: <-------3-------> <-------2-------> <-------1-------> <-------0------->
Exponent Bias: Excess-64

As we can see, this thing only has an 8-bit Exponent, but it has a 55-bit fraction, as opposed to the IEEE
and VMS-G having 11-bit Exponents and 52-bit fractions. Notice also that VMS-D has an Exponent
Bias of a paltry 64. No wonder that it was dropped, though not before mountains of data that depended
on it leached its way across the world. A BitField64 layout of the above data would be like this:
Dim VMS_D_Value As New BitField64 'create with a 64-bit VMS-D Value
With VMS_D_Value
.AddField("Fraction1", 7) '7 bits of highest Fraction (bits 0 to 6)
.AddField("Exponent", 8) '8 bits exponent (bits 7 to 14)
.AddField("Sign", 1) '1 bit sign (bit 15)
.AddField("Fraction2", 16) '16 bits next lower fraction (bits 12 to 27)
.AddField("Fraction3", 16) '16 bits next lower fraction (bits 28 to 47)
.AddField("Fraction4", 16) '16 bits lowest fraction (bits 48 to 63)
End With

To translate this to IEEE format requires some gymnastic ability. The trick here is to translate the 8-bit
exponent in Excess-64 format to an 11-bit exponent in Excess-1023 (1022) bias. This is actually rather
simple; just add 1022 and subtract 128. The tricky business is managing the 55-bit fraction. We can
actually throw the lowest 3 bits away, but to get there, we have to shift all sorts of things around. For
example, we can use the upper 4 bits of Fraction1 to assign to the IEEE’s 4-bit Fraction1 field.
However, then the fun really begins, because we have to squeeze the bottom 3 bits to the top of the next
16-bit fraction word. We can do this by shifting the first 3 bits to the last 3 bits of a word (shift left 13),
and then shift right 3 bits the Fraction2 word, and put these two results together. We trickle this all the
way down through Fraction4, where the final 3 bits 48, 49, 50, the lowest 3 bits (refer to the discussion
on this for VMS-G earlier, 2 pages ago). Because we must break the highest 7 bits up into 3 and 4 bits,
we can simplify our conversion by actually defining two separate fields of 3 and 4 bits to “pre-break” it:
Dim VMS_D_Value As New BitField64 'create with a 64-bit VMS-D Value
With VMS_D_Value
.AddField("Fraction1a", 3) 'first 3 bits of 7 bit highest Fraction (bits 0 to 3)
.AddField("Fraction1b", 4) 'last 4 bits of 7 bit highest Fraction (bits 4 to 7)
.AddField("Exponent", 8) '8 bits exponent (bits 8 to 14)
.AddField("Sign", 1) '1 bit sign (bit 15)
.AddField("Fraction2", 16) '16 bits next lower fraction (bits 12 to 27)
.AddField("Fraction3", 16) '16 bits next lower fraction (bits 28 to 47)
.AddField("Fraction4", 16) '16 bits lowest fraction ( bits 48 to 63)
 Page 544

End With

Using the very same IEEE_Value BitField64 object we used before:


Dim IEEE_Value As New BitField64 'Flat IEEE Double value (no init value, so it is zero)
With IEEE_Value
.AddField("Fraction4", 16) '16 bits lowest fraction (bits 0 to 15)
.AddField("Fraction3", 16) '16 bitsnext higher fraction (bit 16 to 31)
.AddField("Fraction2", 16) '16 bitsnext higher fraction (bits 32 to 47)
.AddField("Fraction1", 4) '16 bitsnext highest fraction (bits 48 to 51)
.AddField("Exponent", 11) '11 bits, field bits 52 to 62
.AddField("Sign", 1) '1 bit, field bit 63
End With

We can perform an otherwise extremely difficult translation with ease. I was also informed by those
supposedly “in-the-know” that this can only be done from C and C++. Well, I am one C++ developer
who is not convinced, because with the BitField64 class, we can do it from VB.NET (and a C#
translation) as well. Consider the following translation code:
IEEE_Value.SetBitsFromByte("Sign", VMS_D_Value.GetBitsToByte("Sign")) 'Transfer sign bit (bit 15)
IEEE_Value.SetBitsFromUInt32("Exponent", VMS_D_Value.GetBitsToUInt32("Exponent") + 1022UI - 128UI) 'convert EX-64 value to EX-1022 (stupid 1023...)
IEEE_Value.SetBitsFromByte("Fraction1", VMS_D_Value.GetBitsToByte("Fraction1b")) 'easily transfer 4 highest bits
' Note that we must add 3 preceding bits to each 16-bit grouping. The previous 3 bits are shifted left to occupy bits 13, 14, and 15 of 16-bit word,
' and matching 16-bit word is shifted down to accommodate it, temporarily tossing its lower 3 bits into the aether. Note also that in doing this all
' the way down, that the final 3 bits, actually occupying bits 48, 49, and 50, are shift out and never used. These bits are minor and do not affect
' accuracy to any noticeable degree (except by bean-counters), but we have no choice –- we are moving 55 bits into a smaller 52-bit field.
IEEE_Value.SetBitsFromUInt32("Fraction2", VMS_D_Value.GetBitsToUInt32("Fraction1a") << 13 Or VMS_D_Value.GetBitsToUInt32("Fraction2") >> 3)
IEEE_Value.SetBitsFromUInt32("Fraction3", VMS_D_Value.GetBitsToUInt32("Fraction2") << 13 Or VMS_D_Value.GetBitsToUInt32("Fraction3") >> 3)
IEEE_Value.SetBitsFromUInt32("Fraction4", VMS_D_Value.GetBitsToUInt32("Fraction3") << 13 Or VMS_D_Value.GetBitsToUInt32("Fraction4") >> 3)
Debug.Print("Result = {0}", IEEE_Value.DblValue.ToString) 'display converted result

What follows is my BitField64 class:


Option Explicit On
Option Strict On
Imports System.Runtime.InteropServices
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
'BitField64 Class
'64-bit bitfield operations.
'
' Field names are case-specific. "Sign" is different from "sign".
'
''Create Instances like this:
'Dim VMS_G_Value As New BitField64 'create with a 64-bit VMS-G Value.
'With VMS_G_Value
' .AddField("Fraction1", 4) '4 bits upper Fraction (bits 0 to 3).
' .AddField("Exponent", 11) '11 bits exponent (bits 4 to 14).
' .AddField("Sign", 1) '1 bit sign (bit 15).
' .AddField("Fraction2", 16) '16 bits next lower fraction (bits 12 to 27).
' .AddField("Fraction3", 16) '16 bits next lower fraction (bits 28 to 47).
' .AddField("Fraction4", 16) '16 bits lowest fraction ( bits 48 to 63).
'End With

'Dim IEEE_Value As New BitField64 'Flat IEEE Double value (no init value, so it is zero).
'With IEEE_Value
' .AddField("Fraction4", 16) '16 bits lowest fraction (bits 0 to 15).
' .AddField("Fraction3", 16) '16 bits next higher fraction (bit 16 to 31).
' .AddField("Fraction2", 16) '16 bits next higher fraction (bits 32 to 47).
' .AddField("Fraction1", 4) '16 bits next highest fraction (bits 48 to 51).
' .AddField("Exponent", 11) '11 bits exponent (52 to 62).
' .AddField("Sign", 1) '1 bit, field bit 63.
'End With

''Assign Values like this:


'IEEE_Value.Int64Value = 1234L 'int64 (Long) value.
'IEEE_Value.Uint64Value = 1234UL 'UInt64 (Unsigned Long).
'IEEE_Value.DblValue = 1234.5678R 'set a double value.

''grab full 64-bit values like this:


'Dim D As Double = IEEE_Value.DblValue
'Dim L As Long = IEEE_Value.Int64Value
'Dim UL As ULong = IEEE_Value.Uint64Value

''assign defined fields like this (Fill VMS-G fields with VAX version of 128.75):
'VMS_G_Value.SetBitsFromByte("Sign", CByte(0)) '1 bit sign (bit 15).
'VMS_G_Value.SetBitsFromUInt32("Exponent", 1032UI) '8 bit Exponent (bits 4-14) (setting 1024+8 = 1032).
'VMS_G_Value.SetBitsFromUInt32("Fraction1", 0UI) '4 bit upper fraction (bits 0-4).
'VMS_G_Value.SetBitsFromUInt64("Fraction2", &H1800UI) '16 bits next lower fraction (bits 16-31).
'VMS_G_Value.SetBitsFromUInt64("Fraction3", 0UI) '16 bits next lower fraction (bits 41-47).
'VMS_G_Value.SetBitsFromUInt64("Fraction4", 0UI) '16 bits next lower fraction (bits 48-63).

''convert VMS-G to IEEE format.


'' 1 bit from VMS Sign to 1 bit IEEE.
'IEEE_Value.SetBitsFromByte("Sign", VMS_G_Value.GetBitsToByte("Sign"))
 Page 545

'' 8 bits from VMS Exposent to 11 bits IEEE (adjust down 2 for IEEE).
'IEEE_Value.SetBitsFromUInt32("Exponent", VMS_G_Value.GetBitsToUInt32("Exponent") – 2UI)
'' 4 bits from VMS Fraction1 to 4 bits IEEE.
'IEEE_Value.SetBitsFromUInt64("Fraction1", VMS_G_Value.GetBitsToUInt64("Fraction1"))
'' 16 bits from VMS Fraction1 to 16 bits IEEE.
'IEEE_Value.SetBitsFromUInt64("Fraction2", VMS_G_Value.GetBitsToUInt64("Fraction2"))
'' 16 bits from VMS Fraction1 to 16 bits IEEE.
'IEEE_Value.SetBitsFromUInt64("Fraction3", VMS_G_Value.GetBitsToUInt64("Fraction3"))
'' 16 bits from VMS Fraction1 to 16 bits IEEE.
'IEEE_Value.SetBitsFromUInt64("Fraction4", VMS_G_Value.GetBitsToUInt64("Fraction4"))

'Debug.Print("Result = {0}", IEEE_Value.DblValue.ToString) 'display connverted result.


'-------------------------------------------------------------------------------------
'------------------------------------------------------------------------------------
Public Class BitField64
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private _BaseData As New structBitField
Private _FieldNames() As String 'store the name defined for each bit field member.
Private _FieldOffsets(,) As Int32 'array to store field offset in 64-bit field, and # of bits assigned to it.
'-------------------------------------------
Private _HoldOffset As Int32 'local storage for a local grab if specified field data.
Private _HoldBitCount As Int32 'local storage for a local grab if specified number of bits assigned to field.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
' Structure Name : structBitField
' Purpose : Local storage for 64-bit field. Overlap fields (Union) so they occupy the same space.
' It can be loaded initially with an In64, UInt64, or a Double Value.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
<StructLayout(LayoutKind.Explicit)> Friend Structure structBitField
<FieldOffset(0)> Friend VarDouble As Double 'Primary storage location, Bit 0-63 represented as Double-Precision.
<FieldOffset(0)> Friend VarUInt64 As UInt64 'Primary storage location, Bit 0-63 represented as Unsigned Long.
<FieldOffset(0)> Friend VarInt64 As Int64 'Primary storage location, Bit 0-63 represented as Signed Long.
End Structure

'*******************************************************************************
' Property Name : FieldCount
' Purpose : Return the number of fields the user has defined, or 0 if none.
'*******************************************************************************
Friend ReadOnly Property FieldCount As Int32
Get
Try
Return UBound(Me._FieldNames) + 1 'get the number of fields defined.
Catch
Return 0 'array not defined.
End Try
End Get
End Property

'*******************************************************************************
' Property Name : Int64Value
' Purpose : Get/Set the Value as a signed Long Integer.
'*******************************************************************************
Friend Property Int64Value As Int64
Get
Return Me._BaseData.VarInt64
End Get
Set(value As Int64)
Me._BaseData.VarInt64 = value
End Set
End Property

'*******************************************************************************
' Property Name : Int64UValue
' Purpose : Get/Set the Value as an Unsigned Long Integer.
'*******************************************************************************
Friend Property Uint64Value As UInt64
Get
Return Me._BaseData.VarUInt64
End Get
Set(value As UInt64)
Me._BaseData.VarUInt64 = value
End Set
End Property

'*******************************************************************************
' Property Name : DblValue
' Purpose : Get/Set the Value as a Double Precision (Real) value.
'*******************************************************************************
Friend Property DblValue As Double
Get
Return Me._BaseData.VarDouble
End Get
Set(value As Double)
Me._BaseData.VarDouble = value
End Set
 Page 546

End Property

'*******************************************************************************
'*******************************************************************************
' Method Name : New
' Purpose : Define a New BitField64 object, initialized with a null value.
'*******************************************************************************
'*******************************************************************************
Friend Sub New()
Me._BaseData.VarInt64 = 0L
End Sub

'*******************************************************************************
'*******************************************************************************
' Method Name : New
' Purpose : Define a New BitField64 object, initialized with a Long Value.
'*******************************************************************************
'*******************************************************************************
Friend Sub New(ByVal Value As Int64)
Me._BaseData.VarInt64 = Value
End Sub

'*******************************************************************************
'*******************************************************************************
' Method Name : New
' Purpose : Define a New BitField64 object, initialized with an Unsigned Long Value.
'*******************************************************************************
'*******************************************************************************
Friend Sub New(ByVal Value As UInt64)
Me._BaseData.VarUInt64 = Value
End Sub

'*******************************************************************************
'*******************************************************************************
' Method Name : New
' Purpose : Define a New BitField64 object, initialized with an Double-Precision Value.
'*******************************************************************************
'*******************************************************************************
Friend Sub New(ByVal Value As Double)
Me._BaseData.VarDouble = Value
End Sub

'*******************************************************************************
' Method Name : InitNewFormat
' Purpose : Erase any current definition and start a new field definition.
' : Because this is a class, it is not likely we will need this,
' : But it is useful if the same class will later be used differently.
'*******************************************************************************
Friend Sub InitNewFormat()
ClearValue64() 'nullify value.
Erase Me._FieldNames 'erase field name list.
Erase Me._FieldOffsets 'erase offsets.
End Sub

'*******************************************************************************
' Method Name : ClearValue64
' Purpose : Clear the value space but do not alter any field definitions.
'*******************************************************************************
Friend Sub ClearValue64()
Me._BaseData.VarInt64 = 0L 'nullify value
End Sub

'*******************************************************************************
' Method Name : AddField
' Purpose : Add a field name and a bit count to reserve for it in this object's definitions.
' :
' NOTE : You are able to force a FieldOffset within the BitField, but you should do
' : this only after you have added any stacking values, if needed. Once such fields are
' : defined, you can declare any number of additional fields with hard-coded
' : offsets within the BitField and a specified number of bits (NumBits).
'*******************************************************************************
Friend Function AddField(ByVal FieldName As String,
ByVal NumBits As Int32,
Optional ByVal FieldOffset As Int32 = -1) As Int32
Dim Count As Int32 = Me.FieldCount 'get number of fields defined.
If String.IsNullOrWhiteSpace(FieldName) Then 'if the field name is not declared...
Throw New Exception("Missing FieldName for this BitField64 object")
Return -2
End If
If NumBits < 1 OrElse NumBits > 64 Then
Throw New Exception("NumBits is out of range for this BitField64 object")
Return -4
End If
If Count = 0 Then 'if no fields defined yet...
ReDim Me._FieldNames(0) 'create initial data storage.
ReDim Me._FieldOffsets(0, 1)
 Page 547

Me._FieldNames(0) = FieldName 'save field name.


Me._FieldOffsets(0, 0) = 0 'init to start offset (new definitions).
If FieldOffset >= 0 Then 'did user specify hard-coded offset?
If FieldOffset + NumBits > 64 Then 'if forced def. takes us over top...
Throw New Exception("FieldOffset+NumBits is out of range for this BitField64 object") 'Danger, Will Robinson!
Return -4
Else
Me._FieldOffsets(0, 0) = FieldOffset 'offset OK, so use this manual offset.
End If
End If
Me._FieldOffsets(0, 1) = NumBits 'save # of bits to assign to field.
Return 0 'return success.
End If
' here, we have fields defined, so see if this is a redef...
For Idx As Int32 = 0 To Count - 1 'scan through list of defined fields.
If FieldName.CompareTo(Me._FieldNames(Idx)) = 0 Then 'a match found?
Throw New Exception("FieldName not defined for this BitField64 object") 'yes, so throw an error.
Return -3
End If
Next
Dim Offset As Int32 = FieldOffset 'init to manual offset.
If FieldOffset < 0 Then 'if FieldOffset is not defined...
Offset = Me._FieldOffsets(Count - 1, 0) + Me._FieldOffsets(Count - 1, 1) 'comp start offset from previous.
End If

If Offset + NumBits > 64 Then 'if new definition takes us over top...
Throw New Exception("NumBits is out of range for this BitField64 object") 'Danger, Will Robinson!
Return -4
Else
ReDim Preserve Me._FieldNames(Count) 'otherwise, set aside storage for the new definition.
Me._FieldNames(Count) = FieldName 'save the field name
Dim Tmp(Count, 1) As Int32 'dim to new size (we can only redim right side on multi-dims),
For Idx As Int32 = 0 To Count - 1 'so use a new array. First, copy the original 2D data to it...
Tmp(Idx, 0) = Me._FieldOffsets(Idx, 0) 'dim 0.
Tmp(Idx, 1) = Me._FieldOffsets(Idx, 1) 'dim 1.
Next
Tmp(Count, 0) = Offset 'save the offset into the bit field for the new data elements.
Tmp(Count, 1) = NumBits 'save the number of bits defined for this field.
Erase Me._FieldOffsets 'erase the original 2D data to release this space to the CLR.
Me._FieldOffsets = Tmp 'assign Tmp array data pointer to Me._FieldOffsets.
Return 0 'return success.
End If
End Function

'*******************************************************************************
' Method Name : GetBitsToByte
' Purpose : Get the bits assign to a Field Name to a Byte.
'*******************************************************************************
Friend Function GetBitsToByte(ByVal FieldName As String) As Byte
Dim Result As Int32 = ComputeIndexing(FieldName, 8) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return CByte(GetBitsFromBuffer(Me._HoldOffset, Me._HoldBitCount, 8)) 'get range of bits.
End If
Return CByte(Result) 'else return error code.
End Function

'*******************************************************************************
' Method Name : SetBitsFromByte
' Purpose : Set the bits assign to a Field Name from a Byte.
'*******************************************************************************
Friend Function SetBitsFromByte(ByVal FieldName As String, ByVal Value As Byte) As Int32
Dim Result As Int32 = ComputeIndexing(FieldName, 8) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return SetBitToBuffer(Me._HoldOffset, Me._HoldBitCount, CULng(Value)) 'set range of bits.
End If
Return Result 'else return error code.
End Function

'*******************************************************************************
' Method Name : GetBitsToInt32
' Purpose : Get the bits assign to a Field Name to a 32-bit Signed Integer.
'*******************************************************************************
Friend Function GetBitsToInt32(ByVal FieldName As String) As Int32
Dim Result As Int32 = ComputeIndexing(FieldName, 32) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return CInt(GetBitsFromBuffer(Me._HoldOffset, Me._HoldBitCount, 32)) 'get range of bits.
End If
Return Result 'else return error code.
End Function

'*******************************************************************************
' Method Name : SetBitsFromInt32
' Purpose : Set the bits assign to a Field Name from a 32-bit Signed Integer
'*******************************************************************************
Friend Function SetBitsFromInt32(ByVal FieldName As String, ByVal Value As Int32) As Int32
 Page 548

Dim Result As Int32 = ComputeIndexing(FieldName, 32) 'init the result value.


If Result = 0 Then 'if found user-supplied fieldname to grab...
Return SetBitToBuffer(Me._HoldOffset, Me._HoldBitCount, CULng(Value)) 'set range of bits.
End If
Return Result 'else return error code.
End Function

'*******************************************************************************
' Method Name : GetBitsToUInt32
' Purpose : Get the bits assign to a Field Name to a 32-bit Unsigned Integer.
'*******************************************************************************
Friend Function GetBitsToUInt32(ByVal FieldName As String) As UInt32
Dim Result As Int32 = ComputeIndexing(FieldName, 32) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return CUInt(GetBitsFromBuffer(Me._HoldOffset, Me._HoldBitCount, 32)) 'get range of bits.
End If
Return CUInt(Result) 'else return error code.
End Function

'*******************************************************************************
' Method Name : SetBitsFromUInt32
' Purpose : Set the bits assign to a Field Name as a 32-bit Unsigned Integer.
'*******************************************************************************
Friend Function SetBitsFromUInt32(ByVal FieldName As String, ByVal Value As UInt32) As Int32
Dim Result As Int32 = ComputeIndexing(FieldName, 32) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return SetBitToBuffer(Me._HoldOffset, Me._HoldBitCount, CULng(Value)) 'get range of bits.
End If
Return Result 'else return error code.
End Function

'*******************************************************************************
' Method Name : GetBitsToInt64
' Purpose : Get the bits assign to a Field Name to a 64-bit signed Long.
'*******************************************************************************
Friend Function GetBitsToInt64(ByVal FieldName As String) As Int64
Dim Result As Int32 = ComputeIndexing(FieldName, 64) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return CLng(GetBitsFromBuffer(Me._HoldOffset, Me._HoldBitCount, 64)) 'get range of bits.
End If
Return CLng(Result) 'else return error code.
End Function

'*******************************************************************************
' Method Name : SetBitsFromInt64
' Purpose : Set the bits assign to a Field Name from a 64-bit signed Long.
'*******************************************************************************
Friend Function SetBitsFromInt64(ByVal FieldName As String, ByVal Value As UInt64) As Int32
Dim Result As Int32 = ComputeIndexing(FieldName, 64) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return SetBitToBuffer(Me._HoldOffset, Me._HoldBitCount, Value) 'set range of bits.
End If
Return Result 'else return error code.
End Function

'*******************************************************************************
' Method Name : GetBitsToUInt64
' Purpose : Get the bits assign to a Field Name to a 64-bit Unsigned Long.
'*******************************************************************************
Friend Function GetBitsToUInt64(ByVal FieldName As String) As UInt64
Dim Result As Int32 = ComputeIndexing(FieldName, 64) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return CULng(GetBitsFromBuffer(Me._HoldOffset, Me._HoldBitCount, 64)) 'get range of bits.
End If
Return CULng(Result) 'else return error code.
End Function

'*******************************************************************************
' Method Name : SetBitsFromUInt64
' Purpose : Set the bits assign to a Field Name from a 64-bit Unsigned Long.
'*******************************************************************************
Friend Function SetBitsFromUInt64(ByVal FieldName As String, ByVal Value As UInt64) As Int32
Dim Result As Int32 = ComputeIndexing(FieldName, 64) 'init the result value.
If Result = 0 Then 'if found user-supplied fieldname to grab...
Return SetBitToBuffer(Me._HoldOffset, Me._HoldBitCount, Value) 'set range of bits.
End If
Return Result 'else return error code.
End Function

'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' Method Name : ComputeIndexing
' Purpose : Find the bit field offset and bit count for a specified field name.
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Private Function ComputeIndexing(ByVal FieldName As String, Optional ByVal ReturnSize As Int32 = 0) As Int32
Dim Count As Int32 = Me.FieldCount 'get the number of fields defined.
If Count = 0 Then 'if none, then, Huston, we have a problem...
 Page 549

Throw New Exception("No Fields Defined for this BitField64 object")


Return -1
End If
If String.IsNullOrWhiteSpace(FieldName) Then 'if the supplied Field name is not valid...
Throw New Exception("Missing FieldName for this BitField64 object")
Return -2
End If
For Idx As Int32 = 0 To Count - 1 'scan through defined fields.
If FieldName.CompareTo(Me._FieldNames(Idx)) = 0 Then 'found the field definition?
Me._HoldOffset = Me._FieldOffsets(Idx, 0) 'yes, so grab bit field offset and store locally.
Me._HoldBitCount = Me._FieldOffsets(Idx, 1) 'and also the # of bits assigned to this field.
If ReturnSize > 0 AndAlso Me._HoldBitCount > ReturnSize Then
Throw New Exception("The desired bit field (" & Me._HoldBitCount &
"-bits) is larger than the receiving " & ReturnSize & "-bit buffer")
Return -5
End If
Return 0 'indicate success.
End If
Next
Throw New Exception("FieldName not defined for this BitField64 object")
Return -3
End Function

'*******************************************************************************
' Method Name : TestBitAt
' Purpose : Test a single indexed bit (0-63) for being set (True) or reset (False).
'*******************************************************************************
Friend Function TestBitAt(ByVal Index As Int32) As Boolean
Dim Result As Int32 = CByte(CheckOffsets(Index, 1)) 'check indexing.
If Result = 0 Then 'if all is well...
Dim Mask As UInt64 = CULng(Math.Pow(2.0R, CDbl(Index))) 'init bit mask to starting position in bit field.
Return CBool(Mask And Me._BaseData.VarUInt64) 'return True if indexed bit is set, otherwise false.
End If
Return False
End Function

'*******************************************************************************
' Method Name : SetBitAt
' Purpose : Set a single indexrd bit (0-63) to set (True) or reset (False).
'*******************************************************************************
Friend Function SetBitAt(ByVal Index As Int32, ByVal SetState As Boolean) As Boolean
Dim Result As Int32 = CByte(CheckOffsets(Index, 1)) 'check indexing
If Result = 0 Then 'if all is well...
Dim Mask As UInt64 = CULng(Math.Pow(2.0R, CDbl(Index))) 'init bit mask to starting position in bit field.
Dim Local As UInt64 = Me._BaseData.VarUInt64 'grab a local copy of the bit field.
Local = Local Or Mask 'first force the indexed bit to be set.
If Not SetState Then 'are we in fact ensuring that it is turned off?
Local = Local Xor Mask 'yes, so make sure the bit is off.
End If
Me._BaseData.VarUInt64 = Local 'plug value back into class storage.
Return True 'return success.
End If
Return False 'failure.
End Function

'*******************************************************************************
' Method Name : GetByteAt
' Purpose : Get a single bytes worth of data from the buffer at byte index 0-7.
'*******************************************************************************
Friend Function GetByteAt(ByVal Index As Int32) As Byte
Dim Result As Int32 = CByte(CheckOffsets(Index, 1, 8)) 'check indexing.
If Result = 0 Then 'if all is well...
Return GetBitsToByte(Index * 8, 8) 'compute the index offset, and specify a byte.
End If
Return CByte(Result) 'return fail code.
End Function

'*******************************************************************************
' Method Name : SetByteAt
' Purpose : Set a single bytes worth of data to the buffer at byte index 0-7.
'*******************************************************************************
Friend Function SetByteAt(ByVal Index As Int32, ByVal ByteValue As Byte) As Boolean
Dim Result As Int32 = CByte(CheckOffsets(Index, 1, 8)) 'check indexing.
If Result = 0 Then 'if all is well...
Return SetBitsToByte(Index * 8, 8, ByteValue) = 0 'return success if result is 0.
End If
Return False 'return failure.
End Function

'*******************************************************************************
' Method Name : GetBitsToByte
' Purpose : Get a series of 1-8 bits (Numbits) at the Indexed offset (0-63) to a Byte.
'*******************************************************************************
Friend Function GetBitsToByte(ByVal Index As Int32, ByVal NumBits As Int32) As Byte
Return CByte(GetBitsFromBuffer(Index, NumBits, 8))
End Function
 Page 550

'*******************************************************************************
' Method Name : SetByteToBits
' Purpose : Set a series of 1-8 bits (Numbits) at the Indexed offset (0-63) from a Byte.
'*******************************************************************************
Friend Function SetBitsToByte(ByVal Index As Int32, ByVal NumBits As Int32, ByVal Value As Byte) As Int32
Return SetBitToBuffer(Index, NumBits, CULng(Value), 8)
End Function

'*******************************************************************************
' Method Name : GetBitsToInt32
' Purpose : Get a series of 1-32 bits (Numbits) at the Indexed offset (0-63) to an Int32.
'*******************************************************************************
Friend Function GetBitsToInt32(ByVal Index As Int32, ByVal NumBits As Int32) As Int32
Return CInt(GetBitsFromBuffer(Index, NumBits, 32))
End Function

'*******************************************************************************
' Method Name : SetByteToInt32
' Purpose : Set a series of 1-32 bits (Numbits) at the Indexed offset (0-63) from an Int32.
'*******************************************************************************
Friend Function SetBitsToInt32(ByVal Index As Int32, ByVal NumBits As Int32, ByVal Value As Int32) As Int32
Return SetBitToBuffer(Index, NumBits, CULng(Value), 32)
End Function

'*******************************************************************************
' Method Name : GetBitsToUInt32
' Purpose : Get a series of 1-32 bits (Numbits) at the Indexed offset (0-63) to a Uint32.
'*******************************************************************************
Friend Function GetBitsToUInt32(ByVal Index As Int32, ByVal NumBits As Int32) As UInt32
Return CUInt(GetBitsFromBuffer(Index, NumBits, 32))
End Function

'*******************************************************************************
' Method Name : SetByteToUInt32
' Purpose : Set a series of 1-32 bits (Numbits) at the Indexed offset (0-63) from a Uint32.
'*******************************************************************************
Friend Function SetBitsToUInt32(ByVal Index As Int32, ByVal NumBits As Int32, ByVal Value As UInt32) As Int32
Return SetBitToBuffer(Index, NumBits, CULng(Value))
End Function

'*******************************************************************************
' Method Name : GetBitsToInt64
' Purpose : Get a series of 1-64 bits (Numbits) at the Indexed offset (0-63) to an Int64.
'*******************************************************************************
Friend Function GetBitsToInt64(ByVal Index As Int32, ByVal NumBits As Int32) As Int64
Return CLng(GetBitsFromBuffer(Index, NumBits, 64))
End Function

'*******************************************************************************
' Method Name : SetBitsToInt64
' Purpose : Set a series of 1-64 bits (Numbits) at the Indexed offset (0-63) from an Int64.
'*******************************************************************************
Friend Function SetBitsToInt64(ByVal Index As Int32, ByVal NumBits As Int32, ByVal Value As Int64) As Int32
Return SetBitToBuffer(Index, NumBits, CULng(Value), 64)
End Function

'*******************************************************************************
' Method Name : GetBitsToUInt64
' Purpose : Get a series of 1-64 bits (Numbits) at the Indexed offset (0-63) to a Uint64.
'*******************************************************************************
Friend Function GetBitsToUInt64(ByVal Index As Int32, ByVal NumBits As Int32) As UInt64
Return GetBitsFromBuffer(Index, NumBits, 64)
End Function

'*******************************************************************************
' Method Name : SetByteToUInt64
' Purpose : Set a series of 1-64 bits (Numbits) at the Indexed offset (0-63) from a Uint64.
'*******************************************************************************
Friend Function SetBitsToUInt64(ByVal Index As Int32, ByVal NumBits As Int32, ByVal Value As UInt32) As Int32
Return SetBitToBuffer(Index, NumBits, CULng(Value))
End Function

'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' Method Name : GetBitsFromBuffer
' Purpose : Grab a selected series of bits from a BitField64 buffer at a specified Index offset.
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Private Function GetBitsFromBuffer(ByVal Index As Int32,
ByVal NumBits As Int32,
Optional ByVal ReturnSize As Int32 = 0) As UInt64
Dim Result As UInt64 = CULng(CheckOffsets(Index, NumBits, ReturnSize))
If Result = 0 Then
Dim Mask As UInt64 = CULng(Math.Pow(2.0R, CDbl(Index))) 'init bit mask to starting position in bit field.
Dim lMsk As UInt64 = 1 'mask to check provided value.
Dim Local As UInt64 = Me._BaseData.VarUInt64 'grab a local copy of the bit field.
For Idx As Int32 = 1 To NumBits 'process for the number of bits assigned to this field.
 Page 551

If CBool(Local And Mask) Then 'is this bit set in the bit field?
Result = Result Or lMsk 'yes, so set bit 0 (get shifted left as needed.
End If
Mask = Mask << 1 'shift mask left one bit.
lMsk = lMsk << 1
Next 'and try again.
End If
Return Result
End Function

'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' Method Name : SetBitToBuffer
' Purpose : Set a selected series of bits from a BitField64 buffer at a specified Index offset.
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Private Function SetBitToBuffer(ByVal Index As Int32,
ByVal Numbits As Int32,
ByVal Value As UInt64,
Optional ByVal ReturnSize As Int32 = 0) As Int32
Dim Result As Int32 = CheckOffsets(Index, Numbits, ReturnSize) 'see if parameters are ok.
If Result = 0 Then
Dim Mask As UInt64 = CULng(Math.Pow(2.0R, CDbl(Index))) 'init bit mask to starting position in bit field.
Dim lMsk As UInt64 = 1 'mask to check provided value.
Dim Local As UInt64 = Me._BaseData.VarUInt64 'grab a local copy of the bit field.
For Idx As Int32 = 1 To Numbits 'process for the number of bits assigned to this field.
Local = Local Or Mask 'force appropriate bit in field (ensures bit updated).
If Not CBool(Value And lMsk) Then 'if a bit is not set in the source value...
Local = Local Xor Mask 'the flup the bit to turn it off.
End If
Mask = Mask << 1 'shift masks left one bit.
lMsk = lMsk << 1
Next 'and try again.
Me._BaseData.VarUInt64 = Local 'stuff result.
End If
Return Result
End Function

'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' Method Name : CheckOffsets
' Purpose : Test parameters fpr being correct
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Friend Function CheckOffsets(ByVal Index As Int32, ByVal Numbits As Int32, Optional ByVal ReturnSize As Int32 = 0) As Int32
If Index < 0 OrElse Index > 63 Then
Throw New Exception("The Index value is out of range for this BitField64 object")
Return -3
ElseIf Index + Numbits > 64 Then
Throw New Exception("NumBits is out of range for this BitField64 object") 'Danger, Will Robinson!
Return -4
ElseIf ReturnSize > 0 AndAlso Numbits > ReturnSize Then
Throw New Exception("The desired bit field (" & Numbits & "-bits) is larger than the receiving " &
ReturnSize & "-bit buffer")
Return -5
Else
Return 0
End If
End Function
End Class
 Page 552

Black Book Tip # 29


Taking Advantage of, and Extending Application Events.
If we ever pay a visit to our current application’s properties
(and the more we know about them and what we can do
there, the more that we will probably do so), one of the
things we may have noticed there is an option to “View
Application events”:
When we select it, the IDE will create a program file
named ApplicationEvents.vb in our main program folder
(we can afterward access it right from the Solution
Explorer). Our normal reaction the first time we do this is to say, “Oh, crap! What did I just do?!”, and
immediately close it without saving it. Unfortunately, though there is no harm done, if we do that, we
will also lose the auto-generated information it pre-loaded it with, thus leaving this file completely blank
afterwards. If we did this, and I have done it enough for all of us, we can easily recover it either by
typing 4 lines of code into it (ugh!), or simply deleting the file from the Solution Explorer and then let
the properties application create it for us again (Yay!), which I recommend.
Consider its contents, which does nothing more than specify that its contents will be part of the My
namespace, and declares a class MyApplication, which we may notice is declared as Partial, and so it is
in fact a part of a broader MyApplication class, and is in fact part of the My namespace:
Namespace My

' The following events are available for MyApplication:


'
' Startup: Raised when the application starts, before the startup form is created.
' Shutdown: Raised after all application forms are closed. This event is not raised if the application terminates abnormally.
' UnhandledException: Raised if the application encounters an unhandled exception.
' StartupNextInstance: Raised when launching a single-instance application and the application is already active.
' NetworkAvailabilityChanged: Raised when the network connection is connected or disconnected.
Partial Friend Class MyApplication

End Class

End Namespace

The main MyApplication class is actually a tiny class defined within the Application.Designer.vb file,
which is in turn a support file for an XML-formatted file named application.myapp, both of which are
stored in our program’s MyProject folder. We can open these files from the Solution Explorer (make
sure that we have the Show All Files button selected in the Solution Explorer’s Toolbar in order to see
designer files from there). We can examine their contents without consequence. For example, the
Application.Designer.vb file initially consists of this small bit of code:
'------------------------------------------------------------------------------
' <auto-generated>
' This code was generated by a tool.
' Runtime Version:4.0.30319.34014
'
' Changes to this file may cause incorrect behavior and will be lost if
' the code is regenerated.
' </auto-generated>
'------------------------------------------------------------------------------

Option Strict On
Option Explicit On

Namespace My

'NOTE: This file is auto-generated; do not modify it directly. To make changes,


' or if you encounter build errors in this file, go to the Project Designer
' (go to Project Properties or double-click the My Project node in
' Solution Explorer), and make changes on the Application tab.
'
Partial Friend Class MyApplication

<Global.System.Diagnostics.DebuggerStepThroughAttribute()>
Public Sub New()
MyBase.New(Global.Microsoft.VisualBasic.ApplicationServices.AuthenticationMode.Windows)
Me.IsSingleInstance = false
Me.EnableVisualStyles = true
Me.SaveMySettingsOnExit = true
Me.ShutDownStyle = Global.Microsoft.VisualBasic.ApplicationServices.ShutdownMode.AfterMainFormCloses
 Page 553

End Sub

<Global.System.Diagnostics.DebuggerStepThroughAttribute()>
Protected Overrides Sub OnCreateMainForm()
Me.MainForm = Global.Qube.frmQubeWorkspace
End Sub

<Global.System.Diagnostics.DebuggerStepThroughAttribute()>
Protected Overrides Sub OnCreateSplashScreen()
Me.SplashScreen = Global.Qube.frmQubeSplashScreen
End Sub
End Class
End Namespace

As we can see, several of the options declared in our Application Properties are embedded within the
code here. We also learn how the program keeps track of its main form and its optional splash screen.
But if we don our mining helmets, there is so very much more that we can do here. This is usually were
we might be forced to do some late-night reading within a well-thumbed stack of arcane documents that
are usually written by the people who are absolutely the least qualified to write such documentation –
which is namely the original code developers, most of whom seem to assume, like the guys on Big
Bang Theory, that everyone within earshot is intimately familiar with all the cryptic jargon they wretch
from their bowels as if they were reciting from Byron.
But a good place to start is to enter “ Protected Overrides ” (with a trailing space) and view all the
possible code we can play with to alter and enhance ApplicationEvents.vb. For example:

Consider OnInitialize, selected above. If we hit the Tab key here, it generates the body of the
OnInitialize function:
Partial Friend Class MyApplication
Protected Overrides Function OnInitialize(commandLineArgs As System.Collections.ObjectModel.ReadOnlyCollection(Of String)) As Boolean
Return MyBase.OnInitialize(commandLineArgs)
End Function
End Class

Suppose we have a splash screen that we want to display longer, because although our splash screen
looks fabulous, the problem may be is that it too quickly disappears behind our main form. Normally,
our solution might be to set the splash screen form’s TopMost property to True, which will keep it above
the main form. But even with this recommended change, it still disappears far too quickly.
As it is, the splash screen is displayed for only two or three seconds. To display it longer, such as a
minimum of 5 seconds, a really great place to change it is in the above OnInitialize method, which is a
method that is invoked prior the splash screen or the main form being instantiated and displayed.
In an inserted blank
line above the Return
statement within the
OnInitialize method, if
we type “Me.”, we get
all sorts of options we
can play with, but one
of them will solve our
splash screen timing issue: MinimumSplashscreenDisplayTime (see the image, above).
 Page 554

With this, we can specify the minimum number of milliseconds for our splash screen to be displayed.
So, for 5 seconds, we would set it to a value of 5000, because there are 1000 milliseconds in one second:
Protected Overrides Function OnInitialize(ByVal commandLineArgs As System.Collections.ObjectModel.ReadOnlyCollection(Of String)) As Boolean
Me.MinimumSplashScreenDisplayTime = 5000 'Set the splash screen timeout to 5 seconds (5000 miliseconds; 1 second = 1000 miliseconds)
Return MyBase.OnInitialize(commandLineArgs)
End Function

Between this and the splash screen form’s TopMost property being set to True, our application has
already taken a giant step toward looking much more professional.
NOTE: We can also set this value by specifying: My.Application.MinimumSplashScreenDisplayTime = 5000.
But this is clearly not all we can do within the MyApplication class, or, even within the My namespace.
For example, we can extend the code to better customize it to our, or our client’s needs. Consider
visiting MSDN’s “Extending the My Namespace in Visual Basic” at http://msdn.microsoft.com/en-
us/library/bb531245.aspx. There, we can learn how to:
 Customize Existing My Namespace Members.
 Add Members to My Objects.
 Add Custom Objects to the My Namespace.
 Add Members to the My Namespace.
 Add Events to Customize My Objects.
 Designing Class Libraries for My.
 Packaging and Deploying Extensions.
One really cool feature is that they show us how to add custom objects to the My namespace using VB
modules, but hiding the module names by preceding the declaration of the modules with
“<HideModuleName>”, as in:
Namespace My
<HideModuleName()>
Module MyCustomModule

End Module
End Namespace

I will often hear gurus warn that this kind of information can be dangerous. For whom, I wonder? For
them? Because the least of it is that as we explore and extend our programming prowess, we may
accidentally become a coveted guru. I am not one to think that my domain and status is a right or
something to be protected. I say, the more – the merrier. The more gurus there are, the more fun we are
all going to have sharing our ideas and techniques, and helping each other to become more proficient
than we were.
 Page 555

Black Book Tip # 30


Enable Built-In Justify-Alignment in a RichTextBox From VB.NET.
Previously, in Black Book Tip # 9 on page 470, I provided a class that we could use to easily display
fully justified text from a TextBox control. I have been asked by a number of people if I would also
modify this class, or create another one, to additionally work with a RichTextBox control. This is
actually very easy to do, and any developer worth the status of being just intermediately knowledgeable
of Visual Basic .NET should be able to make those changes by modifying only a few lines of code.
However, if we have used the WordPad application in
Windows, or the incredible and free Jarte Rich Text Editor
(“Unlock the power behind Microsoft’s WordPad”;
www.jarte.com), we may have noticed that both of these
applications feature not only the standard Left, Right, and
Center alignment options, but they also feature a fourth
option, Justify, which performs fast and perfect full text
justification on their Rich Text Format data.
This extended functionality featured in these text processors
is not performed by auxiliary code within these applications,
perhaps doing something similar to what I did for a TextBox,
but this enhanced functionality is actually built right into the
RichTextBox control that ships with .NET and with Microsoft
Windows ever since Windows XP SP1 was released, and is
capable of performing Full Text justification on its own (for
more technical details, please refer to MSDN’s own article
“About Rich Edit Controls” at http://msdn.microsoft.com/en-
us/library/windows/desktop/bb787873(v=vs.85).aspx). Windows XP SP1
Shipped with the new RichEdit Control 3.0. The thing that is probably confusing about this was that its
DLL was still named Riched20.DLL, the system still sported a RichEdit 1.0 emulator, and its class name
remained RICHEDIT_CLASS. Windows XP SP1 and all versions of Windows following it additionally
featured the newest Rich Text Editor Class, version 4.1, renamed Msftedit.DLL, and with a new class
name of MSFTEDIT_CLASS. But even so, by default .NET still uses RichEdit 3.0.
But now that we know that the RichTextBox control we might have been using for years sported this
powerful, yet clandestine functionality all that time, we may be wondering how to access it. After all, the
control’s SelectionAlignment property does not accept anything except the values
HorizontalAlignment.Left, HorizontalAlignment.Right, and HorizontalAlignment.Center. What
gives? Primarily, it is due to its .NET class wrapper exposed to VB that holds us back.
Actually, just two things stand in our way from using Full Text Justification and many other new
features in our RichTextBox control. The first is the HorizontalAllignment class that is used to service the
SelectionAlignment property of the RichTextBox control, and the other is turning on the control’s
advanced typography options so that enhanced line-breaking and line formatting options are enabled.
The HorizontalAlighment class is defined to support only 3 values: 0=Left-Align, 1=Right-Align, and
2=Center-Align. Anything outside their integer range will result in an exception error. Sadly, the needed
new Justify option is an integer value of 4 (the values actually sent to the RTB are in fact 1 higher than
the HorizontalAlighment property reports). However, we can “cheat” by simply sending it a message.
What we need to do is pass a PARAFORMAT structure (no need for the much longer
PARAFPORMAT2. for our purposes) to the RichTextBox control with the appropriate alignment value
set. But just that is not enough. We also need to inform the RichTextBox that we want it to enable its
 Page 556

Advanced Typography Options that will enable the advanced line-breaking and line formatting options.
Without this, the text will simply remain left-aligned, which is its default state.
For some time there has been a bit of a clatter on the web regarding this issue. Everyone wants to know
how to do it, and I have seen quite a number of people offer up solutions, mostly in C#, in which they
usually state, “I think this should work...”, but all of them I have seen, and there are a lot, actually fail,
but they fail because they do not address this advanced typography issue that MSDN keeps referencing
in their notes. That being said, Lars Larson out of Denmark is the only other person who has managed to
cobble together a free-ware solution, though he did so by brute force as he was trying to help someone
else out. I managed my own more robust version by reading the tea leaves in the MSDN documentation.
Actually, when the documentation states that we must also send the RichTextBox a
EM_SETTYPOGRAPHYOPTIONS message to enable advanced line formatting, it is probably a good idea not to
ignore it, though most of us seem too focused on sending a 4=Justify message to a RichTextBox.
So, for as complicated as most people make it out to be, what we really need to do is the following:
1. Issue a SendMessageLong(RTB.Handle, EM_SETTYPOGRAPHYOPTIONS, TO_ADVANCEDTYPOGRAPHY, TO_ADVANCEDTYPOGRAPHY)
command to the RichTextBox to enable advanced typography options (yes, it was entered twice).
2. Define a PARAFORMAT structure, set its length and alignment members, and send this structure as a message to the
RichTextBox control.
That being said, why not spiff it up further by applying these changes to the text without causing any of
the usual and maddening RichTextBox flicker, especially if we need to select its whole text? We can
select the text without flickering or any brief blanking of the RichTextBox control by instead selecting
text by sending an EM_SETSEL message to the RichTextBox control, along with a tiny CHARRANGE
structure that will specify the Selection Start index and the Selection End Index.
The listing below is my modJustifyRTB module, to force a RichTextBox to justify its text. We can use it
like this: justifyRTB(myRichTextBox, TextAlign.Justify), which we can issue immediately after
loading the Rich Text control with a file or loading it with text data:
Option Strict On
Option Explicit On
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
' modJustifyRTB
' Provide extended RichTextBox services. Beyond Providing Default Left, Right,
' and Center Alignment, a fourth feature is provided, Justify, which allows full
' Text justification.
'
' The JustifyRTB() function provides this service:
'-------------------------------------------------------------------------------
' Method : justifyRTB (as Function or Subroutine)
' Purpose : Performs advanced alignment features on a RichTextBox,
' : featuring Left, Right, Center, and Justify alignment.
'------------:
' Parameters : RTB – the RichTextBox control.
' : Alignment – from the TextAlign enumeration: Left, Right, Center, Justify.
' : SelStart – Start index of text to apply the alignment to.
' : If SelStart = -1, then apply the alignment to the entire text.
' : If SelStart = -2, then use the SelectionStart and SelectionLength
' : properties of RTB to set the range.
' : SelEnd – Character length of text to apply alignment to.
' : If SelEnd = -1, then select to the end of the Text.
' : ResetSelection = True if you want to turn the selection back off (default).
' : Set this parameter to False if you want it left intact.
'------------:
' NOTE : When SelStart is set to -1 or -2, then SelEnd is ignored.
' :
' Returns : TRUE – Advanced options were applied.
' : FALSE – Advanced options application Failed.
' USES : RTBFastSelect() function for faster text selection without flicker.
'-------------------------------------------------------------------------------
' Method : RTBFastSelect (as Function or Subroutine).
' Purpose : Provide Fast Text selection service for a RichTextBox.
'------------:
' Parameters : RTBHandle -------- Handle of RichTextBox to select text within.
' : SelectionStart --- Start index of text to apply the alignment to.
' : SelectionLength – Character length of text to selection.
' Returns : <> 0 – Executed successfully.
' : 0 ------ Failure. Likely index or length value was invalid.
 Page 557

'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
Imports System.Runtime.InteropServices
Module modJustifyRTB
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
'-------------------------------------------------------------------------------
'Enumeration to specify extended text alignment in the RichTextBox.
'-------------------------------------------------------------------------------
Friend Enum TextAlign As Integer
Left = 1 'left align.
Right = 2 'right align.
Center = 3 'center text.
Justify = 4 'full justify (new as of RICHEDIT20.DLL version 3.0).
End Enum

'-------------------------------------------------------------------------------
'structure used to extend text alignment in a RichTextBox.
'-------------------------------------------------------------------------------
<StructLayout(LayoutKind.Sequential)>
Private Structure PARAFORMAT
Dim cbsize As Short 'size of this structure.
Dim dwpad As Short
Dim dwMask As Integer
Dim wNumbering As Short
Dim wReserved As Short
Dim dxStartIndent As Int32
Dim dxRightIndent As Int32
Dim dxOffset As Int32
Dim wAlignment As Short
Dim cTabCount As Short
<VBFixedArray(31)> Dim lTabstops() As Int32
End Structure
'-------------------------------------------------------------------------------
'Used Constants.
'-------------------------------------------------------------------------------
Private Const WM_USER As Int32 = &H400
Private Const EM_SETPARAFORMAT As Int32 = (WM_USER + 71)
Private Const EM_SETTYPOGRAPHYOPTIONS As Int32 = (WM_USER + 202)
Private Const EM_GETTYPOGRAPHYOPTIONS As Int32 = (WM_USER + 203)
Private Const TO_ADVANCEDTYPOGRAPHY As Int32 = &H1
Private Const PFM_ALIGNMENT As Int32 = &H8
'-------------------------------------------------------------------------------
'Pinvoke methods.
'-------------------------------------------------------------------------------
'SemdMessageLong will allow us to set and check the advanced typography option
Private Declare Function SendMessageLong Lib "user32" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByVal lParam As Int32) As Int32
'this version of SendMessage allows us to pass a PARAFORMAT structure to the system
Private Declare Function SendMessage Lib "user32.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByRef lParam As PARAFORMAT) As Int32
'this version of SendMessage allows us to pass a CHARRANGE structure to the system
Private Declare Function SendMessage Lib "user32.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByRef lParam As CHARRANGE) As Int32
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method : justifyRTB
' Purpose : Performs advanced alignment features on a RichTextBox.
'*******************************************************************************
Friend Function justifyRTB(ByRef RTB As RichTextBox,
ByVal Alignment As TextAlign,
Optional ByVal SelStart As Int32 = 0,
Optional ByVal SelEnd As Int32 = -1,
Optional ByVal ResetSelection As Boolean = True) As Boolean
'-------------------------------------------------------------------
If SelStart = -1 Then
RTBFastSelect(RTB.Handle, 0, RTB.Text.Length) 'select all text if SelStart=-1.
ElseIf SelStart <> -2 Then 'no need to select if already selected.
If SelEnd = -1 Then 'user start to end of document?
RTBFastSelect(RTB.Handle, SelStart, RTB.Text.Length - SelStart) 'use selstart to end of document.
Else
RTBFastSelect(RTB.Handle, SelStart, SelEnd) 'else user specified both start and length.
End If
End If
'-------------------------------------------------------------------
Dim fmt As New PARAFORMAT 'set aside Format structure.
fmt.cbsize = CShort(Len(fmt)) 'set the structure's length.
Dim Result As Boolean = False 'assume failure of operation to start.
' first try to tell the RichTextBox to set Advanced Typography options...
 Page 558

If SendMessageLong(RTB.Handle, EM_SETTYPOGRAPHYOPTIONS, TO_ADVANCEDTYPOGRAPHY, TO_ADVANCEDTYPOGRAPHY) <> 0 Then


' test to see if the above turned on the Advanced Typography Options...
If SendMessageLong(RTB.Handle, EM_GETTYPOGRAPHYOPTIONS, 0, 0) = TO_ADVANCEDTYPOGRAPHY Then
fmt.dwMask = PFM_ALIGNMENT 'did, so we are going to set a new alignment value.
fmt.wAlignment = CShort(Alignment) 'inform the structure we are setting alignment.
SendMessage(RTB.Handle, EM_SETPARAFORMAT, 0, fmt) 'apply the alignment instruction.
Result = True 'successfully applied advanced options.
End If
End If
'-------------------------------------------------------------------
If Not Result Then 'if Advanced Typography not available (unlikely).
If Alignment = TextAlign.Justify Then 'Justify selected, force to Left alignment.
Alignment = TextAlign.Left
End If
RTB.SelectionAlignment = DirectCast(Alignment, HorizontalAlignment) 'set alignment normally.
End If
'-------------------------------------------------------------------
If ResetSelection Then 'should we reset the cursor selection?
RTBFastSelect(RTB.Handle, 0, 0) 'yes, so deselect text.
End If
Return Result 'True if we applied advanced options.
End Function

'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
'-------------------------------------------------------------------------------
'structure to specify selection range for alignment option. Unlike usual selection of the text using
'the SelectionStart and Selection Length options, there is no screen blinking, or even temporary
' blanking if the RichTextBox when a new alignment value is set.
'-------------------------------------------------------------------------------
<StructLayout(LayoutKind.Sequential)>
Private Structure CHARRANGE
Dim chrPosnMax As Int32
Dim chrPosnMin As Int32
End Structure
'-------------------------------------------------------------------------------
'Constant used by RTBFastSelect() method, defined below.
'-------------------------------------------------------------------------------
Private Const EM_EXSETSEL As Int32 = (WM_USER + 55)
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method : RTBFastSelect
' Purpose : Provide Fast Text selection service for a RichTextBox.
'*******************************************************************************
Friend Function RTBFastSelect(ByVal RTBHandle As IntPtr,
ByVal SelectionStart As Int32,
ByVal SelectionLength As Int32) As Int32
If SelectionStart >= 0 AndAlso SelectionLength >= 0 Then 'if parameters seem to be OK.
Dim selRange As CHARRANGE 'allow quiet text selection.
selRange.chrPosnMin = SelectionStart 'mark start of text with the current selection length.
selRange.chrPosnMax = SelectionStart + SelectionLength 'mark end of text with the current selection length.
Return SendMessage(RTBHandle, EM_EXSETSEL, 0, selRange) 'select range of text.
End If
Return 0 'failure if bad indexes.
End Function

End Module

NOTE: In more recent versions of the .NET framework, the need for the above RTBFastSelect method has been superseded
by .NET’s own implementation of the RichTextBox control sporting a new overload for their Select method. With it, we can
provide it with the selection start index and the length of the text to select, and it will replace the need for RTBFastSelect
functionality. For example, “myRTB.Select(0, myRTB.Text.Length)” can be used to fast-select an entire document
without causing any screen flicker.
 Page 559

Black Book Tip # 31


Easily Replace Power Pack Shape Controls With Faster Paint Event Code.
Earlier in this document I explained how to use the Paint event of a form or control to draw lines and
shapes (see “Easy Ways to Draw Lines and Shapes, and to Paint in VB.NET” on page 325). I also
explained that we could also use the Visual Basic Power Pack Shape controls that we can download for
free from Microsoft, though this Power Pack comes pre-bundled in Visual Basic as of VB2010.
The advantage of using the Power Pack Shape controls is that they offer the convenience of shape
controls in VB6 in that we can drop a shape on a form, position it, set its color, its width, its fill, if a
rectangle or ellipse background was not transparent, and even easily create rounded rectangles.
However, I pointed out that I prefer to use Paint event code to draw these shapes because they are much
faster. Indeed, Paint event drawing is a whole lot faster. For example, if we have quite a number of
shape controls on a form, we may encounter a lot of flicker on the form if the form updates regularly.
This is eliminated by using Paint event code. This is something I have become all too familiar with.
Although I provided a number of easy methods for computing these shapes in a Paint event within that
article, the truth is most people do not want to be troubled by the bother, or it seems complicated, when
they simply want to “pretty” their form up with some shapes and focus on the code behind the form.
Since writing that article, my own line-
drawing paradigm has shifted so I find
less time to tinker with computing line
positions. For example, I am presently
developing a super spreadsheet/database
application that features a rich and
powerful programming language that
includes its own compiler. On top of that, I
am upgrading my supercharged VB6
VisualProCalc programmable calculator
application to VB.NET, and I am also
developing a complete sample email package that takes full advantage of my email classes featured
earlier in this document (see “Send (SMTP) and Retrieve (POP3) Email with Ease under VB.NET”
on page 382). Additionally, I am also writing several Black Book additions to this document, such as
this one. On top of all that, I had also been slowly going blind. No sympathy, please. I will certainly
always find plenty of things to do. I can still teach. Also, it has but recently started improving! Yay!
But anyway, lately I have found myself using the Power Pack Shape controls more and more, just to
spend less time computing line positions. But at the same time I wished I could still enjoy the incredible
speed of drawing through Paint events, especially in light of the above illustrated app, whose 27 line
controls force an irritating flicker during a form refresh. Grrr... So I thought to myself, “Self, I really
want to still render these shapes through a Paint event to regain all that lost execution speed, so what do
I need to do to convert all the data from these Shape controls and render them using much faster
drawing techniques?” The answer, of course, is to parse the form these controls are located on and
generate Paint event code to render them instead by drawing them ourselves.
This is actually rather easy to do, once we understand how Power Pack Shapes are stored. The first thing
we must understand is that Power Pack Shape controls are not individually stored in a form’s Controls
collection, or in the Controls collection of a container control, such as a Picture Box, a Panel, Tab
Control, or whatever. They are actually stored within a ShapeContainer control, which is in turn stored
in the Controls collection of a form or other container control. A ShapeContainer is automatically
created and added to the appropriate Controls list when we add Shape controls to our form. All Shape
controls that are located on a particular container will feature its own ShapeContainer control. So, if we
 Page 560

have dropped several Shape controls on a form and its controls, one ShapeContainer control will be
generated for each control the shapes are dropped on, and will contain only those shapes that have been
dropped onto that control. For example, if we drop a few Lineshape controls onto the form background,
and dropped a few OvalShape or RectangleShape controls on a Panel, then two ShapeContainer controls
will be generated. One will be attached to the form and contain all the Shape controls we dropped on the
form, and another ShapeContainer will be generated and attached to the Panel, and contain all the Shape
controls we dropped on the Panel. Were we to later delete all Shape controls that were stored in a
ShapeContainer, then once the last Shape control contained within it is deleted, then that
ShapeContainer control is automatically removed. Of course, if we later re-add new Shape controls to
that surface, a new ShapeContainer will be generated and attached to the surface Controls collection.
The trick, or so it seems from the complaints I have seen on the web, is how to process these Shape
controls. Unlike other controls, a Shape control does not inherit from the more generic Control class.
However, a ShapeContainer does, which is how it can be contained within a Controls list, though
myriad complaints also come from developers who are trying to cast them back to a ShapeContainer.
The solution to this problem is simple. Instead of treating and parsing each item in a Controls collection
as a Control, treat it instead as an Object. When any item is wrapped within an Object (technically, an
Object, its data consisting of a single 4-byte IntPtr, just points to the item), the Object simply retains the
item intact, without actual conversion, and it knows the type of object the wrapped item is, and we can
acquire that original type through the Object’s GetType property, which all derived objects inherit from,
which of course includes the wrapped item (all classes we create also ultimately inherit from type
Object, though we do not specify it, but it is one of the base requirements in .NET that all objects inherit
from Object). But the end result is, we are able to cast an Object to a ShapeContainer type.
From this we could process code like the following to find a ShapeContainer object in a Controls list:
For Each Cntrl As Object In myObj.Controls 'check each control in the collection
If TypeOf Cntrl Is Microsoft.VisualBasic.PowerPacks.ShapeContainer Then 'a shape container found?
'yes, so process the ShapeContainer.Shape control list here...
End If
Next

Of course, to find these items, we will need to scan through not only a form’s Controls list, but also the
Controls lists of any child control that in turn has children, to include the Controls list of further, inner
levels of controls. This would be accomplished by implementing a recursive method, which is a method
that can invoke itself. Some developers break into a cold sweat when we mention recursion, but
recursion is easy, fast, very tight code that is our best friend when parsing such generational levels. For
example, exploring a directory tree or a TreeView control are child’s play when using recursion.
However, once we have found our ShapeContainer control, we then need to parse its Shapes collection
(this is one of the main reasons why a ShapeContainer cannot be cast from a Control object, because
this collection differs from a Control’s Controls collection). Regardless, the Shape controls are all
derived from a base Microsoft.VisualBasic.PowerPacks.Shape class, which means that we will be able
to cast a more generic Shape object into a LineShape, RectangleShape, or OvalShape.
Two other things have stood in the way of most people successfully parsing these controls. The first is the
fact that when a person tries to write a recursion routine to parse a Controls list, the interface yells at them
because they cannot cast a Control.ControlCollection list to a Form.ControlCollection list (sounds like
someone has a stutter). This is because a Controls list for a form is different from a Controls list from a
control. Because the Forms class is auto-imported into any form project, if we specify any field as a simple
Control Collection, such as Private Sub RecursivelyScan(ByRef Cntrls As ControlCollection), we are going
to be treating each Controls list as a list from a Form. So what do we do? Easy! Just treat them all instead as
Control.ControlCollection lists. This is because a form’s Control collection inherits from a Control’s Control
collection (this is starting to sound like Abbot and Costello’s famous radio broadcast, “Who’s on First?”).
The other thing they run into is that they try to parse this Shape’s list as a Controls list, which clearly will not
work. However, the solution is simple: We simply invoke yet another method that is specifically designed to
 Page 561

parse a Shapes collection. This keeps each method small and compact and specific to their tasks. So, what we
end up with is an exposed method the user invokes to process a form. This tiny method basically passes the
form’s Controls collection to a private recursive method that scans all controls (as type Object). If they are a
normal control, they are checked for having children, meaning that they feature a Controls collection that
itself contains other controls, and this is in turn passed to the recursive method. If a ShapeContainer is found,
though, it is passed to another method that simply parses its Shapes collection after we Directcast the Object,
which we now know to be in fact a ShapeContainer, to a ShapeContainer, so we can in turn reference its
Shapes collection. There, we test for each type of shape and process them individually.
 When handling a LineShape control, we must consider its start and end coordinates, its line color, its line width
(thickness), and it’s Dash Style, to determine if it should be drawn as a solid line (typical) or some other rendition,
such as dashes, dots, dot-dashes, or whatever.
 When handling a RectangleShape, we must consider its start coordinate, its width, its height, its border color, its
border width, the border’s Dash Style, its background color if its background is opaque, and if its corners are
rounded, and by how much. Other considerations are background image and background patterns, but these
features are so-little used that I will not tackle them in this treatise (but when they are used, they are a God-send).
 When handling an OvalShape, which used to be called an Ellipse in VB6, we must consider its start coordinate
(the top-left corner of an imaginary bounding rectangle), its width, its height, its border color, its border width, the
border’s Dash Style, and its background color if its background is opaque.
With this information, we can create a Pen object that is used to draw borders, create a Brush object to
render any opaque backgrounds, and use the Graphics Drawing methods to render these objects. Note
that rounded rectangles are not supported by the Graphics object, but we can take full and easy
advantage of my RoundedRectangle method described earlier in the fore-mentioned article, “Easy
Ways to Draw Lines and Shapes, and to Paint in VB.NET” (for easy access, we will list it again, here,
though I will simply invoke it from my current method, not trudge off on some long explanation of its
usage (such as, what is an HDC?) – see the article on page 325 for that).
One other consideration is for the optimal definition and use of a Pen or Brush object. First, each should
only be defined once. Second, their properties should be updated to new colors, styles, thicknesses, etc.,
only when they actually change. Lastly, their objects should be disposed of only if they are created.
Another consideration is the code generation. Paint events should only be generated if there is data for
them to process, so we do not end up with empty Paint event code blocks.
I addressed all these issues and created a module named modShapeConvert that exposes a single
method, ShapeConvert. We supply a form name to the method (or simply Me if we are invoking it from
the form we want to scan). We can place the invocation of this method anywhere, but I typically place it
at the end of the Form’s Load event, and even then, only once I have my form in its final “form” and
will not have to move my Shape controls around later to render everything perfectly. For example,
ShapeConvert(Me).

At the end of this method, if it has generated any paint event code, it will inform us in a dialog that it has
placed this code into the clipboard. We can then paste this clipboard data into a text editor, such as
NotePad, and review the listed paint event or events. Change or add to this code as we please, and paste
what portions we want into our form’s code (I typically paste it all at the end of the form’s code). For
example, consider the following sample output code from running this on my frmVisualCalc form that
features quite a number of LineShape controls and a few RectangleShape controls, which were placed on
two panels; one named pnlScroll, and the other named pnlPickKeys.
 Page 562

'Add this code to the Paint() event for the pnlScroll Control
Private Sub PnlScroll_Paint(sender As Object, e As PaintEventArgs) Handles PnlScroll.Paint
Dim g As Graphics = e.Graphics

Dim Pn As New Pen(Color.Black, 1.0!)


g.DrawRectangle(Pn, 2, 1, 25, 21)
pn.Width = 2.0!
g.DrawLine(Pn, 576, 328, 576, 360)

pn.Dispose()
End Sub

'Add this code to the Paint() event for the pnlPicKeys Control
Private Sub pnlPicKeys_Paint(sender As Object, e As PaintEventArgs) Handles pnlPicKeys.Paint
Dim g As Graphics = e.Graphics

Dim Pn As New Pen(Color.Black, 1.0!)


g.DrawLine(Pn, 240, 132, 240, 168)
g.DrawLine(Pn, 288, 132, 288, 168)
pn.Color = SystemColors.ControlDarkDark
pn.Width = 2.0!
g.DrawLine(Pn, 240, 132, 240, 96)
g.DrawLine(Pn, 576, 232, 620, 232)
g.DrawLine(Pn, 144, 132, 384, 132)
g.DrawLine(Pn, 240, 168, 620, 168)
g.DrawRectangle(Pn, 384, 96, 192, 36)
g.DrawLine(Pn, 48, 328, 0, 328)
g.DrawLine(Pn, 240, 360, 0, 360)
g.DrawLine(Pn, 48, 360, 48, 232)
g.DrawLine(Pn, 384, 200, 624, 200)
g.DrawLine(Pn, 96, 232, 48, 232)
g.DrawLine(Pn, 96, 200, 96, 232)
g.DrawRectangle(Pn, 384, 232, 192, 97)
pn.Width = 3.0!
g.DrawLine(Pn, 0, 96, 624, 96)
pn.Width = 2.0!
g.DrawLine(Pn, 240, 392, 240, 168)
g.DrawLine(Pn, 384, 360, 384, 132)
g.DrawLine(Pn, 288, 360, 624, 360)
g.DrawLine(Pn, 480, 360, 480, 392)
g.DrawLine(Pn, 240, 200, 0, 200)
g.DrawRectangle(Pn, 0, 96, 145, 36)
g.DrawLine(Pn, 336, 96, 336, 360)
g.DrawLine(Pn, 288, 168, 288, 388)
g.DrawLine(Pn, 240, 296, 336, 296)
g.DrawLine(Pn, 144, 360, 144, 392)

pn.Dispose()
End Sub

These drawing instructions perfectly duplicated the operation of the LineShape and RectangleShape
controls I had placed on my form. Running them both together proves to me that they do not differ, so I
afterward deleted all my Shape controls (after backing up my code, of course), which in turn removed
the auto-generated ShapeContainer controls. Afterward, I was able to further remove the
Microsoft.VisualBasic.PowerPacks.VS reference from my application references (do this via the
Unused References button, just to be safe, which will ensure the reference is indeed no longer required).
Note that if rounded rectangles were used, a note will be added to the generated code, informing us that
we will need to include the modRoundedRect module (listed at the end of this article) so that the
RoundedRectangle method the generated code invokes will be supported. Lastly, note that if no code
was generated, perhaps due to no Shape controls being found on a form, that we will also be notified of
that, which could explain why we will find the clipboard void of any generated code in that case (though
it might contain any data we had last placed in it).
What follows is my modShapeConvert module code:
 Page 563

Option Strict On
Option Explicit On
'NOTE: The following assumes you have .NET references in your project properties
' for System.Drawing (usually) and also Microsoft.VisualBasic.PowerPacks.VS.
' If an error is reported for the line below, ensure these references exist.
Imports PowerPack = Microsoft.VisualBasic.PowerPacks, Draw2D = System.Drawing.Drawing2D
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
' modShapeConvert static module class
'
' The purpose of this module is to address the screen flicker that can occur when a
' form is really busy with controls. For example, I was working on my VisualProCalc
' calculator, translating it to VB.NET, and the calculator form features over 180
' controls and about 40 lines and rectangles. When the form refreshed, it tended to
' blink as the lines disappeared and then redrew.
'
' The exposed ShapeConvert() method will construct Paint Event Code for drawing lines
' rather than use shape Controls. This will build paint event code to reproduce all
' LineShape, RectangleShape, and OvalShape Controls from the Visual Basic PowerPacks
' faithfully, to include their thickness, color, dash style, and rectangle or oval
' (ellipse) background color, if not transparent.
'
' Rounded Rectangle Support is provided by the RoundRectangle() method defined within
' the modRoundRect module, which is required if rounded rectangles are featured.
'
' Certain features are not yet considered in this solution, such as background images
' and patterns, although they can be added as needed, if required.
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
Module modShapeConvert
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Private ParentForm As Form = Nothing 'storage for form ID purposes. This is
' 'needed, because a ShapeContainer can be
' 'parented by a form, if not another control.
Private MasterData As String = Nothing 'storage for conversion code output.
Private LocalData As String = Nothing 'local construction for each ShapeContainder list.
Private NeedModRoundRect As Boolean = False 'True if modRoundRect module required.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
'*******************************************************************************
' Method : ShapeConvert
' Purpose : Create Paint Event Code to replace PowerPacks LineShape and
' : RectangeShape controls to seriously speed up form updates.
'*******************************************************************************
'*******************************************************************************
Friend Sub ShapeConvert(ByRef Frm As Form)
If Frm.HasChildren Then 'if the form has controls...
ParentForm = Frm 'save this parent form.
ShapeConvertParseControls(Frm.Controls, Nothing) 'process controls on the form.
If MasterData IsNot Nothing Then 'any data written to the output?
Clipboard.SetText(MasterData, TextDataFormat.Text) 'yes, so save the data to the clipboard.
MasterData = Nothing 'and then release the text resources.

MsgBox("Paint Event Code for Line and/or Rectangle Shapes are stored on " &
"the clipboard. Paste them into a text editor for review " &
"and edits, then past the data as needed into your code." & vbCrLf &
vbCrLf &
"Once you have applied these changes to your code, you can " &
"remove the associated shape controls from your forms. Note that " &
"if you remove all shape controls from its wrapping auto-constructed " &
"ShapeContainer, the ShapeContainer will be auto-removed.",
MsgBoxStyle.OkOnly Or MsgBoxStyle.Information,
"Paint Event Replacement Code for Shapes Generated")
Else
MsgBox("No Line, Rectangle, or Oval Shape controls were found on this form.",
MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "No Shape Controls Found")
End If
End If
End Sub

'*******************************************************************************
' Method : ShapeConvertParseControls (recursive method)
' Purpose : Parse through control collections and recurse through any of their
' : children that in turn have child controls. What we are actually looking
' : for here are ShapeContainer controls in this method. Note that we parse
' : through the controls list, treating therm as objects, not as Control
' : objects, because a generic Control class cannot be cast to a ShaprContainer
' : class, but an Object can, because it is actually the ShapeContainer wrapped
' : in a generic Object box, which can easily be cast to the proper type.
'*******************************************************************************
Private Sub ShapeConvertParseControls(ByRef Cntrls As Control.ControlCollection,
ByRef ParentControl As Control)
For Each Cntrl As Object In Cntrls 'check each control in the collection.
If TypeOf Cntrl Is PowerPack.ShapeContainer Then 'a shape container found?
'yes, so process the ShapeContainer.Shape control list, which are different from normal controls on the form.
ShapeConvertParseShapse(DirectCast(Cntrl, PowerPack.ShapeContainer).Shapes, ParentControl)
Else
Dim Ctl As Control = CType(Cntrl, Control) 'otherwise, check out the non-shape control.
If Ctl.HasChildren Then 'is it a container for other controls?
ShapeConvertParseControls(Ctl.Controls, Ctl) 'yes, so recurse, and make this control the parent.
End If
End If
Next
End Sub
 Page 564

'*******************************************************************************
' Method : ShapeConvertParseShapse
' Purpose : Parse through a ShapeContainer.Shape control list and process all
' : LineShape and RectangleShape controls, building paint event code
' : for each form or control that contains them.
'*******************************************************************************
Private Sub ShapeConvertParseShapse(ByRef ShpCollection As PowerPack.ShapeCollection,
ByRef ParentControl As Control)
'---------------------------------------------------------------------------
'init default values used to update Pen and Brush objects only when they differ.
'---------------------------------------------------------------------------
Dim pnColor As Color = Nothing 'used to draw borders.
Dim pnWidth As Single 'pen width of drawing class to draw borders.
Dim DashStyle As Draw2D.DashStyle = Draw2D.DashStyle.Solid 'assume solid DashStyle to be Solid (default).
Dim BrshClr As Color = Nothing 'brush used to draw rectangle background, if opaque.

For Each Shp As PowerPack.Shape In ShpCollection 'scan each contained shape control.
'-----------------------------------------------------------
'RECTANGLE SHAPE
'-----------------------------------------------------------
If TypeOf Shp Is PowerPack.RectangleShape Then 'if a RECTANGLE Shape...
With DirectCast(Shp, PowerPack.RectangleShape)
Dim X1 As Int32 = .Location.X 'get the left coordinate.
Dim Y1 As Int32 = .Location.Y 'get top coordinate.
Dim Wdth As Int32 = .Size.Width 'get width of rectangle.
Dim Hght As Int32 = .Size.Height 'get height of rectangle.
Dim Crnr As Int32 = .CornerRadius 'grab corner radius in case of rounded rectangles.

Dim BrdrColor As Color = .BorderColor 'get the border color.


Dim BrdrWidth As Single = .BorderWidth 'get how wide the border is.
Dim BrdrStyle As Draw2D.DashStyle = .BorderStyle 'get the border drawing style.
Dim BackColor As Color = .BackColor 'get the background color.
'-------------------------------------------------------------------
'update pen properties as needed and output the appropriate text data.
'-------------------------------------------------------------------
ProcessPenData(pnColor, BrdrColor, pnWidth, BrdrWidth, DashStyle, BrdrStyle)
'-------------------------------------------------------------------
'draw the rectangle.
'-------------------------------------------------------------------
If Crnr = 0 Then 'normal, square corners = 0
LocalData &= (" g.DrawRectangle(Pn, " & X1.ToString & ", " & Y1.ToString & ", " &
Wdth.ToString & ", " & Hght.ToString & ")" & vbCrLf)
If .BackStyle = PowerPack.BackStyle.Opaque Then 'is the background opaque (visible)?
If BrshClr = Nothing Then 'yes, but has the brush color been defined yet?
BrshClr = BackColor 'no, so generate brush definition code and active brush color.
LocalData &= (" Dim Brsh As New SolidBrush(" & TranslateColorToText(BackColor) & ")" & vbCrLf & vbCrLf)
ElseIf BrshClr <> BackColor Then 'definition exists, but doe the colors differ?
BrshClr = BackColor 'yes, so update to the new color and generate change for it.
LocalData &= (" Brsh.Color = " & TranslateColorToText(BackColor) & vbCrLf)
End If
LocalData &= (" g.FillRectangle(Brsh, New Rectangle(" & X1.ToString & ", " & Y1.ToString & ", " &
Wdth.ToString & ", " & Hght.ToString & "))" & vbCrLf)
End If
Else 'Rounded Rectangle.
NeedModRoundRect = True 'mark that we need modRoundRect.
If .BackStyle = PowerPack.BackStyle.Opaque Then 'draw with a filled bckgroud if opaque.
LocalData &= (" RoundRectangle(g.GetHdc, pn, BackColor, " &
"New Rectangle(" & X1.ToString & ", " & Y1.ToString & ", " &
Wdth.ToString & ", " & Hght.ToString & "))" & vbCrLf)
Else 'draw with a transparent background if not
LocalData &= (" RoundRectangle(g.GetHdc, pn, Color.Transparent, " &
"New Rectangle(" & X1.ToString & ", " & Y1.ToString & ", " &
Wdth.ToString & ", " & Hght.ToString & "))" & vbCrLf)
End If
End If
End With
'-------------------------------------------------------
'LINE SHAPE.
'-------------------------------------------------------
ElseIf TypeOf Shp Is PowerPack.LineShape Then 'if a LINE Shape...
With DirectCast(Shp, PowerPack.LineShape)
Dim X1 As Int32 = .X1 'get the left coordinate.
Dim X2 As Int32 = .X2 'get the right coordinate.
Dim Y1 As Int32 = .Y1 'get top coordinate.
Dim Y2 As Int32 = .Y2 'get the bottom coordinate.
Dim LineColor As Color = .BorderColor 'get the line color.
Dim LineWidth As Single = .BorderWidth 'get how wide the line is.
Dim LineStyle As Draw2D.DashStyle = .BorderStyle 'get the line drawing style.
'---------------------------------------------------------------
'-------------------------------------------------------------------
'update pen properties as needed and output the appropriate text data
'-------------------------------------------------------------------
ProcessPenData(pnColor, LineColor, pnWidth, LineWidth, DashStyle, LineStyle)
'---------------------------------------------------------------
'draw the line.
'---------------------------------------------------------------
LocalData &= (" g.DrawLine(Pn, " & X1.ToString & ", " & Y1.ToString & ", " &
X2.ToString & ", " & Y2.ToString & ")" & vbCrLf)
End With
 Page 565

'-------------------------------------------------------
'OVAL (ELLIPSE) SHAPE.
'-------------------------------------------------------
ElseIf TypeOf Shp Is PowerPack.OvalShape Then 'if Oval Shape...
With DirectCast(Shp, PowerPack.OvalShape)
Dim X1 As Int32 = .Location.X 'get the left coordinate.
Dim Y1 As Int32 = .Location.Y 'get top coordinate.
Dim Wdth As Int32 = .Size.Width 'get width of rectangle.
Dim Hght As Int32 = .Size.Height 'get height of rectangle.
Dim BrdrColor As Color = .BorderColor 'get the border color.
Dim BrdrWidth As Single = .BorderWidth 'get how wide the border is.
Dim BrdrStyle As Draw2D.DashStyle = .BorderStyle 'get the border drawing style.
Dim BackColor As Color = .BackColor 'get the background color.
'---------------------------------------------------------------
'update pen properties as needed and output the appropriate text data.
'---------------------------------------------------------------
ProcessPenData(pnColor, BrdrColor, pnWidth, BrdrWidth, DashStyle, BrdrStyle)
'-------------------------------------------------------------------
'draw the Ellipse (Oval)
'-------------------------------------------------------------------
LocalData &= (" g.DrawEllipse(pn, " & X1.ToString & ", " & Y1.ToString & ", " &
Wdth.ToString & ", " & Hght.ToString & ")" & vbCrLf)
If .BackStyle = PowerPack.BackStyle.Opaque Then 'is the background opaque (visible)?
If BrshClr = Nothing Then 'yes, but has the brush color been defined yet?
BrshClr = BackColor 'no, so generate brush definition code and active brush color.
LocalData &= (" Dim Brsh As New SolidBrush(" & TranslateColorToText(BackColor) & ")" & vbCrLf & vbCrLf)
ElseIf BrshClr <> BackColor Then 'definition exists, but doe the colors differ?
BrshClr = BackColor 'yes, so update to the new color and generate change for it.
LocalData &= (" Brsh.Color = " & TranslateColorToText(BackColor) & vbCrLf)
End If
LocalData &= (" g.FillEllipse(Brsh, " & X1.ToString & ", " & Y1.ToString & ", " &
Wdth.ToString & ", " & Hght.ToString & "))" & vbCrLf)
End If
End With
End If
Next
'---------------------------------------------------------------------------
'now apply end of event code data.
'---------------------------------------------------------------------------
If LocalData IsNot Nothing Then 'process the following only if LineShape and/or RectangleShape Data Exists.
'-----------------------------------------------------------------------
'generate initial comment and Paint Event code heading.
'-----------------------------------------------------------------------
If ParentControl IsNot Nothing Then 'if the parent is a control, write the event header for that control's Paint event.
MasterData &= (vbCrLf &
" 'Add this code to the Paint() event for the " & ParentControl.Name & " Control" & vbCrLf &
" Private Sub " & ParentControl.Name & "_Paint(sender As Object, e As PaintEventArgs) Handles " &
ParentControl.Name & ".Paint" & vbCrLf)
Else 'if the parent is the form, write the event header for the form's Paint event.
MasterData &= (vbCrLf &
" 'Add this code to the Paint() event for the " & ParentForm.Name & "Form" & vbCrLf &
" Private Sub " & ParentForm.Name & "_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint" & vbCrLf)
End If
If NeedModRoundRect Then
MasterData &= (" '---------------------------------------------------------------------------" & vbCrLf &
" 'NOTE: modRoundRect module is required to support RoundedRectangle() method." & vbCrLf &
" '---------------------------------------------------------------------------" & vbCrLf)
NeedModRoundRect = False 'reset flag.
End If
'-----------------------------------------------------------------------
'Generate simplified graphics interface.
'-----------------------------------------------------------------------
MasterData &= (" Dim g As Graphics = e.Graphics" & vbCrLf & vbCrLf)
'-----------------------------------------------------------------------
'add local data to the master data
'-----------------------------------------------------------------------
MasterData &= (LocalData & vbCrLf)
'-----------------------------------------------------------------------
'close out event code
'-----------------------------------------------------------------------
MasterData &= (" pn.Dispose" & vbCrLf) 'we know this has been generated if local data exists.
If BrshClr <> Nothing Then 'if Brush Color had been defined, then a Brush object had been used.
MasterData &= (" Brsh.Dispose" & vbCrLf)
End If
MasterData &= (" End Sub" & vbCrLf) 'terminate the Paint event code.
LocalData = Nothing 'reset local data for next shape container data.
End If
End Sub

'*******************************************************************************
' Method : ProcessPenData
' Purpose : Determine how to output the Pen data. If the Pen color has not yet been
' : defined, then instantiate the Pen object with the new color and width,
' : otherwise update its Color, Width, and DashStyle as needed.
'*******************************************************************************
Private Sub ProcessPenData(ByRef OldColor As Color, ByVal NewColor As Color,
ByRef OldWidth As Single, ByVal NewWidth As Single,
ByRef OldStyle As Draw2D.DashStyle, ByVal NewStyle As Draw2D.DashStyle)
If OldColor = Nothing Then 'if the pen color has not yet been defined...
OldColor = NewColor 'assume new color and width data and define the Pen object.
OldWidth = NewWidth
LocalData &= (" Dim Pn As New Pen(" & TranslateColorToText(NewColor) & ", " & NewWidth.ToString & "!)" & vbCrLf)
Else 'or else we can assume the pen has been defined, so...
If OldColor <> NewColor Then 'if the new pen color does not match the old...
OldColor = NewColor 'update the color and update the Pen object.
LocalData &= (" pn.Color = " & TranslateColorToText(NewColor) & vbCrLf)
End If
If OldWidth <> NewWidth Then 'if the new pen width does not match the old...
OldWidth = NewWidth 'update the width and update the Pen object.
 Page 566

LocalData &= (" pn.Width = " & NewWidth.ToString & "!" & vbCrLf)
End If
End If
If OldStyle <> NewStyle Then 'if the line's border style does not match the last-set...
OldStyle = NewStyle 'update the dash style and update the Pen object.
LocalData &= (" pn.DashStyle = " & NewStyle.ToString & vbCrLf)
End If
End Sub

'*******************************************************************************
' Method : TranslateColorToText
' Purpose : Translate color value to SystemColor or Color Value.
' : if the color is unknown, use its ARGB value.
'*******************************************************************************
Private Function TranslateColorToText(ByVal Clr As Color) As String
Dim Value As String = Clr.ToString 'grab text version of color.
Dim Idx As Int32 = InStr(Value, "[") 'find bracket, if it exists.
If Clr.IsSystemColor Then 'is it a system color?
Return "SystemColors." & Mid(Value, Idx + 1, Len(Value) - Idx - 1) 'yes, so use SystemColors naming.
ElseIf Clr.IsNamedColor Then 'is it a known color?
Return "Color." & Mid(Value, Idx + 1, Len(Value) - Idx - 1) 'yes, so apply standard color naming.
Else
Return " Color.FromArgb(&H" & Hex(Clr.ToArgb) & ")" 'not known, so apply FromArgb() method.
End If
End Function

End Module

Though I dislike redundancy, it has its merits when it comes to convenience. What follows is the
modRoundedRect module listed much earlier in this document, which is used by the above code:
Option Strict On
Option Explicit On

Module modRoundRect
'*************************************************************************************************************
'RoundRectangle:
' Draw a Rounded Reactanle, provided:
' hDC : The Device Context handle from the device to render to.
' Pn : The pen used to draw the border of the shape.
' BackColor : The color to dawn the background of the shape (You can use Color.Transparent).
' Rect : Rectangle structure containing the start location (X,Y), Width, and Height of the shape in pixels.
' CornerRadius: The radius of the rounded corner in pixels.
'*************************************************************************************************************

'*************************************************************************************************************
' API Stuff.
'*************************************************************************************************************
' solid pen constant
Private Const PS_SOLID As Integer = 0 'Pen Style for solid pen (used by CreatePen())
' create a solid pen. Return its handle
Private Declare Function CreatePen Lib "gdi32.dll" Alias "CreatePen" (ByVal nPenStyle As Integer,
ByVal PixelWidth As Integer,
ByVal crColor As Integer) As Integer
' create a solid brush object. Return its handle
Private Declare Function CreateSolidBrush Lib "gdi32.dll" Alias "CreateSolidBrush" (ByVal crColor As Integer) As Integer
' Select/replace an object. Reutn the old handle for the type of object
Private Declare Function SelectObject Lib "gdi32.dll" Alias "SelectObject" (ByVal hDC As IntPtr,
ByVal hObject As Integer) As Integer
' release the resources of a created object
Private Declare Function DeleteObject Lib "gdi32" Alias "DeleteObject" (ByVal hObject As Integer) As Integer
' Draw a rounded Rectangle. Return non-zero for success
Private Declare Function RoundRect Lib "gdi32.dll" Alias "RoundRect" (ByVal hDC As IntPtr,
ByVal X1 As Integer,
ByVal Y1 As Integer,
ByVal X2 As Integer,
ByVal Y2 As Integer,
ByVal CornerWidth As Integer,
ByVal CornerHeight As Integer) As Integer

'*************************************************************************************************************
'RoundRectangle Method.
'*************************************************************************************************************
Public Sub RoundRectangle(ByVal hDC As IntPtr,
ByVal Pn As Pen,
ByVal BackColor As Color,
ByVal rect As Rectangle,
ByVal CornerRadius As Integer)
Dim oldhPen As Integer 'store old Pen handle
Dim oldhBrush As Integer 'store old Brush handle

' Declare our local Pen and Brush


Dim hPen As Integer = CreatePen(PS_SOLID, CInt(Pn.Width), ARGBtoRGB(Pn.Color)) 'create our Pen and save its handle.
Dim hBrush As Integer = CreateSolidBrush(ARGBtoRGB(BackColor)) 'create our color brush.

' select our Pen and Brush and save the ones we are replacing
oldhPen = SelectObject(hDC, hPen)
oldhBrush = SelectObject(hDC, hBrush)
 Page 567

' draw rounded rectangle


RoundRect(hDC, rect.Location.X,
rect.Location.Y,
rect.Location.X + rect.Size.Width,
rect.Location.Y + rect.Size.Height,
CornerRadius, CornerRadius)

' restore the old Pen and Brush


SelectObject(hDC, oldhPen)
SelectObject(hDC, oldhBrush)

'Delete the resources of our Pen and Brush


DeleteObject(hPen)
DeleteObject(hBrush)
End Sub

Public Sub RoundRectangle(ByVal hDC As IntPtr,


ByVal Pn As Color,
ByVal BackColor As Color,
ByVal rect As Rectangle,
ByVal CornerRadius As Integer)
Dim oldhPen As Integer 'store old Pen handle
Dim oldhBrush As Integer 'store old Brush handle

' Declare our local Pen and Brush


Dim hPen As Integer = CreatePen(PS_SOLID, 1, ARGBtoRGB(Pn)) 'create our Pen and save its handle.
Dim hBrush As Integer = CreateSolidBrush(ARGBtoRGB(BackColor)) 'or create our color brush.

' select our Pen and Brush and save the ones we are replacing
oldhPen = SelectObject(hDC, hPen)
oldhBrush = SelectObject(hDC, hBrush)

' draw rounded rectangle


RoundRect(hDC, rect.Location.X,
rect.Location.Y,
rect.Location.X + rect.Size.Width,
rect.Location.Y + rect.Size.Height,
CornerRadius, CornerRadius)

' restore the old Pen and Brush


SelectObject(hDC, oldhPen)
SelectObject(hDC, oldhBrush)

' Delete the resources of our Pen and Brush


DeleteObject(hPen)
DeleteObject(hBrush)
End Sub

'*************************************************************************************************************
'ARGBtoRGB Method
'Helper function to covert Color ARGB value (AARRGGBB) to RGB (00BBGGRR)
'*************************************************************************************************************
Private Function ARGBtoRGB(ByVal clr As Color) As Integer
Dim vARGB As Integer = clr.ToArgb 'convert color value to AARRGGBB.
Return RGB((vARGB >> 16) And &HFF, (vARGB >> 8) And &HFF, vARGB And &HFF) 'return RGB color (00BBGGRR).
End Function
End Module
 Page 568

Black Book Tip # 32


Dealing With the Form Cursor Not Displaying Over Form Controls.
Under VB6, we would set the screen.Mousepointer property to a desired cursor, such as the hourglass,
and the hourglass would display all across the form, over its controls and everything. When this code
was upgraded to VB.NET, it changed a simple VB6 screen cursor-changing command, such as:
Screen.MousePointer = vbHourglass

to:
System.Windows.Forms.Cursor.Current = System.Windows.Forms.Cursors.WaitCursor 'WaitCursor = Hourglass

Probably the worst thing about this change was that it does not do anything. The problem here is that it
is trying to assign the cursor to the Forms Collection, Perhaps that once propogated the cursor setting to
all forms in that collection, but it does not do so now. That would have been a good idea.
However, where using a Wait Cursor is concerned, a UseWaitCursor property was added to all forms
since .NET Framwork 2.0 that will display the Wait Cursor on the form and all of its child controls when
set to True (there is also My.Application.UseWaitCursor, which will set or reset the UseWaitCursor
property on all open forms in the application). But the UseWaitCursor property does not address
displaying any other possible cursors, especially if we want to display a custom cursor.
Sadly, changing the screen cursor will have no affect on the object-based environment of .NET. This is
because VB6 was a child of the screen surface, and each of its visible controls were treated as a part of
the surface of that screen, and so whatever the screen cursor was set to, they automatically inherited.
Because VB.NET is an object-oriented environment that is distinctly separate from the screen, and it
displays all of its forms as window objects that are anchored on the screen, it therefore treats the screen
as a wholly separate object. This is why setting the screen cursor does not affect a VB.NET, C#, or F#
control, because they each have their own separate cursor interfaces that affect the cursor appearance
when the cursor moves over them.
Because these cursor changes are also tagged in the list of notated upgrades in code migrated to
VB2008, when we encounter them as we scour upgraded code for applying additional suggested
changes, we typically changed the “upgraded” line to a simpler instruction, such as “ Me.Cursor =
Cursors.WaitCursor”, and then continued to march through the other changes needed in the application.
We did this simply because the cursor would actually update when it passed over the form.
The problem with this solution, however, is that the cursor will display the new cursor value only over
the form’s background; hence, the introduction of the UseWaitCursor property. If we move over a control
that is on that form, the cursor will revert to that control’s default cursor, as it will on each of the form’s
other controls. Obviously, it will revert to the newly-set cursor when we move the mouse pointer back
over the form background. This makes perfect sense, because we had only set the form’s cursor to a wait
cursor; we did not set it to any of its controls, which all also possess their own Cursor property.
If we look for assistance online for treating cursors other than the Wait Cursor like this, we will see
blogs and chatrooms scattered with queries about how to in fact update across all controls on the form.
The typical (and correct) answer is to propagate the new cursor setting to all of the form’s controls. This
might give us a blank expression because that sounds like a lot of work, but it is not. Because .NET
attaches Control lists to each form as a Controls collection, we can quickly For...Each our way through
all of them in a fraction of a millisecond. However, this might seem a bit trickier when the form also
features container controls, such as panels and picture boxes, which can in turn sport control lists
containing controls that treat those container objects as their parent, not the form. Because those other
controls are not parented by the form, they are therefore not referenced in the form’s own Controls
collection.
 Page 569

Some people express profound aggravation for the simple fact that a Form Control List, though still
named Controls, and is stated to be of type ControlCollection, just like the Controls containers for a
Panel and a picture box, it is not the same type of ControlCollection as those other objects.
Specifically, a form contains a Form.ControlCollection list, and the other container objects, being
themselves controls, instead contain a Control.ControlCollection list. The confusion usually stems
from the fact that within form code, the System.Windows.Forms.Form class is inherited by default, and so
when we simply type an undecorated ControlCollection, it automatically assumes the Form variation.
Even so, a Form.ControlCollection is inherited from a Control.ControlCollection, and as such, if we
define our processing method to reference only a Control.ControlCollection, even when we pass it the
Controls collection from a form, everything will still be hunky-dory.
My standardized solution is to paste a boiler-plate Property at the top of each form that is always named
MeCursor, which will be used to replace the “Me.Cursor” part of a cursor assignment. Thus, wherever I
am setting the form’s cursor, I would replace the usual “ Me.Cursor” with “MeCursor”, instead, such as
“MeCursor = Cursors.WaitCursor”. The MeCursor property is defined below, along with its recursive
support method, named “SetCursorToChildren”:
'*******************************************************************************
' Property : MeCursor
' Purpose : Set cursor to more than just the main form.
' Usage : When assigning a new cursor to the form, assign it using: MeCursor = NewCursor.
' : Note also that you can assign from the Cursors collection, or load a cursor from resources.
' : Unlike the UseWaitCursor property. You can afterward reassign child control cursors.
'*******************************************************************************
Friend Property MeCursor As Cursor
Get
Return Me.Cursor 'return the form's current cursor setting.
End Get
'------
Set(value As Cursor)
If Me.Cursor <> value Then 'is the form cursor already set to this?
Me.Cursor = value 'no, so set cursor to the main form...
If Me.HasChildren Then 'and if it also has children...
SetCursorToChildren(Me.Controls, value) 'then set its child controls as well.
End If
End If
End Set
End Property

'*******************************************************************************
' Method : SetCursorToChildren
' Purpose : Support MeCursor property.
' : Set parent's cursor also to Child controls.
'*******************************************************************************
Friend Sub SetCursorToChildren(ByRef ControlList As Control.ControlCollection, ByRef Value As Cursor)
For Each Cntrl As Control In ControlList 'process all child controls.
If Cntrl.Visible Then 'is the control visible?
Cntrl.Cursor = Value 'set its cursor.
If Cntrl.HasChildren Then 'does it have child controls?
SetCursorToChildren(Cntrl.Controls, Value) 'process its child controls.
End If
End If
Next
End Sub

Normally, I place the common SetCursorToChildren support method in a common module so that all
forms can share it. Regardless, this solution has served me very well and has never failed me. It is also
incredibly fast, even on massive forms that are heavily laden with controls and layers of controls.
NOTE: Some people would prefer to name this property CurrentCursor instead of MeCursor, or even an archaic
ScreenCursor. If you feel froggy, then jump. Make it comfortable for you, not me.
NOTE: If we have a control that should actually retain a non-busy cursor, such a STOP button in a Search dialog, then we
should set it to the non-busy state, such as Cursors.Default, but after we have set MeCursor to the non-default state.
 Page 570

Black Book Tip # 33


Passing Scalar Values and Strings To and From a Byte Array.
A long time ago in a galaxy far, far away, we saved and read data on our personal computers on cassette
tapes (does anyone except us old farts even remember cassette tapes? Or paper tapes? Or even punched
cards?). A problem with using cassette tapes this way was that each data item had required a leader
buffer of about 150 bytes to synchronize the tape data transfer rates, which were seldom consistent
between tape machines, to the computer’s reading software. So, if we were to write three 16-bit Short
Integers (considered regular Integers back in those ancient 8-bit computer days) to the cassette, this
meant that a long synchronization (synch) “leader” had to be written before each item, and then a 8-bit
length byte was sent out, which was a value of 2, representing the number of bytes to follow, which was
enough to hold the two bytes for a Short Integer, and finally the two bytes of the Short Integer was sent,
and then an additional byte, called a Checksum, was sent so the reader could verify the data was sent
OK. A Checksum in this case was usually the simple addition of all data bytes sent, with over-flow
ignored, and was used to verify that the integrity of the data transfer was to be assumed OK. It was not a
perfect system, but surprisingly reliable, because the receiver, as it read each data byte, summed each
value, and it would compare this summation to the follow-up Checksum Byte, or, as a later innovation,
to a more reliable Checksum Integer, and if they did not match we got the dreaded Checksum Error.
After all that, we started the entire process over again on the next data item.
If that was not bad enough, the data was recorded at only 500 baud, which is to say, 500 bits per second,
which is not all that fast, even on a computer that back then was running at a paltry 0.77MHz. This was
still faster than logging onto bulletin boards on the budding Internet in those days, running at 110 baud,
using a black, heavier-than-lead AT&T handset pressed into a modem cradle (AT&T’s monopoly back
then, fully sanctioned by the US Government, hindered telecommunications technology far more than we
want to ever dare guess. This was one of the main reasons for their breakup by the US Government back
in 1982, which ripped open the flood-gates for all the regional “Baby Bells”. Interestingly, most of the
sudden proliferation of telecommunications advances were coming from AT&T’s own Bell Laboratories,
likely in an attempt to soften the common view back then of them having been an evil monopoly, though
they were made a monopoly in the first place to prevent copycat knock-offs from robbing them of all
their initial technological development and deserved profits, though even this was tainted by corrupt
politicians and money changing hands in secret, smoke-filled rooms, which ultimately prevented genuine
“outside” technological innovations from ever seeing the light of day).
Cassette software innovations were quick to follow, such as adding an option to read and write tapes at a
then-blazing 1500 baud on better quality tape machines. Other innovations came from developers. One
of my very first assembly language programs (written in the code the computer’s Central Processing
Unit (CPU) actually understood), was a cassette interface that used a 16-bit integer length value, which
allowed me to transmit blocks of data of up to 32K at a time, and it wrote a much shorter 50-byte leader,
which medium-quality cassette tape machines could reliably support, and used only a 16-byte synch
buffer between contiguous data blocks.
But that still was not fast enough. Fortunately, Phil Allen, Bill Gates, and Monte Davidoff, before
starting Micro-Soft, later re-monikered Microsoft, worked for Ed Roberts at MITS in Albuquerque, New
Mexico, where they developed the massively powerful and feature-rich BASIC-80, which was also
famously known as Level II BASIC, and was also famously known for a paper tape copy of it being
stolen at a computer exposition by some low-life hobbyists, who then commenced to duplicate the tape
and distribute it by the truckload.
NOTE: Paper tape was durable one-inch-wide black paper ribbon that had a tiny sprocket-hole track down its middle, and
additional, larger holes were punched on either side of the sprocket track by a machine to represent, like Morse Code,
different ASCII values. These used (pre-owned is the idiotic newspeak word now used in its place) devices were popular
among Ham Radio Operators, after Telegraph Wire and News Services and Military Services began retiring them, which they
 Page 571

used to record and rebroadcast information. Well..., until computer hobbyists, not even having cassette interfaces available,
needed an inexpensive medium to save and load their programs, and surplus used paper tape machines were once available
everywhere for almost no money, like their companion teletype keyboards, which were used to replaced the computer panel
switches to enter data. Plus, they also typed on long reams of fanfold perforated paper, which predated the once-popular
modified television monitors, or by RF Channel 3 or 4 encoding on standard TVs that was later adopted by video game
manufacturers (now I am really starting to feel old, by cracky).
But one of the innovations in BASIC-80 was the Make-String functions. These functions allowed us
budding software developers to pack a lot of data into a single text string (characters in these strings
back then were only 8-bit, not the 16-bit Unicode characters we use now), whether it was scalar data,
such as doubles or integers, to multiple text lines. But the point was, these things were packed together,
just like a structure or a spreadsheet table record, and it required only a single synch leader to write all
that data, making the backup and reload processes run at relatively blazing speeds. By our program
being able to know how to process this long string of data, and not only stuff it with various variables,
but extract them as well, business applications became viable on such now-primitive computers, which
caught IBM’s attention, and hence began the world-wide computer explosion. One day we were running
to find a pay phone or thinking about shelling out $120 for a 4-function calculator, and the next day we
are doing everything on our personal computers and smart devices (so, when I hear youngsters wailing
that they only have a crappy old 3GHz computer with a stock hi-rez graphics card, just 2GB of memory,
and a paltry 1TB drive, I say “Cry me a river, build a bridge, and get over it!”).
Interestingly, the idea of storing this data in strings or in byte arrays has never died out. Indeed, I used
packed string arrays in 2006 when I designed a VB6 version of a Union structure that someone needed
(see Implementing Unions in VB.NET on page 265). And I never stopped using byte arrays, which
replaced the old 8-bit string arrays. They are too useful, especially in computer language development,
for which I have designed a number of custom in-house languages for companies over the years. Even
better, I can instantly convert these byte arrays into data streams and rocket that data to and from files or
memory streams, using them to quickly pass massive quantities of information between synchronized
applications, across a network, or across the internet.
But I have always been fascinated that so many recent high-level developers have never heard of,
remembered, or thought of using them as an easy solution to passing massive stores of data in a single stream
of information. They look at me like I have a third eye, even as I explain that they can also embed the
formatting for the stream right within the leader of the byte array itself, eliminating the need to transmit
separate formatting instructions. The best part is, byte array data streams are so easy to use, add data to, and
extract data from. Even better, because we can define their data, layout and even easily encrypt them, they
can be a very secure means to transmitting proprietary information in a single block of data. This method is
so convenient that .NET even supports byte array conversions through their BitConverter class, in turn a
member of the System class, where we can convert any scalar or string into a byte array, or convert a byte
array to a scalar or string. One drawback, though it is easy enough to work around, is that they do not support
the Decimal or Byte type (ironic), which I need to use a lot.
There is nothing magical about the data format. It is simply an array of bytes, where the data, be it 32-bit
integers, 64-bit double-precision floating point values, 128-bit decimal values, 8-bit bytes, 8-bit or 16-bit
strings, or 16-bit characters, is simply stacked on top of each other. One data item ends and the other will
begin immediately after it. All we need to do is keep track of this exacting order. Because only we will
know the order, this is one level of security already defined within this format. Encryption makes this
data even more mysterious to prying eyes, because they will have no clue how to interpret this chain of
data. Only programs designed with knowledge of the stacked data sequencing will be able to use it.
Applications I like using them in is data records or information structures, which often contain a number
of string fields that can often be of variable length. By transmitting a stream that contains the length of a
record, and also the length and type of each item contained within it, the need for maintaining fixed
record lengths are a thing of the past. This data can also be conveniently streamed anywhere, between
 Page 572

computers, between computer applications via messages, to across a local network, an intranet, or the
internet, and even as an email attachment.
In answer to all this, I have written a VB module named modByteAryXChng. This module contains 20
exposed conversion methods; 10 methods to support reading a Decimal, Long, Double, Single, Integer,
Short, Char, Byte, UTF8 String (a string that has each of its Character elements defined from a 8-bit UTF8
value), and a standard Unicode String from a byte array, and 10 methods to support writing a Decimal, Long,
Double, Single, Integer, Short, Char, Byte, UTF8 String, and a standard Unicode String to a byte array. With
these methods, we can go “old-school” and specify offsets within the array to place or fetch each of the data
items we require, or we can allow the methods to determine their sequential positioning. Further, we can
specify a character count for either type of string. When we specify a character count, a null terminator is
either not appended after the string if writing, or not expected if reading the string. If we do not supply a
character count for strings, then during writing, the entire string is saved to the byte array, and then a null
terminator is added to mark its end. If we do not supply a character count when reading a string, the data will
be read until a null terminator is encountered; though the null terminator will not be appended to the string
(it automatically supplies its own internal null terminator, anyway).
NOTE: UTF8 8-bit characters are a very efficient format, and can compress most-all typically 16-bit character values into
an 8-bit package. We probably use these all the time and never realize it. After all, most people tend to think of a Character
as containing a value from 0 to 255, and we typically assign a character to a string using the CHR and ASC methods.
Whether we realize it or not, we are performing UTF8 operations, though we probably just think of them as ASCII.
For the easiest possible use of these methods, we can ignore offsets and character counts. In this case, to
create a byte array that will automatically keep track of where to write and read data, we can defined our
byte array and initialize it like this:
' initialize receiving buffer (assume its starting offset is now 0).
Dim myData() As Byte = Nothing
'
' write a standard Unicode string to the buffer
UniToByteAry("This is a string to stuff to the byte array", myData)
IntToByteAry(1234I, myData) 'write an integer value to the byte array.
DblToByteAry(123.4567R, myData) 'write a double value to the byte array.
DecToByteAry(12345678.12345678D, myData) 'write Decimal value to byte array.
StrToByteAry("Send Chars of string as UTF8 bytes", myData) 'send a UTF8-formatted string to byte array.

We can later read the above written data using the following code. Note that because we are not
supplying offsets, we must read this data in the EXACT order that we wrote it:
ByteOffsetIndex = 0 'initialize starting offset within the byte array.
'
Dim myUni As String = ByteAryToUni(myData) 'pick up a normal String value from the byte array.
Dim myInt As Int32 = ByteAryToInt(myData) 'pick up an Integer value from the byte array.
Dim myDbl As Double = ByteAryToDbl(myData) 'pick up a Double value from the byte array.
Dim myDec As Decimal = ByteAryToDec(myData) 'pick up a Decimal value from the byte array.
Dim myStr As String = ByteAryToStr(myData) 'pick up a UTF8 string of bytes and convert to a string.

If we want to record and later use the offsets to each data item, we do not need to initialize the
ByteOffsetIndex value. We can also read the data in any order of our choosing simply by providing the
desired offset in the byte array. For example:
' initialize receiving buffer (assume its starting offset is now 0).
Dim myData() As Byte = Nothing
'
' write a string to the buffer.
Dim UniOffset As Int32 = GetNextOffset(myData) 'OPTIONAL - offset position in array where string is written.
UniToByteAry("This is a string to stuff to the byte array", myData)
Dim IntOffset As Int32 = GetNextOffset(myData) 'OPTIONAL - offset position in array where Integer is written.
IntToByteAry(1234I, myData) 'write an integer value to the array.
Dim DblOffset As Int32 = GetNextOffset(myData) 'OPTIONAL - offset position in array where Double is written.
DblToByteAry(123.4567R, myData) 'write a double value to the array.
Dim DecOffset As Int32 = GetNextOffset(myData) 'OPTIONAL - offset position in array where Decimal is written.
DecToByteAry(12345678.12345678D, myData) 'write Decimal value to byte array.
Dim StrOffset As Int32 = GetNextOffset(myData) 'OPTIONAL - offset position in array where UTF8 string is written.
StrToByteAry("Send Chars of string as UTF8 bytes", myData) 'send UTF8-formatted string.
 Page 573

We can then read this data in any order we desire. For example:
Dim myDec As Decimal = ByteAryToDec(myData, DecOffset) 'pick up a Decimal value from the byte array.
Dim myStr As String = ByteAryToStr(myData, StrOffset) 'pick up a UTF8 string of bytes and convert to a string.
Dim myInt As Int32 = ByteAryToInt(myData, IntOffset) 'pick up an Integer value from the byte array.
Dim myUni As String = ByteAryToUni(myData, UniOffset) 'pick up a normal String value from the byte array.
Dim myDbl As Double = ByteAryToDbl(myData, DblOffset) 'pick up a Double value from the byte array.

What follows is the modByteAryXChng module:


Option Strict On
Option Explicit On
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
' modByteAryXChng: Copy numeric or string data to and from a byte array, specifying an optional
' offset within an array where the data is to be located (if the array is pre-
' sized). The offset is zero-based, not 1-based as a VB MID() string is, so
' the first position in a string is position 0 in the offset. This is useful
' for reading/writing blocks of fixed data in files as streams.

' The following functions are provided to extract values from a Byte array buffer.
' You will be expected to provide and offset within the buffer where you want to
' read the data into the selected data type. If you are reading fixed-length
' string data, you will also be required that you provide a character count to read,
' otherwise it will read until a null string terminator is encountered
'
'NOTE: If you do not want to specify or keep track of an offset when reading this
' data to the Byte Array, then simply initially assign ByteOffsetIndex = 0
' prior to reading any data. This special variable will maintain the index for
' the next item after each invocation of a read method.
'
'NOTE: For strings, if you do not require fixed-length strings, then do not provide
' a CharCount value with the string. The string routines will then write the
' data to the byte array, and then follow it with a null terminator to mark its end.
' If you do have fixed-length strings, but still do not want to provide a CharCount
' value, then do not do so, and the fixed-length string will be written, and a Null
' terminator will be appended to it.
'
' ByteAryToDec(): convert byte array data to a 128-bit Decimal.
' ByteAryToDbl(): convert byte array data to a 64-bit Double-precision floating point value.
' ByteAryToLng(): convert byte array data to a 64-bit Long Integer.
' ByteAryToSng(): convert byte array data to a 32-bit Single-Precision floating point value.
' ByteAryToInt(): convert byte array data to a 32-bit Integer.
' ByteAryToSht(): convert byte array data to a 16-bit Short Integer.
' ByteAryToByt(): convert byte array data to an unsigned 8-Byte.
' ByteAryToChr(): convert byte array data to a 16-bit Unicode Character.
' ByteAryToStr(): convert byte array data as UTF8 data to a String (each byte is converted to a 16-bit character).
' ByteAryToUni(): convert byte array data to a 16-bit Unicode string (each 2 bytes convert to a 16-bit character).
'-------------------------------------------------------------------------------
' The following subroutines are provided to add values to a Byte buffer.
' They will also automatically adjust the size of the provided array to fit the
' data if the data is detected as being placed beyond the end of the array, so you
' can start out with an empty byte array, such as: Dim myData() As Byte = Nothing.
' Note also that if the byte array is is intially defined as Nothing, that it will
' be automatically initialized to accommodate this data the first time you try to
' write any data to the array.

'NOTE: If you do not want to provide an offset value, which is normally useful in
' pre-sized byte data arrays, where you can access its contained data much like
' a database record, the data will simply be appended to the end of the byte array
' by default. Note further that if you do want to record where each item will be
' appended to the data array, you can simply record the UBound+1 value of the array
' prior to writing the data if you initialized your array to Nothing. However, in
' this case, you can either assume this value is zero at the start, or include
' error trapping, because trying to read a UBound of an array set to Nothing will
' generate an exception error. So try something like this:
'
' Dim myOffset = GetNextOffset(myArray) 'get offset where next item will be written.
'
'NOTE: When writing strings, if you want to write a fixed-length string, you can
' specify a character count, and that data will be written to the byte array
' without a terminating null tag. If you want to transmit a variable-length
' string, or a fixed-length string, but when you later read it you do not want
' to have to specify a character count, then the string will be written to the
' array with a null terminator tag.
'
' DecToByteAry(): convert Decimal to byte array data.
' DblToByteAry(): convert Double to byte array data.
' LngToByteAry(): convert Long to byte array data.
' SngToByteAry(): convert Single to byte array data.
' IntToByteAry(): convert Integer to byte array data.
' ShtToByteAry(): convert Short to byte array data.
' BytToByteAry(): convert Byte to byte array data.
 Page 574

' ChrToByteAry(): convert 16-bit Unicode Character to byte array data.


' StrToByteAry(): convert each character of a String to a UTF8 single byte in the byte array data.
' UniToByteAry(): convert each 16-bit character of a String to consecutive two bytes in the byte array data.
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
Imports System.Runtime.InteropServices
Module modByteAryXChng
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'Create Unions to overlap data types.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<StructLayout(LayoutKind.Explicit)>
Friend Structure CharAndBytes
<FieldOffset(0)> Dim itmChr As Char 'overlapping Unicode Character (16-bits).
<FieldOffset(0)> Dim itmByt0 As Byte 'Byte + 0 (overlapping)
<FieldOffset(1)> Dim itmByt1 As Byte 'Byte + 1.
End Structure

<StructLayout(LayoutKind.Explicit)>
Friend Structure DecimalAndLongs
<FieldOffset(0)> Dim itmDec As Decimal 'overlapping Decimal.
<FieldOffset(0)> Dim itmLng0 As UInt64 'overlapping Int64 (Long).
<FieldOffset(8)> Dim itmLng1 As UInt64 'overlapping Int64 (Long).
End Structure
Friend ByteOffsetIndex As Int32 = 0 'if initialized to 0 before reading from the provided byte array,
' 'this value will contain the offset index for the next data item to read.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
'functions to read data FROM a Byte Array.
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------

'*******************************************************************************
' Method : ByteAryToDec()
' Purpose: convert byte array data to Decimal
'*******************************************************************************
Friend Function ByteAryToDec(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1) As Decimal
Dim st As DecimalAndLongs 'set aside space for a Union structure.
Dim Ary() As Byte = StuffFromByteAry(ByteAry, ByteAryOffset, 16) 'grab 16 Bytes to a Byte array.
st.itmLng0 = BitConverter.ToUInt64(Ary, 0) 'convert the first 8 Bytes to an Unsigned Long.
st.itmLng1 = BitConverter.ToUInt64(Ary, 8) 'convert the second 8 Bytes to an Unsigned Long.
Return st.itmDec 'reconsitute both Unsigned Longs into a Decimal.
End Function

'*******************************************************************************
' Method : ByteAryToDbl()
' Purpose: convert byte array data to Double.
'*******************************************************************************
Friend Function ByteAryToDbl(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1) As Double
Return BitConverter.ToDouble(StuffFromByteAry(ByteAry, ByteAryOffset, 8), 0) 'convert 8 byte array to Double.
End Function

'*******************************************************************************
' Method : ByteAryToLng()
' Purpose: convert byte array data to Long.
'*******************************************************************************
Friend Function ByteAryToLng(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1) As Int64
Return BitConverter.ToInt64(StuffFromByteAry(ByteAry, ByteAryOffset, 8), 0) 'convert 8 byte array to Long.
End Function

'*******************************************************************************
' Method : ByteAryToSng()
' Purpose: convert byte array data to Single.
'*******************************************************************************
Friend Function ByteAryToSng(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1) As Single
Return BitConverter.ToSingle(StuffFromByteAry(ByteAry, ByteAryOffset, 4), 0) 'convert 4 byte array to Single.
End Function
 Page 575

'*******************************************************************************
' Method : ByteAryToInt()
' Purpose: convert byte array data to Integer.
'*******************************************************************************
Friend Function ByteAryToInt(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1) As Int32
Return BitConverter.ToInt32(StuffFromByteAry(ByteAry, ByteAryOffset, 4), 0) 'convert 4 byte array to Integer.
End Function

'*******************************************************************************
' Method : ByteAryToSht()
' Purpose: convert byte array data to Short.
'*******************************************************************************
Friend Function ByteAryToSht(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1) As Int16
Return BitConverter.ToInt16(StuffFromByteAry(ByteAry, ByteAryOffset, 2), 0) 'convert 2 byte array to Short.
End Function

'*******************************************************************************
' Method : ByteAryToByt()
' Purpose: convert byte array data to Byte.
' NOTE : It probably would be faster for the user to do this, but this method
' : is included for completelness and it does to bounds checking.
'*******************************************************************************
Friend Function ByteAryToByt(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1) As Byte
Return StuffFromByteAry(ByteAry, ByteAryOffset, 1)(0) 'grab 1 byte from array and return it.
End Function

'*******************************************************************************
' Method : ByteAryToChr()
' Purpose: convert byte array data to 16-bit Unicode Character.
'*******************************************************************************
Friend Function ByteAryToChr(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1) As Char
Return BitConverter.ToChar(StuffFromByteAry(ByteAry, ByteAryOffset, 2), 0) 'convert 2 byte array to Char.
End Function

'*******************************************************************************
' Method : ByteAryToStr()
' Purpose: convert byte array data as UTF8 data to a String (each byte converted to a 16-bit character).
'*******************************************************************************
Friend Function ByteAryToStr(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1,
Optional ByVal CharCount As Int32 = -1) As String
If ByteAryOffset < 0 Then 'if invoker did not specify a start offset index...
ByteAryOffset = ByteOffsetIndex 'then assume we are using the ByteOffsetIndex value.
Else
ByteOffsetIndex = ByteAryOffset 'always keey this value in check, to prevent overflow.
End If
If CharCount > 0 AndAlso
CharCount + ByteAryOffset - 1 <= UBound(ByteAry) Then 'if byte count seems to be in bounds...
ByteOffsetIndex += CharCount 'bump offset index beyond fixed-length string data.
Dim Str As String = Nothing 'init result.
For Idx As Int32 = 0 To CharCount - 1 'grab the byte count from the array.
Str &= Chr(ByteAry(ByteAryOffset + Idx)) 'stuff to a string.
Next
Return Str.TrimEnd 'strip any space padding from its end.
Else 'if no character count supplied...
Dim Str As String = Nothing 'init result.
For idx As Int32 = ByteAryOffset To UBound(ByteAry) 'scan from current offset toward end of data.
ByteOffsetIndex += 1 'bump offset index for size of current data item.
Dim c As Byte = ByteAry(ByteAryOffset + idx) 'grab code.
If c = 0 Then 'If terminator...
Exit For 'then done.
Else
Str &= Chr(c) 'else append UTF8-formatted byte data as a 16-bit Char.
End If
Next
Return Str.TrimEnd 'strip any space padding from its end.
End If
End Function
 Page 576

'*******************************************************************************
' Method : ByteAryToUni()
' Purpose: Convert byte array data to 16-bit Unicode string (each 2 bytes convert to 16 bit character).
'*******************************************************************************
Friend Function ByteAryToUni(ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1,
Optional ByVal CharCount As Int32 = -1) As String
If ByteAryOffset < 0 Then 'if invoker did not specify a start offset index...
ByteAryOffset = ByteOffsetIndex 'then assume we are using the ByteOffsetIndex value.
Else
ByteOffsetIndex = ByteAryOffset 'always keey this value in check, to prevent overflow.
End If
If CharCount > 0 AndAlso
CharCount * 2 + ByteAryOffset - 1 <= UBound(ByteAry) Then 'if byte count seems to be within bounds...
ByteOffsetIndex += (CharCount << 1) 'bump offset index for fixed-length string data.
Dim St As CharAndBytes 'set up a byte/char exchange.
Dim Str As String = Nothing 'init result.
For Idx As Int32 = 0 To CharCount * 2 - 1 Step 2 'grab 2 bytes at a time from the array.
St.itmByt0 = ByteAry(ByteAryOffset + Idx) 'stuff them to the exchange structure.
St.itmByt1 = ByteAry(ByteAryOffset + Idx + 1)
Str &= St.itmChr 'extract the resulting 16-bit Char to the string.
Next
Return Str.TrimEnd 'strip any spaces from its end.
Else
Dim St As CharAndBytes 'set up a byte/char exchange.
Dim Str As String = Nothing 'init result.
For idx As Int32 = ByteAryOffset To UBound(ByteAry) Step 2
ByteOffsetIndex += 2
St.itmByt0 = ByteAry(ByteAryOffset + idx) 'grab two consecutive bytes.
St.itmByt1 = ByteAry(ByteAryOffset + idx + 1)
If St.itmByt0 = 0 AndAlso St.itmByt1 = 0 Then 'If terminator...
Exit For 'then done.
Else
Str &= St.itmChr 'else append data.
End If
Next
Return Str.TrimEnd 'strip any spaces from its end.
End If
End Function

'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
'functions to write data TO a Byte Array.
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------

'*******************************************************************************
' Method : DecToByteAry()
' Purpose: convert Decimal to Byte Array data.
'*******************************************************************************
Friend Sub DecToByteAry(ByVal NumVal As Decimal,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1)
StuffToByteAry(NumVal, ByteAry, ByteAryOffset) 'send Decimal data to the byte array.
End Sub

'*******************************************************************************
' Method : DblToByteAry()
' Purpose: convert Double to Byte Array data.
'*******************************************************************************
Friend Sub DblToByteAry(ByVal NumVal As Double,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1)
StuffToByteAry(NumVal, ByteAry, ByteAryOffset) 'send Double data to the byte array.
End Sub

'*******************************************************************************
' Method : LngToByteAry()
' Purpose: convert Long to Byte Array data.
'*******************************************************************************
Friend Sub LngToByteAry(ByVal NumVal As Int64,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1)
StuffToByteAry(NumVal, ByteAry, ByteAryOffset) 'send Long data to the byte array.
End Sub
 Page 577

'*******************************************************************************
' Method : SngToByteAry()
' Purpose: convert Single to Byte Array data.
'*******************************************************************************
Friend Sub SngToByteAry(ByVal NumVal As Single,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1)
StuffToByteAry(NumVal, ByteAry, ByteAryOffset) 'send Single data to the byte array.
End Sub

'*******************************************************************************
' Method : IntToByteAry()
' Purpose: convert Integer to Byte Array data.
'*******************************************************************************
Friend Sub IntToByteAry(ByVal NumVal As Int32,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1)
StuffToByteAry(NumVal, ByteAry, ByteAryOffset) 'send Integer data to the byte array.
End Sub

'*******************************************************************************
' Method : ShtToByteAry()
' Purpose: convert Short to Byte Array data.
'*******************************************************************************
Friend Sub ShtToByteAry(ByVal NumVal As Int16,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1)
StuffToByteAry(NumVal, ByteAry, ByteAryOffset) 'send Short data to the byte array.
End Sub

'*******************************************************************************
' Method : BytToByteAry()
' Purpose: convert Byte to Byte Array data.
'*******************************************************************************
Friend Sub BytToByteAry(ByVal NumVal As Byte,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1)
StuffToByteAry(NumVal, ByteAry, ByteAryOffset) 'send Short data to the byte array.
End Sub

'*******************************************************************************
' Method : ChrToByteAry()
' Purpose: convert 16-bit Unicode Character to byte array data.
'*******************************************************************************
Friend Sub ChrToByteAry(ByVal Chr As Char,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1)
StuffToByteAry(Chr, ByteAry, ByteAryOffset) 'send 16-bit Char data to the byte array.
End Sub

'*******************************************************************************
' Method : StrToByteAry()
' Purpose: convert each character of a String to a single byte in the byte array data.
'*******************************************************************************
Friend Sub StrToByteAry(ByVal Str As String,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1,
Optional ByVal FixedLength As Boolean = False)

If Not FixedLength Then 'if not a fixed-length string...


Str &= Chr(0) 'add a null terminator to the string.
End If
Dim LnStr As Int32 = Len(Str) 'get length of string.
If LnStr > 0 Then 'if string contains data...
If ByteAryOffset < 0 Then 'if offset not defined, we will append to end.
If ByteAryOffset < 0 Then 'if offset not defined, we will append to end.
ByteAryOffset = GetNextOffset(ByteAry) 'get offset to data destination.
End If
End If
Dim UB As Int32 = ByteAryOffset + LnStr - 1 'compute require Ubound value for Array.
If ByteAry Is Nothing OrElse
UB > UBound(ByteAry) Then 'if greater than current...
ReDim Preserve ByteAry(UB) 'then size to required size.
End If
For Idx As Int32 = 0 To LnStr - 1
ByteAry(Idx + ByteAryOffset) = CByte(Str.Substring(Idx, 1)) 'stuff each byte to byte array.
Next
End If
End Sub
 Page 578

'*******************************************************************************
' Method : UniToByteAry()
' Purpose: convert each 16-bit character of a String to consecutive two bytes in the byte array data.
'*******************************************************************************
Friend Sub UniToByteAry(ByVal Str As String,
ByRef ByteAry() As Byte,
Optional ByVal ByteAryOffset As Int32 = -1,
Optional FixedLength As Boolean = False)

If Not FixedLength Then 'if not a fixed-length string...


Str &= Chr(0) 'add a null terminator to the string.
End If
Dim LnStr As Int32 = Len(Str) 'get length of string.
If LnStr > 0 Then 'if string contains data...
If ByteAryOffset < 0 Then 'if offset not defined, we will append to end.
ByteAryOffset = GetNextOffset(ByteAry) 'get offset to data destination.
End If
Dim UB As Int32 = ByteAryOffset + LnStr * 2 - 1 'compute require Ubound value for Array.
If ByteAry Is Nothing OrElse
UB > UBound(ByteAry) Then 'if greater than current...
ReDim Preserve ByteAry(UB) 'then size to required size.
End If
Dim St As CharAndBytes 'set up a byte/char exchange.
For Idx As Int32 = 1 To LnStr
St.itmChr = CChar(Mid(Str, Idx, 1)) 'stuff 16-bit unicode character.
ByteAry(ByteAryOffset) = St.itmByt0 'stuff two bytes derives from character.
ByteAry(ByteAryOffset + 1) = St.itmByt1
ByteAryOffset += 2 'add two bytes for each characters.
Next
End If
End Sub

'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
'Support functions.
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------

'*******************************************************************************
' Method : GetNextOffset
' Purpose: Assuming the byte array to be written to is initially set to Nothing,
' : pick up its Ubound value + 1 to get the offset where the next data
' : item will be written to in this array.
'*******************************************************************************
Friend Function GetNextOffset(ByRef ByteAry As Byte()) As Int32.
If ByteAry Is Nothing Then 'if Byte Array is not yet defined...
Return 0 'we will start at its very beginning.
Else
Return UBound(ByteAry) + 1 'otherwise, return its top + 1.
End If
End Function

'*******************************************************************************
' Method : StuffFromByteAry()
' Purpose: Fill sub-byte array with selected bytes from a byte array and return the structure.
'*******************************************************************************
Private Function StuffFromByteAry(ByRef ByteAry() As Byte, ByVal Offset As Int32, ByVal ByteCount As Int32) As Byte()
If ByteCount > 0 Then
If Offset < 0 Then 'if invoker did not specify a start offset index...
Offset = ByteOffsetIndex 'then assume we are using the ByteOffsetIndex value.
Else
ByteOffsetIndex = Offset 'always keey this value in check, to prevent overflow.
End If
ByteOffsetIndex += ByteCount 'bump offset index for size of data.

If ByteCount + Offset - 1 <= UBound(ByteAry) Then 'if everything seems to be in range...


Dim Ary(ByteCount - 1) As Byte 'size the receiving array.
For Idx As Int32 = 0 To ByteCount - 1 'trasnfer needed bytes from the master array.
Ary(Idx) = ByteAry(Idx + Offset)
Next
Return Ary 'return the array for processing.
End If
End If
Return Nothing
End Function
 Page 579

'*******************************************************************************
' Method : StuffToByteAry()
' Purpose: convert Scalar Data to Byte Array data.
' NOTE : AutoExpand Byte Array to fit data we are stuffing into it
'*******************************************************************************
Private Sub StuffToByteAry(ByVal sender As Object, ByRef ByteAry() As Byte, ByVal ByteAryOffset As Int32)
If ByteAryOffset < 0 Then 'if offset not defined, we will append to end.
ByteAryOffset = GetNextOffset(ByteAry) 'get offset to data destination.
End If
Dim Ary() As Byte = Nothing 'initialize array.
Select Case sender.GetType.Name
Case GetType(Decimal).Name
Dim st As DecimalAndLongs 'set up conversion structure.
st.itmDec = DirectCast(sender, Decimal) 'grab decimal values.
Ary = BitConverter.GetBytes(st.itmLng0) 'grab first half as a long.
Dim AryB() As Byte = BitConverter.GetBytes(st.itmLng1) 'grab second half as a long.
ReDim Preserve Ary(15) 'resize first to accomodate second.
For Idx As Int32 = 0 To 7 'transfer second to top of first.
Ary(Idx + 8) = AryB(Idx)
Next
Case GetType(Double).Name
Ary = BitConverter.GetBytes(DirectCast(sender, Double)) 'get byte array of Double Data.
Case GetType(Long).Name
Ary = BitConverter.GetBytes(DirectCast(sender, Long)) 'get byte array of Long Data.
Case GetType(Single).Name
Ary = BitConverter.GetBytes(DirectCast(sender, Single)) 'get byte array of Single Data.
Case GetType(Integer).Name
Ary = BitConverter.GetBytes(DirectCast(sender, Integer)) 'get byte array of Integer Data.
Case GetType(Short).Name
Ary = BitConverter.GetBytes(DirectCast(sender, Short)) 'get byte array of Short Data.
Case GetType(Char).Name
Ary = BitConverter.GetBytes(DirectCast(sender, Char)) 'get byte array of Char Data.
Case GetType(Byte).Name
ReDim Ary(0)
Ary(0) = DirectCast(sender, Byte) 'get byte array of Byte Data.
End Select
'-----------------------------------------------
Dim AryUBound As Int32 = UBound(Ary) 'get Ubound size of data.
Dim UB As Int32 = ByteAryOffset + AryUBound 'compute require Ubound value for destination.
If ByteAry Is Nothing OrElse
UB > UBound(ByteAry) Then 'if greater than current...
ReDim Preserve ByteAry(UB) 'then size to required size.
End If
'-----------------------------------------------
For Idx As Int32 = 0 To AryUBound 'trasnfer required bytes...
ByteAry(Idx + ByteAryOffset) = Ary(Idx) 'to destination buffer.
Next
End Sub

End Module
 Page 580

Black Book Tip # 34


Display Buttons Labels Wider Than the Button Normally Allows.
Have you ever needed to define small buttons that contain little text, but the text is still
too wide to fully display on the button? For example, recently I needed to define a
number of function buttons that had to be sized to 41 x 25 Pixels. My problem was,
even relatively short labels that were of 8-point Consolas Bold did not fully display,
such as the labels “Style”, “Beep”, “Print”, “NxLbl” and “PvLbl”. They would instead
display as “Styl” “Bee”, “Prin”, “NxL” and “PvL”. Grrr...
This was especially troubling, because I was converting this application from VB6,
where the labels had displayed perfectly. It was time to take a look at how VB6 and
VB.NET displayed label text.
After putting on my mining helmet, packing a lunch, notifying the police of my whereabouts, and
disassembling blocks of code, I discovered that VB6 printed label text much more freely than the more
robust methods used by .NET, which tightly limited the zone where text was displayable within more
restrictive margin properties (typically 3 pixels on each edge), all in an attempt to minimize redrawing when
things like a button was pushed. It might make execution a whole lot faster, but it was stepping on my toes.
Not having any plans to put my freak on and do anything that might be diagnosed as insane, all I was
wanting to do was display some labels that would naturally fit within the bounds of the button, though
the text might sometimes approach edges of the buttons that .NET did not normally take kindly to. With
that, I decided to process the painting of the text for all these buttons through their Paint Events.
When we enter the Paint Event for a button, the point at which we enter is right after the button and its
text has been drawn. Therefore, at that point, all the drawing chores have already been performed.
However, with a little creativity, we can redraw and reshape them however we wish. For example, if we
grab the Bounds of a button and draw a rectangle using the background color that the button is sitting
on, we can render the button invisible. We can then redefine its shape, such as drawing a circle, or
drawing an image over its surface, by using some creative rendering based upon whether the cursor is
hovering over the button, pressing it, or releasing it, we can create our own custom interactions. For
example, one trick I like to do is place buttons above a color ribbon and redraw them as file folder tabs,
blending the active button into the ribbon to make it look as though the button and the color ribbon was
merged into a single object. This is very much how tabs (actually buttons) on tab controls are rendered.
The trick for us presently, however, is to not interfere as much as possible with the normal button
drawing process. What we want to do is to draw the label text ourselves, leaving everything else intact.
The problem is, though, that the text for the label has already been drawn onto the button prior to
entering the paint event. Were we to simply try and hide the button’s rendering of the label by setting the
button’s ForeColor property to its BackColor property, we would certainly end up with buttons without
observable text, but the buttons would also look like they were all washed out, lacking a defined border.
This is because the ForeColor property helps define the overall appearance of the button, establishing its
border, and helping to define colorization of its “3D” edge effects. Without this contrast, the appearance
of the button tends to blend into one flat, featureless field.
A quick solution to this issue is to assign the text to the button’s Tag property. Our problem with that is
that we cannot see the button text until the application is running, which makes it difficult to use during
the development phase. The solution to this, of course, is to place the button’s text into its Tag property
and erase its Text property, but only when the application is firing up, and the best place to do this is in
the Form’s Load Event. Because I want to render all my buttons uniformly, so their appearance will not
look any different than other buttons that do not need to be adjusted; I prefer to render all buttons on a
form through the same event processes.
 Page 581

So what I need to do is to process all controls in the Form’s Controls collection. Not only that, because
controls can sit on top of and be part of the background control collections, we will need to process them
as well. To do that, we need to, as we have done previously in these Black Book tips and in other articles
in this document, perform function recursion.
We do this by passing the form’s Controls collection to a recursive method. This recursive method will
parse each control in the provided collection and determine if it is a Button. It might be a good idea to
also test for CheckBox controls whose Appearance property is set to Button as well, because these can
suffer the same issue. If we find a match, we can copy the control’s Text property to its Tag property and
then assign its Text property a value of Nothing to hide its text. We can then assign a Paint Event handler
to it so WE can paint its text. This event handler will grab the button text from its Tag property and
center it on the Button control, or on the Checkbox that is being drawn with a Button appearance.
To handle this, I expose a method named InitButtonControls that takes the desired form as its
parameter. For example, in a form’s Load Event, we can add InitButtonControls(Me) to it. It in turn
simply passes its Controls list to a private recursive method also named InitButtonControls. This is the
beauty in function overloading. That... and we do not have to think up another method name. ;-)
The Paint Event, simply named Button_Paint, is another private method. It renders the generic sender
parameter as a Control, extracts the button text from its Tag property, figures out how to center this text
on the button using the TextRederer.MeasureText method and the dimensions of the current button, and
then draws it using the button’s ForeColor property.
Insert the following code in our form, or in a module for re-use in other applications:
'*******************************************************************************
' Method Name : InitButtonControls (exposed overloaded method)
' Purpose : modify all form buttons to render their text through a Paint event.
'*******************************************************************************
Friend Sub InitButtonControls(ByRef Frm As Form)
InitButtonControls(Frm.Controls) 'process the form's controls list.
End Sub

'*******************************************************************************
' Method Name : InitButtonControls (private overload)
' Purpose : modify all form buttons to render their text through a Paint event.
' : Parse through a control list and find buttons. If a control has
' : children, then process its Controls list through recursion.
'*******************************************************************************
Private Sub InitButtonControls(ByRef ControlsList As Control.ControlCollection)
For Each cntrl As Control In ControlsList 'process each control in the list.
If cntrl.Visible AndAlso TypeOf cntrl Is Button OrElse
(TypeOf cntrl Is CheckBox AndAlso
DirectCast(cntrl, CheckBox).Appearance =
Appearance.Button) Then 'if button or checkbox drawn as button...
If cntrl.Text IsNot Nothing Then 'if its Text property contains data...
cntrl.Tag = cntrl.Text 'copy its Text to its Tag property.
cntrl.Text = Nothing 'and then erase the Text property.
AddHandler cntrl.Paint, AddressOf Button_Paint 'and apply a Paint event handler to it.
End If
End If
If cntrl.HasChildren Then 'if the control, button or not, has child controls...
InitButtonControls(cntrl.Controls) 'then recurse to process those child controls.
End If
Next
End Sub

'*******************************************************************************
' Method Name : Button_Paint
' Purpose : modify display to draw text in a wider field on the buttons than normally allowed.
' : It will also display it perfectly vertically centered, even on thin buttons, which
' : otherwise tend to push the text too far down, even if you had aligned them top-center.
'*******************************************************************************
Private Sub Button_Paint(sender As Object, e As PaintEventArgs)
Dim btn As Control = CType(sender, Control) 'grab the button or checkbox as a Control.
Dim Txt As String = CStr(btn.Tag) 'grab its text from its Tag property.
Dim siz As Size = TextRenderer.MeasureText(Txt, btn.Font) 'get the size of this text.
Dim X As Int32 = (btn.Width - siz.Width) \ 2 + 1 'computer horizontal center. Add 1 for alignment.
Dim Y As Int32 = (btn.Height - siz.Height) \ 2 'computer vertical center.
Dim brsh As New SolidBrush(btn.ForeColor) 'set its background brush to the ForeColor.
e.Graphics.DrawString(Txt, btn.Font, brsh, New Point(X, Y)) 'draw text, centered in the button.
brsh.Dispose() 'release allocated resources.
End Sub 'all done.
 Page 582

Black Book Tip # 35


Performing Selections or inserting RichText Data Off-screen Without Scrolling.
There has been consternation on the web about the maddening habit of a RichTextBox control scrolling
to its SelectStart into view each time a select is made that is not already displayed within its viewport. I
have seen countless examples of developers intercepting the System Message Queue and trying to
intercept focus, because a RichTextBox does not scroll when it does not have focus, or intercepting its
scroll messages so they can stub them out (disable them) while they are trying to apply cosmetic text
edits on its data. They might even send Pinvoke messages to get the scroll range, apply an edit, and then
reset that position, or just save bookmarks and use them to minimize the flickering scroll effects.
The problem with all these solutions is that they still cause sometimes severe screen jitters and jumps.
However, to cleanly solve this issue is actually quite simple. First, we need to save the current selection
position and any selection length. Next, we need to save the position of the character in the top-left
corner of the control. This information allows us to fully recover the control position where we might
have been typing. But even with that, we can still get some display jump as the control momentarily
scrolls to a changed position before returning the display to exactly where it was beforehand. To
completely hide that annoying hiccup, we can temporarily suspend paint updates to the RichTextBox.
After we suspend paint updates to a control, we can do whatever updates we need to do to it, such as
append text, or whatever. The advantage here is that when the updates to a control are suspended,
manipulating them becomes extremely fast (for as fast as displays have gotten, they are still one of the
slowest components of a computer system). And all those changes will also be completely invisible.
Because of this, I tend to lock paint updates to any control, whether it be a RichTextBox, a regular
TextBox, a ListBox, a ComboBox, a ListView, or a TreeView control. Traditionally, this was accomplished
using the LockWindow Pinvoke, though this only allowed one control at a time to have paint events
suspended. Fortunately, .NET supersedes this by providing every control featuring a paint event with
SuspendLayout and ResumeLayout methods that can temporarily suspend their individual paint events.
Once we have completed our behind-the-scenes work, we need to restore the top-left corner position to
the control, restore any previous selection, and finally resume paint updates to the Rich Text control.
Grabbing the current selection points is easy. For example:
Dim pSelStart As Int32 = RTB.SelectionStart 'save current selection and length.
Dim pSelLength As Int32 = RTB.SelectionLength

We can also acquire the character index position in the top-left corner of the control like this:
Dim pCaretTopLeft As Int32 = RTB.GetCharIndexFromPosition(New Point(0, 4)) 'save top-left pos. (allow 4 pixels).

Notice that instead of pointing to the absolute top-left corner ( New Point(0, 0)), we instead pointed 4
pixels down, otherwise we might actually be capturing the previous line, which extends downward.
We then need to lock paint updates to the RichTextBox control. We can do that with the control’s own
SuspendLayout method, such as myControl.SuspendLayout().
Once we have suspended repaints to the control, we can then go about dickering away at it, such as
appending text, a file, or colorizing lines of source code in a source code editor we might be building.
And when we are finished with our aside-changes, we need to reset the top-left position to the top-left of
the control, reset the previous selection, and finally restore painting updates via the control’s
ResumeLayout method to refresh it, to make it look just as it did prior to our side-changes, like so:
With RTB
.SuspendLayout() 'suspend repaints to this control.
'Do whatever you need to do to your text here. And once done...
.Select(pCaretTopLeft, 0) 'select old top-left character position, but using no selection length.
.ScrollToCaret() 'scroll this selection to the top displayed line position of the control.
.Select(pSelStart, pSelLength) 'select old text and length, if any.
.ResumeLayout() 'resume repaints to this control and redraw it for current appearance.
End With
 Page 583

This will avoid any flicker and annoying control scrolling. The only thing we might notice is that if we
initially start and the top line of text was only half-exposed. It will likely shift a pixel or two down for
full exposure, but that sounds to me more like a useful feature. Besides, we cannot easily partial-scroll.
If we have an older version of .NET, we might not be able to use the newer overload of the
RichTextBox’s Select method. In that case, we can either re-select positions normally, such as:
.SelectionStart = pCaretTopLeft 'use if .Select(pos, len) does not work for you.
.SelectionLength = 0 'use if .Select(pos, len) does not work for you.

and:
.SelectionStart = pSelStart 'use if .Select(pos, len) does not work for you.
.SelectionLength = pSelLength 'use if .Select(pos, len) does not work for you.

Or, we can use the flicker-free RTBFastSelect method featured in Black Book Tip # 30, Enable Built-In
Justify-Alignment in a RichTextBox from VB.NET, on page 555. Regardless, setting the SelectionStart
and SelectionLength properties of the RichTextBox will be fast and flicker-free anyway, because painting
to the control has been suspended.
And that is all there is to it!
 Page 584

Black Book Tip # 36


Opening an Associated Application Without a File.
Sometimes we may want to open an application associated with a file extension, but without actually
opening a file with that extension. For example, suppose we want to provide our user with the option to
open whatever editor they have associated with TXT or RTF files, even though, by default, NotePad is
associated with TXT files and WordPad is associated with RTF files.
Users quite often re-associate file types when using File Explorer to be opened by their favorite editors
rather than the defaults. For example, I use Jarte as my RTF file editor. When the applications I use give
me the option to open up an RTF editor, I would like for Jarte to open up instead of WordPad.
Were we to use the system’s RegEdit utility to examine the registry, we find the file extension
associations in the HKey_Classes_Root Hive. We can also find it reflected in the HKey_LocalMachine
Hive under its Software\Classes key. Normally, the default value in the key for the extension is the
extension plus “file”. So, if we were looking for the extension “.txt”, we will usually find “txtfile” as its
default key. If we were looking for the extension “.rtf”, we will usually find “rtffile” as its default key.
For example, the following code will pick up this associated key for “.rtf”:
'get the default key associated with the extension ".rtf".
Dim RTFkey As String = My.Computer.Registry.ClassesRoot.OpenSubKey(".rtf").GetValue(Nothing).ToString

Notice that the GetValue method specified Nothing as its Value member for the key. This is how we
acquire a Default value member.
The next thing we need to do is to find the command path for this retrieved key. This is always found as
the default value under the associated key’s “\Shell\Open\Command” sub-key path. Thus:
'get the default command used for launching the associated application.
Dim RTFcmd As String = My.Computer.Registry.ClassesRoot.OpenSubKey(
RTFkey & "\shell\open\command").GetValue(Nothing).ToString

NOTE: The “shell” part of the path refers to the Windows Shell, such as Shell32.DLL or Shell64.DLL.
We next need to deal with decorations at the end of the retrieved path, which usually specifies the path
to the executable and then parameters and/or optional parameters. For an association with Wordpad, this
is typically "%ProgramFiles%\Windows NT\Accessories\WORDPAD.EXE" "%1". After all, this is the OPEN
command that is used to open files, and so such commands are expected to have a file or files following
them. Thus, we would anticipate seeing such things as %1, or a more robust "%1", or even additional
slash-tagged parameters following the command path. These must be trimmed off, otherwise some
editors will choke when we invoke them using this command string, as Wordpad will, because no valid
parameter path was supplied for the “%1” tag, which it will look for. Notepad just ignores bad files.
We can trim those parameters off using the following simple bit of code:
Dim Index As Int32 = AppPath.IndexOf("""%1") 'check for typical decorations.
If Index = -1 Then
Index = AppPath.IndexOf("%1") 'if robust type not found, check for typical...
End If
If Index = -1 Then
Index = AppPath.IndexOf("/"c) 'if not typical, check for slash-params, which Word uses...
End If
If Index <> -1 Then 'if decorations found, then trim them off.
AppPath = AppPath.Substring(0, Index).TrimEnd
End If
Shell(AppPath, DisplayStyle) 'Finally. launch the associated application.

We should also wrap this whole thing within a error trap, such as a Try...End Try block.
Consider the following function, LaunchDefaultOpenerForExt, that does the things we have discussed:
 Page 585

'*******************************************************************************
' Method Name : LaunchDefaultOpenerForExt
' Purpose : Launch the application that is associated with a specified file extension.
' : Return True if success, or False if the extension was not found or there
' : is no association, or the executable was not found where expected.
'*******************************************************************************
Friend Function LaunchDefaultOpenerForExt (ByVal Extension As String,
Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle.NormalFocus) As Boolean
If Extension.IndexOf("."c) = -1 Then 'prep provided extension.
Extension = "." & Extension 'precede it with a "." if it lacks one.
Else
Extension = IO.Path.GetExtension(Extension.Trim.ToLower) 'ensure we have just the extension.
End If
Try
' get the default key associated with the extension
Dim AppPath As String = My.Computer.Registry.ClassesRoot.OpenSubKey(
Extension).GetValue(Nothing).ToString
' get the default command used for launching the associated application.
AppPath = My.Computer.Registry.ClassesRoot.OpenSubKey(
AppPath & "\shell\open\command").GetValue(Nothing).ToString
' trim any trailing decorations, such as "%1", or parameters, such as /n.
Dim Index As Int32 = AppPath.IndexOf("""%1") 'check for typical decorations.
If Index = -1 Then
Index = AppPath.IndexOf("%1") 'if robust type not found, check for typical...
End If
If Index = -1 Then
Index = AppPath.IndexOf("/"c) 'if no typical, check for slash-params, which Word uses...
End If
If Index <> -1 Then 'if decorations found, then trim them off.
AppPath = AppPath.Substring(0, Index).TrimEnd
End If
Shell(AppPath, DisplayStyle) 'Finally, launch the associated application.
Return True 'return success.
Catch
Return False 'return failure if errors were detected.
End Try
End Function

Grabbing Open-With Preferences


Now that we have looked at associations, we may also want
to take a look at what application the user has decided will
open a file. For example, a TXT file might be associated
with Notepad, but the user may in fact have chosen to open
it by default using another utility, such as the Jarte Rich
Text Editor. They may have done this by right-clicking the
file, selecting the “Open With” option (we may see “Open
With...” instead if it had not been selected before), and
selecting the “Choose Default Program...” option.
In this case we may want to go through just a little more
work and provide a more robust solution to this issue. To
do that, we may choose to instead look into the
HKey_Current_User Registry Hive under the Explorer file
extensions list, which will always be located at
“Software\Microsoft\Windows\CurrentVersion\Explorer\
FileExts”. Here, we will find keys listed for all file extensions that the File Explorer browser knows
about, such as “.txt” and “.rtf”. Under the extension key, we will find an OpenWithList key that will
contain a list of value keys that are demarked by an alphabet Value name (see the illustration). How we
determine which application is to open the file by default is by examining the MRUList value key. Its
text string, “edgacbjif” in this case, contains a list of alphabet characters, defining the alphabet list in
the order they should be displayed in a File Explorer open-with list. More importantly, the very first
entry indicates the program the user wants to use to open files with the TXT extension. In this example,
 Page 586

it indicates that the “e” entry is the application that should be used to open TXT files. In this case, the
“e” value key specifies “Jarte.exe”.
Once we have the application, we need to find its physical system location. Explorer does that by
looking under the HKey_Classes_Root hive in its “Application” key, where we will find a sub-key
named “Jarte.exe”. Beneath it, if we look to its “Shell/Open/Command” branch, we will find that its
default entry specifies the path to the
executable, along with any needed
parameters. For example: "C:\Program
Files (x86)\Jarte\Jarte.exe" "%1" .
programmatically, to gather all this information for the “.rtf” file extension we can first find the
executable defined for opening it like this:
' get the Registry key for the “.rtf” extension.
Dim ExtKey As Microsoft.Win32.RegistryKey = My.Computer.Registry.CurrentUser.OpenSubKey(
"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.rtf\OpenWithList")
' get the name of the application associated with the extension.
Dim AppPath As String = ExtKey.GetValue(ExtKey.GetValue("MRUList").ToString.Substring(0, 1)).ToString
' get the EXE path to the application.
AppPath = My.Computer.Registry.ClassesRoot.OpenSubKey(
"Applications\" & AppPath & "\shell\open\command").GetValue(Nothing).ToString

The first line will define a registry key that is associated by Explorer with the “.rtf” file extension. This
is used by the second line to grab the MRUList key value, and then from its first character of data we
then grab the application that should be used to open it. In my case, this is “Jarte.exe”. The third line
gathers the shell open command associated with the selected application.
Two potential problems presently exist. Not all file extensions contain an OpenWithList key, especially if
the user has never tried to open the file with anything but the default application. Second, even if it has
an OpenWithList key it might not contain a MRUList value key, especially if a user selected Open With,
but then aborted. If either of these scenarios fail, it is not a big issue. In this case we can easily open the
associated application as specified in the above LaunchDefaultOpenerForExt method.:
' get the the Registry key for the sought extension.
Dim ExtKey As Microsoft.Win32.RegistryKey = My.Computer.Registry.CurrentUser.OpenSubKey(
"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" & Extension).OpenSubKey("OpenWithList")
Try
If ExtKey IsNot Nothing Then 'if not nothing; OpenWithList exists...
ExtKey.GetValue("MRUList").ToString() 'if OpenWithList exists, check for MRUList.
End If
Catch
ExtKey = Nothing 'if not, we can open it using the default.
End Try
If ExtKey Is Nothing Then
Return LaunchDefaultOpenerForExt(Extension, DisplayStyle) 'if not, we can open it using the default.
End If
' get the name of the application associated with the extension.
Dim AppPath As String = ExtKey.GetValue(ExtKey.GetValue("MRUList").ToString.Substring(0, 1)).ToString
' get the EXE path to the application.
AppPath = My.Computer.Registry.ClassesRoot.OpenSubKey(
"Applications\" & AppPath & "\shell\open\command").GetValue(Nothing).ToString

Because this opening command will likely hold the same possible trailing decorations as in the previous
example, such as "%1", we will also need to trim them from this data.
My more enhanced methods to support all this, which includes the previous method, is as follows. Note
that it also allows us to grab the associated paths, and common support methods have been added.
 Page 587

Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modLaunchAssociatedApp Static Class Module
' Launch app associated with a file extension, or launch the app selected to open it.
' You can also get the app paths for the above options.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modLaunchAssociatedApp

'*******************************************************************************
' Method Name : LaunchSelectedOpenerForExt
' Purpose : Launch the application that is used to open a specified file extension.
' : Return True if success, or False if the extension was not found or there
' : is no association, or the executable was not found where expected.
' : if TryDefaultIfNotFound = TRUE (default), then if a selected opener not
' : found, then try to launch the default associated app.
'*******************************************************************************
Friend Function LaunchSelectedOpenerForExt(ByVal Extension As String,
Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle.NormalFocus,
Optional ByVal TryDefaultIfNotFound As Boolean = True) As Boolean
Dim AppPath As String =
GetSelectedOpenerForExt(Extension, TryDefaultIfNotFound) 'get the path associated with an extension.
If Not String.IsNullOrWhiteSpace(AppPath) Then 'if one was found...
Shell(AppPath, DisplayStyle) 'launch the associated application, less extensions.
Return True 'return success.
Else
Return False 'fail.
End If
End Function

'*******************************************************************************
' Method Name : LaunchDefaultOpenerForExt
' Purpose : Launch the application that is associated with a specified file extension
' : Return True if success, or False if the extension was not found or there
' : is no association, or the executable was not found where expected.
'*******************************************************************************
Friend Function LaunchDefaultOpenerForExt(ByVal Extension As String,
Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle.NormalFocus) As Boolean
Dim AppPath As String = GetDefaultOpenerForExt(Extension) 'get default associated app.
If Not String.IsNullOrWhiteSpace(AppPath) Then 'if we found it...
If AppPath IsNot Nothing Then 'if we found it...
Shell(AppPath, DisplayStyle) 'launch the associated application, less extensions.
Return True 'return success.
End If
Return False 'return failure if errors were detected.
End Function

'*******************************************************************************
' Method Name : GetSelectedOpenerForExt
' Purpose : Get the path to the application that is associated with a specified file extension.
'*******************************************************************************
Friend Function GetSelectedOpenerForExt(ByVal Extension As String,
Optional ByVal TryDefaultIfNotFound As Boolean = True) As String
Extension = CleanExtension(Extension) 'clean up extension or extract extension.
Try
' get the the Registry key for the sought extension.
Dim ExtKey As Microsoft.Win32.RegistryKey = My.Computer.Registry.CurrentUser.OpenSubKey(
"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" & Extension).OpenSubKey("OpenWithList")
Dim AppPath As String
' get the name of the application associated with the extension.
AppPath = ExtKey.GetValue(ExtKey.GetValue("MRUList").ToString.Substring(0, 1)).ToString
' get the EXE path to the application.
AppPath = My.Computer.Registry.ClassesRoot.OpenSubKey(
"Applications\" & AppPath & "\shell\open\command").GetValue(Nothing).ToString
If AppPath IsNot Nothing Then 'if we found something...
Return StripExts(AppPath) 'return path to associated file, less decorations.
ElseIf TryDefaultIfNotFound Then 'did not, but if we should try default association...
Return GetDefaultOpenerForExt(Extension) 'then try to get app associated with extension.
End If
Catch
If TryDefaultIfNotFound Then 'did not, but if we should try default association...
Return GetDefaultOpenerForExt(Extension) 'then try to get app associated with extension.
End If
End Try
Return Nothing 'fail
End Function

'*******************************************************************************
' Method Name : GetDefaultOpenerForExt
' Purpose : Get path to default app associates with an extension
'*******************************************************************************
Friend Function GetDefaultOpenerForExt(ByVal Extension As String) As String
Try
 Page 588

Extension = CleanExtension(Extension) 'clean up extension or extract extension.


' get the default key associated with the extension.
Dim AppPath As String = My.Computer.Registry.ClassesRoot.OpenSubKey(Extension).GetValue(Nothing).ToString
' get the default command used for launching the associated application.
' trim any trailing decorations, such as "%1", or parameters, such as /n.
Return StripExts(My.Computer.Registry.ClassesRoot.OpenSubKey(
AppPath & "\shell\open\command").GetValue(Nothing).ToString)
Catch
Return Nothing
End Try
End Function

'*******************************************************************************
' Method Name : CleanExtension (support)
' Purpose : make sure we have only an extension.
'*******************************************************************************
Friend Function CleanExtension(ByVal Extension As String) As String
Dim IndexOfDot As Int32 = Extension.LastIndexOf("."c) 'get the last index of the dot, if it has one.
If Extension.IndexOf("\"c) = -1 Then 'if there is no pathing involved...
If IndexOfDot = -1 Then 'but a dot is not found...
Return "." & Extension 'then precede it with a "." if it lacks one.
End If
ElseIf IndexOfDot <> 0 Then 'if dot is not at the start of the string...
Return IO.Path.GetExtension(Extension.Trim.ToLower) 'then ensure we have just the extension.
End If
Return Extension 'did not need to do anything.
End Function

'*******************************************************************************
' Method Name : StripExts (support)
' Purpose : Strip registery extensions to a filepath, such as "%1" and such.
'*******************************************************************************
Friend Function StripExts(ByVal AppPath As String) As String
Dim SlIdx As Int32 = AppPath.IndexOf("/"c) 'get possible slash index, such as Word uses.
Dim Index As Int32 = AppPath.IndexOf("""%") 'check for typical decorations.
If Index = -1 Then
Index = AppPath.IndexOf("%") 'if robust type not found, check for typical...
End If
If Index = -1 OrElse
(SlIdx <> -1 AndAlso SlIdx < Index) Then 'if no typical or slash index defined land less than index...
Index = SlIdx 'then try using slash index.
End If
If Index <> -1 Then 'if decorations found...
AppPath = AppPath.Substring(0, Index) 'then trim them off.
End If
If String.IsNullOrWhiteSpace(AppPath) Then 'some command strings are "%1" "%*" and the like...
Return String.Empty 'ignore these.
End If
If AppPath.Substring(0, 1) = """"c Then 'some are more complex, like...
'"C:\WINDOWS\System32\rundll32.exe" "C:\WINDOWS\System32\msrating.dll",ClickedOnRAT %1"
Dim Idx As Int32 = AppPath.IndexOf(""""c, 1)
If Idx <> -1 Then
AppPath = AppPath.Substring(0, Idx + 1) 'strip to just base path.
End If
End If
Return AppPath.Trim(Chr(0), " "c, """"c) 'return the path and remove any decorations.
End Function

End Module
 Page 589

Black Book Tip # 37


Reliably Comparing Color Values, Even If They Are System Colors.
This is one of those tips I thought was not worth adding to this collection, until today as I was working
on adding a Color Settings interface for my VisualProCalc.NET Personal Programmable Calculator’s
VPL Source Code Editor, and I was caught wondering why one color item I knew had an identical value
as another was not considered the same value. I had seen this scattered across the web in a number of
chat rooms, where people were looking for an explanation. It was funny that I did not understand their
problem, because I worked through it all the time and never gave it much thought, until today, when I
realized as I was quickly correcting my tests that other people are facing the very same problem, but
they did not know the little trick I almost absentmindedly use to get around it.
The problem is that these color values might not be exactly the same, even though their numeric values
might appear to be. It all has to do with how their property data is stored. For example, I was testing two
objects of type Color, both containing White color settings, but when I checked them for being equal,
the comparison failed. Upon closer inspection, I saw that one object reported a description of
“{Name=ffffffff, ARGB=(255, 255, 255, 255)}”, while the other object reported “{Name=White, ARGB=(255,
255, 255, 255)}”. This actually makes them much more different than we might imagine.

What this told me was that even though one object was defined through a generic RGB invocation and
did in fact embed the indicated ARGB values (Alpha component, Red, Green, and Blue), such as will be
returned by the ColorDialog interface, the other color object was tagged as a Defined Color (we know
this because the operating system has knowledge of its defined color name: White), and as such the
integer value it is actually embedded with was not a generic 32-bit Quad definition, as generic color
values store, but it was instead actually an index offset into an internal predefined table of Color Quads.
NOTE: A Color Quad consists of four 8-bit Byte values, each with a range of 0-255, which defines a color’s Alpha
component (opacity, where 0=transparent), Red, Green, and Blue color settings. This is merged into a single 32-bit integer
variable.
When objects are compared for equity, their object’s Data Members, in this case their 32-bit internal
integer fields are compared against each other. Because a Defined Color’s Data Member contains an
index into a pre-defined table of internal colors with names, and a generic color does not, but instead
uses its 32-bit integer to store the actual color value, this is what is triggering the inequity. This is much
like how SystemColor values are defined, which are simply indexes into the system theme’s color table,
which contains the actual 32-bit values. With Color objects, if it is tagged as a Defined Color, its 32-bit
integer Data Member that normally stores the Quad that would otherwise contain the ARGB value, it is
instead treated as a 32-bit integer index.
The solution, therefore, is to instead compare the Color object’s Argb properties. On a generic value, its
resident 32-bit integer Quad is simply exposed via its ToString method to the invoker. If it is a Defined
Color, and hence an indexed color, the indexed predefined color’s Quad within the internal table is
instead referenced and the indexed value stored there is retrieved and returned via its ToString method to
the invoker, representing the 32-bit integer.
Thus, rather than comparing two color values like this:
If orgClrBackground = newClrBackground Then

We should instead compare them like this:


If orgClrBackground.ToArgb = newClrBackground.ToArgb Then”.

And that is all there is to it. And this even works when checking against SystemColor values.
 Page 590

Black Book Tip # 38


Use [Enum].GetValues to Parse Enumeration Declarations as a List.
Sometimes we might need to scan a local or system-defined enumeration to check its definitions, to
extract its member names so they can be displayed in a listing, or else we might want to scan through the
color values known by the system in order to acquire the name assigned to a color value.
To parse the members of any enumeration is really quite easy... once we know how to access them. To
do this, we need to access the enumeration’s base class, which is [Enum]. Notice we get to it by
embracing it in square brackets. We can then access an enumeration’s members as text strings in an
Array object using its GetValues method by providing it with the enumeration’s Type definition.
For example, suppose we had the following local enumeration named Accounts:
Friend Enum Accounts
Checking
Savings
Trading
Annuity
End Enum

We can fill a ListBox named lstAccounts from this enumeration using this line:
Me.ListBox1.Items.AddRange([Enum].GetNames(GetType(Accounts)))
' Or, to make the list ReadOnly:
Me.ListBox1.DataSource = [Enum].GetNames(GetType(Accounts))

For another example, there is an enumeration named KnownColors in the System.Drawing Namespace
that we can take advantage of to search for a match to a Color object we might have, either by using
Color.FromName(SystemColorName) to derive a Color object from the system color table, or by comparing
the color object’s Name property against each enumeration member.
We could assign the KnownColors enumeration list to a local array named ColorDefs like this:
Dim ColorDefs As Array = [Enum].GetValues(GetType(KnownColor)) 'get an array of all colors known to the system

We can now For-Each our way through the array.


Suppose we wanted to acquire the definition name for a color our application user selected, and deliver
that name to a Label, a TextBox, or a ToolTip. If the color the user selected is known to the system, we
want to use that system-defined name. However, if it is not known, we want to instead provide a system-
provided definition string, such as “Color [A=255, R=185, G=116, B=34]”, by using the Color object’s
ToString property, which will break down its ARGB value, or as a Hexadecimal ARGB string using its
Name property, such as “ffb97422”. For example:
'*********************************************************************************
' Method : GetColorDefinition
' Purpose : Return the color name or numeric definition for a color (avoid SystemColors as much as possible).
'*********************************************************************************
Friend Function GetColorDefinition(ByVal Clr As Color) As String
If Clr.IsSystemColor Then 'if it is a SystemColor object...
Dim ClrValue As Int32 = Clr.ToArgb 'get integer version of color for speed.
For Each KnownColor As KnownColor In [Enum].GetValues(GetType(KnownColor)) 'parse each known color enumeration.
Dim vClr As Color = Color.FromName(KnownColor.ToString) 'grab color definition for the known color.
If Not vClr.IsSystemColor AndAlso vClr.ToArgb = ClrValue Then 'if match but not a SystemColor...
Return vClr.ToString 'return its non-SystemColor name.
End If
Next
End If
Return Clr.ToString 'last ditch, or non-SystemColor.
End Function
 Page 591

In the above method, we wanted to avoid using SystemColor values as much as possible, because
SystemColor values will change whenever the user changes their desktop theme, so they are not
consistent. As such, if it is not a system color, we simply return the name (ie, “Red”) or its definition (ie,
“Color [A=255, R=255, G=0, B=0]”). If it is a system color, we want to continue to scan the color table for
a matching non-SystemColor value, which we can find by comparing their integer ToArgb properties.
Notice that we used a local variable named KnownColor of the KnownColor enumerator type.
Although some self-described “gurus” will proclaim (with reverb special audio effects) from their high
lofty perches atop their mountains of sand that this practice will make the code confusing, maybe
because they get confused just trying to figure anything out for themselves without plagiarizing it, the
truth is that it actually causes our code to avoid a lot of bugs. Maybe they should learn real software
development by attending some of my lectures, where we actually have fun breaking down paradigm
barriers and always thinking “outside the box”.
However, if we did need to access the KnownColor enumeration within a block where the KnownColor
local variable is in scope, and so having precedence, we can easily do that by embracing the
enumerator’s name within square brackets, just as we did to grab the enumeration base class. For
example, Dim Red_Def As String = [KnownColor].Red.ToString.
 Page 592

Black Book Tip # 39


Reliably Detecting Transparency Colors.
This is a minor tip, already touched on in-code previously in the SetImageTransparency function
included in both “Setting Context Menu Icon Image Transparency at Runtime” (Black Book Tip # 25 on
page 527) and “Adding Background Transparency to Images on Toolstrips and Menus” (Black Book Tip
# 26 on page 532), but it is one that has caused countless programmers a lot of headaches.
Often when developers check to see if a color is defined as transparent in a graphics method, they will
naturally test a Color object for simply being equal to Color.Transparent. The problem is, even when
they know for a fact that the color is a transparency color, this test will fail more often than not.
The reason for this is obvious if we understand how an image transparency color is declared. For
example, PNG image files can feature a transparency color member. This is a designated color value that
is to be treated as transparent when the image is rendered. When this color is found in the image, which
can be located by checking for a match with its RGB value to the RGB value of the transparency color
(such as “If (transpClr.ToArgb And &HFFFFFF) = (pixelClr.ToArgb And &HFFFFFF) Then... ”), its Alpha
component value is set to zero (“pixelClr = Color.FromArgb(0, pixelClr)”), which renders it fully
transparent. As such, even though its Red, Green, or Blue definition values will not be rendered, these
values will be retained in order to define which color is being treated as transparent in the image.
Therefore, to test for a color being transparent, we simply have to test just its Alpha component member
of an ARGB color for being zero (“If (pixelClr And &HFF000000) = 0 Then...”). Consider the following
method, which will return the renderable color value of an image’s transparency color:
'*********************************************************************************
' Method Name : GetTransparencyColor
' Purpose : return the transparency color of an image as non-transparent color.
'*********************************************************************************
Friend Function GetTransparencyColor(ByRef Img As Image, Optional ByVal X As Int32 = -1, Optional Y As Int32 = -1) As Color
If Img IsNot Nothing Then
With DirectCast(Img, Bitmap) 'treat the image as a bitmap (identical format).
If X < 0 OrElse X >= .Width Then 'if X offset within image not defined...
X = .Width - 1 'then use right edge of image.
End If
If Y < 0 OrElse Y >= .Height Then 'if Y offset within imaged not defined...
Y = .Height - 1 'then use bottom edge of image.
End If
Dim iClr As Int32 = .GetPixel(X, Y).ToArgb 'get the target pixel.
If (iClr And &HFF000000) = 0 Then 'if Alpha value 0, color is transparent.
Return Color.FromArgb(iClr Or &HFF000000) 'return its fully opaque ARGB color definition.
End If
End With
End If
Return Nothing 'did not find a transparency color
End Function

NOTE: We can also test for a Color object that contains any degree of transparency by altering the test to “ If (iClr And
&HFF000000) <> 255 Then...”. Because the result will OR an Alpha component value of 255 (&HFF) to its left-most member
(&HFF000000), it does not matter what value the Alpha component value was previously, whether it was a value of 0 to 254
(&H00 to &HFE), because it will be forced to 255, fully opaque (solid), in the ARGB quad-color definition being returned.
 Page 593

Black Book Tip # 40


Turbo-charge Directory Tree Displays in a TreeView.
Reflecting a directory structure within a TreeView control is a
time-honored tradition. The problem programmers now face
is in displaying such trees for directory structures that have
grown ever more massive and deep. Now-a-days, such
colossal structures can typically contain several tens of
thousands of folders and subfolders, and sometimes hundreds
of thousands of files (I can only hope that the sleep-deprived
developer who invented the original tree structure has been
adequately compensated). Although computer speeds and
drive sizes have grown exponentially over the years, the
number of folders and files contained within them has
increased exponentially as well. VB’s old Dir function, often
used to scan for such folders and files, even on a Quad-Core
4GHz system, is still one of the slowest assets in our
programming arsenal. When File System Objects were
introduced in the Windows Script Host Object Model in 1999,
it was like a breath of fresh air, because we could parse
folders dozens of times faster. But sadly, the number of
folders and files continue to proliferate almost geometrically
as the sizes of our drives balloon, often taxing the concurrent
increase in computer and disc drive speeds. With the
introduction of the .NET System.IO namespace, directory
parsing became supercharged, making FSO look like it was
just creeping along, but the number of files keeps
proliferating like viruses, causing even these significantly
faster objects to often also appear too slow.
Worse, application users have a bad habit of expecting instant gratification. The last thing they ever want
to see is a WaitCursor or an AppStart cursor staring back at them, or their system simply seems to freeze
up for a minute or five after they selected a drive icon because our program is too busy drilling its way
through perhaps hundreds of thousands of files on their data-filled system (see the earlier article,
Comparing VB System I/O Commands; Dir Vs. FSO Vs. FileStreams on page 216 for a practical
comparison example that parses a drive using Dir, FSO, and Streaming).
The trick to making our TreeView work lightening-fast is to simply address just what the user can
actually see. Instead of building a full tree with every file and folder beneath the selected drive or folder,
most of which the user will not bother to peruse, why not just list the drives and folders of the currently
viewed folder, and then wait for them to expand a folder before actually populating it? The trick to
making this look like a fully-defined tree is to ensure that subfolders of the current path that contain sub-
folders or files show expandable icons (+), indicating they can expand. Doing all this is very easy.
The incentive for displaying directory trees this way is that they run unbelievably fast, because all they
have to do is populate the current folder, if it has not already been populated, and check each member to
see if they are sub-folders, and if so, if they themselves contain at least one sub-folder, This causes
directory displays to populate like greased lightening!
To create a demonstration of this technique, all we need to do is:
1. Create a fresh Visual Basic Windows Forms Application and name it DirTest.
2. Finally, without adding any controls to it, just add the following block of code for Form1:
 Page 594

Public Class Form1


'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private rootPath As String = "C:\"
Private WithEvents TreeView1 As New TreeView 'set aside treeview control.
Private ImageList1 As New ImageList 'image list to store images within (we will provide them for you!).
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
'*********************************************************************************
' Method : Form1_Load
' Purpose : Prepare form and initial directory display.
'*********************************************************************************
'*********************************************************************************
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
InitializeImageList(Me.ImageList1) 'load ImageList with images.
Me.TreeView1.Parent = Me 'set treeView parent & make it visible.
TreeView1.Dock = DockStyle.Fill 'fill form with TreeView.
TreeView1.Nodes.Clear() 'clear Treeview.
TreeView1.ImageList = ImageList1 'make sure ImageList1 is attached.
TreeView1.ImageIndex = 2 'default to File image.
TreeView1.ShowNodeToolTips = True 'Allow node Tooltips to display.
Dim RootNode As TreeNode =
TreeView1.Nodes.Add(TreeView1.Nodes.Count.ToString, rootPath, 0, 0) 'create Root Node w/closed folder.
RootNode.ToolTipText = rootPath 'save its path in its tooltip.
DirRecurse(RootNode) 'parse any of its subfolders.
'---------------------------------------------------------------------------
TreeView1.SelectedNode = RootNode 'select the root node.
If CBool(RootNode.Nodes.Count) Then 'if we have children...
RootNode.Expand() 'make sure root node is expanded.
RootNode.EnsureVisible() 'make sure it can be seen.
End If
End Sub

'*********************************************************************************
' Method : TreeView1_BeforeCollapse
' Purpose : React to a node collapsing.
'*********************************************************************************
Private Sub TreeView1_BeforeCollapse(sender As Object, e As TreeViewCancelEventArgs) Handles TreeView1.BeforeCollapse
With e.Node
If .ImageIndex < 2 Then 'folder?
.SelectedImageIndex = 0 'yes, so force to Closed.
.ImageIndex = 0
End If
End With
End Sub

'*********************************************************************************
' Method : TreeView1_BeforeExpand
' Purpose : React to a node expanding.
'*********************************************************************************
Private Sub TreeView1_BeforeExpand(sender As Object, e As TreeViewCancelEventArgs) Handles TreeView1.BeforeExpand
With e.Node
If .ImageIndex < 2 Then 'folder?
.SelectedImageIndex = 1 'yes, so force to Opened.
.ImageIndex = 1
If .Checked Then 'has it been processed yet?
.Checked = False 'mark this folder as processed.
DirRecurse(e.Node) 'parse any of its subfolders.
End If
End If
End With
End Sub

'*********************************************************************************
' Method : DirRecurse
' Purpose : Fill provided TreeView with folders and files as needed.
'*********************************************************************************
Private Sub DirRecurse(ByRef parentNode As TreeNode, Optional ByVal skipDeepSeek As Boolean = False)
parentNode.Nodes.Clear() 'Clear child nodes in case repopulating.
Dim dirs() As String = Nothing 'init local directory storage.
Try
dirs = IO.Directory.GetDirectories(parentNode.ToolTipText) 'get subfolder list, skip if protected.
Catch
End Try
If dirs IsNot Nothing AndAlso dirs.Count <> 0 Then 'if sub-directories exist...
If skipDeepSeek Then 'if Referencing only...
parentNode.Checked = True 'mark this folder as being unprocessed.
parentNode.Nodes.Add("*") 'add faux child node to add "+" connector.
Return 'nothing else to do.
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
For Each dirPath As String In dirs 'else parse each subfolder.
If (GetAttr(dirPath) And (FileAttribute.Hidden Or FileAttribute.System Or FileAttribute.Volume)) = 0 Then
 Page 595

Dim dirNode As TreeNode =


parentNode.Nodes.Add(parentNode.GetNodeCount(False).ToString,
IO.Path.GetFileName(dirPath), 0, 0) 'add new dir node w/closed folder.
dirNode.ToolTipText = dirPath 'save its folder path as its tooltip.
DirRecurse(dirNode, True) 'parse any of its subfolders.
End If
Next
End If
'---------------------------------------------------------------------------
Dim files() As String = Nothing 'init local file storage.
Try
files = IO.Directory.GetFiles(parentNode.ToolTipText) 'get file list in folder, skip if protected.
Catch
End Try
If files IsNot Nothing AndAlso files.Count <> 0 Then 'if files exist...
If skipDeepSeek Then 'if Referencing only...
parentNode.Checked = True 'mark this folder as being unprocessed.
parentNode.Nodes.Add("*") 'add faux child node to add "+" connector.
Return 'nothing else to do.
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
For Each filePath As String In files 'else parse each file.
Dim fileNode As TreeNode =
parentNode.Nodes.Add(parentNode.GetNodeCount(False).ToString,
IO.Path.GetFileName(filePath), 2, 2) 'add new file node w/file image.
fileNode.ToolTipText = filePath 'save its file path as its tooltip.
Next
End If
End Sub

'*********************************************************************************
' Method : InitializeImageList
' Purpose : Imitialize a provided ImageList and fill it with locally-created images.
'*********************************************************************************
Private Sub InitializeImageList(ByRef imgList As ImageList)
imgList.Images.Clear() 'initialize image list.
imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
'------------------------
'ImageStrip for 3 Images
'------------------------
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFOSURBVEhLxZNbDoMwDAR79H5xLm5GPc46BLBIgEqstHJe" &
"Y5bQft7WkmiapoM/3+86brDSRWqB5vCw7vDKUVU5G0doxtm6z0NM5nlme1M3h07EuUw9XseqPGDH9FTf" &
"0ptJFj5qAHs7bGKM8h75TwLD+gHJ16xGSG56Y+0xjlybAFdUYdPK5xeQSbxmRb4W1ULua4RnTl/4wQDH" &
"6k2l4EdZqnibF/ncatfGwDc9HgVw9qrpQTXcxkU+t3pq3T4mAzwZHn2Ble1LjPeIcV232jPnyvPLZavH" &
"8y+ARhiqIc6Kt7V7t4/hyfDoC6DgR1hxtSL/g/ZsZ+Fb1wxXAkStsCn4UdaQzQvsFfut4cIo+piffwF0" &
"pYdYdyb2Mef3tbX6lIffscNStn9mQ7ovgNvwUcPq8VfVYKPOFEHRvoaYc856vCvlOYhwI1ab96S8N7Us" &
"P6rUvtqZka2WAAAAAElFTkSuQmCC"
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
imgList.Images.AddStrip(Image.FromStream(memStream)) 'construct ImageStrip from stream to ImageList1.
memStream.Close() 'release stream resources.
End Sub
End Class

As we can see, this code is not rocket-science. Notice, though, that we take advantage of two events:
1. The BeforeCollapse event handler (Treeview1_BeforeCollapse) simply ensures that the displayed image for the node
shows that it is closed. This is important for directory nodes, which can be displayed open (expanded) and closed
(collapsed). Notice also that we set both the SelectedIndexImage and the IndexImage properties of the node to the
closed folder image. This is also important, because when a folder is selected it is able to display a different image
using the SelectedIndexImage property. Although this is a useful feature, for what we are doing here we need to keep
the images consistent.
2. The BeforeExpand event handler (Treeview1_BeforeExpand) simply ensures that the displayed image for the node
shows that it is opened. Just as with the BeforeExpand event, we ensure that we set both the SelectedIndexImage and
the IndexImage properties of the node to the open folder image. Notice that it checks the parent node’s Checked
property. The DirRecurse method, described below, will set this flag to indicate that this folder has not actually been
populated by its subfolders and/or files. If this flag is True, then its sub-node is actually a faux-node, used so that the
parent node will be displayed within the TreeView with a “+” connector, indicating that it can be expanded. In this
case, its checked state is reset, and finally its folder path is populated to the TreeView control.
The next method employed is DirRecurse. This method will take the TreeView Node provided it and
adds any child nodes it requires. It does this by taking advantage of the node’s FullPath property, which
will provide its folder path (in our case, we could have also used the ToolTipText property).
 Page 596

Notice that we check folders by assigning them to a Dirs String array. Although it might seem
convenient to simply use “Dim dirs() As String = IO.Directory.GetFiles(parentNode.ToolTipText) ” and
then just take advantage of “ dirs.Count” to see if we actually found any subfolders, we embed the sub-
folder gathering, and later file gathering, within an error trap because certain protected system folders
and files will launch security exception errors if we try to burrow into or access them.
If the parent path contains sub-folders, we first make sure that its Nodes list is cleared of any child
nodes. We do this because we may want to repopulate the folder and down-stream files and folders may
have been added or deleted since this list was displayed.
We next check to see if we have also set the optional SkipDeepSeek flag. If the flag is set or if the parent
does not yet contain any nodes, though we presently know that it contains at least one subfolder, we will
set its Checked property to True and add a faux-node so that the parent node will still display a “+”
connector, even though we are not currently populating its associated children. Having nothing else to
do, we can leave the method.
If we are populating the node and we have sub-folders, we take this list and create child nodes with
closed folder images, set its ToolTipText property to the folder’s path, and then recurse this method using
the new node as a parent node. Here, we set the SkipDeepSeek parameter to True so the recursion will
only find out if each child folder contains any of their own children, and sets their Checked property if
so (never setting this flag to True will allow a full directory tree to be populated).
We then check for child files in the parent node’s path. If we find any, like with the directory scan, we
see if the SkipDeepSeek flag is set. If the flag is set, though we presently know that it contains at least
one file, we will set its Checked property to True and add a faux-node so that the parent node will still
display a “+” connector. We can do this because if a faux-node had already been generated by the
directory check, then the parent node’s child count will not be zero.
If we are populating the node and we have files, we take this list and create child nodes with File images
and set its ToolTipText property to the file’s path.
And that is all there is to it!

About the InitializeImageList Method


Notice the final method, InitializeImageList. It builds an ImageStrip from a Base64 string and loads
the derived three images, a Open Folder, a Closed Folder, and a File (Notepad) folder, to the ImageList1
control that can then be used in this example. The topic of embedding images within source code will be
covered later under a number of topics, where we will learn how to build our own in-code image lists
using individually-defined images and image strips.
Notice also that the two controls used, TreeView1 and ImageList1, were not controls dropped onto the
form, but created within our source code. Because the TreeView control is displayed, it set its parent to
the form. This allows it to become visible on the form. We can also optionally add it to the form’s
Controls collection in cases where our code might need to scan the controls assigned to a form. The
ImageList control did not need to go through these steps because it is not being displayed; we are using
it as a disconnected but referenced class instance, like a ditty bag to hold some of our stuff.
NOTES: If we will need to display CheckBoxes in our TreeView, then instead of using a folder node’s Checked property to
indicate if the folder has been processed or not, we can instead place this flag in its Tag property. Of course, we will have to
cast the Tag property object to Boolean in order to test it, such as CBool(parentNode.Tag).
In more complex situations, we can store a small structure or class within the Tag property (this property is of type Object, si
it can store (actually, point to) anything, after all), and this way we can additionally store several different flags or fields.
 Page 597

Black Book Tip # 41


Sorting TreeView Directory Trees in an Orderly Folder and File Fashion.
There are quite a number of programmers out there who want to sort their TreeView listings. Of course,
once they manage to figure out how to do it (it is quite easy using the IComparer interface that we have
already touched on within this document), they often abandon it because it tends to sort everything, and
so folders are no longer displayed before files as they were in our last example (see Black Book Tip # 40,
Turbo-charge Directory Tree Displays in a TreeView, on page 593). Most people want to display sorted
folders first, and then the sorted file list. Others want to go an extra step and also sort the files based on
their extensions. This way, all files with “.txt” are grouped and sorted together, for example.
In the previous Black Book Tip, we assigned each folder an Image Index of 0 when it is collapsed
(closed), and an Image Index of 1 when it was expanded (open). Files had an Image Index of 2 (or
higher). Thus, to sort so that folders are always displayed first, during a comparison, if the left
comparator is a folder and the right comparator is a file, then we should assume that left is less than the
right. If the left is a file and the right is a folder, then the left should be assumed to be greater than the
right. Otherwise, at that point, the left and right comparators are either both folders or both files.
If we wanted to compare extensions of files, then when both comparators are files, we can easily grab
their extensions. Using a strategy similar to testing folders against files, if the left has no extension and
the right does, then assume that the left is less than the right. If the left has an extension and the right
does not, then assume that the left is greater than the right.
After the above tests, if we fall through that far, we will know that both files have extensions. We can
then perform a string comparison on them. If they match, then we want to take the additional step of
comparing their string lengths, because comparing “Test” against “Testing” will be considered equal,
due to a string comparison only testing as far as the length of the shortest string.
If both comparators are folders, or if both are files with identical extensions, or both are files lacking
extensions, then we can perform a normal string comparison.
Because comparisons return the sign of the comparison, where it returns -1 if the left comparator is less
than the right comparator, it returns 1 if the left comparator is greater than the right comparator, and it
returns 0 if both comparators are equal, we can add an additional option and allow descending sorts by
simply multiplying any comparison result by -1.
All this is almost too easy.
The first thing we want to do is to create our TreeView Node comparison class. For that, we create a new
class. Name it TreeNodeComparer. Within its body heading, add “ Implements Icomparer”, and it will
automatically add the required Public Compare function body that is invoked by the sorter function:
Public Class TreeNodeComparer
Implements IComparer

Public Function Compare(x As Object, y As Object) As Integer Implements IComparer.Compare


Throw New NotImplementedException()
End Function
End Class

Here, we are provided the guts of our comparison, where x refers to the left comparator, and y refers to
the right comparator. We will first need to cast them to type TreeNode so we can use them, and then get
the contents of their Text properties to obtain the actual text to perform the comparison on. For example,
replace the “Throw New NotImplementedException()” line within the Compare function with the following:
Dim nodeX As TreeNode = DirectCast(x, TreeNode) 'convert x to a treenode.
Dim nodeY As TreeNode = DirectCast(y, TreeNode) 'convert y to a treenode.
Dim textX As String = nodeX.Text 'grab their text.
Dim textY As String = nodeY.Text
 Page 598

We now have enough information to perform comparisons. For example:


Public Function Compare(x As Object, y As Object) As Integer Implements IComparer.Compare
Dim nodeX As TreeNode = DirectCast(x, TreeNode) 'convert x to a treenode.
Dim nodeY As TreeNode = DirectCast(y, TreeNode) 'convert y to a treenode.
Dim textX As String = nodeX.Text 'grab their text.
Dim textY As String = nodeY.Text

Select Case String.Compare(textX, textY) 'compare strings.


Case -1 'textX < textY.
Return -1
Case 1 'textX > textY.
Return 1
Case Else 'textX = textY.
Return textX.Length.CompareTo(textY.Length) 'compare text lengths.
End Select
End Function

We could use this by defining (and also invoking) a sort when we choose to within our code like this:
Me.TreeView1.TreeViewNodeSorter = New TreeNodeComparer()

Its current problem is, not only does this function presently perform only ascending sorts, but it also
does not discriminate between folders and files. Because my nodes will contain a value of 0 for
collapsed/closed folders, and 1 for expanded/open folders, I can begin the testing within the comparer
function as described above for folders and files. To start, within the body of the class, below the
Implements IComparer declaration, I want to declare these two constants:
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
'the following refer to the TreeeNode.ImageIndex values in a linked ImageList control.
Private Const FolderClosed As Integer = 0 'closed folder image index.
Private Const FolderOpened As Integer = 1 'opened folder image index.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

Then, prior to the string comparison selection block, I would add the following selection block:
Select Case nodeX.ImageIndex 'check for files and folders.
Case Is <= FolderOpened 'nodeX is a folder.
If nodeY.ImageIndex > FolderOpened Then 'but is nodeY a file?
Return -1 'yes, so make a folder lower than a file.
End If
' both objects are folders at this point...
Case Else 'nodeX is a file.
If nodeY.ImageIndex <= FolderOpened Then 'but is nodeY a folder?
Return 1 'yes, so make the file greater than a folder.
End If
' both objects are files at this point...
End Select

Here, we will make sure that folders are always lower than files in comparisons. This way, the folders
will be sorted and the files will be sorted, but the sorted folders will be displayed in the TreeView prior to
the sorted file list within each folder.
If we wanted to additionally sort on file extensions before sorting the files, thus allowing all files with
the same extension to be within its own collective sorted group, we could add this bit of code before the
above End Select, where the code at that point “knows” that both nodes are for files:
' both objects are files at this point...
Dim extX As String = IO.Path.GetExtension(textX) 'grab extensions for left...
Dim extY As String = IO.Path.GetExtension(textY) 'and right items
'---------------------------------------------------
'compare extensions. Only if they match will we fall
'below and compare the complete filenames
'---------------------------------------------------
If extX = String.Empty Then 'if the left file does not have an extension...
If extY <> String.Empty Then 'but the right file does?
Return -1 'yes, so extX < extY.
End If
ElseIf extY = String.Empty Then 'ext is not empty, but is extY?
Return 1 'yes, so extX > extY.
Else
Select Case String.Compare(extX, extY) 'compare extensions.
Case -1 'extX < extY.
Return -1
Case 1 'extX > extY.
Return 1
 Page 599

Case Else 'extX = extY.


Dim tmp As Int32 = extX.Length.CompareTo(extY.Length) 'compare lengths.
If tmp <> 0 Then 'if lengths are not the same.
Return tmp 'return the sign.
End If
End Select
End If 'if we have not yet returned, the extensions are the same, so compare the whole string.

If we want to perform descending sorts, we could either flip the sign of the returned values (except
during the node checks between folders and files so the folders will always display first), or we could
multiply the comparisons and the return values against an integer variable, such as AscDecFlag, that
contains a 1 for ascending, or -1 for descending. We might also want to make sorting on extensions an
option.
To make all these things an option, we can add our own parameterized New constructor method for this
class that will accept optional parameters so that we can set the extension sorting and sorting direction
flags as desired when we invoke a sort. For example:
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private _SortExtensions As Boolean 'True if Sort also checks Extensions.
Private _SortDescending As Boolean 'True if Sort is in Ascending Order.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

Public Sub New(Optional ByVal SortExtensions As Boolean = False, Optional ByVal SortDescending As Boolean = False)
Me._SortExtensions = SortExtensions 'store extension sorting flag.
Me._SortDescending = SortDescending 'store Ascending/Descending sort flag.
End Sub

Apply our custom sort comparison method would require a slight change, such as:
Me.TreeView1.TreeViewNodeSorter = New TreeNodeComparer(SortExtensions, SortDescending)

Of course, we would have to be able to act on these selections. For the SortExtensions option, we could
wrap the previous extension comparison block within an IF block that tests for the _SortExtensions
field being true. We would also have to set our AscDecFlag integer to either 1 (Ascending) or -1
(Descending) based upon the state of _SortDescending. For example:
Dim AscDecFlag As Int32 'Ascending/Descending flag.
If Me._SortDescending Then 'Descending?
AscDecFlag = -1 'yes, so invert the result for Descending.
Else
AscDecFlag = 1 'otherwise treat result normally if Ascending.
End If

And then we would have to apply this flag to the comparisons and return value, except, again, for the
folder versus file tests.
Following is the complete TreeNodeComparer class:
Option Explicit On
Option Strict On
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
' TreeNodeComparer Class Module
' Allow Comparing TreeNodes in a Treeview Collection (used by Sorting method).
' This also will sort the files and folders separately, keeping folders below
' files, even on a Descending sort.
'-----------------------------------------------------------------------------------
' To assign our sort comparer to a TreeView, do something like the following variation:
'Me.Treeview1.TreeviewNodeSorter = New TreeNodeComparer(SortExtensions, SortDescending)
' NOTE: The above line also invokes the sort.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
Public Class TreeNodeComparer
Implements IComparer
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private _SortExtensions As Boolean 'True if Sort also checks Extensions.
Private _SortDescending As Boolean 'True if Sort is in Ascending Order.
'-------------------------------------------------------------------------------
' the following refers to the TreeeNode.ImageIndex values in a linked ImageList control.
Private Const FolderClosed As Integer = 0 'closed folder image index.
Private Const FolderOpened As Integer = 1 'opened folder image index.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
'*******************************************************************************
' Method Name : New
 Page 600

' Purpose : Initialize a new sorting method using a specified column and Sort Order.
'*******************************************************************************
'*******************************************************************************
Public Sub New(Optional ByVal SortExtensions As Boolean = False, Optional ByVal SortDescending As Boolean = False)
Me._SortExtensions = SortExtensions 'store extension sorting flag.
Me._SortDescending = SortDescending 'store Ascending/Descending sort flag.
End Sub

'*******************************************************************************
' Method Name : Compare
' Purpose : String comparison using by the ListView.ListViewItemSorter interface.
'*******************************************************************************
' comparison method used by the Listview (can sort Ascending or Descending).
Public Function Compare(ByVal objX As Object, ByVal objY As Object) As Integer Implements IComparer.Compare
Dim AscDecFlag As Int32 'Ascending/Descending flag.
If Me._SortDescending Then 'Descending?
AscDecFlag = -1 'yes, so invert the result for Descending.
Else
AscDecFlag = 1 'otherwise treat result normally if Ascending.
End If
'---------------------------------------------------------------
Dim nodeX As TreeNode = DirectCast(objX, TreeNode) 'convert objX to a treenode.
Dim nodeY As TreeNode = DirectCast(objY, TreeNode) 'convert objY to a treenode.
Dim textX As String = nodeX.Text 'grab their text.
Dim textY As String = nodeY.Text
'---------------------------------------------------------------------------
'NOTE: ImageIndex 0 = Closed folder, ImageIndex 1 = Open folder. All others are files.
'---------------------------------------------------------------------------
Select Case nodeX.ImageIndex 'check for files and folders.
Case Is <= FolderOpened 'nodeX is a folder.
If nodeY.ImageIndex > FolderOpened Then 'but is nodeY a file?
Return -1 'yes, so make a folder lower than a file.
End If
' both objects are folders at this point...
Case Else 'nodeX is a file.
If nodeY.ImageIndex <= FolderOpened Then 'but is nodeY a folder?
Return 1 'yes, so make the file greater than a folder.
End If
' both objects are files at this point...
If Me._SortExtensions Then 'should we sort by extension as well?
Dim extX As String = IO.Path.GetExtension(textX) 'grab extensions for left...
Dim extY As String = IO.Path.GetExtension(textY) 'and right items.
'---------------------------------------------------
'compare extensions. Only if they match will we fall
'below and compare the complete filenames.
'---------------------------------------------------
If extX = String.Empty Then 'the left file does not have an extension...
If extY <> String.Empty Then 'but the right file does?
Return -AscDecFlag 'yes, so extX < extY.
End If
ElseIf extY = String.Empty 'ext is not empty, but is extY?
Return AscDecFlag 'yes, so extX > extY.
Else
Select Case String.Compare(extX, extY) * AscDecFlag 'compare extensions and flip sign if Descending Sort.
Case -1 'extX < extY.
Return -1
Case 1 'extX > extY.
Return 1
Case Else 'extX = extY.
Dim tmp As Int32 = extX.Length.CompareTo(extY.Length) * AscDecFlag 'compare lengths.
If tmp <> 0 Then 'if lengths are not the same.
Return tmp 'return the sign.
End If
End Select
End If
End If 'if we have not yet returned, the extensions are the same, so compare the whole string.
End Select
'---------------------------------------------------------------
'nodeX and nodeY are either both folders or both files.
'If sorting extensions, any extensions will match by this point.
'---------------------------------------------------------------
Select Case String.Compare(textX, textY) * AscDecFlag 'compare text and flip sign if Descending Sort.
Case -1 'textX < textY.
Return -1
Case 1 'textX > textY.
Return 1
Case Else 'textX = textY.
Return textX.Length.CompareTo(textY.Length) * AscDecFlag 'compare text lengths.
End Select
End Function

End Class

If we were to add this sort class to the last Black Book project (# 40), we would first invoke it after
setting up our initial TreeView display. Here is the modified Black Book # 40 Form1_Load event:
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private rootPath As String = "C:\"
Private WithEvents TreeView1 As New TreeView 'set aside treeview control.
Private ImageList1 As New ImageList 'image list to store images within.
 Page 601

Private SortExt As Boolean = False 'true to sort file extensions (change True/False as needed).
Private SortDes As Boolean = False 'true to sort descending (change True/False as needed).
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
'*********************************************************************************
' Method : Form1_Load
' Purpose : Prepare form and initial directory display.
'*********************************************************************************
'*********************************************************************************
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
InitializeImageList(Me.ImageList1) 'load ImageList with images.
Me.TreeView1.Parent = Me 'set treeView's parent (also makes it visible).
TreeView1.Dock = DockStyle.Fill 'fill form with TreeView.
TreeView1.Nodes.Clear() 'clear Treeview.
TreeView1.ImageList = ImageList1 'make sure ImageList1 is attached.
TreeView1.ImageIndex = 2 'default to File image.
TreeView1.ShowNodeToolTips = True 'Allow node Tooltips to display.
Me.TreeView1.TreeViewNodeSorter = New TreeNodeComparer(SortExt, SortDes) 'provide our sort support to the TreeView.
Dim RootNode As TreeNode =
TreeView1.Nodes.Add(TreeView1.Nodes.Count.ToString, rootPath, 0, 0) 'create Root Node w/closed folder.
RootNode.ToolTipText = rootPath 'save its path in its tooltip.
DirRecurse(RootNode) 'parse any of its subfolders.
'---------------------------------------------------------------------------
TreeView1.SelectedNode = RootNode 'select the root node.
If CBool(RootNode.Nodes.Count) Then 'if we have children...
RootNode.Expand() 'make sure root node is expanded.
RootNode.EnsureVisible() 'make sure it can be seen.
End If
End Sub

Notice that even though in the BeforeExpand event we are populating a folder after we had earlier
applied our sort class, we do not need to worry about forcing a re-sort. That is because the sort class we
applied at the start will be in effect until we change it again, or when the application ends.
Notice finally that if we had applied the sort comparer after we had populated our TreeView, or if we
select different sort parameters, as soon as we assign a new comparer class to the TreeViewNodeSorter
property, a sort will be automatically initiated because changing the sort method would logically also
require a resort, so the control support code is programmed to be smart enough to enforce it.
 Page 602

Black Book Tip # 42


Taking Advantage of VB.NET Drag and Drop Features and Making it E-Z.
A lot of VB6 programmers have complained about how Drag and Drop, the process of being able to
drag an object, such as a filename or list of files from File Explorer to a form control or drag data from
one control to another, has changed between VB6 and VB.NET. When VB6 and C++ were the only
games in town, Visual Studio 6 developers kept complaining, “Why can’t Drag and Drop do this?”,
“Why can’t Drag and Drop do that?”, or “Why can't we Drag and Drop custom objects more easily?”
The list of complaints was long. Well, VB.NET’s Drag and Drop does all that; Microsoft answered
those umpteen feature demands by cramming all that technology into .NET, which is why we see many
differences between the VB6 and VB.NET implementations. Further, VB6 used OLE (Object Linking
and Embedding) to support Drag and Drop, but that is a more primitive and limiting paradigm than the
sophisticated threading .NET uses, which can support the superfluous VS6 user enhancement demands.
So, be careful what we wish for. In truth, I think all the programmer angst stems from the Upgrade
Wizard not upgrading Drag and Drop code for some reason. But the good news is, upgrading that code
is surprisingly easy to do! This is why I keep wondering why the Upgrade Wizard never upgraded it.
Earlier in this tome, in “Notes on E-Z Updates to, and Doing Drag and Drop ” on page 106, I covered basic drag and
drop functionality, showing us how to easily enable dragging files (actually, this involves just the
filepath strings) of a file or files from Explorer to a list control; the most common VS6 use of it.
Basically, we had to keep in mind 3 things to allow filepath Drag and Drop:
1. We must set the AllowDrop property to TRUE on any controls we want to allow dropping items onto, such as a
ListBox, though it could be even a button, a “Drag Here” panel, or anything we choose.
2. If we are dragging objects from somewhere other than the current control, which is typical, we must be sure the
mouse cursor reflects whether we want to allow the type of object being dragged to be dropped onto our
“AllowDrop” control, so we need to add a simple DropEnter event handler to the “AllowDrop” control. Remember,
any control that does not have their AllowDrop property set, or does not have a DropEnter event handler (except in the
event of dropping items from one place to another within a single container), will automatically show a no-entry
cursor under .NET, and it will not allow drops onto them.
3. Finally, we must add a DragDrop event to handle dropping items to the “Allow Drop” control.
Of course, this also involves some rather simple program logic to determine the kind of data that is
being dragged and if we want to allow dropping that kind of data onto our receiver control.
Suppose we have a ListBox named ListBox1 on a form to act as a file receiver. The first thing we must
do is to set its AllowDrop property to True. When we do just that, 1/3 of our battle is already won.
Next, because we are dragging items from outside it, we need to add a DropEnter event for ListBox1.
Because we are processing files, its interrogation code will be extremely simple. If this test passes we
can display an “Allow-Copy” cursor, otherwise we specify None, which displays a “No-Entry” cursor:
Private Sub ListBox1_DragEnter(sender As Object, e As DragEventArgs) Handles ListBox1.DragEnter
If e.Data.GetDataPresent(DataFormats.FileDrop, True) Then 'Note that you can use "FileName" or even "FileDrop" in
e.Effect = DragDropEffects.Copy 'place of DataFormat.FileDrop. Parameter TRUE allows compatible
Else 'formats to be converted to the specified target format.
e.Effect = DragDropEffects.None 'Put up a no-entry sign if it is not of the expected type.
End If
End Sub 'BE SURE to have set the AllowDrop property on ListBox1!

Finally, we must add our DragDrop event for ListBox1 if the user will drop allowable items onto it.
Private Sub ListBox1_DragDrop(sender As Object, e As DragEventArgs) Handles ListBox1.DragDrop
If e.Data.GetDataPresent(DataFormats.FileDrop, True) Then 'verify desired format.
' Get a file array list and allow conversions, and parse each string.
For Each Itm As String In DirectCast(e.Data.GetData(DataFormats.FileDrop, True), String())
Me.ListBox1.Items.Add(Itm) 'Add each dragged item to the bottom of ListBox1.
Next 'BE SURE to have set the AllowDrop property on ListBox1!
End If 'Also, if it does not have a DragEnter event to determine authorized items, THIS event will not fire!
End Sub
 Page 603

But what if we want to drag something within a control, such as in a TreeView, and we want to drag an
object, such as a TreeNode object, and be able to drop it onto only certain members of the TreeView?
This is also quite easy to do. The trick, or so I am told, is in determining if the object being dragged can
be accepted. I think some people consider it complicated because the means by which data formats are
determined can appear complicated. Actually, it is not. Consider determining if the object being dragged
is a string. The typical example for this is the test “ If e.Data.GetDataPresent(GetType(String)) Then...”.
This just checks to see if a string is being dragged. We can also use the DataFormats enumeration, which
lists numerous pre-defined data types. For example, our simple string test could have also been “If
e.Data.GetDataPresent(DataFormats.StringFormat) Then... ”.

If it is True, we can drop it onto our control, so we need to get the string. The typical example for this is
to treat it as an Object: “Dim item As Object = CType(e.Data.GetData(GetType(String)), Object) ”, but we
can instead simplify it to “Dim item As String = e.Data.GetData(GetType(String)).ToString ” because we
already know it is a string. We can then process it however we need to for our application. By the way, if
we want to process it as Object, there is no need for Ctype, because type Object is a very receptive host.
We can apply a similar testing technique to TreeNode objects (or any other object). Our test could be “If
e.Data.GetDataPresent(GetType(TreeNode)) Then... ”, and our assignment can be “Dim item As TreeNode =
DirectCast(e.Data.GetData(GetType(TreeNode)), TreeNode)”. We can also use CType instead of DirectCast.
Although in this case we know the type of object it is, so we also know that CType will not generate
additional code, because there is no actual data conversion, but the compiler will simply assume that
new type, we will always know that the DirectCast compiler directive will never perform any actual
object conversion, and so it will never generate additional code, though if we got soft in the brain and
specified the wrong type, such as an incompatible type, the compiler will nag us about it like the Grand
Inquisitor, Sister Mary Tocomada, for cheating on a math test (we cannot talk her out of anything).
In Black Book Tip # 40, it showed us how to easily and very quickly reflect a drive directory to a
TreeView control. In Black Book Tip # 41, we showed how to perform better sorts on a drive TreeView.
Let us expand those examples even further by allowing users to drag a file to another folder. For our
example, we will not allow folders to be dragged, nor will we allow files to be dragged onto other files.
We will keep our example simple so we can clearly see its logic, although for a more robust application
we will need to prevent protected files and folders from being accessed or moved.
Unlike our previous example, we will not need a DragEnter event because all dragging occurs within
the TreeView itself, which we can initiate with a MouseDown event. However, we will need to check
each Node in the TreeView and determine if it is a folder or a file, and for that we will use a DragOver
event handler. This handler will pick up any TreeNode the cursor is currently moving over.
Unlike Mouse events, the DragOver event exposes a DragEventArgs argument, not a MouseEventArgs
argument, and sadly the DragEventArgs argument does not give us the relative cursor location. Normally,
in Mouse events, such as the MouseMove event for a TreeView, we can grab the TreeNode it is passing
over by taking advantage of the TreeView’s GetNodeAt method and simply provide it with the e.Location
property, which delivers coordinates local to the TreeView. For example: “Dim CurNode As TreeNode =
Me.TreeView1.GetNodeAt(e.Location)”. However, because this handler does not expose cursor coordinates,
we can instead aquire them by taking advantage of the TreeView’s own PointToClient method and utilize
the Cursor’s Position property and compute coordinates local to the TreeView, as in “Dim CurNode As
TreeNode = Me.TreeView1.GetNodeAt(TreeView1.PointToClient(Cursor.Position)) ”. This will convert the
screen coordinates of the Cursor to the local coordinates of the TreeView.
If the gathered TreeNode is defined (IsNot Nothing), then we can check it for being a folder or a file. In
Black Book Tips # 40 and #41, we checked the Node’s ImageIndex property for being 0 for a closed
(collapsed) folder, or 1 for an open (expanded) folder. Higher index values were free for file images.
 Page 604

We can initiate our drag and drop operation in a MouseDown event handler. Here, we could pick up the
TreeNode that the mouse select button has been pressed down on. If it is a file, we can initiate a drag
process by providing it with the detected file TreeNode. For example:
'*********************************************************************************
' Method : TreeView1_MouseDown
' Purpose : See if we want to start dragging a file.
'*********************************************************************************
Private Sub TreeView1_MouseDown(sender As Object, e As MouseEventArgs) Handles TreeView1.MouseDown
If e.Button = MouseButtons.Left Then 'selecting an item with possible drag?
Dim selNode As TreeNode = Me.TreeView1.GetNodeAt(e.Location) 'get the left-clicked node.
If selNode IsNot Nothing Then 'if there was a node found...
If selNode.ImageIndex > 1 Then 'if we are selecting a file...
TreeView1.SelectedNode = selNode 'then first select it...
TreeView1.DoDragDrop(selNode, DragDropEffects.Move) 'and then initiate a MOVE drag-and-drop operation.
End If
TreeView1.Focus() 'ensure focus is on the treeview.
End If
End If
End Sub

Next, we will need to know where we can and cannot drop the TreeNode being dragged. We will use the
DragOver event handler for that. We will first check to see if the item being dragged is a TreeNode. If it
is, we can pick up the node the cursor is dragging over and determine if we can drop the TreeNode on
that control or not. We will not be able to drop it on other files or its own parent folder. We can only drop
it on other folders. If we try dropping it on invalid locations, then nothing will be done. We will also
indicate to the user if they can drop on a location or not by changing the cursor effect.
Thus we could write our DragOver event handler like this:
'*********************************************************************************
' Method : TreeView1_DragOver
' Purpose : Dragging a node over a treeview.
'*********************************************************************************
Private Sub TreeView1_DragOver(sender As Object, e As DragEventArgs) Handles TreeView1.DragOver
Dim effect As DragDropEffects = DragDropEffects.None 'init to not being able to drop the node over the target.
If e.Data.GetDataPresent(GetType(TreeNode)) Then 'if the dragged object is a TreeNode...
Dim destNode As TreeNode
With Me.TreeView1
destNode = .GetNodeAt(.PointToClient(Cursor.Position)) 'get the node currently under the cursor.
End With
If destNode IsNot Nothing Then 'if there was a TreeNode there...
Dim srcNode As TreeNode =
CType(e.Data.GetData(GetType(TreeNode)), TreeNode) 'grab the TreeNode to be dropped.
If Not destNode.Equals(srcNode) AndAlso
destNode.ImageIndex <= 1 Then 'if they are not the same and the destination is a folder...
Dim path As String = srcNode.Parent.ToolTipText 'then grab the dragged node's parent path.
If StrComp(destNode.ToolTipText,
path, CompareMethod.Text) <> 0 Then 'if we are not dropping onto its parent...
effect = DragDropEffects.Move 'allow a drop (we are moving it).
End If
End If
End If
End If
e.Effect = effect 'set the cursor effect to expose.
End Sub

Finally, we will need to react when the user actually drops the dragged item. We can ignore it by not
processing it if they are dropping a node that testing code does not recognize. If we can drop it, we need
to perform house cleaning by moving the file to the new location, and then we will need to move the
TreeNode to its new TreeView location. Because a TreeView folder node might not be populated yet (for
speed), we can force it by expanding it (code within Black Book Tip # 40 does this). In this case we need
not worry about our node because it will be regenerated by the folder being populated. In that case, we
can then find the new node. In either case, we want to set focus on the file node in its new location.
We can do all this in the following DragDrop event code:
'*********************************************************************************
' Method : TreeView1_DragDrop
' Purpose : Dropping a TreeNode on the TreeView1 control.
'*********************************************************************************
Private Sub TreeView1_DragDrop(sender As Object, e As DragEventArgs) Handles TreeView1.DragDrop
If e.Data.GetDataPresent(GetType(TreeNode)) Then 'if the dragged object is valid...
Dim item As TreeNode = CType(e.Data.GetData(GetType(TreeNode)), TreeNode) 'get the dragged object.
 Page 605

If e.Effect = DragDropEffects.Move Then 'if we are handling a move event...


Dim destNode As TreeNode
With Me.TreeView1
destNode = .GetNodeAt(.PointToClient(Cursor.Position)) 'get the node currently under the cursor.
End With
If destNode IsNot Nothing AndAlso
Not destNode.Equals(item) AndAlso
destNode.ImageIndex <= 1 Then 'if we can drop it at the destination...
Dim filename As String = IO.Path.GetFileName(item.ToolTipText) 'grab file's name.
Dim destPath As String = destNode.ToolTipText 'get the destination path.
Dim DestFilePath As String = destPath & "\" & filename 'build a destination filepath.
If IO.File.Exists(DestFilePath) Then 'if a file by that name exists there...
Select Case MsgBox("The destination path already contains a file named" & vbCrLf &
filename & ". Do You want to overwrite it?",
MsgBoxStyle.YesNo Or MsgBoxStyle.Question, "File Already Exists at Destination")
Case MsgBoxResult.No 'the user does not want to over-write it.
Return
End Select
End If
Try 'An Io.File.Move() method seems faster, but we cannot overwrite. This method is safer than first deleting.
IO.File.Copy(item.ToolTipText, DestFilePath, True) 'first try copying the file (can over-write).
Catch
MsgBox("Error moving " & filename & "to " & destPath & ". Aborting.",
MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation, "Error Moving File")
Return
End Try
Try
IO.File.Delete(item.ToolTipText) 'now try deleting the original file.
Catch
MsgBox("Error removing " & filename & "from " & IO.Path.GetDirectoryName(item.ToolTipText) & ". Aborting.",
MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation, "Error Moving File")
Return
End Try
item.ToolTipText = DestFilePath 'assign it the new filepath.
Dim oldParent As TreeNode = item.Parent 'save reference to item's current parent.
item.Remove() 'remove the node from its current parent.
If oldParent.Nodes.Count = 0 Then 'it no longer has children, then close it.
oldParent.ImageIndex = 0 '0 = closed folder image.
oldParent.SelectedImageIndex = 0
End If
If Not destNode.Checked Then 'if folder has been opened or it was empty...
destNode.Nodes.Add(item) 'attach the detached node to its new parent
End If
If Not destNode.IsExpanded Then 'if the destination folder is not expanded...
destNode.Expand() 'expand and possibly repopulate.
End If
item = FindNodePath(destNode, DestFilePath) 'find the file node in its new parent.
If item IsNot Nothing Then 'if it is found (100% likely).
TreeView1.SelectedNode = item 'select it.
item.EnsureVisible() 'make sure it can be seen.
End If
TreeView1.Focus() 'set focus to the treeview.
End If
End If
End If
End Sub

'*********************************************************************************
' Method : FindNodePath
' Purpose : Find the node path in a treeview based upon its filepath.
'*********************************************************************************
Private Function FindNodePath(ByRef Node As TreeNode, ByVal SeekPath As String) As TreeNode
If Node.ToolTipText = SeekPath Then 'if current node contains the sought path...
Return Node 'return a reference to that node.
End If
If Node.Nodes.Count <> 0 Then 'else if it has children...
For Each subNode As TreeNode In Node.Nodes 'check each child.
If subNode.ImageIndex <= 1 Then 'if it is a folder...
Dim nd As TreeNode = FindNodePath(subNode, SeekPath) 'check its node and subnodes...
If nd IsNot Nothing Then 'did it find a match?
Return nd 'yes, so return it.
End If
ElseIf subNode.ToolTipText = SeekPath Then 'does the file contain the sought path?
Return subNode 'yes, so return the node.
End If
Next
End If
Return Nothing 'nothing found.
End Function

NOTE: If we were providing feedback as I often do within a status bar label, we may want to implement a DragLeave event
handler, which will always fire after a DragDrop event. There, we can erase any feedback.
 Page 606

BONUS TIP: Sometimes we want to double-click a file node and do something with it. The TreeView control already takes
care of double-clicking on folder notes. If a node can expand or collapse, its opened state will be automatically flipped by a
double-click. But what of file nodes (or folder nodes with no files)? Suppose we want to display property information on files,
or even open the file? In that case, we might notice that a DoubleClick or MouseDoubleClick are not recognized when a
Drag and Drop operation is in progress (by necessity, it keeps very tight control of the mouse during a Drag and Drop),
which was initiated in our MouseDown event. In this case it might be prudent to instead delay initiating the Drag and Drop
process until we actually know we are in fact trying to drag an item and not just clicking or double-clicking it.
This is actually quite easy to do. All we must do is make a small change to the MouseDown event to simply ensure that the
mouse is being pressed on a file, and then monitor node changes in a MouseMove event where we also verify that the mouse
button is being held down so we do know that we are in fact dragging the mouse (even though the Drag and Drop feature will
now not be set yet because we are also delaying the start of that process). If we find that we have dragged the mouse to a
different node (typically this will be an adjacent node), and because we already know the start node (the node we pressed the
mouse down on) has been verified to be a file, or it is set to Nothing if it is not, we can then start a Drag and Drop process.
Consider the following replacement MouseDown event and the new MouseMove event:
Private MouseDnNode As TreeNode = Nothing 'node saved in TreeView Mouse Down event to enhance drag and drop functionality.

'*********************************************************************************
' Method : TreeView1_MouseDown
' Purpose : See if we want to start dragging a file.
'*********************************************************************************
Private Sub TreeView1_MouseDown(sender As Object, e As MouseEventArgs) Handles TreeView1.MouseDown
Me. MouseDnNode = ActiveTV.GetNodeAt(e.Location) 'get the node the mouse was pressed on.
If Me.MouseDnNode IsNot Nothing AndAlso
Me.MouseDnNode.ImageIndex <= FolderOpened Then 'if this is not a file...
Me.MouseDnNode = Nothing 'then disable the reference.
End If
DirectCast(sender, TreeView).Focus() 'put focus on the TreeView.
End Sub

'*********************************************************************************
' Method : TreeView_MouseMove
' Purpose : Allow dragging item for a row before enabling drag-n-drop.
'*********************************************************************************
Private Sub TreeView_MouseMove(sender As Object, e As MouseEventArgs) Handles TreeView1.MouseMove
If Me.MouseDnNode IsNot Nothing AndAlso e.Button = MouseButtons.Left Then 'if start node is a file and left-mouse drags...
With DirectCast(sender, TreeView)
Dim SelNode As TreeNode = .GetNodeAt(e.Location) 'grab the possible node at the current location.
If SelNode IsNot Nothing AndAlso
SelNode IsNot Me.MouseDnNode Then 'if the current node is not the start node...
Me.MouseDnNode = Nothing 'First, reset start node reference, then...
.DoDragDrop(Me.MouseDnNode, DragDropEffects.Move) 'initiate drag-and-drop MOVE on a KNOWN file.
.Focus() 'put focus on the TreeView.
End If
End With
End If
End Sub

Now we can add and process DoubleClick and MouseDoubleClick events because a Drag and Drop operation will not
begin until the mouse has actually been dragged from one node over another.
 Page 607

Black Book Tip # 43


Taking Advantage of the DateTimePicker Control.
The DateTimePicker control is great, though many new programmers get
more than a bit frustrated with it. Typically, they want their user to pick a
date, but the control seems to close and return a date whenever the user clicks
anything on the calendar after they added event code for it (it seemed to
operate just fine prior to that). Every time they pick on a feature, the calendar
closes and a date is recorded. Their frustration rises from the fact that the
upper left and right triangle buttons are supposed to rotate sequentially
through months, or we should be able to click the month and year header to
bring up a list of months, and click it again to bring up a list of years. Grrr!
Their problem is that they are using the default event when they double-click
the control, which is ValueChanged. And true to its name, each time the date
value changes within the control, such as clicking a date or the left/right
month triangles, this event will fire, and this is where these programmers are
running into trouble. My advice is just to ignore that event, except when we
really do need to keep meticulous track of any and all date changes.
Most people simply want to accept a date when the user actually clicks on a
date, and they do not want the date that was initially in the control to change
if the user selected the dropdown button in the upper right corner, or picked
anywhere outside the calendar. Some industrious developers have written
quite elaborate code to intercept messages for the control, track its dropdown
control, etc., just to provide this service, but in most cases all that effort is
totally unnecessary, and they can get by using just the DropDown and CloseUp
events with very little code and a little bit of creativity. If we want to also check for the user canceling
using the Escape or Alt keys, there is just a tiny bit more code to add in a test.
The dropdown Calendar for a DateTimePicker control is 222 pixels wide and 160 pixels high for its
single-month display. By knowing that, we can totally avoid a truckload of otherwise very complicated
message intercept code.
Create a new Windows Form project and drop a DateTimePicker named DateTimePick1 and a TextBox
named TextBox1 anywhere onto the form (these names would be their defaults).
If we want to reset DateTimePicker1 to the value it held when the user hits its dropdown button, we need
to set aside a local Date variable so we can use it to reset the control if we cancel. For example:
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private dtpOrgDate As Date = Nothing 'original date when DTP calendar drops.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

We can set this variable when the calendar drops into view in its DropDown event:
'*********************************************************************************
' Method : DateTimePicker1_DropDown
' Purpose : The Date Picker calendar dropped down
'*********************************************************************************
Private Sub DateTimePicker1_DropDown(sender As Object, e As EventArgs) Handles DateTimePicker1.DropDown
Me.dtpOrgDate = Me.DateTimePicker1.Value 'grab current date in calendar.
End Sub

Also, we will need to pick up the cursor position to find out where the user clicked. Because the
DateTimePicker control does not provide any cursor information, we can very easily manually calculate
it directly from the Cursor itself relative to DateTimePicker1’s container. For example:
Dim pt As Point = Me.PointToClient(Cursor.Position) 'get cursor relative to DateTimePicker container.
 Page 608

Of course, if we get fancy and place our control on another container, such as a TabPage, GroupBox,
Panel, or whatever, we will need to calculate it from that container. For example:
Dim pt As Point = Me.GroupBox1.PointToClient(Cursor.Position) 'get cursor relative to DateTimePicker container.

Or, we can make our code much more robust by simply using the picker’s parent, like this:
Dim pt As Point = Me.DateTimePicker1.Parent.PointToClient(Cursor.Position) 'get cursor relative to DTP container.

After that, we will need to take a look at where this point is in relation to the DateTimePicker1 control.
We can take advantage of the Bounds property of that control to check for the pick being within the
bounds of the calendar, which is a 222 x 160 pixel field below that control’s bounds. For example:
Dim pt As Point = Me.DateTimePicker1.Parent.PointToClient(Cursor.Position) 'get cursor relative to DTP container.
With Me.DateTimePicker1.Bounds 'using the controls bounds as a guide...
Dim rect As New Rectangle(.Left, .Bottom, 222, 160) 'define bounding rectangle for calendar.
If rect.Contains(pt) Then 'if the pick is within the calendar bounds...
'<...Code for if user clicked on calendar goes here...>
End If
End With

If it is within the bounds of the calendar, we will want to set the picker’s date to TextBox1. Otherwise,
we want to reset the date of the picker. We can easily do that with this small addition to the above code:
Dim pt As Point = Me.DateTimePicker1.Parent.PointToClient(Cursor.Position) 'get cursor relative to DTP container
With Me.DateTimePicker1.Bounds 'using the controls bounds as a guide...
Dim rect As New Rectangle(.Left, .Bottom, 222, 160) 'define bounding rectangle for calendar
If rect.Contains(pt) Then 'if the pick is within the calendar bounds...
Me.TextBox1.SelectedText =
Me.DateTimePicker1.Value.ToLongDateString 'then set the date to the textbox
Else
Me.DateTimePicker1.Value = Me.dtpOrgDate 'not in range, so reset control to its original date
Me.DateTimePicker1.Checked = False 'uncheck the checkbox (set flags date change)
End If
End With

The only thing left to do is to check for the Alt or Escape keys being pressed. We can check for the Alt
key by checking the Boolean result being True with My.Computer.Keyboard.AltKeyDown. However, we
cannot check for the Escape key this way. Normally, people will test for it within a KeyDown or
PreviewKeyDown event and test for something like “ e.KeyCode = Keys.Escape”. However, we can check
it right within our above test by taking advantage of the GetKeyState Pinvoke, declared here:
Private Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Keys) As Short

By passing this interop function the code for the Escape key in the Keys collection (Keys.Escape), if the
returned value is less than zero then the designated key is pressed down (zero if it is up). Technically, we
can also test for the Alt key this way as well (Keys.Alt). If either of these keys is pressed we want to also
reset DateTimePicker1 to the date it held when the calendar dropped down from the control.
What follows is my complete version of this code:
Option Explicit On
Option Strict On
Public Class Form1
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private dtpOrgDate As Date = Nothing 'original date when DTP calendar drops.
Private Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Keys) As Short
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
' Method : DateTimePicker1_DropDown
' Purpose : The Date Picker calendar dropped down.
'*********************************************************************************
Private Sub DateTimePicker1_DropDown(sender As Object, e As System.EventArgs) Handles DateTimePicker1.DropDown
Me.dtpOrgDate = Me.DateTimePicker1.Value 'grab current date in calendar.
End Sub
 Page 609

'*********************************************************************************
' Method : DateTimePicker1_CloseUp
' Purpose : The Date Picker calendar closed up, so see if we should record results.
'*********************************************************************************
Private Sub DateTimePicker1_CloseUp(sender As Object, e As System.EventArgs) Handles DateTimePicker1.CloseUp
If My.Computer.Keyboard.AltKeyDown OrElse
GetKeyState(Keys.Escape) < 0 Then 'Alt or ESC pressed...
Me.DateTimePicker1.Value = Me.dtpOrgDate 'then reset the control to its original date.
Me.DateTimePicker1.Checked = False 'uncheck the checkbox (set flags date change).
Else
Dim pt As Point = Me.DateTimePicker1.Parent.PointToClient(Cursor.Position) 'get cursor relative to DTP container.
With Me.DateTimePicker1.Bounds 'using the controls bounds as a guide...
Dim rect As New Rectangle(.Left, .Bottom, 222, 160) 'define bounding rectangle for calendar.
If rect.Contains(pt) Then 'if the pick is within the calendar bounds...
Me.TextBox1.SelectedText =
Me.DateTimePicker1.Value.ToLongDateString 'then set the date to the TextBox.
Else
Me.DateTimePicker1.Value = Me.dtpOrgDate 'not in range, so reset control to its original date.
Me.DateTimePicker1.Checked = False 'uncheck the checkbox (set flags date change).
End If
End With
End If
Me.TextBox1.Focus() 'set focus to the date TextBox.
End Sub

End Class

If we want to keep the result wholly within the DateTimePicker control’s Value property, just remove its
setting of the text box data and access the Value property of the DateTimePicker to pick up the date,
which is, of course, of type Date. For example, the above code within the the WITH block can be
simplified to:
With Me.DateTimePicker1.Bounds 'using the controls bounds as a guide...
Dim rect As New Rectangle(.Left, .Bottom, 222, 160) 'define bounding rectangle for calendar.
If Not rect.Contains(pt) Then 'if the pick is outside the calendar bounds...
Me.DateTimePicker1.Value = Me.dtpOrgDate 'not in range, so reset control to original date.
Me.DateTimePicker1.Checked = False 'uncheck the checkbox (set flags date change).
End If
End With

Afterward, we can examine the Date held in DateTimePicker1.Value to acquire any selected date.
In this case, it might be useful to set its ShowCheckBox property to True so the user can uncheck this
box if they decide against applying the date in our application, and we can simply test for its Checked
property being True to determine if we want to use the date stored in the control.
Note, however, that when the DateTimePicker’s date value changes, such as when we reset it to its
original value, that this checkbox will again be checked because the value has changed. In this case,
when we are resetting the control’s value to its initial date, we must also be sure to reset the control’s
Checked property to False, as we have been doing all along in our example code. Then, once the user
has closed the calendar, we can easily determine if they selected a date or not through added code at the
bottom of the CloseUp event.
 Page 610

Black Book Tip # 44


Padding Strings and Filling Separator Lines in Proportionally-spaced Text.
Padding string in mono-spaced fonts like Consolas or Courier New is amazingly easy. We can even use
the Lset function to return the provided text that has a specified width, padded on the right with spaces
to fill out the required width. For example: Dim Txt As String = LSet(“Data”, 20) . This creates a 20-
character field filled with spaces, and the “Data” text is located in its first 4 character positions.
But when trying to manually pad text that is displayed in a “prettier”, more professional-looking
proportionally-spaced font can be a real trick, which often includes a lot of trial and error and a lot of
test compiles until the displayed text looks right. This can be especially important for displaying tally
reports, where results are displayed on the right, but we want the totals to line up, such as the following:
Widgets that are stored in the database : 106
Widgets not meeting requirements : 46
Widgets that meet requirements : 60
Widgets unselected by the operator :2
-------------------------------------------------------
Final tally of widgets used in this project : 58

Were we to use LSet to try to format the above in a proportionally-spaced font, the results would look
rather ragged, like the following using the Ariel font (or even worse, depending on the font being used):
Widgets that are stored in the database : 106
Widgets not meeting requirements : 46
Widgets that meet requirements : 60
Widgets unselected by the operator :2
-------------------------------------------------------
Final tally of widgets used in this project: 58

Because in proportionally-spaced fonts, the width of “i” is different from “W”, and the width of a space
character may even be as short as a period “.”, alignment can become a process of frustration as we edit
the source to manually insert spaces, recompile, test, edit again, recompile again, test again, and so on.
However, using a small method, we can quickly pad strings to properly align them. Ideally, if we were
able to just use a vbTab, things would be simpler, but menus and message boxes do not like them, and
sometimes mapping tab positions in a TextBox, Label, or RichTextBox is too much bother or practical for
what might be just a last-second Friday Quittin' Time request from the boss for an important client.
To resolve this issue when we need the text to align, we first need to determine the longest line in the
text, to establish the maximum line width, and we also need the font we want to use to display it.
Typically, people will often resort to using a new font, such as Dim TestFont As New Font("Microsoft San
Serif", 8.0!, FontStyle.Regular). However, this is not compatible with globalization locales, or when
the user changes their desktop theme, which often changes the system font used for displaying the text
within menus and a MsgBox. But, the good news is, one of our namespaces, System.Text, contains a
collection named SystemFonts that we can use to select from several pre-defined fonts, based on the
user’s preferences, such as CaptionFont, DefaultFont, DialogFont, IconTitleFont, MenuFont,
MessageBoxFont, SmallCaptionFont, and StatusFont. Typically, DefaultFont, which is, oddly enough,
the system default, is used by DialogFont, MenuFont, and MessageBoxFont to determine the font to
display text with. For example: Dim sysFont As Font = SystemFonts.DefaultFont. We can, of course, use
the font that is assigned to the control where we will display our text, such as a TextBox or Label.
With the desired font referenced, we can use the MeasureText function of the TextRenderer class of the
System.Text namespace to determine the pixel length of a string. For example: Dim maxLength As Int32 =
TextRenderer.MeasureText("This is my longest line: ", sysFont).Width . In this example, we can establish
our target pixel width by getting the length of the longest line in the data.
 Page 611

If we wanted to, we could also add an additional pixel count to this result for extra padding.
After that, as we build the string, we could measure its length and add spaces until we meet the desired
pixel width. We should also take into consideration padding the strings on the left instead of the right, or
using a character other than a space for padding, such as “-”, “+”, “_”, or “.”. An en dash “–”,
ChrW(&H2013) is also good, for creating a solid line. Indeed, with this feature we could easily construct
dividing lines separating a heading string from its list.
Consider the following function:
'*********************************************************************************
' Method : PadTextString
' Purpose : Pad a text string at the end until it fits a specified pixel width.
' :
' : This method is useful for displaying a list of result values or you need
' : a separator line that fills a required width. This is especially important
' : For text that Is displayed using a proportionally-spaced font (each
' : caracter has different widths and the space character is very narrow), so
' : padding can usually be a process Of trial And Error. This eliminates all
' : that and the frustration that goes along with it, such as the following:
' :
' : Widgets that are stored in the database : 106
' : Widgets not meeting requirements : 46
' : Widgets that meet requirements : 60
' : Widgets unselected by the operator : 2
' : -----------------------------------------------
' : Final tally of widgets used in this project: 58
' :
' : Notice that the above is shown in monispaced text. The ACTUAL strings for
' : each line will have different lengths in order to match their right side,
' : depending on the display font, and actual character widths and of the space.
' :
' : By specifying PadOnLeft=True, the padding will be added to the left of
' : the string, otherwise it will by default pad it on the right.
' :
' : By specifying UseChar with a character value, the padding will be added
' : using the provided character, otherwise it will default to a space.
'*********************************************************************************
Friend Function PadTextString(ByVal Text As String,
ByVal TargetWidth As Int32,
ByRef TargetFont As Font,
Optional ByVal UseChar As Char = " "c,
Optional ByVal PadOnLeft As Boolean = False) As String
Dim pad As New String(UseChar, 5) 'init padding string.
Do While TextRenderer.MeasureText(Text, TargetFont).Width < TargetWidth 'while the text is too short...
If PadOnLeft Then 'if we want to pad the string on the left...
Text = pad & Text 'expand with the padding on left of string.
Else
Text &= pad 'otherwise expand with the padding on the right.
End If
Loop
Do While TextRenderer.MeasureText(Text, TargetFont).Width > TargetWidth 'while the text is too long...
If PadOnLeft Then 'if we want to pad the string on the left...
Text = Text.Substring(1) 'strip a pad char from the left end...
Else
Text = Text.Substring(0, Text.Length - 1) 'strip a pad char from the right end...
End If
Loop 'and try again.
Return Text 'finally, return the string.
End Function

When we provide the above PadTextString function with text to return a modification on, the target
width in pixels that we want to fill the text out with, and a font we want to establish this padding for, it
will return a string padded on the right with the proper number of spaces to fill out the pixel
TargetWidth. As we can see, we can optionally provide a character other than a space in the optional
UseChar parameter. If we provide True for the optional PadOnLeft parameter, the string will be padded
on the left rather than the right.
In the heading of the function we see our example list. Consider the following program code, which will
duplicate this list, properly aligned, in a MsgBox:
 Page 612

Dim iTtlWdgts As Integer = 106 'Total Widgets (should be defined elsewhere in the code).
Dim iGoodWdgts As Integer = 60 'Widgets that are acceptable to the project.
Dim iBadWdgts As Integer = iTtlWdgts - iGoodWdgts 'compute widgets not acceptable.
Dim iRjctWdgts As Integer = 2 'widgets rejected by the operator.
Dim iFinalTotal As Integer = iGoodWdgts - iRjctWdgts 'compute total widgets used in project.

Dim msgFont As Font = SystemFonts.MessageBoxFont 'font used by MsgBox.


Dim longestText As String = "Final tally of widgets used in this project" 'max legnth string in formatted message.
Dim maxWidth As Int32 = TextRenderer.MeasureText(longestText, msgFont).Width 'define test width.
Dim Msg As String = PadTextString("Widgets that are stored in the database", maxWidth, msgFont) & ": " & iTtlWdgts.ToString & vbCrLf &
PadTextString("Widgets not meeting requirements", maxWidth, msgFont) & ": " & iBadWdgts.ToString & vbCrLf &
PadTextString("Widgets that meet requirements", maxWidth, msgFont) & ": " & iGoodWdgts.ToString & vbCrLf &
PadTextString("Widgets unselected by the operator", maxWidth, msgFont) & ": " & iRjctWdgts.ToString & vbCrLf &
PadTextString(Nothing, maxWidth, msgFont, "-"c) & "--" & vbCrLf &
longestText & ": " & iFinalTotal.ToString & vbCrLf
MsgBox(Msg, MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "Widget Results") 'display the message.

Notice in the above we created a separator line made of dashes. Notice also we
added a couple more dashes to dress up the line. Notice even further that we
defined the initial line text as Nothing. We also added a couple extra dashes to
it in the above case, to allow for the addition of the colon and space in the
other lines. Further, because we wanted the text to align before the colons in
the lists, we appended “: ” after each to make sure the colons were aligned.

Adding One or More Fitted Separator Lines in Mid-Text


We can also dynamically build separator lines within text. For example, suppose we have a header that
we want to separate from a list of items, but we do not know the maximum width in advance (typical),
such as one of the list members might be longer than the header. We could initialize our list like this:
Dim Msg As String = "The following clients are added:" & vbCrLf & "---" & vbCrLf

NOTE: Here, we will replace the triple-dash text, "---", once we are ready to actually display it.
Next, append our list of items to the above string and terminate each line with a vbCrLf.
Once finished with adding to the list, determine the over-all width of the text block and replace the "---"
text within the complete message string with a fully-defined line, like the following:
Dim msgFont As Font = SystemFonts.MessageBoxFont 'font used by MsgBox
Dim maxWidth As Int32 = TextRenderer.MeasureText(Msg, msgFont).Width 'define max width
MsgBox(Msg.Replace("---", PadTextString(Nothing, maxWidth, msgFont, "-"c)),
MsgBoxStyle.OkOnly Or MsgBoxStyle.Information,
"Client Add Report") 'add line and display the message

We could redesign the previous example to take advantage of this enhancement as well:
Dim iTtlWdgts As Integer = 106 'Total Widgets (should be defined elsewhere in the code)
Dim iGoodWdgts As Integer = 60 'Widgets that are acceptable to the project
Dim iBadWdgts As Integer = iTtlWdgts - iGoodWdgts 'compute widgets not acceptable
Dim iRjctWdgts As Integer = 2 'widgets rejected by the operator
Dim iFinalTotal As Integer = iGoodWdgts - iRjctWdgts 'compute total widgets used in project

Dim msgFont As Font = SystemFonts.MessageBoxFont 'font used by MsgBox


Dim longestText As String = "Final tally of widgets used in this project" 'max legnth string in formatted message
Dim maxWidth As Int32 = TextRenderer.MeasureText(longestText, msgFont).Width 'define test width
Dim Msg As String = PadTextString("Widgets that are stored in the database", maxWidth, msgFont) & ": " & iTtlWdgts.ToString & vbCrLf &
PadTextString("Widgets not meeting requirements", maxWidth, msgFont) & ": " & iBadWdgts.ToString & vbCrLf &
PadTextString("Widgets that meet requirements", maxWidth, msgFont) & ": " & iGoodWdgts.ToString & vbCrLf &
PadTextString("Widgets unselected by the operator", maxWidth, msgFont) & ": " & iRjctWdgts.ToString & vbCrLf &
"---" & vbCrLf &
longestText & ": " & iFinalTotal.ToString & vbCrLf
maxWidth = TextRenderer.MeasureText(Msg, msgFont).Width 'define max width for text block
Msg = Msg.Replace("---", PadTextString(Nothing, maxWidth, msgFont, "-"c)) 'replace 3-dash text, "---", with full-width dashed line
MsgBox(Msg, MsgBoxStyle.OkOnly Or MsgBoxStyle.Information, "Widget Results") 'display the message
 Page 613

Black Book Tip # 45


Making Sense of Form Default Buttons and Their DialogResult Properties.
This is a rather simple subject, but it has some balding VB programmers pulling their hair out.
They are having fits when simply trying to define one or two default buttons for their forms, and
especially because they cannot seem to get them to also execute code they wrote for those buttons.
Typically, people set up their default buttons, such as assigning a Cancel button to the form’s
CancelButton property, and an Accept or OK button to the form’s AcceptButton property; very nice and
very convenient. We can even set the DialogResult properties on the buttons so that the value assigned
to them will be assigned to the form when the form closes and returns control to the invoker, though we
may have noticed that a button assigned to the form’s CancelButton property will be automatically
assigned a DialogResult of Cancel. Problems occur when the programmer wants to do when, or after the
user selects a button, but before the form closes, such as saving properties or the form’s screen position,
dimensions, and state. Though such things might be better served in the form’s FormClosing event,
there might be other reasons why the programmer wants to massage some data before leaving. However,
regardless, they are a bit surprised when the code that they wrote for the button’s click event does not
even execute, but rather the form closes and returns the DialogResult value assigned to by the button.
What these people need to understand is that the reason why a button has a DialogResult property in the
first place is so we can slap a button onto a form without having to write any event code to support it.
Thus, we can drop a button on the form, assign any DialogResult value other than None to it, and go on
to the next item on our task list. When the program runs, if the user hits one of those buttons, the
DIalogResult in the selected button will be assigned to the form and then the form will automatically
close, skipping any code we might have assigned to the button.
Therefore, if we want to write code for the Cancel or Accept button’s Click event, we should make sure
that the button’s DialogResult property is set to None. This way our custom code will in fact execute. At
the end of it, we can assign a DialogResult value not to the button, but to the form (which is where the
button’s DialogResult property was going to be assigned to anyway, if defined), such as “Me.DialogResult
= DialogResult.Ok”, and then close the form with “Me.Close”.

We should be doubly sure that the DialogResult property is set to None for a Cancel button when we
assign it to the form’s CancelButton property. This auto-assignment does not happen to a button
assigned to the form’s AcceptButton property, though it will never hurt to verify it.

Buttons Assigned a Dialog Result Have a CloseReason of None


Speaking of None, when we let the system take care of our button support because we assigned
DialogResult values to those buttons, when we process code in a FormClosing event, we usually check
e.CloseReason to see if the user closed it normally. Though we generally check this value for being set
to CloseReason.UserClosing, the support code for buttons assigned a DialogResult instead issue a value
of CloseReason.None! As such, we will have to check for both values, in case any other button also
closes the form, but we did not assign them a DialogResult value. For example:
Private Sub BrowserDialog_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
Select Case e.CloseReason
Case CloseReason.UserClosing, CloseReason.None 'DialogResult buttons issue CloseReason.None.
If Me.DialogResult = DialogResult.OK Then 'if user selected OK. Cancel will fall around this.
If Me.WindowState = FormWindowState.Normal Then 'save data if not Minimized or Maximized.
Dim Title As String = My.Application.Info.Title 'Quick access to App.Title.
SaveSetting(Title, "Settings", "FormTp", Me.Top.ToString)
SaveSetting(Title, "Settings", "FormLf", Me.Left.ToString)
SaveSetting(Title, "Settings", "FormWd", Me.Width.ToString)
SaveSetting(Title, "Settings", "FormHt", Me.Height.ToString)
End If
End If
End Select 'The test for CloseReason.UserClosing in this case is not really required, unless you were doing
End Sub ' something if Me.DialogResult did not equal DialogResult.OK.
 Page 614

Black Book Tip # 46


Quick and Easy Text-Justification for Text Boxes, Labels, and Dialog Boxes.
Previously we saw how to display fully justified text in a TextBox control (see Black Book Tip # 9,
“Display TextBox Text Format-Justified at Runtime” on page 470). That solution employed a class that
performed a lot of complex real-time painting services to deliver perfectly justified text, even if the
TextBox was resized or its font was changed. We also learned how to easily enable built-in full text
justification in a RichTextBox control (see Black Book Tip # 30, “Enable Built-In Justify-Alignment in a
RichTextBox from VB.NET” on page 555). But there is yet a third option, and this can even be applied
on-the-fly to text using any font, proportional or monospaced, whether to a TextBox, a Label control, or
even to a system message box via the MsgBox command or through a MessageBox Pinvoke.
The code needed to accomplish this task is unbelievably simple, and that is especially surprising considering
how many developers out on the web have wailed about wanting to do this. This was a recent spur of the
moment solution I wrote in about 30 minutes to justify a lot of message text I had been working with.
I’m sure most everyone has typed an extra space or two between words of a label’s text to fill its lines out
and make them appear justified. After a few times doing this manually, I realized this process could easily be
automated. This type of justification might not be straight-edge perfect, due to between-word spacing having
to be even multiples of a space character of the selected font, but it still looks absolutely fantastic!
For example, compare these MsgBox displays, where the right message was auto-padded with spaces:

To achieve this, I made a small modification to my SizeMessage and SizeMsgLine functions featured in
Black Book Tip # 7, “Sizing a Label or TextBox to Fully Contain a String for Display” on page 465, by
adding an optional parameter, UseCRonly, which, if set to True and the SizeMsgLine method must add
line breaks, they will just be a Carriage return ( vbCr), not the usual Carriage Return and Linefeed
(vbCrLf). This way we can easily find lines needing justification because they will end with only a vbCr.
Those ending with a vbCrLf will not need justification. This is important, because the method may need
to insert additional line breaks to fit the text within a maximum width specification.
For example, the text for the above dialogs were actually defined in-code with this assignment:
Dim msg As String = "Signatures are blocks of text that are added to the end of email data. " &
"They can be used to provide additional contact information, your Web page, " &
"to advertise your business, to add a philosophical anecdote, list published " &
"work, or anything else you wish to throw down on the end of your email. Or, " &
"you could simply apply your name." & vbCrLf & vbCrLf &
"You can insert pictures as well, so you could include your favorite photo, " &
"a shot of the kids doing something stupid and/or funny, or even an image " &
"of your signature. For example, I like to follow my email with this:" & vbCrLf & vbCrLf &
" \|/" & vbCrLf &
" ~ ~" & vbCrLf &
" (@ @)" & vbCrLf &
"--oOO-{_}-OOo------------------------------------------------------" & vbCrLf 'final vbCrLf for V-spacing
 Page 615

For the above examples, I declared a data-width limit of 375 pixels. Also, I used the font we should
normally use for a dialog box, such as the MsgBox, which is SystemFonts.MessageBoxFont. Indeed, the
great thing about this quick-and-dirty solution is that it can work with any font, proportionally-spaced or
mono-spaced, and at any desired point size, although point sizes under 16-points look best.
The way we process this code is rather simple, though the results are quite impressive. Starting with an
initial message, such as defined in the above msg definition, we must process it using the display font
and a desired maximum width. Though a MsgBox has a maximum message width of 375 pixels, if we
are assigning our text to a label or text box, we can use broader dimensions. For the above example, I
submitted my message to SizeAndJustifyMessage, my new quick text-justification method, like this:
SizeAndJustifyMessage(msg, SystemFonts.MessageBoxFont, 375) 'submit message, display font, and maximum width for message

The SizeAndJustifyMessage function checks the message for having multiple physical lines, meaning
there are vbCrLf line breaks embedded within it. If so, we can split it on the line breaks and then process
each physical line. For all this work, it simply invokes the previously defined SizeMessage function,
which feeds each line to the SizeMsgLine method to break them up to fit within the maximum limit.
Its first point of divergence is that it tells SizeMsgLine, via its invocation of SizeMessage, to make any
needed additional breaks using a single vbCr character rather than the default vbCrLf characters.
The second point of divergence occurs after all original lines are processed. At that point, SizeMessage
would return the outside rectangular dimensions that will contain this resulting form of the message text.
However, SizeAndJustifyMessage, though it likewise acquires these dimensions, it first will further
process all lines ending in only a single vbCr to pad them with spaces to emulate justification. So,
basically, we have two levels of the text being broken up: we have the primary line group of physical
lines that each terminate with a vbCrLf, and then their secondary, inner line group of each physical line
that were broken up that were terminated by just a vbCr. Further, we do not want to pad any lines that
end physical lines (lines ending with vbCrLf), but that is easy to determine because they will always be
the very last or only secondary-level line in the group comprising each primary grouping.
The above sounds much more complicated than it really is. If we look at the above msg assignment, all
text up to a vbCrLf is considered a primary, or physical line. These lines are all the text contained in an
array element if we split this message on vbCrLf boundaries, such as “Dim Ary() As String = Split(msg,
vbCrLf)”. The secondary level comes when we feed each of these primary, physical lines to the
SizeMsgLine support method. It checks the submitted line for being longer than the requested maximum
width value. We can check the text’s dimensions by submitting it and the display font to the
MeasureText function of the TextRenderer class (defined within the System.Text namespace). For
example, “Dim msgSize As Size = TextRenderer.MeasureText(Text, SystemFonts.MessageBoxFont)”. We can
check the Width property of the msgSize structure for the currently defined pixel width of the submitted
text, were it displayed using the provided font. If the line width is longer than our submitted limit, 375 in
this case, it will search for where the text can be broken up to fit each segment within that limit. It then
inserts a vbCr at the needed point, removes the trailing space, and then checks the remainder of the line.
Once the SizeAndJustifyMessage function receives the full dimensions of the current state of the
message from SizeMessage, it then uses the invoker-provided maximum text width value to process the
returned adjusted text to additionally pad any lines that are required to be justified.
NOTE: The MeasureText function can also easily process multi-line text. In that case, it will return the widest string dimension of
the text and the height of all the rows of physical lines. However, text-wrapping in a text box is not accounted for, which is why we
had to insert additional line breaks, so the returned vertical height would be correct.

We process each line by feeding it into a very simple loop that splits each vbCrLf-terminated line into a
secondary array of lines split by just vbCr (this will happen if a physical line exceeds the width limit). We
then process each of these secondary array lines and check to see if each contains at least one space. If a
space is found in a line, we break that line further into yet another array, this time split on its spaces. We
 Page 616

then loop, each time, appending just a space to a single word, and then check the resulting line size (we
use a cycling index that increments to a word after each check of the text length). Once our width goal is
met, we process the next secondary line, except for the last (or only) one, which will ultimately end on a
physical line break of the original text (such a break is also assumed on a single line message).
Once this secondary array has finished processing each of its members, the secondary array is rejoined
back into its primary array member, and then the next primary array member is processed.
Once the primary array has completed processing all of its physical line members, that array is finally
rejoined back into the original message string.
My updated modComputeMsgDims module is shown below:
Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modComputeMsgDims Static Class Module (updated)
' DisplayMessage Support Methods.
'-------------------------------------------------------------------------------------
'The SizeMessage function:
'-------------------------
' The SizeMessage function, provided a message, a font that the message will display with,
' and the maximum width of the message container, will size the message to fit that maximum
' width, update the message as needed to add additional line breaks to keep each line within
' the limits of the provided MaxWidth in pixels parameter, and return a Size object that will
' provide the height and Width required to contain that message.
'
' The SizeAndJustifyMessage function:
'-----------------------------------
' The SizeAndJustifyMessage function, provided a message, a font that the message will Display
' with, and the maximum width of the message container, will size the message to fit that
' maximum width update the message as needed to add additional line breaks to keep each line
' within the limits of the provided MaxWidth in pixels parameter and also format-justify the
' text by padding the space fields on non- terminated lines (lines not ending in vbCrLf) to
' make the text appear justified to give the left and right sides of the text even margins,
' and then return a Size object that will provide the height and Width required to contain
' that message. This function also makes allowances for lines beginning with spaces.
'-------------------------------------------------------------------------------------
' Input : ByRef Parameter Message As String
' This is the text that is to be adjusted to fit within a specified MaxWidth value
' measured in pixels. By providing these functions with the text variable refer-
' encing your text, on return this will contain your text and with any needed
' alterations.
'
' : ByRef Fnt As Font
' This is the font you wish to measure the text with. This should be the font that
' you will in turn be displaying the resulting text with. If you are displaying
' data in a MsgBox, you should use SystemFonts.MessageBoxFont. The SystemFonts
' class offers several system fonts and is very useful for international apps,
' because the system fonts on the user'ssystem or theme are used.
'
' : ByVal MaxWidth As Integer
' The maximum width can be anything, but in general a rule of thumb is to maintain
' widths in the range from 200 pixels to 550 pixels. Anything outside this range
' tends to look bad. The default MsgBox text field uses a maximum of 375 pixels.
' If you want the text to be limited to the width bounds of the target textbox
' or label, you can provide that control's Width parameter as the limit.
'
' : Optional ByVal UseCRonly As Boolean = False
' By default, added line breaks to keep the text within the bounds of the specified
' MaxWidth parameter. However, when the SizeAndJustifyMessage() invokes SizeMessage()
' to initially format the text, it sets this Parameter to True.This way it can tell
' which lines should be padded with spaces to format-justify its text. Note, however,
' that displaying text with line breaks consisting of either vbCrLf or vbCr display
' no differently.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
' Returns: Size structure containing the Width and Height of the text field required to
' Display the Message text.
'-------------------------------------------------------------------------------------
' NOTE: You can use the returned Size structure to size a text box or a non-auto- sizing
' label. You can also resize the containing form by subtracting the current height of
' the text box or label and then adding the Size.Height value, and likewise
' subtracting the current width of the textbox or label and then adding the
' Size.Width value. If you anchor all four sides of the textbox or label to the form
' (or its container, which should likewise be anchored), then the text box or label will
' also-size to the size of the text data. Otherwise, you can simply assign the Size
' structure to the Size property of your textbox or label. Yet, if you are formatting
' the text for display within a system message box, you do not need to worry about
' retaining the returned value. The message box itself will acquire this structure and
' adjust its size as needed. Even so, for a message box, the maximum width value should
' be no greater than 375, because it will wrap lines outside this range.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modComputeMsgDims
'*********************************************************************************
' Method : SizeMessage
' Purpose : Compute the display size of the Message text and also update the message as
' : needed to add additional line breaks to keep each line within the limits of
' : the provided MaxWidth in pixels parameter, and return a Size object that will
' : provide the height and Width required to contain that message.
 Page 617

'*********************************************************************************
Friend Function SizeMessage(ByRef Message As String,
ByRef Fnt As Font,
ByVal MaxWidth As Int32,
Optional ByVal UseCRonly As Boolean = False) As Size
Dim pAry() As String = Split(Message, vbCrLf) 'split out each line of the prompt,
For Idx As Int32 = 0 To UBound(pAry) 'then check the width of each one...
pAry(Idx) = SizeMsgLine(pAry(Idx), Fnt, MaxWidth, UseCRonly) 'to make sure each line will wrap properly.
Next
Message = Join(pAry, vbCrLf) 'reconstruct the array, in case anything changed in the above loop.
Return TextRenderer.MeasureText(Message, Fnt) 'return the computed size of the final message.
End Function

'*********************************************************************************
' Method : SizeAndJustifyMessage
' Purpose : Compute the display size of the text and pad it with spaces to fill it out to
' : make it appear format-justified. Also, allow for leading spaces.
'*********************************************************************************
Friend Function SizeAndJustifyMessage(ByRef Message As String,
ByRef Fnt As Font,
ByVal MaxWidth As Int32) As Size
SizeMessage(Message, Fnt, MaxWidth, True) 'initially break the line up to fit within our width limit.
'-----------------------------------------------------------------------
'now see about padding each line
'-----------------------------------------------------------------------
Dim pAry() As String = Split(Message, vbCrLf) 'split out each line of the prompt into a primary array,
For Idx As Int32 = 0 To UBound(pAry) 'then process each line block (made by breaking up by vbCr's)...
Dim sAry() As String = Split(pAry(Idx), vbCr) 'break each sub-item up
For Idy As Int32 = 0 To UBound(sAry) - 1 'pad all except the last line (it had ended with a vbCrLf break).
If Not String.IsNullOrWhiteSpace(sAry(Idy)) AndAlso
Trim(sAry(Idy)).IndexOf(" "c) <> -1 Then 'if this string contains at least 1 embedded space...
Dim Leader As String = String.Empty 'init leading space buffer.
Dim Idz As Int32 = 0 'init index to where nons-space text begins.
Do While sAry(Idy).Substring(Idz, 1) = " " 'while we have leading spaces...
Idz += 1 'bump the start index.
Loop
If Idz <> 0 Then 'if leading spaces were found...
Leader = Space(Idz) 'build the leading space buffer.
Idz = 0 'reset our indexer to 0 (for later).
End If
Dim tAry() As String = Split(Trim(sAry(Idy)), " ") 'break logical line up at all embedded spaces.
' while the final line length is within bounds...
Do While TextRenderer.MeasureText(Leader & Join(tAry, " "), Fnt).Width <= (MaxWidth – 4) 'allow for a 4-pixel field of play.
tAry(Idz) &= " " 'pad an indexed member with an additional single space.
Idz += 1 'bump the index for the next word member.
If Idz = UBound(tAry) Then 'if we are at upper bounds...
Idz = 0 'then cycle index (we do not want to pad the last member word).
End If
Loop
sAry(Idy) = Leader & Join(tAry, " ") 'stitch secondary strings back together.
End If
Next
pAry(Idx) = Join(sAry, vbCrLf) 'stitch strings back together.
Next
Message = Join(pAry, vbCrLf) 'reconstruct the message, in case anything changed.
'-----------------------------------------------------------------------
Return TextRenderer.MeasureText(Message, Fnt) 'return the computed size of the final message.
End Function

'*********************************************************************************
' Method : SizeMsgLine (private support method for the two above exposed methods)
' Purpose : Ensure the Message does not exceed the maximum Width value, MaxWidth, using
' : the supplied font, Fnt, which is presumed to be the the that the text will
' : afterward displayed with.
'*********************************************************************************
Private Function SizeMsgLine(ByVal TextLine As String,
ByRef Fnt As Font,
ByVal MaxWidth As Int32,
Optional ByVal UseCRonly As Boolean = False) As String
Dim lnTerm As String = vbCrLf
If UseCRonly Then
lnTerm = vbCr
End If
Dim TextSize As Size = TextRenderer.MeasureText(TextLine, Fnt) 'get the dynamics of the individual line of text.
Dim Idx As Int32 = TextLine.TrimEnd.Length 'grab the length of the entire text line.
Dim Ofst As Int32 'offset will be 1 if space ends line, and 0 if a dash.
Do While Idx <> 0 AndAlso TextSize.Width > MaxWidth 'is its present width greater than the maximum allowed?
Dim Idy As Int32 = TextLine.LastIndexOf("-"c, Idx - 1) 'track backward for a dash.
Idx = TextLine.LastIndexOf(" "c, Idx - 1) 'also track backward for a space.
If Idy > Idx Then 'if we found a dash before a space...
Idx = Idy 'set index to dash.
Ofst = 0 'allow dash to be included in text data.
Else
Ofst = 1 'skip over space.
End If
TextSize = TextRenderer.MeasureText(TextLine.Substring(0, Idx).TrimEnd, Fnt) 'recalculate that size based on the dash found.
Loop
If Idx > 0 AndAlso Idx < TextLine.TrimEnd.Length Then 'if the length of the text is to be adjusted...
TextLine = TextLine.Substring(0, Idx + 1).TrimEnd & lnTerm &
TextLine.Substring(Idx + 1 + Ofst).Trim 'break the text up with vbCrLf and break that down.
Dim Ary() As String = Split(TextLine, lnTerm) 'now break the text up into an array.
For Idx = 1 To UBound(Ary) '& test each higher line (index 0 is already correct).
Ary(Idx) = SizeMsgLine(Ary(Idx), Fnt, MaxWidth, UseCRonly) 'process and break up each part as needed (recursion).
Next
TextLine = Join(Ary, lnTerm) 'convert array into a single string with linebreaks.
End If
Return TextLine 'return the text to the invoker.
End Function
End Module 'I hope everyone caught the embedded justification joke in this module.
 Page 618

The two exposed functions, SizeMessage and SizeAndJustifyMessage, are easy to use. Their uses are
identical, except that the SizeAndJustifyMessage function will additionally ‘justify’ the text by padding
embedded spaces within it. Notice further that it will leave any leading spaces on a line alone.
Suppose we have a Label control named Label1 and we have turned its
AutoSize property to False so that it can be resized to accommodate any
text shoved into it. To ensure text or expanding this label will not cause it
to be clipped off the form, we will also resize the form to accommodate
the formatted data. We will also initially size the label to fill most of the
form. For example:
Assuming that a variable named Msg of type String contains the message
we want to display, we can achieve all this with the following form code:
Option Strict On
Option Explicit On

Public Class Form1


Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
Dim Msg As String = " Transitioning from Microsoft Visual Basic 6.0 (VB6) to Microsoft Visual Basic .NET (VB.NET ), at first " &
"glance, may look to be an intimidating endeavor. After all, you have likely heard or read through copious magazines " &
"and blogs that there are huge differences between these two developmental platforms. Though many of those differences " &
"are real, many others are simply imagined, engendered by nothing more than unapprised conjecture. Of the real " &
"platform deviations, most are simply due to them having to be expressed differently; plainly because VB.NET " &
"strictly follows a stringent pattern of uniform language syntax, which is something VB6 was not always good at. " &
"Hence, a programming language feature may have to be implemented under VB.NET using a radically different " &
"access conduit than the way it may have been realized under VB6." & vbCrLf &
" Other disparities, some seen as much more profound, actually end up being VB6 features that VB.NET does in " &
"fact support, but, again, due to tight .NET platform architectural specifications, VB.NET cannot support them in " &
"a like manner, but may by necessity have to utilize non-VB6-style invocation rules. Nevertheless, by employing " &
"some simple user-defined helper functions, such as will be demonstrated within this document, you can easily " &
"emulate ""lost"" VB6 commands, or, in most cases, make their functionality more accessible through simpler syntax. " &
"Regardless, you will find that, overall, VB.NET supports all these many differences, both major and minor, in " &
"but different forms, and in all it also implements much more robust techniques to apply their functionality."
Dim sz As Size = SizeAndJustifyMessage(Msg, Me.Label1.Font, Me.Label1.Width) 'initially format the message.
Me.Width = Me.Width - Me.Label1.Width + sz.Width 'initially size the form.
Me.Height = Me.Height - Me.Label1.Height + sz.Height
Me.Label1.Size = sz 'size the display label.
Me.Label1.Text = Msg 'stuff the updated text.
End Sub
End Class

NOTE: If we anchor Label1 to all four sides of the form, we can remove the Label1.Size assignment because the form
resizing will then automatically resize the label’s dimensions for us.
Running this, the display looks great. But, we can do even more here.
Suppose we wanted to be able to resize the form with the mouse and
reformat the message along with it? That is also quite easy to do. We
will need to keep an original copy of the message text, so in the above
Form1 Load event, after we assign the message to Msg, we can store a
copy of it in the Tag property of Label1, as in “Me.Label1.Tag = Msg”.
Next, we will need to add a ReSize event for the form, but we do not
want it to do anything until the form has loaded. We can do this by
defining “Private FormLoaded As Boolean = False” above and outside
of the form’s Load event, and then adding “FormLoaded = True” before
the End Sub command for the event to let all know the form has now
been loaded.
 Page 619

Next, in the resize event, we first test to see if FormLoaded is True, and if so we will need to compute a
new target width. We can do this by subtracting double the value of Label1’s Left property from the
form’s client’s width like so: “Dim NewWidth As Integer = Me.ClientSize.Width - Me.Label1.Left * 2”.
After that, we get a copy of the original message from the label’s Tag property: “Dim Msg As String =
Me.Label1.Tag.ToString”. We then compute a new Size structure: “ Dim sz As Size =
SizeAndJustifyMessage(Msg, Me.Label1.Font, NewWidth)”. Then, if the new size does not match the label’s
current size, we assign the new size to the label.
Altogether, our updated resize-capable form code would look like this:
Option Strict On
Option Explicit On

Public Class Form1


Private FormLoaded As Boolean = False 'true when the form has loaded.

Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load


Dim Msg As String = " Transitioning from Microsoft Visual Basic 6.0 (VB6) to Microsoft Visual Basic .NET (VB.NET ), at first " &
"glance, may look to be an intimidating endeavor. After all, you have likely heard or read through copious magazines " &
"and blogs that there are huge differences between these two developmental platforms. Though many of those differences " &
"are real, many others are simply imagined, engendered by nothing more than unapprised conjecture. Of the real " &
"platform deviations, most are simply due to them having to be expressed differently; plainly because VB.NET " &
"strictly follows a stringent pattern of uniform language syntax, which is something VB6 was not always good at. " &
"Hence, a programming language feature may have to be implemented under VB.NET using a radically different " &
"access conduit than the way it may have been realized under VB6." & vbCrLf &
" Other disparities, some seen as much more profound, actually end up being VB6 features that VB.NET does in " &
"fact support, but, again, due to tight .NET platform architectural specifications, VB.NET cannot support them in " &
"a like manner, but may by necessity have to utilize non-VB6-style invocation rules. Nevertheless, by employing " &
"some simple user-defined helper functions, such as will be demonstrated within this document, you can easily " &
"emulate ""lost"" VB6 commands, or, in most cases, make their functionality more accessible through simpler syntax. " &
"Regardless, you will find that, overall, VB.NET supports all these many differences, both major and minor, in " &
"but different forms, and in all it also implements much more robust techniques to apply their functionality."
Me.Label1.Tag = Msg 'save original copy of message.
Dim sz As Size = SizeAndJustifyMessage(Msg, Me.Label1.Font, Me.Label1.Width) 'initially format the message.
Me.Width = Me.Width - Me.Label1.Width + sz.Width 'initially size the form.
Me.Height = Me.Height - Me.Label1.Height + sz.Height
'Me.Label1.Size = sz 'size the display label.
Me.Label1.Text = Msg 'stuff the updated text.
FormLoaded = True 'allow resize to operate.
End Sub

Private Sub Form1_Resize(sender As Object, e As System.EventArgs) Handles Me.Resize


If FormLoaded Then 'if the form has loaded...
Dim NewWidth As Integer = Me.ClientSize.Width - Me.Label1.Left * 2 'compute the new target width.
Dim Msg As String = Me.Label1.Tag.ToString 'grab an original copy of the message.
Dim sz As Size = SizeAndJustifyMessage(Msg, Me.Label1.Font, NewWidth) 'compute new format.
If Me.Label1.Size <> sz Then 'if the size has actually changed...
'Me.Label1.Size = sz 'resize the display label.
Me.Label1.Text = Msg 'update the formatted text.
End If
End If
End Sub

End Class

With this updated code in place, we can now resize the form with the mouse and the label’s text will
update as needed, fully justified. To make it more robust, we should also define a minimum size for our
form to prevent the code from crashing if we resize it too small (see the form’s MinimumSize
parameter).
NOTE: In the above code, I am assuming that we have anchored all four sides of our label to the form, or docked it. If not,
then uncomment the above “Me.Label1.Size = sz” statement.
 Page 620

Black Book Tip # 47


Owner-Drawn Controls Made E-Z.
If we do any owner-drawing in a ListView, TreeView, ListBox, Tab Control, or ComboBox, we can really
spiff up a project with ease by adding enhancements to images, controls, and so on. Indeed, owner-
drawing this stuff is now so easy it is almost stupid. Sadly, many long in the tooth developers will
scramble for religious artifacts to ward off owner drawing, afraid to screw it up. Actually, they will only
mess up visual appearances, which can be hilarious, but no demons will ooze out of USB ports. Also,
there is typically only a tiny bit of code to mess up in this whole owner-drawing business anyway. This
is especially true under VB.NET, which takes all the tedium and testing of our will to live out of owner-
drawing, which, under VB6, was almost not worth the effort we had to slog through. Indeed, even a
fresh face to VB.NET should master its owner-drawing in less than an hour by just “playing around”.
When working with list controls, such as a ListBox or ComboBox, we should first ensure that the list
index the control event receives is valid. We can verify it by using a “If e.Index <> -1 Then” test. This is
not documented, but it is something I first noticed in VB2008; that sometimes it delivers an index of -1.
It was fixed in VB2010, but many developers still use VB2008 Pro (I love this platform!), and I keep my
guard up. Next, before we actually draw anything, we want to prepare our drawing area by first clearing
it, which we can do easily by simply issuing “ e.DrawBackground()”. After that, it is just like drawing to a
PictureBox control using the e.Graphics object. Of course, some basic knowledge of what the
information and tools provided to us by the DrawItem event object can go a long way.
A DrawItem event handler, whether generated for us or entered by hand, is very simple. For example:
Private Sub DrawListboxItem(ByVal sender As Object, ByVal e As DrawItemEventArgs)

End Sub

NOTE: This hand-made version does not display a Handles shorthand reference, though we could have added it. However,
we can just as easily add them to any control using AddHandler, especially for those created on-the-fly.
The two parameters above, sender and e, provide us with all the tools we will need. The sender object is
actually a reference to our control, which we might define as a ListBox or ComboBox. If we are working
with a ListBox, for example, the basic skeletal logic I add to this method is the following:
Private Sub DrawListboxItem(ByVal sender As Object, ByVal e As DrawItemEventArgs)
If e.Index <> -1 Then 'if the index is not out of bounds (pre-VB2010)...
e.DrawBackground() 'clear background of line position.
With DirectCast(sender, ListBox) 'with the provided listbox...
'The FUN stuff goes here...
End With
End If
End Sub

Here, e.Index provides us with the index to the list item we are working with, which in this case is an
index within the Items collection of the ListBox. We can store class objects here instead of just strings,
though we must provide an overriding ToString method to our classes so that a ListBox or ComboBox
can easily extract displayable text information from it. We can also take advantage of that feature.
Suppose we want to display an image or an icon at the start of each line. In this case, it is important that
we have adjusted the ItemHeight property of our control to accommodate our images. In most situations
we typically work with 16x16-pixel images or icons. By default, the control’s ItemHeight property is set
to 13, though this depends on our system font. In the USA, the default for SystemFonts.DefaultFont is
8.25-point Microsoft San Serif, but this default font can vary between international, accessibility, and
personal desktop theme settings. So, to display our images without overlapping them, we should change
the height to match our images, which in this case is 16. For example: “Me.ListBox1.ItemHeight = 16”.
NOTE: We may prefer to set the ItemHeight parameter to 17 or 18, instead of 16, if our images actually fill their 16-pixel
height definition with no border, just to add a noticeable gap between each row.
 Page 621

Next, we can draw our image. If we are displaying the 2nd image in an ImageList named ImageList1, we
could issue the command “e.Graphics.DrawImage(Me.ImageList1.Images(1), e.Bounds.Location)”. If we draw
an image in our resources named “RadioSel”, we can use “ e.Graphics.DrawImage(My.Resources.RadioSel,
e.Bounds.Location)”. If this were an icon, we could use either “ e.Graphics.DrawIcon(My.Resources.BunnyIcn,
e.Bounds.X, e.Bounds.Y)” or “e.Graphics.DrawImage(My.Resources.BunnyIcn.ToBitMap, e.Bounds.Location)”.
NOTE: Notice that we are using integer values where the documentation requests floating point values. Though thinly
documented, this is allowed because these methods are able to do automatic type conversions between 32-bit integer and
single-precision values through automatic implicit casting in the various graphical methods.
The advantage of using the DrawIcon method, even though we do need to specify both the start X and Y
coordinates (or else we can specify a bounding rectangle, such as “New Rectangle(e.Bounds.Location, New
Size(16, 16))”), is that its transparency color will show the background color behind it so we will not
end up with little background-colored boxes around certain images that lack a transparency color
definition, though using 32-bit PNG images with transparency works just as well.
The reason we are clinging to our e.Bounds structure is because it is not defined relative to the line, but
to the drawing surface. Indeed, if we inspect the e.Bounds.Y value, we will notice that it will actually
specify the target coordinate relative to the top of the ListBox (or the ComboBox drop list, which is
actually a ListBox). We may also notice it takes into consideration margin definitions. By default, a
ListBox or ComboBox line has a 3-pixel margin on each of its four sides.
Finally, were you aware that we can draw outside the e.Bounds rectangle? The Bounds property just
gives us the bounds definition of the line entry we are working on, though we can technically draw to
the entire exposed list surface. I used this once as a non-destructive April Fool’s prank to make
someone’s list look like it was melting, because a ListBox is actually just a PictureBox with a vertical
scrollbar attached to its right side, which is itself a PictureBox with flat buttons.
Next, we usually want to display the text assigned to the ListBox (or ComboBox). This is also very easy
to do. What we need is the text, the font to shape the text, a color brush to paint the text, and the
coordinates of the top-left corner of the text as either a Point, or else the X and Y coordinates for the top-
left corner of the text.
Because we are wrapping code within a With block that defines our ListBox, we can grab the text for the
indexed item with “.Items(e.Index).ToString”, we can reference the ListBox font using “.Font”, and a
black brush using “Brushes.Black”. We also need the coordinate of the top-left corner of the text as
either a Point structure or as X and Y values. Because our images are 16 pixels wide, we want to specify
an X coordinate that is beyond that image, and, for appearances, an additional 4-pixel buffer. To do all
these things, we can specify “e.Graphics.DrawString(.Items(e.Index).ToString, .Font, Brushes.Black,
e.Bounds.X + 16 + 4, e.Bounds.Y)”.

Altogether, our DrawListBoxItem method becomes the following:


Private Sub DrawListboxItem(ByVal sender As Object, ByVal e As DrawItemEventArgs)
If e.Index <> -1 Then 'if the index is not out of bounds...
e.DrawBackground() 'clear background of line position.
With DirectCast(sender, ListBox) 'with the provided ListBox...
e.Graphics.DrawImage(Me.ImageList1.Images(1), e.Bounds.Location) 'draw our image.
'e.Graphics.DrawImage(My.Resources.RadioSel, e.Bounds.Location) 'draw our image (alternate resource version).
'e.Graphics.DrawIcon(My.Resources.RadioNoSel, New Rectangle(e.Bounds.Location, New Size(16, 16))) 'icon version.
e.Graphics.DrawString(.Items(e.Index).ToString, .Font, Brushes.Black,
e.Bounds.X + 16 + 4, e.Bounds.Y) 'draw text of color 4 pixels beyond image.
If .SelectedIndex = e.Index Then 'if this is the selected item...
e.DrawFocusRectangle() 'draw a focus rectangle (not needed by ComboBox).
End If
End With
End If
End Sub

NOTE: When drawing to a ComboBox, e.DrawFocusRectangle has no effect and can be ignored. The ComboBox uses it to
update the selection line as the cursor moves over it. This is actually exposed by the ListBox control it uses as a drop list.
 Page 622

But now that we have this thing, we need to tie it to our ListBox. This is also easy to do. We could have
selected our ListBox and chose its DrawItem event, which would have provided us with this method body:
Private Sub ListBox1_DrawItem(ByVal sender As Object, ByVal e As System.Windows.Forms.DrawItemEventArgs) Handles ListBox1.DrawItem

End Sub

And then we could have inserted the above body code within it, or, we could tie our previous hand-made
version to any ListBox using: “AddHandler Me.ListBox1.DrawItem, AddressOf DrawListBoxItem”.
Finally, we need to tell the ListBox that we want to draw its line items ourselves. We can do that easily
by specifying “Me.ListBox1.DrawMode = DrawMode.OwnerDrawFixed”, or setting that parameter from the IDE.
NOTE: If we are feeling adventurous, we can set Me.ListBox1.DrawMode = DrawMode.OwnerDrawVariable, and draw freely on
the drop list surface. I use it to draw a TreeView type structure to list several options with delineated sub-options, but we can
as easily do that using OwnerDrawFixed. Besides, this mode gives us the line bounds, not the whole surface bounds.
And that is all there is to it! Squeezy-cheesy nice-n-easy!
For a practical example that we can slap together faster than an ISU student making a cram-night order
from Dog Town's Pizza Pit, where we will simply draw numerous system-known color blocks with their
color names in a ListBox, create a new Windows Form Application, and, without adding anything else to
it, add the following form code to its blank form class body:
Option Explicit On
Option Strict On

Public Class Form1


'***********************************************************************************
' Method : Form1_Load
' Purpose : load the form and add a listbox to it, fill it, and add an event handler.
'***********************************************************************************
Private Sub Form1_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
With New ListBox 'to test your VB.NET savvy, do you know how this on-the-fly object stays alive beyond Form1_Load?
.Parent = Me 'let the form adopt this new listbox.
.Sorted = True 'the list will be sorted alphabetically.
.Dock = DockStyle.Fill 'fill the client area with the listbox.
.ItemHeight = 24 'change color field height from 13.
.DrawMode = DrawMode.OwnerDrawFixed 'define owner-drawn mode with fixed-height data.
AddHandler .DrawItem, AddressOf DrawList 'add and event handler for drawing list items.
'---------------------------------------------------------------------------
For Each KnownColor As KnownColor In [Enum].GetValues(GetType(KnownColor)) 'now parse each known color enumeration.
If Not Color.FromName(KnownColor.ToString).IsSystemColor Then 'if we did not grab a system color...
.Items.Add(KnownColor.ToString) 'then add it to the list.
End If
Next
End With 'Note the ListBox references other objects, but nothing seems to reference it... or is that so?
End Sub 'See the VB SECRET at the end of this tip for the answer.

'***********************************************************************************
' Method : DrawList
' Purpose : Draw a single item in the listbox (invoked by listbox for each displayed item.
'***********************************************************************************
Private Sub DrawList(ByVal sender As Object, ByVal e As DrawItemEventArgs)
If e.Index <> -1 Then 'if the index is not out of bounds...
e.DrawBackground() 'clear background of line position.
With DirectCast(sender, ListBox) 'with the provided ListBox...
Dim clrHt As Int32 = .ItemHeight - .Margin.Top - .Margin.Bottom 'compute field height, less margins.
Dim clrWd As Int32 = 64 'field width.
Dim orgPt As New Point(e.Bounds.X + .Margin.Left, e.Bounds.Y + .Margin.Top)
Dim rect As New Rectangle(orgPt, New Size(clrWd, clrHt)) 'define a rectangle to fill with color.
Dim brsh As New SolidBrush(Color.FromName(.Items(e.Index).ToString)) 'define a brush from the color name.
e.Graphics.FillRectangle(brsh, rect) 'fill the rectangle with the color.
brsh.Dispose() 'dispose of the created brush resource.
e.Graphics.DrawRectangle(Pens.Black, rect) 'draw a rectangle around it.
e.Graphics.DrawString(.Items(e.Index).ToString,
.Font, Brushes.Black,
orgPt.X + clrWd + 8, orgPt.Y) 'draw color text 8 pixels beyond image.
If .SelectedIndex = e.Index Then 'if this is the selected item...
e.DrawFocusRectangle() 'draw a focus rectangle around it.
End If
End With 'HINT: Did you notice the event was able to reference the created on-the-fly ListBox?
End If 'Figure out if you can make the ListBox go out of scope and dissolve after Form1_Load.
End Sub
End Class
 Page 623

Notice that we did not even declare a name for our ListBox; we simply
instantiate it. Internal pointer references will keep it alive throughout the
lifetime of the application. We attach it to the form by making the form its
parent, we then dock the ListBox to fit the client bounds of the form, we turn
its sorted option on, we set its Draw Mode to OwnerDrawnFixed, and we finally
attach a draw-item event to it. We next enumerate all known color names in
the system to this list, except for system colors. System colors are defined by
themes, so we will ignore those by not adding any that have their
IsSystemColor property set, so we just focus on the base color palette.
VB SECRETS: We are often told to assign it to a reference and then add that to the form's Controls collection, just to keep a
reference to the object “alive”, but this was something we only had to do in early editions of VB.NET, but as of VB2008, it
already “clandestinely” adds an instantiated form object to its container's Controls Collection. This is similar to how
VB.NET auto-adds instantiated forms to our application's OpenForms Collection; a process any developer worth their salt
should be familiar with. This is how I turn students who love to write programs into professional developers with high-paying
jobs (and to those students who used my homework assignments to secure some of those jobs, you are welcome! We only
really develop our software development skills “on the job”, anyway). We should ever endeavor to stretch and physically
grow our noodle in trying to figure out how this ListBox, created as a temporal object defined within a With declaration,
continues to live not only beyond the End With, but also beyond the end of the Form Load Event, if we understand VB.NET's
strict compliance to Scoping Rules. Figured it out yet? Hint – there are at least three references to it! It is the practice of
incessantly embarking on little research quests and mental exercises like this that separates the wheat from the tares in
software development; that which separates those who are called gurus by others, and those who call themselves gurus (I
love the first type!). Also consider the difference between the terms Software Programmer. Software Developer, Software
Engineer, and Solution Designer. There is always something new to learn. It is startling how many new things we can pick up
each day, even for old dogs like me! Or is it that I am just re-learning the things I have forgotten? To paraphrase Mark
Twain, “The more you learn; the more you know. The more you know; the more you remember. The more you remember; the
more you can forget. The more you forget; the less you remember. The less you remember; the less you know. The less you
know; the less you learn. So why learn?”
The assignment of a control’s Parent property will internally add a “back reference” to the control from its parent in the
Win32 system space, which can be found by enumerating the parent using the FindWindowEx Pinvoke (internally, all
controls are window objects)), thus preventing the child control, named or not, from going out of scope, as local, narrowly-
scoped declarations normally do, and being scooped up by the Garbage Collector when control leaves the End With, let
alone the Load event, which discards objects that are no longer referenced by anything that cannot also be referenced.
When we shut an application down under VB.NET (this also happens under VB6), unlike with C++, we will not cause
memory leaks by not disposing of all our instantiated forms, classes, and controls. One of the protections built into VB in
order to make it a Rapid Application Development (RAD) language platform is that when VB shuts down, it internally takes
a look at its component lists and closes and deallocates any that are still resident. When a Form closes, it rolls through its
Controls list and closes and deallocates them, cascading that process through control collections that any child containers
might hold. Prior to each object being disposed, it checks for any child objects still attached to it, such as controls not added
to a Controls list, and closes them. It winds its way back to the form, closes and deallocates it, then goes on to the next form,
and then any components instantiated that are not part of the forms, such as classes, like Module Classes, and the massive
collection of Delegate Classes, though often derided by users of AddressOf, they are the very backbone of the CLR, and all
event code is marshaled by them, maintaining references to objects that might otherwise fall out of scope, but they will then
dispose of any still-instantiated objects in their lists. Anything missed by this process is picked up by the Garbage Collector,
which will find objects that are no longer referenced, or, as is often the case, no longer referenced by objects that are no
longer referenced, even if down through a chain, and it invokes their finalizers, either auto-built or user-defined, and then
finally closes down the application. Any objects still existing were created outside of the CLR's managed space, because all
managed objects have an internal reference to them, like a GPS beacon or a LoJack device on a vehicle, so the Common
Language Runtime can be sure that every object instantiated in the .NET environs is closed down. Thus,any memory leaks
are on us, likely because we did not close them down through a Dispose or Finalize method. This is why calling .NET a
Managed system is a very cool thing.
 Page 624

Black Book Tip # 48


Owner-Drawn Directory TreeViews Using OwnerDrawAll Made E-Z.
In the last Black Book Tip, I showed how to do general owner-drawing on controls. Yet, some
programmers get frustrated when trying to draw for nodes of TreeView controls. Their principle problem
is that when they want to render the tree and its branch graphics, which means setting the TreeView's
DrawMode property to OwnerDrawAll, they get stuck because the DrawNode event's e.bounds encompasses the
entire line for a given node, but does not provide any rectangle to place text as the OwnderDrawText mode
does, but that mode also draws all the branching graphical elements. OwnerDrawAll is meant to give us
freedom to render each line however we choose. But they want both. The truth is, this is not necessary!
Their confusion stems from how to calculate graphical and text offsets. However, as I will demonstrate,
computing these offsets, no matter how difficult they try to make it, is actually brain-dead simple!
That aside, we can perform some really cool-looking effects quite easily using OwnerDrawAll, such as
emulating the branching used by Windows Explorer. For example, with an open folder,
it displays a down-pointing chevron next to an on-side folder. When closed and it
contains at least one subfolder, it displays a faded right-pointing chevron next to the on-
side folder. If it contains no subfolders, it displays just the on-side folder.
These three graphics are simple 16x16-pixel images. I created my own as
PNG with Transparency files (RGB/A – 32 Bit) with Axialis Software’s
IconWorkshop from a hi-rez screen capture of File Explorer, though we
could even create 16x16 JPEG files with white backgrounds using Windows
Paint and get passable results. However, I did shorten the height of the folder image by 2 pixels from a
full 16-pixels to 14-pixels, to leave a 1-pixel gap above and below it. Just center the other two.
NOTE: We can add these 3 Images to our projects. Just include the sample code listed at the end of this Black Book Tip.
I saved these in an ImageList control named ImageLists. I also defined an enumeration to specify their
image index offsets, in case I add other images and do not want to have to later change any hard-coded
index numbers. We could reference them as FolderSide for the first image of the on-side folder,
FolderIsOpen for the second image, “v”, and FolderCanOpen for the third, “>”. For example:
Friend Enum FolderImages As Int32
FolderSide
FolderIsOpen
FolderCanOpen
End Enum

In our TreeView’s DrawNode event code, the first thing we need to do is compute the indent depth. This
is E-Z. The trick is that we might run into calculation trouble if we have the TreeView’s ShowRootLines
or ShowPlusMinus parameters set to False. But let us ensure we can calculate things perfectly!
To make drawing with OwnerDrawAll almost too easy, consider these TreeView graphical settings:
• Be sure to set the TreeView's ShowRootLines property to True.
• Be sure to set the TreeView's ShowPlusMinus property to True.
• The TreeView's ShowLines property has no noticeable effect when using OwnerDrawAll.
For owner-drawing, some set these to False, but this also causes “click zone” calculations to be back-set
by the width of the Indent property, making image placement trickier. Set to True allows perfect
placement, so we can quickly calculate the placement of images to precisely fit over default “hot” zones
to toggle folder opening and closing when we click them. To compute indents, we set a reference to the
current node, initialize an indent counter to 0, and traverse the branches by checking to see if the node
has a parent node. If so, we increment the counter, point our reference to its parent, and continue
checking in a loop until no ancestral node is found. The resulting counter is multiplied times the Indent
property and added to the Bounds.X property to perfectly position the first folder image location.
 Page 625

For example, consider this code that computes node branch Image offsets in pixels.
Private Sub tvFolderTree_DrawNode(sender As Object, e As DrawTreeNodeEventArgs) Handles tvFolderTree.DrawNode
Dim tv As TreeView = DirectCast(sender, TreeView) 'get a reference to the TreeView container.
Dim nCount As Int32 = 0 'init indent counter to 0.
Dim nd As TreeNode = e.Node 'start with the current node provided to us.
Do While nd.Parent IsNot Nothing 'while the node has a parent...
nCount += 1 'count a generation (indent index).
nd = nd.Parent 'then point to its parent (it assumes the parent object's identity).
Loop 'and try again, channeling the parent.
Dim X As Int32 = e.Bounds.X + nCount * tv.Indent 'compute start offset from the left bounds using a branch node width.
Dim Y As Int32 = e.Bounds.Y 'compute starting offset from the top of TreeView bounds.
'...Other code goes here
End Sub 'NOTE: Offsets will be off if ShowRootLines and ShowPlusMinus were False.

NOTE: The example I am building here does not list files, it being derived from my folder-only BrowserDialog control I had
developed to enable the user to view a starting file path node (see an image of it near the last page of this Black Book Tip).
Later, in Black Book Tip # 52, Extracting Icon Images from Files and Displaying Them in a Directory TreeView, on page
665, we will include files in a TreeView and feature Explorer-style icons for them.
When doing owner drawing on a TreeView, the background is auto-initialized because there is no
e.DrawBackground method available, but be careful! Although it does initialize, it will redraw text
without clearing this space when we move the cursor over the +/- areas. This sometimes causes the
user-drawn text to look slightly bolder. This is due to fonts with alpha-blended edges being redrawn over
themselves. We can easily fix this by simply clearing the exact rectangular area where the text is to be
drawn (we do this to eliminate flicker). Also missing is “e.DrawFocusRectangle”, though we can easily
emulate this feature when a node is selected. Fortunately, providing these two services is incredibly easy.
Indeed, we should actually draw this focus rectangle first (we can also clear the background using this
method), and then paint our images and text over it. By checking the IsSelected property of the node
referenced by e.Node, we can tell if we must draw a focus rectangle or clear the line. We can use
e.Bounds to get the bounds of this rectangle. Of course, in drawing a focus rectangle of a color such as
SystemColors.HighLight, I would prefer the text on that line be white instead of the default (usually
black) to enhance its readability. To consolidate all of these needs, we can start by setting aside a Brush
object that can be used to either draw the focus rectangle or initialize (clear) its background, and then
this brush can be used afterward to draw our text. Of course, because we are creating a brush resource,
we must also be sure to dispose of it when we are finished with it. For example:
Dim Brush As New SolidBrush(tv.BackColor) 'brush used for selection and text coloring.
If e.Node.IsSelected Then 'if the node is selected...
Brush.Color = SystemColors.Highlight 'draw a selection rectangle around it.
e.Graphics.FillRectangle(Brush, e.Bounds)
Brush.Color = tv.BackColor 'change the brush's color to white.
Else
e.Graphics.FillRectangle(Brush, e.Bounds) '(later code will narrow this to text area).
Brush.Color = tv.ForeColor 'we will draw the text normally.
End If
'...Other code goes here
Brush.Dispose() 'release brush resources.

Here, if we find that the node is selected; we will create a brush of SystemColors.HighLight and fill the
bounds of the rectangle, effectively painting the entire node line. This is what a TreeView normally does
when we select its FullRowSelect property. If we did draw a selection rectangle, we then set the brush to
the color tv.BackColor. If it was not selected, we use tv.ForeColor.
We are now ready to draw our images. If we have a folder that contains sub-folders, we will actually
draw two images. The first will be the “v” image if the folder is actually opened, or “>” if it is closed. If
it has no subfolders, we will skip 16 pixels past that space. The second image will be our on-side folder.
However, we can draw them in any order. We could even draw the text first if we wanted to. Because we
will always draw the second image, the on-side folder, we could even get the drawing chores for the text
and that image out of the way before we worry about the “v” or “>” images.
 Page 626

For example:
e.Graphics.DrawString(e.Node.Text, Me.tvFolderTree.Font, Brush, X + 16 + 16 + 4, Y) 'draw text beyond 2 images + 4 pixel buffer.
Brush.Dispose() 'release brush resources. All images 16x16.

e.Graphics.DrawImage(Me.ImageLists.Images(FolderImages.FolderSide), X + 16, Y) ' go ahead & draw on-side folder.

NOTE: Some would argue that instead of specifying “16 + 16 + 4”, we should instead specify “+36”, but I let the compiler’s
optimizer worry about summing those values, while on my end I keep the offset more understandable.
Now all we have left is drawing a “v” image if the node is expanded, a “>” image if it is not expanded
but contains sub-folders, or nothing if it is a folder with no subfolders. For example:
If e.Node.IsExpanded Then 'if the folder is expanded....
e.Graphics.DrawImage(Me.ImageLists.Images(FolderImages.FolderIsOpen), X, Y) 'draw "V" before on-side folder.
ElseIf e.Node.Nodes.Count <> 0 Then 'not expanded, but does it have sub-folders?
e.Graphics.DrawImage(Me.ImageLists.Images(FolderImages.FolderCanOpen), X, Y) 'yes, so draw ">" before on-side folder.
End If

Altogether, and making it a bit more robust, our TreeView’s DrawNode event code becomes:
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
' Folder image Index List for User-Drawn Nodes in a Folder-Based TreeView Control.
' Code to programmatically add these images will be shown at the end of this Black Book Tip.

Friend Enum FolderImages As Int32


FolderSide
FolderIsOpen
FolderCanOpen
End Enum
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
' Method : tvFolderTree_DrawNode
' Purpose : Draw node with indent and state.
'*********************************************************************************
Private Sub tvFolderTree_DrawNode(sender As Object, e As DrawTreeNodeEventArgs) Handles tvFolderTree.DrawNode
Dim tv As TreeView = DirectCast(sender, TreeView) 'get reference to TreeView container.
Dim nCount As Int32 = 0 'init indent counter to 0.
Dim nd As TreeNode = e.Node 'start with current node provided to us.
Do While nd.Parent IsNot Nothing 'while the node as a parent.
nCount += 1 'count a generation (indent index).
nd = nd.Parent 'point back to its parent node...
Loop 'and try again.
Dim pt As New Point(e.Bounds.X + nCount * tv.Indent, e.Bounds.Y) 'compute Top-left coord for drawing.
Dim Rect As New Rectangle(New Point(pt.X + 16 + 16 + 4, pt.Y),
TextRenderer.MeasureText(e.Node.Text, tv.Font)) 'compute bounds of just text area...
Dim Brush As SolidBrush = New SolidBrush(tv.BackColor) 'brush for selection & text coloring.
If e.Node.IsSelected Then 'if the node is selected...
Brush.Color = SystemColors.Highlight 'change background to highlight color.
e.Graphics.FillRectangle(Brush, e.Bounds) '(could use Rect to minimize highlight).
Brush.Color = tv.BackColor 'change the brush's color to white.
Else
e.Graphics.FillRectangle(Brush, Rect) 'clear JUST text area background...
Brush.Color = tv.ForeColor 'we will draw the text normally.
End If

If e.Node.IsExpanded Then 'if the folder is expanded....


e.Graphics.DrawImage(Me.myImages.Images(FolderImages.FolderIsOpen), pt) 'draw "V" in front of on-side folder.
ElseIf e.Node.Nodes.Count <> 0 Then 'not expanded, but has sub-folders?
e.Graphics.DrawImage(Me.myImages.Images(FolderImages.FolderCanOpen), pt) 'yes, draw ">" before on-side folder.
End If
pt.X += 16 'point to on-side folder position.
e.Graphics.DrawImage(Me.myImages.Images(FolderImages.Folderside)), pt) 'draw the on-side folder.
e.Graphics.DrawString(e.Node.Text, tv.Font, Brush, Rect.Location) 'draw text.
Brush.Dispose() 'release brush resources.
End Sub

NOTE: The image to the left was captured from my custom FolderBrowser dialog control featured in
Black Book Tip # 50. It includes all the functionality that I wish the default Folder Browser Dialog
control had, such as easier logical and virtual drive selection, navigation history browsing, an Up-
One-Folder option, and to include displaying and focusing on a starting Selected Path, if I set it.
NOTE: The fact that we are able to draw to the entire surface of a Treeview, as we actually can do in
a ListBox, ComboBox, etc., offers a lot of potential for some fancy design work. However, even
drawing within just the provided bounding rectangle, we can actually emulate all the TreeView
effects, reflecting its properties of ShowRootLines, ShowPlusMinus, and ShowLines. We could also
define images of a vertical line that we could draw to each preceding tree level (just use the control’s Indent parameter to set
 Page 627

the spacing between images, which is typically 19 pixels, though we are free to change that as well). We could also very
easily draw these features manually. This includes the L-shaped branch line, branch connectors, nodes that can expand or
contract, and so on.
The complete source code for the above Folder Browser Dialog, which includes all the images (in-code), can be found in
Black Book Tip # 50 on page 642. See the subsection below to see how to add the three folder images shown earlier and
above, which supports the example code, by just invoking a simple method, InitializeImageList.

Adding the Three Featured Images to the Above Project Sample Code
If we would like to include the three 32-bit PNG with transparency images,
the on-side folder, “V” and “>”, to our project, pass an ImageList control to
the InitializeImageList method below and it will fill it with these three
images. For example, “InitializeImageList(Me.ImageLists)”. See the next tip,
Black Book Tip # 49, Embedding Images within Your Source Code, on page 628 to see how we can
easily embed our own images as text within our source code files.
'*********************************************************************************
' Method : InitializeImageList
' Purpose : Replace or append ">", "V", and On-Side Folder to a provided ImageList.
' :
' NOTE : If you want to append the images to an existing list, set the Replace
' : parameter to FALSE.
'*********************************************************************************
Private Sub InitializeImageList(ByRef imgList As ImageList, Optional ByVal Replace As Boolean = True).
If Replace Then 'if we are filling, not appending images...
imgList.Images.Clear() 'initialize image list.
imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
End If
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.
'--------
'Image 0 FolderSide.
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAA7SURBVDhPY6AKmJnG8J8UDNWGACDB/2eMicKjBgy0ASB1" &
"MAzVCgGkGACjoVohYNQAiAGkYqhWSgEDAwDTxNM7a88G5gAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as Me.myImages.Images(0).
'--------
'Image 1 FolderIsOpen 'V'.
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABUSURBVDhPYxgFVAYzZ878X15e/v/jx4//oUJgAOKDxBcs" &
"WIIijgFAipycnFAMgWmGiYMV4gLoiknSDAPImnx9fUnTDANk2YwOQIYQDLRRQA3AwAAAiXFatNps77AA" &
"AAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as Me.myImages.Images(1).
'--------
'Image 2 FolderCanOpen '>'.
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABVSURBVDhPYxjc4OPHj/+hTNLB9evX/x84cOA/2YacOXPm" &
"/4IFS8g35D8QUMUQkGaKDAFp2rRpE9iQu3fvkmYARS4AaaYoDCgOQIrTAQhQpHmoAAYGAFSwieHivX4c" &
"AAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as Me.myImages.Images(2).
End Sub

'*********************************************************************************
' Method : ConvertBase64ToImage (invoked by InitializeImageList)
' Purpose : Convert a Base64 String to an Image object.
'*********************************************************************************
Private Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function
 Page 628

Black Book Tip # 49


Embedding Images Within Your Source Code.
Have you ever wanted to send quick code to someone, but for them to use it they either had to first add a
slew of images to their resources, or they had to download all our source code to their computer that
includes those images, but maybe we do not want them to have direct access to these proprietary files?
From reading this tome, we might be aware that a form’s “.resx” resource file is actually a text file
(well... technically it is an XML file). Each of its image resource members refers to its assigned image in
one of two ways; by referencing the original image filepath within a local Resources folder where it will
be extracted during compilation, or if we imported it directly from a file, not our resources, then it
instead embeds the image internally as Base64 string data. Consider this ResX file segment entry:
<data name="btnClose.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAA6SURBVDhPYxgFBMF/KE0WAGmGYZIBsmaSDcGmGYYJAmya
0DFOgE0xLowBsCkihOEAmySxeBQwMDAAAGmYP8GTgAQzAAAAAElFTkSuQmCC
</value>
</data>

NOTE: For details on Base64 encoding, refer to a previous article, Send (SMTP) and Retrieve (POP3) Email with Ease
under VB.NET, to its section Translating Base64 Data Back to Its Original Format on page 404.
Here, we see that the image assigned to btnClose is reported as being encoded to a Base64 byte array.
The value assignment for the entry is not the usual relative filepath, but rather it is a raw Base64 string
rendering of the Image object’s binary data.

Converting Between an Image and a Base64 String


The thing that has often frustrated many programmers is first, how to convert an Image object to Base64
format, and second, how to convert the Base64 string back into an Image object. This is at the very heart
of how to embed images into our source code.
If we have had any experience with these object types, it is clear that a Base64 string can convert
directly to a byte array, but it cannot convert directly to a memory stream. Likewise, an image can
directly convert to a memory stream, but it cannot directly convert to a byte array. The initial reaction is
that there is no direct path between them. This is the point where some programmers simply give up.
Indeed, as a result, many have resorted to using a scaled-back solution that only involves images and
memory streams, which are interchangeable. A lot of web solutions I have seen involve importing the
System.Windows.Media.Imaging namespace and using its WriteableBitmap class, to save the image to a
memory stream, and its BitmapImage class, to read a memory stream to a bitmap. But this has me
confused, because conversion between streams and images is not at all complicated, nor does it require
the library code overhead of the Media.Imaging namespace, which is a waste if we are not using any
other part of it. For example, if we want to store our image as a PNG format image to a memory stream
and the image’s name was srcImage, all we would need to do is apply two lines of code:
Dim memStream As New IO.MemoryStream 'memory stream to receive image data.
srcImage.Save(memStream, Imaging.ImageFormat.Png) 'copy current image object to the memory stream as PNG.

Conversely, we can convert a memory stream back to an image object even more easily:
Dim srcImage As Image = Image.FromStream(memStream) 'construct image from stream data.

Clearly, there is no need for any of that additional code overhead.


Lots of frustration is also due to the document scarcity on this subject on MSDN’s website, or at least
the presence of sample code for us to inspect and learn from. If we look up some of the functions, the
documentation related to this process will usually leave us scratching our head. However, even my
modest examples shown above should help clear up many of the mysteries surrounding it.
 Page 629

But still, people want to convert an Image to a Base64 string, and also to convert it back to an Image. As
stated above, people can convert to and from images and memory streams, and they can convert to and
from Base64 strings and byte arrays, but I think the thing they have not thought through is how to
convert between byte arrays and memory streams, which is actually a very easy thing to do.
For example, to convert a Base64 string to a byte array, all we need to do is this:
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.

Conversely, to convert a byte array to a Base64 string, all we need to do is this:


Dim strImg As String = Convert.ToBase64String(bAry) 'construct a Base64 string from a Byte array.

To convert a byte array to a memory stream, all we need to do is this:


Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream

And to convert a memory stream to a byte array, all we need to do is this:


Dim bAry() As Byte = memStream.ToArray() 'convert the stream to a byte array.

So, to put this first part all together, a function to convert an Image to a Base64 string becomes:
'*********************************************************************************
' Method : ConvertImageToBase64
' Purpose : Convert a source Image object to a Base64 String.
'*********************************************************************************
Friend Function ConvertImageToBase64(ByRef srcImage As Image) As String
Dim memStream As New IO.MemoryStream 'memory stream to receive image data.
srcImage.Save(memStream, Imaging.ImageFormat.Png) 'copy current image to the memory stream.
Dim bAry() As Byte = memStream.ToArray() 'convert the stream to a byte array.
memStream.Close() 'done with the memory stream.
Return Convert.ToBase64String(bAry) 'construct a Base64 string.
End Function

And a function to convert a Base64 string to an Image becomes:


'*********************************************************************************
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object.
'*********************************************************************************
Friend Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

Formatting Base64 Strings for Embedding Within Source Code


When we get a string from the ConvertImageToBase64 function, it is just one long continuous line of
text. Granted, we could paste that long line into our code, wrap it in quotation marks, make it an
assignment to some string variable, and it would work, regardless that it looks a bit weird. However, it is
easy to format using another function, and we can even use the invocation to ConvertImageToBase64 as
its parameter. For convenience, the returned code will be assigned to a presumed string name strImg.
A new function, BreakUpBase64String, is designed to take a Base64 String and break it up into
multiple lines of 80 characters apiece. It does this by non-manipulatively parsing the string 80 characters
at a time, wrap each consecutive 80-character segment within quotation marks and then add a Carriage-
Return/Linefeed and continuation cue (“& vbCrLf &”, though if we are using VB.NET previous to
VB2010, we will need to instead append “ & vbCrLf & _”, to include the additional “ _” continuation
tag) until an 80-character block is not left, which will simply append the remainder as a final line. This
last line will just be wrapped within quotation marks. No need for continuations.
 Page 630

The BreakUpBase64String function is defined below:


'*********************************************************************************
' Method : BreakUpBase64String
' Purpose : Break up a Base64 string into a formatted multiline string.
'*********************************************************************************
Friend Function BreakUpBase64String(ByVal srcBase64 As String) As String
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Ary(1) As String 'init with 2 elements.
Ary(0) = Pad & "strImg =" 'init first element, indented 4 spaces.
Dim idX As Int32 = 1 'init array index.
Dim idY As Int32 = 0 'init string offset.
Do While srcBase64.Length > idY + 79 'break up into lines of 80 characters apiece.
Ary(idX) = Pad & """" &
srcBase64.Substring(idY, 80) & """ &" 'break up (change to """ & _" if pre-VB2010)
idX += 1 'bump index
ReDim Preserve Ary(idX) 'expand array (at least for final line).
idY += 80 'point to next group of bytes.
Loop
Ary(idX) = Pad & """" & srcBase64.Substring(idY) & """" 'add final line (no need for continuation).
Return Join(Ary, vbCrLf) 'make one string, split by CrLf.
End Function

With the above function at hand and provided with a Base64 string consisting of the following
long line (the definition of a slick little 32-bit 16x16 closed manila folder with transparency):
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLR
DcMgDETZqXOwRlZgFv/loxMwAQN4Ev6YgHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMwjdN+GJAM4EUKU0qD
oLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII=

BreakUpBase64String will massage the above string and return this:


strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6Y" &
"gHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMw" &
"jdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2" &
"qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII="

As we can see, the above code segment clearly assumes that we have something like “ Dim strImg As
String” declared somewhere above it (this will shortly be cleared up in usable example code).
Of course, as we are developing this code, we will also need to save the results. We could use Interactive
Debug and set a breakpoint on a line where we can save the data to the clipboard, append it to a buffer
that will save the results off to the clipboard once the run is completed, or else just print their results to
the Debug output window. For example:
Dim Img As Image = Me.PictureBox1.Image 'grab image.
Dim strBase64 As String = BreakUpBase64String(ConvertImageToBase64(Img)) 'cvt & fmt Base64.
Clipboard.SetText(strBase64) 'save result to Clipboard.
Debug.Print(Buffer.ToString) 'also save result to Debug Output.

We could also shorten the clipboard-saving code to just this:


Clipboard.SetText(BreakUpBase64String(ConvertImageToBase64(Me.PictureBox1.Image))) 'save result to Clipboard.

Here is an example of dumping the contents of an ImageList control named ImageList1:


Dim Buffer As New System.Text.StringBuilder 'define a buffer.
With Me.ImageList1 'with a sample loaded imagelist.
For Each Img As Image In .Images 'process each of its images.
Buffer.Append(BreakUpBase64String(ConvertImageToBase64(Img))) 'convert, format, and append output.
Next
End With
Clipboard.SetText(Buffer.ToString) 'save results to Clipboard.
Debug.Print(Buffer.ToString) 'also save results to Debug Output.

We could paste the clipboard text to notepad or directly into our source code where we need it.
Suppose in our target application we were going to save this to an ImageList control. The slick thing
here is that we do not really need to drop an ImageList control onto our form, but we can in fact add an
ImageList exactly like VB does it, which is wholly in-code.
 Page 631

In the heading of our form, before our Form Load event, add the following line:
Dim myImages As New ImageList 'create an imagelist in-code.

This example assumes that we want our ImageList to be named myImages, but name it as we please.
In our Form’s Load event, we should assign the image size our ImageList will store. If they are 16x16
pixels, as I am demonstrating in this Black Book entry, we would declare it like this:
Me.myImages.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list (default).

We would then want to fill it. With the previously listed ConvertBase64ToImage function located within
the form’s class body, add the following two other preparatory lines:
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.

We could then add the data we saved to the clipboard after that:
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6Y" &
"gHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMw" &
"jdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2" &
"qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII="

Next, we invoke the ConvertImageToBase64 function to convert our Base64 string into an Image:
Img = ConvertBase64ToImage(strImg) 'grab image from string data.

And finally we add the new image to our ImageList:


Me.myImages.Images.Add(Img) 'add this image to the ImageList.

Suppose we also added a few more converted images. Our new Form code might look like this:
Option Explicit On
Option Strict On

Public Class Form1

'*******************************************************************************
Dim myImages As New ImageList 'create an imagelist in-code.
'*******************************************************************************

'*******************************************************************************
'*******************************************************************************
' Method : Form1_Load
' Purpose : Prepare form
'*******************************************************************************
'*******************************************************************************
Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
Me.myImages.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.
'--------
'Image 0
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6Y" &
"gHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMw" &
"jdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2" &
"qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
Me.myImages.Images.Add(Img) 'add this image as Me.myImages.Images(0).
'--------
'Image 1
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADGSURBVDhPrZLNEYUgDITtyZ5owVpy8/AqoAILoBJuVhDZ" &
"8BfJMHh4O7MimeRLYNj+Kh5Uwt8UaGft+76/Q1AwCrEYY9lZlVJdTIMzxNht7+kQtMXVVpQA13V1SAME" &
"st0WBiQDgpPAJ6VGUJ0Ep8Bn6n6c4tQMKwDn+UNOSmoTvJNXACLJyQCs2kiqq3YIuZEByMUoUIVKR+U6" &
"qQGQ26UgUC9cAdodYFNX/a9jo4/jYO99BkhiGmf0LA6jGE+9AWbGY5mZmfkB5Q1Ux1PgyxIAAAAASUVO" &
"RK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
Me.myImages.Images.Add(Img) 'add this image as Me.myImages.Images(1).
'--------
'Image 2
 Page 632

'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADISURBVDhPrZI7DoMwEER9p9zJR0JyR5GDcADfwp2ruE4B" &
"vMUO/sUCJSMNWMvu20Gg/iZrHmvuEMKK4uOxGKhFzXsv59jW1zlsKh+Qnot0FNrh5Fb0G52lSwBr+9tG" &
"BnIA7A6gaLVQRe9XPFTaexEplmVJAC2AMv4XyxIjgHl+HoAUv2numJTYaJUBIqQ3UJj4cdk0TecryPYY" &
"Te4Ds4j4AJxzGeDCMKY3xed/UFwo3HGKL4B9tXxPClCvmN7iN+cA7Y4/w79JqQ1ta4bPalei7wAAAABJ" &
"RU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
Me.myImages.Images.Add(Img) 'add this image as Me.myImages.Images(2).
'--------
'Image 3
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADXSURBVDhPrZKxEcMgDEXZKTsxku/oXGQQD8AW7lzFdYqE" &
"J6EYC5sq/+5HHJEe39jhb8rp8Wm97/sH1b/HYsCLvW3bZF3brnUMJ2eFXPmUjo1+2NyL/hSbdAbI+fq0" &
"kYEoIBcAmzkKVfR+1YVT6UWkWJbFAFEA5/g3lkOSAOb5KffAz609gJQ4xXAANH6foAMQv97VNE36CAag" &
"tiaq1dYcRHwA67rSowA9QYeGgNJr8fkeBMAGtrWv3hb/B5D3WSrUejGn6s1w+5lLIzTvu31sw0gaBx4o" &
"hC/Omk/mglK4SgAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
Me.myImages.Images.Add(Img) 'add this image as Me.myImages.Images(3).
End Sub

'*******************************************************************************
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object
'*******************************************************************************
Friend Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) ' grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) ' convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) ' construct image from stream data.
memStream.Close() ' release stream resources.
Return Img
End Function
End Class

Were we to assign the above created images to anything, such as to a PictureBox or Button, we would
find out that the images display these four 32-bit alpha-blended 16x16-pixel graphics:

Well, maybe a bit smaller than this (these are blown up 400%).

ImageLists versus Resources


Resources are declared to be far superior to ImageList controls when it comes to storing images. In most
respects this might be true, but one thing that resources cannot do is add or even subtract images, which
is a great advantage of using an ImageList. Another thing about an ImageList is that if we load it with
images at design-time, when we save the source code, it saves all the images as an embedded Image
Strip; the entire collection is stored in the resource file as one continuous stream of Base64 string data,
instead of ,aintaining references to external image files, as the Resource handler will do..
But when we create our interface from scratch, which I often do, we will have to resort to storing the
images outside an image-container control, such as an ImageList, which may not yet exist (we could
embed them within our program as embedded resources, but that is often more trouble than it is worth,
because we might have to supply users with not only our form source code, but its ResX file as well).

Creating BuildImageListCode, an ImageList Initialization Source Code Generator


In the above form code, within its Load event, we initialized an ImageList named myImages with four
images. However, instead of adding a lot of code by hand to implement the Base64 definitions for each
image, it would be much more convenient to simply invoke an initialization method from the form’s
Load event and not have to worry any further. We can create a source code generator rather easily that
we could run from within a sample application with either a pre-loaded ImageList control or having all
the images we need initially stored in the application’s resources, and then create our initialization
method from that. Actually, this last part is what I do by creating a local ImageList, load it from resource
images, and then invoke my code generator to build the list of images from it.
 Page 633

Therefore, the first thing we need is an ImageList. If we drop this control on the form and pre-load it,
that is just fine and dandy, but I often place my build code right within my target application. When I do
not need it, I simply convert all that code into comments and do not have to worry about adding or
deleting a temporary ImageList control from my form in the meantime.
For example, during development I load my resources with whatever images I may need, create a local
ImageList, add the images to it in the order I require in-code, and then generate the initialization source
code by invoking BuildImageListCode. Keeping the resources intact and the code available during
application development, I can reactivate it any time I need to make a change to the image list. Once
development is complete, I can delete this commented code and remove the images from my resources.
Consider the following typical example of my code, where in my heading I define my local code-only
ImageList named myImages:
'*********************************************************************************
Private myImages As New ImageList
'*********************************************************************************

Next, in my Form’s Load event, I add the


following code, which details 23 images stored
in my resources, as shown here, which I then
load into my myImages ImageList, like so (note
that a couple left- and right-pointing black triangles are missing, which I had added later):
'-----------------------------------------------------------------------------
'Set up the image list
'-----------------------------------------------------------------------------
myImages.ImageSize = New Size(16, 16) 'set its image size to 16x16.
With myImages.Images
'if you add images with no transparency, you can specify a color to use for transparency after the resource image:
' .Add(My.Resources.FolderClosed, Color.Magenta) 'specify Color.Magenta to be used as transparency color for this image.
Try
.Add(My.Resources.FolderClosed) 'add images from resources.
.Add(My.Resources.ShortcutClosed) 'closed shortcut folder.
.Add(My.Resources.FolderOpened) 'open folder.
.Add(My.Resources.ShortcutOpened) 'opened shortcut folder.
.Add(My.Resources.drv_FIXD) 'fixed drive.
.Add(My.Resources.drv_CDRM) 'CD drive.
.Add(My.Resources.drv_Remv) 'removable drive.
.Add(My.Resources.drv_RAMD) 'RAM drive.
.Add(My.Resources.drv_Ntwk) 'network drive.
.Add(My.Resources.Desktop) 'my Desktop.
.Add(My.Resources.Documents) 'my Documents.
.Add(My.Resources.Downloads) 'my Downloads.
.Add(My.Resources.Music) 'my Music.
.Add(My.Resources.Pictures) 'my Pictures.
.Add(My.Resources.Videos) 'my Videos.
.Add(My.Resources.Recent) 'my Rcent folder.
.Add(My.Resources.FolderCanOpen) '> Folder Can Open.
.Add(My.Resources.FolderIsOpen) 'V Folder Is Open.
.Add(My.Resources.FolderSide) 'folder on side.
.Add(My.Resources.FolderSideShortcut) 'folder on side shortcut.
.Add(My.Resources.UpOneFolder) 'up one folder image.
.Add(My.Resources.ArrowLF) 'new black left triangle arrow.
.Add(My.Resources.ArrowRT) 'new black right triangle arrow.
Catch
End Try
End With

The next thing I do is invoke my BuildImageListCode method, providing it with a reference to my


ImageList, myImages:
BuildImageListCode(myImages) 'generate initialization source code.

The BuildImageListCode method is wrapped in a module named modBuildImageListCode. It also


wraps the ConvertImageToBase64 function and the BreakUpBase64String function.
It is listed below.
 Page 634

Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modBuildImageListCode Static Class Module
' Build Image List Source Code Constructor.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modBuildImageListCode

'*******************************************************************************
' Method : BuildImageListCode
' Purpose : Provided an existing filled Image list, this method will create code that can
' : load up an ImageList from scratch, filling it with the images you desire
' : without requiring the user to first load those images into their resources.
' :
' : For example, if you want the target ImageList to be named 'myImageList',
' : define that in your program heading. For example:
' : Private myImageList As New ImageList
' :
' : Next, fill it with the needed images, either in-code or manually, and then pass
' : it to this method, preferably within your Form's Load() event. For example:
' : BuildImageListCode(Me.myImageList)
' :
' : This method will save the fully-constructed VB source code it to the Clipboard.
' :
' : Next, convert the above BuildImageListCode() command line into a comment, plus
' : any other code that may have previously initialized the ImageList.
' :
' : Next, paste the Clipboard code into your form's code body. This will add a
' : Function named InitializeImageList() and a support function named
' : ConvertBase64ToImage(). Right below where you had disabled the invocation of
' : the BuildImageListCode() method, enter this command: InitializeImageList() by
' : passing it the name of your target ImageList. For example:
' : InitializeImageList(Me.myImageList)
' :
' : InitializeImageList() will define each image as a Base64 text string. By passing
' : that to the ConvertBase64ToImage() method, it will convert this string into an
' : Image object and return it, where it will be added to the target ImageList. It
' : will process all images originally defined in the initial, now-not-existing
' : imagelist (well, you can test this without deleting it, just do not reference it).
' :
' : Now, when you run your program, this invocation will create all desired images
' : and add them to myImageList, or whatever you chose to name it.
'*******************************************************************************
Friend Sub BuildImageListCode(ByRef srcImageList As ImageList)
With srcImageList.Images
Dim Ln As String = "'" & New String("*"c, 81) & vbCrLf 'build a header line.
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Buffer As New System.Text.StringBuilder
Dim NewSize As String = srcImageList.ImageSize.Width.ToString & ", " &
srcImageList.ImageSize.Height.ToString
Buffer.Append(vbCrLf & Ln &
"' Method : InitializeImageList" & vbCrLf &
"' Purpose : Imitialize a provided ImageList and fill it with locally-created images." & vbCrLf &
"' :" & vbCrLf &
"' NOTE : If you want to append the images to an existing list, set the Replace" & vbCrLf &
"' : parameter to FALSE." & vbCrLf &
Ln &
"Private Sub InitializeImageList(ByRef imgList As ImageList, Optional ByVal Replace As Boolean = True)" & vbCrLf &
Pad & "If Replace Then 'if we are filling, not appending images." & vbCrLf &
Pad & Pad & "imgList.Images.Clear() 'initialize image list." & vbCrLf &
Pad & Pad & "imgList.ImageSize = New Size(" & NewSize & ") 'define 16x16 pixel images in this list." & vbCrLf &
Pad & "End If" & vbCrLf &
Pad & "Dim strImg As String 'string to be assigned image data as Base64 text." & vbCrLf &
Pad & "Dim Img As Image 'image to receive data from the memory stream." & vbCrLf)
For idX As Int32 = 0 To .Count - 1 'process all images in the ImageList.
Buffer.Append(Pad & "'--------" & vbCrLf &
Pad & "'Image " & idX.ToString & vbCrLf &
Pad & "'--------" & vbCrLf)
Buffer.Append(BreakUpBase64String(ConvertImageToBase64(.Item(idX)))) 'break up and format Base64 string.
Buffer.Append(vbCrLf &
Pad & "Img = ConvertBase64ToImage(strImg) 'grab image from string data." & vbCrLf &
Pad & "imgList.Images.Add(Img) 'add this image as imgList.Images(" & idX.ToString & ")." & vbCrLf)
Next
Buffer.Append("End Sub" & vbCrLf & vbCrLf &
Ln &
"' Method : ConvertBase64ToImage" & vbCrLf &
"' Purpose : Convert a Base64 String to an Image object" & vbCrLf &
Ln &
"Private Function ConvertBase64ToImage(ByVal strImg As String) As Image" & vbCrLf &
Pad & "Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array." & vbCrLf &
Pad & "Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream." & vbCrLf &
Pad & "Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data." & vbCrLf &
Pad & "memStream.Close() 'release stream resources." & vbCrLf &
Pad & "Return Img 'return image." & vbCrLf &
"End Function" & vbCrLf)
Clipboard.SetText(Buffer.ToString, TextDataFormat.Text) 'save a copy of the buffer to the clipboard.
End With
End Sub

'*********************************************************************************
' Method : BreakUpBase64String
' Purpose : Break up a Base64 string into a formatted multiline string.
'*********************************************************************************
Friend Function BreakUpBase64String(ByVal srcBase64 As String) As String
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Ary(1) As String 'init with 2 elements.
Ary(0) = Pad & "strImg =" 'init first element.
 Page 635

Dim idX As Int32 = 1 'init array index.


Dim idY As Int32 = 0 'init string offset.
Do While srcBase64.Length > idY + 79 'breat up into lines of 80 characters apiece.
Ary(idX) = Pad & """" &
srcBase64.Substring(idY, 80) & """ &" 'break up a portion (change to """ & _" if pre-VB2010).
idX += 1 'bump index.
ReDim Preserve Ary(idX) 'expand array (at least for final line.
idY += 80 'point to next group of bytes.
Loop
Ary(idX) = Pad & """" & srcBase64.Substring(idY) & """" 'add final line (no need for continuation.
Return Join(Ary, vbCrLf) 'make one string, split by CrLf.
End Function

'*********************************************************************************
' Method : ConvertImageToBase64
' Purpose : Convert a source Image object to a Base64 String.
'*********************************************************************************
Friend Function ConvertImageToBase64(ByRef srcImage As Image) As String
Dim memStream As New IO.MemoryStream 'memory stream to receive image data.
srcImage.Save(memStream, Imaging.ImageFormat.Png) 'copy current image to the memory stream.
Dim bAry() As Byte = memStream.ToArray() 'convert the stream to a byte array.
memStream.Close() 'done with the memory stram.
Return Convert.ToBase64String(bAry) 'construct a Base64 string.
End Function

End Module

After this method executes, we find the complete initialization source code with a support function,
ConvertBase64ToImage, in the Clipboard. With my 23 images, the resulting clipboard-stored code is:
'*********************************************************************************
' Method : InitializeImageList
' Purpose : Imitialize a provided ImageList and fill it with locally-created images.
' :
' NOTE : If you want to append the images to an existing list, set the Replace
' : parameter to FALSE.
'*********************************************************************************
Private Sub InitializeImageList(ByRef imgList As ImageList, Optional ByVal Replace As Boolean = True)
If Replace Then 'if we are filling, not appending images.
imgList.Images.Clear() 'initialize image list.
imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
End If
imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.
'--------
'Image 0
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6Y" &
"gHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMw" &
"jdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2" &
"qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(0).
'--------
'Image 1
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAC9SURBVDhPrZLNEYQgDIXtaXuyBWrJzcNWQAUUQCXcrCDL" &
"4zcQWT34Zp4ZM74vgGyviieV9jN5+rD0eZ7PIQjMQi+EUN60SlSGaXKGKO/buDo0dbhaiyLAOdchDeBJ" &
"T7sxIBng99R4pDgIqivBLvBYum+nOA5DBeA4vvhG/f6mOwARdQCqdO3NAO/zmSlAlQJgonA+qwVgCNb6" &
"BzCcQa2Q7KVJFzbGsLU2A2RYKgHiMq+MMK56A6yMy7IyM/MPlpZwRe7GRGcAAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(1).
'--------
'Image 2
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADISURBVDhPrZI7DoMwEER9p9zJR0JyR5GDcADfwp2ruE4B" &
"vMUO/sUCJSMNWMvu20Gg/iZrHmvuEMKK4uOxGKhFzXsv59jW1zlsKh+Qnot0FNrh5Fb0G52lSwBr+9tG" &
"BnIA7A6gaLVQRe9XPFTaexEplmVJAC2AMv4XyxIjgHl+HoAUv2numJTYaJUBIqQ3UJj4cdk0TecryPYY" &
"Te4Ds4j4AJxzGeDCMKY3xed/UFwo3HGKL4B9tXxPClCvmN7iN+cA7Y4/w79JqQ1ta4bPalei7wAAAABJ" &
"RU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(2).
'--------
'Image 3
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADQSURBVDhPrZIxDoMwDEVzp94pR0LKxtCDcIDcgo2pzB0o" &
"z8TCDiFd+NJvkLGffyPCY8rptVmv67qh8rovBmpRW5ZFnktbW+dwqnxAWnbpKFyH1VfRn6JJp4Cc29t6" &
"BnIA8g6gmKNQRd9Peai09yJSTNOkgCgAH//GsiQJYBzfcg/83LoGkBKnGE7AnS4A4pe7Gobh+AsK4LTW" &
 Page 636

"mkQ25rKJD2CeZ3p8gr+AfbvG53twADdYTppra3wH0BPZGptqM2w/czdsRZ0tLZf3ImnsuKMQfsoaZf1g" &
"N8EXAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(3).
'--------
'Image 4
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhP5Y+xDcMgEEXZiQ3oKanpWIGWyiMg0bnIBEzA" &
"APRUVB6E6J9yjmNbltMmX3qSfdz7xuLHUkoZV7zWjuGFZVkuOS2bpmm01k6FLb33D1AClwpSSiPnTEMc" &
"7kV8oNZKYM85R6wFEOf5MWKMBJZ4GefeexKUUkNKSfB8/Q3At8BzCIEkFhhjDInW2ncBBwOAAhShQGtN" &
"ywDvXHCQt8EhbsAipFviPix8Lf5VhHgCWJYzPQzt+z0AAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(4).
'--------
'Image 5
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFFSURBVDhPhdE9jsIwEAXg3JSOK9Cm4ghIdBR7gpwgfeip" &
"UtGhbdLRef2N7QCC3X3SyOPxvDc/7j7her2m8/mcLpdLmqYpuUN9/huIt9st3e/3tCxL+GLjOP4vMs9z" &
"kIBAkL9TFkghQAhq+jskIUKcOb90UEQOh0PcoVIeECxVCsns+/0+hEIgd0LAXj4KrJVyojGI9X3/EMjm" &
"/usuBKODOrP2wyqI7na7/FaClfaKNsJqWaxhGIbo4O1LXQT9AHWJjaxqGaHET6ev8F8WqSoByYyQZEv0" &
"huS0PMbXyfF4LCICzagTU8H5/OYH/IqTbbfbiHeqcZp6M3EEZnlI4jqUKyYndsCRoAOPTm0iG8E44vwm" &
"yAjGHoho+bmqFs3J2p01UYJ2FgJABMxPWQXJm81m9cUVekalP1Dj648gqda+DmpqRtf9AP0hnw19P0q2" &
"AAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(5).
'--------
'Image 6
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrY/BDQMhDATpiZ5ogQ7oIW1QAQXcnxcvGsg/" &
"D4e1DmJxMSFRVhr58K3XYFby3tOKZgG6YKLHnYhuVGvlCo7DcW2WjQAxPIc0y+cA5xzTjmStHefeY6Mm" &
"GYDhnPPvAe9olldACIFm5ieoN4A5pXRh6wYY1nRuWIOAUgpvRI0x8vdcm5sr/GM71K/fQ3CWoCdB7xIg" &
"OX8M5D81QD5hltyuBnzLGBbqzV3+JWOejKK9y4VDarMAAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(6).
'--------
'Image 7
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACKSURBVDhP5Y/BDYAgEASp1lLogR8PK6ACCuBPOZi9uJeT" &
"KIJfN1kleDOC+1fa2eU0730LYWsx7ksSBVNK2hmJgjlnqRWMJAqWUh5hCjALRkiCtVaBWUqsiLMqwAIB" &
"PBL0IGZFgAcHUCu5+yO+cw2YkU2ehHd8AS8CRCWroI1cB+9V0OYz2GcCdO4AJDgrNEoV+vUAAAAASUVO" &
"RK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(7).
'--------
'Image 8
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFSSURBVDhPrZIxjoNADEU5RQ5AR01FS88Z0nEFWiqOgESX" &
"Yk/ACVJtNXVSUVFEQiutcgKH9zVOCNF2+6WvMWP7+4+T5F9hK+Z5tuv1Kno8TZMty0LaYukLJCg8n8/W" &
"tq3Vda34fr+rgTwCfIcQROqBBCj2KcQkvQFsBYgR6LrO+r5XPhnH0YZhME4X2Frn5J6apmmsLEsJ4JYe" &
"OXAREqfTlxpo9EKeRcyZ57lquKdHi0MAS9jzpDdXVaXJxJwQIb7lACCCbS5odCcUF0Uh28fj8emEHA7Z" &
"iwQAIoBF8QQmU4zlLMsU+462iO0vxPvnpMPhoOnbxlj6N8KQr/uIJF6Js5h+B5vck8bb7aJpnGFIPmog" &
"kED4MZGl6B+3Tvy9fK/poFMuYo0zTVM9MyHYk4kWn8A51MlHDeRX0dJYFmpOGvb0n3BP7QGRLVjYXpRv" &
"7t9h9gBC6HW76IZ0JgAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(8).
'--------
'Image 9
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABrSURBVDhPzZPBDcAgDAOzEzuxEzuxEyWQSG3lEos+2kgH" &
"j9j3QEJ+MLm2V+iRSgsRBCuAZYURwKITCWDJGPsdwWW/EkTlmXkQMOUJEFDlamwJvHwW6KRc+qOs0Yxn" &
"x+0Cih7Wwh37EJ+NyAFYvu7sFAcsUAAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(9).
'--------
'Image 10
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADRSURBVDhPlZHBDYQgFETpyQ4shjo8WYIJNw9bgXVwtwez" &
"F27eWB77EZYoYV8yQSEzf0SVWNeXr2WM+RF7x3F4EFuGwxrnnDz5aN73/QoRW6YO0NZ7+5aXQDISQhux" &
"ZcoAjIOx3zUEAefzPEcty/IcQG3MqPwEOM8zrrcB1ErmukEpoIXYMgQ8weQkaAaoMLkl6G5QTu5usG3b" &
"JQw6bDOYFcFtADcL9URjbRQXiqAZUDcYx9EPWnuNpOVfDfi1paC7wZ2gGdDDYwAHPZqmSQKU+gA5Z34b" &
"jO2S6AAAAABJRU5ErkJggg=="
 Page 637

Img = ConvertBase64ToImage(strImg) 'grab image from string data.


imgList.Images.Add(Img) 'add this image as imgList.Images(10).
'--------
'Image 11
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABfSURBVDhP7ZHLEQAREAUnJznJSU6bE2bXK4zxWScHXfUu" &
"6L4gDWMfb5xYPEvXcz7Ji93AwQHLj7V/VwLVosNujryTUm+FDNYjigzmkYEM+pEFGbSRHzLIkQ0ZsDiW" &
"iQJ9a0WWG+2+xgAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(11).
'--------
'Image 12
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACFSURBVDhPxZLNDYAgDIXZiZ3YyQUcxnQUDh65If0hNgEB" &
"68GXNPAS+rUU3FBbzhRmCWA/TiNEAB6sXQgAI5ggCmCDUCJkryDFvoAIALdUvXgvfk0KgOJOPgC4i78B" &
"0xnQIRmYBkBkT2tP9UAbd8XHZFR9phFgqD4AP9EiIKXU3D3ALNm5C0L52yY+Ei+PAAAAAElFTkSuQmCC" &
""
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(12).
'--------
'Image 13
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACvSURBVDhPxZOxDcMgEEXZyRWte5degTmYAuk6F1HmoI6u" &
"Z4kU8QQX/ikk4hwpil34SU9IBh7X2J3PslzkiBrYSxcI9J9gEyD+aC9YQRfg+3db8Hp7qO0bMBMEiTFK" &
"Skk3bMgKusAQgszzrCtigUhyznq4vQzXdVVBF/DeyziO6jRNNTSoNkrMwlVgJqBOHIbYxyWdoq6llN+B" &
"oAFWCdYLzRYCXWAP78ARX3/EaTj3BE/HY6gtq9MqAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(13).
'--------
'Image 14
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAD3SURBVDhPpVK5DcMwDNQU3sE1B3LHOVS5TsvORZoU2cED" &
"aCfmTo+R2EEkIAQOtCHe+WhdYMUYnZjneQhtPpNZfGHxUDT9BGdYFwHim4CKuoiUXgXafKV/OkjJ/Pm4" &
"uVrydV3dTDxZcFPBs/YdGIgkN4EYA0SDKwRSKgJtvtJPDtTwNQCWVbU4gABdiFnfAdgQUcfS2Llgmqbc" &
"A9B1MALWRYDg4fkWuErCGmWlAQdngQxcI8bwPPAPvgoAy7Lk3neA63uHZgErHe9dB/u++7bdjyzkAOUk" &
"otcV2nylfzpgkFqYmEgGyY8glZ/IuggQPBxBm6/0fyqEF35t4Jp72sWRAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(14).
'--------
'Image 15
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEdSURBVDhPjZK9kYMwEIXVkzugETKVQgnMKHPgCqiAApQT" &
"EZExl5CR7enb49nCdz77zbzR39uPFRDQtm22LIsNw2B937u7rrPr9WbzPBvnHvxLHOacPcwICFM4jqOD" &
"GF9CCFPMSAjRicCCqBMZOYAwwX3ffRMQ62marG1bh8sppft8XVfPB+5LEWKMMXoHFOtqEk9mDVyQAFXX" &
"qJ+s9gVgXhcypzY8349DDBQTktQBNXJgAzphFWsNWAD2WNdyAJtM1O4nJnsH0BYbLDh8JzL6Yg5AQPSC" &
"UCxdp/wz1ka/OpD0glAqXwDnLzsZ1V2+BDRNY5fyP/BPxPISMUBEhkL5KD8DLqVfrEL9H4gMWfkoPwP+" &
"kwBH2UMCfOIHIIRv0OL04WmkM8UAAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(15).
'--------
'Image 16
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABVSURBVDhPYxjc4OPHj/+hTNLB9evX/x84cOA/2YacOXPm" &
"/4IFS8g35D8QUMUQkGaKDAFp2rRpE9iQu3fvkmYARS4AaaYoDCgOQIrTAQhQpHmoAAYGAFSwieHivX4c" &
"AAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(16).
'--------
'Image 17
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABUSURBVDhPYxgFVAYzZ878X15e/v/jx4//oUJgAOKDxBcs" &
"WIIijgFAipycnFAMgWmGiYMV4gLoiknSDAPImnx9fUnTDANk2YwOQIYQDLRRQA3AwAAAiXFatNps77AA" &
"AAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(17).
'--------
'Image 18
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAA7SURBVDhPY6AKmJnG8J8UDNWGACDB/2eMicKjBgy0ASB1" &
 Page 638

"MAzVCgGkGACjoVohYNQAiAGkYqhWSgEDAwDTxNM7a88G5gAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(18).
'--------
'Image 19
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABqSURBVDhP7YxRCoAwDEN7J+/knbyTV9hZqumWbgw3quif" &
"D0IgbSKvsK2id1RqFYS6LyH9Ax8NIBiqL1PnzdERdusG6Lma8edWzMIDIKX0fIBlOjCPDLRlL9IjAxDL" &
"LcjxfCVrFuxxogkiB2Z21dLgH0jYAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(19).
'--------
'Image 20
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADBSURBVDhPtZLBDcMgDEXZqXNkDVZgFt9y6ARMwACehFM5" &
"90D5xjQkNE1atV/6QSb+TxZgfiamS+6dUsqQ/n4vBLbCXoxRq1Ea7cO0cYUMtmY9HTYlwDQ2HxigJ6Cu" &
"i6ZpKl+uRa/7TRYqk4QQFMBWAZjESrgZ9eDSB8A8X0dAH96FKICIXk9QCg22ejFz7fs3YH0OvVvfIWDP" &
"PUAOEXeJTTGoJ+2cy977bApOniwKjHTW6EdOXiMgmORTQwL4XsY8ADUZdcZU+bcCAAAAAElFTkSuQmCC" &
""
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(20).
'--------
'Image 21
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABKSURBVDhPpYxBCgAwCMP8/6c3ChNECrM2UHpKQuC8XwF5" &
"HUh5FaiyHOiyFGAyNoKJuS9MqhvBxNwYJmMSdgDYAWAHgB0AdoAQcQHymD/B+6ambAAAAABJRU5ErkJg" &
"gg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(21).
'--------
'Image 22
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABKSURBVDhPpYxBCgAgDMP2/08rBQdDKnZroPSUxIN1fgwC" &
"ViQD40gNjCJ3oB1hAUyGyTkJJtZ9YVJOgomYjCUDSwaWDCwZWHKDiA2SBz/Bns8W7AAAAABJRU5ErkJg" &
"gg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(22).
End Sub

'*********************************************************************************
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object.
'*********************************************************************************
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

I can then paste this generated code into my form class and convert to comments all the code working
with the imgList object, to include the invocation of the BuildImageListCode method. Below the last
line of this disabled code, I will insert the following line to execute the new code from within the form’s
Load event:
InitializeImageList(Me.myImages) 'initialize ImageList.

This code assumes that we have defined an ImageList control name myImages, either in-code or as a
control we had dropped on the form (but if we dropped it on the form, it would have been easier to
simply pre-load it with images at development time because it actually stores the image data as Base64
string data in a separate .resX resources file).
 Page 639

These 23 icons (0-22) are those that service my BrowserDialog form, actually featured in the next Black
Book Tip, which I have also referred to previously. It offers all the functionality of the default
FolderBrowserDialog control, plus, when it comes up, it will show the SelectedPath location if we had pre-
set it. The interesting thing about this form is that to create it, we just add a COM referencer to Shell32,DLL,
add a form named BrowserDialog, and then drop the source code onto the form’s code page. All images it
uses in its buttons, its TreeView, and its ComboBox are all defined in-code, and all controls, such as buttons
and including those controls just mentioned, are created in the form’s Load event.

Creating Single Base64 Image Data


We can create single-image Base64 code using a modified version of the BuildmageListCode method.
This new function, BuildImageCode, is designed to work with just a single image, not an ImageList as
the other versions were designed for. If we would like to take advantage of it, its module,
modBuildImageCode, follows:
Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modBuildImageCode Static Class Module
' Build a Single Image Source Code Constructor.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modBuildImageCode

'*******************************************************************************
' Method : BuildImageCode
' Purpose : Provided an existing Image, this method will create code that will create a
' : formatted Base64 string in the clipboard that can then be pasted into your app
' : without requiring the user to first load those images into their resources.
' :
' : Use BuildImageCode list this, asuming the image is loaded in your Resources:
' : BuildImageCode(My.Resources.MyImage)
' :
' : This method will save the fully-constructed VB source code it to the Clipboard.
' :
' : Next, convert the above BuildImageCode() command line into a comment.
' :
' : Next, paste the Clipboard code into your form's code body. This will add a
' : Function named InitializeImage() and a support function named
' : ConvertBase64ToImage(). Right below where you had disabled the invocation of
' : the BuildImageListCode() method, enter this command:
' : me.MyLocalImage As Image = InitializeImage()
' :
' : InitializeImage() will define the image as a Base64 text string. By passing
' : that to the ConvertBase64ToImage() method, it will convert this string into an
' : Image object and return it.
'*******************************************************************************
Friend Sub BuildImageCode(ByRef srcImage As Image)
Dim Ln As String = "'" & New String("*"c, 81) & vbCrLf 'build a header line.
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Buffer As New System.Text.StringBuilder
Dim NewSize As String = srcImage.Size.Width.ToString & ", " &
srcImage.Size.Height.ToString
Buffer.Append(vbCrLf & Ln &
"' Method : InitializeImage" & vbCrLf &
Ln &
"Private Function InitializeImage() As Image" & vbCrLf)
'-----------------------------------------------------------
'build data
'-----------------------------------------------------------
Buffer.Append(BreakUpBase64String(ConvertImageToBase64(srcImage)) & vbCrLf) 'break up and format Base64 string.
'---------------------------------------------------------------
'Finish out source code using common code.
'---------------------------------------------------------------
Buffer.Append(Pad & "Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array." & vbCrLf &
Pad & "Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream." & vbCrLf &
Pad & "Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data." & vbCrLf &
Pad & "memStream.Close() 'release stream resources." & vbCrLf &
Pad & "Return Img 'return image." & vbCrLf &
"End Function" & vbCrLf)
Clipboard.SetText(Buffer.ToString, TextDataFormat.Text) 'save a copy of the buffer to the clipboard.
End Sub

'*********************************************************************************
' Method : ConvertImageToBase64
' Purpose : Convert a source Image object to a Base64 String.
'*********************************************************************************
Friend Function ConvertImageToBase64(ByRef srcImage As Image) As String
Dim memStream As New IO.MemoryStream 'memory stream to receive image data.
srcImage.Save(memStream, Imaging.ImageFormat.Png) 'copy current image to the memory stream.
Dim bAry() As Byte = memStream.ToArray() 'convert the stream to a byte array.
memStream.Close() 'done with the memory stream.
Return Convert.ToBase64String(bAry) 'construct a Base64 string.
End Function

'*********************************************************************************
' Method : BreakUpBase64String
 Page 640

' Purpose : Break up a Base64 string into a formatted multiline string.


'*********************************************************************************
Friend Function BreakUpBase64String(ByVal srcBase64 As String) As String
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Ary(1) As String 'init with 2 elements.
Ary(0) = Pad & "Dim strImg As String =" 'init first element.
Dim idX As Int32 = 1 'init array index.
Dim idY As Int32 = 0 'init string offset.
Do While srcBase64.Length > idY + 79 'breat up into lines of 80 characters apiece.
Ary(idX) = Pad & """" &
srcBase64.Substring(idY, 80) & """ &" 'break up a portion (change to """ & _" if pre-VB2010).
idX += 1 'bump index.
ReDim Preserve Ary(idX) 'expand array (at least for final line.
idY += 80 'point to next group of bytes.
Loop
Ary(idX) = Pad & """" & srcBase64.Substring(idY) & """" 'add final line (no need for continuation.
Return Join(Ary, vbCrLf) 'make one string, split by CrLf.
End Function

End Module

To use it is easy. With the image we want to convert to a Base64 string available, pass it to the
BuildImageCode method like this: “BuildImageCode(My.Resources.MyImage)”, or this: “BuildImageCode(MyImage)” ,
or this: “BuildImageCode(Image.FromFile("C:\MyImage.png")) ”, or this “BuildImageCode(Me.PictureBox1.Image)”, or
this: “BuildImageCode(Me.ListBox1.Images(0))”, or however we have our image stored.
Once this method has executed, the code it built is saved to the clipboard. Just paste it to Notepad if we
want to examine it, or paste right into our code page. For example, consider this definition for a single
image; the 16x16-pixel closed translucent folder:
'*********************************************************************************
' Method : InitializeImage
'*********************************************************************************
Private Function InitializeImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6Y" &
"gHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMw" &
"jdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2" &
"qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII="
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

Creating Single Base64 Icon Data


Icons are stored differently than Images. As such, they cannot be stored in an ImageList without being
converted to Images, such as by using the icon’s ToBitmap method. However, sometimes it is useful to
embed an icon in-code. We can supply the BuildIconCode method with an icon resource and it will
create single image Base64 code using a mildly modified version of the above BuildmageCode method.
If we would like to take advantage of it, its module, modBuildIconCode, follows:
Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modBuildIconCode Static Class Module
' Build a Single Icon Source Code Constructor.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modBuildIconCode

'*******************************************************************************
' Method : BuildIconCode
' Purpose : Provided an existing Icon, this method will create code that will create a
' : formatted Base64 string in the clipboard that can then be pasted into your app
' : without requiring the user to first load those images into their resources.
' :
' : Use BuildIconCode list this, asuming the image is loaded in your Resources:
' : BuildIconCode(My.Resources.MyIcon)
' :
' : This method will save the fully-constructed VB source code it to the Clipboard.
' :
' : Next, convert the above BuildIconCode() command line into a comment.
' :
' : Next, paste the Clipboard code into your form's code body. This will add a
' : Function named InitializeIcon() and a support function named
' : ConvertBase64ToIcon(). Right below where you had disabled the invocation of
' : the BuildImageListCode() method, enter this command:
' : me.MyLocalImage As Image = InitializeImage()
' :
 Page 641

' : InitializeImage() will define the image as a Base64 text string. By passing
' : that to the ConvertBase64ToImage() method, it will convert this string into an
' : Image object and return it.
'*******************************************************************************
Friend Sub BuildIconCode(ByRef srcIcon As Icon)
Dim Ln As String = "'" & New String("*"c, 81) & vbCrLf 'build a header line.
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Buffer As New System.Text.StringBuilder
Dim NewSize As String = srcIcon.Size.Width.ToString & ", " &
srcIcon.Size.Height.ToString
Buffer.Append(vbCrLf & Ln &
"' Method : InitializeIcon" & vbCrLf &
Ln &
"Private Function InitializeIcon() As Icon" & vbCrLf)
'-----------------------------------------------------------
'build data
'-----------------------------------------------------------
Buffer.Append(BreakUpBase64String(ConvertIconToBase64(srcIcon)) & vbCrLf) 'break up and format Base64 string.
'---------------------------------------------------------------
'Finish out source code using common code
'---------------------------------------------------------------
Buffer.Append(Pad & "Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array." & vbCrLf &
Pad & "Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream." & vbCrLf &
Pad & "Dim Icn As new Icon(memStream) 'construct icon from stream data." & vbCrLf &
Pad & "memStream.Close() 'release stream resources." & vbCrLf &
Pad & "Return Icn 'return icon." & vbCrLf &
"End Function" & vbCrLf)
Clipboard.SetText(Buffer.ToString, TextDataFormat.Text) 'save a copy of the buffer to the clipboard.

End Sub

'*********************************************************************************
' Method : ConvertIconToBase64
' Purpose : Convert a source Icon object to a Base64 String
'*********************************************************************************
Friend Function ConvertIconToBase64(ByRef srcIcon As Icon) As String
Dim memStream As New IO.MemoryStream 'memory stream to receive image data.
srcIcon.Save(memStream) 'copy current icon to the memory stream.
Dim bAry() As Byte = memStream.ToArray() 'convert the stream to a byte array.
memStream.Close() 'done with the memory stram.
Return Convert.ToBase64String(bAry) 'construct a Base64 string.
End Function

'*********************************************************************************
' Method : BreakUpBase64String
' Purpose : Break up a Base64 string into a formatted multiline string.
'*********************************************************************************
Friend Function BreakUpBase64String(ByVal srcBase64 As String) As String
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Ary(1) As String 'init with 2 elements.
Ary(0) = Pad & "Dim strImg As String =" 'init first element.
Dim idX As Int32 = 1 'init array index.
Dim idY As Int32 = 0 'init string offset.
Do While srcBase64.Length > idY + 79 'breat up into lines of 80 characters apiece.
Ary(idX) = Pad & """" &
srcBase64.Substring(idY, 80) & """ &" 'break up a portion (change to """ & _" if pre-VB2010).
idX += 1 'bump index.
ReDim Preserve Ary(idX) 'expand array (at least for final line.
idY += 80 'point to next group of bytes.
Loop
Ary(idX) = Pad & """" & srcBase64.Substring(idY) & """" 'add final line (no need for continuation.
Return Join(Ary, vbCrLf) 'make one string, split by CrLf.
End Function

End Module

Consider the following clipboard output for my 256-color 32x32 Red Marble icon, which we can assign
to a variable using something like “Dim redMarbleIcon As Icon = InitializeIcon()”:
'*********************************************************************************
' Method : InitializeIcon
'*********************************************************************************
Private Function InitializeIcon() As Icon
Dim strImg As String =
"AAABAAEAICAQAAEABADoAgAAFgAAACgAAAAgAAAAQAAAAAEABAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAA" &
"AAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//" &
"AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEREREAAAAAAAAAAAAAARERERERERAAAAAAAAAAABER" &
"ERERERERAAAAAAAAABEREREREREREREAAAAAAAEREREREREREREREAAAAACZmREREREREREREREAAAAA" &
"mZmZkRERERERERERAAAACZmZmZmRERERERERERAAAJmZmZmZmZERERERERERAACZmZmZmZmZERERERER" &
"EQAAmZmZmZmZmZEREREREREACZmZmZmZmZmZkREREREREAmZmZmZmZmZmZkRERERERAJmZmZmZmZmZmZ" &
"kREREREQCZmZmZmZmZmZmZkREREREAmZmZmZmZmZmZmZkRERERAJmZmZmZmZmZmZmZEREREQCZmZmZmZ" &
"mZmZmZmZEREREAmZmfiZmZmZmZmZmRERERAAmZn/iZmZmZmZmZmREREAAJmZ/4mZmZmZmZmZkRERAACZ" &
"mf+JmZmZmZmZmZkREQAACZn/+JmZmZmZmZmZERAAAACZn/+ImZmZmZmZmREAAAAAmZn//4iJmZmZmZkR" &
"AAAAAAmZn///+JmZmZmZEAAAAAAAmZmf//+ZmZmZmQAAAAAAAACZmZmZmZmZmQAAAAAAAAAACZmZmZmZ" &
"mZAAAAAAAAAAAAAAmZmZmQAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8A///4AB//4AAH/8AAA/+AAAH/AA" &
"AA/gAAAHwAAAA8AAAAOAAAABgAAAAYAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAA" &
"AAGAAAABgAAAAcAAAAPAAAAD4AAAB/AAAA/4AAAf/AAAP/4AAH//gAH///AP/w=="
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Icn As New Icon(memStream) 'construct icon from stream data.
memStream.Close() 'release stream resources.
Return Icn 'return icon.
End Function
 Page 642

Black Book Tip # 50


Replace the BrowseFolderDialog Control With a Custom BrowserDialog Form.
I have already made references to my custom BrowserDialog form to replace
the default FolderBrowserDialog control.
The FolderBrowser dialog features all the functionality I wish that
FolderBrowserDialog had, such as easier logical and virtual drive
selection, navigation history browsing, an Up-One-Folder option, and it
displays and sets focus on a starting Selected Path, if we selected it.
For some reason FolderBrowserDialog does not highlight or show the
SelectedPath property’s node if we had assigned an initial path to it, but
rather it always sets focus to its RootFolder node, which is set by default to
our Documents folder, unless we had specified a different root from the
Environment.SpecialFolder enumeration. I have no problem with a
RootFolder having display focus if both or neither SelectedPath or
RootFolder parameters were specified by the user, but if the user sets only SelectedPath, then I believe it
should have focus instead.
This custom folder browser does a lot of interesting things. It supports all properties featured in the
FolderBrowserDialog control, though I did add a Title property so we can also change the form title if we
want to. It enumerates active logical drives, it adds virtual folders to places like Desktop, Pictures, Recent,
etc., it gives us access to both forward and backward browsing history, it performs owner drawing in a
Drives List ComboBox and a directory folder TreeView control. It also stores not text, but a custom class in
the ComboBox Items collection. It also creates all its controls from scratch, sets them up and positions them,
attaches events, and, as demonstrated in the previous Black Book entry, it generates all its own images.
To use the code, we will first need to make a COM reference to Shell32.DLL in our application and
ensure that its Embed Interop Types parameter is set to False.
To do this, go to our Project Properties, select References, select the “Add...” button and then the COM tab
on the Add Reference dialog, hit the M key to skip down to the M’s in the list, scroll to the “Microsoft Shell
Control and Automation”, click it to select it and place a check in its checkbox (if post-VB2010), and finally
click the OK button to add it. The IDE will create a non-COM version of the DLL named interop.shell32.dll.
It will also recreate it during any build if we ever delete it, as it is now in our references.
But we also need to ensure that our application does not embed interop types for this particular class.
Normally this is OK and is considered a best practice, but in this case, these controls need to use the
GUIDs (Global Unique Identifiers) as delivered by the Shell32 provider, not automatic GUIDs
generated for embedded objects. To do that, after we have added the COM reference to Shell32.Dll, we
need to click that entry to bring up its properties in the Properties Window. There, we need to set its
Embed Interop Types property to False from its default True state.
Next, just add a new windows form to our project and name it BrowserDialog. Once created, double-
click that form to quickly bring up its code page. Finally paste the code at the end of this Black Book
Entry to the code page, replacing all pre-existing code found in the form..
To use it, we would implement it just like we would a FolderBrowserDialog
control, to include ensuring to dispose of the object’s resources (except if we
are one to simply drop a FolderBrowserDialog onto our forms instead of
creating one in-code). Although the Garbage Collector, which runs constantly
in the background, as of VB2010, would clean this up for us if we forgot to tidy
up, I like to dispose of all resources I create whenever I can. For example, we
might normally bring up the default FolderBrowserDialog in-code like this:
 Page 643

With New FolderBrowserDialog


.Description = "Browse around for a new folder" 'set a description.
.ShowNewFolderButton = True 'allow user to create a new folder.
.SelectedPath = "C:\Windows" 'custom start path.
'.RootFolder = Environment.SpecialFolder.CommonDesktopDirectory 'allow setting general start path.
If .ShowDialog(Me) = DialogResult.OK Then 'launch dialog and check result. OK?
MsgBox("You selected:" & vbCrLf & .SelectedPath) 'if they hit OK, show the selected path.
Else
MsgBox("You hit Cancel") 'if user cancelled.
End If
.Dispose() 'dispose of FolderBrowserDialog resources.
End With

To use the new BrowserDialog form, we can make just one change – the control we reference, and we
have the additional option to change the form’s Title from “Browse For Folder” if we wish to:
With New BrowserDialog
.Title = "We can set the Form Title" '(NEW) optionally set a title.
.Description = "Browse around for a new folder" 'set a description.
.ShowNewFolderButton = True 'allow user to create a new folder.
.SelectedPath = "C:\Windows" 'custom start path. Set Focus here.
'.RootFolder=Environment.SpecialFolder.CommonDesktopDirectory 'allow setting general start path.
If .ShowDialog(Me) = DialogResult.OK Then 'launch dialog and check result. OK?
MsgBox("You selected:" & vbCrLf & .SelectedPath) 'if they hit OK, show the selected path.
Else
MsgBox("You hit Cancel") 'if user canceled.
End If
.Dispose() 'dispose of BrowserDialog resources.
End With

Following is the BrowserDialog form code. Notice that a module is appended to the end of the form class
named modBrowserDialogSupport, and after that is appended another class named DriveItem that
supports the logical and virtual Drive entries in the cboDrives ComboBox items list. We do not need to
break these out to separate files, but we can load them together in the same BrowserDialog Form file.
Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' BrowserDialog Form Class (NOTE: You may need to add 'Imports System' if VB2010 or previous).
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Public Class BrowserDialog
Inherits System.Windows.Forms.Form
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private _SelectedPath As String 'path set by user to start or result.
Private _StartPath As String 'path to actually focus on, depending on if a root folder was specified.
Private _Root As String 'root of start path.
Private _RootFolder As Environment.SpecialFolder 'root folder indicator.
Private _HistList As New List(Of String) 'keep track of list history.
Private _HistListIndex As Int32 = 0 'index into _HistList.
Private _Title As String = "Browse For Folder" 'default title for form.
'---------------------------------------------------------------------------------
Private lblDescription As New Label 'Description label.
Private cboDrives As New ComboBox 'Drive ComboBox.
Private tvFolderTree As New TreeView 'Directory Folder TreeView.
Private tsNavigation As New ToolStrip 'Navigation ToolStrip.
Private tsbNavPrev As ToolStripSplitButton 'Navigate Previous button for toolstrip.
Private tsbNavUp As ToolStripButton 'Navigate Up button for toolstrip.
Private tsbNavNext As ToolStripSplitButton 'Navigate Next button for toolstrip.
Private btnCancel As New Button 'Cancel button.
Private btnOK As New Button 'Accept button.
Private btnMakeNewFolder As New Button 'create new folder button.
Private btnMakeNewFolderVisible As Boolean = False 'flag indicating if new folder button should be visible.
Private ToolTips As New ToolTip 'tooltip control.
Private myImages As New ImageList 'ImageList control to store images in this form.
'---------------------------------------------------------------------------------
Private Const EdgeOfst As Int32 = 12 'edge offset from top and left for controls.

Private Declare Auto Function MessageBox Lib "user32" (ByVal hwnd As IntPtr,
ByVal lpMessage As String,
ByVal lpTitle As String,
ByVal wStyles As MsgBoxStyle) As MsgBoxResult
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
' Property : MeCursor
' Purpose : Set cursor to more than just the main form.
' Usage : When assigning a new cursor to the form, assign it using: MeCursor = NewCursor.
' : Note also that you can assign from the Cursors collection, or load a cursor from resources.
' : Unlike the UseWaitCursor property. You can afterward reassign child control cursors.
'*********************************************************************************
Private Property MeCursor As Cursor
Get
Return Me.Cursor 'return the form's current cursor setting.
 Page 644

End Get
'------
Set(value As Cursor)
If Me.Cursor <> value Then 'is the form cursor already set to this?
Me.Cursor = value 'no, so set cursor to the main form...
If Me.HasChildren Then 'and if it also has children...
SetCursorToChildren(Me.Controls, value) 'then set its child controls as well.
End If
End If
End Set
End Property
'*********************************************************************************
' Method : SetCursorToChildren
' Purpose : Support MeCursor property
' : Set parent's cursor also to Child controls
'*********************************************************************************
Private Sub SetCursorToChildren(ByRef ControlList As Control.ControlCollection, ByRef Value As Cursor)
For Each Cntrl As Control In ControlList 'process all child controls.
If Cntrl.Visible Then 'is the control visible?
Cntrl.Cursor = Value 'set its cursor.
If Cntrl.HasChildren Then 'does it have child controls?
SetCursorToChildren(Cntrl.Controls, Value) 'process its child controls.
End If
End If
Next
End Sub
'*********************************************************************************
' Property : Title
' Purpose : Get/Set Title for form.
'*********************************************************************************
Friend Property Title As String
Get
Return Me._Title
End Get
'----
Set(value As String)
Me._Title = value
Me.Text = value
End Set
End Property
'*********************************************************************************
' Property : Description
' Purpose : Get/Set Description for header label box.
'*********************************************************************************
Friend Property Description As String
Get
Return Me.lblDescription.Text
End Get
'----
Set(value As String)
If Me.lblDescription.AutoSize Then
Me.lblDescription.AutoSize = False
End If
Me.lblDescription.Text = value
End Set
End Property
'*********************************************************************************
' Property : ShowNewFolderButton
' Purpose : Get/Set ShowNewFolderButton.
'*********************************************************************************
Friend Property ShowNewFolderButton As Boolean
Get
Return btnMakeNewFolderVisible
End Get
'----
Set(value As Boolean)
btnMakeNewFolderVisible = value
Me.btnMakeNewFolder.Visible = value
End Set
End Property
'*********************************************************************************
' Property : SelectedPath
' Purpose : Get/Set SelectedPath.
'*********************************************************************************
Friend Property SelectedPath As String
Get
Return Me._SelectedPath
End Get
'----
Set(value As String)
If Not String.IsNullOrWhiteSpace(value) Then
If IO.Directory.Exists(value) Then
Me._SelectedPath = value
Return
End If
End If
Throw New Exception("Invalid SelectedPath value")
Return
End Set
End Property
'*********************************************************************************
' Property : RootFolder
' Purpose : Get/Set RootFolder.
'*********************************************************************************
Friend Property RootFolder As Environment.SpecialFolder
 Page 645

Get
If Me._RootFolder = Nothing Then
Return Environment.SpecialFolder.MyDocuments
Else
Return Me._RootFolder
End If
End Get
'----
Set(value As Environment.SpecialFolder)
Try
Me._RootFolder = value
Catch
Throw New Exception("Invalid RootFolder specification")
End Try
End Set
End Property
'*******************************************************************************
'*******************************************************************************
' Method : BrowserDialog_Load
' Purpose : Prepare form.
'*******************************************************************************
'*******************************************************************************
Private Sub BrowserDialog_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim sz As New Size(380, 390) 'default startup size.
Me.Size = sz 'initially match size with minimum size.
Me.MinimumSize = sz
Me.Text = Me._Title 'set title.
Me.StartPosition = FormStartPosition.CenterParent 'center on parent.
'-----------------------------------------------------------------------
Me._StartPath = GetCustomSetting("FolderBrowserPath",
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)).ToString
With Screen.PrimaryScreen.WorkingArea 'compare any save parameters against the current screen bounds definition.
'get saved/default windows positioning for top, left, width, and height.
Dim Tp As Int32 = CInt(GetCustomSetting("FolderBrowserTop", Me.Top.ToString))
Dim Lf As Int32 = CInt(GetCustomSetting("FolderBrowserLeft", Me.Left.ToString))
Dim Wd As Int32 = CInt(GetCustomSetting("FolderBrowserWidth", Me.Width.ToString))
Dim Ht As Int32 = CInt(GetCustomSetting("FolderBrowserHeight", Me.Height.ToString))
Dim SystemDir As String = Environment.SystemDirectory
'-----------------------------------------------------------------------
'make sure that these settings are within the work area of the current screen,
'and adjust them if they are not...
'-----------------------------------------------------------------------
If Tp < .Top Then 'keep frame within client rectangle.
Tp = .Top 'if top out of PrimaryScreen bounds, set to its top.
End If
If Lf < .Left Then
Lf = .Left 'if left out of PrimaryScreen bounds, set to its left.
End If
If Wd > .Width Then
Wd = .Width 'if width out of PrimaryScreen bounds, set to its width.
Lf = .Left
End If
If Ht > .Height Then
Ht = .Height 'if height out of PrimaryScreen bounds, set to its height.
Tp = .Top
End If
If Lf + Wd > .Left + .Width Then 'if left + width goes out of PrimaryScreen bounds...
Lf = .Left 'just set left to its left, because width is in bounds.
End If
If Tp + Ht > .Top + .Height Then 'if top + height is out of PrimaryScreen bounds...
Tp = .Top 'just set top to its top, because height is in bounds.
End If
'-----------------------------------------------------------------------
'apply valid location and sizing settings to the main form.
'-----------------------------------------------------------------------
Me.Top = Tp
Me.Left = Lf
Me.Width = Wd
Me.Height = Ht
End With
''---------------------------------------------------------------------------
''Set up the image list
''---------------------------------------------------------------------------
InitializeImageList(Me.myImages) 'initialize ImageList.
'---------------------------------------------------------------------------
'Set up Cancel button.
'---------------------------------------------------------------------------
With btnCancel
.Parent = Me 'establish this control's parent.
.Text = "Cancel"
sz = TextRenderer.MeasureText(.Text, .Font) 'get size of text.
sz.Width += 40 'width + standard 40 pixels for spacing.
sz.Height += 9 'height + 9 pixels for spacing.
.Size = sz 'set size of button.
.Left = Me.ClientSize.Width - .Width - EdgeOfst 'set to right bottom.
.Top = Me.ClientSize.Height - .Height - EdgeOfst
.Anchor = AnchorStyles.Bottom Or AnchorStyles.Right 'anchor there.
Me.ToolTips.SetToolTip(Me.btnCancel, "Reject any selections") 'set tool tip.
.DialogResult = DialogResult.Cancel 'auto-exit with Cancel.
Me.CancelButton = Me.btnCancel 'treat this as the form CANCEL button.
End With
'---------------------------------------------------------------------------
'Set up OK button
'---------------------------------------------------------------------------
With btnOK
.Parent = Me 'establish this control's parent.
.Text = "OK"
.Size = sz 'set size of button.
 Page 646

.Left = Me.btnCancel.Left - .Width - 6 'set to left of OK with a standard gap.


.Top = Me.btnCancel.Top 'match top to cancel button.
.Anchor = AnchorStyles.Bottom Or AnchorStyles.Right 'anchor to bottom right.
Me.ToolTips.SetToolTip(Me.btnOK, "Accept selection") 'set tool tip.
.DialogResult = DialogResult.OK 'auto-exit with OK.
Me.AcceptButton = btnOK 'treat this as the form ACCEPT button.
End With
'---------------------------------------------------------------------------
'Set up Make New Folder button.
'---------------------------------------------------------------------------
With btnMakeNewFolder
.Parent = Me 'establish this control's parent.
.Text = "&Make New Folder"
sz = TextRenderer.MeasureText(.Text, .Font)
.Width = sz.Width + 40 'width + standard 40 pixels for spacing.
.Height = sz.Height + 9 'height + 9 pixels for spacing.
.Left = EdgeOfst 'align combobox left location.
.Top = Me.btnCancel.Top 'match top to cancel button.
.Anchor = AnchorStyles.Bottom Or AnchorStyles.Left 'anchor to bottom left
Me.ToolTips.SetToolTip(Me.btnMakeNewFolder, "Add a new folder to the currently selected folder")
.Visible = btnMakeNewFolderVisible 'set its visibility.
AddHandler .Click, AddressOf btnMakeNewFolder_Click 'set up its CLICK event.
End With
'---------------------------------------------------------------------------
'Set up ComboBox to list Drives.
'---------------------------------------------------------------------------
With Me.cboDrives
.Parent = Me 'establish this control's parent.
.Left = EdgeOfst 'align combobox left location.
.Anchor = AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top 'set anchoring.
.DrawMode = DrawMode.OwnerDrawFixed 'enable drawing images into list.
.ItemHeight = 17 'allow for 16x16 pixel icons with a 1-pixel gap.
.DropDownStyle = ComboBoxStyle.DropDownList 'avoid user typing into CBO's text box.
Me.ToolTips.SetToolTip(Me.cboDrives, "Select a different virtual or logical drive")
'-----------------------------------------------------------------------
AddHandler .DrawItem, AddressOf cboDrives_DrawItem 'add event code triggers.
AddHandler .SelectedIndexChanged, AddressOf cboDrives_SelectedIndexChanged
AddHandler .DropDownClosed, AddressOf cboDrives_DropDownClosed
End With
'---------------------------------------------------------------------------
'Set up main description
'---------------------------------------------------------------------------
With Me.lblDescription
.Parent = Me 'define control's parent.
.BackColor = Color.Transparent 'remove any shading.
.Left = EdgeOfst 'init position.
.Top = EdgeOfst
.AutoSize = False 'allow label to flex (should be already set by Description).
.Width = Me.ClientRectangle.Width - EdgeOfst * 2
.Height = SizeMessage(Me.lblDescription.Text, .Font, .Width).Height + EdgeOfst 'size based off text (even if empty).
.Anchor = AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top
End With
'---------------------------------------------------------------------------
'Set up Folder TreeView.
'---------------------------------------------------------------------------
With Me.tvFolderTree
.Parent = Me 'define control's parent.
.Left = EdgeOfst 'align treeview horizontal position.
.Width = Me.lblDescription.Width 'match description width.
.Anchor = AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top Or AnchorStyles.Bottom
.ShowRootLines = True 'use so tabbing aligns.
.ShowPlusMinus = True 'use so user can click nodes to auto-expand and collapse.
.ShowNodeToolTips = True 'Allow node tooltips to display.
If Me.myImages.Images.Count <> 0 Then
.ShowLines = False 'no need.
.ImageList = myImages 'make sure ImageList1 is attached.
.ImageIndex = DriveImages.FolderSide 'default to folder on side.
.DrawMode = TreeViewDrawMode.OwnerDrawAll 'we want to draw everything.
Else
.ShowLines = True
.ImageList = Nothing
.DrawMode = TreeViewDrawMode.Normal 'we do not want to draw everything.
End If
AddHandler .AfterSelect, AddressOf tvFolderTree_AfterSelect 'add event code triggers.
AddHandler .BeforeExpand, AddressOf tvFolderTree_BeforeExpand
AddHandler .DrawNode, AddressOf tvFolderTree_DrawNode
End With
'---------------------------------------------------------------------------
'Set up Navigation panel.
'---------------------------------------------------------------------------
With Me.tsNavigation
.Parent = Me 'define control's parent.
.Dock = DockStyle.None 'do not dock it.
.BackColor = Color.Transparent 'remove any shading.
.GripStyle = ToolStripGripStyle.Hidden 'hide grip.
.LayoutStyle = ToolStripLayoutStyle.HorizontalStackWithOverflow 'set display style.
.CanOverflow = True
.Left = Me.tvFolderTree.Left + Me.tvFolderTree.Width - .Width 'position navigation toolbar above TV at right end.
.Top = Me.lblDescription.Top + Me.lblDescription.Height
.ShowItemToolTips = True 'allow nodes to show paths.
.Anchor = AnchorStyles.Top Or AnchorStyles.Right 'anchor to top right.
With .Items 'now build buttons.
'-------------------------------
'Add Navigate to Previous button.
'-------------------------------
tsbNavPrev = New ToolStripSplitButton("tsbNavPrev", Me.myImages.Images(DriveImages.ArrowLeft), Nothing, "tsbNavPrev")
With tsbNavPrev
.DisplayStyle = ToolStripItemDisplayStyle.Image 'image only.
.ToolTipText = "Browse backward through the current browsing history." & vbCrLf &
 Page 647

"When enabled you can browse sequentially with the" & vbCrLf &
"arrow button, Or select the dropdown to select the" & vbCrLf &
"history point you want to return to."
AddHandler .ButtonClick, AddressOf tsbNavPrev_ButtonClick 'add event code triggers.
AddHandler .DropDownOpening, AddressOf tsbNavPrev_DropDownOpening
AddHandler .DropDownItemClicked, AddressOf tsbNavPrev_DropDownItemClicked
End With
.Add(tsbNavPrev)
'-------------------------------
'Add Navigate UP button.
'-------------------------------
tsbNavUp = New ToolStripButton("tsbNavUp", Me.myImages.Images(DriveImages.UpOneFolder), AddressOf tsbNavUp_Click, "tsbNavUp")
With tsbNavUp
.DisplayStyle = ToolStripItemDisplayStyle.Image 'image only.
.ToolTipText = "Browse up one folder to the parent folder of where you are now"
End With
.Add(tsbNavUp)
'-------------------------------
'Add Navigate to next button.
'-------------------------------
tsbNavNext = New ToolStripSplitButton("tsbNavNext", Me.myImages.Images(DriveImages.ArrowRight), Nothing, "tsbNavNext")
With tsbNavNext
.DisplayStyle = ToolStripItemDisplayStyle.Image 'image only.
.ToolTipText = "Browse forward through the current browsing history." & vbCrLf &
"When enabled you can browse sequentially with the" & vbCrLf &
"arrow button, or select the dropdown to select the" & vbCrLf &
"history point you want to advance to."
AddHandler .ButtonClick, AddressOf tsbNavNext_ButtonClick 'add event code triggers.
AddHandler .DropDownOpening, AddressOf tsbNavNext_DropDownOpening
AddHandler .DropDownItemClicked, AddressOf tsbNavNext_DropDownItemClicked
End With
.Add(tsbNavNext)
End With
Me.cboDrives.Top = .Top + (.Height - Me.cboDrives.Height) \ 2 'adjust combobox vertical size.
Me.cboDrives.Width = .Left - Me.cboDrives.Left 'adjust combobox width to fill row with toolbar.
Me.tvFolderTree.Top = .Top + .Height 'adjust treeview vertical position.
Me.tvFolderTree.Height = Me.btnOK.Top - Me.tvFolderTree.Top - 8
End With
'---------------------------------------------------------------------------
'finally, figure out the target path, and the root.
'---------------------------------------------------------------------------
If Not String.IsNullOrWhiteSpace(Me._SelectedPath) Then 'if a selected path was supplied by user...
Me._StartPath = Me._SelectedPath.Trim 'set it as the start path (SP initialize to default).
End If
If Me._RootFolder <> Nothing Then 'if a root folder specified...
Me._StartPath = Environment.GetFolderPath(Me._RootFolder) 'then it takes presidence over any selected path.
End If
Me._Root = RemoveSlash(IO.Directory.GetDirectoryRoot(Me._StartPath)) 'get the Root from the path, less trailing backslash.
'---------------------------------------------------------------------------
'populate drive combobox and then select current drive.
'---------------------------------------------------------------------------
With Me.cboDrives
PopulatecboDrives() 'populate the cboDrives list...
For Each di As DriveItem In .Items 'then find the desired Root drive in the list.
If di.Drive.Equals(Me._Root) Then 'if we found it...
.SelectedItem = di 'select it.
Exit For 'and we are done checking the list.
End If
Next
End With
End Sub

'*******************************************************************************
' Method : BrowserDialog_FormClosing
' Purpose : closing form.
'*******************************************************************************
Private Sub BrowserDialog_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
Select Case e.CloseReason 'check for closing reason.
Case CloseReason.UserClosing, CloseReason.None 'DialogResult buttons issue CloseReason.None.
SaveCustomSetting("FolderBrowserTop", Me.Top.ToString) 'save form screen positions.
SaveCustomSetting("FolderBrowserLeft", Me.Left.ToString)
SaveCustomSetting("FolderBrowserWidth", Me.Width.ToString)
SaveCustomSetting("FolderBrowserHeight", Me.Height.ToString)
SaveCustomSetting("FolderBrowserPath", Me._SelectedPath) 'save the last selected path.
End Select
End Sub
'*******************************************************************************
' Method : Reset
' Purpose : Reset Data to defaults.
'*******************************************************************************
Friend Sub Reset()
Me.Description = "Folder Browser"
Me.btnMakeNewFolder.Visible = False
Me._RootFolder = Nothing
Me._SelectedPath = Nothing
Me._StartPath = GetCustomSetting("FolderBrowserPath", Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)).ToString
End Sub
'*******************************************************************************
' Method : tvFolderTree_AfterSelect
' Purpose : React to a node is selected.
'*******************************************************************************
Private Sub tvFolderTree_AfterSelect(sender As Object, e As TreeViewEventArgs)
Me._SelectedPath = e.Node.ToolTipText 'set the selected path.
Me.AddtoList(Me._SelectedPath) 'try to add the path to the history list.
Me.tsbNavUp.Enabled = e.Node.Parent IsNot Nothing 'enable UpOnefolder button as needed.
End Sub
'*******************************************************************************
 Page 648

' Method : tvFolderTree_BeforeExpand


' Purpose : React to a node expanding.
'*******************************************************************************
Private Sub tvFolderTree_BeforeExpand(sender As Object, e As TreeViewCancelEventArgs)
With e.Node
If .Checked Then 'has it been processed yet?
.Checked = False 'mark this folder as processed.
'-------------------------------------------------------------------
MeCursor = Cursors.WaitCursor 'show that we are busy.
DirRecurse(e.Node) 'parse any of its subfolders.
MeCursor = Cursors.Default 'no longer busy.
End If 'this will re-invoke this method, but...
End With
End Sub

'*********************************************************************************
' Method : tvFolderTree_DrawNode
' Purpose : Draw node with indent and state.
'*********************************************************************************
Private Sub tvFolderTree_DrawNode(sender As Object, e As DrawTreeNodeEventArgs)
Dim tv As TreeView = DirectCast(sender, TreeView) 'get reference to TreeView container.
Dim nCount As Int32 = 0 'init indent counter to 0.
Dim nd As TreeNode = e.Node 'start with current node provided to us.
Do While nd.Parent IsNot Nothing 'while the node as a parent.
nCount += 1 'count a generation (indent index).
nd = nd.Parent 'point back to its parent node...
Loop 'and try again.
Dim pt As New Point(e.Bounds.X + nCount * tv.Indent, e.Bounds.Y) 'compute Top-left coordinate for drawing.
Dim Rect As New Rectangle(New Point(pt.X + 16 + 16 + 4, pt.Y),
TextRenderer.MeasureText(e.Node.Text, tv.Font)) 'compute bounds of just text area...
Dim Brush As SolidBrush = New SolidBrush(tv.BackColor) 'brush used for selection and text coloring.
If e.Node.IsSelected Then 'if the node is selected...
Brush.Color = SystemColors.Highlight 'change background to highlight color.
e.Graphics.FillRectangle(Brush, e.Bounds) '(we could use Rect here to minimize highlight).
Brush.Color = tv.BackColor 'change the brush's color to white.
Else
e.Graphics.FillRectangle(Brush, Rect) 'clear JUST text area background...
Brush.Color = tv.ForeColor 'we will draw the text normally.
End If

If e.Node.IsExpanded Then 'if the folder is expanded....


e.Graphics.DrawImage(Me.myImages.Images(DriveImages.FolderIsOpen), pt) 'draw "V" in front of on-side folder.
ElseIf e.Node.Nodes.Count <> 0 Then 'not expanded, but does it have sub-folders?
e.Graphics.DrawImage(Me.myImages.Images(DriveImages.FolderCanOpen), pt) 'yes, so draw ">" in front of on-side folder.
End If
pt.X += 16 'point to on-side folder position.
e.Graphics.DrawImage(Me.myImages.Images(e.Node.ImageIndex), pt) 'draw the on-side folder or on-side shortcut.
e.Graphics.DrawString(e.Node.Text, tv.Font, Brush, Rect.Location) 'draw text.
Brush.Dispose() 'release brush resources.
End Sub

'*******************************************************************************
' Method : DirRecurse
' Purpose : Fill provided TreeView with folders and files as needed.
'*******************************************************************************
Private Sub DirRecurse(ByRef parentNode As TreeNode, Optional ByVal skipDeepSeek As Boolean = False)
parentNode.Nodes.Clear() 'Clear all child nodes in case we are repopulating.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'first check for link files in the current folder that reference folders
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Dim Files() As String = Nothing 'init local link file storage.
Try
Files = IO.Directory.GetFiles(parentNode.ToolTipText, "*.lnk") 'get a list of all .lnk files.
Catch
End Try
Dim lnkFolders As New List(Of String) 'init lnk folder list.
If Files IsNot Nothing AndAlso Files.Count <> 0 Then 'do we have lnk files?
For Each lnkFile As String In Files 'yes, so check them for folders.
Try 'trap in case it no longer exists...
Dim lnkPath As String = GetShortcutLinkToPath(lnkFile) 'get lnk's path to its target.
If lnkPath IsNot Nothing Then 'if a path was returned (likely).
Dim Attr As FileAttribute = GetAttr(lnkPath) 'get its attribute.
If (Attr And FileAttribute.Directory) =
FileAttribute.Directory Then 'if it is a folder...
If (Attr And (FileAttribute.Hidden Or
FileAttribute.System Or
FileAttribute.Volume)) = 0 AndAlso
IO.Directory.Exists(lnkPath) Then 'and if special attributes are not assigned...
Dim FndIf0 As Int32 = lnkPath.Length 'see if it already exists in our list.
For Each Path As String In lnkFolders 'process each entry in the current folder list.
If Path.Length.CompareTo(FndIf0) = 0 AndAlso
Path.CompareTo(lnkPath) = 0 Then 'did we find it (matches length and name)?
FndIf0 = 0 'yes, so flag it.
Exit For 'no need to keep looking if we found it (duh).
End If
Next
If FndIf0 <> 0 Then 'if we did not find it...
lnkFolders.Add(lnkPath) 'then add it to our local list if a folder.
End If
End If
End If
End If
Catch
End Try
Next
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'check sub-folders.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Page 649

Dim dirs() As String = Nothing 'init local directory storage.


Try
dirs = IO.Directory.GetDirectories(AddSlash(parentNode.ToolTipText)) 'get a list of any subfolders, skip if protected.
Catch
End Try
Dim Bol As Boolean = dirs IsNot Nothing AndAlso dirs.Count <> 0 'True if directories exist.
If Bol OrElse lnkFolders.Count <> 0 Then 'if sub-directories or lnk folders exist...
If skipDeepSeek Then 'if Referencing only...
parentNode.Checked = True 'mark this folder as being unprocessed.
parentNode.Nodes.Add("*") 'add a faux child node to add "+" branch connector.
lnkFolders.Clear()
Return 'nothing else to do.
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If Bol Then
For Each dirPath As String In dirs 'else parse each subfolder.
If (GetAttr(dirPath) And (FileAttribute.Hidden Or
FileAttribute.System Or
FileAttribute.Volume)) = 0 Then 'if not special folder...
Dim dirNode As TreeNode =
parentNode.Nodes.Add(parentNode.GetNodeCount(False).ToString,
IO.Path.GetFileName(dirPath),
DriveImages.FolderSide,
DriveImages.FolderSide) 'add new dir node w/closed folder.
dirNode.ToolTipText = dirPath 'save its folder path as its tooltip.
dirNode.Tag = False 'tag as not a shortcut.
DirRecurse(dirNode, True) 'parse any of its subfolders.
End If
Next
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If lnkFolders.Count <> 0 Then 'if link folder exist...
For Each dirPath As String In lnkFolders 'parse each lnk path.
Dim dirNode As TreeNode =
parentNode.Nodes.Add(parentNode.GetNodeCount(False).ToString,
IO.Path.GetFileName(dirPath),
DriveImages.FolderSideShortcut,
DriveImages.FolderSideShortcut) 'add as new dir node w/closed folder.
dirNode.ToolTipText = dirPath 'save its folder path as its tooltip.
dirNode.Tag = True 'tag as a shortcut.
DirRecurse(dirNode, True) 'parse any of its subfolders.
Next
End If
End If
End Sub

'*********************************************************************************
' Method : FindNodePath
' Purpose : Find the node path in a treeview.
'*********************************************************************************
Private Function FindNodePath(ByVal SeekPath As String) As TreeNode
Dim RootNode As TreeNode = Me.tvFolderTree.Nodes(0) 'start at the TreeView's base node.
If SeekPath = RootNode.ToolTipText Then 'if we already have a match...
Return RootNode 'then simply return the node.
End If
SeekPath = AddSlash(SeekPath) 'add a terminating "\" if one not there.
Dim Idx As Int32 = -1 'init backslash index.
If SeekPath.Contains(AddSlash(RootNode.ToolTipText)) Then 'if the seek path contain the root node...
Idx = RootNode.ToolTipText.Length 'then start index beyond it.
End If
'---------------------------------------------------------------------------
Do While Idx <> SeekPath.LastIndexOf("\"c) 'while there is data to find.
Idx = SeekPath.IndexOf("\"c, Idx + 1) 'find the next backslash.
Dim BasePath As String = SeekPath.Substring(0, Idx) 'grab the path left of it.
RootNode = FindNodePath(RootNode, BasePath) 'find the path to it via an overload.
If RootNode Is Nothing OrElse Idx = SeekPath.LastIndexOf("\"c) Then 'if we did not find the node or we are at the target...
Exit Do 'then done
ElseIf Not RootNode.IsExpanded Then 'othewise, if tne node is not expanded...
RootNode.Expand() 'then make sure it is expanded (and populated).
End If
Loop
Return RootNode 'finally, return the target node.
End Function

'*********************************************************************************
' Method : FindNodePath (overload)
' Purpose : Find the node path in a treeview.
'*********************************************************************************
Private Function FindNodePath(ByRef Node As TreeNode, ByVal SeekPath As String) As TreeNode
If Node.ToolTipText = SeekPath Then 'if current node contains the sought path...
Return Node 'return a reference to that node.
End If
If Node.Nodes.Count <> 0 Then 'else if it has children...
For Each subNode As TreeNode In Node.Nodes 'check each child.
Dim nd As TreeNode = FindNodePath(subNode, SeekPath) 'check its node and subnodes...
If nd IsNot Nothing Then 'did it find a match?
Return nd 'yes, so return it.
End If
Next
End If
Return Nothing 'nothing found.
End Function

'*********************************************************************************
' Method : btnMakeNewFolder_Click
' Purpose : Add a new folder from the current node.
'*********************************************************************************
Private Sub btnMakeNewFolder_Click(sender As Object, e As EventArgs)
Dim newPath As String = Nothing 'added path.
 Page 650

'---------------------------------------------------------------------------
Dim selNode As TreeNode = Me.tvFolderTree.SelectedNode 'get the treeview node selected.
If selNode Is Nothing Then 'if nothing is selected...
Return 'simply leave.
End If
Dim folderPath As String = selNode.ToolTipText 'grab the node's file path.
Dim newSubfolder As String
'---------------------------------------------------------------------------
Do
newSubfolder = InputBox("The selected folder path Is" & vbCrLf &
folderPath & vbCrLf & vbCrLf &
"Enter the name for a New sub-folder:",
"New Email Storage Folder") 'prompt for folder.
If String.IsNullOrWhiteSpace(newSubfolder) Then 'if the user canceled...
Me.tvFolderTree.Focus() 'set focus to treeview.
Exit Sub 'then leave.
Else
If folderPath.Last = "\"c Then 'folder path already backslash terminated?
newPath = folderPath & Trim(newSubfolder) 'yes, so just apply new folder.
Else
newPath = folderPath & "\" & Trim(newSubfolder) 'else apply new folder path.
End If
If IO.File.Exists(newPath) OrElse IO.Directory.Exists(newPath) Then 'if it already exists as file or folder...
If MessageBox(Me.Handle, "'" & newSubfolder & "' already exists!",
"Path Already Exists",
MsgBoxStyle.RetryCancel Or MsgBoxStyle.Exclamation) = DialogResult.Cancel Then 'if user cancels...
Exit Sub 'then simply leave.
End If
Else
Try
IO.Directory.CreateDirectory(newPath) 'else try to create the folder...
Catch
If MessageBox(Me.Handle, "'" & newSubfolder &
"' cannot be created! It may be invalid",
"Folder Creation Error",
MsgBoxStyle.RetryCancel Or MsgBoxStyle.Exclamation) = DialogResult.Cancel Then 'if error and user cancels...
Exit Sub 'then simply leave.
End If
Continue Do 'else try again
End Try
Exit Do
End If
End If
Loop
'---------------------------------------------------------------------------
selNode = selNode.Nodes.Add(selNode.Nodes.Count.ToString,
newSubfolder,
DriveImages.FolderClosed, DriveImages.FolderClosed) 'add the new node.
selNode.ToolTipText = newPath 'set its path.
'---------------------------------------------------------------------------
If Not selNode.IsVisible Then 'if we cannot see the node...
selNode.EnsureVisible() 'make sure the new node is seen.
End If
Me.tvFolderTree.SelectedNode = selNode 'make sure the new node is selected.
Me.tvFolderTree.Focus() 'set focus to treeview.
End Sub

'*********************************************************************************
' Method : PopulatecboDrives
' Purpose : Populate the cboDrives list.
'*********************************************************************************
Private Sub PopulatecboDrives()
Me.cboDrives.Items.Clear() 'clear any current list.
Me.cboDrives.Items.Add(New DriveItem("Desktop")) 'Add Desktop item.
Me.cboDrives.Items.Add(New DriveItem("Documents")) 'Add Documents item.
Me.cboDrives.Items.Add(New DriveItem("Downloads")) 'Add Downloads item.
Me.cboDrives.Items.Add(New DriveItem("Music")) 'Add Music item.
Me.cboDrives.Items.Add(New DriveItem("Pictures")) 'Add Pictures item.
Me.cboDrives.Items.Add(New DriveItem("Videos")) 'Add Videos item.
Me.cboDrives.Items.Add(New DriveItem("RecentPlaces")) 'Add Recent Places item.
'Me.cboDrives.Items.Add(New DriveItem("ThisPC")) 'Add Videos item.
'---------------------------------------------------------------------------
Dim Drives() As String = Environment.GetLogicalDrives 'get list of drives.
For Each Drv As String In Drives 'now check each one of them out.
Dim di As New IO.DriveInfo(Drv) 'get the info for the drive.
If di.IsReady Then 'if it is ready...
Me.cboDrives.Items.Add(New DriveItem(di)) 'build and add a DriveItem class object to the cbo list.
End If
Next
End Sub

'*********************************************************************************
' Method : cboDrives_DrawItem
' Purpose : Draw an item in the cboDrives list.
'*********************************************************************************
Private Sub cboDrives_DrawItem(sender As Object, e As DrawItemEventArgs)
e.DrawBackground() 'clear the background.
If e.Index <> -1 Then 'ignore out of range index.
Dim cboBox As ComboBox = DirectCast(sender, ComboBox) 'get the listbox pointed to by this event.
Dim DriveItem As DriveItem = DirectCast(cboBox.Items(e.Index), DriveItem) 'get the Drive item from the list.
Dim X As Int32 = e.Bounds.X 'init drawing from left position.
Dim Y As Int32 = e.Bounds.Y
If Me.myImages.Images.Count <> 0 Then
Dim img As Image
If Not DriveItem.Type = IO.DriveType.Unknown AndAlso
Y >= cboBox.ItemHeight Then 'if drive info and also list dropped down...
img = Me.myImages.Images(DriveImages.FolderCanOpen) 'draw image for ">".
e.Graphics.DrawImage(img, X, Y)
X += img.Width 'move beyond image.
 Page 651

End If
img = Me.myImages.Images(DriveItem.ImageIndex) 'Get the image stored In the ImageList.
e.Graphics.DrawImage(img, X, Y) 'draw image at start of line.
X += img.Width + 4 'move beyond image with buffer.
End If
e.Graphics.DrawString(DriveItem.ToString, cboBox.Font,
Brushes.Black, CSng(X), CSng(Y)) 'draw the text after the bitmaps.
End If
End Sub

'*********************************************************************************
' Method : cboDrives_SelectedIndexChanged
' Purpose : A new choice was made in the cboDrives list.
'*********************************************************************************
Private Sub cboDrives_SelectedIndexChanged(sender As Object, e As EventArgs)
With DirectCast(sender, ComboBox)
If .SelectedIndex <> -1 Then 'if the index is OK...
Me._Root = DirectCast(.SelectedItem, DriveItem).Drive 'grab the root path from the list object.
If Me._Root & "\" <> IO.Directory.GetDirectoryRoot(Me._StartPath) Then 'if it does not match the last start path...
Me._StartPath = Me._Root 'then ignore the start path and begin anew.
End If
'-----------------------------------------------------------------------
MeCursor = Cursors.WaitCursor 'we are busy.
Me.tvFolderTree.SuspendLayout() 'avoid flicker.
Me.tvFolderTree.Nodes.Clear()
Dim RootNode As TreeNode =
Me.tvFolderTree.Nodes.Add(tvFolderTree.Nodes.Count.ToString, Me._Root, 0, 0) 'create Root Node w/closed folder.
RootNode.ToolTipText = Me._Root 'save its path in its tooltip.
RootNode.Tag = False 'tag it as not a shortcut.
DirRecurse(RootNode) 'parse any of its subfolders.
'-----------------------------------------------------------------------
If CBool(RootNode.Nodes.Count) Then 'if we have children...
RootNode.Expand() 'make sure root node is expanded.
End If
'-----------------------------------------------------------------------
RootNode = FindNodePath(Me._StartPath) 'open directory to the node.
If RootNode IsNot Nothing Then 'if we found it (very likely).
Me.AddtoList(RootNode.ToolTipText) 'add item to list.
tvFolderTree.SelectedNode = RootNode 'select this node.
RootNode.EnsureVisible() 'make sure it can be seen.
Me.tsbNavUp.Enabled = RootNode.Parent IsNot Nothing 'enable UpOnefolder button as needed.
End If
Me.tvFolderTree.ResumeLayout() 'allow pending updates to process.
MeCursor = Cursors.Default 'show that we are no longer busy.
End If
Me.tvFolderTree.Focus() 'set focus to TreeView.
End With
End Sub

'*********************************************************************************
' Method : cboDrives_DropDownClosed
' Purpose : Set focus back to treeview when the cboDrives list closes.
'*********************************************************************************
Private Sub cboDrives_DropDownClosed(sender As Object, e As EventArgs)
Me.tvFolderTree.Focus()
End Sub

'*********************************************************************************
' Method : AddtoList
' Purpose : Add an item to the Browser History list.
'*********************************************************************************
Private Sub AddtoList(ByVal Path As String)
With Me._HistList
If .Count <> 0 Then 'if the history contains items.
If .Item(Me._HistListIndex) = Path Then 'does it match the current path?
Return 'yes, so ignore it (already set).
End If
Do While .Count - 1 <> Me._HistListIndex 'otherwise, remove history following.
.RemoveAt(Me._HistListIndex + 1)
Loop
End If
.Add(Path) 'add a new path to the list.
Me._HistListIndex = .Count - 1 'point to it.
Me.tsbNavNext.Enabled = False 'cannot yet navigate forward.
Me.tsbNavPrev.Enabled = Me._HistListIndex > 0 'see if we can navigate backward.
End With
End Sub

'*********************************************************************************
' Method : tsbNavPrev_ButtonClick
' Purpose : Scan backward through the browser selection list.
'*********************************************************************************
Private Sub tsbNavPrev_ButtonClick(sender As Object, e As EventArgs)
Me._HistListIndex -= 1 'naveigate back one.
NavFwdBack_Support() 'process selection.
End Sub

'*********************************************************************************
' Method : tsbNavPrev_DropDownOpening
' Purpose :Populate Nav Prev dropdown list.
'*********************************************************************************
Private Sub tsbNavPrev_DropDownOpening(sender As Object, e As EventArgs)
With tsbNavPrev.DropDownItems
.Clear() 'clear current dropdown list.
For Idx As Int32 = Me._HistListIndex - 1 To 0 Step -1 'populate with previous items from current.
Dim Item As String = Me._HistList(Idx) 'grab an item.
Dim itm As ToolStripItem = .Add(IO.Path.GetFileName(Item), Me.myImages.Images(DriveImages.FolderSide))
If String.IsNullOrWhiteSpace(itm.Text) Then 'if it was a root drive...
Item &= "\" 'add a backslash to the root path.
 Page 652

itm.Text = Item 'reset the empty text.


End If
itm.ToolTipText = Item 'set the path to the tooltip.
itm.Tag = Idx 'save the history index.
Next
End With
End Sub

'*********************************************************************************
' Method : tsbNavPrev_DropDownItemClicked
' Purpose : Scan backward through the browser selection list.
'*********************************************************************************
Private Sub tsbNavPrev_DropDownItemClicked(sender As Object, e As ToolStripItemClickedEventArgs)
Me._HistListIndex = CInt(e.ClickedItem.Tag) 'set the history index.
NavFwdBack_Support() 'process the selection.
End Sub

'*********************************************************************************
' Method : tsbNavUp_Click
' Purpose : Go up one folder.
'*********************************************************************************
Private Sub tsbNavUp_Click(sender As Object, e As EventArgs)
Dim selNode As TreeNode = Me.tvFolderTree.SelectedNode 'get the treeview node selected.
If selNode Is Nothing OrElse selNode.Parent Is Nothing Then 'if nothing is selected...
Return 'simply leave.
End If
selNode = selNode.Parent 'grab parent node.
Me.tvFolderTree.SelectedNode = selNode 'select it.
selNode.EnsureVisible() 'ensure it is visible.
Me.tsbNavUp.Enabled = selNode.Parent IsNot Nothing 'enable UpOnefolder button as needed.
Me.tvFolderTree.Focus()
End Sub

'*********************************************************************************
' Method : tsbNavNext_ButtonClick
' Purpose : Scan forward through the browser selection list.
'*********************************************************************************
Private Sub tsbNavNext_ButtonClick(sender As Object, e As EventArgs)
Me._HistListIndex += 1 'navigate forward.
NavFwdBack_Support() 'process selection.
End Sub

'*********************************************************************************
' Method : tsbNavNext_DropDownOpening
' Purpose :Populate Nav Next dropdown list.
'*********************************************************************************
Private Sub tsbNavNext_DropDownOpening(sender As Object, e As EventArgs)
With tsbNavNext.DropDownItems
.Clear() 'clear current dropdown list.
For Idx As Int32 = Me._HistListIndex + 1 To Me._HistList.Count - 1 'populate with following items from current.
Dim Item As String = Me._HistList(Idx) 'grab an item.
Dim itm As ToolStripItem = .Add(IO.Path.GetFileName(Item), Me.myImages.Images(DriveImages.FolderSide))
If String.IsNullOrWhiteSpace(itm.Text) Then 'if it was a root drive...
Item &= "\" 'add a backslash to the root path.
itm.Text = Item 'reset the empty text.
End If
itm.ToolTipText = Item 'set the path to the tooltip.
itm.Tag = Idx 'save the history index.
Next
End With
End Sub

'*********************************************************************************
' Method : tsbNavNext_DropDownItemClicked
' Purpose : Scan forward through the browser selection list.
'*********************************************************************************
Private Sub tsbNavNext_DropDownItemClicked(sender As Object, e As ToolStripItemClickedEventArgs)
Me._HistListIndex = CInt(e.ClickedItem.Tag) 'set the history index.
NavFwdBack_Support() 'process the selection.
End Sub

'*********************************************************************************
' Method : NavFwdBack_Support
' Purpose : Support scanning forward and backward through the browser selection list.
'*********************************************************************************
Private Sub NavFwdBack_Support()
Me.tsbNavNext.Enabled = Me._HistListIndex < Me._HistList.Count - 1 'set enablement for FWD button.
Me.tsbNavPrev.Enabled = Me._HistListIndex > 0 'set enablement of BAK button.
'-------------------------------------------------------------------
Me._StartPath = Me._HistList(Me._HistListIndex) 'grab indexed item.
Dim Root As String = RemoveSlash(IO.Directory.GetDirectoryRoot(Me._StartPath)) 'get the root of the path.
With Me.cboDrives
For Each di As DriveItem In .Items 'next, find the desired Root drive in the list.
If di.Drive.Equals(Root) Then 'if we found it...
If Not .SelectedItem.Equals(di) Then 'if it does not match the currently selected item...
.SelectedItem = di 'then set it as the selection.
End If
Exit For 'and we are done checking the list.
End If
Next
Dim NewNode As TreeNode = FindNodePath(Me._StartPath) 'find and open path to nide in treeview.
If NewNode IsNot Nothing Then
Me.tvFolderTree.SelectedNode = NewNode 'select node.
Me.tsbNavUp.Enabled = NewNode.Parent IsNot Nothing 'enable UpOneFolder button as needed.
End If
End With
Me.tvFolderTree.Focus() 'set focus to treeview.
End Sub
 Page 653

'*********************************************************************************
' Method : GetCustomSetting
' Purpose : Get value from Custom Section of myEmail Registry entry.
'*********************************************************************************
Friend Function GetCustomSetting(ByVal Key As String, ByVal DefaultValue As String) As Object
Return GetSetting(My.Application.Info.Title, "Settings", Key, DefaultValue)
End Function

'*********************************************************************************
' Method : SaveCustomSetting
' Purpose : Save value to Custom Section of myEmail Registry entry.
'*********************************************************************************
Friend Sub SaveCustomSetting(ByVal Key As String, ByVal Value As String)
If Value Is Nothing Then 'if the Value is set to Nothing...
Dim Text As String = CStr(GetCustomSetting(Key, Nothing)) 'get the value from the key.
If Not String.IsNullOrWhiteSpace(Text) Then 'if the value is not already deleted...
Try
DeleteSetting(My.Application.Info.Title, "Settings", Key) 'then simply delete the key
Catch
End Try
End If
Else
SaveSetting(My.Application.Info.Title, "Settings", Key, Value) 'otherwise, Value contains data, so write it.
End If
End Sub

'*********************************************************************************
' Method : InitializeImageList
' Purpose : Imitialize a provided ImageList and fill it with locally-created images.
' :
' NOTE : If you want to append the images to an existing list, set the Replace
' : parameter to FALSE.
'*********************************************************************************
Private Sub InitializeImageList(ByRef imgList As ImageList, Optional ByVal Replace As Boolean = True)
If Replace Then 'if we are filling, not appending images.
imgList.Images.Clear() 'initialize image list.
imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
End If
imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.
'--------
'Image 0
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6Y" &
"gHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMw" &
"jdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2" &
"qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(0).
'--------
'Image 1
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAC9SURBVDhPrZLNEYQgDIXtaXuyBWrJzcNWQAUUQCXcrCDL" &
"4zcQWT34Zp4ZM74vgGyviieV9jN5+rD0eZ7PIQjMQi+EUN60SlSGaXKGKO/buDo0dbhaiyLAOdchDeBJ" &
"T7sxIBng99R4pDgIqivBLvBYum+nOA5DBeA4vvhG/f6mOwARdQCqdO3NAO/zmSlAlQJgonA+qwVgCNb6" &
"BzCcQa2Q7KVJFzbGsLU2A2RYKgHiMq+MMK56A6yMy7IyM/MPlpZwRe7GRGcAAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(1).
'--------
'Image 2
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADISURBVDhPrZI7DoMwEER9p9zJR0JyR5GDcADfwp2ruE4B" &
"vMUO/sUCJSMNWMvu20Gg/iZrHmvuEMKK4uOxGKhFzXsv59jW1zlsKh+Qnot0FNrh5Fb0G52lSwBr+9tG" &
"BnIA7A6gaLVQRe9XPFTaexEplmVJAC2AMv4XyxIjgHl+HoAUv2numJTYaJUBIqQ3UJj4cdk0TecryPYY" &
"Te4Ds4j4AJxzGeDCMKY3xed/UFwo3HGKL4B9tXxPClCvmN7iN+cA7Y4/w79JqQ1ta4bPalei7wAAAABJ" &
"RU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(2).
'--------
'Image 3
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADQSURBVDhPrZIxDoMwDEVzp94pR0LKxtCDcIDcgo2pzB0o" &
"z8TCDiFd+NJvkLGffyPCY8rptVmv67qh8rovBmpRW5ZFnktbW+dwqnxAWnbpKFyH1VfRn6JJp4Cc29t6" &
"BnIA8g6gmKNQRd9Peai09yJSTNOkgCgAH//GsiQJYBzfcg/83LoGkBKnGE7AnS4A4pe7Gobh+AsK4LTW" &
"mkQ25rKJD2CeZ3p8gr+AfbvG53twADdYTppra3wH0BPZGptqM2w/czdsRZ0tLZf3ImnsuKMQfsoaZf1g" &
"N8EXAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(3).
'--------
'Image 4
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhP5Y+xDcMgEEXZiQ3oKanpWIGWyiMg0bnIBEzA" &
"APRUVB6E6J9yjmNbltMmX3qSfdz7xuLHUkoZV7zWjuGFZVkuOS2bpmm01k6FLb33D1AClwpSSiPnTEMc" &
"7kV8oNZKYM85R6wFEOf5MWKMBJZ4GefeexKUUkNKSfB8/Q3At8BzCIEkFhhjDInW2ncBBwOAAhShQGtN" &
"ywDvXHCQt8EhbsAipFviPix8Lf5VhHgCWJYzPQzt+z0AAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(4).
'--------
 Page 654

'Image 5
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFFSURBVDhPhdE9jsIwEAXg3JSOK9Cm4ghIdBR7gpwgfeip" &
"UtGhbdLRef2N7QCC3X3SyOPxvDc/7j7her2m8/mcLpdLmqYpuUN9/huIt9st3e/3tCxL+GLjOP4vMs9z" &
"kIBAkL9TFkghQAhq+jskIUKcOb90UEQOh0PcoVIeECxVCsns+/0+hEIgd0LAXj4KrJVyojGI9X3/EMjm" &
"/usuBKODOrP2wyqI7na7/FaClfaKNsJqWaxhGIbo4O1LXQT9AHWJjaxqGaHET6ev8F8WqSoByYyQZEv0" &
"huS0PMbXyfF4LCICzagTU8H5/OYH/IqTbbfbiHeqcZp6M3EEZnlI4jqUKyYndsCRoAOPTm0iG8E44vwm" &
"yAjGHoho+bmqFs3J2p01UYJ2FgJABMxPWQXJm81m9cUVekalP1Dj648gqda+DmpqRtf9AP0hnw19P0q2" &
"AAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(5).
'--------
'Image 6
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrY/BDQMhDATpiZ5ogQ7oIW1QAQXcnxcvGsg/" &
"D4e1DmJxMSFRVhr58K3XYFby3tOKZgG6YKLHnYhuVGvlCo7DcW2WjQAxPIc0y+cA5xzTjmStHefeY6Mm" &
"GYDhnPPvAe9olldACIFm5ieoN4A5pXRh6wYY1nRuWIOAUgpvRI0x8vdcm5sr/GM71K/fQ3CWoCdB7xIg" &
"OX8M5D81QD5hltyuBnzLGBbqzV3+JWOejKK9y4VDarMAAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(6).
'--------
'Image 7
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACKSURBVDhP5Y/BDYAgEASp1lLogR8PK6ACCuBPOZi9uJeT" &
"KIJfN1kleDOC+1fa2eU0730LYWsx7ksSBVNK2hmJgjlnqRWMJAqWUh5hCjALRkiCtVaBWUqsiLMqwAIB" &
"PBL0IGZFgAcHUCu5+yO+cw2YkU2ehHd8AS8CRCWroI1cB+9V0OYz2GcCdO4AJDgrNEoV+vUAAAAASUVO" &
"RK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(7).
'--------
'Image 8
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFSSURBVDhPrZIxjoNADEU5RQ5AR01FS88Z0nEFWiqOgESX" &
"Yk/ACVJtNXVSUVFEQiutcgKH9zVOCNF2+6WvMWP7+4+T5F9hK+Z5tuv1Kno8TZMty0LaYukLJCg8n8/W" &
"tq3Vda34fr+rgTwCfIcQROqBBCj2KcQkvQFsBYgR6LrO+r5XPhnH0YZhME4X2Frn5J6apmmsLEsJ4JYe" &
"OXAREqfTlxpo9EKeRcyZ57lquKdHi0MAS9jzpDdXVaXJxJwQIb7lACCCbS5odCcUF0Uh28fj8emEHA7Z" &
"iwQAIoBF8QQmU4zlLMsU+462iO0vxPvnpMPhoOnbxlj6N8KQr/uIJF6Js5h+B5vck8bb7aJpnGFIPmog" &
"kED4MZGl6B+3Tvy9fK/poFMuYo0zTVM9MyHYk4kWn8A51MlHDeRX0dJYFmpOGvb0n3BP7QGRLVjYXpRv" &
"7t9h9gBC6HW76IZ0JgAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(8).
'--------
'Image 9
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABrSURBVDhPzZPBDcAgDAOzEzuxEzuxEyWQSG3lEos+2kgH" &
"j9j3QEJ+MLm2V+iRSgsRBCuAZYURwKITCWDJGPsdwWW/EkTlmXkQMOUJEFDlamwJvHwW6KRc+qOs0Yxn" &
"x+0Cih7Wwh37EJ+NyAFYvu7sFAcsUAAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(9).
'--------
'Image 10
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADRSURBVDhPlZHBDYQgFETpyQ4shjo8WYIJNw9bgXVwtwez" &
"F27eWB77EZYoYV8yQSEzf0SVWNeXr2WM+RF7x3F4EFuGwxrnnDz5aN73/QoRW6YO0NZ7+5aXQDISQhux" &
"ZcoAjIOx3zUEAefzPEcty/IcQG3MqPwEOM8zrrcB1ErmukEpoIXYMgQ8weQkaAaoMLkl6G5QTu5usG3b" &
"JQw6bDOYFcFtADcL9URjbRQXiqAZUDcYx9EPWnuNpOVfDfi1paC7wZ2gGdDDYwAHPZqmSQKU+gA5Z34b" &
"jO2S6AAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(10).
'--------
'Image 11
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABfSURBVDhP7ZHLEQAREAUnJznJSU6bE2bXK4zxWScHXfUu" &
"6L4gDWMfb5xYPEvXcz7Ji93AwQHLj7V/VwLVosNujryTUm+FDNYjigzmkYEM+pEFGbSRHzLIkQ0ZsDiW" &
"iQJ9a0WWG+2+xgAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(11).
'--------
'Image 12
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACFSURBVDhPxZLNDYAgDIXZiZ3YyQUcxnQUDh65If0hNgEB" &
"68GXNPAS+rUU3FBbzhRmCWA/TiNEAB6sXQgAI5ggCmCDUCJkryDFvoAIALdUvXgvfk0KgOJOPgC4i78B" &
"0xnQIRmYBkBkT2tP9UAbd8XHZFR9phFgqD4AP9EiIKXU3D3ALNm5C0L52yY+Ei+PAAAAAElFTkSuQmCC" &
""
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(12).
'--------
'Image 13
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACvSURBVDhPxZOxDcMgEEXZyRWte5degTmYAuk6F1HmoI6u" &
"Z4kU8QQX/ikk4hwpil34SU9IBh7X2J3PslzkiBrYSxcI9J9gEyD+aC9YQRfg+3db8Hp7qO0bMBMEiTFK" &
 Page 655

"Skk3bMgKusAQgszzrCtigUhyznq4vQzXdVVBF/DeyziO6jRNNTSoNkrMwlVgJqBOHIbYxyWdoq6llN+B" &
"oAFWCdYLzRYCXWAP78ARX3/EaTj3BE/HY6gtq9MqAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(13).
'--------
'Image 14
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAD3SURBVDhPpVK5DcMwDNQU3sE1B3LHOVS5TsvORZoU2cED" &
"aCfmTo+R2EEkIAQOtCHe+WhdYMUYnZjneQhtPpNZfGHxUDT9BGdYFwHim4CKuoiUXgXafKV/OkjJ/Pm4" &
"uVrydV3dTDxZcFPBs/YdGIgkN4EYA0SDKwRSKgJtvtJPDtTwNQCWVbU4gABdiFnfAdgQUcfS2Llgmqbc" &
"A9B1MALWRYDg4fkWuErCGmWlAQdngQxcI8bwPPAPvgoAy7Lk3neA63uHZgErHe9dB/u++7bdjyzkAOUk" &
"otcV2nylfzpgkFqYmEgGyY8glZ/IuggQPBxBm6/0fyqEF35t4Jp72sWRAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(14).
'--------
'Image 15
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEdSURBVDhPjZK9kYMwEIXVkzugETKVQgnMKHPgCqiAApQT" &
"EZExl5CR7enb49nCdz77zbzR39uPFRDQtm22LIsNw2B937u7rrPr9WbzPBvnHvxLHOacPcwICFM4jqOD" &
"GF9CCFPMSAjRicCCqBMZOYAwwX3ffRMQ62marG1bh8sppft8XVfPB+5LEWKMMXoHFOtqEk9mDVyQAFXX" &
"qJ+s9gVgXhcypzY8349DDBQTktQBNXJgAzphFWsNWAD2WNdyAJtM1O4nJnsH0BYbLDh8JzL6Yg5AQPSC" &
"UCxdp/wz1ka/OpD0glAqXwDnLzsZ1V2+BDRNY5fyP/BPxPISMUBEhkL5KD8DLqVfrEL9H4gMWfkoPwP+" &
"kwBH2UMCfOIHIIRv0OL04WmkM8UAAAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(15).
'--------
'Image 16
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABVSURBVDhPYxjc4OPHj/+hTNLB9evX/x84cOA/2YacOXPm" &
"/4IFS8g35D8QUMUQkGaKDAFp2rRpE9iQu3fvkmYARS4AaaYoDCgOQIrTAQhQpHmoAAYGAFSwieHivX4c" &
"AAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(16).
'--------
'Image 17
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABUSURBVDhPYxgFVAYzZ878X15e/v/jx4//oUJgAOKDxBcs" &
"WIIijgFAipycnFAMgWmGiYMV4gLoiknSDAPImnx9fUnTDANk2YwOQIYQDLRRQA3AwAAAiXFatNps77AA" &
"AAAASUVORK5CYII="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(17).
'--------
'Image 18
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAA7SURBVDhPY6AKmJnG8J8UDNWGACDB/2eMicKjBgy0ASB1" &
"MAzVCgGkGACjoVohYNQAiAGkYqhWSgEDAwDTxNM7a88G5gAAAABJRU5ErkJggg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(18).
'--------
'Image 19
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABqSURBVDhP7YxRCoAwDEN7J+/knbyTV9hZqumWbgw3quif" &
"D0IgbSKvsK2id1RqFYS6LyH9Ax8NIBiqL1PnzdERdusG6Lma8edWzMIDIKX0fIBlOjCPDLRlL9IjAxDL" &
"LcjxfCVrFuxxogkiB2Z21dLgH0jYAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(19).
'--------
'Image 20
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADBSURBVDhPtZLBDcMgDEXZqXNkDVZgFt9y6ARMwACehFM5" &
"90D5xjQkNE1atV/6QSb+TxZgfiamS+6dUsqQ/n4vBLbCXoxRq1Ea7cO0cYUMtmY9HTYlwDQ2HxigJ6Cu" &
"i6ZpKl+uRa/7TRYqk4QQFMBWAZjESrgZ9eDSB8A8X0dAH96FKICIXk9QCg22ejFz7fs3YH0OvVvfIWDP" &
"PUAOEXeJTTGoJ+2cy977bApOniwKjHTW6EdOXiMgmORTQwL4XsY8ADUZdcZU+bcCAAAAAElFTkSuQmCC" &
""
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(20).
'--------
'Image 21
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABKSURBVDhPpYxBCgAwCMP8/6c3ChNECrM2UHpKQuC8XwF5" &
"HUh5FaiyHOiyFGAyNoKJuS9MqhvBxNwYJmMSdgDYAWAHgB0AdoAQcQHymD/B+6ambAAAAABJRU5ErkJg" &
"gg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.Add(Img) 'add this image as imgList.Images(21).
'--------
'Image 22
'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABKSURBVDhPpYxBCgAgDMP2/08rBQdDKnZroPSUxIN1fgwC" &
"ViQD40gNjCJ3oB1hAUyGyTkJJtZ9YVJOgomYjCUDSwaWDCwZWHKDiA2SBz/Bns8W7AAAAABJRU5ErkJg" &
"gg=="
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
 Page 656

imgList.Images.Add(Img) 'add this image as imgList.Images(22).


End Sub

'*********************************************************************************
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object.
'*********************************************************************************
Private Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

End Class

'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' BrowserSupport Static Class Module
' Supprt for BrowserDialog class.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module BrowserSupport
'*********************************************************************************
'Enumerator: DriveImages
'Purpose : Reference to images in the ImageList.
'*********************************************************************************
Friend Enum DriveImages As Int32
None = -1
FolderClosed
ShortcutClosed
FolderOpened
ShortcutOpened
Folers = ShortcutOpened
Fixed
CDRom
Removable
Ram
Network
Desktop
Documents
Downloads
Music
Pictures
Videos
Recent
FolderCanOpen
FolderIsOpen
FolderSide
FolderSideShortcut
UpOneFolder
ArrowLeft
ArrowRight
End Enum

'*******************************************************************************
' Method Name : GetShortcutLinkToPath
' Purpose : Retrieve the command path a shortcut file links to.
'
'This method requires COM references to:
' Microsoft Shell Controls and Automation (Shell32.dll), and ensure its Embed Interop Types parmameter = False.
'*******************************************************************************
Friend Function GetShortcutLinkToPath(ByVal ShortcutFilePath As String) As String
' define our shell class object as a link to our operating system shell.
Dim Shell As Shell32.Shell = New Shell32.ShellClass
' define a folder object to the link file's directory folder.
Dim Folder As Shell32.Folder = Shell.NameSpace(IO.Path.GetDirectoryName(ShortcutFilePath))
' define a link to the shortcut file object from the folder object.
Dim FolderItem As Shell32.FolderItem = Folder.ParseName(IO.Path.GetFileName(ShortcutFilePath))

If FolderItem IsNot Nothing Then 'if it exists...


Return DirectCast(FolderItem.GetLink, Shell32.ShellLinkObject).Path 'then return the link's command path
Else
Return Nothing 'otherwise failure, so indicate so
End If
End Function

'*********************************************************************************
' Method : AddSlash
' Purpose : Add a terminating backslash to a drive/path if required. This function
' : is useful for building paths, and the string you are working with may
' : or may not already have a backslash appended to it.
'*********************************************************************************
Friend Function AddSlash(ByVal strPath As String) As String
If strPath.Last = "\"c Then 'already have a backslash?
Return strPath 'yes, so simply return the string.
Else
Return strPath & "\" 'otherwise, return the string with a backslash.
End If
End Function

'*********************************************************************************
' Method : RemoveSlash
' Purpose : Remove any existing terminating backslash from a path.
'*********************************************************************************
Friend Function RemoveSlash(ByVal strPath As String) As String
If strPath.Last = "\"c Then 'already have a backslash?
Return strPath.Substring(0, strPath.Length - 1) 'yes, so remove trailing slash if it exists.
 Page 657

Else
Return strPath 'otherwise, simply return the string.
End If
End Function

End Module

'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
' DriveItem Class (NOTE: You may need to add 'Imports System' if VB2010 or previous)
' Keep track of drive info in a Drive-Based ComboBox.
'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
Public Class DriveItem
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Friend Drive As String 'root path, such as C:\.
Friend Volume As String 'volumn name for the drive.
Friend Type As IO.DriveType 'the drive type flag.
Friend ImageIndex As DriveImages 'the image index for the drive type.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

'*******************************************************************************
'*******************************************************************************
' Method Name : New
' Purpose : Set up the class using a DriveInfo structure.
'*******************************************************************************
'*******************************************************************************
Friend Sub New(ByVal DrvInfo As IO.DriveInfo)
Me.Drive = RemoveSlash(DrvInfo.Name) 'grab drive (C:\).
Me.Volume = DrvInfo.VolumeLabel 'volume label.
Me.Type = DrvInfo.DriveType 'get the drive type enumeration.
'-----------------------------------------------
'now compute the image index for the drive type.
'-----------------------------------------------
Select Case Me.Type
Case IO.DriveType.Fixed
Me.ImageIndex = DriveImages.Fixed
Case IO.DriveType.CDRom
Me.ImageIndex = DriveImages.CDRom
Case IO.DriveType.Removable
Me.ImageIndex = DriveImages.Removable
Case IO.DriveType.Ram
Me.ImageIndex = DriveImages.Ram
Case IO.DriveType.Network
Me.ImageIndex = DriveImages.Network
Case Else
Me.ImageIndex = DriveImages.Fixed
End Select
End Sub

'*******************************************************************************
'*******************************************************************************
' Method Name : New (overload)
' Purpose : Set up the class using a Text Name for generic items, such as Desktop, Documents, Music, etc.
'*******************************************************************************
'*******************************************************************************
Friend Sub New(ByVal DrvInfo As String)
Select Case DrvInfo.Trim.ToLower
Case "desktop"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
Me.Volume = "Desktop" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = DriveImages.Desktop 'set image index.
Case "documents"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
Me.Volume = "Documents" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = DriveImages.Documents 'set image index.
Case "downloads"
Me.Drive = Environment.GetEnvironmentVariable("USERPROFILE") & "\Downloads"
Me.Volume = "Downloads" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = DriveImages.Downloads 'set image index.
Case "music"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic)
Me.Volume = "Music" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = DriveImages.Music 'set image index.
Case "pictures"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)
Me.Volume = "Pictures" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration
Me.ImageIndex = DriveImages.Pictures 'set image index.
Case "videos"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos)
Me.Volume = "Videos" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = DriveImages.Videos 'set image index.
Case "recentplaces"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.Recent)
Me.Volume = "Recent Places" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = DriveImages.Recent 'set image index.
End Select
End Sub

'*******************************************************************************
' Method Name : New
' Purpose : Initialize a new sorting method using the default column 0 and Ascending Sort Order.
 Page 658

'*******************************************************************************
Public Overrides Function ToString() As String
If Me.Type = IO.DriveType.Unknown Then 'virtual drive, suck as desktop, etc.
Return Me.Volume
Else 'otherwise a logical drive.
Return Me.Volume & " (" & Me.Drive & ") [" & Me.Type.ToString & " Drive]"
End If
End Function

End Class

'******************************************************************************
' Copyright © 2012-2021 David Ross Goben. All rights reserved.
'******************************************************************************
 Page 659

Black Book Tip # 51


Make Using, Creating, and Embedding ImageStrips E-Z.
An ImageStrip is a single image object that
in turn consists of two or more images that
all have the same size and the same color
depth. They can consist of a single row of
images joined next to each other, or as a
series of rows stacked atop each other that
will in turn be treated as if they were a
single row of images. Color depths are
typically 16 colors (4 Bits per Pixel, or
BPP), 256 indexed colors (8 BPP), RGB
true color (24 BPP), and RGB true color
with alpha component (32 BPP). In 32 BPP
images, the transparency is coded right within the Alpha component of a color and it can be used to
render rounded images with smooth edges. For color depths of 24 BPP or less, a fixed color is used for
the transparency color. Each pixel in the image that has that color will be rendered as transparent.
Typically, this color used is either a day-glow green ( RGB(0,255,0)) or that god-awful magenta
(RGB(255,0,255)). The problem with this is that it means that there is one less color in a limited pallet of
colors that we can use for drawing our images, and that rounded shapes do not have the smooth edges
that Alpha-channel transparency offers. But how much stuff do we want to draw as Magenta?
Consider the above ImageStrip examples, picked up from the Aixilis WorkShop website on their page
under the title, What is an Image Strip? (http://www.axialis.com/tutorials/image-strip.html)
Looking at the internet, it seems we can find free ImageStrips plastered everywhere. They often have
really nifty images that we would want to take advantage of, or we would just like to lift an image or
two and add them to our own application. But some are stuck on how to do that.
It seems that a number of things are frustrating numerous programmers: 1) They do not know how to
load an ImageStrip, 2) They do not know how to create their own ImageStrip, or 3) They do not know
how to split an ImageStrip and to collect specific images.
Fortunately, doing all of these things is almost insanely easy to do, and the solution to all of them is to
just use the simple ol’ ImageList control.
If we use an ImageList control and add images to it, just as we have done within this document, such as
in the last two Black Book Tips, # 49 and # 50, then we have been using ImageStrips all along and
probably never knew it, because an ImageList will store the images we add as an internal ImageStrip.
Were we to look at this control’s exposed properties by applying the dot operator to the name of an
ImageList in our program code, we will find a property named ImageStream, but we cannot access the
image data through this; it is actually an ImageListStreamer handle, which technically indexes into an
internal reference list, which in turn points to the binary data stored within our program’s resources.
Although the documentation for this property states that “You can pass this handle to another instance
of an ImageList,” this is not true, because the property is actually Read-Only. Indeed, if we look to the
documentation for an ImageListStreamer, we will see that “you cannot use this class to load images to
an ImageList control by directly assigning the ImageStream property from one ImageList. Instead you
should directly assign one ImageList instance to another (for example, ImageList2 = ImageList1)”.
Even so, because we do have total access to all of its images, we can easily construct an ImageStrip
image and create an ImageStrip file with surprisingly little code, as I will demonstrate.
 Page 660

Loading an ImageStrip
This is as easy as using the AddStrip method from the Images collection of our ImageList. We should
first initialize the image sizes, such as “Me.myImageList.ImageSize = New Size(16, 16)”, but this is not
really necessary for 16x16 images, which is its default. This is especially important for stacked images,
where the strip is not just a single row of images, but two or more rows of them. It will process this
image internally left-to-right and row-by-row, as though it were one long narrow image.
To use the AddStrip method, we need to specify the Image object that contains the ImageStrip. We can
load this image from a file, or load it from our resources. For example:
Me.myImages.Images.AddStrip(Image.FromFile("C:\Users\JoeSchmindy\Pictures\ImageStrip.png"))

Or...
Me.myImages.Images.AddStrip(My.Resources.ImageStrip)

Once the ImageStrip is loaded, we can use it just as though we had simply loaded a series of images.

Creating Your Own ImageStrip


Once we have loaded our ImageList with images, it might be nice to save them off as an ImageStrip.
Because an ImageStrip is just an image file, let us first review how we save them. By default, VB.NET
saves images as PNG files (Portable Network Graphics), which is much more efficient than the old
default of BMP (Bitmap) used by VB6. So, even if we saved our file from our image using code like
“myImage.Save("C:\Users\JoeSchmindy\Pictures\ImageStrip.bmp")”, and even though we gave it a “.bmp”
extension, it will still be saved as a PNG file. To actually save it as a bitmap file, we would have to
specify “myImage.Save("C:\Users\JoeSchmindy\Pictures\ImageStrip.bmp", Drawing.Imaging.ImageFormat.Bmp)”.
Now, consider the following subroutine, SaveImageStrip:
'*******************************************************************************
' Method : SaveImageStrip
' Purpose : Save the images in an ImageList to a specified file as an ImageStrip.
' : by default, the ImageStrip will be saved as a PNG file and you should
' : add a ".png" file extension to your file. If you want to save it in a
' : different format, you will have to specify that format. For example,
' : a bitmap formap specification is "Drawing.Imaging.ImageFormat.Bmp".
'*******************************************************************************
Friend Sub SaveImageStrip(ByRef imgList As ImageList,
ByVal DestFilePath As String,
Optional ByVal ImageType As Drawing.Imaging.ImageFormat = Nothing)
If ImageType Is Nothing Then
ImageType = Drawing.Imaging.ImageFormat.Png '.NET default for image saves is PNG.
End If
With imgList
Dim imgWidth As Int32 = .ImageSize.Width 'get the width of a single image.
Dim imgCount As Int32 = .Images.Count 'get the number of images in the imagelist.
Dim newImage As New Bitmap(imgWidth * imgCount, .ImageSize.Height) 'size the bitmap imagestrip.
Dim g As Graphics = Graphics.FromImage(newImage) 'create a graphics handler for it.
For Idx As Int32 = 0 To imgCount - 1 'process each image to the imagestrip.
.Draw(g, New Point(Idx * imgWidth, 0), Idx) 'draw an image to the right of the previous image.
Next
g.Dispose() 'dispose of resources.
Try 'save the imagestrip to a destination filepath.
DirectCast(newImage, Image).Save(DestFilePath, ImageType)
Catch
End Try
End With
End Sub

Here, we use the ImageList control to draw each of its images to a bitmap that is sized to fit all of its
images side-by-side. We pick up the established individual image width from the control, and then the
count of images from its Images collection. We then create a new bitmap, newImage, that is sized to be
the count of images times the pixel width of a single image, and by the image size height. We next create
a graphics interface for it so we can draw to it. Following that, we cycle through each of its images and
draw them across newImage, shoulder-to-shoulder. Once done, we dispose of our graphics interface and
save the bitmap, cast as an Image object (identical format), to the specified file path.
 Page 661

After our ImageList is loaded (if we did not already pre-load it at development time), suppose we want
to save its images as an ImageStrip in PNG file format, we could use the above method something like
this: “SaveImageStrip(Me.myImages, "C:\LocalImages\ImageStrip2.png")”. This would save the ImageList,
here named myImages, to a file named ImageStrip2.png to an existing specified folder.
Once it is saved, we can load it from a file or from resources to another program as previously shown.

Splitting an ImageStrip
This is a moot topic by now, because in the process of covering how to save an ImageStrip from an
ImageList control, we saw that we went through no effort to split them out. We can access each
individual image from the Images collection of the ImageList as though we had loaded them
individually. Indeed, the only thing left to cover here is how to save individual images off, but we
already saw that demonstrated when the Save method was used to save the ImageStrip Image to a file.

Updating Black Book Tip # 49 to Use ImageStrips


Now that we are experts at loading, building, and splitting out ImageStrips, why not revisit Black Book
Tip # 49, Embedding Images within Your Source Code, and enhance it to optionally build source code to
embed an ImageStrip into our source code? The advantage is we do not need to build the ImageList from
individual images, but we can define them more efficiently using a single block of code.
What follows is the updated BuildImageListCode program module:
Option Explicit On
Option Strict On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modBuildImageListCode Static Class Module
' Build Image List Source Code Constructor
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Module modBuildImageListCode

'*******************************************************************************
' Method : BuildImageListCode
' Purpose : Provided an existing filled Image list, this method will create code that can
' : load up an ImageList from scratch, filling it with the images you desire
' : without requiring the user to first load those images into their resources.
' :
' : For example, if you want the target ImageList to be named 'myImageList',
' : define that in your program heading. For example:
' : Private myImageList As New ImageList
' :
' : Next, fill it with the needed images, either in-code or manually, and then pass
' : it to this method, preferably within your Form's Load() event. For example:
' : BuildImageListCode(Me.myImageList)
' :
' : This method will save the fully-constructed VB source code to the Clipboard.
' :
' : Next, convert the above BuildImageListCode() command line into a comment, plus
' : any other code that may have previously initialized the ImageList.
' :
' : Next, paste the Clipboard code into your form's code body. This will add a
' : Function named InitializeImageList() and a support function named
' : ConvertBase64ToImage(). Right below where you had disabled the invocation of
' : the BuildImageListCode() method, enter the command InitializeImageList by
' : passing it the name of your target ImageList. For example:
' : InitializeImageList(Me.myImageList)
' :
' : InitializeImageList() will define each image as a Base64 text string. By passing
' : that to the ConvertBase64ToImage method, it will convert this string into an
' : Image object and return it, where it will be added to the target ImageList. It
' : will process all images originally defined in the initial, now-not-existing
' : imagelist (well, you can test this without deleting it, just do not reference it).
' :
' : Now, when you run your program, this invocation will create all desired images
' : and add them to myImageList, or whatever you chose to name it.
'*******************************************************************************
Friend Sub BuildImageListCode(ByRef srcImageList As ImageList, Optional ByVal MakeImageStrip As Boolean = False)
With srcImageList.Images
Dim Ln As String = "'" & New String("*"c, 81) & vbCrLf 'build a header line.
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Buffer As New System.Text.StringBuilder
Dim NewSize As String = srcImageList.ImageSize.Width.ToString & ", " &
srcImageList.ImageSize.Height.ToString
Buffer.Append(vbCrLf & Ln &
"' Method : InitializeImageList" & vbCrLf &
"' Purpose : Imitialize a provided ImageList and fill it with locally-created images." & vbCrLf &
"' :" & vbCrLf &
"' NOTE : If you want to append the images to an existing list, set the Replace" & vbCrLf &
"' : parameter to FALSE." & vbCrLf &
Ln &
"Private Sub InitializeImageList(ByRef imgList As ImageList, Optional ByVal Replace As Boolean = True)" & vbCrLf &
 Page 662

Pad & "If Replace Then 'if we are filling, not appending images." & vbCrLf &
Pad & Pad & "imgList.Images.Clear() 'initialize image list." & vbCrLf &
Pad & Pad & "imgList.ImageSize = New Size(" & NewSize & ") 'define pixel image sixe in this list." & vbCrLf &
Pad & "End If" & vbCrLf &
Pad & "Dim strImg As String 'string to be assigned image data as Base64 text." & vbCrLf &
Pad & "Dim Img As Image 'image to receive data from the memory stream." & vbCrLf)
'---------------------------------------------------------------
'build data as a single ImageStrip.
'---------------------------------------------------------------
If MakeImageStrip Then
With srcImageList
Dim imgWidth As Int32 = .ImageSize.Width 'get the width of a single image.
Dim imgCount As Int32 = .Images.Count 'get the number of images in the ImageList.
Dim newImage As New Bitmap(imgWidth * imgCount, .ImageSize.Height) 'size the bitmap ImageStrip.
Dim g As Graphics = Graphics.FromImage(newImage) 'create a graphics handler for it.
For Idx As Int32 = 0 To imgCount - 1 'process each image to the ImageStrip.
.Draw(g, New Point(Idx * imgWidth, 0), Idx) 'draw the image to the right of the previous image.
Next
g.Dispose() 'dispose of resources.
Buffer.Append(Pad & "'------------------------" & vbCrLf &
Pad & "'ImageStrip for " & imgCount.ToString & " Images." & vbCrLf &
Pad & "'------------------------" & vbCrLf)
Buffer.Append(BreakUpBase64String(ConvertImageToBase64(DirectCast(newImage, Image)))) 'break up and format Base64 string.
Buffer.Append(vbCrLf &
Pad & "Img = ConvertBase64ToImage(strImg) 'grab image from string data" & vbCrLf &
Pad & "imgList.Images.AddStrip(Img) 'add this as Images 0 - " & (imgCount - 1).ToString & vbCrLf)
End With
Else
'-----------------------------------------------------------
'build data as individual images.
'-----------------------------------------------------------
For idX As Int32 = 0 To .Count - 1 'process all images in the ImageList.
Buffer.Append(Pad & "'--------" & vbCrLf &
Pad & "'Image " & idX.ToString & vbCrLf &
Pad & "'--------" & vbCrLf)
Buffer.Append(BreakUpBase64String(ConvertImageToBase64(.Item(idX)))) 'break up and format Base64 string.
Buffer.Append(vbCrLf &
Pad & "Img = ConvertBase64ToImage(strImg) 'grab image from string data." & vbCrLf &
Pad & "imgList.Images.Add(Img) 'add this image as imgList.Images(" & idX.ToString & ")." & vbCrLf)
Next
End If
'---------------------------------------------------------------
'Finish out source code using common code.
'---------------------------------------------------------------
Buffer.Append("End Sub" & vbCrLf & vbCrLf &
Ln &
"' Method : ConvertBase64ToImage" & vbCrLf &
"' Purpose : Convert a Base64 String to an Image object." & vbCrLf &
Ln &
"Private Function ConvertBase64ToImage(ByVal strImg As String) As Image" & vbCrLf &
Pad & "Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array." & vbCrLf &
Pad & "Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to memory stream." & vbCrLf &
Pad & "Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data." & vbCrLf &
Pad & "memStream.Close() 'release stream resources." & vbCrLf &
Pad & "Return Img 'return image." & vbCrLf &
"End Function" & vbCrLf)
Clipboard.SetText(Buffer.ToString, TextDataFormat.Text) 'save a copy of the buffer to the clipboard
End With
End Sub
'*******************************************************************************
' Method : SaveImageStrip
' Purpose : Save the images in an ImageList to a specified file as an ImageStrip.
' : by default, the ImageStrip will be saved as a PNG file and you should
' : add a ".png" file extension to your file. If you want to save it in a
' : different format, you will have to specify that format. For example,
' : a bitmap formap specification is "Drawing.Imaging.ImageFormat.Bmp".
'*******************************************************************************
Friend Sub SaveImageStrip(ByRef imgList As ImageList,
ByVal DestFilePath As String,
Optional ByVal ImageType As Drawing.Imaging.ImageFormat = Nothing)
If ImageType Is Nothing Then
ImageType = Drawing.Imaging.ImageFormat.Png '.NET default for image saves is PNG.
End If
With imgList
Dim imgWidth As Int32 = .ImageSize.Width 'get the width of a single image.
Dim imgCount As Int32 = .Images.Count 'get the number of images in the ImageList.
Dim newImage As New Bitmap(imgWidth * imgCount, .ImageSize.Height) 'size the bitmap ImageStrip
Dim g As Graphics = Graphics.FromImage(newImage) 'create a graphics handler for it
For Idx As Int32 = 0 To imgCount - 1 'process each image to the ImageStrip.
.Draw(g, New Point(Idx * imgWidth, 0), Idx) 'draw the image to the right of the previous image.
Next
g.Dispose() 'dispose of resources.
Try 'save the imagestrip to a destination filepath.
DirectCast(newImage, Image).Save(DestFilePath, ImageType)
Catch
End Try
End With
End Sub
'*********************************************************************************
' Method : BreakUpBase64String
' Purpose : Break up a Base64 string into a formatted multiline string.
'*********************************************************************************
Friend Function BreakUpBase64String(ByVal srcBase64 As String) As String
Dim Pad As String = Space(4) 'init padding for text lines.
Dim Ary(1) As String 'init with 2 elements.
Ary(0) = Pad & "strImg =" 'init first element.
Dim idX As Int32 = 1 'init array index.
 Page 663

Dim idY As Int32 = 0 'init string offset.


Do While srcBase64.Length > idY + 79 'breat up into lines of 80 characters apiece.
Ary(idX) = Pad & """" &
srcBase64.Substring(idY, 80) & """ &" 'break up a portion (change to """ & _" if pre-VB2010).
idX += 1 'bump index
ReDim Preserve Ary(idX) 'expand array (at least for final line.
idY += 80 'point to next group of bytes.
Loop
Ary(idX) = Pad & """" & srcBase64.Substring(idY) & """" 'add final line (no need for continuation.
Return Join(Ary, vbCrLf) 'make one string, split by CrLf.
End Function
'*********************************************************************************
' Method : ConvertImageToBase64
' Purpose : Convert a source Image object to a Base64 String.
'*********************************************************************************
Friend Function ConvertImageToBase64(ByRef srcImage As Image) As String
Dim memStream As New IO.MemoryStream 'memory stream to receive image data.
srcImage.Save(memStream, Imaging.ImageFormat.Png) 'copy current image to the memory stream.
Dim bAry() As Byte = memStream.ToArray() 'convert the stream to a byte array.
memStream.Close() 'done with the memory stream.
Return Convert.ToBase64String(bAry) 'construct a Base64 string.
End Function
End Module

With the above update, were the images displayed below loaded into an ImageList named myImages, I
could construct a new block of source code into the clipboard, ready for pasting, by invoking the
BuildImageListCode() method using “BuildImageListCode(Me.myImages, True)”. The second logical
parameter will tell the method to build the images as a single ImageStrip.

'*********************************************************************************
' Method : InitializeImageList
' Purpose : Imitialize a provided ImageList and fill it with locally-created images.
' :
' NOTE : If you want to append the images to an existing list, set the Replace
' : parameter to FALSE.
'*********************************************************************************
Private Sub InitializeImageList(ByRef imgList As ImageList, Optional ByVal Replace As Boolean = True)
If Replace Then 'if we are filling, not appending images.
imgList.Images.Clear() 'initialize image list.
imgList.ImageSize = New Size(16, 16) 'define pixel image size in this list.
End If
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.
'------------------------
'ImageStrip for 30 Images
'------------------------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAAeAAAAAQCAYAAADOMaw4AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAABKySURBVHhe7Zwvr1xHEsXfpzAyMjMeFGpuaGw2XyHUKHiR" &
"pWEBq5UWLPEnCFqwHpwgI4NI0UobM0sLZutXXedO3bp9/8ybmeRZ2iOddN/u6ur/fW7fN87DPfD58+fT" &
"8Xg8/fLLL6ePHz+eeAaRvRnff//9aYlmcrHPp4off/zrqfJwOIxI2m+//bY6lrv98bQ7FFpaZG/HweqB" &
"d8a7d+9O8OXLl5so+yj+8OXLF19jHz58OL1//975ww8/+Hh9+vTpRH6Y/h/rYKy+qfHinNGamON33333" &
"pNcB+/pht3Pu9nvnPvjmzRvf92Hag+bM+ezZs9HzQvpNwZ5bY5gOiLPsUeyhpxM9CubHwTmxxjB92kB4" &
"WSxfv371BU+ctJ9++mnU4S3wgfrv71bqECJuamA8HvcemslNB8Wcej1saKg4g79F+K4BYlHB+Ank0xaJ" &
"cBTroonuqfDxAvy3f/1+t34DDkiAuPrLwwKxAZShLGPE+mJcCJkzyJyx5tj0hE/58D3sH06XMIoN6Nks" &
"MYrNwceXsD0ugxdsxncreUmCxDuHmuqdYxfsizWwXqjzTuugtvMSNuz2Jr52phl3+8Z9kBdKxkzCjEiX" &
"M8D7SIhgV7ERycu2FLwlXGDxPcMZEbacx6FXln4C1sTeLi06t4HOU8ZSdt4CA2tjCeSb2WTM1KfHMtzc" &
"BmwodRIBdvH998kOxtYBDkgQ5qtog3QWX4USYTO5ugO0Eb+0j03KItVGBeSrX7RfhzxwByYKVzFQBdiy" &
"fOwECS8Li9twFOvi1gK8s7ZEygS06xrig3GHPQHe28G0swPHQ3uuN2DmAj+EeYNpvphLFrrmUASUXwJ2" &
"Eb0rEMXTcbeJPQG9tnyBjw0g3pLmwVizJrdQ8yFSNtwIUfMU5AUnwPcaWC+AetlHUfRWMM9q+/aQchTO" &
"Y9Ij7SXk9DzYGNqG8WfKBprHCPMaV1zP2ZaCFewj5mWJ2IT5CC4o+J4h+aAIj6cRXMLw4WUz0AzWw97G" &
"iWsaLyz03dPiKyIvNHpRoQGA8cywpIFAcxFpA2iH8i6h2t+8BHqGmWE2C2wQXuChrRgWj42Fk47zDKLI" &
"IhggBkpvbn4Qx7PS3HAbuva0mcnRjYnFlV8kaK+eW1+OPnj0BSCiU7GbUmI2opWNZowEGOFFMD1k1xnI" &
"p17VHcW6uLUAQ2tqt3xu96WgLD50OCKux+Ph9I+//8U2Sxvnw2F3Oh64uZl4HPaTGzAHAnOidcf88cyc" &
"vn371usQ9Rkfrq1DyrMemPdIuhueqgADnltyH9rzjFOPNY9nxp44cxduhKh1CuURumUCc9UDbYOK68WN" &
"z9G0IYoD+c3+t6YBr8NaNxumA9fjnmpx1nSzm/J4tDMv6qG9L1688DJZOAKeDufAeINsa5wg5mSRnXlz" &
"eB+pY45TX8ByyGtscfZ2C9tzja8L8M72OkQneHZBNj7YM+2XrbfAoHUyh7yOzHwoRztaGm3ezrQezsLL" &
"4bVE2bUKx1DjrI++uPk0RSVsONIQE4RDmyWKLSILMOLrAxnP0Ey2+PF2sNA5eHluyQ1MBgczYR4D6kJ4" &
"CUnHhva8evXK/bGRfSFuEOAsZCN2BJiN0gT0OGwaQSLzZwgw7Imw2r0n/wKCmI/RDfhg7UR8JcDv3rWD" &
"aG8CzFePegNmLJgvQMi6YF4QX4RY7QOMJ8/MKWE5hEdQWeaecpF8FzxlAQaktawp2OfsE81ZpsaOMJPP" &
"qoy9758xosYpyCscoDOlAqGFFawd5jeKg57vrWnAvKrt/RAb1jKUPeFud06vRHwlwIwbZw/srFv3BQXG" &
"nXGB+RzJtsYJYk4W2Zk3h4sKdSxQa8DM5cNSLc+CiDoQKUHpsoFeVy4Q4Iymv6xLzgLGCvDlwEXZ0jQe" &
"PQGOR3/hYY1onXD+V5JOO1p/piILw+ckVPvdAZMk4ZkjmyyTSsOJozXY9rgJrTYdC30QYCPPtaNLyALc" &
"o5ks+fH2IbwsGDEO/aEc7SGdAcVehy5jwjNtpi7ihLwIYEO6T8KKAFcRGzEJML4YN4Sy3oAzQR73Hm4r" &
"wM2X2myPIz+MBUBUyROz2PYIYi7GN2DLPMBdm2O/AZsAcwtmA9UbMOPG+sQXm4V5k/gyj2qf5pRn1ipx" &
"baIeKIMP7CnL3ETWLPCnAyCSHDyTrv5WPHUBBqS37DEYG8Z/LL528zBq3Cr1t7nOQR61LQO7Zt7AXFZQ" &
"N+Jr2R7PYO3oYE2Y+DXUtJ4NCM/zITYSVtkrzVTWNsWYVYAB65rzh7GLJGHwBxgPfrj1/PlzvzXTX85s" &
"kG0pWBFz0s0L9ObN4ecSvo1xRo3DyGM+SKMMISCIaDescfkVtOb81mtz688WIrjMNeRlnfUn9ASY/P/8" &
"/M+hDH0ljzHXuEv/iNMO4oSX0oo3AebgUEU4r8LLhOYGSQDDiYPGUx7RMDP7TzBAx+gwPkAUG5AbJmYB" &
"NhNvh56VRtkCL4vw0h5ImzPjIPSytIU0Joe2kceCxQfh69evvR3ECdUmnim3JMCD0Hbo+UWA58BLjAho" &
"WxTr4tYCTNRvvx0/jBfwee/QzJ38kAsqDUiQGEvo4mrzdrTxPdmNdxfk15uED8Z6A2YsWKM63LVRtH7z" &
"uJJPnZQR8TEHyrB+KENI+cjqgrXBDYVQtoQ53Q0LvgUBBuQ1kzMYlzbOZ+EVNWaEmeQxR75/xoialoFd" &
"M2/QmZKhm69lT27BrB3KtNIDJn4NNa1nA8yr2t4PY605iZNO3AXY5oSXTAk0eVWAGbelGzBQyDpjn4ic" &
"Ya3Osw0hBStiTrp5gd68OdR+sfVkSvpi5vJhKZZuQUSHeH4GbQzbs8YTsJ52Np9+w2Ve7fwglAArpN0Z" &
"eT/SJuAv+/HFDb2hHr5SaH6w5bzR+iFfZS+B2j844JBBhCANpQJC8mkoosPk8wYGlU4jBPxYsTPtsBXw" &
"xcJns4Io4lAbKlXvHK1o9uNtQnhpu3xQb6UOYcp4QQPtIo90iA31s3npN2+f1Kk8JscP2QsFeJTfEWAX" &
"vAWCaPss7iHAwNtc/DAWYG/jzvyyfkjLItwjwA4flAP50FgikADjg3lm/iS+emajaFxJ4zljbRwBc6w1" &
"Q/lI7qKK7RbxBd+KAAPym1mDxtUPwWAV4Mr9sX0GZFzDjRC1LAO7Zt5QBZh6taZinXiaQNqVAjxJb7gs" &
"pFwVLZED/5IbsAhYbxWUBdnWOEHMSTcvMJk3tdmi1OChnmHsM+81ZC4iD9Asi19G+WRf6W+7fvMlbmNk" &
 Page 664

"jbKzKtaj+fe1aTasOyHvSa0PxNZqsLN+LMCkEWK7RYDHYzLFMCZyAhlYnBFnkSI4ElyRTxs0nAklBDrs" &
"JNoW+CFLZ9sn6JbuB7PFWUBAdc/BsrfQfSC8+aYO6YtI/VC2hCoPosrhANdbI33m0CdOul4ghDkBrsIL" &
"pzbnQ6B3A843XxFE2x188vNfDBOOWOsr+VaGsuGmDyuX22hFRs+AOQUsbtaGv3Ha+OoAZh3o5qs1oU2Q" &
"BRgyzvr1s8in6OPBhMM/SU9vwKw7zQ3+thBbkMexB2zkl5B2R9YssGFfcAiyhtbEF2wVUOzEKAqIz3JD" &
"+QofmyVg00ynAizxzQJMmHlvAealWWsk1snoFkwaZVrpARO/hpqm50m6tao5j5C0XnoOyeeQNpX1W/DS" &
"J2jGjbUE1wRYZ7XAp2g9Z1vjBDEn3bzAZN68D96vMSVANYe+eHqDpVh6CxzEIfsTtH16flacsuwt/xmb" &
"9U+i6zdgo/5dta9Ne5buCHlf0ibdfiEXCQkwdkqnT6Rp/RCnbIVl8d9hDCrUfnwMoCIopwgwhwhiqxug" &
"BFiNxw5RYkNBNiMTpH8fqMOLzkDibAAJDnVRhnSVJV5Dq8xD7PPtl2cg/xJfiL2IXRZebOUjw50ZsKPt" &
"fP6k71l4w7QBoRgJneV3WG0az4eAxoP+iggufy81Mw/1t9PowwAXYWevjh43iC8w260CrC8FEh59Pq6i" &
"zI8hmBtAWXywHkBPgJ1sJJsa/m1kvQHrgNd8rQGb3otMBfm0E3v8U09krQJb9scW8QUIYhbKOcquCKj3" &
"pQfPWy9fEaXHIL3QwdhIgM9s861x4+AjrGSNhxshalsGds28QesJUKfWkxBrxfP0TJlWesDEr6Gm9WyA" &
"+7WWtf/aM2sr1penE/bsXLxsPnS4+3Mc3PUGzB5jL/QEWP4A5yPnFv2FpJMGsq1xgpiTbl5gNG+1zZY0" &
"PBOHGoeWuizAW0Ion+wv//xs/fO9huDa83ATNkqfQD7H895s64ZxOouw+qZn/WCOurV+iGtdCeo/eWpn" &
"SztD6fiYgIaxSCW8HKJVeAVNMqRRdJAFQpjzeKOgUn3mZkGQrg5IhNVoUeVF0rIAE+ZyWYRZKKRV4VXc" &
"S3fAjetoB5XT/xlMd9E3IBBJ3KrwwpzvtPPCmcSMMQH5tgsRLBetuEGCaP8I20V4o/gCs68CnNsMJHr6" &
"nwcM/xOBOIzJp/1+C7ZQ8wPIwwdrDc4KsJF1SFhvwIDNhV+1xaofvbSIQIIKeuMo0EbZ4j+SN4My6t8a" &
"biHAhJlK21C+wsuCGg+OwPisCTBhJen3EOB8080wsyGPtUOZVnrAxK+hpvVsADW4b+KsK50xCtXvZuuW" &
"HtchX+kHvuXr3Ilz17ibnMGG5jFC9gIvwfwAi9svZzdzBLItBQXOa+ZjToCVb9HRvNFW+qL+qL9KI5/n" &
"c8rjBTjHVQ9j4Z+YbT4nN+EQYMYQMC7UTQjyOJIugZXYEh748WfEEWfi1K31Q5yyFZYV6a3/FWo/PmZB" &
"AzMjeQQmQ50iFEmnEkhZBIZ0FgK2pGUb0VyOmPMoT1gEGHg6wDcHH88rwushN89KhPfXX392f4T+71A7" &
"duBiAUbExCRmEmDGRESAuUX5DRJGndGPCdZFeF18rTvetnP7mz3iz7OHCRK9kfjCqIsf5+glIgsxkEBx" &
"IAIXYLPP3LsA47M91xuwIMEDqksvLSKQDZgbR6D1/BjxvRS3EGDA3uJZaR6ul68YyrbHs3/iLekM7eks" &
"vFWA+eQsSpA5FFnj4UaImpaBXTNv0HrSi5nWk5DTRdoQxYWJX0NN69kAr4eQNdUjedQZ9Q72SutR4kt5" &
"YNvPxxmBIZ+8gPuCAmXlhzEXsq3Rgbha0M0DNT/Pm14YzLOHljQ8Ex/oKY3RdggshXZ54CAO1e82fh4d" &
"4hpTdMR/eGXjUm/CkGfWB+PBOtWLCMiaRpuAxBcivhJd+kSILT7WBBhYtrMHtR8fVwFnTAgCQqM4uAhZ" &
"5FTCgcfn6CyMdBzScJ7pEPE8OALpmdh3BBh4HtgivAJCqgOahQq58fJTdEttP0nnFpwOcsjbpftFsQyI" &
"hCngIrGRrYcmKNGM2Ruw2iSC6E8X8yK8LL706Sy6mecyblOQBe1SUBYfOhQRV+aY9EHA9yYYtpH2Rv55" &
"Uu8GDBgbtaW+tEAEGWgNilG8C3xG9K64hQCzd4grBB6ul6/wci3qcF+AeHAA81W/drHfRcbwjxLgrWDt" &
"0IYoLkz8GmpazwZ4elpTs3GNkcoYA00wJBq+dkXrn+Lk6VYXBcHgbw3Z1uiIeejmgZpf502CWylBdqac" &
"1H9gKZZuwSXUeKIjjI9uwPkmzFhprRHWddITYLRD6RJf4qRD4lsFeAlqPz6uBg71iUKLjcWim6KeIXEO" &
"QYSRzSj7S0iVreYJhsldE14BIa3kxstBxU1YB1bPjv4gwJtoYtKEccxoRvcG3COIfs1iKsLL4gusiUV4" &
"xeVyzOU1xAcHIkBc/cYc7eX/iOWb2z//tPnYcgPmBcdvzSG8ejEE2GArRvE/FRLGNc4I6Eh8AfEhXC+/" &
"Be4PEG9JDZccPvWFkvUcbgT538IBT0GA8/mkc2cuTt06p4wOF9QQYG64WYS11v23EHAK75fCJWRbCoJ6" &
"w43nAWv5hkFszbOHWXzVd/W79H3Iv5SURSz58Z//8CqFEt8l9AQ4Hh2prSNUAX4srfjE96PhrTfobYPO" &
"UQl/u1OcdCYvQ8Uv5BJq5xbtaQOHsgYFckBV6uWhMtxcDQnwFmyp9yzC6+IL+gI8fkm4FzhgIOK6hbKP" &
"4g4OdAnwEiTAUexJoLfe1hhFAfHu1yPSe2Whl9yO8Nh8tqQG9nR9SdzK3uH2GNxIgK+BxuUxnAX7nHZC" &
"/n7tL1oc/CbCpKVzwPtFKNHukbM421JQ4FxmTjri6ljLD8ivM7Vxjt6YknYRAfrS4xqwMR8O+hYvsqtg" &
"vUmADUNbHsnbIvrmhxwdYhK4fSDMQpjeG5vriWYNoK1VlHnOfRDCxdVAgHN9S6ziMweEd4v4Am4n9W+/" &
"/kvCbwQS4C18agJ8JejLEm+Be/i8Gfz2OG3jIjmXLHzS4Oth9M00t4lo/gTNmeGGnf5t5DePOIYHVBFe" &
"Y7gZRHUrNS/X4eHhf4oC0kFP8ppsAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.AddStrip(Img) 'add this as Images 0 – 29.
End Sub

'*********************************************************************************
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object.
'*********************************************************************************
Private Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function
 Page 665

Black Book Tip # 52


Extracting Icon Images from Files and Displaying Them in a Directory TreeView.
WARNING: This Tip is long and very dense, Tens of thousands of programmers have been asking for this
solution, so here it is. Be careful what you wish for...
Black Book Tip # 40 turbo-charged TreeView directories by parsing folders
only when we were to view their content. Black Book Tip # 48 modernized
directory listings by owner-drawing the TreeView. Black Book Tip # 49
embedded images in source code. Next, Black Book Tip # 50 merged all this to
build a new Folder Browser Dialog for our applications. Finally, Black Book
Tip # 51 combined images into more efficient ImageStrips. Now, we will
combine all this new knowledge and also display files with their own embedded
or associated icons.
With all the enhancements we have explored, one thing noticeably
missing was decent file representation. Although Black Book Tip # 40
showed files in a TreeView, they all shared the same boring icon. But, the
good news is, there is an easy way to grab those images from files, from
executables, DLLs, and from executables associated with non-executable
files. The issue with some programmers is they have had trouble finding file associations, or they do not
know how to rescale the icons to 16x16-pixel images, because the standard .NET extraction method
returns 32x32-pixel images. And, even if they can rescale them, some rescaled icons look rather pitiful
when they are scaled down 75%, especially those without Alpha Blending, which is particularly
frustrating when crisp 16x16-pixel icons typically also exist in those files. The last big hurdle is how to
organize these collected icons to avoid inevitable, and usually overly profuse duplicates, and how to
prevent such a collection from overloading our application's data space if the user explored innumerable
folders containing a huge abundance of files, In this Black Book Tip, we will address all these issues.
If the target file is an icon, it is easy enough to extract an image instance of it. For example: “ Dim Img As
Image = New Icon(IconPath).ToBitmap”. Whether they are an icon library or not, or what their default icons
are, they typically require rescaling, because embedded 32x32-pixel icons are given precedence.
To aquire an image from a cursor file requires just a small bit of work. Grabbing the cursor is easy
enough to do with something like “ Dim Cur As New Cursor(FilePath)”, but then we need to pick up its
image and ensure it is scaled to 16x16, which we can do using its DrawStretched method. For example:
'*******************************************************************************
' Method : GetCursorImage
' Purpose : Get a cursor as a 16x16 image.
'*******************************************************************************
Friend Function GetCursorImage(ByVal FilePath As String) As Bitmap
Try
Dim Cur As Cursor = New Cursor(FilePath) 'grab cursor object from cursor file.
Dim Bmp As New Bitmap(16, 16) 'instantiate a 16x16 target drawing surface.
Dim g As Graphics = Graphics.FromImage(Bmp) 'instantiate a graphics interface for that surface.
Cur.DrawStretched(g, New Rectangle(0, 0, 16, 16)) 'compress the 32x32 cursor image to fit Bmp's 16x16 surface.
g.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!).
Cur.Dispose() 'we no longer need the cursor object.
Return Bmp 'return the new bitmap. Can be assigned directly to an Image Object.
Catch 'let errors fall below.
End Try
Return Nothing 'some enhanced cursors cannot be loaded.
End Function

To use it, simply assign the returned bitmap to our Image variable (Type Bitmap = Type Image).
To extract the image from an EXE or DLL file is easy using the ExtractAssociatedIcon method of the
Icon object. It retrieves the default icon within the provided EXE or DLL file path that its developer had
assigned as its default; the icon that should be associated with the file. This method is very simple to use.
Dim Icn As Icon = Icon.ExtractAssociatedIcon(FilePath) 'grab default 32x32-pixel icon.
 Page 666

This method returns a 32x32 icon, so we need to rescale it to our 16x16 target image by rescaling it.
Fortunately, rescaling a bitmap is also simple. But to do that we first need to access the bitmap image of
that returned icon, which we can obtain easily using the icon’s ToBitmap property. For example:
Friend Function GetImageFromExeDll(ByVal FilePath As String) As Bitmap
Try
Dim IcnBmp As Bitmap = Icon.ExtractAssociatedIcon(FilePath).ToBitmap 'create a 32x32 bitmap of an app's default icon.
If IcnBmp IsNot Nothing Then 'if we got something...
Dim Bmp As New Bitmap(16, 16) 'instantiate a new drawing surface.
Dim g As Graphics = Graphics.FromImage(Bmp) 'instantiate a graphics interface for that surface.
g.DrawImage(IcnBmp, New Rectangle(0, 0, 16, 16)) 'draw Icn 32x32 image scaled down to 16x16 Bmp.
g.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!).
IcnBmp.Dispose() 'this bitmap was instantiated, so dispose of it.
Return Bmp 'return the new bitmap image.
End If
Catch 'let errors fall below.
End Try
Return Nothing 'report failure so a default image can be used.
End Function

The other problem we face is that a lot of files do not have icons, such as a picture, document, or font.
Many of these are opened by other files, so in those cases we can access its file association. For
example, Notepad might be associated with a text file (type extension .txt). We can use the
GetSelectedOpenerForExt method from Black Book Tip # 36 to acquire the path to the application
associated with a file extension. With that method, if the user had personally selected an application to
open a file type with, the path to that application will be returned. If the user has not designated an
application to open it, then if there is a default application associated with it, the path to that program
will be returned instead. If no applications are yet associated with the file type, then we can detect that
by the method returning a path of Nothing. For example:
Dim Associate As String = GetSelectedOpenerForExt(FilePath) 'get path to application associated with FilePath.

We could then extract an image using the above GetImageFromExeDll if the association exists.
Sometimes there are no applications associated with a file because it is either not practical, the user has
not yet selected a default application for that type, or an application that is designed to work with that
file type has not been installed. For those, we should have a default generic ‘File’ image set aside.
In any case, there are a number of images that we can reserve for files we may choose not to extract
images for, such as fonts, compressed files, and others that should act as substitutes in case those files
lack a default association, or they lack extractable icons. For example, some DLL files contain icons, but
a great many do not. Technically, a DLL is an EXE file, though it is usually a provider that has no stand-
alone functionality, but is a class library exposing method and property members that act to service other
applications that are able to exploit them. An example is the Moreicons.dll in C:\Windows\System32,
which is a common storage locker for many common system icons. Some EXE files also lack icons, or
they have types that typical interop services cannot manage, such as icons having dimensions beyond
48x48. Even some cursors cannot load without excessive effort that is really not worth all that fuss.
And there is the issue with some rescaled 32x32-pixel images looking like crap at 16x16. If we can, the
best recourse is to do just a little extra work and grab pristine 16x16 images directly from the file.
The time-tested interop Pinvoke that is most-often used to access icons in executables, DLLs, and single
Icon files has been ExtractIconEx. This method can obtain access to an icon or list of icons, either
16x16 (small), 32x32/48x48 (large), or both, and we can also use this function to get the number of
icons that are embedded within the file. We can then employ the DrawIconEx Pinvoke to draw an icon
to an image object (it can also be used to draw a cursor, though using a cursor’s Draw or
DrawStretched methods are easier to use). The trick here, or so I am told, is to get them to work.
Actually, doing this is quite easy. The problem has been that most who I have helped were not paying
attention to the fact that some of the P.Invoke parameters must be passed ByRef, not ByVal.
Consider these highly detailed and powerful definitions for ExtractIconEx and DrawIconEx:
 Page 667

'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'INTEROP METHOD: ExtractIconEx(lpszFile,nIconIndex,*phiconLarge,*phiconSmall,nIcons) As Int32
' Creates an array of handles to large or small icons extracted from the specified executable file, DLL, or icon file.
'
'PARAMETERS:
'lpszFile [in, ByVal], Type: String:
' The name of an executable file, DLL, or icon file from which icons will be extracted.
'
'nIconIndex [in, ByVal], Type: Int32:
' The zero-based index of the first icon to extract. For example, if this value is zero, the function extracts
' the first icon in the specified file. If this value is –1 and phiconLarge and phiconSmall, described below, are
' both IntPtr.Zero, the function returns the total number of icons in the specified file. If the file is an executable
' file or DLL, the return value is the number of RT_GROUP_ICON resources. If the file is an .ico file, the return value
' is 1. If this value is a negative number and either phiconLarge or phiconSmall is not IntPtr.Zero, the function
' begins by extracting the icon whose resource identifier is equal to the absolute value of nIconIndex. For example,
' use -3 to extract the icon whose resource identifier is 3.
'
'phiconLarge [out, optional, ByRef], Type: IntPtr() or IntPtr or IntPtr.Zero:
' A single IntPtr or a sized IntPtr array for icon handles that receives handles to the large icons extracted from
' the file. If this parameter is IntPtr.Zero, no large icons (32x32, 48x48) are extracted from the file.
'
'phiconSmall [out, optional, ByRef], Type: IntPtr() or IntPtr or IntPtr.Zero:
' A single IntPtr or a sized IntPtr array for icon handles that receives handles to the small icons extracted from
' the file. If this parameter is NULL, no small icons (16x16) are extracted from the file.
'
'nIcons [in, ByVal], Type: UInt32:
' The number of icons to be extracted from the file.

'Private Declare Function ExtractIconEx Lib "shell32.dll" Alias "ExtractIconExA" (


ByVal lpszFile As String,
ByVal nIconIndex As Int32,
ByRef phiconLarge As IntPtr,
ByRef phiconSmall As IntPtr,
ByVal nIcons As UInt32) As Int32
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'INTEROP METHOD: DrawIconEx(hdc,xLeft,yTop,hIcon,cxWidth,cyHeight,istepIfAniCur,hbrFlickerFreeDraw,diFlags) As Boolean
'Draws icon or cursor into specified device context using specified raster op, and sizing icon or cursor as specified.
'PARAMETERS:
''hdc [in, ByVal], Type: IntPtr:
' A handle to the device context into which the icon or cursor will be drawn. Use the GetHdc property from your
' Graphics interface object for this.
'
'xLeft [in, ByVal], Type: Int32:
' The logical x-coordinate of the upper-left corner of the icon or cursor.
'
'yTop [in, ByVal], Type: In32:
' The logical y-coordinate of the upper-left corner of the icon or cursor.
'
'hIcon [in, ByVal], Type: IntPtr:
' A handle to the icon or cursor to be drawn. This parameter can identify an animated cursor.
'
'cxWidth [in, ByVal], Type: IntPtr:
' The logical width of the icon or cursor. If this parameter is zero and the diFlags parameter is DI_DEFAULTSIZE,
' the function uses the SM_CXICON system metric value to set the width. If this parameter is zero and
' DI_DEFAULTSIZE is not used, the function uses the actual resource width.
'
'cyHeight [in, ByVal], Type: Int32:
' The logical height of the icon or cursor. If this parameter is zero and the diFlags parameter is DI_DEFAULTSIZE,
' the function uses the SM_CYICON system metric value to set the height. If this parameter is zero and
' DI_DEFAULTSIZE is not used, the function uses the actual resource height.
'
'istepIfAniCur [in, ByVal], Type: UInt32:
' The index of the frame to draw, if hIcon identifies an animated cursor. This parameter is ignored if hIcon does
' not identify an animated cursor.
'
'hbrFlickerFreeDraw [in, optional, ByVal], Type: IntPtr:
' A handle to a brush that the system uses for flicker-free drawing. If hbrFlickerFreeDraw is a valid brush
' handle, the system creates an offscreen bitmap using the specified brush for the background color, draws the
' icon or cursor into the bitmap, and then copies the bitmap into the device context identified by hdc. If
' hbrFlickerFreeDraw is NULL, the system draws the icon or cursor directly into the device context.
'
'diFlags [in, ByVal], Type: UInt32:
' The drawing flags. This parameter can be one of the following values.
' --Value-- --Meaning--
' DI_MASK (1) Draws the icon or cursor using the mask.
' DI_IMAGE (2) Draws the icon or cursor using the image.
' DI_NORMAL (3) Combination of DI_IMAGE and DI_MASK.
' DI_COMPAT (4) This flag is ignored.
' DI_DEFAULTSIZE (8) Draws the icon or cursor using the width and height specified by the system metric
' values for icons, if the cxWidth and cyHeight parameters are set to zero. If this flag
' is not specified and cxWidth and cyHeight are set to zero, the function uses the actual
' resource size.
' DI_NOMIRROR (16) Draws the icon as an unmirrored icon. By default, the icon is drawn as a mirrored icon
' if hdc is mirrored.
 Page 668

Private Declare Function DrawIconEx Lib "user32" (ByVal hdc As IntPtr,


ByVal xLeft As Int32,
ByVal yTop As Int32,
ByVal hIcon As IntPtr,
ByVal cxWidth As Int32,
ByVal cyHeight As Int32,
ByVal istepIfAniCur As Int32,
ByVal hbrFlickerFreeDraw As IntPtr,
ByVal diFlags As Int32) As Boolean

Private Const DI_Mask As Integer = 1 'Draws the icon or cursor using the mask.
Private Const DI_Image As Integer = 2 'Draws the icon or cursor using the image.
Private Const DI_NORMAL As Integer = 3 'Combination of DI_IMAGE and DI_MASK.
'Private Const DI_COMPATL As Integer = 4 'This flag is ignored.
Private Const DI_DEFAULTSIZE As Integer = 8 'Draw icon or cursor using default system sizes.
Private Const DI_NOMIRROR As Integer = 16 'Draws the icon as an unmirrored icon even if HDC is mirrored.

Now, suppose that we have a string variable named FilePath that contains the path to an executable. To
get the number of icons it contains, we could use the ExtractIconEx method like this:
Dim iconCount As Int32 = ExtractIconEx(FilePath, -1&, IntPtr.Zero, IntPtr.Zero, 0&)

To grab only the first icon handle from an executable for a 16x16 image, we could use this:
Dim hIcon As IntPtr 'icon handle. Used when extracting a single icon from a file.
ExtractIconEx(FilePath, 0&, IntPtr.Zero, hIcon, 1&) 'get handle of 1st (0) icon (small 16x16) (count = 1) to hIcon.
' Target^ Index^ Null^ Small Rcvr^ ^Count

Once we have the handle for the icon, we need to copy its icon image to a bitmap/image. This is where
most programmers get into trouble. This issue usually stems from them forgetting to clear resources, in
particular the Graphics interface object. Consider this working sample method that performs this task:
'*******************************************************************************
' Method : GetIconImage
' Purpose : Get an image from the EXE/DLL file icon.
'*******************************************************************************
Friend Function GetIconImage(ByVal FilePath As String, Optional ByVal Index As Int32 = 0) As Bitmap
Dim bmp As Bitmap
Dim g As Graphics

If ExtractIconEx(FilePath, -1&, IntPtr.Zero, IntPtr.Zero, 0&) <> 0 Then 'if it contains icons...
Dim hIcon As IntPtr 'icon handle. Used when extracting icons from executables.
ExtractIconEx(FilePath, Index, IntPtr.Zero, hIcon, 1&) 'get handle of file's first or indexed icon (small 16x16).
If CBool(hIcon) Then 'if it has a handle...
bmp = New Bitmap(16, 16) 'instantiate a new 16x16-pixel drawing surface.
g = Graphics.FromImage(bmp) 'instantiate a graphics interface for that surface.
Dim Result As Boolean = DrawIconEx(g.GetHdc,
0&, 0&, hIcon,
16, 16,
0&, IntPtr.Zero,
DI_NORMAL) 'draw icon to bitmap (DI_NORMAL is an Integer value of 3).
g.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!).
If Result Then 'if the image was drawn...
Return bmp 'return the bitmap.
Else
bmp.Dispose() 'otherwise dispose of bitmap resource.
End If 'and fall below.
End If
End If
'---------------------------------------------------------------
''failed, so try grabbing a 32x32 icon that is the default associated with the file and resize it to 16x16.
'---------------------------------------------------------------
Try
bmp = Icon.ExtractAssociatedIcon(FilePath).ToBitmap 'failed, so try grabbing a 32x32 bitmap of associated icon.
If bmp IsNot Nothing Then 'if we got something...
Dim bm As New Bitmap(16, 16) 'instantiate a new 16x16-pixel drawing surface.
g = Graphics.FromImage(bm) 'instantiate a graphics interface for that surface.
g.DrawImage(bmp, New Rectangle(0, 0, 16, 16)) 'draw 32x32 image scaled down to 16x16.
g.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!).
Return bm 'return the bitmap .
End If
Catch 'let errors fall below.
End Try
Return Nothing 'report failure so default image will be used.
End Function

If the ExtractIconEx function returns a valid icon handle ( hIcon <> 0), then we create a destination bitmap
surface that is 16x16-pixels. We then create a Graphics Interface object for it named g. Next, we use the
DrawIconEx Pinvoke to draw the icon image to the bitmap. Following that, we dispose of the Graphics
Interface object’s resources. This is where most everyone gets into trouble, because if we do not dispose of it,
 Page 669

the GDI drawing will be lost because the graphics object is still managing it before the Garbage Collector
can get to it. Indeed, after disposing of g, if we created a brand new graphics interface for the image, the
modified image would become the new validated default. This is how the AutoRedraw feature works in a
VB6 PictureBox. If the drawing succeeded (its result was True), then we return the bitmap, which the
invoker can assign to the Image property of a picture box. If it failed, we fall back to using the
ExtractAssociatedIcon method to grab a 32x32 image and scale it down to 16x16. If that also fails, we
dispose of the bitmap image resource and return with Nothing.
Indeed, many programmers simply forgo the time-tested ExtractIconEx Pinvoke and use the Icon
object’s ExtractAssociatedIcon method exclusively. This is fine and it is also simpler, now that we
know how to rescale them from the 32x32-pixel original it returns to us. I still use the ExtractIconEx
method because images defined at 16x16 are generally much crisper than re-scaled 32x32 images.
Of course, this can all fail if we are trying to deal with Vista-style icons that contain 64x64, 128x128,
and 256x256 icons. Extracting those requires much more work, but we are trying to display icons as
16x16 images in a TreeView, so we should not have to worry about them, and I will not deal with it here.
However, if we fail to secure an image, we will need to fall back on a default image to display.
In my image list, I added default 16x16 images for Cursors, EXEs,
(Generic) Files, Fonts, TrueType Fonts, Icons/Images, Zipped, video,
audio, DLLs, DOS, RegEditing, Binary, and Log applications.
As we parse each file in a folder, we start with an absolute generic default image, which is File. We then
grab the extension of each file and look for specific matches. If we find an extension match, we should
immediately replace the reference to the generic default with a reference to our less generic image
defined for that extension type. Sometimes that is all we need to do, such as for fonts and compressed
files. Otherwise, we must try to extract an icon from them, grab an icon from an associated executable,
extract a cursor image, or whatever we can do to graphically represent the file.
Getting a file’s extension is easy: “Dim Ext As String = IO.Path.GetExtension(FilePath).ToLower”.

This will return the extension with a leading dot, such as “.txt”, if an extension exists. We can then do a
Select Case and test for various Extensions. For example, consider this extension testing prototype:
If (GetAttr(FilePath) And
(FileAttribute.Hidden Or FileAttribute.System)) <> 0 Then 'if this is not an acceptable file...
ExtIdx = Images.None 'hide image.
ElseIf Not String.IsNullOrWhiteSpace(Ext) Then 'else if it has an extension...
Select Case Ext 'check extension.
Case ".ico" 'ICON.
ExtIdx = Images.IconFile 'init default Icon image.
' Add image from icon to our image list code goes here.
Case ".pic", ".png", ".bmp", ".gif", ".jpg", ".jpeg", ".pic", ".tiff", ".dib"
ExtIdx = Images.IconFile 'init default Icon image.
' Add default or associated image from the file to our image list code goes here.
Case ".mov", ".bic", ".flv", ".avi", ".mpg", ".mpg4", ".mpeg", ".mpeg4", ".asf", "wmv", ".swf", “.divx”
ExtIdx = Images.MovieFile 'init default Movie image.
' Add default or associated image from the file to our image list code goes here.
Case ".zip", ".7z", ".chm", ".iso", ".tar", ".ar", ".arj", ".cab", ".ntfs"
ExtIdx = Images.ZipFile 'init default for compressed file.
'Add default or associated image from the file to our image list code goes here.
Case ".mp3", ".wav", ".aac", ".wma" 'audio file.
ExtIdx = Images.SoundFile 'init default sound image.
'Add default or associated image from the file to our image list code goes here.
Case ".ani" 'animated cursor.
ExtIdx = Images.CursorFile 'set default image (do nothing else).
Case ".cur" 'cursor.
ExtIdx = Images.CursorFile 'init default cursor image.
' Add default or image from the cursor file to our image list code goes here.
Case ".fon" 'font.
ExtIdx = Images.FontFile 'set default image (do nothing else).
Case ".ttf" 'TrueType font.
ExtIdx = Images.TtfFile 'set default image (do nothing else).
Case ".bat", ".com" 'Batch, Command file.
ExtIdx = Images.ExeFile 'set default image (do nothing else).
Case ".reg" '--------------------------------------------Registry file.
ExtIdx = Images.RegFile 'set default image (do nothing else).
Case ".dll" '--------------------------------------------DLL.
 Page 670

ExtIdx = Images.DllFile 'init default EXE image (faster if we use the default).
' Add default or image from the file to our image list code goes here.
Case ".bin", ".dat", ".ocx", ".oca", ".dep", ".nls" '----Binary/Data file.
ExtIdx = Images.BinFile 'set default image.
' Add default or associated image from the file to our image list code goes here.
Case ".log", ".ini", ".cfg" '----------------------------LOG/INI file.
ExtIdx = Images.LogFile 'set default image.
'Add default or associated image from the file to our image list code goes here.
Case ".exe" 'executable.
ExtIdx = Images.ExeFile 'init default EXE image.
' Add default or image from the file to our image list code goes here.
Case Else 'process undetected types.
' Add default or associated image from the file to our image list code goes here.
End Select
End If

This coding prototype will invoke a number of yet unnamed methods, but we will list those shortly.
Notice, though, that Link files (shortcuts) are ignored here. They are a wrinkle that we will deal with,
but for the moment we will focus on normal files. Notice also that the above extension tests are general
and we may want to augment them to include a favorite format. What is important is the ExtIdx Integer
variable, which is an index into an ImageList repository for various images collected from files. It will
contain the list of our default general images, plus any we choose to add to it as we process files. When a
new image is generated, this variable will be updated to the index to the new image added to the list.
However, if it already exists in the list, it should not re-add a duplicate image, but instead grab the index
to the image for that file already stored in the collection.
As we can also see, default image types are referenced through an enumeration named Images. This is a
simple list; much of it should already be familiar because we have used a similar list previously:
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'Enumerator: Images
'Purpose : Reference to images in the ImageList.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Friend Enum Images As Int32
None = -1
Fixed '---- Logical Drive Types.
CDRom
Removable
Ram
Network
Desktop '---- Virtual Drive Types.
Documents
Downloads
Music
Pictures
Videos
Recent
FolderCanOpen '----- Folder Images.
FolderIsOpen
FolderSide
FolderSideShortcut
CursorFile '----- Default File types (NEW STUFF).
ExeFile
FileFile
FontFile
TtfFile
IconFile
ZipFile
MovieFile
SoundFile
DllFile
DosFile
RegFile
BinFile
LogFile
_LastItem '---- Mark end of this list, where new indexes can be dynamically added to our ImageList.
End Enum

This enumeration will also provide image indexing support for a Drive Selector ComboBox and
Directory TreeView, much as we have used previously, as demonstrated in our last Black Book Tip (we
will soon be provided with the definitions for all these images).
 Page 671

As mentioned, the only real snag we can run into when processing images for files are Link Files, or
better known as Shortcut Files, which have a typically hidden “.lnk” file extension. We can get the file
this link actually points to by using the short method GetShortcutLinkToPath, provided in Black Book
Tip # 3 on page 455 (Get the Linked File Path from a Shortcut File). This part is no big deal. What adds
a bit of complexity is the fact that a Link file can point to a file or to a directory folder. Also, sometimes
their target path no longer exists because the target may have been deleted, moved, or renamed.
Additionally, it would be nice to tag them with a little “shortcut tag” in our list. For example:
The tag is actually a small 8x8-pixel image, defined like so:
'--------
'ShortcutTag (special 8x8-pixel image).
'--------
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACgSURBVDhPnZPRDYQgEEStjcr4oiAqoBcL4e6ZPBMX8Twm" &
"GZddh2EhsH3RV1lrJW49pdRLKQdzzud4RvQXA4r/AP2tQWvtsQP+A8YXAyYBIvmMUTcYsIKrUXOv8qdB" &
"hEL52sCzkHaijvhooCBOiPntGTCWnroT1JkPBlEoYt38NGCPFKUrCzuzPtwDiegNpgaxk0jvyPAWVnkY" &
"8Fnlvu/9AwNtbbu4Ug9YAAAAAElFTkSuQmCC"
Dim ShortcutTag As Image = ConvertBase64ToImage(strImg) 'grab image from string data.

Here, we have already used ConvertBase64ToImage in recent Black Book tips, to include the last one.
Also, ShortcutTag should actually be defined as a field in the header of our program.
We can paint this small image to the bottom left corner of a 16x16-pixel image. Painting this is also very
easy and is much like we had painted a 32x32-pixel icon image to a 16x16-pixel bitmap, except that we
will not rescale and we will use a target rectangle pointing to the lower-left corner of the bitmap.
Consider the AddRealImage method that will paint a shortcut tag to a copy of an existing image:
'*********************************************************************************
' Method : AddRealImage (Support) (You may soon understand its naming. ShortcutTag was defined earlier as an image.)
' Purpose : Add a new image to the ImageList and set the updated image index to ExtIndex. If it is a link, also add a tag.
'*********************************************************************************
Private Sub AddRealImage(ByRef Img As Image, ByVal IsLink As Boolean, ByRef ExtIndex As Int32)
If IsLink Then 'if we are processing a shortcut link...
Dim bmp As New Bitmap(Img, 16, 16) 'instantiate a copy of the provided image.
Dim g As Graphics = Graphics.FromImage(bmp) 'instantiate a graphics interface for the copy.
g.DrawImage(ShortcutTag, New Rectangle(0, 8, 8, 8)) 'draw shortcut on bitmap in its bottom-left corner.
g.Dispose() 'dispose of g resource (DO THIS OR LOSE IMAGE).
Img = bmp 'update the referenced image.
End If 'NOTICE above that we can just assign a Bitmap object to an Image object without casting it.
ExtIndex = Me.myImages.Images.Count 'update associated extension index to image index.
Me.myImages.Images.Add(Img) 'then add the image to the ImageList at that index.
End Sub

Notice we do not paint directlu to the Img object, but we replace it with a new bitmap. Were we to draw to
the Img object, its Graphics provider continues to exist, which means it will refresh back to its original
image because background painting events will in time be fired when the app’s message queue becomes idle
and the GDI (Graphical Display Interface) changes (our drawing) will be discarded. Therefore, we dispose
of our copy’s graphics provider before assigning it to the image (Image and Bitmap types are identical),
replacing the original Img object. We next assign the file a new index by grabbing the Imagelist’s current
image count, and then append the image to the list, which also inserts the image at that index.
This brings us to another issue, which is how to prevent adding a ton of duplicate images to the list. For
example, opening a folder full of text files that are all associated with Notepad or our favorite editor. We
would not want to fill the ImageList up with a lot of identical images for our associated application.
Also, what of additional images that we want to have a shortcut tag painted on them?
This is easy to deal with. After considering the creation of a class, I realized that a simple SortedList collection
could easily provide this service. Using its IndexOfKey method we can check for duplicates. By storing files
paths from which images were added, and to include those with shortcut tags added ( just add a “.lnk” extension
to the path to flag them), we can detect previous additions. And, when matches are found, instead of storing a new
image to the ImageList, we can extract the index of the image stored with the filepath in the SortedList.
 Page 672

The great thing with a sorted list is that we do not even need to attach it to the form ( well, technically,
we could do the same for a sorted ListBox). All we need to do is maintain a reference to the object. For
example, in the heading of our form code, we could add these declarations:
Private myImages As New ImageList 'ImageList control to store 16x16 images of icons in this form.
Private AddedExtList = New SortedList(Of String, String) 'keep track of file extensions with application associations.
Private Me.ImgSrcList = New SortedList(Of String, Int32) 'keep track of file paths from which we extracted images.Private
AddedBase As Int32 'base offset to top of default iamges, where file images can append.

MyImages is our ImageList that will maintain all our images. We define it this way to both avoid
dropping a control on the form and to simplify coding at the cost of a little setting-up.
AddedExtList is SortedList, still to be covered, which will be used to keep track of file extensions that
an associated application has been found for. It stores the extension as the key and the full path to the
associated application as its value member. Using this can bypass the need to parse the same file
extension again and again for an association. Later, we will enhance it so that extensions that do not
have any application association can be added to the list, to avoid reparsing them time and again.
ImgSrcList is a SortedList, used to store a list of files we extracted an image from and their ImageList index.
This would include not only EXEs and DLLs, but icons, cursors, and the images from executables associated
with files. It stores the lowercase app filepath as its key, and its image index as the list’s value member.
The AddedBase integer variable will contain an index within the myImages ImageList beyond the
default images, marking where new file images can be appended to the list.
We first load our ImageList with default images. Once done, we can add shortcut tagged versions for our
default images. For example, if our ShortcutTag Image variable has been assigned its image:
'---------------------------------------------------------------------------------
'Build shortcut versions of defaults in myImages.
'---------------------------------------------------------------------------------
For Idx As Int32 = Images.CursorFile To Images.DosFile 'duplicate CursorFile to DosFile with a shortcut version.
Img = New Bitmap(Me.myImages.Images(Idx), 16, 16) 'create a duplicate of an existing image in myImages.
AddRealImage(Img, True, (Idx)) 'add it as a shortcut-tagged version (pass Idx ByVal, not ByRef).
Next
AddedBase = Me.myImages.Images.Count 'set index base for addl. images referenced in ImgSrcList collection.

Notice in the invocation to AddRealImage, the Index parameter, Idx, was actually passed ByVal, even
though the method expects it to be passed ByRef. This is because we do not want the method to alter our
Idx variable, which would mess up our loop indexing, so we embraced it within parentheses to actually
pass a copy (a clone) of that variable ByRef to the method. This way the copy will be altered instead and
it will be thrown away on return, rather than altering the actual Idx variable (technically, adding the
parentheses causes it to be treated as a numeric expression, which returns a result equal to Idx).
In the coding prototype listing shown earlier, we indicated that we add images from the files or from
associated files. This can be achieved by invoking one of two methods: AddAssociatedImage and
AddImageFromFile. These in turn invoke support methods to achieve all of their magic:
'*********************************************************************************
' Method : AddAssociatedImage
' Purpose : Add an image from a file associated with the specified file, if one exists. ExtIndex gets ImageList index of icon.
'*********************************************************************************
Private Sub AddAssociatedImage(ByVal FilePath As String, ByVal IsLink As Boolean, ByRef ExtIndex As Int32)
Dim Associate As String = GetSelectedOpenerForExt(FilePath) 'is there an exe associated with this file?
If Not String.IsNullOrWhiteSpace(Associate) Then 'if there is an associated file...
AddImageFromFile(Associate, IsLink, ExtIndex) 'add associated image.
End If 'if this fails, we will use the default image.
End Sub 'NOTE: We will modify this method later to take advantage of AddedExtList to really speed associations up.

'*********************************************************************************
' Method : AddImageFromFile
' Purpose : Adds an image from the file and update the Item's image index, or
' : the path already is registered, so set the image index to it.
'*********************************************************************************
Private Function AddImageFromFile(ByVal FilePath As String, ByVal IsLink As Boolean, ByRef ExtIndex As Int32) As Boolean
FilePath = FilePath.ToLower 'make sure FilePath is lowercase.
Dim fp As String = FilePath 'init a match of FilePath.
If IsLink Then 'is it a link file?
fp &= ".lnk" 'yes, so append a .lnk extension to the copy.
 Page 673

End If
'---------------------------------------------------------------
If Me.ImgSrcList.IndexOfKey(FilePath) = -1 Then 'if the item does not yet exist in our list...
Dim Img As Image 'init image to set and assign to the list.
If IO.Path.GetExtension(FilePath) = ".cur" Then 'if it is a cursor...
Img = GetCursorImage(FilePath) 'then load the cursor's image (this is faster).
Else
Img = GetIconImage(FilePath) 'else load the image from a file (EXE, DLL, Icon).
End If
If AddImage(Img, IsLink, ExtIndex) Then 'update the ImageList if an image was grabbed.
Me.ImgSrcList.Add(fp, ExtIndex) 'if an image was added, then add fp to ImgSrcList.
Return True 'and indicate an image was added.
End If
Else
ExtIndex = Me.ImgSrcList.Item(FilePath) 'otherwise, grab assigned index.
End If 'if this fails, we will use the default image.
Return False 'indicate an image was NOT added.
End Function
'*********************************************************************************
' Method : AddImage (Support Method)
' Purpose : Add an image to the ImageList and set the image index. If it is a link, add a tag.
'*********************************************************************************
Private Function AddImage(ByRef Img As Image, ByVal IsLink As Boolean, ByRef ExtIndex As Int32) As Boolean
If Img IsNot Nothing Then 'if image is valid...
AddRealImage(Img, IsLink, ExtIndex) 'then add it.
Return True
ElseIf IsLink AndAlso ExtIndex < Images._LastItem Then 'else compute the index of the...
ExtIndex = ExtIndex + Images._LastItem - Images.CursorFile 'shortcut rendition of base icon.
End If
Return False 'no image added. Use ExtIndex value.
End Function

'*********************************************************************************
' Method : AddRealImage (Support Method) (ShortcutTag was defined earlier as an image.)
' Purpose : Add a new image to the ImageList and set the image index. If it is a link, add a tag.
'*********************************************************************************
Private Sub AddRealImage(ByRef Img As Image, ByVal IsLink As Boolean, ByRef ExtIndex As Int32)
If IsLink Then 'if we are processing a shortcut link...
Dim bmp As New Bitmap(Img, 16, 16) 'instantiate a copy of the provided image.
Dim g As Graphics = Graphics.FromImage(bmp) 'instantiate a graphics interface for the copy.
g.DrawImage(ShortcutTag, New Rectangle(0, 8, 8, 8)) 'draw shortcut on bitmap in its bottom-left corner.
g.Dispose() 'dispose of graphics resource.
Img = bmp 'update the referenced image.
End If
ExtIndex = Me.myImages.Images.Count 'update extension index in image list.
Me.myImages.Images.Add(Img) 'then add the image to the image list.
End Sub

With these methods, we can begin fleshing out our previous prototype extension testing code, like so:
If (GetAttr(FilePath) And
(FileAttribute.Hidden Or FileAttribute.System)) <> 0 Then 'if this is not an acceptable file...
ExtIdx = Images.None 'hide image.
IsLink = False 'make sure flag is reset.
ElseIf Not String.IsNullOrWhiteSpace(Ext) Then 'else if it has an extension defined...
Select Case Ext 'check extension.
Case ".ico" '--------------------------------------------ICON.
ExtIdx = Images.IconFile 'init default Icon image.
AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab first icon from file and update image.
Case ".pic", ".png", ".bmp", ".gif", ".jpg",
".jpeg", ".pic", ".tiff", ".dib" '------------------IMAGE.
ExtIdx = Images.IconFile 'init default Icon image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab associated file icon and update the image list.
Case ".mov", ".bic", ".flv", ".avi", ".mpg", ".mpg4", ".mpeg",
".mpeg4", ".asf", "wmv", ".swf", “.divx” '-----------VIDEO.
ExtIdx = Images.MovieFile 'init default Movie image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab associated file icon and update the image list.
Case ".zip", ".7z", ".chm", ".iso", ".tar",
".ar", ".arj", ".cab", ".ntfs" '--------------------Compressed files (ZIP).
ExtIdx = Images.ZipFile 'init default for compressed file (do nothing else).
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab associated file icon and update the image list.
Case ".mp3", ".wav", ".aac", ".wma" '--------------------AUDIO.
ExtIdx = Images.SoundFile 'init default image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab associated file icon and update the image list.
Case ".ani" '--------------------------------------------animated cursor.
ExtIdx = Images.CursorFile 'set default image (do nothing else).
Case ".cur" '--------------------------------------------CURSOR.
ExtIdx = Images.CursorFile 'init default cursor image.
AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab cursor image from file and update the image list.
Case ".fon" '--------------------------------------------FONT.
ExtIdx = Images.FontFile 'set default image (do nothing else).
Case ".ttf" '--------------------------------------------TrueType font (TTF).
ExtIdx = Images.TtfFile 'set default image.
Case ".bat", ".com" '------------------------------------Batch, Command file (DOS).
 Page 674

ExtIdx = Images.ExeFile 'set default image (do nothing else).


Case ".reg" '--------------------------------------------Registry file.
ExtIdx = Images.RegFile 'set default image (do nothing else).
Case ".dll" '--------------------------------------------DLL.
ExtIdx = Images.DllFile 'init default EXE image (faster if we use the default).
AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab first icon from file and update the image list.
Case ".bin", ".dat", ".ocx", ".oca", ".dep", ".nls" '----Binary/Data file.
ExtIdx = Images.BinFile 'set default image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab associated file icon and update the image list.
Case ".log", ".ini", ".cfg" '-----------------------------LOG/INI file.
ExtIdx = Images.LogFile 'set default image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab associated file icon and update the image list.
Case ".exe" '--------------------------------------------EXE.
ExtIdx = Images.ExeFile 'init default EXE image.
AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab first icon from file and update the image list.
Case Else '----------------------------------------------process undetected types.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab associated file icon and update the image list.
End Select
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'add a file node to our TreeView, tvDirTree.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If ExtIdx <> Images.None Then 'if we have an image reference...
If Not IsLink Then 'if this is NOT a linked file...
LnkPath = FilePath 'simplify the code below by assuming it is.
End If
With Me.tvDirTree
Dim dirNode As TreeNode =
parentNode.Nodes.Add(parentNode.GetNodeCount(False).ToString,
IO.Path.GetFileName(LnkPath), ExtIdx, ExtIdx) 'add as new file node w/appropriate index.
dirNode.ToolTipText = FilePath 'save its path as its tooltip.
End With
End If

Notice that we also appended code to add a node to the TreeView in the listing.
But as we see from the above, if we have a linked file, it seems we have already grabbed the path the
link file points to and have by then determined that it is in fact a file and is valid. Also, a Boolean flag,
IsLinked, is set to True if it is in fact a linked file. We also would have to maintain a reference to the link
file itself, which is provided by the string variable LnkPath.
We can preprocess the files and check for linked files by preceding the above with this block of code:
IsLink = False 'init flag to not being a link.
Dim ExtIdx As Int32 = Images.FileFile 'init default image to File.
Dim Ext As String = IO.Path.GetExtension(FilePath).ToLower 'get the file extension.
If Ext = ".lnk" Then 'if this is a link file...
Dim linkedPath As String = GetShortcutLinkToPath(FilePath) 'get the link's path to its target.
If linkedPath IsNot Nothing Then 'if the linked path exists (should)...
If IO.Directory.Exists(linkedPath) Then 'if it is actually a directory folder...
linkedPath = Nothing 'kill linked path.
ElseIf Not IO.File.Exists(linkedPath) Then 'if it is a file and it no longer exists...
linkedPath = Nothing 'kill linked path.
End If
End If
If linkedPath IsNot Nothing Then 'if a valid file path was returned...
IsLink = True 'mark it as a link.
LnkPath = FilePath 'save full path to lnk file.
FilePath = linkedPath 'let file assume linked Path for now.
Ext = IO.Path.GetExtension(linkedPath).ToLower 'get the actual target file's extension.
Else
ExtIdx = Images.None 'ignore if linked path is nothing.
Ext = Nothing
End If
End If

We can then wrap the above two blocks of code within a body that parses a string array named Files,
which is a listing of all files retrieved from a selected directory path:

Dim IsLink As Boolean 'set to True if a file is a shortcut link.


Dim LnkPath As String = Nothing 'path of .lnk file when IsLink is set to True.
For Each FilePath As String In Files 'parse each file in folder...
'-------------------------------
'Above FilePath test code goes here...
'-------------------------------
Next 'FilePath
 Page 675

We now have files taken care of, but directory folders can also have shortcuts. What this means is that we
actually need to process the file list twice. The first time we can fill it with just link files ( Files =
IO.Directory.GetFiles(pPath, "*.lnk")), and then a second time, as we have just covered, with all files
(Files = IO.Directory.GetFiles(pPath)). The pPath string variable contains the parent node’s folder path.
When processing just the link files, we will test for those that only refer to directory folders. We can
store the references to any linked folders in a List Collection. We can parse them to find all actual folders
whose linked paths still exist, and afterward add the collected linked folders to the TreeView.
One other thing we have to look into is something that has driven some programmers batty, and that is
that while parsing a folder from the BeforeExpand event in a TreeView, the program will hang and not
exit if the user tries to exit the application, such as by hitting the “ X” button before the event has
completed. This error occurs because the application is dependant on a sequence of processes within the
TreeView, such as displaying content after the expand, and then triggering an AfterExpand event. The
solution to this issue is to cancel the application exit in the FormClosing event, and then start a 10ms
timer to let things settle before actually closing the form. We can declare a global Boolean flag, such as
“Private _pClosing As Boolean = False” in our form, and then in our Closing event do this:
'*******************************************************************************
' Method : Form_FormClosing
' Purpose : Prepare form for closing.
'*******************************************************************************
Private Sub Form_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
If Me.DirRecursing Then 'are we recursing a directory branch?
LockWindowUpdate(IntPtr.Zero) 'yes, release any restrained paint events.
Me.tmrStart.Enabled = False 'turn off all 3 timers. This one ensures the TreeView will have focus after form loads.
Me.tmrScanning.Enabled = False 'timer used for updating folder/file reports.
Me.tmrEnd.Enabled = False 'timer used when user is trying to close app during directory recursing process.
Me._pClosing = True 'flag indicating that we want to close the app.
e.Cancel = True 'but cancel the actual form close for now...
End If
End Sub

Then, within the DirRecurse directory scanning, which we will soon see, we will check the _pClosing
flag and exit any and all loops as soon as we can. Once we turn the DirRecursing flag off, which is set
before we invoke scanning, we can enable the timer tmrEnd, which will then close the file:
'*******************************************************************************
' Method : tmrEnd_Tick
' Purpose : make sure TreeView has focus.
'*******************************************************************************
Private Sub tmrEnd_Tick(sender As Object, e As EventArgs)
Me.DirRecursing = False 'beat a dead horse to ensure this is disabled.
Me.tmrEnd.Enabled = False 'disable THIS timer.
Me._pClosing = False 'turn off our wanting to close flag.
Me.Close() 'then close shop.
End Sub

The other timers in the above list are useful as well. tmrStart is activated at the end of the Form’s Load
event. It is a simple 10ms timer that gives the display enough time to finish setting up before setting
focus onto the TreeView control, to make sure all controls are “up”. The tmrScanning timer is used
while parsing massive directories. Without the arcane knowledge of File Explorer, we must parse all
files and folders for icons because we do not maintain inner fast-access tables where these items can be
enumerated immediately in the background.
We will seldom see tmrScanning in action, unless we are looking at something with thousands of files,
such as C:\Windows\System32, for example, even though it will already know in a jiffy how many files
there are in a folder, but what takes time is extracting truckloads of images for the various files.
Even so, there are some things we can do to really speed things up. One example that can double the
scanning speed is to use the default image for DLLs. We can do that by simply disabling the invocation
of AddImageFromFile when we detect a “.dll” file extension from the above file testing code:
Case ".dll" '--------------------------------------------DLL.
ExtIdx = Images.DllFile 'init default EXE image (faster if we use the default).
'AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab first icon from file and update the image list.
 Page 676

Still another thing we can do is to ensure that file extensions that do not have programs associated with
them should be added to the extension list anyway, so that we are not wasting time trying time and again
to find a file associated with it. We can do this in the AddAssociatedImage method by ensuring that if an
association is not found, or if the associated file does not have an icon we can access, that we will use
the list without going through that process again. An update to the AddAssociatedImage method is:
'*********************************************************************************
' Method : AddAssociatedImage (Updated, much faster version)
' Purpose : Add an image from a file associated with the specified file, if one exists.
'*********************************************************************************
Private Sub AddAssociatedImage(ByVal FilePath As String, ByVal IsLink As Boolean, ByRef ExtIndex As Int32)
Dim Ext As String = CleanExtension(FilePath).ToLower 'get FilePath extension.
Dim x As String = String.Empty 'init to no additional extension.
If IsLink Then x = ".lnk" ' 'if FilePath is a Link then set additional .lnk extension.
If Me.AddedExtList.IndexOfKey(Ext & x) <> -1 Then 'if the extension list contains a matching extension...
Dim Assoc As String = Me.AddedExtList.Item(Ext & x) 'grab associated file or ExtIndex reference.
If Assoc.Substring(0, 1) = "-" Then 'is it actually a negative integer?
ExtIndex = -CInt(Assoc) 'yes, so grab it as a default extension index.
Else
ExtIndex = Me.ImgSrcList(Assoc) 'else grab link image index associated with it.
End If
Else
Dim Assoc As String = GetSelectedOpenerForExt(Ext).ToLower 'check for an app associated with this file.
If Not String.IsNullOrWhiteSpace(Assoc) Then 'if there is an associated file...
If AddImageFromFile(Assoc, IsLink, ExtIndex) Then 'then add associated image, and if it was added...
Me.AddedExtList.Add(Ext & x, Assoc.ToLower & x) 'then add the extension to to association list.
Return
End If
End If 'if this fails, we will use the default image.
Me.AddedExtList.Add(Ext, (-ExtIndex).ToString) 'failed, so add the default extension as a negative number.
End If
End Sub

In the above code, if we have an extension that has no associated image to assign to it, we add it to
AddExtList, yet another SortedList Collection, anyway, but instead of assigning it a path to the file that
contains the associated image, we assign it the negative value for the default image associated with that
extension. Then, in the code above it, if a match is found for the extension in the list, we assign ExtIndex to
the positive of the negative value. Technically, we can actually be assigning a value to ExtIndex that it might
already contain, but I have been toying with some alternate ideas that could take advantage of this place in
the code, so I will leave it intact, though you are free to simplify it.
Another thing we can do is turn off the Remove Integer Overflow Checks in the Advanced Compile Options
on the Compile tab of the Project Properties. This is off by default in C#, which is why it will run slightly
faster than VB. The only time we really need these overflow checks in place is when we are dealing with
very large integer math and we do not want integers to overflow and roll over into negatives.
Another thing we can do that can triple the program’s speed is to run it without debug (Cntrl+F5, or we can
add a button for Run Without Debug in to our toolbar).
What follows is the complete form code, which includes supporting classes and modules, all in a single
source code file, much as we did in the last Black Book Tip. This thing contains a lot of cool tricks. All we
need to do is create a new Windows Forms Application, and then add a COM reference in our Project
Properties to Microsoft Shell Controls and Automation, and turn its Embed Interop Types option to False.
Finally, overwrite its empty Form1 class shell on Form1’s code page with the file following this
paragraph. We may want to resize the form to start with a bigger surface display. This code will create
all its controls and place them properly on the form, based on how we sized our form. It will also
establish a minimum size in case we went insane and sized it to fit within a postage stamp.
Option Explicit On
Option Strict On
'*************************************************************************************
' frmTreeViewTest Form Class
' TreeView Demonstrations with folders and file icon images
'
' On a new form, add all the components to display a TreeView directory structure of
' a Drive, including images extracted from the files. A ComboBox is provided for
' selecting logical and virtual drives.
'*************************************************************************************
Public Class frmTreeViewTest 'make sure this name matches the form you drop this code into.
 Page 677

'*********************************************************************************
' DECLARATIONS.
'*********************************************************************************
Private _StartPath As String = "C:\" 'path to actually focus on, depending on if a root folder was specified.
Private _Root As String 'root of start folder path. C:\ from C:\Windows\System32, for example.
Private _pNode As TreeNode = Nothing 'base node of recursive scan. Used to count its folders and files.
Private _pDirCnt As Int32 = 0 'subfolder count accumulator when scanning a folder.
Private _pFilCnt As Int32 = 0 'file count accumulator when scanning a folder.
Private _pClosing As Boolean = False 'true when user closing form while scan running.
'---------------------------------------------------
'the following two flags are used to bypass treeview
'display updates when certain processes are being performed.
'---------------------------------------------------
Private ImgInit As Boolean = False 'True when ImageList is initializing.
Private DirRecursing As Boolean = False 'True when we are recursing directories during a scan.
'---------------------------------------------------
'Controls to add to form.
'---------------------------------------------------
Private AddedExtList As SortedList(Of String, String) 'keep track of file extensions with application associations.
Private ImgSrcList As SortedList(Of String, Int32) 'keep track of file paths from which we extracted images.
Private ToolTips As ToolTip 'tooltip control.
Private myImages As ImageList 'ImageList control to store 16x16 images of icons in this form.
Private lblFoldersHdr As Label 'report field header for Folders report.
Private lblFolders As Label 'report number of subfolders found.
Private lblFilesHdr As Label 'report field header for Files report.
Private lblFiles As Label 'report number of files found.
Private cboDrives As ComboBox 'Drive selection ComboBox.
Private tvDirList As TreeView 'Directory Folder/File TreeView.
Private tmrScanning As Timer 'timer used for updating folder/file reports.
Private tmrStart As Timer 'startup timer. Used to set focus to treeview on startup.
Private tmrEnd As Timer 'closing timer. Used when recursing and the user tries to close the form.
' 'The program will crash and loop enlessly if a folder is opening.
' 'then and the user tries to close the form. This will prevent that.
'---------------------------------------------------
Private AddedBase As Int32 'base offset to top of default iamges, where file images can append.
Private ShortcutTag As Image 'reference to an 8x8 shortcut tag image.
'---------------------------------------------------
Private Const EdgeOfst As Int32 = 12 'form client edge offset from top and left for controls.
Private Const MinSizeWd As Int32 = 400 'Minimum Form Width.
Private Const MinSizeHt As Int32 = 300 'Minimum Form Height.
'---------------------------------------------------

'*******************************************************************************
' Method : LockWindowUpdate
' Purpose : The LockWindowUpdate function disables or enables drawing in the
' : specified window. Only one window can be locked at a time.
' : We will use this because SuspendLayout will not stop the treeview
' : display from clearing when it is being reset.
'*******************************************************************************
Private Declare Function LockWindowUpdate Lib "user32.DLL" Alias "LockWindowUpdate" (ByVal hWndLock As IntPtr) As Boolean
'Parameters:
' hWndLock [in, ByVal], Type: IntPtr
' The window in which drawing will be disabled. If this parameter is NULL (IntPtr.Zero), drawing
' in the locked window is enabled.

'Return value
' If the function succeeds, the return value is nonzero (True).
' If the function fails, the return value is zero (False), indicating that an
' error occurred or another window was already locked.

'*******************************************************************************
' Property : MeCursor
' Purpose : Set cursor to more than just the main form.
' Usage : When assigning a new cursor to the form, assign it using: MeCursor = NewCursor.
' : Note also that you can assign from the Cursors collection, or load a cursor from resources.
' : Unlike the UseWaitCursor property. You can afterward reasign child control cursors.
'*******************************************************************************
Private Property MeCursor As Cursor
Get
Return Me.Cursor 'return the form's current cursor setting.
End Get
'------
Set(value As Cursor)
If Me.Cursor <> value Then 'is the form cursor already set to this?
Me.Cursor = value 'no, so set cursor to the main form...
If Me.HasChildren Then 'and if it also has children...
SetCursorToChildren(Me.Controls, value) 'then set its child controls as well.
End If
End If
End Set
End Property

'*******************************************************************************
' Method : SetCursorToChildren
' Purpose : Support MeCursor property
 Page 678

' : Set parent's cursor also to Child controls.


'*******************************************************************************
Friend Sub SetCursorToChildren(ByRef ControlList As Control.ControlCollection, ByRef Value As Cursor)
For Each Cntrl As Control In ControlList 'process all child controls.
If Cntrl.Visible Then 'is the control visible?
Cntrl.Cursor = Value 'set its cursor.
If Cntrl.HasChildren Then 'does it have child controls?
SetCursorToChildren(Cntrl.Controls, Value) 'process its child controls.
End If
End If
Next
End Sub

'*******************************************************************************
' Method : Form_Load
' Purpose : Prepare form for startup.
'*******************************************************************************
Private Sub Form_Load(sender As Object, e As EventArgs) Handles MyBase.Load
With Me
.MinimumSize = New Size(MinSizeWd, MinSizeHt) 'prevent user from sizing form too small.
.ShowIcon = False 'hide any icon on the form.
.Text = "Sample Directory Browser With File Icons" 'give the form a title.
.SizeGripStyle = SizeGripStyle.Show 'show a sizing grip in the bottom-right corner.
End With
'-------------------------------------------------------
'Set up the lists that will keep track of files and extension associations from which icons have been added.
'-------------------------------------------------------
Me.AddedExtList = New SortedList(Of String, String) 'keep track of file extensions with application associations.
Me.ImgSrcList = New SortedList(Of String, Int32) 'keep track of file paths from which we extracted images.
'-------------------------------------------------------
'Set up the ToolTip control.
'-------------------------------------------------------
Me.ToolTips = New ToolTip 'tooltip control.
'-------------------------------------------------------
'Set up the image list
'-------------------------------------------------------
Me.myImages = New ImageList 'ImageList control to store 16x16 images of icons in this form.
InitializeImageList(Me.myImages, True, True) 'initialize ImageList (minimal init).
'-------------------------------------------------------
'Set up report fields.
'-------------------------------------------------------
Me.lblFoldersHdr = New Label 'Folder Header Label, displays "Folders:" at bottom of screen.
With Me.lblFoldersHdr
.Parent = Me 'allow control display by assigning to a displayed component.
.Anchor = AnchorStyles.Left Or AnchorStyles.Bottom 'anchor bias to the bottom-left corner of the form.
.Left = EdgeOfst 'offset from left edge of client area of form.
.Top = Me.ClientSize.Height - .Height 'place at the bottom of the form.
.AutoSize = True 'allow label to auto-size.
.Text = "Subfolders:" 'assign its display text.
End With
'-------------------------------------------------------
Me.lblFolders = New Label 'subfolder count display.
With Me.lblFolders
.Parent = Me 'allow control display by assigning to displayed component.
.Anchor = AnchorStyles.Left Or AnchorStyles.Bottom 'anchor bias to the bottom-left forner of the form.
.Left = EdgeOfst + Me.lblFoldersHdr.Width 'set to right of above label.
.Top = Me.lblFoldersHdr.Top 'align top with previous label.
.AutoSize = True 'allow label to auto-size.
.Text = "0" 'this will be updated to the actual subfolder count.
End With
'-------------------------------------------------------
Me.lblFilesHdr = New Label 'Files Header Label, displays "Files:" at bottom of screen.
With Me.lblFilesHdr
.Parent = Me 'allow control display by assigning to displayed component.
.Anchor = AnchorStyles.Left Or AnchorStyles.Bottom 'anchor bias to the bottom-left corner of the form.
.Left = 120 '1-inch from left.
.Top = Me.lblFoldersHdr.Top 'align top with previous labels.
.AutoSize = True 'allow label to auto-size.
.Text = "Files:" 'assign its display text.
End With
'-------------------------------------------------------
Me.lblFiles = New Label 'File count display.
With Me.lblFiles
.Parent = Me 'allow control display by assigning to displayed component.
.Anchor = AnchorStyles.Left Or AnchorStyles.Bottom 'anchor bias to the bottom-left corner of the form.
.Left = 120 + Me.lblFilesHdr.Width 'set to right of above label.
.Top = Me.lblFoldersHdr.Top 'align top with previous labels.
.AutoSize = True 'allow label to auto-size.
.Text = "0" 'this will be updated to the actual subfile count.
End With
'-------------------------------------------------------
'Set up Scanning Report Timer.
'-------------------------------------------------------
Me.tmrScanning = New Timer 'timer used for updating folder/file reports.
With Me.tmrScanning
 Page 679

.Enabled = False 'make sure presently disabled.


AddHandler .Tick, AddressOf tmrScanning_Tick 'add event handler.
End With
'-------------------------------------------------------
'Set up cboDrive ComboBox control.
'-------------------------------------------------------
Me.cboDrives = New ComboBox
With Me.cboDrives
.Parent = Me 'establish this control's parent.
.Left = EdgeOfst 'align combobox left location.
.Top = EdgeOfst 'align combobox top location.
.Width = Me.ClientSize.Width - EdgeOfst * 2 'fill width of form with spacing.
.Anchor = AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top 'set anchoring.
.ItemHeight = 17 'allow 16x16 pixel icons with a 1-pixel gap.
.DropDownStyle = ComboBoxStyle.DropDownList 'avoid user typing into CBO's text box.
Me.ToolTips.SetToolTip(Me.cboDrives, "Select a different virtual or logical drive")
.DrawMode = DrawMode.OwnerDrawFixed 'enable drawing images into list.
'-----------add event handlers.
AddHandler .DrawItem, AddressOf cboDrives_DrawItem
AddHandler .SelectedIndexChanged, AddressOf cboDrives_SelectedIndexChanged
AddHandler .DropDownClosed, AddressOf cboDrives_DropDownClosed
End With
'-------------------------------------------------------
'Set up tvDirTree TreeView control.
'-------------------------------------------------------
Me.tvDirList = New TreeView
With Me.tvDirList
.Parent = Me 'define control's parent.
.Left = EdgeOfst 'align treeview horizontal position.
.Top = Me.cboDrives.Top + Me.cboDrives.Height 'place below ComboBox.
.Width = Me.cboDrives.Width 'match ComboBox width.
.Height = Me.lblFoldersHdr.Top - .Top - EdgeOfst 'fill rest of form.
.Anchor = AnchorStyles.Left Or AnchorStyles.Right Or
AnchorStyles.Top Or AnchorStyles.Bottom 'anchor bias to all four sides.
.ItemHeight = 18 'allow full images to have a gap between them.
.ShowRootLines = True 'use this so that tabbing aligns.
.ShowPlusMinus = True 'use this so user can click nodes to auto-expand and collapse.
.ShowNodeToolTips = True 'Allow node tooltips to display.
.ShowLines = False 'no need for this option.
.ImageList = myImages 'make sure our ImageList is attached.
.ImageIndex = Images.FolderSide 'default to folder on side.
.HideSelection = False 'do not hide selections.
.DrawMode = TreeViewDrawMode.OwnerDrawAll 'we want to draw overything.
'-----------add event handlers.
AddHandler .AfterSelect, AddressOf tvDirList_AfterSelect
AddHandler .BeforeExpand, AddressOf tvDirList_BeforeExpand
AddHandler .AfterExpand, AddressOf tvDirList_AfterExpand
AddHandler .AfterCollapse, AddressOf tvDirList_AfterCollapse
AddHandler .MouseDoubleClick, AddressOf tvDirList_MouseDoubleClick
AddHandler .DrawNode, AddressOf tvDirList_DrawNode
End With
'-------------------------------------------------------
'populate drive ComboBox and then select current drive.
'-------------------------------------------------------
Me._Root = RemoveSlash(
IO.Directory.GetDirectoryRoot(Me._StartPath)) 'get the Root from the path, less trailing backslash.
With Me.cboDrives
PopulatecboDrives() 'populate the cboDrives list...
For Each di As DriveItem In .Items 'then find the desired Root drive in the list.
If di.Drive.Equals(Me._Root) Then 'if we found it...
.SelectedItem = di 'select it.
Exit For 'and we are done checking the list.
End If
Next
End With
'-------------------------------------------------------
'Set up End Timer control.
'-------------------------------------------------------
Me.tmrEnd = New Timer
With Me.tmrEnd
.Enabled = False 'disable the end timer.
.Interval = 10 'set interval to 10ms.
AddHandler .Tick, AddressOf tmrEnd_Tick 'add event handler.
End With
'-------------------------------------------------------
'Set up Startup Timer control. Used to ensure TreeView will have startup focus.
'-------------------------------------------------------
Me.tmrStart = New Timer
With Me.tmrStart
.Interval = 10 'set interval to 10ms.
AddHandler .Tick, AddressOf tmrStart_Tick 'add event handler.
.Enabled = True 'start the startup timer.
End With
End Sub
 Page 680

'*******************************************************************************
' Method : tmrEnd_Tick
' Purpose : Let the display catch up and events settle down before actually closing form.
'*******************************************************************************
Private Sub tmrEnd_Tick(sender As Object, e As EventArgs)
Me.DirRecursing = False 'beat a dead horse to ensure this disabled.
Me.tmrEnd.Enabled = False 'disable the timer.
Me._pClosing = False 'turn off our flag.
Me.Close() 'then close shop.
End Sub

'*******************************************************************************
' Method : tmrStart_Tick
' Purpose : make sure TreeView has focus.
'*******************************************************************************
Private Sub tmrStart_Tick(sender As Object, e As EventArgs)
Me.tmrStart.Enabled = False 'disable the timer.
Me.tvDirList.Focus() 'then set focus on the treeview.
End Sub

'*******************************************************************************
' Method : Form_FormClosing
' Purpose : Prepare form for shutdown.
'*******************************************************************************
Private Sub Form_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
If Me.DirRecursing Then 'are we recursing a directory branch?
LockWindowUpdate(IntPtr.Zero) 'yes, so release any restrained paint events.
Me.tmrStart.Enabled = False 'turn off all timers.
Me.tmrScanning.Enabled = False
Me.tmrEnd.Enabled = False
Me._pClosing = True 'flag indicating we want to close the form.
e.Cancel = True 'but cancel the actual form close for now...
End If
End Sub

'*******************************************************************************
' Method : Form_FormClosed
' Purpose : Prepare to exit application by disposing of heavy resources.
'*******************************************************************************
Private Sub Form_FormClosed(sender As Object, e As System.Windows.Forms.FormClosedEventArgs) Handles Me.FormClosed
Me.ImgSrcList.Clear() 'keep track of file paths from which we extracted images.
Me.AddedExtList.Clear() 'keep track of file extensions with application associations.
Me.ToolTips.Dispose() 'tooltip control.
Me.lblFoldersHdr.Dispose() 'report field header for Folders report.
Me.lblFolders.Dispose() 'report number of subfolders found.
Me.lblFilesHdr.Dispose() 'report field header for Files report.
Me.lblFiles.Dispose() 'report number of files found.
Me.cboDrives.Dispose() 'Drive selection ComboBox.
Me.tvDirList.Dispose() 'Directory Folder/File TreeView.
Me.tmrScanning.Dispose() 'timer for updating folder/file reports.
Me.tmrStart.Dispose() 'startup timer. Used to set focus to treeview on startup.
Me.tmrEnd.Dispose() 'Let the display catch up and events settle down before actually closing form.
Me.myImages.Dispose() 'ImageList control to store images in this form (dipose of last).
End Sub

'*********************************************************************************
' Method : PopulatecboDrives
' Purpose : Populate the cboDrives list.
'*********************************************************************************
Private Sub PopulatecboDrives()
Me.cboDrives.Items.Clear() 'clear any current list in case re-invoked.
Me.cboDrives.Items.Add(New DriveItem("Desktop")) 'Add Desktop item.
Me.cboDrives.Items.Add(New DriveItem("Documents")) 'Add Documents item.
Me.cboDrives.Items.Add(New DriveItem("Downloads")) 'Add Downloads item.
Me.cboDrives.Items.Add(New DriveItem("Music")) 'Add Music item.
Me.cboDrives.Items.Add(New DriveItem("Pictures")) 'Add Pictures item.
Me.cboDrives.Items.Add(New DriveItem("Videos")) 'Add Videos item.
Me.cboDrives.Items.Add(New DriveItem("RecentPlaces")) 'Add Recent Places item.
'Me.cboDrives.Items.Add(New DriveItem("ThisPC")) 'Add ThisPC item.
'---------------------------------------------------------------------------
Dim Drives() As String = Environment.GetLogicalDrives 'get list of drives.
For Each Drv As String In Drives 'now check each one of them out.
Dim di As New IO.DriveInfo(Drv) 'get the info for the drive.
If di.IsReady Then 'if it is ready...
Me.cboDrives.Items.Add(New DriveItem(di)) 'build DriveItem class object to cbo list.
End If
Next
End Sub

'*********************************************************************************
' Method : cboDrives_DrawItem
' Purpose : Draw an item in the cboDrives list.
'*********************************************************************************
Private Sub cboDrives_DrawItem(sender As Object, e As DrawItemEventArgs)
e.DrawBackground() 'clear the background.
 Page 681

If e.Index <> -1 Then 'ignore out of range index.


Dim cboBox As ComboBox = DirectCast(sender, ComboBox) 'get ListBox pointed to by this event.
Dim DriveItem As DriveItem = DirectCast(cboBox.Items(e.Index), DriveItem) 'get the Drive item from the list.
Dim X As Int32 = e.Bounds.X 'init drawing from left position.
Dim Y As Int32 = e.Bounds.Y
Dim img As Image
If Not DriveItem.Type = IO.DriveType.Unknown AndAlso
Y >= cboBox.ItemHeight Then 'if drive info and list dropped down...
img = Me.myImages.Images(Images.FolderCanOpen) 'draw image for ">".
e.Graphics.DrawImage(img, X, Y)
X += img.Width 'move beyond image.
End If
img = Me.myImages.Images(DriveItem.ImageIndex) 'Get the image stored In the ImageList.
e.Graphics.DrawImage(img, X, Y) 'draw image at start of line.
X += img.Width + 4 'move beyond image with buffer.
Dim Brush As SolidBrush = New SolidBrush(cboBox.ForeColor) 'ensure no color clashes.
e.Graphics.DrawString(DriveItem.ToString, cboBox.Font,
Brush, CSng(X), CSng(Y)) 'draw the text after the bitmaps.
Brush.Dispose() 'dispose of resources.
End If
End Sub

'*********************************************************************************
' Method : cboDrives_SelectedIndexChanged
' Purpose : A new choice was made in the cboDrives list.
'*********************************************************************************
Private Sub cboDrives_SelectedIndexChanged(sender As Object, e As EventArgs)
With DirectCast(sender, ComboBox)
If .SelectedIndex <> -1 Then 'if the index is OK...
LockWindowUpdate(Me.tvDirList.Handle) 'lock out treeview display updates.
InitializeImageList(Me.myImages) 'RE-init ImageList (rid extra images).
Me._Root = DirectCast(.SelectedItem, DriveItem).Drive 'grab the root path from the list object.
If Me._Root & "\" <> IO.Directory.GetDirectoryRoot(Me._StartPath) Then 'if does not match last start path...
Me._StartPath = Me._Root 'then ignore start path and begin anew.
End If
'-----------------------------------------------------------------------
MeCursor = Cursors.WaitCursor 'we are busy.
Me.tvDirList.Nodes.Clear() 'erase all current nodes.
Dim RootNode As TreeNode =
Me.tvDirList.Nodes.Add(tvDirList.Nodes.Count.ToString, Me._Root, 0, 0) 'create Root Node w/closed folder.
RootNode.ToolTipText = Me._Root 'save its path in its tooltip.
RootNode.Tag = False 'tag it as not a shortcut.
DirRecursing = True 'prevent TreeView updates when recursing.
InitCounter(RootNode) 'init scanning reports.
DirRecurse(RootNode) 'parse any of its subfolders.
DirRecursing = False 're-enable updates.
'-----------------------------------------------------------------------
If CBool(RootNode.Nodes.Count) Then 'if we have children...
RootNode.Expand() 'make sure root node is expanded.
End If
'-----------------------------------------------------------------------
RootNode = FindNodePath(Me._StartPath) 'open directory to the node.
If RootNode IsNot Nothing Then 'if we found it (very likely).
tvDirList.SelectedNode = RootNode 'select this node.
RootNode.EnsureVisible() 'make sure it can be seen.
End If
MeCursor = Cursors.Default 'show that we are no longer busy.
LockWindowUpdate(IntPtr.Zero) 'resume display updates.
End If
Me.tvDirList.Focus() 'set focus to TreeView.
End With
End Sub

'*********************************************************************************
' Method : cboDrives_DropDownClosed
' Purpose : Set focus back to treeview when the cboDrives list closes.
'*********************************************************************************
Private Sub cboDrives_DropDownClosed(sender As Object, e As EventArgs)
Me.tvDirList.Focus()
End Sub

'*******************************************************************************
' Method : tvDirList_AfterSelect
' Purpose : if a folder selected and open, get its count.
'*******************************************************************************
Private Sub tvDirList_AfterSelect(sender As Object, e As TreeViewEventArgs)
If e.Node.IsExpanded Then 'only folders expand.
CountFoldersFiles(e.Node) 'get a count of the node's nodes.
ElseIf e.Node.Parent IsNot Nothing Then 'if it has a parent.
CountFoldersFiles(e.Node.Parent) 'get a count of the parent's nodes.
End If
Me.tvDirList.Focus() 'set focus to TreeView.
End Sub

'*******************************************************************************
 Page 682

' Method : tvDirList_BeforeExpand


' Purpose : React to a node expanding.
'*******************************************************************************
Private Sub tvDirList_BeforeExpand(sender As Object, e As TreeViewCancelEventArgs)
With e.Node
If .Checked Then 'has it been processed yet?
.Checked = False 'mark this folder as processed.
'-------------------------------------------------------------------
LockWindowUpdate(Me.tvDirList.Handle)
MeCursor = Cursors.WaitCursor 'show that we are busy.
DirRecursing = True 'indicating that we are scanning folders.
InitCounter(e.Node) 'init scanning report.
DirRecurse(e.Node) 'parse any of its subfolders.
DirRecursing = False 'no longer recursing through folders.
MeCursor = Cursors.Default 'no longer busy.
LockWindowUpdate(IntPtr.Zero) 're-enable TreeView updates.
Me.tvDirList.Focus() 'set focus to TreeView.
End If
End With
'---------------------------------------------------------------------------
'if the used tried to exit the app during recursion, avoid application crashes
'by letting the display of objects catch up before actually disposing of them.
'---------------------------------------------------------------------------
If Me._pClosing Then 'we want to close the form?
Me.tmrEnd.Enabled = True 'yes, but let the timer do it.
End If
End Sub

'*******************************************************************************
' Method : CountFoldersFiles
' Purpose : Get folder and file count of parent node, if it exists.
'*******************************************************************************
Private Sub tvDirList_AfterExpand(sender As Object, e As TreeViewEventArgs)
Me.tmrScanning.Enabled = False 'make sure the timer is off.
CountFoldersFiles(e.Node) 'get count of sub-nodes.
Me.tvDirList.Focus() 'set focus to TreeView.
End Sub

'*******************************************************************************
' Method : tvDirList_AfterCollapse
' Purpose : Get folder and file count of parent node, if it exists.
'*******************************************************************************
Private Sub tvDirList_AfterCollapse(sender As Object, e As TreeViewEventArgs)
If e.Node.Parent IsNot Nothing Then 'if this is not the root folder...
CountFoldersFiles(e.Node.Parent) 'get a count of the parent's nodes.
Me.tvDirList.Focus() 'set focus to TreeView.
End If
End Sub

'*******************************************************************************
' Method : tvDirList_MouseDoubleClick
' Purpose : User Double-Clicked on a node.
'*******************************************************************************
Private Sub tvDirList_MouseDoubleClick(sender As Object, e As MouseEventArgs)
Dim nd As TreeNode = Me.tvDirList.SelectedNode 'get node clicked on.
If nd Is Nothing Then 'if no node clicked, do nothing.
Return
End If
'---------------------------------------------------------------------------
If IO.File.Exists(nd.ToolTipText) Then 'if a node is selected and it is for file...
Dim Path As String = GetSelectedOpenerForExt(nd.ToolTipText) 'get a reference path for it.
If Not String.IsNullOrWhiteSpace(Path) Then 'if a file association exists...
If Path.Contains(" "c) Then 'if it contains a space...
Path = """" & Path & """" 'wrap it in quotes.
End If
If nd.ToolTipText.Contains(" "c) Then 'if the file contains a space...
Path &= " """ & nd.ToolTipText & """" 'add path w/quotes.
Else
Path &= " " & nd.ToolTipText 'add path w/o quotes.
End If
Else
Path = nd.ToolTipText 'no association, so use file itself.
If Path.Contains(" "c) Then 'if it contains a space...
Path = """" & Path & """" 'wrap it in quotes.
End If
End If
Shell(Path, AppWinStyle.NormalFocus) 'shell out to it.
End If
Me.tvDirList.Focus() 'set focus to TreeView.
End Sub

'*********************************************************************************
' Method : tvDirList_DrawNode
' Purpose : Draw node with indent and state.
'*********************************************************************************
 Page 683

Private Sub tvDirList_DrawNode(sender As Object, e As DrawTreeNodeEventArgs)


If e.Node.ImageIndex = -1 OrElse ImgInit OrElse DirRecursing Then 'if image index -1 or list updating...
Return 'do nothing.
End If
Dim tv As TreeView = DirectCast(sender, TreeView) 'get reference to TreeView container.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'determine the mode's indent level.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Dim nCount As Int32 = 0 'init indent counter to 0.
Dim nd As TreeNode = e.Node 'start with current node provided to us.
Do While nd.Parent IsNot Nothing 'while the node as a parent.
nCount += 1 'count a generation (indent index).
nd = nd.Parent 'point back to its parent node...
Loop 'and try again.

Dim Rect As Rectangle = e.Bounds 'otherwise, get copy of line bounds...


Dim pt As New Point(Rect.X + nCount * tv.Indent, Rect.Y) 'compute Top-left coordinate for drawing.
Rect.X = pt.X + 16 + 16 + 4 'point to text drawing area...
Rect.Size = TextRenderer.MeasureText(e.Node.Text, tv.Font) 'compute text size to minimize flicker...
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Dim Brush As SolidBrush = New SolidBrush(tv.BackColor) 'brush used for selection and text coloring.
If e.Node.IsSelected Then 'if the node is selected...
Brush.Color = SystemColors.Highlight 'change background to highlight color.
e.Graphics.FillRectangle(Brush, e.Bounds) '(we could use Rect here to min highlight).
Brush.Color = Color.White 'change the brush's color to white.
Else
e.Graphics.FillRectangle(Brush, Rect) 'clear JUST text area background...
Brush.Color = tv.ForeColor 'we will draw the text normally.
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If e.Node.IsExpanded Then 'if the folder is expanded....
e.Graphics.DrawImage(Me.myImages.Images(Images.FolderIsOpen), pt) 'draw "V" in front of on-side folder.
ElseIf e.Node.Nodes.Count <> 0 Then 'not expanded, but does it have sub-folders?
e.Graphics.DrawImage(Me.myImages.Images(Images.FolderCanOpen), pt) 'yes, so draw ">" in front of on-side folder.
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
pt.X += 16 'point to on-side folder position.
e.Graphics.DrawImage(Me.myImages.Images(e.Node.ImageIndex), pt) 'draw the on-side folder or on-side shortcut.
e.Graphics.DrawString(e.Node.Text, tv.Font, Brush, Rect.Location) 'draw text.
Brush.Dispose() 'release brush resources.
End Sub

'*********************************************************************************
' Method : InitCounter
' Purpose : Prepare to start scanning a folder.
'*********************************************************************************
Private Sub InitCounter(ByRef ParentNode As TreeNode)
Me._pNode = ParentNode 'save parent node to keep scanning.
Me.lblFolders.Text = "0" 'init report fields for subfolder and file counts.
Me.lblFiles.Text = "0"
Me.lblFolders.Refresh() 'make sure the user sees them.
Me.lblFiles.Refresh()
Me.tmrScanning.Interval = 10 'init interval to 10ms.
Me.tmrScanning.Enabled = True 'turn on the timer.
End Sub

'*********************************************************************************
' Method : tmrScanning_Tick
' Purpose : Scanning folder, so update current counts.
'*********************************************************************************
Private Sub tmrScanning_Tick(sender As Object, e As EventArgs)
With Me.tmrScanning
.Enabled = False 'make sure the timer is off.
CountFoldersFiles(_pNode, True) 'disable timer and get current count. Flag as scanning.
.Interval = 2000 'set interval to 2 seconds if not at this value.
.Enabled = True 're-enable timer.
End With
End Sub

'*********************************************************************************
' Method : CountFoldersFiles
' Purpose : Get the current count of folders and files in the current folder.
'*********************************************************************************
Private Sub CountFoldersFiles(ByRef ParentNode As TreeNode, Optional Scanning As Boolean = False)
If Me.tmrScanning.Enabled Then 'do nothing here if the folder scanning is enabled.
Return 'note that tmrScanning turns itself off before invoking this,
End If 'and then turns itself back on when this completes.
'---------------------------------------------------
With ParentNode
Dim Dirs As Int32 = 0 'init accumulators.
Dim Files As Int32 = 0
For Each nd As TreeNode In .Nodes 'parse each child node immediately under parent.
If nd.ImageIndex < Images.CursorFile Then 'if a folder...
Dirs += 1 'bump folder counter.
Else
 Page 684

Files += 1 'else bump file counter.


End If
Next
Dim DirCnt As String = Dirs.ToString 'prepare report.
Dim FilCnt As String = Files.ToString
If Scanning Then 'this flag is set during tmrScanning tick to indicate intermediates.
DirCnt &= "..."
FilCnt &= " of possible " & _pFilCnt.ToString & "..."
End If

If Me.lblFolders.Text <> DirCnt Then 'if the folder counts do not match...
Me.lblFolders.Text = DirCnt 'update subfolder report.
Me.lblFolders.Refresh()
End If

If Me.lblFiles.Text <> FilCnt Then 'if the file counts do not match...
Me.lblFiles.Text = FilCnt 'update files report.
Me.lblFiles.Refresh()
End If
End With
End Sub

'*******************************************************************************
' Method : DirRecurse
' Purpose : Fill provided TreeView with folders and files as needed.
'*******************************************************************************
Private Sub DirRecurse(ByRef parentNode As TreeNode, Optional ByVal skipDeepSeek As Boolean = False)
If Me._pClosing Then 'if user closing form...
Return 'then do nothing.
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Dim pPath As String = AddSlash(parentNode.ToolTipText) 'get parent node's path and add a backslash.
Dim pKey As Int32 = 0 'initialize simple key
parentNode.Nodes.Clear() 'Clear all child nodes in case repopulating.
If parentNode.Equals(Me._pNode) Then
_pDirCnt = 0 'init folder count.
_pFilCnt = 0 'init file count.
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'first check for link files in the current folder that actually reference directory folders.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Dim Files() As String = Nothing 'init local file storage.
Try
Files = IO.Directory.GetFiles(pPath, "*.lnk") 'get a list of all .lnk files.
Catch
End Try
Dim lnkFolderList As New List(Of String) 'init lnk folder list.
If Files IsNot Nothing Then 'do we have any lnk files?
For Each lnkFile As String In Files 'yes, check each for referencing folders.
Dim linkedPath As String = GetShortcutLinkToPath(lnkFile) 'get lnk's path to its target.
If linkedPath IsNot Nothing Then 'if a path was returned (likely).
If IO.Directory.Exists(linkedPath) Then 'if path is existing directory folder...
Dim Attr As FileAttribute = GetAttr(linkedPath) 'get attributes and check for normal dir.
If (Attr And (FileAttribute.Directory Or
FileAttribute.Hidden Or
FileAttribute.System Or
FileAttribute.Volume)) =
FileAttribute.Directory Then 'and if special attributes not assigned...
lnkFolderList.Add(lnkFile) 'add file it to local linked folder list.
End If
End If
End If
If Me._pClosing Then 'if user closing form...
lnkFolderList.Clear() 'then clear anything from list.
Exit For
End If
Next lnkFile
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'get list of all files. Get it here in case there are no sub-folders, so we can
'still determine of the parent folder should show a '>' tag for expandability.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Try
If Me._pClosing Then 'if trying to close form...
Files = Nothing 'then assume we gathered nothing.
Else
Files = IO.Directory.GetFiles(pPath) 'get a list if all files in the folder.
End If
Catch
End Try
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'put that process on pause and now check for sub-folders.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Dim dirs() As String = Nothing 'init local directory storage.
Try
 Page 685

If Me._pClosing Then 'if user closing form...


dirs = Nothing 'then pretend we gathered nothing.
Files = Nothing
Else
dirs = IO.Directory.GetDirectories(pPath) 'get list of subfolders, ignore protected.
End If
Catch
End Try
Dim Bol As Boolean = dirs IsNot Nothing 'True if directories exist.
If Bol OrElse
lnkFolderList.Count <> 0 OrElse
Files IsNot Nothing Then 'if sub-dirs, files, or lnk folders exist...
If skipDeepSeek Then 'if Referencing only...
parentNode.Checked = True 'mark this folder as not yet processed.
parentNode.Nodes.Add("*") 'add faux child to add ">" connector.
lnkFolderList.Clear() 'get rid of any added resources.
Return 'nothing else to do.
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If Bol Then 'if sub-folders exist...
If parentNode.Equals(Me._pNode) Then 'if we are scanning the main parent...
Me._pDirCnt = dirs.Count + lnkFolderList.Count 'get total directory count.
End If
For Each dirPath As String In dirs 'then parse each subfolder.
If (GetAttr(dirPath) And (FileAttribute.Hidden Or
FileAttribute.System Or
FileAttribute.Volume)) =
FileAttribute.Normal Then 'if it is not a special folder...
Dim dirNode As TreeNode =
parentNode.Nodes.Add(pKey.ToString,
IO.Path.GetFileName(dirPath),
Images.FolderSide,
Images.FolderSide) 'add new dir node w/closed folder.
pKey += 1 'bump key.
dirNode.ToolTipText = dirPath 'save its folder path as its tooltip.
dirNode.Tag = False 'tag as not a shortcut.
DirRecurse(dirNode, True) 'brief check of subfolders for children.
If parentNode.Equals(Me._pNode) Then 'if we are scanning the main parent...
Application.DoEvents() 'let tmrScanning get a share of run-time.
End If
ElseIf parentNode.Equals(Me._pNode) Then 'if we are scanning the main parent...
Me._pDirCnt -= 1 'rejected folder, so drop one.
End If
If Me._pClosing Then 'if the user is closing the form...
lnkFolderList.Clear() 'clear any added resources.
Exit For
End If
Next dirPath
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If lnkFolderList.Count <> 0 Then 'if link folder data exists...
For Each lnkPath As String In lnkFolderList 'parse each lnk path.
Dim linkedPath As String = GetShortcutLinkToPath(lnkPath) 'get lnk's path to its target.
lnkPath = lnkPath.Remove(lnkPath.LastIndexOf("."c), 4) 'remove ".lnk" from end of folder path.
Dim dirNode As TreeNode =
parentNode.Nodes.Add(pKey.ToString,
IO.Path.GetFileName(lnkPath),
Images.FolderSideShortcut,
Images.FolderSideShortcut) 'add as new dir node w/closed folder.
pKey += 1 'bump key.
dirNode.ToolTipText = linkedPath 'save its linked folder path as its tooltip.
dirNode.Tag = True 'tag as a shortcut.
DirRecurse(dirNode, True) 'parse any of its subfolders.
If Me._pClosing Then 'if user closing form...
Files = Nothing
Exit For
End If
Next lnkPath
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'process any list of files.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If Files IsNot Nothing Then 'if files exist.
If parentNode.Equals(Me._pNode) Then 'if we are scanning the main parent...
_pFilCnt = Files.Count 'update total file count.
End If

Dim IsLink As Boolean 'set to True if a file is a shortcut link.


Dim LnkPath As String = Nothing 'path of .lnk file when IsLink is set True.
For Each FilePath As String In Files 'parse each file in folder...
IsLink = False 'init flag to not being a link.
Dim ExtIdx As Int32 = Images.FileFile 'init default image to File.
Dim Ext As String = IO.Path.GetExtension(FilePath).ToLower 'get the file extension.
If Ext = ".lnk" Then 'if this is a link file...
Dim linkedPath As String = GetShortcutLinkToPath(FilePath) 'get the link's path to its target.
 Page 686

If linkedPath IsNot Nothing Then 'if the linked path exists (should)...
If IO.Directory.Exists(linkedPath) Then 'if it is actually a directory folder...
linkedPath = Nothing 'kill linked path.
ElseIf Not IO.File.Exists(linkedPath) Then 'if it is a file and it no longer exists...
linkedPath = Nothing 'kill linked path.
End If
End If
If linkedPath IsNot Nothing Then 'if a valid file path was returned...
IsLink = True 'mark it as a link.
LnkPath = FilePath.Remove(FilePath.LastIndexOf("."c), 4) 'save full path to lnk file, less ".lnk".
FilePath = linkedPath 'let file assume linked Path for now.
Ext = IO.Path.GetExtension(linkedPath).ToLower 'get the actual target file's extension.
Else
ExtIdx = Images.None 'ignore if linked path is nothing.
Ext = Nothing
End If
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If (GetAttr(FilePath) And
(FileAttribute.Hidden Or FileAttribute.System)) <> 0 Then 'if this is not an acceptable file...
ExtIdx = Images.None 'hide image.
IsLink = False 'make sure flag is reset.
ElseIf Not String.IsNullOrWhiteSpace(Ext) Then 'else if it has an extension defined...
Select Case Ext 'check extension.
Case ".ico" '--------------------------------------------ICON.
ExtIdx = Images.IconFile 'use default icon image.
AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab first icon from file and update image.
Case ".pic", ".png", ".bmp", ".gif", ".jpg",
".jpeg", ".pic", ".tiff", ".dib" '------------------IMAGE.
ExtIdx = Images.IconFile 'use default icon image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab assoc file icon and update image list.
Case ".mov", ".bic", ".flv", ".avi", ".mpg", ".mpg4" ".mpeg",
".mpeg4", ".asf", "wmv", ".swf" ".divx", '----------VIDEO.
ExtIdx = Images.MovieFile 'init default image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab assoc file icon and update image list.
Case ".zip", ".7z", ".chm", ".iso", ".tar",
".ar", ".arj", ".cab", ".ntfs" '--------------------compressed files (ZIP).
ExtIdx = Images.ZipFile 'init default for compressed file.
Case ".mp3", ".wav", ".aac", ".wma" '--------------------AUDIO.
ExtIdx = Images.SoundFile 'init default image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab assoc file icon and update image list.
Case ".ani" '--------------------------------------------animated cursor.
ExtIdx = Images.CursorFile 'set default image (do nothing else).
Case ".cur" '--------------------------------------------CURSOR.
ExtIdx = Images.CursorFile 'init default cursor image.
AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab cursor image and update image list.
Case ".fon" '--------------------------------------------FONT.
ExtIdx = Images.FontFile 'set default image (do nothing else).
Case ".ttf" '--------------------------------------------TrueType font (TTF).
ExtIdx = Images.TtfFile 'set default image.
Case ".bat", ".com" '------------------------------------Batch, Command file (DOS).
ExtIdx = Images.ExeFile 'set default image (do nothing else).
Case ".reg" '--------------------------------------------Registry file.
ExtIdx = Images.RegFile 'set default image (do nothing else).
Case ".dll" '--------------------------------------------DLL.
ExtIdx = Images.DllFile 'init def EXE image (faster using default).
'AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab 1st file icon and update image list.
Case ".bin", ".dat", ".ocx", ".oca", ".dep", ".nls" '----Binary/Data file.
ExtIdx = Images.BinFile 'set default image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab assoc file icon and update image list.
Case ".log", ".ini", ".cfg" '----------------------------LOG/INI file.
ExtIdx = Images.LogFile 'set default image.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab assoc file icon and update image list.
Case ".exe" '--------------------------------------------EXE.
ExtIdx = Images.ExeFile 'init default EXE image.
AddImageFromFile(FilePath, IsLink, ExtIdx) 'grab 1st file icon and update image list.
Case Else '----------------------------------------------process undetected types.
AddAssociatedImage(FilePath, IsLink, ExtIdx) 'grab assoc file icon and update image list.
End Select
End If
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'add a file node to our TreeView, tvDirTree.
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If ExtIdx <> Images.None Then 'if we have an image reference...
If Not IsLink Then 'if this is not a linked file...
LnkPath = FilePath 'simplify the code below by assuming it is.
End If
With Me.tvDirList
Dim dirNode As TreeNode =
parentNode.Nodes.Add(pKey.ToString,
IO.Path.GetFileName(LnkPath), ExtIdx, ExtIdx) 'add as new file node w/appropriate index.
pKey += 1 'bump key
dirNode.ToolTipText = FilePath 'save its path as its tooltip.
dirNode.Tag = IsLink 'tag true if a link file.
End With
 Page 687

ElseIf parentNode.Equals(Me._pNode) Then


Me._pFilCnt -= 1 'rejected file, so drop one.
End If
If parentNode.Equals(Me._pNode) Then 'if we are scanning the main parent...
Application.DoEvents() 'let tmrScanning get a share of run-time.
End If
If Me._pClosing Then 'if user closing form...
Exit For
End If
Next 'FilePath
End If
End If
lnkFolderList.Clear() 'release link folder storage.
End Sub

'*******************************************************************************
' Method : AddAssociatedImage
' Purpose : Add an image from a file associated with the specified file, if one exists.
'*******************************************************************************
Private Sub AddAssociatedImage(ByVal FilePath As String, ByVal IsLink As Boolean, ByRef ExtIndex As Int32)
Dim Ext As String = CleanExtension(FilePath).ToLower 'get FilePath extension.
Dim x As String = String.Empty 'init to no additional extension.
If IsLink Then x = ".lnk" ' 'if Path is a Link then set additional .lnk extension.
If Me.AddedExtList.IndexOfKey(Ext & x) <> -1 Then 'if the extension list contains a matching extension...
Dim Assoc As String = Me.AddedExtList.Item(Ext & x) 'grab associated file or ExtIndex reference.
If Assoc.Substring(0, 1) = "-" Then 'is it actually a negative integer?
ExtIndex = -CInt(Assoc) 'yes, so grab a default extension index.
Else
ExtIndex = Me.ImgSrcList(Assoc) 'else grab link image index associated with it.
End If
Else
Dim Assoc As String = GetSelectedOpenerForExt(Ext).ToLower 'check for an app associated with this file.
If Not String.IsNullOrWhiteSpace(Assoc) Then 'if there is an associated file...
If AddImageFromFile(Assoc, IsLink, ExtIndex) Then 'then add associated image, and if it was added...
Me.AddedExtList.Add(Ext & x, Assoc.ToLower & x) 'then add the extension to to association list.
End If 'otherwise, ExtIndex was set to existing extension.
Return
End If 'if this fails, we will use the default image.
Me.AddedExtList.Add(Ext, (-ExtIndex).ToString) 'failed, so add default extension as a negative number.
End If
End Sub

'*******************************************************************************
' Method : AddImageFromFile
' Purpose : Adds an image from the file and update the Item's image index, or
' : the path already is registered, so set the image index to it.
'*******************************************************************************
Private Function AddImageFromFile(ByVal FilePath As String, ByVal IsLink As Boolean, ByRef ExtIndex As Int32) As Boolean
FilePath = FilePath.ToLower 'make sure it is lowercase.
Dim fp As String = FilePath 'init a match of FilePath.
If IsLink Then fp &= ".lnk" ' 'yes, so append a .lnk extension if this is a link file.
'---------------------------------------------------------------
If Me.ImgSrcList.IndexOfKey(fp) = -1 Then 'if the item does not exist in our list...
Dim Img As Image 'init image to set and assign to the list.
If IO.Path.GetExtension(FilePath).ToLower = ".cur" Then 'if it is a cursor... (ToLower is a safety net)
Img = GetCursorImage(FilePath) 'then load the cursor's image.
Else
Img = GetIconImage(FilePath) 'else load the image from a file (EXE, DLL, Icon).
End If
If AddImage(Img, IsLink, ExtIndex) Then 'update the imagelist if an image was grabbed.
Me.ImgSrcList.Add(fp.ToLower, ExtIndex) 'if an image was added, then add fp to ImgSrcList
Return True 'and indicate an image was added.
End If
Else
ExtIndex = Me.ImgSrcList.Item(fp) 'otherwise, grab assigned index.
End If 'if this fails, we will use the default image.
Return False 'indicate an image was NOT added.
End Function

'*******************************************************************************
' Method : AddImage (Support Method)
' Purpose : Add an image to the ImageList and set the image index. If it is a link, add a tag.
'*******************************************************************************
Private Function AddImage(ByRef Img As Image, ByVal IsLink As Boolean, ByRef ExtIndex As Int32) As Boolean
If Img IsNot Nothing Then 'if image is valid...
AddRealImage(Img, IsLink, ExtIndex) 'then add it.
Return True
ElseIf IsLink AndAlso ExtIndex < Images._LastItem Then 'else compute the index of the...
ExtIndex = ExtIndex + Images._LastItem - Images.CursorFile 'shortcut rendition of default icon.
End If
Return False 'use current ExtIndex value.
End Function

'*******************************************************************************
' Method : AddRealImage (Support Method)
 Page 688

' Purpose : Add an image to the ImageList and set the image index. If it is a link, add a tag.
'*******************************************************************************
Private Sub AddRealImage(ByRef Img As Image, ByVal IsLink As Boolean, ByRef ExtIndex As Int32)
If IsLink Then 'if we are processing a shortcut link...
Dim bmp As New Bitmap(Img, 16, 16) 'instantiate a copy of the provided image.
Dim g As Graphics = Graphics.FromImage(bmp) 'instantiate graphics interface for the copy.
g.DrawImage(ShortcutTag, New Rectangle(0, 8, 8, 8)) 'draw shortcut on bitmap in bottom-left corner.
g.Dispose() 'dispose of graphics resource.
Img = bmp 'update the referenced image.
End If
ExtIndex = Me.myImages.Images.Count 'update extension index in image list.
Me.myImages.Images.Add(Img) 'then add the image to the image list.
End Sub

'*******************************************************************************
' Method : FindNodePath
' Purpose : Find the node path in a treeview.
'*******************************************************************************
Private Function FindNodePath(ByVal SeekPath As String) As TreeNode
Dim RootNode As TreeNode = Me.tvDirList.Nodes(0) 'start at the TreeView's base node.
If SeekPath = RootNode.ToolTipText Then 'if we already have a match...
Return RootNode 'then simply return the node.
End If
SeekPath = AddSlash(SeekPath) 'add a terminating "\" if one not there.
Dim Idx As Int32 = -1 'init backslash index.
If SeekPath.Contains(AddSlash(RootNode.ToolTipText)) Then 'if the seek path contains the root node...
Idx = RootNode.ToolTipText.Length 'then start index beyond it.
End If
'-----------------------------------------------------------------------
Do While Idx <> SeekPath.LastIndexOf("\"c) 'while there is data to find.
Idx = SeekPath.IndexOf("\"c, Idx + 1) 'find the next backslash.
Dim BasePath As String = SeekPath.Substring(0, Idx) 'grab the path left of it.
RootNode = FindNodePath(RootNode, BasePath) 'find the path to it via an overload.
If RootNode Is Nothing OrElse Idx = SeekPath.LastIndexOf("\"c) Then 'if did not find node or are at target...
Exit Do 'then done.
ElseIf Not RootNode.IsExpanded Then 'othewise, if tne node is not expanded...
RootNode.Expand() 'then make sure it is expanded (and populated).
End If
Loop
Return RootNode 'finally, return the target node.
End Function

'*******************************************************************************
' Method : FindNodePath (overload)
' Purpose : Find the node path in a treeview.
'*******************************************************************************
Private Function FindNodePath(ByRef Node As TreeNode, ByVal SeekPath As String) As TreeNode
If Node.ToolTipText = SeekPath Then 'if current node contains the sought path...
Return Node 'return a reference to that node.
End If
If Node.Nodes.Count <> 0 Then 'else if it has children...
For Each subNode As TreeNode In Node.Nodes 'check each child.
Dim nd As TreeNode = FindNodePath(subNode, SeekPath) 'check its node and subnodes...
If nd IsNot Nothing Then 'did it find a match?
Return nd 'yes, so return it.
End If
Next
End If
Return Nothing 'nothing found.
End Function

'*******************************************************************************
' Method : InitializeImageList
' Purpose : Imitialize a provided ImageList and fill it with locally-created images.
' :
' NOTE : If you want to append the images to an existing list, set the Replace
' : parameter to FALSE.
'*******************************************************************************
Private Sub InitializeImageList(ByRef imgList As ImageList,
Optional ByVal Replace As Boolean = True,
Optional JustBasics As Boolean = False)
ImgInit = True 'prevent TreeView updates while we are doing this...
If Replace Then 'if we are filling, not appending images.
imgList.Images.Clear() 'initialize image list.
imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list.
Me.AddedExtList.Clear() 'reset file extensions with application associations.
Me.ImgSrcList.Clear() 'reset file paths from which we extracted images.
End If
Dim strImg As String 'string to be assigned image data as Base64 text.
Dim Img As Image 'image to receive data from the memory stream.
'------------------------
'ImageStrip for 30 Images.
'------------------------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAAeAAAAAQCAYAAADOMaw4AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
 Page 689

"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAABKySURBVHhe7Zwvr1xHEsXfpzAyMjMeFGpuaGw2XyHUKHiR" &
"pWEBq5UWLPEnCFqwHpwgI4NI0UobM0sLZutXXedO3bp9/8ybmeRZ2iOddN/u6ur/fW7fN87DPfD58+fT" &
"8Xg8/fLLL6ePHz+eeAaRvRnff//9aYlmcrHPp4off/zrqfJwOIxI2m+//bY6lrv98bQ7FFpaZG/HweqB" &
"d8a7d+9O8OXLl5so+yj+8OXLF19jHz58OL1//975ww8/+Hh9+vTpRH6Y/h/rYKy+qfHinNGamON33333" &
"pNcB+/pht3Pu9nvnPvjmzRvf92Hag+bM+ezZs9HzQvpNwZ5bY5gOiLPsUeyhpxM9CubHwTmxxjB92kB4" &
"WSxfv371BU+ctJ9++mnU4S3wgfrv71bqECJuamA8HvcemslNB8Wcej1saKg4g79F+K4BYlHB+Ank0xaJ" &
"cBTroonuqfDxAvy3f/1+t34DDkiAuPrLwwKxAZShLGPE+mJcCJkzyJyx5tj0hE/58D3sH06XMIoN6Nks" &
"MYrNwceXsD0ugxdsxncreUmCxDuHmuqdYxfsizWwXqjzTuugtvMSNuz2Jr52phl3+8Z9kBdKxkzCjEiX" &
"M8D7SIhgV7ERycu2FLwlXGDxPcMZEbacx6FXln4C1sTeLi06t4HOU8ZSdt4CA2tjCeSb2WTM1KfHMtzc" &
"BmwodRIBdvH998kOxtYBDkgQ5qtog3QWX4USYTO5ugO0Eb+0j03KItVGBeSrX7RfhzxwByYKVzFQBdiy" &
"fOwECS8Li9twFOvi1gK8s7ZEygS06xrig3GHPQHe28G0swPHQ3uuN2DmAj+EeYNpvphLFrrmUASUXwJ2" &
"Eb0rEMXTcbeJPQG9tnyBjw0g3pLmwVizJrdQ8yFSNtwIUfMU5AUnwPcaWC+AetlHUfRWMM9q+/aQchTO" &
"Y9Ij7SXk9DzYGNqG8WfKBprHCPMaV1zP2ZaCFewj5mWJ2IT5CC4o+J4h+aAIj6cRXMLw4WUz0AzWw97G" &
"iWsaLyz03dPiKyIvNHpRoQGA8cywpIFAcxFpA2iH8i6h2t+8BHqGmWE2C2wQXuChrRgWj42Fk47zDKLI" &
"IhggBkpvbn4Qx7PS3HAbuva0mcnRjYnFlV8kaK+eW1+OPnj0BSCiU7GbUmI2opWNZowEGOFFMD1k1xnI" &
"p17VHcW6uLUAQ2tqt3xu96WgLD50OCKux+Ph9I+//8U2Sxvnw2F3Oh64uZl4HPaTGzAHAnOidcf88cyc" &
"vn371usQ9Rkfrq1DyrMemPdIuhueqgADnltyH9rzjFOPNY9nxp44cxduhKh1CuURumUCc9UDbYOK68WN" &
"z9G0IYoD+c3+t6YBr8NaNxumA9fjnmpx1nSzm/J4tDMv6qG9L1688DJZOAKeDufAeINsa5wg5mSRnXlz" &
"eB+pY45TX8ByyGtscfZ2C9tzja8L8M72OkQneHZBNj7YM+2XrbfAoHUyh7yOzHwoRztaGm3ezrQezsLL" &
"4bVE2bUKx1DjrI++uPk0RSVsONIQE4RDmyWKLSILMOLrAxnP0Ey2+PF2sNA5eHluyQ1MBgczYR4D6kJ4" &
"CUnHhva8evXK/bGRfSFuEOAsZCN2BJiN0gT0OGwaQSLzZwgw7Imw2r0n/wKCmI/RDfhg7UR8JcDv3rWD" &
"aG8CzFePegNmLJgvQMi6YF4QX4RY7QOMJ8/MKWE5hEdQWeaecpF8FzxlAQaktawp2OfsE81ZpsaOMJPP" &
"qoy9758xosYpyCscoDOlAqGFFawd5jeKg57vrWnAvKrt/RAb1jKUPeFud06vRHwlwIwbZw/srFv3BQXG" &
"nXGB+RzJtsYJYk4W2Zk3h4sKdSxQa8DM5cNSLc+CiDoQKUHpsoFeVy4Q4Iymv6xLzgLGCvDlwEXZ0jQe" &
"PQGOR3/hYY1onXD+V5JOO1p/piILw+ckVPvdAZMk4ZkjmyyTSsOJozXY9rgJrTYdC30QYCPPtaNLyALc" &
"o5ks+fH2IbwsGDEO/aEc7SGdAcVehy5jwjNtpi7ihLwIYEO6T8KKAFcRGzEJML4YN4Sy3oAzQR73Hm4r" &
"wM2X2myPIz+MBUBUyROz2PYIYi7GN2DLPMBdm2O/AZsAcwtmA9UbMOPG+sQXm4V5k/gyj2qf5pRn1ipx" &
"baIeKIMP7CnL3ETWLPCnAyCSHDyTrv5WPHUBBqS37DEYG8Z/LL528zBq3Cr1t7nOQR61LQO7Zt7AXFZQ" &
"N+Jr2R7PYO3oYE2Y+DXUtJ4NCM/zITYSVtkrzVTWNsWYVYAB65rzh7GLJGHwBxgPfrj1/PlzvzXTX85s" &
"kG0pWBFz0s0L9ObN4ecSvo1xRo3DyGM+SKMMISCIaDescfkVtOb81mtz688WIrjMNeRlnfUn9ASY/P/8" &
"/M+hDH0ljzHXuEv/iNMO4oSX0oo3AebgUEU4r8LLhOYGSQDDiYPGUx7RMDP7TzBAx+gwPkAUG5AbJmYB" &
"NhNvh56VRtkCL4vw0h5ImzPjIPSytIU0Joe2kceCxQfh69evvR3ECdUmnim3JMCD0Hbo+UWA58BLjAho" &
"WxTr4tYCTNRvvx0/jBfwee/QzJ38kAsqDUiQGEvo4mrzdrTxPdmNdxfk15uED8Z6A2YsWKM63LVRtH7z" &
"uJJPnZQR8TEHyrB+KENI+cjqgrXBDYVQtoQ53Q0LvgUBBuQ1kzMYlzbOZ+EVNWaEmeQxR75/xoialoFd" &
"M2/QmZKhm69lT27BrB3KtNIDJn4NNa1nA8yr2t4PY605iZNO3AXY5oSXTAk0eVWAGbelGzBQyDpjn4ic" &
"Ya3Osw0hBStiTrp5gd68OdR+sfVkSvpi5vJhKZZuQUSHeH4GbQzbs8YTsJ52Np9+w2Ve7fwglAArpN0Z" &
"eT/SJuAv+/HFDb2hHr5SaH6w5bzR+iFfZS+B2j844JBBhCANpQJC8mkoosPk8wYGlU4jBPxYsTPtsBXw" &
"xcJns4Io4lAbKlXvHK1o9uNtQnhpu3xQb6UOYcp4QQPtIo90iA31s3npN2+f1Kk8JscP2QsFeJTfEWAX" &
"vAWCaPss7iHAwNtc/DAWYG/jzvyyfkjLItwjwA4flAP50FgikADjg3lm/iS+emajaFxJ4zljbRwBc6w1" &
"Q/lI7qKK7RbxBd+KAAPym1mDxtUPwWAV4Mr9sX0GZFzDjRC1LAO7Zt5QBZh6taZinXiaQNqVAjxJb7gs" &
"pFwVLZED/5IbsAhYbxWUBdnWOEHMSTcvMJk3tdmi1OChnmHsM+81ZC4iD9Asi19G+WRf6W+7fvMlbmNk" &
"jbKzKtaj+fe1aTasOyHvSa0PxNZqsLN+LMCkEWK7RYDHYzLFMCZyAhlYnBFnkSI4ElyRTxs0nAklBDrs" &
"JNoW+CFLZ9sn6JbuB7PFWUBAdc/BsrfQfSC8+aYO6YtI/VC2hCoPosrhANdbI33m0CdOul4ghDkBrsIL" &
"pzbnQ6B3A843XxFE2x188vNfDBOOWOsr+VaGsuGmDyuX22hFRs+AOQUsbtaGv3Ha+OoAZh3o5qs1oU2Q" &
"BRgyzvr1s8in6OPBhMM/SU9vwKw7zQ3+thBbkMexB2zkl5B2R9YssGFfcAiyhtbEF2wVUOzEKAqIz3JD" &
"+QofmyVg00ynAizxzQJMmHlvAealWWsk1snoFkwaZVrpARO/hpqm50m6tao5j5C0XnoOyeeQNpX1W/DS" &
"J2jGjbUE1wRYZ7XAp2g9Z1vjBDEn3bzAZN68D96vMSVANYe+eHqDpVh6CxzEIfsTtH16flacsuwt/xmb" &
"9U+i6zdgo/5dta9Ne5buCHlf0ibdfiEXCQkwdkqnT6Rp/RCnbIVl8d9hDCrUfnwMoCIopwgwhwhiqxug" &
"BFiNxw5RYkNBNiMTpH8fqMOLzkDibAAJDnVRhnSVJV5Dq8xD7PPtl2cg/xJfiL2IXRZebOUjw50ZsKPt" &
"fP6k71l4w7QBoRgJneV3WG0az4eAxoP+iggufy81Mw/1t9PowwAXYWevjh43iC8w260CrC8FEh59Pq6i" &
"zI8hmBtAWXywHkBPgJ1sJJsa/m1kvQHrgNd8rQGb3otMBfm0E3v8U09krQJb9scW8QUIYhbKOcquCKj3" &
"pQfPWy9fEaXHIL3QwdhIgM9s861x4+AjrGSNhxshalsGds28QesJUKfWkxBrxfP0TJlWesDEr6Gm9WyA" &
"+7WWtf/aM2sr1penE/bsXLxsPnS4+3Mc3PUGzB5jL/QEWP4A5yPnFv2FpJMGsq1xgpiTbl5gNG+1zZY0" &
"PBOHGoeWuizAW0Ion+wv//xs/fO9huDa83ATNkqfQD7H895s64ZxOouw+qZn/WCOurV+iGtdCeo/eWpn" &
"SztD6fiYgIaxSCW8HKJVeAVNMqRRdJAFQpjzeKOgUn3mZkGQrg5IhNVoUeVF0rIAE+ZyWYRZKKRV4VXc" &
"S3fAjetoB5XT/xlMd9E3IBBJ3KrwwpzvtPPCmcSMMQH5tgsRLBetuEGCaP8I20V4o/gCs68CnNsMJHr6" &
"nwcM/xOBOIzJp/1+C7ZQ8wPIwwdrDc4KsJF1SFhvwIDNhV+1xaofvbSIQIIKeuMo0EbZ4j+SN4My6t8a" &
"biHAhJlK21C+wsuCGg+OwPisCTBhJen3EOB8080wsyGPtUOZVnrAxK+hpvVsADW4b+KsK50xCtXvZuuW" &
"HtchX+kHvuXr3Ilz17ibnMGG5jFC9gIvwfwAi9svZzdzBLItBQXOa+ZjToCVb9HRvNFW+qL+qL9KI5/n" &
"c8rjBTjHVQ9j4Z+YbT4nN+EQYMYQMC7UTQjyOJIugZXYEh748WfEEWfi1K31Q5yyFZYV6a3/FWo/PmZB" &
"AzMjeQQmQ50iFEmnEkhZBIZ0FgK2pGUb0VyOmPMoT1gEGHg6wDcHH88rwushN89KhPfXX392f4T+71A7" &
"duBiAUbExCRmEmDGRESAuUX5DRJGndGPCdZFeF18rTvetnP7mz3iz7OHCRK9kfjCqIsf5+glIgsxkEBx" &
"IAIXYLPP3LsA47M91xuwIMEDqksvLSKQDZgbR6D1/BjxvRS3EGDA3uJZaR6ul68YyrbHs3/iLekM7eks" &
"vFWA+eQsSpA5FFnj4UaImpaBXTNv0HrSi5nWk5DTRdoQxYWJX0NN69kAr4eQNdUjedQZ9Q72SutR4kt5" &
"YNvPxxmBIZ+8gPuCAmXlhzEXsq3Rgbha0M0DNT/Pm14YzLOHljQ8Ex/oKY3RdggshXZ54CAO1e82fh4d" &
"4hpTdMR/eGXjUm/CkGfWB+PBOtWLCMiaRpuAxBcivhJd+kSILT7WBBhYtrMHtR8fVwFnTAgCQqM4uAhZ" &
"5FTCgcfn6CyMdBzScJ7pEPE8OALpmdh3BBh4HtgivAJCqgOahQq58fJTdEttP0nnFpwOcsjbpftFsQyI" &
"hCngIrGRrYcmKNGM2Ruw2iSC6E8X8yK8LL706Sy6mecyblOQBe1SUBYfOhQRV+aY9EHA9yYYtpH2Rv55" &
"Uu8GDBgbtaW+tEAEGWgNilG8C3xG9K64hQCzd4grBB6ul6/wci3qcF+AeHAA81W/drHfRcbwjxLgrWDt" &
"0IYoLkz8GmpazwZ4elpTs3GNkcoYA00wJBq+dkXrn+Lk6VYXBcHgbw3Z1uiIeejmgZpf502CWylBdqac" &
"1H9gKZZuwSXUeKIjjI9uwPkmzFhprRHWddITYLRD6RJf4qRD4lsFeAlqPz6uBg71iUKLjcWim6KeIXEO" &
"QYSRzSj7S0iVreYJhsldE14BIa3kxstBxU1YB1bPjv4gwJtoYtKEccxoRvcG3COIfs1iKsLL4gusiUV4" &
"xeVyzOU1xAcHIkBc/cYc7eX/iOWb2z//tPnYcgPmBcdvzSG8ejEE2GArRvE/FRLGNc4I6Eh8AfEhXC+/" &
"Be4PEG9JDZccPvWFkvUcbgT538IBT0GA8/mkc2cuTt06p4wOF9QQYG64WYS11v23EHAK75fCJWRbCoJ6" &
"w43nAWv5hkFszbOHWXzVd/W79H3Iv5SURSz58Z//8CqFEt8l9AQ4Hh2prSNUAX4srfjE96PhrTfobYPO" &
"UQl/u1OcdCYvQ8Uv5BJq5xbtaQOHsgYFckBV6uWhMtxcDQnwFmyp9yzC6+IL+gI8fkm4FzhgIOK6hbKP" &
"4g4OdAnwEiTAUexJoLfe1hhFAfHu1yPSe2Whl9yO8Nh8tqQG9nR9SdzK3uH2GNxIgK+BxuUxnAX7nHZC" &
"/n7tL1oc/CbCpKVzwPtFKNHukbM421JQ4FxmTjri6ljLD8ivM7Vxjt6YknYRAfrS4xqwMR8O+hYvsqtg" &
"vUmADUNbHsnbIvrmhxwdYhK4fSDMQpjeG5vriWYNoK1VlHnOfRDCxdVAgHN9S6ziMweEd4v4Am4n9W+/" &
"/kvCbwQS4C18agJ8JejLEm+Be/i8Gfz2OG3jIjmXLHzS4Oth9M00t4lo/gTNmeGGnf5t5DePOIYHVBFe" &
"Y7gZRHUrNS/X4eHhf4oC0kFP8ppsAAAAAElFTkSuQmCC"
Img = ConvertBase64ToImage(strImg) 'grab image from string data.
imgList.Images.AddStrip(Img) 'add this as Images 0 – 29.
'--------'
'Image 30 ShortcutTag (special 8x8-pixel image)
 Page 690

'--------
strImg =
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" &
"YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACgSURBVDhPnZPRDYQgEEStjcr4oiAqoBcL4e6ZPBMX8Twm" &
"GZddh2EhsH3RV1lrJW49pdRLKQdzzud4RvQXA4r/AP2tQWvtsQP+A8YXAyYBIvmMUTcYsIKrUXOv8qdB" &
"hEL52sCzkHaijvhooCBOiPntGTCWnroT1JkPBlEoYt38NGCPFKUrCzuzPtwDiegNpgaxk0jvyPAWVnkY" &
"8Fnlvu/9AwNtbbu4Ug9YAAAAAElFTkSuQmCC"
ShortcutTag = ConvertBase64ToImage(strImg) 'grab image from string data.
'---------------------------------------------------------------------------------
'make shortcut versions of defaults.
'---------------------------------------------------------------------------------
If Not JustBasics Then
For Idx As Int32 = Images.CursorFile To Images._LastItem - 1 'duplicate default images with shortcut versions.
Img = New Bitmap(Me.myImages.Images(Idx), 16, 16) 'create a duplicate of an existing image.
AddRealImage(Img, True, (Idx)) 'add as shortcut-tagged version (pass Idx ByVal, not ByRef).
Next
End If
AddedBase = Me.myImages.Images.Count 'save index for addl. images ref'd in AddedList collection.
ImgInit = False 'resume TreeView updates.
End Sub

'*******************************************************************************
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object.
'*******************************************************************************
Private Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

End Class

'*******************************************************************************
'*******************************************************************************
' DirTreeTestSupport Static Class Module
' Supprt for Dir Tree Test.
'*******************************************************************************
'*******************************************************************************

Module DirTreeTestSupport
'*******************************************************************************
'Enumerator: Images
'Purpose : Reference to images in the ImageList.
'*******************************************************************************
Friend Enum Images As Int32
None = -1
'---- Logical Drive Types
Fixed
CDRom
Removable
Ram
Network
'---- Virtual Drive Types
Desktop
Documents
Downloads
Music
Pictures
Videos
Recent
'----- Folder Images
FolderCanOpen
FolderIsOpen
FolderSide
FolderSideShortcut
'----- Default File types
CursorFile
ExeFile
FileFile
FontFile
TtfFile
IconFile
ZipFile
MovieFile
SoundFile
DllFile
DosFile
RegFile
BinFile
LogFile
'---- Mark end of this list
_LastItem
 Page 691

End Enum
'*******************************************************************************

'*******************************************************************************
' Method Name : GetShortcutLinkToPath
' Purpose : Retrieve the command path a shortcut file links to.
'
'This method requires COM references to:
' Microsoft Shell Controls and Automation (Shell32.dll), and ensure its Embed Interop Types parmameter = False.
'*******************************************************************************
Friend Function GetShortcutLinkToPath(ByVal ShortcutFilePath As String) As String
Try
Dim Shell As Shell32.Shell = New Shell32.ShellClass 'define shell class obj as link to OS shell.
Dim Folder As Shell32.Folder =
Shell.NameSpace(IO.Path.GetDirectoryName(ShortcutFilePath)) 'define folder obj to link file's dir.
Dim FolderItem As Shell32.FolderItem =
Folder.ParseName(IO.Path.GetFileName(ShortcutFilePath)) 'define link to shortcut obj from folder obj.
If FolderItem IsNot Nothing Then 'if it exists...
Return DirectCast(FolderItem.GetLink, Shell32.ShellLinkObject).Path.Trim 'then return the link's command path.
End If
Catch
End Try
Return Nothing 'otherwise failure, so indicate so
End Function

'*********************************************************************************
' Method : AddSlash
' Purpose : Add a terminating backslash to a drive/path if required. This function
' : is useful for building paths, and the string you are working with may
' : or may not already have a backslash appended to it.
'*********************************************************************************
Friend Function AddSlash(ByVal strPath As String) As String
If strPath.Last = "\"c Then 'already have a backslash?
Return strPath 'yes, so simply return the string.
Else
Return strPath & "\" 'otherwise, return the string with a backslash.
End If
End Function

'*********************************************************************************
' Method : RemoveSlash
' Purpose : Remove any existing terminating backslash from a path.
'*********************************************************************************
Friend Function RemoveSlash(ByVal strPath As String) As String
If strPath.Last = "\"c Then 'already have a backslash?
Return strPath.Substring(0, strPath.Length - 1) 'yes, so remove trailing slash if it exists.
Else
Return strPath 'otherwise, simply return the string.
End If
End Function

End Module

'***********************************************************************************
'***********************************************************************************
' DriveItem Class (NOTE: You may need to add 'Imports System' if VB2010 or previous.)
' Keep track of drive info in a Drive-Based ComboBox.
'***********************************************************************************
'***********************************************************************************
Public Class DriveItem
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Friend Drive As String 'root path, such as C:\.
Friend Volume As String 'volume name for the drive.
Friend Type As IO.DriveType 'the drive type flag.
Friend ImageIndex As Images 'the image index for the drive type.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method Name : New
' Purpose : Set up the class using a DriveInfo structure.
'*******************************************************************************
Friend Sub New(ByVal DrvInfo As IO.DriveInfo)
Me.Drive = RemoveSlash(DrvInfo.Name) 'save drive (C:\).
Me.Volume = DrvInfo.VolumeLabel 'save volume label.
Me.Type = DrvInfo.DriveType 'save the drive type enumeration.
'-----------------------------------------------
'now compute the image index for the drive type.
'-----------------------------------------------
Select Case Me.Type
Case IO.DriveType.Fixed
Me.ImageIndex = Images.Fixed
Case IO.DriveType.CDRom
Me.ImageIndex = Images.CDRom
Case IO.DriveType.Removable
Me.ImageIndex = Images.Removable
 Page 692

Case IO.DriveType.Ram
Me.ImageIndex = Images.Ram
Case IO.DriveType.Network
Me.ImageIndex = Images.Network
Case Else
Me.ImageIndex = Images.Fixed
End Select
End Sub

'*******************************************************************************
' Method Name : New (overload)
' Purpose : Set up the class using a Text Name for generic items, such as Desktop, Documents, Music, etc.
'*******************************************************************************
Friend Sub New(ByVal DrvInfo As String)
Select Case DrvInfo.Trim.ToLower
Case "desktop"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
Me.Volume = "Desktop" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = Images.Desktop 'set image index.
Case "documents"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
Me.Volume = "Documents" 'volume label
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration
Me.ImageIndex = Images.Documents 'set image index
Case "downloads"
Me.Drive = Environment.GetEnvironmentVariable("USERPROFILE") & "\Downloads"
Me.Volume = "Downloads" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = Images.Downloads 'set image index.
Case "music"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.MyMusic)
Me.Volume = "Music" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = Images.Music 'set image index.
Case "pictures"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)
Me.Volume = "Pictures" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = Images.Pictures 'set image index.
Case "videos"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos)
Me.Volume = "Videos" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = Images.Videos 'set image index.
Case "recentplaces"
Me.Drive = Environment.GetFolderPath(Environment.SpecialFolder.Recent)
Me.Volume = "Recent Places" 'volume label.
Me.Type = IO.DriveType.Unknown 'set the drive type enumeration.
Me.ImageIndex = Images.Recent 'set image index.
End Select
End Sub

'*******************************************************************************
' Method Name : ToString
' Purpose : Return default string data.
'*******************************************************************************
Public Overrides Function ToString() As String
If Me.Type = IO.DriveType.Unknown Then 'virtual drive, suck as desktop, etc.
Return Me.Volume
Else 'otherwise a logical drive.
Return Me.Volume & " (" & Me.Drive & ") [" & Me.Type.ToString & " Drive]"
End If
End Function

End Class

'*************************************************************************************
'*************************************************************************************
' modGetIconFromFile Static Class Module
' Extract Icons, Images, Cursors from a Filepath.
'*************************************************************************************
'*************************************************************************************
Module modGetIconFromFile
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
'INTEROP METHOD: ExtractIconEx(lpszFile,nIconIndex,*phiconLarge,*phiconSmall,nIcons) As Int32
' Creates an array of handles to large or small icons extracted from the specified executable file, DLL, or icon file.
'
'PARAMETERS:
'lpszFile [in, ByVal], Type: String:
' The name of an executable file, DLL, or icon file from which icons will be extracted.
'
'nIconIndex [in, ByVal], Type: Int32:
' The zero-based index of the first icon to extract. For example, if this value is zero, the function extracts
' the first icon in the specified file. If this value is –1 and phiconLarge and phiconSmall are both IntPtr.Zero,
 Page 693

' the function returns the total number of icons in the specified file. If the file is an executable file or DLL,
' the return value is the number of RT_GROUP_ICON resources. If the file is an .ico file, the return value is 1.
' If this value is a negative number and either phiconLarge or phiconSmall is not IntPtr.Zero, the function begins
' by extracting the icon whose resource identifier is equal to the absolute value of nIconIndex. For example, use
' -3 to extract the icon whose resource identifier is 3.
'
'phiconLarge [out, optional, ByRef], Type: IntPtr() or IntPtr or IntPtr.Zero:
' A single IntPtr or a sized IntPtr array for icon handles that receives handles to the large icons extracted from
' the file. If this parameter is IntPtr.Zero, no large icons (32x32, 48x48) are extracted from the file.
'
'phiconSmall [out, optional, ByRef], Type: IntPtr() or IntPtr or IntPtr.Zero:
' A single IntPtr or a sized IntPtr array for icon handles that receives handles to the small icons extracted from
' the file. If this parameter is NULL, no small icons (16x16) are extracted from the file.
'
'nIcons [in, ByVal], Type: UInt32:
' The number of icons to be extracted from the file.
Private Declare Function ExtractIconEx Lib "shell32.dll" Alias "ExtractIconExA" (
ByVal lpszFile As String,
ByVal nIconIndex As Int32,
ByRef phiconLarge As IntPtr,
ByRef phiconSmall As IntPtr,
ByVal nIcons As UInt32) As Int32
'- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
'INTEROP METHOD: DrawIconEx(hdc,xLeft,yTop,hIcon,cxWidth,cyHeight,istepIfAniCur,hbrFlickerFreeDraw,diFlags) As Boolean
'Draws an icon or cursor into the specified device context, performing the specified raster operations, and stretching
'or compressing the icon or cursor as specified.

'PARAMETERS:
'hdc [in, ByVal], Type: IntPtr:
' A handle to the device context into which the icon or cursor will be drawn. Use the GetHdc property from your
' Graphics interface object for this.
'
'xLeft [in, ByVal], Type: Int32:
' The logical x-coordinate of the upper-left corner of the icon or cursor.
'
'yTop [in, ByVal], Type: In32:
' The logical y-coordinate of the upper-left corner of the icon or cursor.
'
'hIcon [in, ByVal], Type: IntPtr:
' A handle to the icon or cursor to be drawn. This parameter can identify an animated cursor.
'
'cxWidth [in, ByVal], Type: IntPtr:
' The logical width of the icon or cursor. If this parameter is zero and the diFlags parameter is DI_DEFAULTSIZE,
' the function uses the SM_CXICON system metric value to set the width. If this parameter is zero and
' DI_DEFAULTSIZE is not used, the function uses the actual resource width.
'
' cyHeight [in, ByVal], Type: Int32:
' The logical height of the icon or cursor. If this parameter is zero and the diFlags parameter is DI_DEFAULTSIZE,
' the function uses the SM_CYICON system metric value to set the height. If this parameter is zero and
' DI_DEFAULTSIZE is not used, the function uses the actual resource height.
'
'istepIfAniCur [in, ByVal], Type: UInt32:
' The index of the frame to draw, if hIcon identifies an animated cursor. This parameter is ignored if hIcon does
' not identify an animated cursor.
'
'hbrFlickerFreeDraw [in, optional, ByVal], Type: IntPtr:
' A handle to a brush that the system uses for flicker-free drawing. If hbrFlickerFreeDraw is a valid brush
' handle, the system creates an offscreen bitmap using the specified brush for the background color, draws the
' icon or cursor into the bitmap, and then copies the bitmap into the device context identified by hdc. If
' hbrFlickerFreeDraw is NULL, the system draws the icon or cursor directly into the device context.
'
'diFlags [in, ByVal], Type: UInt32:
' The drawing flags. This parameter can be one of the following values.
' --Value-- --Meaning--
' DI_MASK (1) Draws the icon or cursor using the mask.
' DI_IMAGE (2) Draws the icon or cursor using the image.
' DI_NORMAL (3) Combination of DI_IMAGE and DI_MASK.
' DI_COMPAT (4) This flag is ignored.
' DI_DEFAULTSIZE (8) Draws the icon or cursor using the width and height specified by the system metric
' values for icons, if the cxWidth and cyHeight parameters are set to zero. If this flag
' is not specified and cxWidth and cyHeight are set to zero, the function uses the actual
' resource size.
' DI_NOMIRROR (16) Draws the icon as an unmirrored icon. By default, the icon is drawn as a mirrored icon
' if hdc is mirrored.
Private Declare Function DrawIconEx Lib "user32" (ByVal hdc As IntPtr,
ByVal xLeft As Int32,
ByVal yTop As Int32,
ByVal hIcon As IntPtr,
ByVal cxWidth As Int32,
ByVal cyHeight As Int32,
ByVal istepIfAniCur As Int32,
ByVal hbrFlickerFreeDraw As IntPtr,
ByVal diFlags As Int32) As Boolean

Private Const DI_Mask As Integer = 1 'Draws the icon or cursor using the mask.
 Page 694

Private Const DI_Image As Integer = 2 'Draws the icon or cursor using the image.
Private Const DI_NORMAL As Integer = 3 'Combination of DI_IMAGE and DI_MASK.
'Private Const DI_COMPATL As Integer = 4 'This flag is ignored.
Private Const DI_DEFAULTSIZE As Integer = 8 'Draw icon or cursor using default system sizes.
Private Const DI_NOMIRROR As Integer = 8 'Draws the icon as an unmirrored icon even if HDC is mirrored.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method : GetIconCount
' Purpose : Get the number of icons in an EXE/DLL file.
'*******************************************************************************
Friend Function GetIconCount(ByVal FilePath As String) As Int32
Return ExtractIconEx(FilePath, -1&, IntPtr.Zero, IntPtr.Zero, 0&)
End Function

'*******************************************************************************
' Method : GetIconImage
' Purpose : Get an image from the EXE/DLL file icon.
'*******************************************************************************
Friend Function GetIconImage(ByVal FilePath As String, Optional ByVal Index As Int32 = 0) As Bitmap
Dim bmp As Bitmap
Dim g As Graphics

If GetIconCount(FilePath) <> 0 Then 'if it contains icons...


Dim hIcon As IntPtr 'icon handle used when extracting from executables.
ExtractIconEx(FilePath, Index, IntPtr.Zero, hIcon, 1&) 'get handle of file's 1st or indexed icon (small 16x16).
If CBool(hIcon) Then 'if it has a handle...
bmp = New Bitmap(16, 16) 'create a new 16x16-pixel drawing surface.
g = Graphics.FromImage(bmp) 'create a graphics interface for it.
Dim Result As Boolean = DrawIconEx(g.GetHdc,
0&, 0&, hIcon,
16, 16,
0&, IntPtr.Zero,
DI_NORMAL) 'draw icon to bitmap.
g.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!).
If Result Then 'if image was drawn...
Return bmp
Else
bmp.Dispose() 'otherwise dispose of bitmap resource.
End If 'and fall below.
End If
End If
'---------------------------------------------------------------
''failed, so try grabbing a 32x32 icon that is the default associated with the file.
'---------------------------------------------------------------
Try
bmp = Icon.ExtractAssociatedIcon(FilePath).ToBitmap 'failed, try grabbing 32x32 bitmap of associated icon.
If bmp IsNot Nothing Then 'if we got something...
Dim bm As New Bitmap(16, 16) 'create a new bitmap.
g = Graphics.FromImage(bm) 'define a graphics interface for it.
g.DrawImage(bmp, New Rectangle(0, 0, 16, 16)) 'draw 32x32 image scaled down to 16x16.
g.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!).
Return bm 'return image.
End If
Catch
End Try
Return Nothing 'report failure so default image will be used
End Function

'*******************************************************************************
' Method : GetCursorImage
' Purpose : Get a cursor as a 16x16 image.
'*******************************************************************************
Friend Function GetCursorImage(ByVal FilePath As String) As Bitmap
Dim cur As Cursor
Try
cur = New Cursor(FilePath) 'grab cursor from file.
Catch
Return Nothing 'some cursors are incompatible.
End Try
Dim bmp As New Bitmap(16, 16) 'create a drawing surface.
Dim g As Graphics = Graphics.FromImage(bmp) 'create a graphics interface for it.
cur.DrawStretched(g, New Rectangle(0, 0, 16, 16)) 'draw it to a 16x16 surface.
g.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!).
cur.Dispose()
Return bmp 'return the bitmap/image.
End Function

End Module

'*************************************************************************************
'*************************************************************************************

' modLaunchAssociatedApp Static Class Module


' Launch app associate with a file extension, or launch the app selected to open it.
 Page 695

' You can also get the app paths for the above options.
'*************************************************************************************
'*************************************************************************************
Module modLaunchAssociatedApp

'*******************************************************************************
' Method Name : LaunchSelectedOpenerForExt
' Purpose : Launch the application that is used to open a specified file extension
' : Return True if success, or False if the extension was not found or there
' : is no association, or the executable was not found where expected.
' : if TryDefaultIfNotFound = TRUE (default), then if a selected opener not
' : found, then try to launch the default associated app.
'*******************************************************************************
Friend Function LaunchSelectedOpenerForExt(ByVal Extension As String,
Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle.NormalFocus,
Optional ByVal TryDefaultIfNotFound As Boolean = True) As Boolean
Dim AppPath As String =
GetSelectedOpenerForExt(Extension, TryDefaultIfNotFound) 'get the path associated with an extension.
If Not String.IsNullOrWhiteSpace(AppPath) Then 'if one was found...
Shell(AppPath, DisplayStyle) 'launch the associated application, less extensions.
Return True 'return success.
Else
Return False 'fail.
End If
End Function

'*******************************************************************************
' Method Name : LaunchDefaultOpenerForExt
' Purpose : Launch the application that is associated with a soecified file extension
' : Return True if success, or False if the extension was not found or there
' : is no association, or the executable was not found where expected.
'*******************************************************************************
Friend Function LaunchDefaultOpenerForExt(ByVal Extension As String,
Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle.NormalFocus) As Boolean
Dim AppPath As String = GetDefaultOpenerForExt(Extension) 'get default associated app.
If Not String.IsNullOrWhiteSpace(AppPath) Then 'if we found it...
Shell(AppPath, DisplayStyle) 'launch the associated application, less extensions.
Return True 'return success.
End If
Return False 'return failure if errors were detected.
End Function

'*******************************************************************************
' Method Name : GetSelectedOpenerForExt
' Purpose : Get the path to the application that is associated with a soecified file extension.
'*******************************************************************************
Friend Function GetSelectedOpenerForExt(ByVal Extension As String,
Optional ByVal TryDefaultIfNotFound As Boolean = True) As String
Extension = CleanExtension(Extension) 'clean up extension or extract extension.
Try
' get the the Registry key for the sought extension.
Dim ExtKey As Microsoft.Win32.RegistryKey = My.Computer.Registry.CurrentUser.OpenSubKey(
"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" & Extension).OpenSubKey("OpenWithList")
Dim AppPath As String
' get the name of the application associated with the extension.
AppPath = ExtKey.GetValue(ExtKey.GetValue("MRUList").ToString.Substring(0, 1)).ToString
' get the EXE path to the application.
AppPath = My.Computer.Registry.ClassesRoot.OpenSubKey(
"Applications\" & AppPath & "\shell\open\command").GetValue(Nothing).ToString
If AppPath IsNot Nothing Then 'if we found something...
Return StripExts(AppPath) 'return path to associated file, less decorations.
ElseIf TryDefaultIfNotFound Then 'did not, but if should we try default association...
Return GetDefaultOpenerForExt(Extension) 'then try to get app associated with extension.
End If
Catch
If TryDefaultIfNotFound Then 'did not, but if we should try default association...
Return GetDefaultOpenerForExt(Extension) 'then try to get app associated with extension.
End If
End Try
Return String.Empty 'fail.
End Function

'*******************************************************************************
' Method Name : GetDefaultOpenerForExt
' Purpose : Get path to default app associates with an extension.
'*******************************************************************************
Friend Function GetDefaultOpenerForExt(ByVal Extension As String) As String
Try
Extension = CleanExtension(Extension) 'clean up extension or extract extension.
' get the default key associated with the extension.
Dim AppPath As String = My.Computer.Registry.ClassesRoot.OpenSubKey(Extension).GetValue(Nothing).ToString
' get the default command used for launching the associated application.
' trim any trailing decorations, such as "%1", or parameters, such as /n.
Return StripExts(My.Computer.Registry.ClassesRoot.OpenSubKey(AppPath &
"\shell\open\command").GetValue(Nothing).ToString)
 Page 696

Catch
End Try
Return String.Empty
End Function

'*******************************************************************************
' Method Name : CleanExtension (support)
' Purpose : make sure we have only an extension.
'*******************************************************************************
Friend Function CleanExtension(ByVal Extension As String) As String
If String.IsNullOrWhiteSpace(Extension) Then 'if the data does not exist...
Return String.Empty
Else
Dim IndexOfDot As Int32 = Extension.LastIndexOf("."c) 'get the last index of the dot, if it has one.
If Extension.IndexOf("\"c) = -1 Then 'if there is no pathing involved...
If IndexOfDot = -1 Then 'but a dot is not found...
Return "." & Extension 'then precede it with a "." if it lacks one.
End If
ElseIf IndexOfDot <> 0 Then 'if dot is not at the start of the string...
Return IO.Path.GetExtension(Extension.Trim.ToLower) 'then ensure we have just the extension.
End If
Return Extension 'did not need to do anything.
End If
End Function

'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' Method Name : StripExts (support)
' Purpose : Strip registery extensions to a filepath, such as "%1" and such.
'+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Friend Function StripExts(ByVal AppPath As String) As String
Dim SlIdx As Int32 = AppPath.IndexOf("/"c) 'get possible slash index, such as Word uses.
Dim Index As Int32 = AppPath.IndexOf("""%") 'check for typical decorations.
If Index = -1 Then
Index = AppPath.IndexOf("%") 'if robust type not found, check for typical...
End If
If Index = -1 OrElse
(SlIdx <> -1 AndAlso SlIdx < Index) Then 'if no typical or slash index defined land less than index...
Index = SlIdx 'then try using slash index.
End If
If Index <> -1 Then 'if decorations found...
AppPath = AppPath.Substring(0, Index) 'then trim them off.
End If
If String.IsNullOrWhiteSpace(AppPath) Then 'some command strings are "%1" "%*" and the like...
Return String.Empty 'ignore these.
End If
If AppPath.Substring(0, 1) = """"c Then 'some are more complex, like...
' Example: "C:\WINDOWS\System32\rundll32.exe" "C:\WINDOWS\System32\msrating.dll",ClickedOnRAT %1".
Dim Idx As Int32 = AppPath.IndexOf(""""c, 1)
If Idx <> -1 Then
AppPath = AppPath.Substring(0, Idx + 1) 'strip to just base path.
End If
End If
Return AppPath.Trim(Chr(0), " "c, """"c) 'return the path and remove any decorations.
End Function

End Module

'******************************************************************************
' Copyright © 2014 - 2021 David Ross Goben. All rights reserved.
'******************************************************************************
 Page 697

Black Book Tip # 53


Determining If a Computer Has an Internet Connection.
As opposed to the dial-up-only connections of the early 1980s and 1990s, internet connections are so
common anymore that it surprises many, having become accustomed to all telecom services, from land-
lines, cable, wireless phone, to satellite, providing internet service that the thought of needing to check
for an internet connection seems an antiquated idea. But when I first moved from Southwest Florida to
Central Florida in 2005, I lived for almost a year “out in the sticks”, suiting Jeff Foxworthy’s remark,
“You might be a redneck if... directions to your house include the words ‘Turn off the paved road...’” I
had gone from being on-line all the time with a fast cable internet service to living under ‘primitive’
telecommunications conditions, once again using a Dial-up service and implementing commando-style
internet connection techniques: “get on, do one’s business furiously, and then get off.” As one would
expect, only after I moved “to town”, returning to the world of super-fast and always-on cable internet
connections did my previous residence finally get DSL service (Digital Subscriber Line) made available
as its old copper wires were updated to fiber-optic cables (chalk it up to Murphy's Law). Now, having
moved yet again to Kissimmee, FL, I have been intentionally living off the grid for almost two years,
even as I write this, in order to reorganize my thinking processes and reconnect to the Father/Mother of
the Beginnings. It is incredible the spiritual and cognitive awakening we experience. You should try it!
Most times, we can rely on the fact that most-all networks anymore make an internet connection
available to their clients, and so we can invoke a simple function like the following:
'*******************************************************************************
' Method : CheckNetworkConnection
' Purpose : Return True if the computer is connected to a network.
'*******************************************************************************
Friend Function CheckNetworkConnection() As Boolean
Return My.Computer.Network.IsAvailable()
End Function

If invoking the function CheckNetworkConnection returns True, then we are connected to a network.
However, in some instances we are sometimes actually in a home network that might not have an active
internet connection via cable or DSL, except for on-demand Dial-up or satellite internet service.
Thus, we should dig deeper into the system. If the computer has any kind of internet service it will sport
a DLL in its system folder ( Environment.GetFolderPath(Environment.SpecialFolder.System)) named
WinInet.dll. This DLL is loaded when one installs internet service or when our system recognizes that
internet service is available. If the system does not find this file, then it does not have internet service
available.
To check for the existence of this DLL is really easy. For example:
'*******************************************************************************
' Method : HaveWinInetDll
' Purpose : Return True if the computer has WinInet.DLL available.
'*******************************************************************************
Friend Function HaveWinInetDll() As Boolean
Return IO.File.Exists(Environment.GetFolderPath(Environment.SpecialFolder.System) & "\wininet.dll")
End Function

If we do have it, that is still no guarantee that the system presently has it, but at least we can use this
DLL’s InternetGetConnectedState method to easily check for the system’s current internet status.
The DLL’s InternetGetConnectedState function takes two Integer parameters. The first should be an
integer variable that will receive status flags. The second integer is reserved and we should always set it
to zero. This function also returns a Boolean flag that indicates if the internet was detected or not. If this
result is True, then it has determined that the system has a live internet connection. For example:
 Page 698

Private Declare Function InternetGetConnectedState Lib "wininet.dll" (ByRef icsFlags As Integer,


ByVal dwReserved As Integer) As Boolean

'*******************************************************************************
' Method Name : SystemHasInternet
' Purpose : Return True if the system is connected to the Internet.
'*******************************************************************************
Friend Function SystemHasInternet(ByRef Ics As Integer) As Boolean
Ics = 0 'init Internet Connection State to no connection.
If HaveWinInetDll() Then 'see if wininet.dll file is available...
Try
Return InternetGetConnectedState(Ics, 0) 'check connection and return True if we have the Internet.
Catch
End Try
End If
Return False 'failed. No internet service is active.
End Function

Of course, whether we succeed or fail to find an active Internet connection, it is also useful to know
what is going on. The Ics integer will be non-zero if there is any sort of internet connection state, even if
it is to report that it exists but is presently offline. A value of zero indicates that there is no service.
The first 7 bits of this status field are set aside for status flags, leaving room for possible later expansion
in the other bits. Even so, of these 7 defined bits, bit 3 has been retired and another, bit 7, with the dawn
of cable-based internet, has become unreliable. These 7 bits are defined as follows:
INTERNET_CONNECTION_MODEM = 1 '00000001 binary – Bit 0 - Dial-up or DSL.
INTERNET_CONNECTION_LAN = 2 '00000010 binary – Bit 1 - Local Area Network.
INTERNET_CONNECTION_PROXY = 4 '00000100 binary – Bit 2 - Network server funnels service to clients.
INTERNET_CONNECTION_MODEM_BUSY = 8 '00001000 binary – Bit 3 - (NO LONGER USED).
INTERNET_RAS_INSTALLED = 16 '00010000 binary – Bit 4 - Remote Access Service.
INTERNET_CONNECTION_OFFLINE = 32 '00100000 binary – Bit 5 - Not presently connected.
INTERNET_CONNECTION_CONFIGURED = 64 '01000000 binary – Bit 6 - (UNRELIABLE/NOT NEEDED IF LAN/RAS DETECTED).

We can better manage these in an enumeration and apply it to our Pinvoke declaration:
Private Declare Function InternetGetConnectedState Lib "wininet.dll" (ByRef icsFlags As icState,
ByVal dwReserved As Integer) As Boolean

Friend Enum icState As Int32


icNONE = 0 'No connection
icMODEM = 1 '00000001 binary (INTERNET_CONNECTION_MODEM).
icLAN = 2 '00000010 binary (INTERNET_CONNECTION_LAN).
icPROXY = 4 '00000100 binary (INTERNET_CONNECTION_PROXY).
'icBUSY = &H8 '00001000 binary (INTERNET_CONNECTION_MODEM_BUSY - NO LONGER USED!).
icRASINSTALLED = 16 '00010000 binary (INTERNET_RAS_INSTALLED).
icOFFLINE = 32 '00100000 binary (INTERNET_CONNECTION_OFFLINE).
icCONFIGURED = 64 '01000000 binary (INTERNET_CONNECTION_CONFIGURED - UNRELIABLE/NOT NEEDED IF LAN/RAS DETECTED).
End Enum

We can check for any of these bits by performing an AND command on the returned Ics integer:
Dim sName As String = String.Empty 'init text result.
If connectionState Then 'if there is a connection...
If CBool(Ics And icState.icRASINSTALLED) Then 'then build a report of the type.
sName &= "RAS "
End If
If CBool(Ics And icState.icMODEM) Then
sName &= "Modem "
End If
If CBool(Ics And icState.icLAN) Then
sName &= "LAN "
End If
If CBool(Ics And icState.icPROXY) Then
sName &= "Proxy "
End If
If CBool(Ics And icState.icOFFLINE) Then
sName &= "- Offline "
End If
If (Ics And (icState.icLAN Or icState.icRASINSTALLED)) = 0 AndAlso
(Ics And icState.icCONFIGURED) = 0 Then
sName &= "- Not Configured"
End If
End If
If sName.Length = 0 Then
sName = "Not Configured"
End If
 Page 699

We can put this all together into a module for general access:
Option Strict On
Option Explicit On

Module modCheckInternetConnect
'**************************************************************************
'Check internet connections on the local system.
'**************************************************************************
'This module provides the following 7 useful functions:
'
' CheckNetworkConnection() : Return TRUE if the computer is connected to a network.
' HaveWinInetDll() : Return TRUE if the computer has WinInet.DLL available.
' ComputerHasInternet() : Return TRUE if the computer is connected to the Internet.
' InternetConnected() : Return TRUE if the computer is connected to the internet and return status string.
' CheckInternetConnect() : Return TRUE if the computer is connected to the internet (simple).
' CheckInternetConnectType(): Get Internet Connection flags as type icState.
' CheckInternetConnectName(): Get the Connection Name as a string (i.e., LAN).
'*******************************************************************************

'**************************************************************************
' Pinvoke and structure required to gather information for an internet connection.
'**************************************************************************
Private Declare Function InternetGetConnectedState Lib "wininet.dll" (ByRef icsFlags As icState,
ByVal dwReserved As Integer) As Boolean
'Ics (Internet Connection State) return flags.
Friend Enum icState As Int32
icNONE = 0 'No connection.
icMODEM = 1 '00000001 binary (INTERNET_CONNECTION_MODEM). Dial-Up or DSL.
icLAN = 2 '00000010 binary (INTERNET_CONNECTION_LAN). Local Area Network.
icPROXY = 4 '00000100 binary (INTERNET_CONNECTION_PROXY). Proxy Server.
'icBUSY = 8 '00001000 binary (INTERNET_CONNECTION_MODEM_BUSY - NO LONGER USED!).
icRASINSTALLED = 16 '00010000 binary (INTERNET_RAS_INSTALLED). Remote Access Service.
icOFFLINE = 32 '00100000 binary (INTERNET_CONNECTION_OFFLINE). Offline.
icCONFIGURED = 64 '01000000 binary (INTERNET_CONNECTION_CONFIGURED - UNRELIABLE/NOT NEEDED IF LAN/RAS DETECTED).
End Enum

'*******************************************************************************
' Method : CheckNetworkConnection
' Purpose : Return True if the computer is connected to a network.
'*******************************************************************************
Friend Function CheckNetworkConnection() As Boolean
Return My.Computer.Network.IsAvailable()
End Function

'*******************************************************************************
' Method : HaveWinInetDll
' Purpose : Return TRUE if the computer has WinInet.DLL available.
'*******************************************************************************
Friend Function HaveWinInetDll() As Boolean
Return IO.File.Exists(Environment.GetFolderPath(Environment.SpecialFolder.System) & "\wininet.dll")
End Function

'*******************************************************************************
' Method Name : ComputerHasInternet
' Purpose : Return TRUE if the computer is connected to the Internet.
' : It also returns status flags in its Ics parameter.
'*******************************************************************************
Friend Function ComputerHasInternet(ByRef Ics As icState) As Boolean
Ics = 0 'init Internet Connection State to no connection.
If HaveWinInetDll() Then 'see if wininet.dll file is available...
Try
Return InternetGetConnectedState(Ics, 0) 'check connection and return True if we have the Internet.
Catch
End Try
End If
Return False 'failed
End Function

'*******************************************************************************
' Method : InternetConnected
' Purpose : Return TRUE if the computer is connected to the Internet.
' : It also returns status flags in its eConnectionInfo parameter.
' : It also optionally returns a connection type in its sConnectionName string, such as "Modem" or "LAN".
'*******************************************************************************
Friend Function InternetConnected(ByRef eConnectionInfo As icState,
Optional ByRef sConnectionName As String = Nothing) As Boolean

Dim Ics As icState = icState.icNONE 'init Internet Connection State to no connection.


Dim connectionActive As Boolean = ComputerHasInternet(Ics) 'see if internet service is available.
sConnectionName = String.Empty 'init text result.
If connectionActive Then 'if it is...
If CBool(Ics And icState.icRASINSTALLED) Then 'then build a report of the type.
sConnectionName &= "RAS "
 Page 700

End If
If CBool(Ics And icState.icMODEM) Then
sConnectionName &= "Modem "
End If
If CBool(Ics And icState.icLAN) Then
sConnectionName &= "LAN "
End If
If CBool(Ics And icState.icPROXY) Then
sConnectionName &= "Proxy "
End If
If CBool(Ics And icState.icOFFLINE) Then
sConnectionName &= "- Offline "
End If
If (Ics And (icState.icLAN Or icState.icRASINSTALLED)) = 0 AndAlso
(Ics And icState.icCONFIGURED) = 0 Then
sConnectionName &= "- Not Configured"
End If
If sConnectionName.Length = 0 Then
sConnectionName = "Not Configured"
End If
sConnectionName = sConnectionName.TrimEnd 'get optional result text.
eConnectionInfo = DirectCast(Ics And &H77, icState) 'return IC flags.
' &H77 = 0111 0111 binary.
' ---- ----
' 0||| 0|||
' ||| ||+--1 MODEM.
' ||| |+--2 LAN.
' ||| +--4 PROXY.
' ||+--16 RAS_INSTALLED.
' |+--32 CONNECTION_OFFLINE.
' +--64 CONNECTION_CONFIGURE (UNRELIABLE).
End If
Return connectionActive 'return True if there is a connection.
End Function

'*******************************************************************************
' Method : CheckInternetConnect
' Purpose : Returns TRUE if the computer has an active internet connection (simple).
'*******************************************************************************
Friend Function CheckInternetConnect() As Boolean
Return InternetConnected(New icState)
End Function

'*******************************************************************************
' Method : GetInternetConnectType
' Purpose : Get connection type flags
' : Returns an integer. By ANDing values specified in icState, you can check specific states.
' : A result of 0 means no connection. You might want to check for an internet connection
' : first using CheckInternetConnect() above.
'*******************************************************************************
Friend Function GetInternetConnectType() As icState
Dim Ics As icState 'flag field to resutn Internet Connection State.
If InternetConnected(Ics) Then 'if the check successed...
Return Ics 'return flags.
Else
Return 0 'fail.
End If
End Function

'*******************************************************************************
' Method : GetInternetConnectName
' Purpose : Get connection name.
'
' NOTE: You might want to check for an internet connection
' first using CheckInternetConnect() above.
'*******************************************************************************
Friend Function GetInternetConnectName() As String
Dim Ics As icState 'flag field to resutn Internet Connection State.
Dim sName As String = Nothing 'connection name.

If InternetConnected(Ics, sName) Then 'determine whether we have a connection:


Return sName 'we do.
Else
Return Nothing 'we do not.
End If
End Function

End Module

This module provides the CheckNetworkConnection, HaveWinInetDLL, and ComputerHasInternet


functions that we have already outlined. It also features four other methods.
 Page 701

InternetConnected is a general purpose method, It invokes ComputerHasInternet (which in turn invokes


HaveWinInetDLL), but then it evaluates the returned ICS (Internet Connection State) flags and builds a
status string that informs us of what type of connection we have, and if it is not online or is not
connected. For example:
Dim Ics As icState 'Internet Connection State.
Dim sName As String = Nothing 'result string.
Dim Connected As Boolean = InternetConnected(Ics, sName) 'get status.
MsgBox(sName) 'display connection type.

If we have a Dial-up Modem or DSL service, it might report “Modem” if it is connected, or else
“Modem – Offline”. Cable internet providers typically deliver service by way of a Remote Access
Service within a Local Area Network, and so we will likely see a status of “RAS LAN”. If we see “Not
Connected”, then the user does not have an active internet connection. Of course, we can also ascertain
this from the Boolean result stored in the Connected integer. We can compare the value set in the Ics
integer variable, as demonstrated above, by ANDing members of the icState enumeration to check
specific flags. If the Ics integer is zero, then there is no connection.
CheckInternetConnect is a simpler version of ComputerHasInternet,delivering only a Boolean result,
indicating if we have an active internet connection or not.
CheckInternetType is also a simple version of ComputerHasInternet, but it returns an integer of type
icState, which holds the Internet Connection State flags of the computer. We can compare this value by
ANDing members of the icState enumeration to check specific flags. If the Ics integer is zero, then there
is no connection.
CheckInternetConnect is another simple version of ComputerHasInternet, but it delivers only a string
result, which describes the connection type.
With these methods, we have more than enough in our toolbox to easily determine the user’s internet
connection state.
 Page 702

Black Book Tip # 54


Manipulating Color Value Members with Ease.
The ToArgb property of a Color or SystemColors structure is really useful to break individual color
values out of a color and to also extract the actual color values defined in a SystemColors member. The
problem is if we want to manipulate a value, such as change its Alpha component value to make a color
more transparent, and then stuff the value back into a color variable using its FromArgb method.
To do just a bit of this, it is easy enough to do by hand. For example, to set the variable Clr of type Color
to an Alpha component value of 64 (1/4 transparent) is as simple as “Clr = Color.FromArgb(64, Clr)”.
However, it starts getting a bit convoluted when we try much more than that, such as manipulating its
individual Red, Green, and Blue color values, especially when we are performing a dithering operation
on the colors of a landscape and we need to add or subtract an offset. In that case, it would be easier to
have a ready object where we can inspect, set, and alter its individual color members at will.
For that reason, I created a structure named ARGB that can deliver all these services. We can create a
new ARGB structure and initialize it with a color, manipulate it at will, and then gather the result as an
integer, as a color, or even set or extract a Win32 RGB color value, which is structured differently than a
Color’s Argb value.
An Argb value is structured in the hexadecimal format &HAARRGGBB, where AA = 2 hex digits for the
Alpha component value (&h00 - &hFF; 0-255), RR = 2 hex digits for the 0-255 Red color value, GG = 2
hex digits for the 0-255 Green color value, and BB = 2 hex digits for the 0-255 Blue color value.
A Win32 Rgb value is structured in the hexadecimal format &H00BBGGRR, where BB = 2 hex digits for
the Blue color value, GG = 2 hex digits for the Green color value, and RR = 2 hex digits for the Red color
value.
We might notice from the above that in the Argb and Rgb definitions that color values Red and Blue are
swapped. Even so, I still see many gurus on the web advising people “that all you need to do is AND
&HFFFFFF to a color’s ToArgb value to get a Win32 RGB value.” Maybe that is why there is a lot of
anger on some of those forums when that advice leads to some interesting, though not expected results.
The great thing about structures under .NET is that they can feature methods, properties, and
constructors. Although ARGB is a structure, cleaning up easily in memory when it goes out of scope, we
can very easily define it as a class instead, to give the object persistence as long as it is referenced by
something. I will leave that to you to do if you choose to make it a class. It is very easy to do.
Even as a structure, an ARGB object is very easy to manipulate. We can even create it much as we would
a regular Color structure, but easier! For example:
Dim ClrA As New ARGB(Color.RoyalBlue) 'init new ARGB structure with Royal Blue.
Dim ClrB As New ARGB(64, Color.LightBlue) 'init Light Blue with Alpha Component at 25% opacity.
Dim ClrC As New ARGB(128, 255, 0, 240) 'init Light Megenta at 50% opacity.
Dim ClrD As New ARGB(255, 0, 255) 'init Magenta with full opacity.
Dim vArgb As Integer = ClrA.Argb 'get integer value of Royal Blue.
ClrB.Color = ClrA.Color 'replace Light Blue with Royal Blue.
ClrB.Argb = vArgb 'replace Light Blue with Argb integer version of Royal Blue
ClrB.RGB = &HFF0000 'win32 RGB Blue (&H00FF0000) to Argb Blue (&HFF0000FF).
Dim vRgb As Integer = ClrC.RGB 'convert Argb (&H80FF00DC) to RGB (&H00DC00FF).
ClrD.Blue = 128 'replace Blue 255 value with 128.
ClrD.Alpha = 64 'change Alpha Component from 255 to 64.
Dim clr As Color = ClrD.Color 'transform changes to a color value.

The modARGB module containing the ARGB structure is defined on the next page:
 Page 703

Option Strict On
Option Explicit On

Module modARGB
'*******************************************************************************
' Convert an ARGB color value to and from individual colors.
'*******************************************************************************
' modARGB - Convert an ARGB color value back to individual colors. The intrinsic
' Color.ToARGB function provides a convenient tool for converting separate
' Alpha, Red, Green, and Blue colors into an Int32 value, but there is no
' ready way to take an integer value and break these elements back out.
'
' This structure is rich in properties to assign colors, Argb integer, or
' individual color values to the structure, manipulate them, and return them
' as you require them. You can even set and get color values used by Win32
' RGB integer values.
'*******************************************************************************

'*******************************************************************************
' Structure: ARGB
' Purpose : Provide color interface to easily manipulate color values.
'*******************************************************************************
Friend Structure ARGB
Private _argbAlpha As Int32 'ALPHA blend value (0-255).
Private _argbRed As Int32 'Red Color Depth (0-255).
Private _argbGreen As Int32 'Green Color Depth (0-255).
Private _argbBlue As Int32 'Blue Color Depth (0-255).

'*******************************************************************************
' Structure: New
' Purpose : Create a new ARGB structure and initialize its values with a color.
' Example : Dim Clr As New ARGB(Me.BackColor)
'*******************************************************************************
Friend Sub New(ByVal Color As Color)
Me.Color = Color
End Sub

'*******************************************************************************
' Structure: New
' Purpose : Create a new ARGB structure and initialize its values with a
' : specified Alpha blend value and a color.
' Example : Dim Clr As New ARGB(64, Me.BackColor)
'*******************************************************************************
Friend Sub New(ByVal Alpha As Integer, ByVal Color As Color)
Me.Color = Color
Me.Alpha = Alpha
End Sub

'*******************************************************************************
' Structure: New
' Purpose : Create a new ARGB structure and initialize its values with a
' : specified Alpha blend, Red, Green, and Blue values.
' Example : Dim Clr As New ARGB(255, 64, 128, 255)
'*******************************************************************************
Friend Sub New(ByVal Alpha As Integer, ByVal Red As Integer, ByVal Green As Integer, ByVal Blue As Integer)
Me.Alpha = Alpha 'set Alpha blend,
Me.Red = Red 'set Red color,
Me.Blue = Blue 'set Blue color,
Me.Green = Green 'set Green color,
End Sub

'*******************************************************************************
' Structure: New
' Purpose : Create a new ARGB structure and initialize its values with a
' : specified Red, Green, and Blue values (Alpha set to 255; fully opaque).
' Example : Dim Clr As New ARGB(64, 128, 255)
'*******************************************************************************
Friend Sub New(ByVal Red As Integer, ByVal Green As Integer, ByVal Blue As Integer)
Me._argbAlpha = 255 'set fully opaque,
Me.Red = Red 'set Red color,
Me.Blue = Blue 'set Blue color,
Me.Green = Green 'set Green color,
End Sub

'*******************************************************************************
' Structure: New
' Purpose : Create a new ARGB structure and initialize its values with an
' : integer argb value (&HAARRGGBB).
' Example : Dim Clr As New ARGB(-4934476)
'*******************************************************************************
Friend Sub New(ByVal Argb As Integer)
Me.Argb = Argb
End Sub
 Page 704

'*******************************************************************************
' Property : Alpha
' Purpose : Get/Set Alpha Blend value (0-255; &H00 - &HFF).
'*******************************************************************************
Friend Property Alpha As Integer
Get
Return Me._argbAlpha
End Get
Set(value As Integer)
Me._argbAlpha = value And &HFF
End Set
End Property

'*******************************************************************************
' Property : Red
' Purpose : Get/Set Red Color value (0-255; &H00 - &HFF).
'*******************************************************************************
Friend Property Red As Integer
Get
Return Me._argbRed
End Get
Set(value As Integer)
Me._argbRed = value And &HFF
End Set
End Property

'*******************************************************************************
' Property : Green
' Purpose : Get/Set Green Color value (0-255; &H00 - &HFF).
'*******************************************************************************
Friend Property Green As Integer
Get
Return Me._argbGreen
End Get
Set(value As Integer)
Me._argbGreen = value And &HFF
End Set
End Property

'*******************************************************************************
' Property : Blue
' Purpose : Get/Set Blue Color value (0-255; &H00 - &HFF).
'*******************************************************************************
Friend Property Blue As Integer
Get
Return Me._argbBlue
End Get
Set(value As Integer)
Me._argbBlue = value And &HFF
End Set
End Property

'*******************************************************************************
' Property : Color
' Purpose : Get/Set Color using Color value.
'*******************************************************************************
Friend Property Color As Color
Get
Return Color.FromArgb(Me._argbAlpha, Me._argbRed, Me._argbGreen, Me._argbBlue)
End Get
Set(value As Color)
Me.Argb = value.ToArgb
End Set
End Property

'*******************************************************************************
' Property : Argb
' Purpose : Get/Set Color using Argb integer value (&HAARRGGBB).
'*******************************************************************************
Friend Property Argb As Integer
Get
Return Me._argbAlpha << 24 + Me._argbRed << 16 + Me._argbGreen << 8 + Me._argbBlue
End Get
Set(value As Integer)
Me._argbAlpha = (value >> 24) And &HFF 'extract alpha value.
Me._argbRed = (value >> 16) And &HFF 'extract red value.
Me._argbGreen = (value >> 8) And &HFF 'extract green value.
Me._argbBlue = value And &HFF 'extract blue value.
End Set
End Property

'*******************************************************************************
' Property : RGB
' Purpose : Get/Set Color using Win32 RGB colors (&H00BBGGRR).
'*******************************************************************************
 Page 705

Friend Property RGB As Integer


Get
Return Me._argbBlue << 16 + Me._argbGreen << 8 + Me._argbRed
End Get
Set(value As Integer)
Me._argbAlpha = 255 'set fully opaque. RGB does not use this value.
Me._argbBlue = (value >> 16) And &HFF 'extract red value from RGB.
Me._argbGreen = (value >> 8) And &HFF 'extract green value from RGB.
Me._argbRed = value And &HFF 'extract blue value from RGB.
End Set
End Property
End Structure

End Module
 Page 706

Black Book Tip # 55


Converting Byte Sizes to Formatted Byte, KB, MB, GB, or TB Strings.
When processing file sizes, such as we get from a System.IO.FileInfo object’s Length property, which
returns the size of the file in bytes, it is more useful to report this size in manageable terms, such as
Kilobytes, Megabytes, Gigabytes, or Terabytes. For example, reporting 6956137122 for a file size might
not make much sense to the user. Even if we go to the trouble to format it, such as using this function:
Private Function GetFileSizeString(ByVal FilePath As String) As String
Try
If IO.File.Exists(FilePath) Then 'if file exists...
Dim fil As New IO.FileInfo(FilePath) 'get file info object for it.
Dim Amount As Long = fil.Length 'grab byte size of the file.
Return Amount.ToString("#,##0") 'return it as a formatted string.
End If
Catch
End Try
Return "0" 'fail (file not found).
End Function

It will report “6,956,137,122”, which is clearer but not much better. For the user, a report of “ 6.48 GB”
or, better, “6.48 GB (6,956,137,122 Bytes)” would make much more sense to them.
The trick for many, though, is to determine which format we should use, whether it is TB, GB, MB, KB,
or even just a byte count if it is under 1KB (1024 bytes).
Actually, this is very easy. We first need to break the Long Integer down into the various sizes, which we
can make even easier by using any previous calculation to simplify the next one. For example:
Dim KSize As Double = CDbl(Amount) / 1024.0# 'compute size in Kilobytes.
Dim MSize As Double = KSize / 1024.0# 'compute size in Megabytes.
Dim GSize As Double = MSize / 1024.0# 'compute size in Gigabytes.
Dim TSize As Double = GSize / 1024.0# 'compute size in Terabytes.

The next step is to determine the largest result that has a whole number value that is non-zero. Thus, we
would first check TSize, and if that fails, then GSize, then MSize, and finally KSize. For example, if
“Fix(TSize)”, which returns a whole number value of type Double, is not equal to 0.0# (or 0.0R), then
we will check the MSize value, and so on down the line to KSize.
If we do find a non-zero whole number, we will want to return a result with the appropriate size
reported. However, if all these tests fail, then we have a value that is less than 1 Kilobyte, so we simply
want to report its Byte size. For example:
Dim ByteSize As String = Amount.ToString("#,##0") & " Bytes" 'format Byte size.
Dim Result As String = String.Empty 'init result string.
If ShowByteSize Then 'if we want to report the byte size...
Result = " (" & ByteSize & ")" 'initialize with that size.
End If
If Fix(TSize) <> 0.0# Then 'if the value is in Terabyte range...
Return TSize.ToString("0.00") & " TB" & Result 'return a report for that size.
ElseIf Fix(GSize) <> 0.0# Then 'if the value is in Gigabyte range...
Return GSize.ToString("0.00") & " GB" & Result 'return a report for that size.
ElseIf Fix(MSize) <> 0.0# Then 'if the value is in Megabyte range...
Return MSize.ToString("0.00") & " MB" & Result 'return a report for that size.
ElseIf Fix(KSize) <> 0.0# Then 'if the value is in Kilobyte range...
Return KSize.ToString("0.00") & " KB" & Result 'return a report for that size.
Else
Return ByteSize 'else < 1KB, so return Byte size.
End If

Putting this altogether in a function named FormatKB, we can simply invoke it with our long value as a
parameter and it will report the size, such as replacing the “Return Amount.ToString("#,##0")” in the
previous GetFileSizeStreing method with “Return FormatKB(Amount)”, or “Return FormatKB(Amount, True)”
if we want the byte size to be included in the report.
My module, modFormatKB, which contains the FormatKB method, follows:
 Page 707

Option Strict On
Option Explicit On

Module modFormatKB
'Convert file size in bytes into proper strings such as "1.41 KB".
'****************************************************
' modFormatKB:
' The provided FormatKB functions helps you convert file size in bytes into proper strings such as
' "1.41 KB" or "1.32 MB". An optional Boolean flag allows the byte size to be appended. There are
' two versions of this method. One will accept an Integer value, and the other will accept a Long
' Integer value, as might be returned by a System.IO.FileInfo object.
'
'EXAMPLE:
' Dim MySize As Long = 3882445
' Debug.Print(FormatKB(MySize, True))
' The result prints "3.70 MB (3,882,445 Bytes)"
'
'NOTE: It printed 3.70 because 1KB=1024 bytes. 1MB=1,048,576 (1024 x 1024).
'****************************************************

'****************************************************
' Method : FormatKB (Accepts Int32 Value for byte count)
' Purpose : Helps you convert file size in bytes into proper strings such as "1.41 KB" or "1.32 MB."
' : To also include byte size (if not < 1K), then set optional parameter ByteSize to True.
'****************************************************
Public Function FormatKB(ByVal Amount As Int32,
Optional ByVal ShowByteSize As Boolean = False) As String
Return FormatKB(CLng(Amount), ShowByteSize) 'invoke Long version of method.
End Function

'****************************************************
' Method : FormatKB (Accepts Long Value for byte count)
' Purpose : Helps you convert file size in bytes into proper strings such as "1.41 KB" or "1.32 MB."
' : To also include byte size (if not < 1K), then set optional parameter ByteSize to True.
'****************************************************
Public Function FormatKB(ByVal Amount As Long,
Optional ByVal ShowByteSize As Boolean = False) As String
Dim KSize As Double = CDbl(Amount) / 1024.0# 'compute size in Kilobytes.
Dim MSize As Double = KSize / 1024.0# 'compute size in Megabytes.
Dim GSize As Double = MSize / 1024.0# 'compute size in Gigabytes.
Dim TSize As Double = GSize / 1024.0# 'compute size in Terabytes.

Dim ByteSize As String = Amount.ToString("#,##0") & " Bytes" 'format Byte size.
Dim Result As String = String.Empty 'init result string.
If ShowByteSize Then 'if we want to report the byte size...
Result = " (" & ByteSize & ")" 'initialize with that size.
End If
If Fix(TSize) <> 0.0# Then 'if the value is in Terabyte range...
Return TSize.ToString("0.00") & " TB" & Result 'return a report for that size.
ElseIf Fix(GSize) <> 0.0# Then 'if the value is in Gigabyte range...
Return GSize.ToString("0.00") & " GB" & Result 'return a report for that size.
ElseIf Fix(MSize) <> 0.0# Then 'if the value is in Megabyte range...
Return MSize.ToString("0.00") & " MB" & Result 'return a report for that size.
ElseIf Fix(KSize) <> 0.0# Then 'if the value is in Kilobyte range...
Return KSize.ToString("0.00") & " KB" & Result 'return a report for that size.
Else
Return ByteSize 'else < 1KB, so return Byte size.
End If
End Function

End Module
 Page 708

Black Book Tip # 56


Getting, Enumerating, and Changing Screen Settings.
Often developers need to get a list of all possible screen settings available for the system running their
application, perhaps to offer them as display options for a game so the user can play them in a higher or
lower resolution, or they simply need to gather more information than they can pick up from the
Screen.PrimaryScreen structure, or they need to change these settings. We are able to collect values for the
current display’s resolution in pixels using the Screen.PrimaryScreen structure’s Width and Height
parameters, and we can also compute its color depth using its BitsPerPixel parameter.
The bits per pixel value, or BPP, can be used to easily compute how many possible colors can be
assigned to each pixel on the display for the setting it is assigned to. This value can be 1, 2, 4, 8, 15, 16,
24, or 32, though of late, with the advent of digital monitors, a value of 32 is becoming typical. If we
raise 2 to the power of the BPP value, we will get the number of colors available. 2 1 = 2 colors, 22 = 4
colors, 24 = 16 colors, and 28 = 256 colors. High Color values are 215 = 32,768 colors and 216 = 65,536
colors. True Color values are 224 = 16,777,216 colors and 232 = 4,294,967,296 colors.
For example, this function will return a text report of the resolution settings for the current display:
'*******************************************************************************
' Method Name : GetCurrentDisplaySize
' Purpose : Return the settings for the current display as a string, formatted
' : "XXXxYYY pixels; nn bpp, nnnn Colors".
'*******************************************************************************
Friend Function GetCurrentDisplaySize() As String
With Screen.PrimaryScreen
Dim BPP As Int32 = .BitsPerPixel
Dim Result As String = .Bounds.Width.ToString & "x" &
.Bounds.Height.ToString & " pixels; " &
BPP.ToString & " bpp, " &
Math.Pow(2.0#, BPP).ToString & " Colors"
Select Case BPP
Case 1, 2, 4, 8 : Return Result
Case 15, 16 : Return Result & " (High Color)"
Case 24, 32 : Return Result & " (True Color)"
Case Else 'last ditch effort
With .Bounds
If BPP > 0 Then
Return Result & ", color format unknown"
Else
Return .Width.ToString & "x" &.Height.ToString & "; 8 bpp, 256 Colors"
End If
End With
End Select
End With
End Function

Even so, sometimes developers also require the display frequency or the fixed display rendering mode,
which this structure does not provide. But to get that information, as I might guess from the large
number of people on the web who have been asking how to do it, might not seem easy to do. Granted,
there is a Pinvoke named EnumScreenSettings that most everyone knows about that that can be used to
acquire available screen settings for a system’s display, but most people who use it claim it does not
work. Actually, it does work, and quite well. The problem, however, is that most everyone defines the
DEVMODE structure required by the method incorrectly. The place where they run into trouble is that
they declare the two string members of the structure of type TCHAR instead as type CHAR using
“<VBFixedString (CCHDEVICENAME)> Public dmDeviceName As String” and “<VBFixedString (CCHFORMNAME)>
Public dmFormName As String”. (CCHDEVICENAME and CCHFORMNAME are integer constants set to a value of 32.)

For translating CHAR, using the VBFixedString declaration works fine. This is not so for types TCHAR or BYTE.
If we pass a DEVMODE structure through the EnumScreenSettings function with its string members
declared with VBFixedString, we get an unhandled exception error because the structure ends up being
one character too long for each string. Many people try to “fix” it by changing the dmBitsPerPel member
(Pel is an older form of Pixel; Picture Element) from a 32-bit integer to a 16-bit short integer. But the
mapping is already off, and so data is incorrectly copied into the structure.
 Page 709

Worse, they typically use a DEVMODE structure that is formatted for a printer, not the rendering of it
that is designed for displays. To clarify this point, let us first examine how MSDN (Microsoft
Development Network) defines the DEVMODE structure in C++ in their WINGDI.h header file:
typedef struct _devicemodeA { //Declare a Structure of type _devicemode.
TCHAR dmDeviceName[CCHDEVICENAME]; //TCHAR = Unmanaged Type ByValTStr. vbFixedString works w/CHAR, not TCHAR.
WORD dmSpecVersion; //WORD = Short Integer or Int16
WORD dmDriverVersion; //NOTE: ANSI TCHAR is equivalent to BYTE. The Unicode version is WCHAR.
WORD dmSize;
WORD dmDriverExtra;
DWORD dmFields; //DWORD = Integer or Int32.
union { //This union will allow the following two structures to use the same space.
struct { //this first structures declares 8 short integers, consuming 16 bytes.
short dmOrientation;
short dmPaperSize;
short dmPaperLength;
short dmPaperWidth;
short dmScale;
short dmCopies;
short dmDefaultSource;
short dmPrintQuality;
};
struct { //this second structure also consumes 16 bytes.
POINTL dmPosition; //POINTL is same as .NET Point structure (two-Int32 values, using 8 bytes).
DWORD dmDisplayOrientation;
DWORD dmDisplayFixedOutput;
};
};
short dmColor;
short dmDuplex;
short dmYResolution;
short dmTTOption;
short dmCollate;
TCHAR dmFormName[CCHFORMNAME];
WORD dmLogPixels;
DWORD dmBitsPerPel;
DWORD dmPelsWidth;
DWORD dmPelsHeight;
union { //this union allows dmDisplayFlags and dmNup to occupy the same space.
DWORD dmDisplayFlags;
DWORD dmNup;
};
DWORD dmDisplayFrequency;
#if (WINVER >= 0x0400) //if WinNT40 or greater...
DWORD dmICMMethod; // the following declarations are used only by a printer device...
DWORD dmICMIntent;
DWORD dmMediaType;
DWORD dmDitherType;
DWORD dmReserved1; //presently must be set to 0.
DWORD dmReserved2; //presently must be set to 0.
#if (WINVER >= 0x0500) || (_WIN32_WINNT >= 0x0400) //if Window98/WinNT40 or greater...
DWORD dmPanningWidth; //presently must be set to 0 (used by printers).
DWORD dmPanningHeight; //presently must be set to 0 (used by printers).
#endif
#endif
} DEVMODEA, *PDEVMODEA, *LPDEVMODEA; //assign structure to DEVMODE, pointer PDEVMODE, and long pointer DEVMODE

If we understand that WORD and short represent VB Short integers (Int16), DWORD represents VB Integers
(Int32), POINTL is a VB Point structure, that TCHAR (same as BYTE) should be interpreted as an unmanaged
ByVal Fixed-Length String, and the Union allows more than one set of variables to occupy the same
space, we can construct our DEVMODE structure properly. Following is the corrected DEVMODE
structure, including the definition for the EnumScreenSettings Pinvoke. Be sure to also include “ Imports
System.Runtime.InteropServices” at the top of our class or module.
'create a 16-byte union of 8 int16 printer values over a Point (8 bytes) and 2 int32 values (8 bytes)
<StructLayout(LayoutKind.Explicit)> Friend Structure DEVMODE_union1
' struct {
<FieldOffset(0)> Friend dmOrientation As Int16 'printer only, selects the orientation of the paper.
<FieldOffset(2)> Friend dmPaperSize As Int16 'printer only, selects the size of the paper to print on.
<FieldOffset(4)> Friend dmPaperLength As Int16 'printer only, override len of paper specified by dmPaperSize.
<FieldOffset(6)> Friend dmPaperWidth As Int16 'printer only, override width of paper specified by dmPaperSize.
<FieldOffset(8)> Friend dmScale As Int16 'Specifies factor*100 by which printed output to be scaled (1=.01).
<FieldOffset(10)> Friend dmCopies As Int16 'Select # of copies printed if device supports multi-page copies.
<FieldOffset(12)> Friend dmDefaultSource As Int16 'Specifies the paper source (0,1,2...).
<FieldOffset(14)> Friend dmPrintQuality As Int16 'Specifies the printer resolution.
' }
' struct {
<FieldOffset(0)> Public dmPosition As Point
<FieldOffset(8)> Friend dmDisplayOrientation As Int32 'display only, orientation which images should be presented.
 Page 710

<FieldOffset(12)> Friend dmDisplayFixedOutput As Int32 'fixed-res displays, how it show low-res mode on a hi-res display.
' }
End Structure

'create a 4-byte union of two overlapping int32 values (dmDisplayFlags for display, dmNup for a printer).
<StructLayout(LayoutKind.Explicit)> Friend Structure DEVMODE_union2
<FieldOffset(0)> Friend dmDisplayFlags As Int32 'Specifies the device's display mode.
<FieldOffset(0)> Friend dmNup As Int32 'Specifies where NUP is done (N-Up = # pages rendered on 1 sheet).
End Structure

<StructLayout(LayoutKind.Sequential)> Friend Structure DEVMODE.


Friend Const CCHDEVICENAME As Int32 = 32 'length for friendly device name.
Friend Const CCHFORMNAME As Int32 = 32 'length for form name.

'friendly device name (do not use shortform <VBFixedArray(str)> or <VBFixedString(str)>. Must be ByValTStr).
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=CCHDEVICENAME)> Friend dmDeviceName As String.
Friend dmSpecVersion As Int16 'version # of initialization data specification on which structure is based.
Friend dmDriverVersion As Int16 'The driver version number assigned by the driver developer.
Friend dmSize As Int16 'size in bytes of DEVMODE struct (set by user: Len(DevModeStruct)).
Friend dmDriverExtra As Int16 '# of bytes of private driver-data that follow this struct (not inc in dmSize).
Friend dmFields As Int32 'Specifies if certain members of DEVMODE struct have been initialized.
'---------------------------------------
Friend u1 As DEVMODE_union1.
'---------------------------------------
Friend dmColor As Int16 'Switches between color and monochrome on color printers.
Friend dmDuplex As Int16 'Selects duplex or double-sided printing for printers capable of duplex printing.
Friend dmYResolution As Int16 'Specifies the y-resolution, in dots per inch, of the printer.
Friend dmTTOption As Int16 'Specifies how TrueType fonts should be printed.
Friend dmCollate As Int16 'Specifies whether collation should be used when printing multiple copies.
'specify the name of the form to use; for example, "Letter" or "Legal".
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=CCHFORMNAME)> Friend dmFormName As String.
Friend dmLogPixels As Int16 'The number of pixels per logical inch. Printer drivers do not use this member.
Friend dmBitsPerPel As Int32 'Specifies the color resolution, in bits per pixel, of the display device.
Friend dmPelsWidth As Int32 'Specifies the width, in pixels, of the visible device surface.
Friend dmPelsHeight As Int32 'Specifies the height, in pixels, of the visible device surface.
'---------------------------------------
Friend u2 As DEVMODE_union2.
'---------------------------------------
Friend dmDisplayFrequency As Int32 'Specifies frequency in Hz of display device in particular mode.
'All the following are for Image Color Management For printers
Friend dmICMMethod As Int32 'For ICM applications, system examines this to determine how to handle ICM support.
Friend dmICMIntent As Int32 'Specifies which color matching method, or intent, should be used by default.
Friend dmMediaType As Int32 'Specifies type of media printed on, such as standard, glossy, or transparency.
Friend dmDitherType As Int32 'Specifies how dithering is done; none, course, fine, lineart, or grayscale.
Friend dmReserved1 As Int32 'Not used; must be zero.
Friend dmReserved2 As Int32 'Not used; must be zero.
Friend dmPanningWidth As Int32 'This member must be zero.
Friend dmPanningHeight As Int32 'This member must be zero.
End Structure

' The EnumDisplaySettings() function retrieves information about one of the graphics modes for a display device.
' To retrieve info for all graphics modes of a display device, make a series of invocations to function, using iModeNum.
' as an index, incrementing from 0. The function will return TRUE if index is valid. FALSE indicates index out of range.
' This method sets dmBitsPerPel, dmPelsWidth, dmPelsHeight, dmDisplayFlags, and dmDisplayFrequency members of DEVMODE struct.
Private Declare Function EnumDisplaySettings Lib "user32.DLL" Alias "EnumDisplaySettingsA" (
ByVal lpszDeviceName As IntPtr,
ByVal iModeNum As Int32,
ByRef lpDevMode As DEVMODE) As Boolean

This DEVMODE structure version actually does work, and for both printers and display devices. Notice
that the two string members are declared as Unmanaged Type ByValTStr, which properly translates type
TCHAR (and BYTE). The use of vbFixedString only works with type CHAR (though, mind you, most structures
do tend to use type CHAR), not with TCHAR or BYTE, which indicated a fixed-length string of characters (type
CHAR expects and will add an additional character for its required null terminator (which TCHAR and BYTE
do not require), and this will throw a fixed-size structure’s footprint off). Also, we should notice an 8-
short integer block (16 bytes) that differs between printers and displays. For printers, this 16-byte field is
consumed by 8 short 16-bit integers that are specifically geared to printers, not for a display screen. For
displays, the dmPosition member’s Point structure consumes 8 bytes, and the two additional 32-bit
integer members, dmDisplayOrientation and dmFixedDIsplayOutput, consume the other 8 bytes.
For people who had managed to get DEVMODE to work with the Pinvoke, their next complaint is
usually that there are a number of duplicate display settings, which have the same X and Y values, the
same BPP, and even the same display frequency, such as 60hz. But the truth is that they are not
duplicates. What is going on, but usually unseen, is that additional versions are set aside for lower-
 Page 711

resolution modes being displayed on higher-resolution displays. For this, please refer to the
dmDisplayFixedOutput member. This integer member will have one of three constant values assigned to it:
'data for DEVMODE's dmDisplayFixedOutput member.
Friend Const DMDFO_DEFAULT As Int32 = 0 'The display's default setting.
Friend Const DMDFO_CENTER As Int32 = 1 'The low-resolution image is centered in the larger screen space.
Friend Const DMDFO_STRETCH As Int32 = 2 'The low-resolution image is stretched to fill the larger screen space.

If we examine all of the presumed “duplicates”, we will in fact find each is sporting an entirely different
dmDisplayFixedOutput value.

Consider my module, modEnumDisplaySettings:


Option Strict On
Option Explicit On
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
' modEnumScreenResolutions Static Module Class
' Enumerate current system's screen resolutions.
'-------------------------------------------------------------------------------------
'-------------------------------------------------------------------------------------
Imports System.Runtime.InteropServices
Module modEnumScreenResolutions
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
'create a 16-byte union of 8 int16 printer values over a Point (8 bytes) and 2 int32 values (8 bytes).
<StructLayout(LayoutKind.Explicit)> Friend Structure DEVMODE_union1
' struct {
<FieldOffset(0)> Friend dmOrientation As Int16 'printer only, selects the orientation of the paper.
<FieldOffset(2)> Friend dmPaperSize As Int16 'printer only, selects the size of the paper to print on.
<FieldOffset(4)> Friend dmPaperLength As Int16 'printer only, overrides paper length specified by dmPaperSize.
<FieldOffset(6)> Friend dmPaperWidth As Int16 'printer only, overrides paper width specified by dmPaperSize.
<FieldOffset(8)> Friend dmScale As Int16 'Specifies actor*100 the printed output is scaled (1=.01).
<FieldOffset(10)> Friend dmCopies As Int16 '# of copies printed if device supports multi-page copies.
<FieldOffset(12)> Friend dmDefaultSource As Int16 'Specifies the paper source (0,1,2...).
<FieldOffset(14)> Friend dmPrintQuality As Int16 'Specifies the printer resolution.
' }
' struct {
<FieldOffset(0)> Public dmPosition As Point
<FieldOffset(8)> Friend dmDisplayOrientation As Int32 'display only, orientation images should be presented.
<FieldOffset(12)> Friend dmDisplayFixedOutput As Int32 'fixed-res displays, how low-res shows mode on hi-res display.
' }
End Structure

'create a 4-byte union of two overlapping int32 values (dmDisplayFlags for display, dmNup for a printer).
<StructLayout(LayoutKind.Explicit)> Friend Structure DEVMODE_union2
<FieldOffset(0)> Friend dmDisplayFlags As Int32 'Specifies the device's display mode.
<FieldOffset(0)> Friend dmNup As Int32 'Specifies where NUP is done (N-Up = # pages on 1 sheet).
End Structure

<StructLayout(LayoutKind.Sequential)> Friend Structure DEVMODE.


Friend Const CCHDEVICENAME As Int32 = 32 'length for friendly device name.
Friend Const CCHFORMNAME As Int32 = 32 'length for form name.

'friendly device name (do not use shortform <VBFixedArray(str)> or <VBFixedString(str)>. Must be ByValTStr).
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=CCHDEVICENAME)> Friend dmDeviceName As String.
Friend dmSpecVersion As Int16 'version # of initialization data specification on which struct is based.
Friend dmDriverVersion As Int16 'driver version number assigned by the driver developer.
Friend dmSize As Int16 'byte size of DEVMODE struct (set by user: Len(DevModeStruct)).
Friend dmDriverExtra As Int16 '# of bytes of private driver-data that follow this struct (not inc in dmSize).
Friend dmFields As Int32 'if certain members of DEVMODE struct have been initialized.
'---------------------------------------
Public u1 As DEVMODE_union1.
'---------------------------------------
Friend dmColor As Int16 'Switches between color and monochrome on color printers.
Friend dmDuplex As Int16 'Select duplex or 2-sided printing for printers capable of duplex printing.
Friend dmYResolution As Int16 'Specifies the y-resolution, in dots per inch, of the printer.
Friend dmTTOption As Int16 'Specifies how TrueType fonts should be printed.
Friend dmCollate As Int16 'Specifies if collation should be used when printing multiple copies.
'specify the name of the form to use; for example, "Letter" or "Legal".
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=CCHFORMNAME)> Friend dmFormName As String.
Friend dmLogPixels As Int16 'The number of pixels per logical inch. Printer drivers do not use this member.
Friend dmBitsPerPel As Int32 'Specifies the color resolution, in bits per pixel, of the display device.
Friend dmPelsWidth As Int32 'Specifies the width, in pixels, of the visible device surface.
Friend dmPelsHeight As Int32 'Specifies the height, in pixels, of the visible device surface.
'---------------------------------------
Public u2 As DEVMODE_union2.
'---------------------------------------
Friend dmDisplayFrequency As Int32 'frequency in Hz of display device in a particular mode.
'All the following are for Image Color Management For printers.
Friend dmICMMethod As Int32 'For ICM apps, system examines this to determine how to handle ICM support
Friend dmICMIntent As Int32 'Specifies which color matching method, or intent, should be used by default
Friend dmMediaType As Int32 'type of media being printed on; standard, glossy, or transparency
 Page 712

Friend dmDitherType As Int32 'how dithering to be done, such as none, course, fine, lineart, or grayscale
Friend dmReserved1 As Int32 'Not used; must be zero
Friend dmReserved2 As Int32 'Not used; must be zero
Friend dmPanningWidth As Int32 'This member must be zero
Friend dmPanningHeight As Int32 'This member must be zero
End Structure

' The EnumDisplaySettings() function retrieves information about one of the graphics modes for a display device.
' To retrieve info for all graphics modes of display device, make a series of invokecations to function, using iModeNum
' as an index, incrementing from 0. Function returns TRUE if index valid. FALSE if index was out of range.
' Sets the dmBitsPerPel, dmPelsWidth, dmPelsHeight, dmDisplayFlags, and dmDisplayFrequency members of DEVMODE struct.
Private Declare Function EnumDisplaySettings Lib "user32.DLL" Alias "EnumDisplaySettingsA" (
ByVal lpszDeviceName As IntPtr,
ByVal iModeNum As Int32,
ByRef lpDevMode As DEVMODE) As Boolean

' The ChangeDisplaySettings function changes the settings of the default display device to the specified graphics mode.
' In existing, valid DEVMODE struct, set desired values (if not already set) to dmBitsPerPel, dmPelsWidth, dmPelsHeight,
' dmDisplayFlags, and dmDisplayFrequency. Apply each associated flag to dmFields member: DM_BITSPERPEL, DM_PELSWIDTH,
' DM_PELSHEIGHT, DM_DISPLAYFLAGS, DM_DISPLAYFREQUENCY, or DM_POSITION (OR multiples). Provide DEVMODE struct to lpDevMode
' parameter, and appropriate CDS constants to dwFlags parameter. Refer to DISP_CHANGE constants for result flags.
Public Declare Function ChangeDisplaySettings Lib "user32.DLL" Alias "ChangeDisplaySettingsA" (
ByRef lpDevMode As DEVMODE,
ByVal dwFlags As Int32) As Integer

'optional Indexes for EnumDisplaySettings()'s iModeNum parameter.


Friend Const ENUM_CURRENT_SETTINGS As Int32 = -1 'Retrieve the current settings for the display device.
Friend Const ENUM_REGISTRY_SETTINGS As Int32 = -2 'Retrieve settings for display device that are stored in registry.
' 'useful when testing different resolutions, so "current" might not
' 'be the setting saved off in the registry.
'orientation setting for DEVMODE's dmDisplayOrientation member.
Friend Const DMDO_DEFAULT As Int32 = 0 'The display is in the natural orientation. It is the default.
Friend Const DMDO_90 As Int32 = 1 'The display is rotated 90 degrees (clockwise) from DMDO_DEFAULT.
Friend Const DMDO_180 As Int32 = 2 'The display is rotated 180 degrees (clockwise) from DMDO_DEFAULT.
Friend Const DMDO_270 As Int32 = 3 'The display is rotated 270 degrees (clockwise) from DMDO_DEFAULT.

'data for DEVMODE's dmDisplayFixedOutput member.


Friend Const DMDFO_DEFAULT As Int32 = 0 'display's default setting.
Friend Const DMDFO_CENTER As Int32 = 1 'low-res image is centered in the larger screen space.
Friend Const DMDFO_STRETCH As Int32 = 2 'low-res image is stretched to fill the larger screen space.

'change flags user assigns to dmFields in DEVMODE structure when applying a new setting to a display.
'OR constants for each associated dm-parameter you assign using ChangeDisplaySettings().
Friend Const DM_BITSPERPEL As Int32 = &H00040000 'apply setting assigned to dmBitsPerPel.
Friend Const DM_PELSWIDTH As Int32 = &H00080000 'apply setting assigned to dmPelsWidth.
Friend Const DM_PELSHEIGHT As Int32 = &H00100000 'apply setting assigned to dmPelsHeight.
Friend Const DM_DISPLAYFLAGS As Int32 = &H00200000 'apply setting assigned to dmDisplayFlags.
Friend Const DM_DISPLAYFREQUENCY As Int32 = &H00400000 'apply setting assigned to dmDisplayFrequency.
Friend Const DM_POSITION As Int32 = &H00000020 'apply setting assigned to dmPosition.

'dwFlags parameter values for ChangeDisplaySettings().


'0 'The graphics mode for the current screen will be changed dynamically.
Friend Const CDS_UPDATEREGISTRY As Int32 = &H1 'graphics mode for current screen changed dynamically and graphics
' 'mode will be updated in registry. Mode info stored in USER profile.
Friend Const CDS_TEST As Int32 = &H2 'The system tests if the requested graphics mode could be set.
Friend Const CDS_FULLSCREEN As Int32 = &H4 'Mode is temp in nature. If changing to and from other desktop,
' 'this mode will not be reset.
Friend Const CDS_GLOBAL As Int32 = &H8 'Settings will be saved in global settings area. Only valid when
' 'specified with the CDS_UPDATEREGISTRY flag.
Friend Const CDS_SET_PRIMARY As Int32 = &H10 'This device will become the primary device.
Friend Const CDS_NORESET As Int32 = &H10000000 'The settings will be saved in the registry, but will not take
' 'effect. Only valid when specified with the CDS_UPDATEREGISTRY flag.
Friend Const CDS_RESET As Int32 = &H40000000 'The settings should be changed, even if the requested settings are
' 'the same as the current settings.

'result flags returned by ChangeDisplaySettings()


Friend Const DISP_CHANGE_SUCCESSFUL As Int32 = 0 'The settings change was successful.
Friend Const DISP_CHANGE_RESTART As Int32 = 1 'The computer must be restarted for the graphics mode to work.
Friend Const DISP_CHANGE_FAILED As Int32 = -1 'The display driver failed the specified graphics mode.
Friend Const DISP_CHANGE_BADMODE As Int32 = -2 'The graphics mode is not supported.
Friend Const DISP_CHANGE_NOTUPDATED As Int32 = -3 'Unable to write settings to the registry.
Friend Const DISP_CHANGE_BADFLAGS As Int32 = -4 'Invalid set of flags was passed in.
Friend Const DISP_CHANGE_BADPARAM As Int32 = -5 'Invalid parameter passed. This can include a combination of flags.
Friend Const DISP_CHANGE_BADDUALVIEW As Int32 = -6 'Settings change was unsuccessful because system DualView capable.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method : EnumScreenResolutions
' Purpose : Build a List of type DEVMODE of all screen resolutions avaiable for the primary display.
' NOTES: If you supply a non-zero Selected Index, it will return a list containing only that indexed resolution.
' If you supply ENUM_CURRENT_SETTINGS (-1), only the current display setting will be returned.
' If you supply ENUM_REGISTRY_SETTINGS (-2), only the display setting defined in the registry will be returned.
' The returned List will contain one or more DEVMODE structures. You can feed any one of them to the
' GetResolutionString() function to get a string representation of the display, such as:
' "1920x1080 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz,
 Page 713

Display Fixed Output = Default, Default Orientation"


'*******************************************************************************
Friend Function EnumScreenResolutions(Optional SelectIndex As Int32 = 0) As List(Of DEVMODE).
Dim lst As New List(Of DEVMODE) 'init list to return to invoker.
Dim DM As New DEVMODE 'structure to receive each valid screen mode.
'DM.Initialize()
DM.dmSize = CShort(Len(DM)) 'set size of structure (important!)

Dim iModeNum As Int32 = SelectIndex 'init DevMode index to list base (typically 0).
If SelectIndex < ENUM_REGISTRY_SETTINGS Then 'if out of range, then assume single selection.
Return lst 'return empty list if out of range.
End If
'-----------------------------------------------------------------------
Do While EnumDisplaySettings(IntPtr.Zero, iModeNum, DM) 'grab indexed resolution while index is valid.
lst.Add(DM) 'add acquired DEVMODE structure to list.
If SelectIndex <> 0 Then 'if user specified index, return only it.
Exit Do
End If
iModeNum += 1 'else bump index to next and try it.
Loop
'-----------------------------------------------------------------------
Return lst 'return result list.
End Function

'*******************************************************************************
' Method : GetResolutionString
' Purpose : Return a string representation of the display data in the provided DEVMODE structure.
'*******************************************************************************
Friend Function GetResolutionString(DM As DEVMODE) As String
Dim bpp As Int32 = DM.dmBitsPerPel 'commonly used value, so simplify access.
Dim result As String = DM.dmPelsWidth.ToString & "x" &
DM.dmPelsHeight.ToString & " pixels; " &
bpp.ToString & " bpp, " &
Math.Pow(2.0#, bpp).ToString("0,000") & " Colors" 'ie: 640x480 pixels; 4 bpp, 16 Colors.
Select Case bpp
Case 15, 16 : result &= " (High Color)"
Case 24, 32 : result &= " (True Color)"
End Select
If DM.dmLogPixels = 0 Then 'check dots per inch.
result &= ", System Default dpi, "
Else
result &= ", " & DM.dmLogPixels.ToString & " dpi, "
End If
If DM.dmDisplayFrequency = 1 Then 'if display frequency set to hardware default...
result &= "Hardware default"
Else
result &= DM.dmDisplayFrequency.ToString("00") & " hz" 'else display frequency (ie, 60 hz, 56 hz, etc).
End If
result &= ", Display Fixed Output = " 'how low-res mode displayed in hi-res display.
Select Case DM.u1.dmDisplayFixedOutput
Case DMDFO_DEFAULT 'do as setting require.
result &= "Default"
Case DMDFO_CENTER 'low-res image centered in larger screen.
result &= "Center"
Case DMDFO_STRETCH 'low-res image stretched to fill larger screen.
result &= "Stretch"
End Select
Select Case DM.u1.dmDisplayOrientation
Case DMORIENT_PORTRAIT
result &= ", Portrait"
Case DMORIENT_LANDSCAPE
result &= ", Landscape"
Case Else
result &= ", Default Orientation"
End Select
Return result 'return formatted string.
End Function
End Module

To take advantage of this module is really easy. For example:


Dim lst As List(Of DEVMODE) = EnumScreenResolutions() 'enumerate all available display settings.
Dim Idx As Int32 = 0 'init index.
For Each DM As DEVMODE In lst 'now list them all in Debug.
Debug.Print(Idx.ToString & ". " & GetResolutionString(DM)) 'display string report for each gathered DEVMODE structure.
Idx += 1 'bump index.
Next
 Page 714

The EnumScreenResolution method will return a strong list of type DEVMODE for all settings
available for our display. We can then pass each one of them through the GetResolutionString method to
build a report string that can be used to display all pertinent information for each setting. For example,
my Display has the following 45 settings (indexed 0-44):
0. 640x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
1. 640x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 59 hz, Display Fixed Output = Default, Default Orientation
2. 720x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 59 hz, Display Fixed Output = Default, Default Orientation
3. 720x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
4. 720x576 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
5. 720x576 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Center, Default Orientation
6. 720x576 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Stretch, Default Orientation
7. 720x576 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 50 hz, Display Fixed Output = Default, Default Orientation
8. 800x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
9. 800x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Center, Default Orientation
10. 800x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Stretch, Default Orientation
11. 800x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 56 hz, Display Fixed Output = Default, Default Orientation
12. 800x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 56 hz, Display Fixed Output = Center, Default Orientation
13. 800x480 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 56 hz, Display Fixed Output = Stretch, Default Orientation
14. 800x600 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
15. 800x600 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 56 hz, Display Fixed Output = Default, Default Orientation
16. 1024x600 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
17. 1024x600 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Center, Default Orientation
18. 1024x600 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Stretch, Default Orientation
19. 1024x768 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
20. 1152x648 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 50 hz, Display Fixed Output = Default, Default Orientation
21. 1280x720 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 59 hz, Display Fixed Output = Default, Default Orientation
22. 1280x720 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
23. 1280x720 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 50 hz, Display Fixed Output = Default, Default Orientation
24. 1280x768 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
25. 1280x768 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Center, Default Orientation
26. 1280x768 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Stretch, Default Orientation
27. 1280x800 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
28. 1280x960 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
29. 1280x1024 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
30. 1400x1050 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
31. 1400x1050 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Center, Default Orientation
32. 1400x1050 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Stretch, Default Orientation
33. 1440x900 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
34. 1600x900 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
35. 1600x900 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Center, Default Orientation
36. 1600x900 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Stretch, Default Orientation
37. 1600x1200 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
38. 1680x1050 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
39. 1776x1000 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 50 hz, Display Fixed Output = Default, Default Orientation
40. 1920x1080 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 59 hz, Display Fixed Output = Default, Default Orientation
41. 1920x1080 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation
42. 1920x1080 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 50 hz, Display Fixed Output = Default, Default Orientation
43. 1920x1080 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 29 hz, Display Fixed Output = Default, Default Orientation
44. 1920x1080 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 30 hz, Display Fixed Output = Default, Default Orientation

Apart from noticing that my old 32-inch 16:9 digital monitor features only 32-bit color settings, we can
also see that some of the entries, which one might at first assume to be duplicates, actually do have a
different Display Fixed Output setting assigned to them, such as:
4. 720x576 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation (dmDisplayFixedOutput = 0)
5. 720x576 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Center, Default Orientation (dmDisplayFixedOutput = 1)
6. 720x576 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Stretch, Default Orientation (dmDisplayFixedOutput = 2)

But what if we want to get such details for the current display? As we can see from the module listing,
we can send a value of ENUM_CURRENT_SETTINGS, or -1, as an optional parameter to the
EnumScreenResolutions method (EnumScreenResolutions(-1)) to gather this data. Mine returns:
1920x1080 pixels; 32 bpp, 4,294,967,296 Colors (True Color), System Default dpi, 60 hz, Display Fixed Output = Default, Default Orientation.

If we are changing the resolution, so the “current” resolution might not be the one currently set in the
registry, we can gather the settings directly from the registry by sending ENUM_REGISTRY_SETTINGS, or -2,
instead (EnumScreenResolutions(-2)). Gathering registry settings might be required if we have temporarily
changed the current display settings from their original, registry-based settings.
If we send a non-zero index as a parameter to EnumScreenResolutions, such as 10, it will return the
display setting that is the 11th setting (offset from zero) in the internal list.
 Page 715

Selecting a New Display Setting


We are able to select a new display setting by using one of the DEVMODE structures that was returned
in a list from the EnumScreenResolution method. We must then inform the system of all parameters we
want to change that differ from the current setting by applying the appropriate flags to the DEVMODE’s
dmFields member. As such, I tend to just use a copy of the current setting, modify it, and then flag each
change to dmFields. As shown in the module, we can change from one to six member values and we must
also assign each changed member’s associated flag to dmFlags using:
'change flags user assigns to dmFields in DEVMODE structure when applying a new setting to a display.
'OR constants for each associated dm-parameter you assign using ChangeDisplaySettings().
Friend Const DM_BITSPERPEL As Int32 = &H00040000 'apply setting assigned to dmBitsPerPel.
Friend Const DM_PELSWIDTH As Int32 = &H00080000 'apply setting assigned to dmPelsWidth.
Friend Const DM_PELSHEIGHT As Int32 = &H00100000 'apply setting assigned to dmPelsHeight.
Friend Const DM_DISPLAYFLAGS As Int32 = &H00200000 'apply setting assigned to dmDisplayFlags.
Friend Const DM_DISPLAYFREQUENCY As Int32 = &H00400000 'apply setting assigned to dmDisplayFrequency.
Friend Const DM_POSITION As Int32 = &H00000020 'apply setting assigned to dmPosition.

To use multiple settings, we perform a bit-wise OR on them. For example:


Dim DMorg As DEVMODE = EnumScreenResolutions(ENUM_CURRENT_SETTINGS)(0) 'get current setting (grab only DEVMODE entry).
Dim DMtemp As DEVMODE = DMorg 'make a copy for changing resolution.
DMtemp.dmPelsWidth = 800 'change to 800x600 resolution.
DMtemp.dmPelsHeight = 600
DMtemp.dmFields = DM_PELSWIDTH Or DM_PELSHEIGHT 'inform system these parameters are to change.
Dim Result As Int32 = ChangeDisplaySettings(DMtemp, CDS_TEST) 'first test changes to see if they are OK...
If Result = DISP_CHANGE_SUCCESSFUL Then 'if we succeeded in the test...
Result = ChangeDisplaySettings(DMtemp, CDS_UPDATEREGISTRY Or CDS_RESET) 'actually apply new settings to the display.
If Result = DISP_CHANGE_SUCCESSFUL Then 'if we succeeded in the test...
MsgBox("800x600. ENTER to reset.") 'pause to admire the change.
DMorg.dmFields = DM_PELSWIDTH Or DM_PELSHEIGHT 'we are resetting screen dimens to originals.
ChangeDisplaySettings(DMorg, CDS_UPDATEREGISTRY Or CDS_RESET) 'reset display to original setting.
End If
End If

Note in our example that we first tested our new parameters for being valid using the CDS_TEST option
before actually updating the display using CDS_RESET. This is always a safe practice, in case the display is
not able to support the selected display resolution for some reason.
Also notice that after the ChangeDisplaySetting method was invoked without the CDS_TEST parameter,
our screen changed to 800x600 resolution. Afterward, it was changed back to its original setting.
Actually, because we are using an existing DEVMODE structure that already has the pertinent fields
filled; dmBitsPerPel, dmPelsWidth, dmPelsHeight, dmDisplayFlags, and dmDisplayFrequency, we could have set
dmFields to all these associated settings and updated the display dynamically with a value of Zero for the
dwFlags parameter, and so not bothered with any CDS flags, thus ChangeDisplaySettings(DMtemp, 0) would
have done the trick. This will of course not update the registry, which is also handy if the game crashes
and we must reboot. In this case the display will come back up in its registry-defined settings rather than
the settings we may have meant to only temporarily assign.
The possible result values returned by the ChangeDisplaySettings function are as follows:
'result flags returned by ChangeDisplaySettings()
Friend Const DISP_CHANGE_SUCCESSFUL As Int32 = 0 'The settings change was successful.
Friend Const DISP_CHANGE_RESTART As Int32 = 1 'The computer must be restarted for the graphics mode to work.
Friend Const DISP_CHANGE_FAILED As Int32 = -1 'The display driver failed the specified graphics mode.
Friend Const DISP_CHANGE_BADMODE As Int32 = -2 'The graphics mode is not supported.
Friend Const DISP_CHANGE_NOTUPDATED As Int32 = -3 'Unable to write settings to the registry.
Friend Const DISP_CHANGE_BADFLAGS As Int32 = -4 'invalid set of flags passed in.
Friend Const DISP_CHANGE_BADPARAM As Int32 = -5 'invalid parameter. Can include invalid flag Or combination Of flags.
Friend Const DISP_CHANGE_BADDUALVIEW As Int32 = -6 'settings change unsuccessful because system is DualView capable.

Additional information can be found by simply doing web searches for “enumscreenresolutions msdn”,
“devmode msdn”, and “changedisplaysettings msdn”. Most constants and structures are defined in Visual
C’s WinGdi.h file, though the above results for ChangeDisplaySettings are declared in the WinUser.h
file. (found at C:\Program Files (x86)\Microsoft Visual Studio xx.0\VC\include for Visual Studio
previous to VS2015, or at C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\um for
VS2015).
 Page 716

Black Book Tip # 57


Printing Plain, Formatted and WYSIWYG Documents Made E-Z.
VB6 programmers migrating to VB.NET can get a bit vocal in their frustration when trying to use the
much more powerful VB.NET Printer interface. I find this curious, because as VB.NET was being
developed, they, en mass, ranted, raved, and chomped at the bit over the viscerally-hated VB6 printer
interface. Instead of the dated, archaic interface VB6 offered, they demanded unrestricted access to their
printers, and to print to them exactly as if they were drawing to a Form or PictureBox. With VB.NET,
they got everything they demanded; they got unrestricted access to their printers, and, instead of
resorting to Raster Graphic Pinvokes each time they required complex drawings, they can draw them
exactly as if they were drawing to a Form or PictureBox. Yet, such massively expanded printer control
consequently requires far more complex objects (even though they are still really easy to manage). They
finally got the dimension of control they feverishly campaigned for, but now they are upset because
printing is not as simple as it was under VB6; which was the very thing they had hated with puissant
vitriol. Even so, VB.NET still supports the simplistic VB6-style interface if developers do want to use it.

Using VB6 Printer Functionality under VB.NET


VB6 had a very primitive interface. Indeed, it was very much like displaying text on the screen under
DOS Basic. We would start with a Printer object, created using something like “ Dim Printer As New
Printer”, and then we use that new object to send text to the current printer, or to a printer selected with
a PrintDialog control. If we wanted to do anything more complicated, we had to resort to Pinvokes.
For using the VB6-style printer interface under VB.NET, we simply have to add a reference to
“PowerPacks.Vs”, and then we can create our VB6-compatible printer object using “Dim Printer As New
PowerPacks.Printing.Compatibility.VB6.Printer”. We can now print using it just like we did under VB6.

Printing Simple Text under VB.NET


Although printing using the VB6-style interface is OK, especially when printing raw text, I much prefer
the VB.NET interface due to the greater control I have over the document. Apart from just printing a
document, we can control exactly how and where text, drawings and images are rendered, such as
wrapping text around an image (not as difficult as some think, but experiment using a PictureBox, and
then translate those skills to a Printer). Like a PictureBox, we can draw shapes, text, or images on each
page. Better, we can use a PrintDocument control dropped on a form, instantiate a new PrintDocument
object, or we can create our own class that inherits from it and wrap all the printing duties within it.
Of course, this all sounds like a lot of fun, but how do we do it? For as complicated as many people
make the VB.NET printer interface out to be, it is actually all very simple. Let us start at the beginning.
To do anything, such as select a printer, set up the page format, perform a print preview, and even to
print, it all starts with a PrintDocument interface declared within the System.Drawing.Printing
Namespace. Without it, we cannot do all the other exciting things we can otherwise do, because the
printer selector, page setup, and print preview interfaces all require us to have a working PrintDocument
object through which we can inspect settings, apply new settings, or format our printed text.
The first thing we need to do is to just print text to the default printer (easy). Once we have that working and
understood, we can turn to the task of selecting printers (really easy), setting up page settings (really easy),
and performing a print preview (amazingly easy). We will also throw font selection in just for fun.
If we are new to printing under VB.NET, it is probably easiest to start out by just dropping a
PrintDocument control onto our form. This way we have instant access to 5 event methods, 4 of which
we normally need concern ourselves with, though in most cases we actually need only 1 or 2 of them.
However, once we gain some VB.NET programming moxie, we will not bother with dropping such non-
window controls onto our form, but we will create them all in-code and with absolute ease.
 Page 717

Our first step in this process is to import the Printing namespace. In the heading of our code, include:
Imports System.Drawing.Printing 'import printer interface namespace.

Even if we are not yet software savvy with VB.NET, let us try and make ourselves savvier. Before any
form class methods, define a new PrintDocument instance named prtDoc, like so:
Private WithEvents prtDoc As New PrintDocument 'printer I/O interface.

We added the “WithEvents” verb to attach a Delegate Class object to it, so we are able to afterwards
select events for our prtDoc object through the dropdown lists at the top of our code page.
At the top of our code page, if we select prtDoc
from the left dropdown, and then drop the right
dropdown, we see 5 available events displayed:
BeginPrint, Disposed, EndPrint, PrintPage,
and QueryPageSettings:
'-----------------------------------------------------------------------------------------------
'Sample PrintDocument event shells available to our new PrintDocument control.
'-----------------------------------------------------------------------------------------------
Private Sub prtDoc_BeginPrint(sender As Object, e As PrintEventArgs) Handles prtDoc.BeginPrint
'NOTE: This event is typically required so we can prepare to print. We can actually skip this if we do SIMPLE print jobs.
End Sub

Private Sub prtDoc_Disposed(sender As Object, e As System.EventArgs) Handles prtDoc.Disposed


'NOTE: This event is typically optional.
End Sub

Private Sub prtDoc_EndPrint(sender As Object, e As PrintEventArgs) Handles prtDoc.EndPrint


'NOTE: This event is typically optional.
End Sub

'NOTE: You might later realize that we can fully emulate VB6-style printing using just the following event.
Private Sub prtDoc_PrintPage(sender As Object, e As PrintPageEventArgs) Handles prtDoc.PrintPage
'NOTE: This event is required so we can print pages.
End Sub

Private Sub prtDoc_QueryPageSettings(sender As Object, e As QueryPageSettingsEventArgs) Handles prtDoc.QueryPageSettings


'NOTE: This event is typically optional.
End Sub

The BeginPrint event is fired when a print job begins, before the first page is printed. Here we can
initialize our print job, set an index to the start of our text, or open/rewind a FileStream to print. This is
important because the PrintPage event fires for each page in the document, so we must save our place
between pages. The print job can also be canceled by setting the e.Cancel property to True.
Disposed should be used if we need to know when our PrintDocument object was disposed of, where we
might want to ensure other references are detached (set to Nothing), or expensive resources (objects
consuming massive system memory) must be freed. I am normally obsessive about resource control, so I
have had little need of it, but we can clearly see its use in more complex printer interfaces.
The EndPrint event is fired after a print job is completed. We might need to rewind a pointer or other
things, such as close a FileStream if we are printing from a file.
The PrintPage event is fired for each page to print. To write to the page, use the Graphics object of the
PrintPageEventArgs object (e.Graphics). Use it as we would to draw to the form or to a PictureBox. For
example, to draw a line of text on the page to be printed, draw it using e.Graphics.DrawString. Be
mindful, however, that coordinates used here are not in pixels, but in 0.01-inch units (100 per inch).
After rendering a page, we should set the e.HasMorePages property to True if there are still more pages to
print. The default is False, indicating that there are no pages left. Individual page settings can be
modified through the PrintDocument object’s DefaultPageSettings member. To print each document
page using different settings, handle that in the QueryPageSettings event, which fires immediately
before the PrintPage event. The print job can be canceled by setting the e.Cancel property to True.
 Page 718

The QueryPageSettings event fires prior to a PrintPage event. It can be useful to change how a
particular page will be formatted. The e parameter is defined as QueryPageSettingsEventArgs. We
modify the members of e.PageSettings, or by assigning a pre-set PageSettings object to it (e.PageSettings
= myPageSettings). Changes made to e.PageSettings affects only the current page, not the PrintDocument
object’s own PageSettings member, which stores default settings defined in our PrintDocument object.
The print job can be canceled by setting the e.Cancel property to True.
If we want to control the font or maintain an index into a block of text, like a string or TextBox, we need
to set aside variables accessible by our events, but by PrintPage in particular. In most cases, simple print
jobs need only BeginPrint (only if we need to setup a print job, like open a file) and PrintPage, though
we may need EndPrint if we want to close a FileStream we opened in BeginPrint.
Suppose we set aside these variables:
Protected _prtText As String = Nothing 'text to be printed.
Protected _prtFont As Font = New Font("Times New Roman", 10) 'font used for printing.
Protected _prtIndex As Integer = 0 'index into text where we are printing from.
Protected _prtColor As Color = SystemColor.ButtonText 'print color. Use in case High Contrast for vision impaired.

Here, _prtText will hold the text we want to print. Further suppose that we initialized it like this:
Me._prtText = "Four score and seven years ago our fathers brought forth on this continent," &
" a new nation, conceived in Liberty, and dedicated to the proposition that" &
" all men are created equal." & vbCrLf & vbCrLf &
"Now we are engaged in a great civil war, testing whether that nation, or any" &
" nation so conceived and so dedicated, can long endure. We are met on a great" &
" battle-field of that war. We have come to dedicate a portion of that field," &
" as a final resting place for those who here gave their lives that that nation" &
" might live. It is altogether fitting and proper that we should do this." & vbCrLf & vbCrLf &
"But, in a larger sense, we can not dedicate – we can not consecrate – we can" &
" not hallow – this ground. The brave men, living and dead, who struggled here," &
" have consecrated it, far above our poor power to add or detract. The world will" &
" little note, nor long remember what we say here, but it can never forget what" &
" they did here. It is for us the living, rather, to be dedicated here to the" &
" unfinished work which they who fought here have thus far so nobly advanced." &
" It is rather for us to be here dedicated to the great task remaining before us" &
" – that from these honored dead we take increased devotion to that cause for" &
" which they gave the last full measure of devotion – that we here highly resolve" &
" that these dead shall not have died in vain – that this nation, under God, shall" &
" have a new birth of freedom – and that government of the people, by the people," &
" for the people, shall not perish from the earth." & vbCrLf

We can also set the font we want to print with by assigning a new font to _prtFont. For example:
“Me._prtFont = New Font("Verdana", 14)”.
In our BeginPrint event, we first want to check to see if _prtText contains data. If it does not, we want to
cancel the print job and leave. If it does contain data, we will want to initialize the index for the printing
process to Zero, setting the start of the text. We also want to check the font. If it has not been set, then
we should set a default. Consider the following BeginPrint event code (we do not require an EndPrint
event with this simple interface):
'*******************************************************************************
' Method Name : prtDoc_BeginPrint
' Purpose : Initialize for starting print job.
'*******************************************************************************
Private Sub prtDoc_BeginPrint(sender As Object, e As PrintEventArgs) Handles prtDoc.BeginPrint
If Me._prtText Is Nothing Then 'if no text to print, then cancel.
e.Cancel = True
Else
Me._prtIndex = 0 'init index to start of text.
If Me._prtFont Is Nothing Then 'if no font is selected, use a default.
Me._prtFont = New Font("Times New Roman", 10)
End If
End If
End Sub

Some like to clear out their text buffer in the EndPrint event to release resources, such as set _prtText to
Nothing, but if we do that, then also be sure that the BeginPrint event can re-access that data.
All we have left is the process of actually printing each page. This can take a lot of forms, where we might
print one line at a time, print a whole page at a time, or even process HP/GL code to render a blueprint.
 Page 719

Drawing Simple Text a Full Page at a Time


To draw a full page of text without needing to print each line individually is really rather easy. This same
technique can also be applied in more sophisticated tasks, such as to print a selected block of text to a
particular rectangular area on the target page, such as around a rendered image. Assuming the local
variables _prtTest, _prtFont, and _prtIndex are defined as shown on the previous page, and our
PrintBegin and PrintEnd events have been defined as shown there, we can draw the text, no matter how
long or how many pages it requires, by using the following PrintPage event:
'*******************************************************************************
' Method Name : prtDoc_PrintPage
' Purpose : Print a page. Keep track of each page using and updating _prtIndex.
'*******************************************************************************
Private Sub prtDoc_PrintPage(sender As Object, e As PrintPageEventArgs) Handles prtDoc.PrintPage
Dim marginLeft, marginRight, marginTop As Integer 'left, right, and top margins.
Dim printableWidth, printableHeight As Integer 'printable field dimensions on sheet.

With DirectCast(sender, PrintDocument).DefaultPageSettings 'Set print area size and margins.


marginLeft = .Margins.Left 'also determines start of printable area.
marginRight = .Margins.Right 'Note that all dimensions are in 0.01-inch increments.
marginTop = .Margins.Top
If .Landscape Then 'if landscape, then swap Height and Width
printableHeight = .PaperSize.Width - marginLeft - marginRight
printableWidth = .PaperSize.Height - marginTop - .Margins.Bottom
Else
printableWidth = .PaperSize.Width - marginLeft - marginRight
printableHeight = .PaperSize.Height - marginTop - .Margins.Bottom
End If
End With

'Use StringFormatFlags.LineLimit for the text layout of our document. With this setting:
' Only entire lines are laid out in the formatting rectangle. By default layout continues until
' the end of the text, or until no more lines are visible as a result of clipping, whichever
' comes first. Note that the default settings allow the last line to be partially obscured by a
' formatting rectangle that is not a whole multiple of the line height. To ensure that only whole
' lines are seen, specify this value and be careful to provide a formatting rectangle at least as
' tall as the height of one line.
Dim strFormat As New StringFormat(StringFormatFlags.LineLimit)

'See how many characters we can stuff within the print area...
Dim charsFitted, linesFitted As Int32 'receive number of characters and lines that fit.
e.Graphics.MeasureString(Me._prtText.Substring(Me._prtIndex),
Me._prtFont,
New Size(printableWidth, printableHeight),
strFormat,
charsFitted,
linesFitted) 'use charsFitted to update each start of page by summing it to _prtIndex.
' 'note we could also track line counts through linesFitted.
' Print the current page.
Dim Brsh As New SolidBrush(Me._prtColor) 'color to paint text with.
e.Graphics.DrawString(Me._prtText.Substring(Me._prtIndex, charsFitted),
Me._prtFont,
New SolidBrush(Me.ForeColor),
New RectangleF(marginLeft, marginTop, printableWidth, printableHeight),
strFormat)
Brsh.Dispose() 'dispose of used resource.
Me._prtIndex += charsFitted 'bump to the next page.
e.HasMorePages = Not String.IsNullOrWhiteSpace(Me._prtText.Substring(Me._prtIndex)) 'Determine if more text to print.
End Sub

Notice that we did not need to do anything special to inform the system that the page is ready for printing.
We just simply leave the event. When that happens, that page is sent to the printer ( or at least to the spooler),
and then the next page, if there are any additional pages, is processed. Were we processing each line at a
time, such as printing an array, we might use the linesFitted variable to bump an array index.
Notice that I checked for more pages using the String.IsNullOrWhiteSpace method. I cannot count the
number of times I got a blank page at the end because a few non-printable characters were left to print.
Notice further that instead of measuring the text using the usual TextRenderer.MeasureString method, we
use the MeasureString method from PrintPageEventArgs (e). Notice even further that this method contains
some additional features, such as a StringFormat parameter, plus variables to receive the number of
characters fitted into the provided target rectangle, and another integer variable to receive the number of
lines printed. We can take the charsFitted parameter and add it to our current character index to point to
 Page 720

the start of the text for the next page ( Me._prtIndex += charsFitted). We can also use this variable to
specify the number of characters to draw in the e.Graphics.DrawString method.
And now that we have all these parts in place, it would be a really good
idea to actually be able to start a print job. That is as easy as entering
“Me.prtDoc.Print”.
Consider the following, where a btnPrintText_Click event services a
button named btnPrintText. Note that the TextBox, txtData, could
easily be the contents of a FileStream instead and the font could be the
font assigned to the TextBox, or a font selected from a FontDialog:
Option Explicit On
Option Strict On
Option Infer Off

Imports System.Drawing.Printing

Public Class frmPrintTest


'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private WithEvents prtDoc As New PrintDocument 'printer I/O interface.
'---------------------------------------------------------------
Protected _prtFont As Font = New Font("Times New Roman", 10) 'font used for printing.
Protected _prtIndex As Integer = 0 'index into text where we are printing from.
Protected _prtColor As Color = SystemColor.ButtonText 'print color. Use in case High Contrast for vision impaired.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method Name : Form_Load
' Purpose : Sample method to print text to the default document.
'*******************************************************************************
Private Sub Form_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' Fill Form TextBox with some data.
Me.txtData.Text = "Four score and seven years ago our fathers brought forth on this continent," &
" a new nation, conceived in Liberty, and dedicated to the proposition that" &
" all men are created equal." & vbCrLf & vbCrLf &
"Now we are engaged in a great civil war, testing whether that nation, or any" &
" nation so conceived and so dedicated, can long endure. We are met on a great" &
" battle-field of that war. We have come to dedicate a portion of that field," &
" as a final resting place for those who here gave their lives that that nation" &
" might live. It is altogether fitting and proper that we should do this." & vbCrLf & vbCrLf &
"But, in a larger sense, we can not dedicate – we can not consecrate – we can" &
" not hallow – this ground. The brave men, living and dead, who struggled here," &
" have consecrated it, far above our poor power to add or detract. The world will" &
" little note, nor long remember what we say here, but it can never forget what" &
" they did here. It is for us the living, rather, to be dedicated here to the" &
" unfinished work which they who fought here have thus far so nobly advanced." &
" It is rather for us to be here dedicated to the great task remaining before us" &
" – that from these honored dead we take increased devotion to that cause for" &
" which they gave the last full measure of devotion – that we here highly resolve" &
" that these dead shall not have died in vain – that this nation, under God, shall" &
" have a new birth of freedom – and that government of the people, by the people," &
" for the people, shall not perish from the earth." & vbCrLf
End Sub

'*******************************************************************************
' Method Name : prtDoc_BeginPrint
' Purpose : Initialize for starting print job.
'*******************************************************************************
Private Sub prtDoc_BeginPrint(sender As Object, e As PrintEventArgs) Handles prtDoc.BeginPrint
If Me.txtData.Text Is Nothing Then 'if no text to print, then cancel.
e.Cancel = True
Else
Me._prtIndex = 0 'init index to start of text.
If Me._prtFont Is Nothing Then 'if no font is selected, use a default.
Me._prtFont = New Font("Verdana", 10)
End If
End If
End Sub

'*******************************************************************************
' Method Name : prtDoc_PrintPage
' Purpose : Print a page. Keep track of each page using and updating _prtIndex.
'*******************************************************************************
Private Sub prtDoc_PrintPage(sender As Object, e As PrintPageEventArgs) Handles prtDoc.PrintPage
Dim marginLeft, marginRight, marginTop As Integer 'left, right, and top margins.
Dim printableWidth, printableHeight As Integer 'printable field dimensions on sheet.

With DirectCast(sender, PrintDocument).DefaultPageSettings 'Set print area size and margins.


marginLeft = .Margins.Left 'also determines start of printable area.
 Page 721

marginRight = .Margins.Right 'Note all dimensions are in 0.01-inch increments.


marginTop = .Margins.Top
If .Landscape Then 'if landscape, then swap Height and Width.
printableHeight = .PaperSize.Width - marginLeft - marginRight
printableWidth = .PaperSize.Height - marginTop - .Margins.Bottom
Else
printableWidth = .PaperSize.Width - marginLeft - marginRight
printableHeight = .PaperSize.Height - marginTop - .Margins.Bottom
End If
End With

'Use StringFormatFlags.LineLimit for the text layout of our document. With this setting:
' Only entire lines are laid out in the formatting rectangle. By default layout continues until
' the end of the text, or until no more lines are visible as a result of clipping, whichever
' comes first. Note that the default settings allow the last line to be partially obscured by a
' formatting rectangle that is not a whole multiple of the line height. To ensure that only whole
' lines are seen, specify this value and be careful to provide a formatting rectangle at least as
' tall as the height of one line.
Dim strFormat As New StringFormat(StringFormatFlags.LineLimit)

' See how many characters we can stuff within the print area...
Dim charsFitted, linesFitted As Int32 'receive number of characters and lines that fit.
e.Graphics.MeasureString(Me.txtData.Text.Substring(Me._prtIndex),
Me._prtFont,
New Size(printableWidth, printableHeight),
strFormat,
charsFitted,
linesFitted)

' Print the current page.


Dim Brsh As New SolidBrush(Me._prtColor) 'color to paint text with.
e.Graphics.DrawString(Me.txtData.Text.Substring(Me._prtIndex, charsFitted),
Me._prtFont,
Brsh,
New RectangleF(marginLeft, marginTop, printableWidth, printableHeight),
strFormat)
Brsh.Dispose() 'dispose of used resource.
Me._prtIndex += charsFitted 'bump to the next page.
' Determine if there is more text to print.
e.HasMorePages = Not String.IsNullOrWhiteSpace(Me.txtData.Text.Substring(Me._prtIndex))
End Sub

'*******************************************************************************
' Method Name : btnPrintText_Click
' Purpose : Print the document.
'*******************************************************************************
Private Sub btnPrintText_Click(sender As Object, e As EventArgs) Handles btnPrintText.Click
Me.prtDoc.Print()
End Sub

End Class

Drawing Simple Text on a Page One Line at a Time


Using the DefaultPageSettings object of our prtDoc control, we have the dimensions of the page in 100-
units per inch increments, which means 1-inch contains 100 measurement units. What this comes down
to is an 8.5 x 11-inch page is 850 units wide by 1100 units long. Margins are likewise measured in 100-
units per inch. Note the fact that units measured with PrintPageEventArgs are also measured in 100-
units per inch, not pixels, and so coordinates should always be computed in 100-units per inch.
If we want to compute 1/ 100-units per inch dimensions of our print area, we could do this:
Dim lMargin As Int32 = Me.prtDoc.DefaultPageSettings.Margins.Left 'left margin in 1/100-inch units.
Dim rMargin As Int32 = Me.prtDoc.DefaultPageSettings.Margins.Right 'right margin in 1/100-inch units.
Dim tMargin As Int32 = Me.prtDoc.DefaultPageSettings.Margins.Top 'top margin in 1/100-inch units.
Dim bMargin As Int32 = Me.prtDoc.DefaultPageSettings.Margins.Bottom 'bottom margin in 1/100-inch units.
Dim prtWd As Int32 = CInt(Me.prtDoc.DefaultPageSettings.PrintableArea.Width) 'printable width in 1/100-inch units.
Dim prtHt As Int32 = CInt(Me.prtDoc.DefaultPageSettings.PrintableArea.Height) 'printable height in 1/100-inch units.
If Me.prtDoc.DefaultPageSettings.Landscape Then 'if printing landscale...
Dim tmp As Int32 = prtWd 'swap prtWd and PrtHt.
prtWd = prtHt
prtHt = tmp
End If
Dim LineHeight As Int32 = CInt(Me._prtFont.Height / 0.96) 'get 1/100-inch height from font height(96-pixels per inch / 100).
 Page 722

This gives us all the information we need to render text, images, and drawings to the printed page if we
want to compute our drawing placement manually. We can use these values to keep data within the
printable region by starting our left edges at lMargin and our top margin at tMargin. We can advance to a
new line by resetting our X position to lMargin and increasing our Y position by lineHeight. If our Y
position exceeds tMargin+prtHt, we are done with this page. If we have more text to render, we would set
e.HasMorePages to True. If X exceeds lMargin+prtWd, we need to back off our index until it fits.

To end the page, we simply return from the event. The internal print process running in the background
after we issue a prtDoc.Print instruction and starts processing the first PrintPage event, and then the next
page, if there is one, is rendered through subsequent PrintPage event invocations.
Again, we just have to keep in mind that we are processing measurements in 1/100-inch units, not pixels.
The trick with drawing one line at a time is usually line width, because it would not take much for the
text to bleed off the right margin if we are not vigilant. However, to the rescue is the updated
modComputeMsgDims module listed in Black Book Tip # 46, Quick and Easy Text-Justification for
Text Boxes, Labels, and Dialog Boxes, on page 614. Using either its SizeMessage or
SizeAndJustifyMessage methods, they can automatically force the text to fit within the left and right
margins of the printable area. Consider this updated form code to support this:
Option Explicit On
Option Strict On
Option Infer Off

Imports System.Drawing.Printing

Public Class frmPrintTest


'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private WithEvents prtDoc As New PrintDocument 'printer I/O interface.
'---------------------------------------------------------------
Protected _prtTextAry() As String = Nothing 'Formatted text to be printed.
Protected _prtFont As Font = New Font("Times New Roman", 10) 'font used for printing.
Protected _prtIndex As Integer = 0 'index into text where we are printing from.
Protected _prtColor As Color = SystemColor.ButtonText 'print color. Use in case High Contrast for vision impaired.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method Name : Form_Load
' Purpose : Sample method to print text to the default document.
'*******************************************************************************
Private Sub Form_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' Fill Form TextBox with some data
Me.txtData.Text = "Four score and seven years ago our fathers brought forth on this continent," &
" a new nation, conceived in Liberty, and dedicated to the proposition that" &
" all men are created equal." & vbCrLf & vbCrLf &
"Now we are engaged in a great civil war, testing whether that nation, or any" &
" nation so conceived and so dedicated, can long endure. We are met on a great" &
" battle-field of that war. We have come to dedicate a portion of that field," &
" as a final resting place for those who here gave their lives that that nation" &
" might live. It is altogether fitting and proper that we should do this." & vbCrLf & vbCrLf &
"But, in a larger sense, we can not dedicate – we can not consecrate – we can" &
" not hallow – this ground. The brave men, living and dead, who struggled here," &
" have consecrated it, far above our poor power to add or detract. The world will" &
" little note, nor long remember what we say here, but it can never forget what" &
" they did here. It is for us the living, rather, to be dedicated here to the" &
" unfinished work which they who fought here have thus far so nobly advanced." &
" It is rather for us to be here dedicated to the great task remaining before us" &
" – that from these honored dead we take increased devotion to that cause for" &
" which they gave the last full measure of devotion – that we here highly resolve" &
" that these dead shall not have died in vain – that this nation, under God, shall" &
" have a new birth of freedom – and that government of the people, by the people," &
" for the people, shall not perish from the earth." & vbCrLf
End Sub

'*******************************************************************************
' Method Name : prtDoc_BeginPrint
' Purpose : Initialize for starting print job
'*******************************************************************************
Private Sub prtDoc_BeginPrint(sender As Object, e As PrintEventArgs) Handles prtDoc.BeginPrint
If Me.txtData.Text Is Nothing Then 'if no text to print, then cancel.
e.Cancel = True
Else
Me._prtIndex = 0 'init index to start of text.
If Me._prtFont Is Nothing Then 'if no font is selected, use a default.
 Page 723

Me._prtFont = New Font("Verdana", 10)


End If
With DirectCast(sender, PrintDocument).DefaultPageSettings
Dim printableWidth As Int32
If .Landscape Then 'use height of landscape to compute pixel width used by SizeAndJustifyMessage.
printableWidth = CInt((.PaperSize.Height - .Margins.Top - .Margins.Bottom) * 0.96)
Else
printableWidth = CInt((.PaperSize.Width - .Margins.Left - .Margins.Right) * 0.96)
End If
Dim tmp As String = Me.txtData.Text 'get an alterable copy of the text.
SizeAndJustifyMessage(tmp, Me._prtFont, printableWidth) 'format text to fit within width.
Me._prtTextAry = Split(Join(Split(tmp, vbCrLf), vbCr), vbCr) 'break up text into an array.
End With
End If
End Sub

'*******************************************************************************
' Method Name : prtDoc_EndPrint
' Purpose : Finished Printing.
'*******************************************************************************
Private Sub prtDoc_EndPrint(sender As Object, e As PrintEventArgs) Handles prtDoc.EndPrint
Erase Me._prtTextAry 'remove any formatted text data.
End Sub

'*******************************************************************************
' Method Name : prtDoc_PrintPage
' Purpose : Print a page. Keep track of each page using and updating _prtIndex.
'*******************************************************************************
Private Sub prtDoc_PrintPage(sender As Object, e As PrintPageEventArgs) Handles prtDoc.PrintPage
Dim marginLeft, marginRight, marginTop As Integer 'left, right, and top margins.
Dim printableHeight As Integer 'printable field dimensions on sheet.

With DirectCast(sender, PrintDocument).DefaultPageSettings 'Set print area size and margins.


marginLeft = .Margins.Left 'also determines start of printable area.
marginRight = .Margins.Right
marginTop = .Margins.Top
If .Landscape Then 'if landscape, then swap Height and Width.
printableHeight = .PaperSize.Width - marginLeft - marginRight
Else
printableHeight = .PaperSize.Height - marginTop - .Margins.Bottom
End If
End With

Dim Y As Int32 = marginTop 'init to top of page.


Dim Ylimit As Int32 = marginTop + printableHeight 'compute bottom text limit.
Dim LineHeight As Int32 = CInt(Me._prtFont.Height / 0.96) 'get the height of font in 1/100-inch units.
Dim aryUB As Int32 = UBound(Me._prtTextAry) 'get the upper bounds of the array.
Dim Brsh As New SolidBrush(Me._prtColor) 'select color, in case user did not choose black.
Do While Y < Ylimit AndAlso Me._prtIndex <= aryUB 'while we can print lines and lines are left...
e.Graphics.DrawString(Me._prtTextAry(Me._prtIndex), Me._prtFont, Brsh, New Point(marginLeft, Y)).
Me._prtIndex += 1 'bump array index.
Y += LineHeight 'bump Y index by one printed line.
Loop
Brsh.Dispose() 'dispose of created resource.
e.HasMorePages = Me._prtIndex <= aryUB
End Sub

'*******************************************************************************
' Method Name : btnPrintText_Click
' Purpose : Print the document.
'*******************************************************************************
Private Sub btnPrintText_Click(sender As Object, e As EventArgs) Handles btnPrintText.Click
Me.prtDoc.Print()
End Sub

End Class

There are many ways to draw data, to include drawing shapes, lines, images, and changing colors. If we
have drawn to a form or a PictureBox, we already know how to draw to the printer. And I will beat the
poor exhausted horse by reminding we once again that measurements are in 1/100-inch units.
NOTE: If we are still thinking about my early mention of wrapping text around an image, which requires much more space
for an already large Black Book Tip, consider this approach: Draw the image, say to the left side of the page and halfway
down. Next, draw the text down to the top of the image as a block of text, though allow for a small gap between the text and
the image. Next, compute text width from the right side of the image, plus a gap, to the right page margin, and down to the
bottom of the image as a block of text. Finally, using the left and right page margins again, finish the page as a block of text,
just as we did above the image. Try it on a PictureBox to hone our skill. Soon it will become child's play. After a while we will
place images anywhere, and with justified text. Tricky, but that is why commercial word processors make the big bucks.
 Page 724

Selecting a Printer
Using the PrinterDialog control is easy. Though some programmers get frustrated
trying to figure out how to assign the dialog’s options to their PrintDocument
object, we really should not concern ourselves because the dialog will actually set
it for us automatically if we simply follow instructions and provide the dialog with
a reference to our PrintDocument object as the documentation tells us to. Some
programmers complain that the dialog keeps reporting an unhandled exception
error when they try to use it, but this is simply because they are not assigning their
PrintDocument object to the control’s Document property.
Because we typically invoke the PrintDialog when we choose to print our document (probably why the
dialog has a Print button in place of an OK button), we will update our btnPrintText_Click event code:
'*******************************************************************************
' Method Name : btnPrintText_Click
' Purpose : Print the document by calling up the PrintDialog.
'*******************************************************************************
Private Sub btnPrintText_Click(sender As Object, e As EventArgs) Handles btnPrintText.Click
With New PrintDialog 'instantiate a new PrintDialog instance.
.Document = Me.prtDoc 'PrintDocument object used to collect print settings.
.AllowSomePages = True 'allow user to select pages to print.
.ShowHelp = False 'do not show the Help button (shown on OLD form dialog, anyway).
.UseEXDialog = True 'show enhanced Printer dialog (False = simple (OLD) dialog).
'Hitting the Print button returns a result of DialogResult.OK, otherwise DialogResult.Cancel.
If .ShowDialog(Me) = DialogResult.OK Then 'show dialog, change prtDoc settings if user hits Print (OK).
Me.prtDoc.Print() 'print the document.
End If
.Dispose() 'dispose of PrintDialog resources.
End With
End Sub 'Over 90% of VB.NET printing problems are solved by the above highlighted line.

Selecting Page Setup Options


The part that runs a lot of coders new to VB.NET printing into trouble is updating page and printer settings
from the PageSetupDialog. Frustration first comes when they try updating settings, like paper width and
height, where they get exception errors. For example, they try setting up a PageSetupDialog control, but
when they try to show it, it blows up. This error, for those who bother reading the error’s description before
crying “Stupid computer!” is due, like with the PrintDialog, to not setting the control’s Document property to
the PrintDocument object being used. Further, there is no need to update settings after the dialog has closed,
because it, like the PrintDialog, has updated our PrintDocument object automatically for us as long as we
provide it with our PrintDocument reference (some “gurus” will advise us to supply it with new instances of
PageSettings and PrinterSettings objects, but I will show this to be brain-challenged advice in a moment).
Setting up a PageSettingsDialog is more elaborate than the other dialogs, but it is actually quite easy to
understand and use. With a button named btnPageSetup, consider the following Click event code:
'*******************************************************************************
' Method Name : btnPageSetup_Click
' Purpose : Set up a printed page format.
'*******************************************************************************
Private Sub btnPageSetup_Click(sender As Object, e As EventArgs) Handles btnPageSetup.Click
' the PageSetupDialog control enables users to change page-related print settings, including margins and paper orientation.
With New PageSetupDialog
' allow setting margins section of the dialog box.
.AllowMargins = True
' allow setting the orientation section of the dialog box (landscape versus portrait).
.AllowOrientation = True
' allow setting the paper section of the dialog box (paper size and paper source).
.AllowPaper = True
' allow selecting the Printer from this dialog (this shows only on versions of Windows prior to Windows Vista).
.AllowPrinter = True
' During margin settings, if displayed in millimeters, it will be converted to and from hundredths of an inch.
.EnableMetric = True
' Show the network as needed in the printer dialog (shown if local network available).
.ShowNetwork = My.Computer.Network.IsAvailable()
' PrintDocument object to use to format data.
.Document = Me.prtDoc 'assign document object to dialog – apparently most programmers forget to do this!
.ShowDialog() 'Show the dialog.
.Dispose() 'dispose of dialog resources.
End With
End Sub
 Page 725

This dialog is not complicated; it just has many options. Also, the red-
highlighted options either do not show on new systems (the Printer
button). and if we do not have a local network (I am not talking about
our Cable or DSL Internet LAN), we will not see a network button.
Notice we set the Document property to our PrintDocument object. This
is extremely important! In most examples on blogs and forums, they
tell us to use temporary instances of PageSettings and PrinterSettings,
and assign them to the dialog to avert crashes. But these properties are
meant to access those objects from our own PrintDocument object, if
we but assign the Document property to it, as its documentation tells us!
It also makes more sense to take advantage of the settings we might
already have, or had previously set during a previous dialog session.
Actually, I think the reason people try to reassign them is when they
try to examine them via the dialog's Document property, it provides no GET properties, and so we might
therefore assume them set to Nothing. The truth is, it just funnels assignments to those objects within our
own PrintDocument object (hence, we should just access our own PrintDocument object to examine
these properties!). The reason they are exposed at all is if we need to apply new default settings. So, in
most cases we should skip assigning these new object instances and just assign our PrintDocument
object to the dialog’s Document property if we want to use its settings, and all will work perfectly!

Selecting a Printer Font and Font Color


Now that we are all experts at printing (we will get to WYSIWYG soon!), we might as well provide an
interface to the FontDialog. With a button named btnSelectFont on our form, what follows is event
code we might assign to it, though notice that we also address the APPLY button that it features:
'*******************************************************************************
' Method Name : btnSelectFont_Click
' Purpose : Select a font to display with.
' NOTE : Assume we are displaying the text in a TextBox named txtData.
'*******************************************************************************
Private Sub btnSelectFont_Click(sender As System.Object, e As EventArgs) Handles btnSelectFont.Click
With New FontDialog
.FontMustExist = True 'all listed fonts must exist.
.ShowApply = True 'show apply button.
AddHandler .Apply, AddressOf FontDialog_Apply 'attach Apply button to event handler.
.ShowEffects = True 'show effect options.
.Color = Me._prtColor 'init to current color.
.ShowColor = True 'allow user to select a color.
If Me._prtFont IsNot Nothing Then 'if a font is presently defined...
.Font = Me._prtFont 'then use it (otherwise use default).
End If
If .ShowDialog(Me) = DialogResult.OK Then 'did user choose accept selection?
Me._prtFont = .Font 'yes, so save the font locally...
Me._prtColor = .Color 'along with the color selection.
End If
RemoveHandler .Apply, AddressOf FontDialog_Apply 'detach Apply event handler.
.Dispose() 'dispose of dialog resources.
End With 'NOTE: Apply event is only used while FontDialog is displayed and if the Apply button is displayed.
End Sub

'*******************************************************************************
' Method Name : FontDialog_Apply
' Purpose : Demonstrate Font selection by changing the TextBox.
'*******************************************************************************
Private Sub FontDialog_Apply(ByVal sender As Object, ByVal e As EventArgs)
With DirectCast(sender, FontDialog) 'access the provided FontDialog control.
Me.txtData.Font = .Font 'reflect the font in the textbox.
Me.txtData.ForeColor = .Color 'reflect the font color in the TextBox.
End With
End Sub

We save the font and font color selection to local storage, in our case _prtFont and _prtColor, for use
by the PrintPage event. If the Apply button was selected, the font and color are reflected in the TextBox.
 Page 726

Our demonstration of the APPLY button on the FontDialog was simple and easy, but it seems to be the
source of a lot of frustration to many developers. Even after programmers figure out that they have to
use an Apply event for their FontDialog control, they think they can only use it if they drop the control
onto the form. Actually, like with our prtDoc object, we could have instantiated a new instance of a
FontDialog “WithEvents”, such as “Private WithEvents Font_Dialog As New FontDialog ”, and then we
could access its Apply event from the dropdowns at the top of the code page, but because I write event
headers all the time from scratch, for me to type “ Private Sub FontDialog_Apply(ByVal sender As Object,
ByVal e As EventArgs)”, especially with autocomplete and intellisense active, is almost too easy.

Also, notice that I am applying this to a new instance of the FontDialog control that has not even been
named. Taking advantage of With...End With blocks makes this very easy to do. Even in the Apply
event, we do not need to declare a control name, but to simply cast the sender object to a FontDialog.
Notice finally that I detached the event handler from the control before leaving the Font Dialog event. It
will be removed anyway when it goes out of scope or is disposed of by the Delegate Class object that the
CLR assigns to it, but I just like to take responsibility and keep my code clean (it is an old C++ habit.
Indeed. VB.NET has it EASY compared to other languages, where we must meticulously manage
disposal of all instantiated objects ourselves. How many times have we disposed of all controls on a
form, or even forms, not to mention all the class objects we used? We are a spoiled lot under VB.NET,
where doing this in other programming languages would cause endless memory leaks!).

Performing a Print Preview


I have probably seen more consternation on the web over this than I have seen on any of the other
topics. The interesting thing is, it is one of the easiest dialogs to support! The trick, though, is to get the
printing processes correct. Once the support structure for a PrintDocument object is in place, everything
else is easy. Like everything else, be sure to assign our PrintDocument object to its Document property.
With a button on the form named btnPrintPreview, add the following Click event code to the form:
'*******************************************************************************
' Method Name : btnPrintPreview_Click
' Purpose : Do a print preview.
'*******************************************************************************
Private Sub btnPrintPreview_Click(sender As Object, e As EventArgs) Handles btnPrintPreview.Click
With New PrintPreviewDialog 'process print preview.
.Document = Me.prtDoc 'PrintDocument object to use.
.UseAntiAlias = True 'smooth font display.
.ShowDialog(Me) 'show print preview.
.Dispose() 'dispose of dialog resources.
End With
End Sub

Like with the PrintDialog and the PageSetupDialog


controls, using the PrintPreviewDialog is a snap. Assign the
PrintDocument object to the control’s Document property,
maybe set the UseAntiAlias property to True to smooth the
edges of the screen-displayed font in the print preview, and
we are off to the races. We can even print our document to
the default printer from within the dialog.
NOTE: A lot of programmers get themselves into trouble here because
in their EndPrint event they might flush the printed text data so to clear
resources. However, the PrintPreviewDialog uses our printing interface
to render its previews as though we were sending it to a printer, and
hitting the PRINT button might then not find any text when it re-runs
the printer interface, and at that point the BeginPrint event, or if it is not defined or if it is not programmed to re-access the
data. we might not find anything to print! In that case re-running the PrintPreviewDialog will also not find any data that time
around. Therefore, we must either maintain the data throughout the program run, or rewind, re-open, or re-access that data
in in a BeginPrint event (this is one of the best reasons for implementing this print job setup event).
 Page 727

Supporting the Help Buttons on Dialogs


A lot of programmers think adding the HELP button on forms, such as the ShowHelp properties of the
PrintDialog, the PageSetupDialog, and the FontDialog controls is a good idea, but then they get
frustrated when pressing the displayed button does not do anything. Actually, it is because this help does
not come automatically, but we must supply that help. Even more, we must provide event code for the
dialog control to react to it being pressed.
As I had demonstrated with event support for the Apply button in the FontDialog control code, we must
provide help through a HelpRequest event for the controls.
For the PrintDialog control, Microsoft has reported “This property was marked obsolete in Windows
2000 and does not affect this and later versions of Windows.” So, if we enable the control’s UseEXDialog
property by setting it to True, it will not even be displayed. However, if we do not set this property to
True, so we will see the older-style printer interface, we will in fact see it, which is why the UseEXDialog
property exists, because so many developers still wanted that older dialog functionality.
With that in mind, consider this modified version of our btnPrintText_Click event code:
'*******************************************************************************
' Method Name : btnPrintText_Click
' Purpose : Print the document by calling up the older style PrintDialog.
'*******************************************************************************
Private Sub btnPrintText_Click(sender As Object, e As EventArgs) Handles btnPrintText.Click
Me.prtDoc.Text = Me.txtData.Text 'update text, in case of a change.
With New PrintDialog 'instantiate a new PrintDialog instance.
.Document = Me.prtDoc 'PrintDocument object used to collect print settings.
.AllowSomePages = True 'allow user to select pages to print.
.UseEXDialog = False 'DO NOT show enhanced Printer dialog (False = simple dialog).
.ShowHelp = True 'show the Help button only in the simple dialog.
AddHandler .HelpRequest,
AddressOf PrintDialog_HelpRequest 'attach HelpRequest handler.
' Hitting the Print button returns a result of DialogResult.OK, otherwise DialogResult.Cancel.
If .ShowDialog(Me) = DialogResult.OK Then 'show dialog, change prtDoc settings if user hits Print (OK).
Me.prtDoc.Print() 'print the document.
End If
' detach HelpRequest event handler.
RemoveHandler .HelpRequest, AddressOf PrintDialog_HelpRequest
.Dispose() 'dispose of PrintDialog resources.
End With
End Sub

'*******************************************************************************
' Method Name : PrintDialog_HelpRequest
' Purpose : Provide Help for using the Print Dialog.
'*******************************************************************************
Private Sub PrintDialog_HelpRequest(sender As Object, e As EventArgs)
MsgBox("Just choose a stupid printer!", , "Print Dialog Help")
End Sub

Of course, we might want to be a bit less of a jerk in our help, but to try and convince our application’s
users that we are really a nice person. We can easily throw up a more elaborate MsgBox, a help form,
open a HTML or MHTML document, a text file in Notepad, or a RTF file in WordPad.

The PageSetupDialog and FontDialog controls, on the other hand, still actively support their Help
buttons, and we provide support through their HelpRequest events just like we do with the PrintDialog.
NOTE: This concept also applies to the Apply button in the PageSetupDialog control.
 Page 728

Creating a Print Document Class


Prior to creating a really cool printer interface for a Rich Text Box that supports embedded graphics, let
us wrap up printing simple text in a bow by encapsulating all our plain text printing interface code,
saving it to a class file so we can add it to other projects and let them also take advantage of it. Indeed,
this class can make printing much easier than printing was under VB6, yet with more power!
It is not difficult at all because we have already written all the code it requires. The only things that are
different are that we will add properties to the class to set the text to print, the font, and the font color.
Also, instead of using the regular control events, we will take advantage of the more obscure, but very
easy to use class-bound ON-events, specifically OnBeginPrint and OnPrintPage.
Consider my PrintText class that inherits from PrintDocument and we can use this in place of it:
Option Explicit On
Option Strict On
Option Infer Off
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' PrintText Class (Inherits from System.Drawing.Printing.PrintDocument
' Simple Print Document Class using a single font.
' It will need to exploit only the OnPrintPage event and a VERY SIMPLE OnBeginPrint event.
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imports System.Drawing
Public Class PrintText
Inherits Printing.PrintDocument
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Protected _prtText As String = String.Empty 'Property variable for the text to be printed.
Protected _prtIndex As Integer = 0 'index into text where we are printing from.
Protected _prtFont As Font = New Font("Times New Roman", 10) 'Property variable for the Font the user wishes to use.
Protected _prtColor As Color = SystemColor.ButtonText 'print color. Use in case High Contrast for vision impaired.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
' Property : Text
' Purpose : Get/Set the text that we want to print.
'*********************************************************************************
Public Property Text As String
Get
Return Me._prtText
End Get
Set(ByVal Value As String)
If Value Is Nothing Then
Me._prtText = String.Empty
Else
Me._prtText = Value
End If
End Set
End Property

'*********************************************************************************
' Property : Font
' Purpose : Get/Set the Font that we want to print with.
'*********************************************************************************
Public Property Font As Font
Get
Return Me._prtFont
End Get
Set(value As Font)
If value Is Nothing Then
Me._prtFont = New Font("Times New Roman", 10)
Else
Me._prtFont = value
End If
End Set
End Property

'*********************************************************************************
' Property : FontColor
' Purpose : Get/Set the Font Color that we want to print with.
'*********************************************************************************
Public Property FontColor As Color
Get
Return Me._prtColor
End Get
Set(value As Color)
Me._prtColor = value
End Set
End Property

'*******************************************************************************
 Page 729

' Method : New


' Purpose : optional constructor to assigned text we want to print.
'*******************************************************************************
Public Sub New(Optional ByVal Text As String = Nothing)
Me.Text = Text
End Sub

'*******************************************************************************
' Method : New
' Purpose : optional constructor to assigned font we want to print with, and text we want to print.
'*******************************************************************************
Public Sub New(ByVal Font As Font, Optional ByVal Text As String = Nothing)
Me.Font = Font
Me.Text = Text
End Sub

'*******************************************************************************
' Method : New
' Purpose : optional constructor to assigned font and text from a provided TextBox.
'*******************************************************************************
Public Sub New(ByRef TextBox As TextBox)
If TextBox IsNot Nothing Then
Me.Font = DurectCast(TextBox.Font.Clone, Font) 'Clone copy of Font object.
Me.FontColor = TextBox.ForeColor 'grab copy of forecolor.
Me.Text = TextBox.Text 'grab copy of text.
End If
End Sub

'*******************************************************************************
' Method : OnBeginPrint
' Purpose : Override the default OnBeginPrint method of the PrintDocument.
'*******************************************************************************
Protected Overrides Sub OnBeginPrint(e As Printing.PrintEventArgs)
MyBase.OnBeginPrint(e) 'do base processing. REQUIRED.
If Me._prtText Is Nothing Then 'if no text to print, then cancel.
e.Cancel = True
Else
Me._prtIndex = 0 'init index to start of text.
If Me._prtFont Is Nothing Then 'if no font is selected, use a default.
Me._prtFont = New Font("Times New Roman", 10)
End If
End If
End Sub

'*******************************************************************************
' Method : OnPrintPage
' Purpose : Override the default OnPrintPage method of the PrintDocument.
'*******************************************************************************
Protected Overrides Sub OnPrintPage(ByVal e As Printing.PrintPageEventArgs)
Dim marginLeft, marginRight, marginTop As Integer 'left, right, and top margins.
Dim printableWidth, printableHeight As Integer 'printable field dimensions on sheet.

MyBase.OnPrintPage(e) 'do base processing. REQUIRED.


With MyBase.DefaultPageSettings 'Set print area size and margins.
marginLeft = .Margins.Left 'also determines start of printable area.
marginRight = .Margins.Right 'Note all dimensions are in 1/100-inch units.
marginTop = .Margins.Top
printableWidth = .PaperSize.Width - marginLeft - marginRight
printableHeight = .PaperSize.Height - marginTop - .Margins.Bottom
End With

If MyBase.DefaultPageSettings.Landscape Then 'if the user selected to print in Landscape mode...


Dim swap As Integer = printableHeight 'then swap dimensions for printable area.
printableHeight = printableWidth
printableWidth = swap
End If

'Use StringFormatFlags.LineLimit for the text layout of our document. With this setting:
' Only entire lines are laid out in the formatting rectangle. By default layout continues until
' the end of the text, or until no more lines are visible as a result of clipping, whichever
' comes first. Note that the default settings allow the last line to be partially obscured by a
' formatting rectangle that is not a whole multiple of the line height. To ensure that only whole
' lines are seen, specify this value and be careful to provide a formatting rectangle at least as
' tall as the height of one line.
Dim strFormat As New StringFormat(StringFormatFlags.LineLimit)
' See how many characters we can stuff within the print area...
Dim charsFitted, linesFitted As Int32 'receive number of characters and lines that fit.
e.Graphics.MeasureString(Me._prtText.Substring(_prtIndex),
Me.Font,
New Size(printableWidth, printableHeight),
strFormat,
charsFitted,
linesFitted)
' Print the current page
Dim Brsh As New SolidBrush(Me._prtColor) 'select color, in case user did not choose black.
e.Graphics.DrawString(Me._prtText.Substring(_prtIndex, charsFitted),
Me.Font,
 Page 730

Brsh,
New RectangleF(marginLeft, marginTop, printableWidth, printableHeight),
strFormat)
Brsh.Dispose() 'dispose of created resources.
_prtIndex += charsFitted 'bump to the next page.
' Determine if there is more text to print
e.HasMorePages = Not String.IsNullOrWhiteSpace(Me._prtText.Substring(Me._prtIndex))
End Sub

End Class

With this class, we can clean up our form by not needing to include PrintDocument event handlers,
because the new class object will take care of all that for us. Consider the updated form:
Option Explicit On
Option Strict On
Option Infer Off

Imports System.Drawing.Printing
Public Class frmPrintTest
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private prtDoc As New PrintText 'printer I/O interface. This could have been declared WithEvents.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method Name : Form_Load
' Purpose : Sample method to print text to the default document.
'*******************************************************************************
Private Sub Form_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load
' Fill Form TextBox with some data.
Me.txtData.Text = "Four score and seven years ago our fathers brought forth on this continent," &
" a new nation, conceived in Liberty, and dedicated to the proposition that" &
" all men are created equal." & vbCrLf & vbCrLf &
"Now we are engaged in a great civil war, testing whether that nation, or any" &
" nation so conceived and so dedicated, can long endure. We are met on a great" &
" battle-field of that war. We have come to dedicate a portion of that field," &
" as a final resting place for those who here gave their lives that that nation" &
" might live. It is altogether fitting and proper that we should do this." & vbCrLf & vbCrLf &
"But, in a larger sense, we can not dedicate – we can not consecrate – we can" &
" not hallow – this ground. The brave men, living and dead, who struggled here," &
" have consecrated it, far above our poor power to add or detract. The world will" &
" little note, nor long remember what we say here, but it can never forget what" &
" they did here. It is for us the living, rather, to be dedicated here to the" &
" unfinished work which they who fought here have thus far so nobly advanced." &
" It is rather for us to be here dedicated to the great task remaining before us" &
" – that from these honored dead we take increased devotion to that cause for" &
" which they gave the last full measure of devotion – that we here highly resolve" &
" that these dead shall not have died in vain – that this nation, under God, shall" &
" have a new birth of freedom – and that government of the people, by the people," &
" for the people, shall not perish from the earth." & vbCrLf
End Sub

'*******************************************************************************
' Method Name : btnPrintText_Click
' Purpose : Print the document by calling up the PrintDialog.
'*******************************************************************************
Private Sub btnPrintText_Click(sender As Object, e As EventArgs) Handles btnPrintText.Click
With New PrintDialog 'instantiate a new PrintDialog instance.
Me.prtDoc.Text = Me.txtData.Text 'update text, in case of a change.
.Document = Me.prtDoc 'PrintDocument object used to collect print settings.
.AllowSomePages = True 'allow user to select pages to print.
.ShowHelp = False 'do not show the Help button.
.UseEXDialog = True 'show enhanced Printer dialog (False = simple dialog).
'Hitting the Print button returns a result of DialogResult.OK, otherwise DialogResult.Cancel.
If .ShowDialog(Me) = DialogResult.OK Then 'show dialog, change prtDoc settings if user hits Print (OK).
Me.prtDoc.Print() 'print the document.
End If
.Dispose() 'dispose of PrintDialog resources.
End With
End Sub

'*******************************************************************************
' Method Name : btnPageSetup_Click
' Purpose : Set up a printed page format.
'*******************************************************************************
Private Sub btnPageSetup_Click(sender As Object, e As EventArgs) Handles btnPageSetup.Click
' the PageSetupDialog control enables users to change page-related
' print settings, including margins and paper orientation.
With New PageSetupDialog
' allow setting margins section of the dialog box.
.AllowMargins = True
' allow setting the orientation section of the dialog box (landscape versus portrait).
.AllowOrientation = True
' allow setting the paper section of the dialog box (paper size and paper source).
 Page 731

.AllowPaper = True
' allow selecting the Printer from this dialog (this shows only on versions of Windows prior to Vista).
.AllowPrinter = True
' During margin settings, if displayed in millimeters, it will be converted to and from hundredths of an inch.
.EnableMetric = True
' Show the network as needed in the printer dialog (shown if local network available).
.ShowNetwork = My.Computer.Network.IsAvailable()
'PrintDocument object to use to format data.
.Document = Me.prtDoc 'PrintDocument object used to collect print settings.
.ShowDialog() 'show the dialog.
.Dispose() 'dispose of dialog resources.
End With
End Sub

'*******************************************************************************
' Method Name : btnSelectFont_Click
' Purpose : Select a font to print with.
'*******************************************************************************
Private Sub btnSelectFont_Click(sender As Object, e As EventArgs) Handles btnSelectFont.Click
With New FontDialog
.FontMustExist = True 'all listed fonts must exist.
.ShowApply = True 'show apply button.
AddHandler .Apply, AddressOf FontDialog_Apply 'attach Apply button to event handler.
.ShowEffects = True 'show effect options.
.Color = Me.prtDoc.FontColor 'init to current color.
.ShowColor = True 'allow user to select a color.
If Me.prtDoc.Font IsNot Nothing Then 'if a font is presently defined.
.Font = Me.prtDoc.Font 'use it (otherwise use default).
End If
If .ShowDialog(Me) = DialogResult.OK Then 'did user chose accept selection?
Me.prtDoc.Font = .Font 'yes, so save the font...
Me.prtDoc.FontColor = .Color 'and the color.
End If
RemoveHandler .Apply, AddressOf FontDialog_Apply 'detach Apply event handler.
.Dispose() 'dispose of dialog resources.
End With
End Sub

'*******************************************************************************
' Method Name : FontDialog_Apply
' Purpose : Demonstrate Font selection by changing the TextBox.
'*******************************************************************************
Private Sub FontDialog_Apply(ByVal sender As Object, ByVal e As EventArgs)
With DirectCast(sender, FontDialog)
Me.txtData.Font = .Font
Me.txtData.ForeColor = .Color
End With
End Sub

'*******************************************************************************
' Method Name : btnPrintPreview_Click
' Purpose : Do a print preview.
'*******************************************************************************
Private Sub btnPrintPreview_Click(sender As Object, e As EventArgs) Handles btnPrintPreview.Click
Me.prtDoc.Text = Me.txtData.Text 'update text, in case of a change.
With New PrintPreviewDialog 'process print preview.
.Document = Me.prtDoc 'PrintDocument object to use.
.UseAntiAlias = True 'smooth font display.
.ShowDialog(Me) 'show print preview.
.Dispose() 'dispose of dialog resources.
End With
End Sub
End Class

NOTE: As an exercise, try updating the PrintText class to print one line at a time. Also try using the SizeMessage or
the SizeAndJustifyMessage methods from the modComputeMsgDims module, as demonstrated earlier. Also consider
adding PrintAppend and PrintLine methods to dynamically add text and lines of text to the _prtText variable. Add a
PagePrint method to initiate printing accumulated data to the printer. We could also add an internal method to monitor
when a page is full and go ahead and just print that single page. We could even incorporate the printer Dialogs within
the class, to make form code even simpler!
 Page 732

Printing WYSIWYG RichTextBox Text


Printing What-You-See-Is-What-You-Get Rich Text might seem to be a more trying endeavor, because we
have various fonts, styles, effects, and sizes that must be accounted for. The fact is, to print the contents
of a RichTextBox control, complete with images and formatted text (even Full justification if we take
advantage of Black Book Tip # 30, “Enable Built-In Justify-Alignment in a RichTextBox from VB.NET”
on page 555), only requires that we send a system message, EM_FORMATRANGE, along with a
FORMATRANGE structure, to have a RichTextBox control format a range of text for a specific device,
such as our printer. The trick, though, according to many, is getting there.
Martin Müller of 4voice AG wrote an article for MSDN Magazine in their January 2003 issue named
“Getting WYSIWYG Print Results from a .NET RichTextBox”, https://msdn.microsoft.com/en-
us/library/ms996492.aspx. This is an excellent article, but I think it greatly confused readers because it got a
bit thick, especially because it listed C++ and C# code mixed right in with VB code, but it was really
confusing in the parts where it is formatting fonts, point sizes, effects and such through Pinvokes and
structures, which is required in C++, rather than using the built-in functionality of the VB and C#
RichTextBox control. Also, most developers want to know exactly how to get into the root essentials of
how to do what they need to do, and forget about using Pinvokes for doing a lot of things that are more
easily done through the control’s wrapper class functionality. Also, his approach required creating a new
RichTextBox class that inherits from the RichTextBox control, which is a good example, but most people
want to simply add this functionality to code and RichTextBox controls they already have in place
without needing to redesign their software more than they need to, to print or print-preview its contents.
In that light, I will apply Martin’s knowledge to regular RichTextBox controls we might already have
placed on our application forms, and I will cut out all the additional stuff that is not necessary (hard to
belive for a bloviater like me). I will also greatly simplify this code using overloaded methods so we can
remove unnecessary tasks like allocating, copying, and deallocating memory (this may have been
necessary until VB2005 came along). I still wrap all this added functionality in a class so we do not have
to worry about much of it, creating an extended PrintDocument object named PrintRichText. We
instantiate it in place of a regular PrintDocument object, just as we just did with the simpler PrintText
class, shown previously. The only additional task we must perform is to assign the RichTextBox control
we want to use with it to the object’s RichTextControl property (note that we can therefore actually
process several different RichTextBox controls sequentially using the very same PrintRichText object!).
An additional property, ShowMarginTags, when set to True, will display blue corner tags on the displayed
sheet to mark where the test boundary margins are located. Its default state is False.
In order to get a RichTextBox to render its display output to another device, such as our printer, as
previously mentioned, requires we send it a EM_FORMATRANGE message, along with a
FORMATRANGE structure.
The FORMATRANGE structure is defined like this:
'Information that a rich edit control uses to format its output for a particular device.
'This structure is used with the EM_FORMATRANGE message.
<StructLayout(LayoutKind.Sequential)>
Friend Structure FORMATRANGE
Friend hdc As IntPtr 'A HDC for the device to render to, if being used to send the output to a device.
Friend hdcTarget As IntPtr 'An HDC for the target device to format for.
Friend rc As RECT 'The area within the rcPage rectangle to render to. Units are measured in twips (Ack!).
Friend rcPage As RECT 'The entire area of a page on the rendering device. Units are measured in twips.
Friend chrg As CHARRANGE 'The range of characters to format.
End Structure

As we can see, the devil is in the details. This simple-looking structure requires that we provide it with a
Device Context for the device we want to render to, plus a couple of RECT structures and a
CHARRANGE structure.
 Page 733

Normally I would use a .NET Rectangle in place of a Win32 RECT structure (and a modified one at
that!), but because of the way we are using it, it will simply be much easier to just define RECT, because
a Rectangle structure’s Right and Bottom properties are read-only, so we would otherwise have to
assign these values instead to the Rectangle’s Width and Height properties, which does not settle well
with me, because even though it would in fact work correctly when we passed the Rectangle to a
Pinvoke, if we were to look at the Right and Bottom properties, they would appear to be reporting too-
large values, even though the core integers stored in its footprint are actually correct. To me, that would
invite bugs into later code enhancements.
<StructLayout(LayoutKind.Sequential)>
Friend Structure RECT
Friend left As Int32 'TRIVIA NOTE: I never understood why the VB6 Upgrade wizard changed a couple values to Left_Renamed
Friend top As Int32 'and Right_Renamed. Because they would have worked just fine after upgrade if they left them alone.
Friend right As Int32 'The upgrade notes said they would conflict with a Form, but this is not true, and would only be
Friend bottom As Int32 'true if they were declared in an enumeration, where VB6 could specify members without qualifying
End Structure 'them with the assigned Enume name, which I thought was a really poor idea on Microsoft's part.

This structure will be used to provide the page’s boundaries ( PrintPageEventArgs.PageBounds) and the
printable region on the page (PrintPageEventArgs.MarginBounds).
The CHARRANGE structure is simpler, providing the Start and End positions in the document for
printing:
' Specifies a range of characters in a rich edit control.
' If the cpMin and cpMax members are equal, the range is empty. The range includes everything if cpMin is 0 and cpMax is –1.
<StructLayout(LayoutKind.Sequential)>
Friend Structure CHARRANGE
Friend cpMin As Int32 'Character position index immediately preceding the first character in the range.
Friend cpMax As Int32 'Character position immediately following the last character in the range.
End Structure

The SendMessage Pinvoke and the EM_FORMATRANGE message are declared like this:
' Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified
' window and does not return until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByVal lParam As IntPtr) As Int32
Private Declare Function SendMessage Lib "user32.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByRef lParam As FORMATRANGE) As Int32
Private Const WM_USER As Int32 = &H400&
Private Const EM_FORMATRANGE As Int32 = WM_USER + 57 'Formats a range of text in a rich edit control for a specific device.

As we can see, we provided two different versions of the SendMessage Pinvoke. The first specifies a
lParam as an IntPtr, used to pass a value of IntPtr.Zero, which will be used to reset the affected
RichTextBox and cause it to again render to its own display interface, and the other specified an lParam
as our above FORMATRANGE structure. Notice further that it is passed ByRef. This simple use of
overloads allows us to bypass any need to copy the FORMATRANGE structure to an allocated neutral
memory location, and afterward de-allocate it (I be lovin’ overloads when they cut out a TON of code).
All that is left to do is to render it.
Within our PrintRichText class, which inherits from PrintDocument, we must import the
System.Drawing, System.Drawing.Printing, and System.Runtime.InteropServices Namespaces and
then set aside the following common fields (variables):
Protected _prtRTB As RichTextBox 'RichText Control.
Protected _prtIndex As Integer = 0 'index into text where we are printing from.
Protected _prtShowMargins As Boolean = False 'show margin indicators on page.

Here, we store the RichTextBox control we are working with, the index into the text of the RichTextBox
where the print process maintains an index to the start of each page, and a flag that will allow the printer
interface to display margin markers. We service these using the following properties:
 Page 734

'*********************************************************************************
' Property : RichTextControl
' Purpose : Get/Set the RichText Control that we want to print.
'*********************************************************************************
Friend Property RichTextControl As RichTextBox
Get
Return Me._prtRTB
End Get
Set(value As RichTextBox)
Me._prtRTB = value
End Set
End Property

'*********************************************************************************
' Property : ShowMarginTags
' Purpose : Get/Set the Show Margin Tags on Page.
'*********************************************************************************
Friend Property ShowMarginTags As Boolean
Get
Return Me._prtShowMargins
End Get
Set(value As Boolean)
Me._prtShowMargins = value
End Set
End Property

As with the PrintText class, we do not expose the _prtIndex field to the user. It is employed exclusively
by the class.
The BeginPrint, EndPrint, and PrintPage event duties needed by the PrintDocument object as
contained within the PrintRichText class by using the ON-versions of these events, just as they were
within the PrintText class:
'*******************************************************************************
' Method : OnBeginPrint
' Purpose : Override the default OnBeginPrint method of the PrintDocument.
'*******************************************************************************
Protected Overrides Sub OnBeginPrint(e As PrintEventArgs)
MyBase.OnBeginPrint(e) 'do base class duties. REQUIRED.
If Me._prtRTB Is Nothing OrElse
Me._prtRTB.IsDisposed Then 'if no RichText to print, then cancel.
e.Cancel = True
Else
Me._prtIndex = 0 'init index to start of text.
End If
End Sub

'*******************************************************************************
' Method Name : OnEndPrint
' Purpose : Finished Printing.
'*******************************************************************************
Protected Overrides Sub OnEndPrint(e As PrintEventArgs)
MyBase.OnEndPrint(e) 'perform base class duties. REQUIRED.
FormatRtbRangeDone() 'release rendering to our PrintRichText object.
End Sub

'*******************************************************************************
' Method : OnPrintPage
' Purpose : Override the default OnPrintPage method of the PrintDocument.
'*******************************************************************************
Protected Overrides Sub OnPrintPage(ByVal e As PrintPageEventArgs)
MyBase.OnPrintPage(e) 'do base class duties. REQUIRED.
If ShowMarginTags Then 'if we should show margin markers...
Dim lPage, tPage, rPage, bPage As Int32
With e.PageBounds 'get page bounds.
lPage = .Left
tPage = .Top
rPage = .Right
bPage = .Bottom
End With

Dim lMargin, rMargin, tMargin, bMargin As Int32


With e.MarginBounds 'get margin sizes.
lMargin = .Left
tMargin = .Top
rMargin = rPage - .Right
bMargin = bPage - .Bottom
End With

e.Graphics.DrawLine(Pens.Blue, lPage + lMargin \ 2, tPage + tMargin, lPage + lMargin, tPage + tMargin) 'top-left.
e.Graphics.DrawLine(Pens.Blue, lPage + lMargin, tPage + tMargin \ 2, lPage + lMargin, tPage + tMargin)
 Page 735

e.Graphics.DrawLine(Pens.Blue, rPage - rMargin, tPage + tMargin, rPage - rMargin \ 2, tPage + tMargin) 'top-right.
e.Graphics.DrawLine(Pens.Blue, rPage - rMargin, tPage + tMargin \ 2, rPage - rMargin, tPage + tMargin)

e.Graphics.DrawLine(Pens.Blue, lPage + lMargin \ 2, bPage - bMargin, lPage + lMargin, bPage - bMargin) 'bottom-left.
e.Graphics.DrawLine(Pens.Blue, lPage + lMargin, bPage - bMargin, lPage + lMargin, bPage - bMargin \ 2)

e.Graphics.DrawLine(Pens.Blue, rPage - rMargin, bPage - tMargin, rPage - rMargin \ 2, bPage - bMargin) 'bottom-right.
e.Graphics.DrawLine(Pens.Blue, rPage - rMargin, bPage - bMargin, rPage - rMargin, bPage - bMargin \ 2)
End If
'---------------------------------------------------------------------------
' make the RichTextBoxEx calculate and render as much text as will fit on the page
' and remember the last character printed for the beginning of the next page.
Me._prtIndex = Me.FormatRtbRange(False, e, Me._prtIndex, Me._prtRTB.TextLength)
e.HasMorePages = Me._prtIndex < Me._prtRTB.TextLength 'check if there are more pages to print.
End Sub

Notice that the OnPrintPage event would actually contain only 3 lines of code were it not for displaying
the optional margin tags (set by Me.prtDoc.ShowMarginTags = True).
The only thing left is to define the support methods FormatRtbRangeDone and FormatRtbRange:
'*******************************************************************************
' Method : FormatRtbRangeDone
' Purpose : Free cached data from rich edit control after printing.
'*******************************************************************************
Private Sub FormatRtbRangeDone()
SendMessage(Me._prtRTB.Handle, EM_FORMATRANGE, 0, IntPtr.Zero)
End Sub

'*******************************************************************************
' Method : FormatRtbRange
' Purpose : Calculate or render the contents of our RichTextBox for printing.
'
' Parameter "measureOnly": If True, only the calculation is performed, otherwise the text is rendered as well.
' Parameter "e" : The PrintPageEventArgs object from the PrintPage event.
' Parameter "charFrom" : Index of first character to be printed.
' Parameter "charTo" : Index of last character to be printed.
' Return value : (Index of last character that fitted on the page) + 1.
'*******************************************************************************
Private Function FormatRtbRange(ByVal measureOnly As Boolean,
ByVal e As PrintPageEventArgs,
ByVal charFrom As Int32,
ByVal charTo As Int32) As Int32

Dim charRange As CHARRANGE 'Specify which characters to print.


With charRange
.cpMin = charFrom 'Character position index immediately preceding the first character in the range.
.cpMax = charTo 'Character position immediately following the last character in the range.
End With

Dim rectInsidePageMargins As RECT 'Specify the area inside page margins in twips.
With rectInsidePageMargins
.top = hInchToTwips(e.MarginBounds.Top) 'convert 1/100-inch units to twips.
.bottom = hInchToTwips(e.MarginBounds.Bottom)
.left = hInchToTwips(e.MarginBounds.Left)
.right = hInchToTwips(e.MarginBounds.Right)
End With

Dim rectPageArea As RECT 'Specify the page area in twips.


With rectPageArea
.top = hInchToTwips(e.PageBounds.Top)
.bottom = hInchToTwips(e.PageBounds.Bottom)
.left = hInchToTwips(e.PageBounds.Left)
.right = hInchToTwips(e.PageBounds.Right)
End With

Dim hdc As IntPtr = e.Graphics.GetHdc() 'Get device context of output device.

Dim fmtRange As FORMATRANGE 'Fill in the FORMATRANGE structure.


With fmtRange
.hdc = hdc 'A HDC for the device to render to, if being used to send the output to a device.
.hdcTarget = hdc 'An HDC for the target device to format for.
.rc = rectInsidePageMargins 'The area within the rcPage rectangle to render to. Units are measured in twips.
.rcPage = rectPageArea 'The entire area of a page on the rendering device. Units are measured in twips.
.chrg = charRange 'The range of characters to format.
End With
Dim wParam As Int32 = CInt(Not measureOnly) And 1 'Non-Zero wParam means render, Zero means measure.
 Page 736

Dim result As Int32 = SendMessage(Me._prtRTB.Handle, EM_FORMATRANGE,


wParam, fmtRange) 'Send Win32 message and get start of next page.
e.Graphics.ReleaseHdc(hdc) 'and release the device context.
Return result 'return start of next page.
End Function

'*******************************************************************************
' Method : hInchToTwips
' Purpose : Convert between 1/100 inch (unit used by the .NET framework)
' : and twips (1/1440 inch, used by Win32 API calls).
'
' Parameter "hInch": Value in /100 inch units.
' Return value : Value in twips.
'*******************************************************************************
Private Function hInchToTwips(ByVal hInch As Integer) As Int32
Return Convert.ToInt32(hInch * 14.4)
End Function

The FormatRangeDone method simply disconnects the RichTextBox rendering from our PrintRichText
class object and returns all rendering control back to the RichTextBox itself.
Notice also that the FormatRtbRange method has its own support method, hInchToTwips, which is used
to convert the 1/100-inch measurement units used by a PrintDocument’s PrintPageEventArgs.Graphics
interface to twips (1/1440-inch units) used by printing devices out in the Win32 universe (and by VB6).
As we can see, we first load up a CHARRANGE structure with the start and end text position indexes of
the document. We then format one RECT structure with the page range (dimensions), rendering all
members from 1/100-inch units to twips. We next do likewise for the margin bounds, which defines the
printable area on the page. After that, we have all we need to fill our FORMATRANGE structure.
Be sure to notice that when we grab the Device Context (Dim hdc As IntPtr = e.Graphics.GetHdc()) of our
PrintDocument object (PrintRichText in this case), that we must be sure to release it when we are done
with it (e.Graphics.ReleaseHdc(hdc)). If we do not release it, our PrintDocument object will become
inoperable and an unhandled exception error might result, or worse, just hang until we stop it. Even so,
this should not be considered a dangerous or even a scary thing; just good manners, because only one
thing can control a Device Context at a time, so we must actually just give control of it back to the
system, which we had borrowed it from, by releasing it once we are done using it.
When we send the EM_FORMATRANGE message along with the FORMATRANGE structure, the
method is nice enough to return an index to the start of the next page. We can use it on subsequent
invocations to the OnPrintPage event, and use it to determine if we are finished printing.
What follows is my complete PrintRichText class:
Option Explicit On
Option Strict On
Option Infer Off
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' PrintRichText Class (Inherits from System.Drawing.Printing.PrintDocument
' RichText Print Document Class
' Extrapolated from Martin Müller's January 2003 article on MSDN Magazine,
' "Getting WYSIWYG Print Results from a .NET RichTextBox".
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Imports System.Drawing, System.Drawing.Printing, System.Runtime.InteropServices

Friend Class PrintRichText


Inherits PrintDocument
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Protected _prtRTB As RichTextBox 'RichText Control.
Protected _prtIndex As Integer = 0 'index into text where we are printing from.
Protected _prtShowMargins As Boolean = False 'show margin indicators on page.
'---------------------------------------------------------------------------------
<StructLayout(LayoutKind.Sequential)>
Friend Structure RECT
Friend left As Int32
Friend top As Int32
Friend right As Int32
Friend bottom As Int32
End Structure

'Specifies a range of characters in a rich edit control.


 Page 737

'If the cpMin and cpMax members are equal, the range is empty. The range includes everything if cpMin is 0 and cpMax is –1.
<StructLayout(LayoutKind.Sequential)>
Friend Structure CHARRANGE
Friend cpMin As Int32 'Character position index immediately preceding the first character in the range.
Friend cpMax As Int32 'Character position immediately following the last character in the range.
End Structure

'Information that a rich edit control uses to format its output for a particular device.
'This structure is used with the EM_FORMATRANGE message.
<StructLayout(LayoutKind.Sequential)>
Friend Structure FORMATRANGE
Friend hdc As IntPtr 'A HDC for the device to render to, if being used to send the output to a device.
Friend hdcTarget As IntPtr 'An HDC for the target device to format for.
Friend rc As RECT 'The area within the rcPage rectangle to render to. Units are measured in twips.
Friend rcPage As RECT 'The entire area of a page on the rendering device. Units are measured in twips.
Friend chrg As CHARRANGE 'The range of characters to format.
End Structure

'Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified
'window and does not return until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByVal lParam As IntPtr) As Int32
Private Declare Function SendMessage Lib "user32.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByRef lParam As FORMATRANGE) As Int32
Private Const WM_USER As Int32 = &H400&
Private Const EM_FORMATRANGE As Int32 = WM_USER + 57 'Format range of text in rich edit control for specific device.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
' Property : RichTextControl
' Purpose : Get/Set the RichText Control that we want to print.
'*********************************************************************************
Friend Property RichTextControl As RichTextBox
Get
Return Me._prtRTB
End Get
Set(value As RichTextBox)
Me._prtRTB = value
End Set
End Property
'*********************************************************************************
' Property : ShowMarginTags
' Purpose : Get/Set the Show Margin Tags on Page.
'*********************************************************************************
Friend Property ShowMarginTags As Boolean
Get
Return Me._prtShowMargins
End Get
Set(value As Boolean)
Me._prtShowMargins = value
End Set
End Property
'*******************************************************************************
' Method : OnBeginPrint
' Purpose : Begin Print Job. Override the default OnBeginPrint method of the PrintDocument.
'*******************************************************************************
Protected Overrides Sub OnBeginPrint(e As PrintEventArgs)
MyBase.OnBeginPrint(e) 'do base class duties. REQUIRED.
If Me._prtRTB Is Nothing OrElse
Me._prtRTB.IsDisposed Then 'if no RichText to print, then cancel.
e.Cancel = True
Else
Me._prtIndex = 0 'init index to start of text.
End If
End Sub
'*******************************************************************************
' Method Name : OnEndPrint
' Purpose : Finished Printing. Override the default OnEndPrint method of the PrintDocument.
'*******************************************************************************
Protected Overrides Sub OnEndPrint(e As PrintEventArgs)
MyBase.OnEndPrint(e) 'perform base class duties. REQUIRED.
FormatRtbRangeDone() 'release rendering to our PrintRichText object.
End Sub
'*******************************************************************************
' Method : OnPrintPage
' Purpose : Print a Page. Override the default OnPrintPage method of the PrintDocument.
'*******************************************************************************
Protected Overrides Sub OnPrintPage(ByVal e As PrintPageEventArgs)
MyBase.OnPrintPage(e) 'do base class duties. REQUIRED.
 Page 738

If ShowMarginTags Then 'if we should show margin markers...


Dim lPage, tPage, rPage, bPage As Int32
With e.PageBounds 'get page bounds.
lPage = .Left
tPage = .Top
rPage = .Right
bPage = .Bottom
End With
Dim lMargin, rMargin, tMargin, bMargin As Int32
With e.MarginBounds 'get margin sizes.
lMargin = .Left
tMargin = .Top
rMargin = rPage - .Right
bMargin = bPage - .Bottom
End With
e.Graphics.DrawLine(Pens.Blue, lPage + lMargin \ 2, tPage + tMargin, lPage + lMargin, tPage + tMargin) 'top-left.
e.Graphics.DrawLine(Pens.Blue, lPage + lMargin, tPage + tMargin \ 2, lPage + lMargin, tPage + tMargin)
e.Graphics.DrawLine(Pens.Blue, rPage - rMargin, tPage + tMargin, rPage - rMargin \ 2, tPage + tMargin) 'top-right.
e.Graphics.DrawLine(Pens.Blue, rPage - rMargin, tPage + tMargin \ 2, rPage - rMargin, tPage + tMargin)
e.Graphics.DrawLine(Pens.Blue, lPage + lMargin \ 2, bPage - bMargin, lPage + lMargin, bPage - bMargin) 'bottom-left.
e.Graphics.DrawLine(Pens.Blue, lPage + lMargin, bPage - bMargin, lPage + lMargin, bPage - bMargin \ 2)
e.Graphics.DrawLine(Pens.Blue, rPage - rMargin, bPage - tMargin, rPage - rMargin \ 2, bPage - bMargin) 'bottom-right.
e.Graphics.DrawLine(Pens.Blue, rPage - rMargin, bPage - bMargin, rPage - rMargin, bPage - bMargin \ 2)
End If
'---------------------------------------------------------------------------
'make the RichTextBoxEx calculate and render as much text as will fit on the page
'and remember the last character printed for the beginning of the next page
Me._prtIndex = Me.FormatRtbRange(False, e, Me._prtIndex, Me._prtRTB.TextLength)
e.HasMorePages = Me._prtIndex < Me._prtRTB.TextLength 'check if there are more pages to print.
End Sub
'*******************************************************************************
' Method : FormatRtbRangeDone
' Purpose : Free cached data from rich edit control after printing.
'*******************************************************************************
Private Sub FormatRtbRangeDone()
SendMessage(Me._prtRTB.Handle, EM_FORMATRANGE, 0, IntPtr.Zero)
End Sub
'*******************************************************************************
' Method : FormatRtbRange
' Purpose : Calculate or render the contents of our RichTextBox for printing.
'
' Parameter "measureOnly": If True, only the calculation is performed, otherwise the text is rendered as well.
' Parameter "e" : The PrintPageEventArgs object from the PrintPage event.
' Parameter "charFrom" : Index of first character to be printed.
' Parameter "charTo" : Index of last character to be printed.
' Return value : (Index of last character that fitted on the page) + 1.
'*******************************************************************************
Private Function FormatRtbRange(ByVal measureOnly As Boolean,
ByVal e As PrintPageEventArgs,
ByVal charFrom As Int32,
ByVal charTo As Int32) As Int32
Dim charRange As CHARRANGE 'Specify which characters to print.
With charRange
.cpMin = charFrom 'Character position index immediately preceding the first character in the range.
.cpMax = charTo 'Character position immediately following the last character in the range.
End With
Dim rectInsidePageMargins As RECT 'Specify the area inside page margins in twips.
With rectInsidePageMargins
.top = hInchToTwips(e.MarginBounds.Top) 'convert 1/100-inch units to twips.
.bottom = hInchToTwips(e.MarginBounds.Bottom)
.left = hInchToTwips(e.MarginBounds.Left)
.right = hInchToTwips(e.MarginBounds.Right)
End With
Dim rectPageArea As RECT 'Specify the page area in twips.
With rectPageArea
.top = hInchToTwips(e.PageBounds.Top)
.bottom = hInchToTwips(e.PageBounds.Bottom)
.left = hInchToTwips(e.PageBounds.Left)
.right = hInchToTwips(e.PageBounds.Right)
End With
Dim hdc As IntPtr = e.Graphics.GetHdc() 'Get device context of output device.
Dim fmtRange As FORMATRANGE 'Fill in the FORMATRANGE structure.
With fmtRange
.hdc = hdc 'A HDC for the device to render to, if being used to send the output to a device.
.hdcTarget = hdc 'An HDC for the target device to format for.
.rc = rectInsidePageMargins 'The area within the rcPage rectangle to render to. Units are measured in twips.
.rcPage = rectPageArea 'The entire area of a page on the rendering device. Units are measured in twips.
.chrg = charRange 'The range of characters to format.
 Page 739

End With
Dim wParam As Int32 = CInt(Not measureOnly) And 1 'Non-0 wParam means render, 0 means measure.
Dim result As Int32 = SendMessage(Me._prtRTB.Handle, EM_FORMATRANGE,
wParam, fmtRange) 'Send Win32 message and get start of next page.
e.Graphics.ReleaseHdc(hdc) 'and release the device context.
Return result 'return start of next page.
End Function
'*******************************************************************************
' Method : hInchToTwips
' Purpose : Convert between 1/100 inch (unit used by the .NET framework)
' : and twips (1/1440 inch, used by Win32 API calls).
'
' Parameter "hInch": Value in 1/100 inch units.
' Return value : Value in twips.
'*******************************************************************************
Private Function hInchToTwips(ByVal hInch As Integer) As Int32
Return Convert.ToInt32(hInch * 14.4)
End Function
End Class

Assuming we have a form named frmPrintTest, with a RichTextBox control named rtbData, and 3
buttons named btnPrintText, btnPageSetup, and btnPrintPreview, labeled however we desire, consider
the following form class code (paste whatever Rich Text we want into the RichTextBox):
Option Explicit On
Option Strict On
Option Infer Off

Imports System.Drawing.Printing

Public Class frmPrintTest


'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private WithEvents prtDoc As New PrintRichText 'printer I/O interface.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method Name : Form_Load
' Purpose : Sample method to print text to the default document.
'*******************************************************************************
Private Sub Form_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'Fill Form TextBox with some data
Me.rtbData.Text = "Four score and seven years ago our fathers brought forth on this continent," &
" a new nation, conceived in Liberty, and dedicated to the proposition that" &
" all men are created equal." & vbCrLf & vbCrLf &
"Now we are engaged in a great civil war, testing whether that nation, or any" &
" nation so conceived and so dedicated, can long endure. We are met on a great" &
" battle-field of that war. We have come to dedicate a portion of that field," &
" as a final resting place for those who here gave their lives that that nation" &
" might live. It is altogether fitting and proper that we should do this." & vbCrLf & vbCrLf &
"But, in a larger sense, we can not dedicate – we can not consecrate – we can" &
" not hallow – this ground. The brave men, living and dead, who struggled here," &
" have consecrated it, far above our poor power to add or detract. The world will" &
" little note, nor long remember what we say here, but it can never forget what" &
" they did here. It is for us the living, rather, to be dedicated here to the" &
" unfinished work which they who fought here have thus far so nobly advanced." &
" It is rather for us to be here dedicated to the great task remaining before us" &
" – that from these honored dead we take increased devotion to that cause for" &
" which they gave the last full measure of devotion – that we here highly resolve" &
" that these dead shall not have died in vain – that this nation, under God, shall" &
" have a new birth of freedom – and that government of the people, by the people," &
" for the people, shall not perish from the earth." & vbCrLf
Me.prtDoc.RichTextControl = Me.rtbData 'assign our Rich Text Control to our PrintDocument object
End Sub

'*******************************************************************************
' Method Name : btnPrintText_Click
' Purpose : Print the document by calling up the PrintDialog.
'*******************************************************************************
Private Sub btnPrintText_Click(sender As Object, e As EventArgs) Handles btnPrintText.Click
With New PrintDialog 'instantiate a new PrintDialog instance.
.Document = Me.prtDoc 'PrintDocument object used to collect print settings.
.AllowSomePages = True 'allow user to select pages to print.
.UseEXDialog = True 'show enhanced Printer dialog (False = simple dialog).
'Hitting the Print button returns a result of DialogResult.OK, otherwise DialogResult.Cancel.
If .ShowDialog(Me) = DialogResult.OK Then 'show dialog, change prtDoc settings if user hits Print (OK).
Me.prtDoc.Print() 'print the document.
End If
.Dispose() 'dispose of PrintDialog resources.
End With
End Sub

'*******************************************************************************
' Method Name : btnPageSetup_Click
 Page 740

' Purpose : Set up a printed page format.


'*******************************************************************************
Private Sub btnPageSetup_Click(sender As Object, e As EventArgs) Handles btnPageSetup.Click
' the PageSetupDialog control enables users to change page-related
' print settings, including margins and paper orientation.
With New PageSetupDialog
.AllowMargins = True
' allow setting the orientation section of the dialog box (landscape versus portrait).
.AllowOrientation = True
' allow setting the paper section of the dialog box (paper size and paper source).
.AllowPaper = True
' allow selecting the Printer from this dialog (this shows only on versions of Windows prior to Vista).
.AllowPrinter = True
' During margin settings, if displayed in millimeters, it will be converted to and from hundredths of an inch.
.EnableMetric = True
' Show the network as needed in the printer dialog (shown if local network available).
.ShowNetwork = My.Computer.Network.IsAvailable()
' PrintDocument object to use to format data.
.Document = Me.prtDoc 'provide a reference to our PrintDocument object.
.ShowDialog() 'show the dialog.
.Dispose() 'dispose of dialog resources.
End With
End Sub

'*******************************************************************************
' Method Name : btnPrintPreview_Click
' Purpose : Do a print preview.
'*******************************************************************************
Private Sub btnPrintPreview_Click(sender As Object, e As EventArgs) Handles btnPrintPreview.Click
With New PrintPreviewDialog 'process print preview.
.Document = Me.prtDoc 'PrintDocument object to use.
.UseAntiAlias = True 'smooth font display.
Me.prtDoc.ShowMarginTags = True 'enable displaying margin corner tags.
.ShowDialog(Me) 'show print preview. OK = printed document.
Me.prtDoc.ShowMarginTags = False 'disable displaying margin corner tags.
.Dispose() 'dispose of dialog resources.
End With
End Sub

End Class

We might notice in many ways that


printing from a Rich Text Control is
simpler and easier than rendering plain
text from a TextBox. Go figure. Any
complexity comes for formatting the
text and images in the control, which we
will have to still supply if we are writing
a Rich Text editor.
Notice further that any extensions we
apply to our RichTextBox controls, such
as full text justification, will still be
supported when rendering to the printer
or to a print preview.
 Page 741

Black Book Tip # 58


Enhancing ProgressBar Marquees and Adding Bling to ProgressBars Made E-Z.
If we want to display a ProgressBar Marquee that loops continuously until our
application finishes doing whatever it is taking an indeterminate time doing, then
make sure our application properties has its Enable XP Visual Styles option
checked (default) and that we also set our ProgressBar’s Style property to
Marquee. By default, its MarqueeAnimationSpeed property has its value set to 100ms, meaning that its
progress is updated every 1/10th of a second. This is fine, but a shorter period often looks much better.
Though it does the job, it looks a bit lame with a plain green block sliding across it. It would look much
slicker if we could add our own image to it, but until then, it is easy enough to draw our own progress
marquees using a PictureBox and Timer. We can capture a lot of slick-looking progress bars. For example, I
had captured this progress image from an application I was installing:
This image is dimensioned 110x22 pixels. We could add it to our application resources and then paint it
to a PictureBox’s Image object at a starting offset of -110 (starting fully left of the PictureBox), and
using a timer set anywhere from 15ms to 100ms, keep bumping its left position on every tick event by 4
pixels or so until we pass the right edge of the PictureBox with the left edge of our progress image. At
that point we can reset its starting index back to -110 and keep rolling it rightward into and out of view.
Of course, to do this we must also make sure that our PictureBox has an image upon which we can
paint! By default, a new PictureBox has its Image property set to Nothing. Trying to paint to that will
only result in an exception error. However, initializing this drawing canvas is also very easy to do. We
could do something like the following to initialize our PictureBox, which I here named picProgress:
With Me.picProgress
.Image = New Bitmap(.Width, .Height) 'create a new image space (this property was set to Nothing by default).
Graphics.FromImage(.Image).Clear(.BackColor) 'paint a blank background on the image using a temp Graphics interface.
End With

This defines a blank field set to the PictureBox BackColor property – SystemColor.Control by default.
We must also maintain a reference and position offsets for the “progress” image we will paint on the
control. Instead of using hard-coded numbers, we should set aside constants to define the number of
pixels to increment the image during each timer tick, and how often the timer should actually “tick”:
Private ProgressImage As Image 'progress image. The image to paint within our Faux Marquee PictureBox.
Private Const ProgressInc As Int32 = 4 'how many pixels to imcrement image rightward.
Private Const ProgressSpeed As Int32 = 20 'how often our timer should tick.

We can load our image, ProgressImage, from our resources, from a file, or define it from a Base64
binary conversion string, taking advantage of the ConvertBase64Image function we have used in
previous Black Book Tips. The following definition is for the image, shown full-size to the right.:
Me.ProgressImage = ConvertBase64ToImage(
"iVBORw0KGgoAAAANSUhEUgAAAG4AAAAWCAIAAACuSD4AAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAABF9JREFUWEftmOtLVEEYxvvfut+IKIiiKAqCoCDwQ1BQFBQUFAUFBUVBQdGau3nL" &
"LCtN0zLNS2qamXfdm7ur6+pePvScM+vs7JyZ98wx+xLbJ33xnDPz67nMOetC9r/ZYGh6NjQ1G56YCePX" &
"kanw8GRkcCLSPx75PhbpGo1i2D4Sbf011/xzrnFormEwVv8jhmFlX9zfG/f1JJ51J558m3/UOY/h3faF" &
"O58XbrUlr39KXm1JXmlexPB8Y+rc+9SZd6myt0un3yydrF/G8Pir5aO16cM16UPV6f1VmX2VGQx3+7O7" &
"/NkdFdltL7JbyrObfDkM1z/PbfTl8DMmmO+syFp/GcjsfWldhWtxhyM1aXbPE6+XcX88paxhCU/E8EJj" &
"6lLTIlaC9WBVN9uSGGKd979aa8bKsf7yngSGVX3x2v44doc9YqfYL4bYe+fvKDj0jllMQAZDUBqdtoiB" &
"20wwlEcZDIbwC0bjDpS4WI1yoIASi5BQ3iZQNhShPOZEGcgAJWBJKDf4cpvLi1H6sxzlwWrrPuyeSpQX" &
"mxYvf8yjxNowFFE+7UpAEBhiRxbKAQvlh6EYQ/nFRtk9GoWwTFF6UKWNEv+HTJVYihLltZYkNoAhUJ5V" &
"oVSocgXldr0qoVn2l+YomSqxHiVKSIFAqVPl2BqixIMrHAa/V6xKjlKnShiTQAklYshUqTP4AdvgBEo8" &
"HSglVWKdD2yUjwVV0gZnqhyadFPlJIkSIjdHybLyRqs7SqhSRmlnpdPgRFaaoITBCVXKBneg7BBUSaFE" &
"7RCqREZgSKtSZ3ARpc7gOlUSKPMGF7ISdyBqh6mS1w6BMq9K2+A6VRoZvFA7U2GGkteOpcphGaWUlQ87" &
"8ih5g4tZaapKg9qRstJVlVKDM5RwD0MJg/OspFF6aHCnKnExoUo8WKlKJ0pshqgdlIbS4FvpBg9k9gTy" &
"KAu1U2ehPFVvPQUHL1GVLCsRO8rDEEMp1o6IEgbXNTiCsYCSNjhTpYzSPldylETtsMOQGmWdhwYnaocZ" &
"nM7KtTL4wMSqshKXaVGuZKXzXMlrR0IpGhyqwVCtSr3BOcq1NbgJSlY7pllJN7hSlcrakVDKWel421Fm" &
"pfNthz4M0edK0eCSKpHvWLmclcUNjp5QovTW4Lx2lKrEg3ntILwJg7vWDnCYq5Kj5FkpqVKZlYQqgZK9" &
"OOqyUtngFErpxREGN0dJG1xSpWhwoISmzFEiQyVVumYlMzhXJasdhhKqZA3uRCm97Ri9OOpQMoMXGvzf" &
"HIaAwGlwJ0qcK1d9GOIGF4/ooip5VnpqcEVW0ihpVfKs5AYXUUIFrig9ZSWOR1AlR8kMjmtNslIyOG9w" &
"nSollODg4VxJqFJ+cSz+yCYaXELpmpVAYGLwv/mcwVUpovSqStOsLH2vZKpUNjj27vltx6lKunYYSulc" &
"iSWaN3jpeyXV4CJKsXZK3ytdVOn64kjXTul7pQtKlpWl75UyysK5Uo9SZ3AR5f/8vTIY+gP6qo66EqhC" &
"lAAAAABJRU5ErkJggg==") 'build ProgressImage from string data
 Page 742

We should save the horizontal position on picProgress.Image where we want to draw ProgressImage to.
We could set aside a field variable for this, but I just stuff it into the Tag property of the PictureBox. We
must also enable our timer, set to an interval of 20 (ProgressSpeed), named tmrProgress:
Me.ProgressImage.Tag = -Me.ProgressImage.Size.Width 'set ProgressImage startup position (note the (-) unary minus).
Me.tmrProgress.Enabled = True 'enable the progress timer.

All we need now is the Tick Event code to support the tmrProgress timer:
Private Sub tmrProgress_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrProgress.Tick
Me.tmrProgress.Enabled = False 'disable the progress timer for now.
Dim g As Graphics = Graphics.FromImage(Me.picProgress.Image) 'get a graphics interface to the image to draw to.
g.Clear(Me.picProgress.BackColor) 'first clear any previous drawing...
Dim Idx As Int32 = CInt(Me.ProgressImage.Tag) 'grab the current X offset for rendering the image.
g.DrawImage(Me.ProgressImage, Idx, 0) 'draw the progress image to the PictureBox at X,Y.
g.Dispose() 'release drawing interface (this locks in the change).
Idx += ProgressInc 'bump the X offset.
If Idx > Me.picProgress.Image.Width Then 'if it exceeds the drawing surface...
Idx = -Me.ProgressImage.Width 'reset the X index back to being fully left of the image.
End If
Me.ProgressImage.Tag = Idx 'Save the updated left index.
Me.picProgress.Refresh() 'update the display of the drawing.
Application.DoEvents() 'Do temporary idle state to let paint events fire.
Me.tmrProgress.Enabled = True 're-enable the progress timer.
End Sub

Here, we first disabled the TmrProgress timer to reset it. We then grab a
graphics interface for the Image object in our PictureBox so we can draw
to it. With it, we first erase any drawing we may have already done on its
Image. Next, we pick up our X offset value from the Tag property of our
Progress PictureBox, draw ProgressImage to the PictureBox Image, and
then bump the X offset. If its new sum exceeds the size of the PictureBox
Image, we reset the index to full left of it. We next refresh it to indicate to
the system message queue it needs to be repainted, so to display its
updated drawing. DoEvents is then invoked, which forces system Paint
updates as it gives our application a short catnap while waiting for any pending repaints to finish their
work. Finally, TmrProgress is reset so it can keep updating the progress marquee.
To demonstrate how easy this all really is, create a new Windows Form project, and add the following to
the code page for the form (I am assuming that the form is named Form1):
Option Explicit On
Option Strict On
Option Infer Off
Public Class Form1
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private ProgressImage As Image 'progress image. The image to paint within our Faux Marquee PictureBox.
Private Const ProgressInc As Int32 = 4 'how many pixels to imcrement image rightward.
Private Const ProgressSpeed As Int32 = 20 'how often our timer should tick.
Private WithEvents tmrProgress As New Timer 'timer to increment ProgressImage in picProgress.
Private WithEvents picProgress As New PictureBox 'PictureBox to represent the continuous progress bar.
Private WithEvents btnStartStop As New Button 'button to start and stop the example.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
' Method : Form_Load
' Purpose : Initialize Faux ProgressBar emulation Demo.
'*********************************************************************************
Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
Me.ProgressImage = InitializeImage() 'grab progress image.
With Me.picProgress
.Parent = Me 'set parent. This also sets control visibility to True.
.Location = New Point(12, 12) 'position it at the top-left of the form.
.Width = Me.ClientRectangle.Width - .Left * 2 'size it across the form and keep left and right margins uniform.
.Height = ProgressImage.Size.Height 'set the picture height to that of the progress image.
.BorderStyle = BorderStyle.FixedSingle 'make its border more defined (not flat or borderless).
.Anchor = AnchorStyles.Top Or AnchorStyles.Left 'anchor it to the top-left corner of the form.
.Image = New Bitmap(.Width, .Height) 'create image space (this property was set to Nothing by default).
Graphics.FromImage(.Image).Clear(.BackColor) 'paint blank background on image using a temp Graphics interface.
End With
With btnStartStop 'button to start and stop the sample.
.Parent = Me 'set parent. This also sets control visibility to True.
.Location = New Point(12, Me.picProgress.Top + Me.picProgress.Height + 12)
.Size = New Size(100, 24) 'make big enough to accommodate text with default height of 24.
 Page 743

.Text = "Start/Stop Test" 'give it some text.


End With
Me.tmrProgress.Interval = ProgressSpeed 'set timer interval to 15 ms.
End Sub

'*********************************************************************************
' Method : btnStartStop_Click
' Purpose : Start and stop the custom Progress bar.
'*********************************************************************************
Private Sub btnStartStop_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnStartStop.Click
If Me.tmrProgress.Enabled Then 'if the timer is currently running...
Me.tmrProgress.Enabled = False 'turn off timer.
With Me.picProgress
Graphics.FromImage(.Image).Clear(.BackColor) 'initialize its image to blank.
.Refresh() 'update its display.
End With
Else
Me.ProgressImage.Tag = -Me.ProgressImage.Size.Width 'set ProgressImage startup position.
Me.tmrProgress.Enabled = True 'enable the timer.
End If
End Sub

'*********************************************************************************
' Method : tmrProgress_Tick
' Purpose : Update the Continuous ProgressBar.
'*********************************************************************************
Private Sub tmrProgress_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrProgress.Tick
Me.tmrProgress.Enabled = False 'disable the progress timer for now.
Dim g As Graphics = Graphics.FromImage(Me.picProgress.Image) 'get a graphics interface to the image to draw to.
g.Clear(Me.picProgress.BackColor) 'first clear any previous drawing...
Dim Idx As Int32 = CInt(Me.ProgressImage.Tag) 'grab the current X offset for rendering the image.
g.DrawImage(Me.ProgressImage, Idx, 0) 'draw the progress image to the PictureBox.
g.Dispose() 'release drawing interface (this locks in the change).
Idx += ProgressInc 'bump the X offset.
If Idx > Me.picProgress.Image.Width Then 'if it exceeds the drawing surface...
Idx = -Me.ProgressImage.Width 'reset X back to the left of the image.
End If
Me.ProgressImage.Tag = Idx 'save the updated left index.
Me.picProgress.Refresh() 'update the display of the drawing.
Application.DoEvents() 'Do temporary idle state to let paint events fire.
Me.tmrProgress.Enabled = True 're-enable the progress timer.
End Sub

'*********************************************************************************
' Method : InitializeImage
'*********************************************************************************
Private Function InitializeImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAAG4AAAAWCAIAAACuSD4AAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAABF9JREFUWEftmOtLVEEYxvvfut+IKIiiKAqCoCDwQ1BQFBQUFAUFBUVBQdGau3nL" &
"LCtN0zLNS2qamXfdm7ur6+pePvScM+vs7JyZ98wx+xLbJ33xnDPz67nMOetC9r/ZYGh6NjQ1G56YCePX" &
"kanw8GRkcCLSPx75PhbpGo1i2D4Sbf011/xzrnFormEwVv8jhmFlX9zfG/f1JJ51J558m3/UOY/h3faF" &
"O58XbrUlr39KXm1JXmlexPB8Y+rc+9SZd6myt0un3yydrF/G8Pir5aO16cM16UPV6f1VmX2VGQx3+7O7" &
"/NkdFdltL7JbyrObfDkM1z/PbfTl8DMmmO+syFp/GcjsfWldhWtxhyM1aXbPE6+XcX88paxhCU/E8EJj" &
"6lLTIlaC9WBVN9uSGGKd979aa8bKsf7yngSGVX3x2v44doc9YqfYL4bYe+fvKDj0jllMQAZDUBqdtoiB" &
"20wwlEcZDIbwC0bjDpS4WI1yoIASi5BQ3iZQNhShPOZEGcgAJWBJKDf4cpvLi1H6sxzlwWrrPuyeSpQX" &
"mxYvf8yjxNowFFE+7UpAEBhiRxbKAQvlh6EYQ/nFRtk9GoWwTFF6UKWNEv+HTJVYihLltZYkNoAhUJ5V" &
"oVSocgXldr0qoVn2l+YomSqxHiVKSIFAqVPl2BqixIMrHAa/V6xKjlKnShiTQAklYshUqTP4AdvgBEo8" &
"HSglVWKdD2yUjwVV0gZnqhyadFPlJIkSIjdHybLyRqs7SqhSRmlnpdPgRFaaoITBCVXKBneg7BBUSaFE" &
"7RCqREZgSKtSZ3ARpc7gOlUSKPMGF7ISdyBqh6mS1w6BMq9K2+A6VRoZvFA7U2GGkteOpcphGaWUlQ87" &
"8ih5g4tZaapKg9qRstJVlVKDM5RwD0MJg/OspFF6aHCnKnExoUo8WKlKJ0pshqgdlIbS4FvpBg9k9gTy" &
"KAu1U2ehPFVvPQUHL1GVLCsRO8rDEEMp1o6IEgbXNTiCsYCSNjhTpYzSPldylETtsMOQGmWdhwYnaocZ" &
"nM7KtTL4wMSqshKXaVGuZKXzXMlrR0IpGhyqwVCtSr3BOcq1NbgJSlY7pllJN7hSlcrakVDKWel421Fm" &
"pfNthz4M0edK0eCSKpHvWLmclcUNjp5QovTW4Lx2lKrEg3ntILwJg7vWDnCYq5Kj5FkpqVKZlYQqgZK9" &
"OOqyUtngFErpxREGN0dJG1xSpWhwoISmzFEiQyVVumYlMzhXJasdhhKqZA3uRCm97Ri9OOpQMoMXGvzf" &
"HIaAwGlwJ0qcK1d9GOIGF4/ooip5VnpqcEVW0ihpVfKs5AYXUUIFrig9ZSWOR1AlR8kMjmtNslIyOG9w" &
"nSollODg4VxJqFJ+cSz+yCYaXELpmpVAYGLwv/mcwVUpovSqStOsLH2vZKpUNjj27vltx6lKunYYSulc" &
"iSWaN3jpeyXV4CJKsXZK3ytdVOn64kjXTul7pQtKlpWl75UyysK5Uo9SZ3AR5f/8vTIY+gP6qo66EqhC" &
"lAAAAABJRU5ErkJggg=="
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

End Class

We should also experiment using a different progress image that we have either captured or created.
 Page 744

Notice that I created my PictureBox progress bar, my button to start and stop the demonstration, and my
timer all in-code, as I have demonstrated elsewhere in this document. In our own application, we may
want to just drop controls on our form and deal with them more conveniently that way, but creating them
in-code sure makes demo code easier, because we, as a user, do not have to first drop any controls on the
form or rename them to get the demo code to work. It will do all that for us.

Enhancing a Custom ProgressBar Marquee by Tiling It


But enhancements do not have to end with a
custom Image for our Continuous ProgressBar.
We can tile the image to fill the PictureBox, and
animate it to roll endlessly across it. That is really
easy to do. Consider these changes to the
tmeProgress_Tick event code, which will tile the
ProgressImage across the whole PictureBox, no
matter where the start index is located:
'*********************************************************************************
' Method : tmrProgress_Tick
' Purpose : Update for a Tiled Continuous ProgressBar.
'*********************************************************************************
Private Sub tmrProgress_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrProgress.Tick
Me.tmrProgress.Enabled = False 'disable the progress timer for now.
Dim g As Graphics = Graphics.FromImage(Me.picProgress.Image) 'get a graphics interface to the image to draw to.
Dim Idx As Int32 = CInt(Me.ProgressImage.Tag) 'grab the current X offset for rendering the image.
Dim ImageW As Int32 = Me.picProgress.Image.Width 'save PictureBox width.
Dim PgrssW As Int32 = Me.ProgressImage.Width 'save image width.
Dim Idy As Int32 = Idx 'set secondary index to start point.
Do While Idy < ImageW 'while we have not exceeded the display area...
g.DrawImage(Me.ProgressImage, Idy, 0) 'draw the image to the PictureBox.
Idy += Me.ProgressImage.Width 'tile right.
Loop
Idy = Idx 'reset secondary index to start point.
Do While Idy > 0 'while it is greater than zero...
Idy -= PgrssW 'tile left.
g.DrawImage(Me.ProgressImage, Idy, 0) 'draw the image to the PictureBox.
Loop
g.Dispose() 'release drawing interface (this locks in the change).
Idx += ProgressInc 'bump the X offset.
If Idx > ImageW Then 'if it exceeds the drawing surface...
Idx = Idy 'reset X origin to <= 0.
End If
Me.ProgressImage.Tag = Idx 'save the updated left index.
Me.picProgress.Refresh() 'update the display of the drawing.
Application.DoEvents() 'let other things happen.
Me.tmrProgress.Enabled = True 're-enable the progress timer.
End Sub

Notice first that we do not initially clear the drawing surface. This is because we are going to completely
repaint it. We next set up a secondary index, Idy, that will be updated so we can tile the image. While
Idy is less than the width of the PictureBox image, we will draw a copy of the progress image one after
the other until the image rightward of the Idx index can be filled. We then reset the secondary index and
draw leftward until Idy is less than zero. This will mean that we have now drawn to the entire drawing
surface. Once Idx exceeds the drawing surface, it is reset to Idy, which is less than or equal to zero.
Consider the above image definition by replacing it with the following in-code definitions for a Green
Aero Glare image, suitable for a much cooler Tiled ProgressBar Marquee:
 Page 745

'*********************************************************************************
' Method : InitializeImage (Green Aero image suitable for a Tiled Continuous ProgressBar)
'*********************************************************************************
Private Function InitializeImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAAGUAAAANCAIAAAA/uTevAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAACV9JREFUWEellulbWmcaxv08V2dp0zRtuqZtEhNjNEaNe0RUgisaAUHFBRERVFAw" &
"uKFRo8YlsZ35S9IYE3cRRBTEDVERQVEzXf+Guc95hWDS6cw1c133h+PhnPd57t+zHIOuTCeEzDFu6pkR" &
"hvToBXasKSvJnJuynJ9m5WasFGatCjlrJfc3ynibFcKtyhKHtGxHVrErlzgVMle9fF+l2G+o92hUHo36" &
"UNt02Kz1tjYftbUet7efdOhOOjtfP+z+Z8+jH3t6f3rU/3PfY0r9g78MQEO/Dj7x67ch6Olvw9DIvxd5" &
"AKKe9707/OsgjsKBA788Hvi5HyH6fupDuJ4fux++7up83ak76Wg71iEl5NbkbdEcahsPHig9TfVuNZKX" &
"u1TVe3WwA1OwVuyQwibMwnLuWnGWTcBe4QNF8lJeojkHcIIuvIy4+Crys/GozyfufDkRe2ky7pupeEC8" &
"OpMUPHMXKEP1zDA989Z82i1DeqTxHpjGmDLiFrPiF3NAFgcBbqqlAIeyrDwKsU2ASHnrooKNMr6doowk" &
"Srel5Ts14t0aZFblrEOKNS4lwQ3Vuhvr3GoI6InIn7hPHiDGZHv1eLHKWVu5S9kr3a5G/YRbEr5dDIf5" &
"G6UobfaqMIM2ybJy0ywFyI2xnH93iZNozkXCMHxnIRMWIo0s2IEpWAudS4FNmIXly9MJsH9pMhYoAOTT" &
"8WjAAaLzY7egcy/Cgt4fDX1/9Oa5F1DY+bFw3P1oLAJPfPIq8uKrKHD84pRjrJ/jtdlk0pKId9vAijKy" &
"kQQIogLIDCmm0+ByVotIbxJq8AaH8Am3BBkQvIXsv+EF4pW7ctBHDfztgNpw1ksQEXH9pEhTxC9mx5oo" &
"RkgVM3QKaDYZgGDn2+kEtMhXkzGgA7OwDO8E0IcvwoDlg9GbQPS35zf++jzkLz9cD8IV/sZdHzIKJHnH" &
"hywayHAizv2WRhY8k4R4iIrYyAC1ogc5k3Qc6ol0ySyj0VD2gC4jvOTokXdbDPqPvKTv8BLYxdzNcooX" &
"aS6aF/odvFC8BF9PoagRBhYKjDKj2Ndn3+omihfMwjKBBYEGmKCZaFg3AOvPP4QE4YpGRoHEz4AaiAzd" &
"iJ70I8PpKMiVmUQyqqTFkAdpMVQS9URVke69Ff4pr/VS+Cm0v5lKuIXn/5uXLJAXutjPC92NBFA2FI+e" &
"QTRXBjYJBjB8PhXrhUwf1VxT8V9Pvd1cGC/YJ81FYJHmAizCK+QPkH388jZBRm83tBg1lSgLWgwlujHH" &
"IC2GVve3GKrKtNzHLsu0FQYsMnGxo0oUsMXAi/piuM7y8o3k7/LCK4G8cBrORCUIL2o9rwrJekYC9DCC" &
"15thJNsKOWOfIP8/bK5wNFfAJJ4213uYR1wBGaHoH8x3kFGLDIeencrTrwGKhhZDAfEdoFuM+rySFYaC" &
"Y634Fr8EKww+MZLwDOf/My8QP8urDFNPeKGvA5cXtqp/GJEnGUaquWYS4cK/ufx7nTTX704iYL337FoQ" &
"aTNCzddoZ9YZWf8+atGoBhlM0mWByJAWjex0JNFi5FtJRtK/wuiRpFY+5gsUyEi+4UUje5cXhjeQF779" &
"4FVEfxxRD4RAIHQ0WV5nh/G0uc7Aote8/yN4trPe3vEUrGfXoD89Cw76cPQWdH404qPRWxdeRFx4cRv6" &
"eCzyk7Goiy+jPn0Z/dmrO5+/ivliPOaribivJzHziVem7gZPJ4fMMELnmOH69Ih5VpSBfceYGWfKTljk" &
"JC/lpy5z71n52Tb87ybirpfzNsRCu0S0VV2+La/cUVTt1smcSvleY51LrdzXQCp3E9TgeaD2aCkdNDd6" &
"tBDukJ+g+n11ratR4Wqs2VNJnUrJbl3FtqLMUVO8VS3YlCAEAiEcgrKxwiw85jKXsXQ/ycyJN+XGLGRH" &
"GzMiDewIfXrYXGrobMr1GcbV6aTLU4nfTCZcmoj/cjwWHuEUfuH647EomgPFBGQoRM/Dzz0P/+B5WNDV" &
"8WRKE4zgiRTo2iTEvD7JDJlKvTGVFjqddnM6PWzmXvgMO3yWfXsuI0qfGa3PjpnPiTNwEo15yQv3U0zc" &
"VHMha0nAXirKspTkWkvzbeW8NYlwXSbaqCm3K8SOeum2qmZXU+tsUu1pG1zNmv3WB+72Zo+u5aCz9eBh" &
"2yGldm9Xh7ebVo/O2w3hDvkJwpN4XuvRNbnbGvdbVa7meqdW4WyS7Tbi8EqHstxeK9qUIyh/TcJdrUQO" &
"nJWyLIsoY7mIvSRMNxcyF3kMU0HSQn6CMQ/Jx+hzYSRSnxUxlwFr8Bg2w4LfkKk02AeEYJoJyIDPlfHk" &
"y7SC7sxyoBhojhM7l0eLE6fPj9fnJ+jvJ+oLkuYh7l0DL9nATzEWMhcEaQtClqmYvViSZRZlL5flLlfk" &
"W8Rcq4RvkwpssqJVuWi9rmJTKbU3yhwaxbZWudPS4GzXuHRa18NWd1e7p0d30PvwoK/7sP+Rd6DXO9h3" &
"ROt4qP94+DEtXEC4Q37qPRrEk3i+67AfL+L1Nnd3y36X1tWJY3G4cre1brtZ7miSbqkrN1WIXrpeV7Im" &
"F67WFNqqeSvSAqskzyJGqtlLpZlmEZJnmYrSFgSwwzDyYQ0GYRN+4Rre4/QUCjABGfCJns2llROUqMdD" &
"3CQ99y7RPB9KnucxQMdQmGIQMA2CVKMgzShMXyhmmUrYi6KMxbIsc3nOUjlnWZxvkXAtVXxrtcBWU2xT" &
"lKzWlq3XV240SO1quUNb69CqdtrUTl3TXmfLfnebu6fD3dfpAamBR4eDfd6h/qPhgaOng8enGjr5bpgW" &
"LqDBkxFyf+D46eOjJ3i+93Co53AQr+MQnbsPB+JYHK52dqh225TbrQqHtnqrqWpTXbnRWLGhLF2rR0pF" &
"NjnS41mlSBUJI20kDwswAjswBWswCJvwC9fwDgIUCpoMQUShnCsIAg5aQp+KUiFjUZqxON1YxDKWQPcW" &
"RBkmqDRzsTybUkWuuTJvWVJgqeJZqvlWmXBFXmyrFa3Wla8pxesN0k1NjR2wWpSAtdOh3etq3utu2+/t" &
"cPd3eQZ6DgZgu9/7ZOBoZOhoZPj4+ycnfycaOflHoJ767g+ffD90/N3g0chj7whe7PUO9xwM4qhOd3/7" &
"fm/LXg9CaJydDTu6+u02xVYzolfbNZINdcV6A1JCYkgPSRZaZUgYaSN5zpIkxyyGHZiCNRiETdov2BWD" &
"ADj4mAhS5ikx5gX/Ape7b9s8iicpAAAAAElFTkSuQmCC"
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

Enhancing a ProgressBar Marquee by Emulating Aero Glass


Suppose we would instead like to use a ProgressBar
Marquee such as the popular Aero Glass ProgressBar
Marquee that has been a standard feature on Windows
Platforms ever since Windows Vista. Even though it is
cooler than a brass toilet seat on the dark side of an
iceberg in the midst of Arctic Winter, for some crazy
reason .NET does not provide us with a way to turn it
on under the Framework's own default ProgressBar
(Grrr), However, we can easily emulate it using two images. We would first tile a small image to the
PictureBox to represent the general background for the ProgressBar emulation, and then paint another
“glare” image on top of that to create the Aero Glass Glare progress effect that slides across it. This
simple effect is incredibly impressive.
Consider the following demonstration code that will perform these tasks. It will tile the general
background image, which is rather small, across the width of the PictureBox, and then draw the Aero
Glass Progress Image over the top of it, moving it from left to right in a continuous motion.
 Page 746

Option Explicit On
Option Strict On
Public Class Form1
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private ProgressBack As Image 'progress bar background image.
Private ProgressImage As Image 'progress bar glare image.
Private Const ProgressInc As Int32 = 4 'how many pixels to increment image rightward.
Private Const ProgressSpeed As Int32 = 20 'how often our timer should tick.
Private WithEvents tmrProgress As New Timer 'timer to increment ProgressImage in picProgress.
Private WithEvents picProgress As New PictureBox 'PictureBox to represent the continuous progress bar.
Private WithEvents btnStartStop As New Button 'button to start and stop the example.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
'*********************************************************************************
' Method : Form_Load
' Purpose : Initialize Form.
'*********************************************************************************
'*********************************************************************************
Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
Me.ProgressBack = InitializeProgressBackImage() 'grab ProgressBar background image.
Me.ProgressImage = InitializeProgressBarImage() 'grab ProgressBar glare image.
With Me.picProgress
.Parent = Me 'set parent. This also sets control visibility to True.
.Location = New Point(12, 12) 'position it in top-left of form.
.Width = Me.ClientRectangle.Width - .Left * 2 'size it across the form.
.Height = ProgressImage.Size.Height 'set picture holder height.
.BorderStyle = BorderStyle.FixedSingle 'make border more visible.
.Anchor = AnchorStyles.Top Or AnchorStyles.Left 'anchor it to the top-left corner of the form..
.Image = New Bitmap(.Width, .Height) 'create image space (was set to Nothing).
Graphics.FromImage(.Image).Clear(.BackColor) 'initialize the image to a blank background.
End With

With btnStartStop 'button to start and stop the sample.


.Parent = Me 'set parent. This also sets control visibility to True.
.Location = New Point(12, Me.picProgress.Top + Me.picProgress.Height + 12)
.Size = New Size(100, 24) 'make big enough to accommodate text.
.Text = "Start/Stop Test" 'give it some text.
End With
Me.tmrProgress.Interval = ProgressSpeed 'set timer interval to 10 ms.
End Sub

'*********************************************************************************
' Method : btnStartStop_Click
' Purpose : Start and stop the custom Progress bar.
'*********************************************************************************
Private Sub btnStartStop_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnStartStop.Click
If Me.tmrProgress.Enabled Then 'if the timer is currently running...
Me.tmrProgress.Enabled = False 'turn off timer.
With Me.picProgress
Graphics.FromImage(.Image).Clear(.BackColor) 'initialize its image to blank.
.Refresh() 'update its display.
End With
Else
Me.ProgressImage.Tag = -Me.ProgressImage.Size.Width 'set ProgressImage startup position.
Me.tmrProgress.Enabled = True 'enable the timer.
End If
End Sub

'*********************************************************************************
' Method : tmrProgress_Tick
' Purpose : Update the Continuous ProgressBar.
'*********************************************************************************
Private Sub tmrProgress_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrProgress.Tick
Me.tmrProgress.Enabled = False 'disable timer for now.
Dim g As Graphics = Graphics.FromImage(Me.picProgress.Image) 'get a graphics interface to the image to draw to.
Dim ImageW As Int32 = Me.picProgress.Image.Width 'drawing area width.
Dim PgrsbW As Int32 = Me.ProgressBack.Width 'progress background image width.
Dim PgrsiW As Int32 = Me.ProgressImage.Width 'progress bar glare image width.
Dim Idy As Int32 = 0 'set secondary index to start point.
Do While Idy < ImageW 'while we have not exceeded the display area...
g.DrawImage(Me.ProgressBack, Idy, 0) 'tile the progress background image to the PictureBox.
Idy += PgrsbW 'tile rightward.
Loop
Dim Idx As Int32 = CInt(Me.ProgressImage.Tag) 'grab the current X offset the progress glare image.
g.DrawImage(Me.ProgressImage, Idx, 0) 'draw the progress image to the PictureBox.
g.Dispose() 'release drawing interface.
Idx += ProgressInc 'bump the X offset.
If Idx > ImageW Then 'if it exceeds the drawing surface...
Idx = -PgrsiW 'reset the X into to the left of the image.
End If
Me.ProgressImage.Tag = Idx 'update the left index.
Me.picProgress.Refresh() 'update the display of the drawing.
Application.DoEvents() 'Do temporary idle state to let paint events fire.
Me.tmrProgress.Enabled = True 're-enable timer.
 Page 747

End Sub

'*********************************************************************************
' Method : InitializeProgressBackImage – Tiled ProgressBar Background Image.
'*********************************************************************************
Private Function InitializeProgressBackImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAACsAAAANCAIAAAC/ygC8AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAAADpJREFUOE9jYNugOrBo1AWjLgChURcMChfwbdIZWMSguNN2YBGD8T6fgUUMVgeC" &
"BhYxOBwKH0h0KBwAxvoj3nECOjYAAAAASUVORK5CYII="
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

'*********************************************************************************
' Method : InitializeProgressBarImage - ProgressBar Glare Image.
'*********************************************************************************
Private Function InitializeProgressBarImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAAGUAAAANCAIAAAA/uTevAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAACV9JREFUWEellulbWmcaxv08V2dp0zRtuqZtEhNjNEaNe0RUgisaAUHFBRERVFAw" &
"uKFRo8YlsZ35S9IYE3cRRBTEDVERQVEzXf+Guc95hWDS6cw1c133h+PhnPd57t+zHIOuTCeEzDFu6pkR" &
"hvToBXasKSvJnJuynJ9m5WasFGatCjlrJfc3ynibFcKtyhKHtGxHVrErlzgVMle9fF+l2G+o92hUHo36" &
"UNt02Kz1tjYftbUet7efdOhOOjtfP+z+Z8+jH3t6f3rU/3PfY0r9g78MQEO/Dj7x67ch6Olvw9DIvxd5" &
"AKKe9707/OsgjsKBA788Hvi5HyH6fupDuJ4fux++7up83ak76Wg71iEl5NbkbdEcahsPHig9TfVuNZKX" &
"u1TVe3WwA1OwVuyQwibMwnLuWnGWTcBe4QNF8lJeojkHcIIuvIy4+Crys/GozyfufDkRe2ky7pupeEC8" &
"OpMUPHMXKEP1zDA989Z82i1DeqTxHpjGmDLiFrPiF3NAFgcBbqqlAIeyrDwKsU2ASHnrooKNMr6doowk" &
"Srel5Ts14t0aZFblrEOKNS4lwQ3Vuhvr3GoI6InIn7hPHiDGZHv1eLHKWVu5S9kr3a5G/YRbEr5dDIf5" &
"G6UobfaqMIM2ybJy0ywFyI2xnH93iZNozkXCMHxnIRMWIo0s2IEpWAudS4FNmIXly9MJsH9pMhYoAOTT" &
"8WjAAaLzY7egcy/Cgt4fDX1/9Oa5F1DY+bFw3P1oLAJPfPIq8uKrKHD84pRjrJ/jtdlk0pKId9vAijKy" &
"kQQIogLIDCmm0+ByVotIbxJq8AaH8Am3BBkQvIXsv+EF4pW7ctBHDfztgNpw1ksQEXH9pEhTxC9mx5oo" &
"RkgVM3QKaDYZgGDn2+kEtMhXkzGgA7OwDO8E0IcvwoDlg9GbQPS35zf++jzkLz9cD8IV/sZdHzIKJHnH" &
"hywayHAizv2WRhY8k4R4iIrYyAC1ogc5k3Qc6ol0ySyj0VD2gC4jvOTokXdbDPqPvKTv8BLYxdzNcooX" &
"aS6aF/odvFC8BF9PoagRBhYKjDKj2Ndn3+omihfMwjKBBYEGmKCZaFg3AOvPP4QE4YpGRoHEz4AaiAzd" &
"iJ70I8PpKMiVmUQyqqTFkAdpMVQS9URVke69Ff4pr/VS+Cm0v5lKuIXn/5uXLJAXutjPC92NBFA2FI+e" &
"QTRXBjYJBjB8PhXrhUwf1VxT8V9Pvd1cGC/YJ81FYJHmAizCK+QPkH388jZBRm83tBg1lSgLWgwlujHH" &
"IC2GVve3GKrKtNzHLsu0FQYsMnGxo0oUsMXAi/piuM7y8o3k7/LCK4G8cBrORCUIL2o9rwrJekYC9DCC" &
"15thJNsKOWOfIP8/bK5wNFfAJJ4213uYR1wBGaHoH8x3kFGLDIeencrTrwGKhhZDAfEdoFuM+rySFYaC" &
"Y634Fr8EKww+MZLwDOf/My8QP8urDFNPeKGvA5cXtqp/GJEnGUaquWYS4cK/ufx7nTTX704iYL337FoQ" &
"aTNCzddoZ9YZWf8+atGoBhlM0mWByJAWjex0JNFi5FtJRtK/wuiRpFY+5gsUyEi+4UUje5cXhjeQF779" &
"4FVEfxxRD4RAIHQ0WV5nh/G0uc7Aote8/yN4trPe3vEUrGfXoD89Cw76cPQWdH404qPRWxdeRFx4cRv6" &
"eCzyk7Goiy+jPn0Z/dmrO5+/ivliPOaribivJzHziVem7gZPJ4fMMELnmOH69Ih5VpSBfceYGWfKTljk" &
"JC/lpy5z71n52Tb87ybirpfzNsRCu0S0VV2+La/cUVTt1smcSvleY51LrdzXQCp3E9TgeaD2aCkdNDd6" &
"tBDukJ+g+n11ratR4Wqs2VNJnUrJbl3FtqLMUVO8VS3YlCAEAiEcgrKxwiw85jKXsXQ/ycyJN+XGLGRH" &
"GzMiDewIfXrYXGrobMr1GcbV6aTLU4nfTCZcmoj/cjwWHuEUfuH647EomgPFBGQoRM/Dzz0P/+B5WNDV" &
"8WRKE4zgiRTo2iTEvD7JDJlKvTGVFjqddnM6PWzmXvgMO3yWfXsuI0qfGa3PjpnPiTNwEo15yQv3U0zc" &
"VHMha0nAXirKspTkWkvzbeW8NYlwXSbaqCm3K8SOeum2qmZXU+tsUu1pG1zNmv3WB+72Zo+u5aCz9eBh" &
"2yGldm9Xh7ebVo/O2w3hDvkJwpN4XuvRNbnbGvdbVa7meqdW4WyS7Tbi8EqHstxeK9qUIyh/TcJdrUQO" &
"nJWyLIsoY7mIvSRMNxcyF3kMU0HSQn6CMQ/Jx+hzYSRSnxUxlwFr8Bg2w4LfkKk02AeEYJoJyIDPlfHk" &
"y7SC7sxyoBhojhM7l0eLE6fPj9fnJ+jvJ+oLkuYh7l0DL9nATzEWMhcEaQtClqmYvViSZRZlL5flLlfk" &
"W8Rcq4RvkwpssqJVuWi9rmJTKbU3yhwaxbZWudPS4GzXuHRa18NWd1e7p0d30PvwoK/7sP+Rd6DXO9h3" &
"ROt4qP94+DEtXEC4Q37qPRrEk3i+67AfL+L1Nnd3y36X1tWJY3G4cre1brtZ7miSbqkrN1WIXrpeV7Im" &
"F67WFNqqeSvSAqskzyJGqtlLpZlmEZJnmYrSFgSwwzDyYQ0GYRN+4Rre4/QUCjABGfCJns2llROUqMdD" &
"3CQ99y7RPB9KnucxQMdQmGIQMA2CVKMgzShMXyhmmUrYi6KMxbIsc3nOUjlnWZxvkXAtVXxrtcBWU2xT" &
"lKzWlq3XV240SO1quUNb69CqdtrUTl3TXmfLfnebu6fD3dfpAamBR4eDfd6h/qPhgaOng8enGjr5bpgW" &
"LqDBkxFyf+D46eOjJ3i+93Co53AQr+MQnbsPB+JYHK52dqh225TbrQqHtnqrqWpTXbnRWLGhLF2rR0pF" &
"NjnS41mlSBUJI20kDwswAjswBWswCJvwC9fwDgIUCpoMQUShnCsIAg5aQp+KUiFjUZqxON1YxDKWQPcW" &
"RBkmqDRzsTybUkWuuTJvWVJgqeJZqvlWmXBFXmyrFa3Wla8pxesN0k1NjR2wWpSAtdOh3etq3utu2+/t" &
"cPd3eQZ6DgZgu9/7ZOBoZOhoZPj4+ycnfycaOflHoJ767g+ffD90/N3g0chj7whe7PUO9xwM4qhOd3/7" &
"fm/LXg9CaJydDTu6+u02xVYzolfbNZINdcV6A1JCYkgPSRZaZUgYaSN5zpIkxyyGHZiCNRiETdov2BWD" &
"ADj4mAhS5ikx5gX/Ape7b9s8iicpAAAAAElFTkSuQmCC"
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function
End Class
 Page 748

Drawing Custom ProgressBars of User-Defined Heights


So far we have drawn custom ProgressBars whose heights have
been dictated by the height of the progress Image. But suppose we
need a ProgressBar that is much taller or even narrower than the
source image? This is also easy to do. Consider the image to the
right. It is our previous image, but displayed three times as tall.
We can of course set our custom ProgressBar to any height we choose, but as an example I made it easy
by setting the picProgress PictureBox’s height like so:
With Me.picProgress
.Parent = Me 'set parent.
.Location = New Point(12, 12) 'position it in top-left of form.
.Width = Me.ClientRectangle.Width - .Left * 2 'size it across the form.
.Height = Me.ProgressImage.Size.Height * 3 'set picture holder height to 3x normal.
.BorderStyle = BorderStyle.FixedSingle 'make border more visible.
.Anchor = AnchorStyles.Top Or AnchorStyles.Left 'anchor it.
.Image = New Bitmap(.Width, .Height) 'create image space (was set to Nothing).
Graphics.FromImage(.Image).Clear(.BackColor) 'initialize the image to a blank background.
End With

But were we to make only this simple little change, if we then ran the code, it would certainly display a
taller PictureBox, but the progress images would still be drawn to their original defined heights. What
we need to do is to stretch the image to fill the new height of the PictureBox and its Image property.
Previously we had demonstrated stretching images. In particular we had stretched images from Icons in
Black Book Tip # 52, Extracting Icon Images from Files and Displaying them in a Directory TreeView
on page 665. How we did that was to take advantage of the many overloads available to the Graphics
interface’s DrawImage method. This method currently has 30 overloads defined (Google or Bing
“graphics.drawImage method” to find references to these overloads on MSDN). Other drawing
methods also feature a rich repertoire of overloads that is well worth the while to explore. Of the
DrawImage overloads, they allow us not only to draw the image starting at a specified starting point that
defines its top-left corner, as we have been currently doing, but we can also draw only portions of the
source image (a technique called Cropping), or we can specify the target size, either as a rectangle, as
we had done when we were rescaling icon images, or by specifying the offsets for the left, top, width,
and height of the target area. If the size of the target rectangle differs from the original image, the
DrawImage method will rescale the rendered image to fit within the target rectangle, stretching or
compressing it as needed in order to automatically shoehorn it into place with absolute ease..
We have been using the template Graphics.DrawImage(Image, TargetX, TargetY), which draws the source
image, using its original physical size, to a location specified by a coordinate pair. But, were we to examine
the second listed overload, Graphics.DrawImage(Image, TargetX, TargetY, TargetWidth, TargetHeight), this
version of the method will draw the Image at a specific location and to a specific target size.
Thus, apart from specifying the height of our PictureBox that stands in for a ProgressBar, we can update
the tmrProgress_Tick event code to accommodate whatever size we require for the ProgressBar:
'*********************************************************************************
' Method : tmrProgress_Tick
' Purpose : Update the Continuous ProgressBar, adapted for different bar heights.
'*********************************************************************************
Private Sub tmrProgress_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrProgress.Tick
Me.tmrProgress.Enabled = False 'disable timer for now.
Dim g As Graphics = Graphics.FromImage(Me.picProgress.Image) 'get a graphics interface to the image to draw to.
Dim ImageW As Int32 = Me.picProgress.Image.Width 'drawing area width.
Dim ImageH As Int32 = Me.picProgress.Image.Height 'drawing area height.
Dim PgrsbW As Int32 = Me.ProgressBack.Width 'progress background image width.
Dim PgrsiW As Int32 = Me.ProgressImage.Width 'progress bar glare image width.
Dim Idy As Int32 = 0 'set secondary index to start point.
Do While Idy < ImageW 'while we have not exceeded the display area...
g.DrawImage(Me.ProgressBack, Idy, 0, PgrsbW, ImageH) 'tile the progress background image to the PictureBox.
Idy += PgrsbW 'tile rightward.
Loop
Dim Idx As Int32 = CInt(Me.ProgressImage.Tag) 'grab current X offset for rendering progress image.
g.DrawImage(Me.ProgressImage, Idx, 0, PgrsiW, ImageH) 'draw the progress image to the PictureBox.
 Page 749

g.Dispose() 'release drawing interface.


Idx += ProgressInc 'bump the X offset.
If Idx > ImageW Then 'if it exceeds the drawing surface...
Idx = -PgrsiW 'reset the X into to the left of the image.
End If
Me.ProgressImage.Tag = Idx 'update the left index.
Me.picProgress.Refresh() 'update the display of the drawing.
Application.DoEvents() 'Do temporary idle state to let paint events fire.
Me.tmrProgress.Enabled = True 're-enable timer.
End Sub

Adding Bling to the Standard ProgressBar Using Images


We can also emulate the standard (regular) ProgressBar, but using custom images. To do so requires just
a little simple math and the ability to clip images. Under older operating systems, we may have simply
painted a color rectangle to draw the actual progress bar that creeps across the PictureBox.
For example, suppose we had a local Integer variable name ProgressPercent, defined like so:
Private ProgressPercent As Int32 = 0 'percentage of tasks completed (0 to 100).

Whenever our code progressed in its task, it would calculate the percentage of the full task completed,
and then assign this result to the ProgressPercent variable using the following simple template formula,
assuming all members are integers: ProgressPercent = TasksCompleted * 100 \ AllTasksToComplete . This
algorithm will store an integer value from 0 to 100 in the ProgressPercent variable.
In our picProgress_Tick event method, we would have to first determine if we need to update our image
dimensions. If we do, we must calculate the percentage of the length of the PictureBox we would need
to draw from the left of our progress bar. For example, where ImageW is the width of the PictureBox:
Dim PgrsAct As Int32 = ProgressPercent * ImageW \ 100 'compute required pixel width of progress indicator bar.

We could then use the Graphics Interface’s FillRectangle method to draw a color rectangle:
g.FillRectangle(Brushes.Blue, 0, 0, PgrsAct, ImageH) 'draw a blue rectangle to display progress indicator bar.

Though this procedure is so “old school”, it is interesting to note that this


technique has actually made resurgence with the advent of smart-devices
and pads. Even more, it is also interesting that the real reason why this
technology is redialing back to these more “ancient” techniques is not
because it is more “trendy”, where “flat is the new black” of the graphical fashion world, but because the
graphics interfaces of these new devices are so mind-numbingly slow when it comes to graphics
rendering, no matter how their vendors might try to mask that fact behind “cool” transitions, effects, and
using ad campaigns to convince the consumers that not the vendors, but that they, the consumers,
wanted this (when what the consumers really want is the same 3D effects on their portable devices as
they have on their PCs). The fact is, those of us who have been in the business long enough noticed
right away that this is the very same graphical evolution that earlier desktop computers went through
from its boring flat graphics to its eye-popping hi-rez 3D image rendering. Watch, and in time we will
see that as these devices become faster and faster, their image rendering technology will become more
sophisticated, eventually evolving to rival desktop and laptop computers, and at which time such 3D
effects and rendering, presently standard on PCs, will suddenly become “the new black”.
All that aside, let us now emulate a standard ProgressBar, but using our own images.
The trick to this is that we will not draw all the way across the PictureBox except when the
ProgressPercent value is at 100, and so we must assume that we will just render to a target rectangle that
is only as wide as PgrsAct (ProgressPercent * ImageW \ 100).
 Page 750

To keep things simple, we will start out by rendering just our background image, ProgressBack. Once we
have that mastered, we will then add a real-time progress effect, a glare, by sliding ProgressImage across
it, clipping it as needed so it will never draw beyond the edge of our progress indicator bar limit.
The trick to tiling the background image is that if we always use the full width of this image, we might
spill over where we want the right edge of the progress indicator bar to actually be. As such, we should
only tile full images for as far as we can, and then clip (partially paint) a final rendering, if needed.
So, first, we should compute how many full background images we can initially tile. We can do this
easily enough by dividing the PgrsAct value by the width of the background image, which is, as
demonstrated earlier, held by the variable PgrsBkW:
If ProgressPercent < 0 OrElse
ProgressPercent > 100 Then 'if there is no need to update...
Return
End If

Dim g As Graphics = Graphics.FromImage(Me.picProgress.Image) 'get a graphics interface to the image to draw to.
Dim ImageW As Int32 = Me.picProgress.Image.Width 'drawing area width.
Dim ImageH As Int32 = Me.picProgress.Image.Height 'drawing area height.
Dim PgrsBkW As Int32 = Me.ProgressBack.Width 'progress background image width.
Dim PgrsBkH As Int32 = Me.ProgressBack.Height 'progress background image height.
Dim PgrsBrW As Int32 = Me.ProgressImage.Width 'progress bar image width.
Dim PgrsBrH As Int32 = Me.ProgressImage.Height 'progress bar image height.
Dim PgrsAct As Int32 = ProgressPercent * ImageW \ 100 'compute pixel width of progress indicator bar.
Dim AbsoluteWd As Int32 = (PgrsAct \ PgrsBkW) * PgrsBkW 'compute bar as rendered by full copies of background.
Dim Idx As Int32 = 0 'set the index to start point.
With Me.picProgress
g.Clear(.BackColor) 'initialize the image to a blank background.
If ProgressPercent > 0 Then 'if not 0%...
Do While Idx < AbsoluteWd 'while we have not exceeded the full display area...
g.DrawImage(Me.ProgressBack, Idx, 0, PgrsBkW, ImageH) 'tile the progress background image to the picturebox.
Idx += PgrsBkW 'tile rightward.
Loop
End With
g.Dispose() 'release drawing interface.
Me.picProgress.Refresh() 'update the display of the drawing.
Application.DoEvents() 'Do temporary idle state to let paint events fire.

The next thing we need to do is determine if we need to additionally paint a final, partial (clipped) image
of the background, which we would do immediately after the above Loop instruction:
Dim Remainder As Int32 = PgrsAct Mod PgrsBkW 'compute # pixels to draw after whole images tiled.
If Remainder <> 0 Then 'if we have something left...
'draw a partial image here...
End If

We will need to draw a clip of our background image to a target location with a specified size. This is
important if we have also changed the height of our emulated ProgressBar PictureBox, as we have
already demonstrated. For that, we can choose from a number of DrawImage method overloads, though
I will focus here on DrawImage(Image As Image, DestRect As Rectangle, srcX As Int32, srcY As Int32,
srcWidth As Int32, srcHeight As Int32, srcUnit As GraphicsUnit) . This seems like a mouthful, but it is
actually quite simple. With it, we specify the source image, the destination rectangle, and then the top-
left X and Y offsets to start rendering from the source image, and finally the width and height to use
from the source image. These last four parameters allow us to specify a rectangular portion of the source
image to draw from. As we can see, we must also specify the Graphics Units that we are using. To keep
everything simple, we should only need to specify pixels (GraphicsUnit.Pixel).
Our target rectangle will specify the starting left index, which is the next pixel after the previously
rendered background image (if any), presently held by Idx, the top of the target Image, 0, the width we
want to render, held by Remainder, and the height, which is the height of the target Image, held by ImageH.
We next have to specify the portion of the background image we want to render from. We will always
begin painting from its top-left corner, which is coordinate 0,0, and then specify the width we want to
render, Remainder, and finally the height of the background image, PgrsBkH. Thus, the above becomes:
 Page 751

Dim Remainder As Int32 = PgrsAct Mod PgrsBkW 'compute # pixels to draw after whole images tiled
If Remainder <> 0 Then 'if we have something left...
g.DrawImage(Me.ProgressBack,
New Rectangle(Idx, 0, Remainder, ImageH),
0, 0, Remainder, PgrsBkH, GraphicsUnit.Pixel) 'draw a portion of the Image to a location and size
End If

Therefore, so far, our tmrProgress_Tick event code becomes:


'*********************************************************************************
' Method : tmrProgress_Tick
' Purpose : ProgressBar emulation, using just a background image.
'*********************************************************************************
Private Sub tmrProgress_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrProgress.Tick
If ProgressPercent < 0 OrElse
ProgressPercent > 100 Then 'if there is no need to update...
Return
End If
Me.tmrProgress.Enabled = False 'disable timer for now.
Dim g As Graphics = Graphics.FromImage(Me.picProgress.Image) 'get a graphics interface to the image to draw to.
Dim ImageW As Int32 = Me.picProgress.Image.Width 'drawing area width.
Dim ImageH As Int32 = Me.picProgress.Image.Height 'drawing area height.
Dim PgrsbW As Int32 = Me.ProgressBack.Width 'progress background image width.
Dim PgrsbH As Int32 = Me.ProgressBack.Height 'progress background image height.
Dim PgrsiW As Int32 = Me.ProgressImage.Width 'progress bar image width.
Dim PgrsiH As Int32 = Me.ProgressImage.Height 'progress bar image height.
Dim PgrsAct As Int32 = ProgressPercent * ImageW \ 100 'compute pixel width of progress indicator bar.
Dim AbsoluteWd As Int32 = (PgrsAct \ PgrsiW) * PgrsbW 'compute bar as rendered by full copies of background.
Dim Idx As Int32 = 0 'set the index to start point.
With Me.picProgress
g.Clear(.BackColor) 'initialize the image to a blank background.
If ProgressPercent > 0 Then 'if not 0%...
'g.FillRectangle(Brushes.Blue, 0, 0, PgrsAct, ImageH) 'draw blue rectangle to display progress indicator bar.
Do While Idx < AbsoluteWd 'while we have not exceeded the full display area...
g.DrawImage(Me.ProgressBack, Idx, 0, PgrsbW, ImageH) 'tile the progress background image to the PictureBox.
Idx += PgrsbW 'tile rightward.
Loop
Dim Remainder As Int32 = PgrsAct Mod PgrsbW 'compute # pixels to draw after whole images tiled.
If Remainder <> 0 Then 'if we have something left...
g.DrawImage(Me.ProgressBack,
New Rectangle(Idx, 0, Remainder, ImageH),
0, 0, Remainder, PgrsbH, GraphicsUnit.Pixel) 'draws a portion of the Image to a location and size.
End If
'<<-We will render the animated progress glare image in this space->>
End If
End With
g.Dispose() 'release drawing interface.
Me.picProgress.Refresh() 'update the display of the drawing.
Application.DoEvents() 'Do temporary idle state to let paint events fire.
Me.tmrProgress.Enabled = True 're-enable timer.
End Sub

Now that we can emulate a simple ProgressBar using a custom image, it would look even better if we
animated it with a cycling glare effect, especially because it is being constantly redrawn anyway within
our timer event, which is firing every 20 milliseconds (or whatever we set variable ProgressSpeed to).
Like with drawing the background image to the right edge of the desired progress indicator dimensions,
we may need to clip the animated glare image, ProgressImage, if its right edge exceeds the right limit of
the progress indicator bar. As we had done previously, we maintain the index for the starting left edge of
our glare image in the Tag property of our foreground glare image.
We will also need to determine if we must clip this image when we render it. To do that, we simply
check to see if its right edge will extend beyond the right extent of our progress indicator. If it does, we
need to clip it back to stay within bounds. Since we are all experts by now, or at least comfortable with
rendering partial, or clipped images, this additional step should be child’s play:
Idx = CInt(Me.ProgressImage.Tag) 'grab the current X offset for the glare image.
Dim SrcW As Int32 = PgrsiW 'init ScrW to the full width of progress Glare Image.
If Idx + SrcW > PgrsAct Then 'if the Glare Image end will exceed desired bar width...
SrcW -= (Idx + SrcW - PgrsAct) 'trim the Glare Image width to what we can draw of it.
End If
g.DrawImage(Me.ProgressImage,
New Rectangle(Idx, 0, SrcW, ImageH),
0, 0, SrcW, PgrsiH, GraphicsUnit.Pixel) 'draws a portion of Glare Image to a location and size.

Altogether this becomes:


 Page 752

'*********************************************************************************
' Method : tmrProgress_Tick
' Purpose : ProgressBar emulation, using a background image with an animated glare.
'*********************************************************************************
Private Sub tmrProgress_Tick(ByVal sender As Object, ByVal e As EventArgs) Handles tmrProgress.Tick
If ProgressPercent < 0 OrElse
ProgressPercent > 100 Then 'if there is no need to update...
Return
End If

Me.tmrProgress.Enabled = False 'disable timer for now.


Dim g As Graphics = Graphics.FromImage(Me.picProgress.Image) 'get a graphics interface to the image to draw to.
Dim ImageW As Int32 = Me.picProgress.Image.Width 'drawing area width.
Dim ImageH As Int32 = Me.picProgress.Image.Height 'drawing area height.
Dim PgrsbW As Int32 = Me.ProgressBack.Width 'progress background image width.
Dim PgrsbH As Int32 = Me.ProgressBack.Height 'progress background image height.
Dim PgrsiW As Int32 = Me.ProgressImage.Width 'progress bar image width.
Dim PgrsiH As Int32 = Me.ProgressImage.Height 'progress bar image height.
Dim PgrsAct As Int32 = ProgressPercent * ImageW \ 100 'compute pixel width of progress indicator bar.
Dim AbsoluteWd As Int32 = (PgrsAct \ PgrsbW) * PgrsbW 'compute bar as rendered by full copies of background.
Dim Idx As Int32 = 0 'set the index to start point.
With Me.picProgress
g.Clear(.BackColor) 'initialize the image to a blank background.
If ProgressPercent > 0 Then 'if not 0%...
Do While Idx < AbsoluteWd 'while we have not exceeded the full display area...
g.DrawImage(Me.ProgressBack, Idx, 0, PgrsbW, ImageH) 'tile the progress background image to the PictureBox.
Idx += PgrsbW 'tile rightward.
Loop
Dim Remainder As Int32 = PgrsAct Mod PgrsbW 'compute # pixels to draw after whole images tiled.
If Remainder <> 0 Then 'if we have something left...
g.DrawImage(Me.ProgressBack,
New Rectangle(Idx, 0, Remainder, ImageH),
0, 0, Remainder, PgrsbH, GraphicsUnit.Pixel) 'draws a portion of the Image to a location and size.
End If
Idx = CInt(Me.ProgressImage.Tag) 'grab the current X offset for the Glare Image.
Dim SrcW As Int32 = PgrsiW 'init SrcW to the full width of progress Glare Image.
If Idx + SrcW > PgrsAct Then 'if the Glare Image end will exceed desired bar width...
SrcW -= (Idx + SrcW - PgrsAct) 'trim the Glare Image width to what we can draw of it.
End If
g.DrawImage(Me.ProgressImage,
New Rectangle(Idx, 0, SrcW, ImageH),
0, 0, SrcW, PgrsiH, GraphicsUnit.Pixel) 'draws a portion of Glare Image to a location and size.
End If
End With
g.Dispose() 'release drawing interface.
Idx += ProgressInc 'bump the offset.
If Idx > ImageW Then 'if it exceeds the drawing surface...
Idx = -PgrsiW 'reset origin to < 0.
End If
Me.ProgressImage.Tag = Idx 'update the index.
Me.picProgress.Refresh() 'update the display of the drawing.
Application.DoEvents() 'Do temporary idle state to let paint events fire.
Me.tmrProgress.Enabled = True 're-enable timer.
End Sub

All that is left to do is perhaps displaying the percentage completed within


the center of the ProgressBar, much as older systems used to do. We can
render this effect easily using the graphics interface’s DrawString method
right within the Tick event method, just before we dispose of the Graphics
Interface, between the highlighted End If and the End With, above, like so:
End If
Dim fnt As New Font("Arial", 12.0!, FontStyle.Bold) 'font to use.
Dim pCent As String = ProgressPercent.ToString & " %" 'define percentage string.
Dim sz As Size = TextRenderer.MeasureText(pCent, fnt) 'find its dimensions.
Dim X As Int32 = (.Width - sz.Width) \ 2 'compute how to center it horizontally.
Dim Y As Int32 = (.Height - sz.Height) \ 2 'compute how to center it vertically.
g.DrawString(pCent, fnt, Brushes.Black, New Point(X, Y)) 'draw the string to the ProgressBar.
fnt.Dispose() 'release created font.
End With
g.Dispose() 'release drawing interface.
 Page 753

Adding Super Bling to Your ProgressBars Using Complex Images


One popular image format for progress bars is the so-called Glossy
Buttons or Jelly images. They are three-dimensional and fun-looking,
and are presented in a variety of colors. Plus, rendering them is actually
quite easy to do, and we can keep track of them with only a small
amount of additional math. If we look for images of buttons on the
internet, such as if we Google or Bing “Glossy Button Images” or even
“Button Images”, we will find a quite massive variety of them. Using
MS Paint and some patience, we can capture them and break them up
into segments that can be blended together.
Typically, a Jelly image is actually composed of three separate parts; a
left, middle, and right image, such as the following three segments:

NOTE: These three images are dimensioned as follows: Left = 17x40 pixels, Middile = 28x40
pixels, and Right = 17x40 pixels.
The middle image will be tiled to fill out the target width, less the width of the left and right images. Just
as we did earlier with clipping images, we will clip the middle image as needed so that the over-all
target width will be satisfied. The only wrinkle in this process is when the target width is less than the
width of not only the combined left and right images, but less than just one of the end images.
Actually, all we need to do is keep track of the width of all three
images. We would start by rendering the right image, moving it
rightward just as we did with placing our glare effect image on the
PictureBox, but so that its right edge matches the desired progress
target width. Thus, if the width limit is tiny, most of the right image
could be hidden beyond the left edge of the PictureBox. Once we
have rendered it, we subtract the width of the right image from the
target width, and if the result is greater then zero, we determine if we
can render all, or just a portion of the left image. If the result is less
than then width of the left image, we move the origin of the left
image leftward until its right end will join the right image. If we can
render the full left image and there is still a positive result after
subtracting its width, we will try to tile the middle image as needed,
and then render a final or only clipped portion, if required, until the
center area between the left and right images is filled.
As frustrated as some people get over trying to do this, it is surprisingly easy to do. Create a new project
and drop the form code on the next page into the form’s code page, assuming the form is named Form1,
and then run it. With the mouse, click within the PictureBox and watch the displayed progress match it.
The featured UpdateProgress method will take a reference to the target PictureBox and the percentage
(0-100) to render to it, such as “UpdateProgress(Me.PicProgress, 75)”:
 Page 754

Option Explicit On
Option Strict On
Public Class Form1
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
Private ProgressLft As Image 'progress left image.
Private progressMdl As Image 'progress middle image.
Private ProgressRht As Image 'progress right image.
Private WithEvents picProgress As New PictureBox 'picturebox to represent the continuous progress bar.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*********************************************************************************
' Method : Form_Load
' Purpose : Initialize Form.
'*********************************************************************************
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
ProgressLft = InitializeLeftImage() 'build progress left image.
progressMdl = InitializeMiddleImage() 'build progress middle image.
ProgressRht = InitializeRightImage() 'build progress right image.

With Me.picProgress
.Parent = Me 'set parent.
.Location = New Point(12, 12) 'position it in top-left of form.
.Width = Me.ClientRectangle.Width - .Left * 2 'size it across the form.
.Height = progressMdl.Size.Height 'set picture holder height.
.BorderStyle = BorderStyle.FixedSingle 'make border more visible.
.Anchor = AnchorStyles.Top Or AnchorStyles.Left 'anchor it.
.Image = New Bitmap(.Width, .Height) 'create image space (was set to Nothing).
Graphics.FromImage(.Image).Clear(.BackColor) 'initialize the image to a blank background.
End With
End Sub

'*********************************************************************************
' Method : picProgress_MouseClick
' Purpose : User picked on the image, so update progress to that point.
'*********************************************************************************
Private Sub picProgress_MouseClick(sender As Object, e As MouseEventArgs) Handles picProgress.MouseClick
Dim pic As PictureBox = DirectCast(sender, PictureBox) 'get referenced PictureBox.
UpdateProgress(pic, e.X * 100 \ pic.Image.Width) 'update progress image with percentage.
End Sub

'*********************************************************************************
' Method : UpdateProgress
' Purpose : Update referenced progress image to the selected percentage.
'*********************************************************************************
Private Sub UpdateProgress(ByRef pic As PictureBox, ByVal Percent As Int32)
Dim g As Graphics = Graphics.FromImage(pic.Image) 'get a graphics interface to the image to draw to.
Dim ImageW As Int32 = pic.Image.Width 'drawing area width.
Dim ImageH As Int32 = pic.Image.Height 'drawing area height.
Dim PgrsAct As Int32 = Percent * ImageW \ 100 'compute required pixel width of progress indicator.

g.Clear(pic.BackColor) 'first clear any previous drawing...


If PgrsAct > 0 Then
Dim imgLftW As Int32 = Me.ProgressLft.Width 'get widths of images.
Dim imgMdlW As Int32 = Me.progressMdl.Width
Dim imgRgtW As Int32 = Me.ProgressRht.Width
Dim imgMdlH As Int32 = Me.progressMdl.Height 'get height of middle image.

Dim X As Int32 = PgrsAct - imgRgtW 'compute start of right image.


g.DrawImage(Me.ProgressRht, X, 0, imgRgtW, ImageH) 'draw what we can of the right image.
If X > 0 Then 'if we have room left over...
If X <= imgLftW Then 'if we can show just the left image...
X -= imgLftW 'compute start for left image.
g.DrawImage(Me.ProgressLft, X, 0, imgLftW, ImageH) 'draw what we can of the left image.
Else 'we have room for middle image data...
g.DrawImage(Me.ProgressLft, 0, 0, imgLftW, ImageH) 'so draw the left image all the way left.
X -= imgLftW 'figure how much room is left.
Dim Remainder As Int32 = X Mod imgMdlW 'compute # pixels to draw after whole images tiled.
Dim Cnt As Int32 = X \ imgMdlW 'compute how may whole middle images can be tiled.
X = imgLftW 'init start position beyond left image.
Do While Cnt <> 0
g.DrawImage(Me.progressMdl, X, 0, imgMdlW, ImageH) 'tile the middle image to fill space.
X += imgMdlW 'bump offset.
Cnt -= 1
Loop
If Remainder <> 0 Then 'if we have something left...
g.DrawImage(Me.progressMdl,
New Rectangle(X, 0, Remainder, ImageH),
0, 0, Remainder, imgMdlH, GraphicsUnit.Pixel) 'draw a portion of Image to a location and size.
End If
End If
End If
Dim fnt As New Font("Arial", 12.0!, FontStyle.Bold) 'font to use.
Dim pCent As String = Percent.ToString & " %" 'define percentage string.
Dim sz As Size = TextRenderer.MeasureText(pCent, fnt) 'find its dimensions.
Dim Xf As Int32 = (ImageW - sz.Width) \ 2 'compute how to center it horizontally.
 Page 755

Dim Yf As Int32 = (ImageH - sz.Height) \ 2 'compute how to center it vertically.


g.DrawString(pCent, fnt, Brushes.Black, New Point(Xf, Yf)) 'draw the string to the progressbar.
fnt.Dispose() 'release created font.
End If
g.Dispose() 'release drawing interface.
pic.Refresh() 'update the display of the drawing.
End Sub

'*********************************************************************************
' Method : InitializeLeftImage.
'*********************************************************************************
Private Function InitializeLeftImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAABEAAAAoCAIAAACXeobIAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAAB6tJREFUSEt91flb0gkewPHvnzDzdGzH1NjWlGV5lOaWTdeUNVubmmlWWp5p2abZ" &
"YWkeeSCKgoJXeITKIYJIgAgKCAjKKSiHoIKIAh6keYSaNs/iPrO7z86zzz7P+9fX83w+n18+gH1a9/+b" &
"ndJNWbVanZwv6qFx2BhK+/82M7Yh66Rm1DAg7u+jslkfSFRIPT6trPFZUf2jvJo/mtlp3bRNazINDmik" &
"nXxuBZb0FFwb+Ax6KaHwwsPCy49KrifB/2icwGBQcvv4yJaPTwtqg57Bfo4G+USALibAwtLqXiPIMCz7" &
"v4zVotHp+51DF9e3xGRVnY8r8okAn46B3k5Dva2mN7RLWFK9XGf+j3HuoBvuZ/K7c6owoS/hPuH5B4Pz" &
"fCJLQ9OxsGYht3/Mal9adKytrK3/bpxgYlxF72ZnVTbdTCk9EQ76c1D+sXtlUXmkelq/RGsZs81Pzy/b" &
"FxyfFhybZnZqyDyu4osF0AZC2GvEPwHoYCgs8DUOghFyFSbN2OyoZc5gnd/MMr9pbBaNSiurIVDic5Gn" &
"o8B7A3L3BEF8YmtTq9gtHK1UZ5Xpndmkut/bNCMjCiaf6zz/lUSoS+C7rddArvcqAt60QPHiFu5Qh8TA" &
"2MzI/FfAtG1I0i+qaaGEp1V63QNv+zV3y7Ui90jk7ew2MKavlqZs6lQ7Q3dp/h0wOaHu4HZnIDAX40v2" &
"BRV8fxW89W8wr5j6kCzyGyQPjBFBCVIYUQYjymGtchhpM0A/rGggt0dlv/cML9wVAPnuetm2oPKj0air" &
"qaS4kq7n1bw3dcL0D8J0VG86qi+9YTOgf1AKbSRdS4b/dLt4W2Dpd4FV20OQrtFNZ5+TQvIYkSWch3Be" &
"fDk/vqInoVKQUCV0BvDFwozK5tNxsL0h8C03q7+/Vf+nOw0/xTaffEa+kskIyGcFF3JDINyQYn5ISU8o" &
"VBAKEwIUNiepGH0iCr4rFLklFLXlDnbHA/z++DaP5Ha/tK4L2ZxLebzLoJ7LBQJ/cK9/kcgfIgYwVEZc" &
"fuOxyIqdd1Bb7uK23ifuiiXvS6QdSek8/objm8k7lSM4ndfrBxKdAYvPFEnPQGRADYEWno06HFm9IwKz" &
"9QFxeyxl9+N2l2TmwZcctzSee5bAK6fveJ7kRIHMu0juDVH4lCiBCtzHsEyUazRyZ1Tz9jjyjsf0PUlM" &
"lxfs/a95hzIEru/6juRL3MDyYxCFe8mAO0zlXqoGENiPoZkNrrF1ux4SdiRSdyUz9r5i7UvnHsgSHMzr" &
"OwyWuBX3H4Upj8FVHhUaz6ohz2odAHea7CbXBNTuRNLOZ/QfUrv2ZXAP5AoOFohci6VupQr38kHParVX" &
"zdDx+mFv1Kh3gwEox1LCctBuTxp/SP64O5W5N7P7QL7AFSI6Uio/WqH0eK86Xqf1btCfxIz64ox/wZtO" &
"EcaBKjw1Ih/jkdz040vqnrcsl3z+oWKRG1zmUT3gVafxbtT5YkdOtRj9Wk0/k83nKJPnqBaglkiLKcR6" &
"P0fvT6O55HD2Q3rdyqWeSKU3SuOL1fsRRs+2mc5TzRfplsudVn/WlD9rGkBT6IlQ3JnUJtcs+gEw7xBc" &
"7FWr8G1S++H159oMF2kmf+bkryzbde70jZ6ZAKE9sPcTQO7qTK3AX0pv9MilH4b2HEXKfdGqs0T9L1TD" &
"Feb4NY4loGcquG82RPIpTD53R/H5rmIBYAs4eXWEG9mNviCaZ3mPV6PyDHHoUrvhGtscyLfeEk2Hye3h" &
"yvkH6oVo7WKMbilWvwz0SfhwTFsECH0BRPKt5J3Eq85Th//KNt0UWMIkU+FKe5RmPla/GD+69Nj45YnJ" &
"8XeTA+hXCFAkanIp9gYIf66S5Ucc/IU5cqPHHCabuj84G6ObSxhdfGJaTppwpFhXXthWX02tAmqVsK2D" &
"nl+DjwBjrpbTz+Hl/uyRIPFkhGomVj/3eGwhaWL5uW0ldWY1zf717dzXzPmvwPCQiM3rrMa1JkHRt6Ct" &
"l2q6r3bpg2WWB1r7I+Pn5Mmll9OON/bVjPmv7xbWcxc38pc2gNFhsUjMIdKoOdW4WGhzIJx6nSwP7jXe" &
"184kmj4/ty6nza5kzq/lLKyDljfAjm9Fjm+AYUQ6oOCzuYwqDDEVgb1f0hzcyLnVqbqntDwyzr2wLr21" &
"r7z7vJa/tF745Vvxym/Q1d8A46hMoxKIRKyPdArsA/4pFB2OIIViebc52mi1Lcn8OX3GkTO3VrC4XvRl" &
"A7ryrdRpTIZ+naZXIe/m8TqwpNbCGmwSHBv1nnIXxw9nqx8qzCljc1kzDtD8WvHSOuzLRpljAzCPKUZ0" &
"YvUAXybu6mJR0MQW0HtMCgIXU9EW3tR9v0MRLxtLHbXnWJeK7A7owip8+SswOT7gXEmrFirkHKGAzmC2" &
"oQn44jrcKwTuEZwQXUOPxPfEMQaSRYa3Qzawea7U/gWwTqhMRvmwTqRS8qRi5zuk0jtaMQQ8AoXNqcam" &
"VOAfVpLj6hkJeN5TmuQFezBNqANsFvWESWkckQ6phUo5R9TL4HdTmAwiidxch8EUItGv4eiUMswTREtC" &
"ZVs8sj2hjglMWTUW8+C4sX9EJ1IP8uVSVq+AzuWQmR0ECqWZ0IptwGEQKDQYicmoxLyCY1+U4f4Ba+wL" &
"6k7V+j0AAAAASUVORK5CYII="
Return ConvertBase64ToImage(strImg)
End Function

'*********************************************************************************
' Method : InitializeMiddleImage.
'*********************************************************************************
Private Function InitializeMiddleImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAABwAAAAoCAIAAABihA14AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAABBRJREFUSEutzOlTU1cYBvDz57iwLyoK0kWgu7YKbbW2FhERCrTi0qJoCItbgCRs" &
"CVZrQmfqQu0CdKYl2A9CEGmCS2VxSsIyBqQSzSS5CVmYvvc9OeHmEtoPzcxvMu99nueEtF37IeJIUe0l" &
"dJmhn2GTf1NYc6mg+ut86cU8iZrsl2r+U67goERVbuWVfZJv9lZc3l1+cefRVvJL38OI6L79oOv2/R9/" &
"H27vvkOm56wRM2udtDwbn5wjrkVvZLk9XvLshTOyFmxO8tcTawRNIDI0Nhtx5Nch8/91lzIFkW91IxHQ" &
"g3SPKNJw0xhRBvlNAzmhvRNUvgrhJpxBhLdmAJCDKn1Y+QKiaoWBAp6+QNVf0Moj2Qr9SrtCidpsxYCY" &
"fCBHoc9R9OfIeSSj7q7INpmYaAAyQ2UFDPJkg2RznVEkRSYmGmxhUkOlUTIDSWh4GB9OXP2yYAjjoESU" &
"JJDMe5Bcf5+sV46sY9YGKXhrmOAAxlQUiuaNghjlCBWrfBSj+JOsVz8OWofWUqqANarlARXVxosGal5M" &
"2+PYgPE4oB4nsVpTUIzGFB0qCgk3sVoziGs3xwOtCSS0mxJREm8CkKQbM4ko4cZM/PVpEEddWwYV3cAY" &
"JIOOmQ1oY8fMpo6ZlO8ZuDumSUqnBWxCGzstG34OSAY/UU9oS5ebOy1buiypTFqXZWu3Jb17lrGkd1nI" &
"S7p5kA56eFt75tNQKvgNPAV8hTPwsm7+Fd38q728bSjj1nwm7++sW/NZveApeaPfCl5Hr6GsPl5mwEJG" &
"3wKE0NLlm/3Wt/S8t/XWdwZ429GOIP0CyTbadhltO5n30LtG2w5DwHbDC1rBDMbZw7acYdv74J7tg3u2" &
"D5k9AuSTMQf4eMyxl/lolLdn1E7tHrVDCAO63Dfm+BSMO3LHHftRHjogQA5NcYcmuYJJ7iCTjw5MOvMo" &
"sxNCGMAMxoVTXBH6bIorZkpQKTvI4Tk3+GKW9zkqRSVMMVaALsvm3EfQ0Tn3MXR8BXLC6gHl6Cv0pdVz" &
"nDmGaEuX4KTVU/Hcc+q55zQjEYBPctLuqwh1mpEwlXaflKmy+6odvhqHr5Y5I0ATIuX8VQLVnL+GqUVn" &
"0FnmHOc/z/kvIJmLV8fwn5iTs+4lcE7gPHMBydxLdaieaXAvyZECLApgCC2hz4SE7wN/sRgAL5WoMSz3" &
"khKRepcfNAjIkSLI7W9kmlCz298SDuRNLn+jy0/oS2UoKCgYgWbUglpdftUqoGrhfM2cjzQ6fU2hmoM4" &
"H4xaGRVSozYxr9rpVTm9rQ5vs91DJAazUOUKUmNAlcGEzNViJkpqMEn+MJ0amiDFml6eFn9XUaLtLdHo" &
"QGlAb6hAWKzRFWl0hVd6SJn8apmC+o4dYRxBK8Pl53gchkNx9R/C4z3Uyy8aCAAAAABJRU5ErkJggg=="
Return ConvertBase64ToImage(strImg)
End Function

'*********************************************************************************
' Method : InitializeRightImage.
'*********************************************************************************
Private Function InitializeRightImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAABEAAAAoCAIAAACXeobIAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAAB6RJREFUSEuF0flX0nsex/Hvn3DvsZqWW1O3blmWS2lO2W27Zd2pSc00Ky3XtGzS" &
 Page 756

"bLE0l1QQN1BwC5dQWUQQCRBBAQFB2UFZBBVFEnAhzSXUtM7gdM49M3d+mHMeP36e531e5wNgKO00Dpsv" &
"6tHp5ZNW3cyk3j71fwAP8mqeFNanlTUW1ePfkahUNkus6Bsx9lsntNO2wT+9/g64mgS/+KDk3P2CCwkF" &
"gU+gjyG1FVhSJ5/br5WaTANTNt3M/zYwLPslghyWVnc+AeYTAf41Ghz0BPY4vxbZ8p7bxzcaVc7sz41c" &
"b2ZJDQ3tktfV9JtpqJMxUJ8IyNm4wpisquL6FudUvUFhtWj/q1leXVtwrFrti1zFGKxZGJqO9Yks3R+c" &
"5xMOCn0Oz6nCMPnd+iHFf24DPs477POOqbmlMducRGeppymi8khH7pT9HAQ6Fg6+nlKaVdlE72Z/GFf/" &
"kQFGy5zRumHEMqsdm+EqTUUYYeBL3P5Q2M9BYGcW9hIBbSDwxQLzuHpmciMDpHrbdzKDk1Wqt7ZwdKlV" &
"bJ/Y2p1BRbsCck9GQeJzkTUEilons/17GMCUjH7H2GDskBhbuINQvDjgVYvrnYpNV8C7A99cSoQ6P5DJ" &
"5w4PKzcadJf2D02dGqdamgqC6buZ3eYeiXS5Urj591yvO5DwtMqaFopEIZqyDQIwknxDqxxGdJJBCVII" &
"RvQKyQvJInvF1G/6B+zHy5A9Qfnn40syEJgObvfEBw2Q3tC3AeXUm/5O+KpO+LSaF1fSdTmVdDgatTmo" &
"/IerZdsDijzDC6Ky3zaQ2w1DSiChSrihUhBf0RNfzr8P50WWcELyGKefklyjm7aEIH8IrNocWPrLzeIr" &
"yXBoI0kxIAVCYcJQqCCkpCekmB9SxA0u4AaAWJcyGcefkH+Jbf7LrYYfb9S7XK/eFQI/GQfLqGzmi4WA" &
"f5HYv1DkD+m9mC+4CO65kMc7l83xS+vySG7fG9+29R7e5RbWJRS1PRR5LAqeVIymsDnAqSLZqULpKYjY" &
"Dyw6mdd7Ikfgm8k7+opzKKVzTyJteyx5012iy23ctluoI5EVcaBGDJUB+JSovIuU3oXyY/myo3kSr5w+" &
"9yyBWxpv/3PO7mTmjoftW2Ipm+4Rt0ZgDkZWh2ejagg0wL1U4w5Tu5f0HylSukHkh0AS1zd9BzIEe1/y" &
"dj9j70xibn1I3xJH3hbV7BqNDMtEVeDeA57Ves+qQY8K7RG4+jBM5VasOAiR7M/r25cl2JPO3fWCtT2Z" &
"sTWRuv0+wTW2LjSzAYF9D3g3GL1RI0frh7xqBj2rNe7lA26lStdi6f580b5cwZ4M7k+pXdue0HckklwT" &
"UKHZTXBnc4Iw/je8yRc3ehwz4t1gOFqn83irPlyhOlQqdy0S7QMJdmV270hl/pT83u1RY1gOuhxLAc5Q" &
"LWcoE7+SzX6tphMto77YYe9GvVed1qO63w0uO1As2g3i73zN+utzqkdyUwQIU4WnAv6sKX/W5MVO63m6" &
"5SzVfLrN5EcY8cUavFFaT6TKrVy6t6h3dw5nbxrN+yk6pgBbS6QBgb0fA4T2az3TV7lTv7Ns/syJ8zTT" &
"mTajH97g26TxqlUegIv3QXiuWfRTqU2JUByaQgduK+dvKT+FyWdDJB+D+2YCeiavcCyXmOO/UY2niQZf" &
"tPowUn4Q2uORS7+Q3phagSd3dQKxhqUY/WK0buGeZj5cNRcmt98QTQXyrVfY5gvtxlPEQa9GlWd5jy+Y" &
"di27Ma+OwBZwgH+aHI9Mjoejn+NHFmMNC1HauXCVPUwyeV1g+TvbdJY6dByv9q3knQOTIsBoOKatT8IH" &
"XkyuPLOtpFiXkz4446WEkYUY/ezdgZkw2eS1HvNvzGE/4sCZStY1MD65FIsiURVKAZA59+X17Jc0+5fU" &
"6ZWnNme59HBsPtYwG6GeDhJP+LOHz+Dll8vpERAMqAbf1kHXqIUAaHE9d2H9zfxaxtyXV/aV51OO5InF" &
"B6Of7unswTLL5S7DhZruG9DWJCi6GtfK5nUODYqAQsdXiOMreGk9Z34tc241bWb5qXUp0fTprm46uHf0" &
"KlkeCKfGQptzqnFEGlUk5owMiQHoyrfi5W8Fn7+CFtfefFp9bV9+ZnXemb2jstzoVAc3cu6WNKcisFUY" &
"IpvL6FfyjcNSoHTlG3T5a+Hn9fyFtZzZ1fRpR5L5U7TGdpOjC8XywhGkx1A07B3+PZ0iErG0asHoiAwo" &
"c6zDPq8XL66B51azph0pY7P3leZwtuY2jh/1lpIExxbUYLGkVh6vQynv1mt7TUYFAF/6Ap1fKbQ7cqyL" &
"qSP2eNnY3Q5leFN3TEVbCgIHfotBE1u6WBSZuEvTzx/Wi81jSqDU/hlinn09aEsWGeMY/ZH4nuga+gM4" &
"4QUCV1yHQxPwDGabUEBXyjk6jdA5ZmK8H0gT6p+xBx7TJAl4Xlw9434lOaUCn1ONRaCwGAKe3tHK51Kl" &
"4k61ijekF5lG5dYPaiChjhmPbE+obHuEaEkpw7yEowuQ6DoMhkRuZjKI/G6KqJehknMGNcLRYekHk8pm" &
"0QDPynAv4NiMSgwEiUGg0A04DKEVS6E0MzsIXA65V0CXS1maAecS0fiowmIemLRq/wW3bwvq0U4DPAAA" &
"AABJRU5ErkJggg=="
Return ConvertBase64ToImage(strImg)
End Function

'*********************************************************************************
' Method : ConvertBase64ToImage
' Purpose : Convert a Base64 String to an Image object.
'*********************************************************************************
Private Function ConvertBase64ToImage(ByVal strImg As String) As Image
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data.
memStream.Close() 'release stream resources.
Return Img 'return image.
End Function

End Class
 Page 757

Black Book Tip # 59


Adding a Horizontal ScrollBar to a ComboBox DropDown List.
In a previous article, Extending VB.NET Controls Functionality, under
the subsection “Sizing the ComboBox DropDown List Width” on page
373, we were shown how to adjust the width of the dropdown list from a
ComboBox to a different, and typically a wider size, in order to display the
whole of its member items. However, this is not always practical because
the list of items might be exceedingly long. As a result, setting it to the maximum width of its widest
member might not be feasible, though setting it wider nonetheless would make it much more
manageable and practical for us and our application users. But even so, in this case our users will still
not be able to see the full text of those members of the dropdown list that exceed its width, unless we are
able to also add a horizontal scroll bar to that dropdown list.
Previously, we also explored how to access the TextBox control that is embedded in a ComboBox, also a
member of the above article in the subsection “Adding a CheckBox to the Edit Field of a ComboBox”
on page 371, where we employed the system’s FindWindowEx Pinvoke (Dim lhWnd As IntPtr =
FindWindowEx(Me.ComboBox1.Handle, IntPtr.Zero, "EDIT", vbNullString) ) to acquire its handle in order to
adjust its margins so that a CheckBox could rest within its left side and not cover displayed text.
To get the handle for the ListBox, its dropdown list, which has a class name of COMBOLBOX (notice
the embedded “L”), is not as easy to grab because it is not a child of the ComboBox control as the
TextBox and the dropdown button are, and so we are not able to use the FindWindowEx function, or even
use EnumChildWindows to get its handle. It is an unbound and separate control, not a member of a
ComboBox, that is just associated with the ComboBox and is simply displayed below it. As such, many
programmers have often jumped through incredible hoops to trap this list control, such as monitoring the
message queue and finding the first message to a ListBox after the dropdown message is intercepted.
That is a lot of work, and it is totally unnecessary, because a Pinvoke exists named GetComboBoxInfo
that will gather all this information. The problem programmers have with this method, however, is that
they have trouble getting the associated structure, COMBOBOXINFO, to even work. So far, I have
found a single issue that is at the root of their trouble, and that is setting its cbSize member to the size of
the structure. Due to the less-than-robust design of the COMBOBOXINFO structure, it causes a Len
instruction to issue an exception error when they declare the structure’s members as Dim or Public,
because of non-common gaps in the natural alignment of members that is not typical of most “approved”
Win32 structures, which is why Len normally has no problems with them. But if they declared them as
Friend, there will be no error reported, but sadly, the length returned is zero. The real problem is the first
field is 4 bytes long, but the second is 16. See the problem? We can fix this in 2 ways. 1) Add 3 dummy
Int32 fields after cbSize, or 2) Use the runtime interop Marshal.SizeOf function to get the proper length
(well, we can get by just setting cbSize to 64; its length with natural alignment space accounted for).
Here is the COMBOBOXINFO structure and the GetComboBoxInfo Pinvoke:
' Used by GetComboBoxInfo() to get ComboBox references
<StructLayout(LayoutKind.Sequential)>
Private Structure COMBOBOXINFO
Friend cbSize As Int32 'size of this structure must be set before using (use Marshal.SizeOf(), or 64).
Friend rcItem As Rectangle 'natural alignment chokes here because we stack a 16-byte structure atop a 4-byte field.
Friend rcButton As Rectangle
Friend stateButton As Int32 'state of the dropdown button.
Friend hwndCombo As IntPtr 'handle of ComboBox.
Friend hwndEdit As IntPtr 'handle of child EditBox (Class name EDIT).
Friend hwndList As IntPtr 'handle of associated (but not child) ListBox (class name COMBOLBOX).
End Structure

' Retrieves information about the specified ComboBox.


Private Declare Function GetComboBoxInfo Lib "user32.DLL" (ByVal hwndCombo As IntPtr,
ByRef pcbi As COMBOBOXINFO) As Boolean
 Page 758

As we can see, in using this structure and Pinvoke, we can not only also get the handle to the EDIT
window, but we can get the handle of the dropdown ListBox as well in the structure’s hwndList member.
We can implement the above by using the following simple code, assuming that the namespace
System.Runtime.InteropServices is also imported to support the structure and the Marshal class:
Dim cboBox As ComboBox = DirectCast(sender, ComboBox) 'get reference to affected ComboBox.
Dim cInfo As COMBOBOXINFO 'storage for combobox info.
cInfo.cbSize = Marshal.SizeOf(cInfo) 'assign structure's length.
If GetComboBoxInfo(cboBox.Handle, cInfo) Then 'if we grabbed combobox info...
'if we captured the structure, do more here...
End If

Now that we have access to the Handle for the dropdown ListBox, how can we use that to add a
horizontal scroll bar to the ListBox? Without much effort really, but we will need three more Pinvokes
and three more constants in order to interrogate the control associated with this acquired handle:
' Changes an attribute of the specified window. The function also sets the
' 32-bit (DWORD) value at the specified offset into the extra window memory.
Private Declare Function SetWindowLong Lib "user32.DLL" Alias "SetWindowLongA" (ByVal hwnd As IntPtr,
ByVal nIndex As Int32,
ByVal dwNewLong As Int32) As Int32
' Retrieves information about the specified window. The function also retrieves
' the 32-bit (DWORD) value at the specified offset into the extra window memory.
Private Declare Function GetWindowLong Lib "user32.DLL" Alias "GetWindowLongA" (ByVal hwnd As IntPtr,
ByVal nIndex As Int32) As Int32
' Sends the specified message to a window or windows. The SendMessage function
' invokes the window procedure for the specified window and does not return
' until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByVal lParam As Int32) As Int32
Private Const LB_SETHORIZONTALEXTENT As Int32 = &H194 'adjust horizontal extent of a ListBox.
Private Const GWL_STYLE As Int32 = -16 'get/set a window style.
Private Const WS_HSCROLL As Int32 = &H100000 'apply a Horizontal Scroll style.

Control enhancements are applied to other controls as Styles, such as Vertical and Horizontal Scrollbars
on a PictureBox, which is what a ListBox really is on its surface (its text is simply painted onto the
PictureBox sureface).
What we need to do is grab the current Style set for the ListBox, referenced by cInfo.hwndList above, and
see if it has a Horizontal ScrollBar Style assigned to it. If not, we will apply that Style.
Dim Styl As Int32 = GetWindowLong(cInfo.hwndList, GWL_STYLE) 'get current ComboBox ListBox style.
If (Styl And WS_HSCROLL) = 0 Then 'if no horizontal ScrollBar exists...
SetWindowLong(cInfo.hwndList, GWL_STYLE, Styl Or WS_HSCROLL) 'apply a horizontal ScrollBar to it.
'we still need to define a maximum horizontal extents so the new horizontal scroll bar will be displayed...
End If

Once we have applied a Horizontal Scrollbar to the ListBox control, we need to increase the horizontal
extent of the ListBox so that the new Horizontal Scrollbar will actually be displayed. We can do this by
measuring each member of the ListBox’s data list and determining the longest member, and finally
setting the horizontal extent of the ListBox scroll bar from that:
Dim maxWidth As Int32 = cboBox.Width 'get the current width of the ComboBox as a base.
For Each itm As String In cboBox.Items 'check each member of the ComboBox....
Dim ln As Int32 = TextRenderer.MeasureText(itm, cboBox.Font).Width 'get the length of each member.
If maxWidth < ln Then 'if the member is longer than the current max...
maxWidth = ln 'then update the max width.
End If
Next
SendMessage(cInfo.hwndList, LB_SETHORIZONTALEXTENT, maxWidth, 0) 'set the horizontal extent of the ListBox.

And that is all there is to it! Consider the module modCboAddHrzScrollbar, listed on the next page. We
just invoke its AddHrzScrollbar method with the selected ComboBox as a parameter to apply a
Horizontal Scrollbar to it.
 Page 759

Option Strict On
Option Explicit On
'-------------------------------------------------------------------------------------
' modCboAddHrzScrollbar Static Module Class
' Add a Horizontal ScrollBar to a ComboBox.
'-------------------------------------------------------------------------------------
Imports System.Runtime.InteropServices
Module modCboAddHrzScrollBar
'-------------------------------------------------------------------------------------
' To add a Horizontal ScrollBar to a ComboBox, simply invoke the GetComboBoxInfo()
' method with the ComboBox you want to add a Horizontal ScrollBar to as a parameter.
'
' This method will set the horizontal extent (the maximum you can scroll to the right
' to) from the longest member in the ComboBox.
'
' NOTE: If you will be adding or removing members from this ComboBox, so that you will
' need to update the extents of the ComboBox, you will want to replace the following line:
' If (Styl And WS_HSCROLL) = 0 Then
'with...
' If True Then
'-------------------------------------------------------------------------------------
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
' Used by GetComboBoxInfo() to get ComboBox references.
<StructLayout(LayoutKind.Sequential)>
Private Structure COMBOBOXINFO
Friend cbSize As Int32 'size of this structure must be set before using (use Marshal.SizeOf())
Friend rcItem As Rectangle
Friend rcButton As Rectangle
Friend stateButton As Int32 'state of the dropdown button.
Friend hwndCombo As IntPtr 'handle of ComboBox.
Friend hwndEdit As IntPtr 'handle of child EditBox (Class name EDIT).
Friend hwndList As IntPtr 'handle of associated (but not child) ListBox (class name COMBOLBOX).
End Structure

' Retrieves information about the specified ComboBox.


Private Declare Function GetComboBoxInfo Lib "user32.DLL" (ByVal hwndCombo As IntPtr,
ByRef pcbi As COMBOBOXINFO) As Boolean
' Changes an attribute of the specified window. The function also sets the
' 32-bit (long) value at the specified offset into the extra window memory.
Private Declare Function SetWindowLong Lib "user32.DLL" Alias "SetWindowLongA" (ByVal hwnd As IntPtr,
ByVal nIndex As Int32,
ByVal dwNewLong As Int32) As Int32
' Retrieves information about the specified window. The function also retrieves
' the 32-bit (DWORD) value at the specified offset into the extra window memory.
Private Declare Function GetWindowLong Lib "user32.DLL" Alias "GetWindowLongA" (ByVal hwnd As IntPtr,
ByVal nIndex As Int32) As Int32
' Sends the specified message to a window or windows. The SendMessage function
' calls the window procedure for the specified window and does not return
' until the window procedure has processed the message.
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As IntPtr,
ByVal wMsg As Int32,
ByVal wParam As Int32,
ByVal lParam As Int32) As Int32
Private Const LB_SETHORIZONTALEXTENT As Int32 = &H194 'adjust horizontal extent of a ListBox.
Private Const GWL_STYLE As Int32 = -16 'get/set a window style.
Private Const WS_HSCROLL As Int32 = &H100000 'apply a Horizontal Scroll style.
')))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

'*******************************************************************************
' Method : AddHrzScrollbar
' Purpose : Add a Horizontal ScrollBar to a ComboBox.
'*******************************************************************************
Friend Sub AddHrzScrollbar(ByRef cboBox As ComboBox)
Dim cInfo As COMBOBOXINFO 'storage for ComboBox info.
cInfo.cbSize = Marshal.SizeOf(cInfo) 'get structure length.
If GetComboBoxInfo(cboBox.Handle, cInfo) Then 'if we grabbed ComboBox info...
'if we captured the structure, do more here...
Dim Styl As Int32 = GetWindowLong(cInfo.hwndList, GWL_STYLE) 'get current ComboBox ListBox style.
If (Styl And WS_HSCROLL) = 0 Then 'if no horizontal ScrollBar exists...
SetWindowLong(cInfo.hwndList, GWL_STYLE, Styl Or WS_HSCROLL) 'apply a horizontal ScrollBar to it.
Dim maxWidth As Int32 = cboBox.Width 'get width of the ComboBox as a base.
For Each itm As String In cboBox.Items 'check each member of the ComboBox....
Dim ln As Int32 = TextRenderer.MeasureText(itm, cboBox.Font).Width 'get the length of each member.
If maxWidth < ln Then 'if the member is longer than the max...
maxWidth = ln 'then update max width.
End If
Next
SendMessage(cInfo.hwndList, LB_SETHORIZONTALEXTENT, maxWidth, 0) 'set horizontal extent of the ListBox.
End If
End If
End Sub
End Module
 Page 760

Black Book Tip # 60


Restoring 3D Curved-Surface Appearances for Buttons and Tabs.
If we have an operating system that came after Windows
Vista, such as Windows 8/8.1 or Windows 10, Button
controls and TabPage tabs are now rendered with a flat
appearance, losing their older ‘3D’ curved-surface quality.
This is part of Microsoft’s strategy to dial back the Desktop
and Laptop imaging technology in order to be more
compatible and more easily blend with the graphically
archaic, more primitive, and much slower “smart-devices”
and “smart-pads” that are not yet smart enough to be
capable of PC-level graphics acceleration without toasting their CPUs. This is severely disappointing to
most developers who do not want to have to wait for so-called smart-device processors to evolve enough
to finally think fast and cool enough to get their 3D effects back onto their PCs, regardless of how
incessantly marketing departments try to convince us that for some ridiculous, babbling like a drool
monkey reason that, somehow, it is we, not manufacturers (who cannot yet deliver cool-running
handheld 3D graphics), who want this dial-back. Even so, disregarding the fact that they take us for
unintelligent, non-reasoning fools, emulating 3D buttons is actually quite easy.
Defining a 3D Image Template
All we really need is to define a 3D curved surface background image that we can
assign to buttons and other controls. Rendering the effect manually using a Paint
event is a bit cumbersome, as we can see in the Paint event listing below, because
it is not simply a matter of drawing the upper half a lighter color than its lower half,
but it also involves an outer border, enhancement lines, and such. For example:
Private Sub Button1_Paint(sender As Object, e As PaintEventArgs) Handles Button1.Paint, Button2.Paint
With DirectCast(sender, Button)
Dim brsh As New SolidBrush(SystemColors.ControlLightLight) 'button edge if not selected.
If .Bounds.Contains(.Parent.PointToClient(Cursor.Position)) OrElse .Focused Then 'if mouse is hovering or selecting...
brsh.Color = Color.FromArgb(8, SystemColors.Highlight) 'button edge will be washed-out highlight.
End If
e.Graphics.FillRectangle(brsh,
New Rectangle(New Point(0, 0), New Size(.Width, .Height))) 'draw border by filling all.
brsh.Color = SystemColors.Control 'color for top half of button.
e.Graphics.FillRectangle(brsh,
New Rectangle(New Point(3, 3), New Size(.Width - 6, .Height \ 2))) 'draw upper half of button.
brsh.Color = SystemColors.ControlLight 'color for lower half (why is this darker?)
e.Graphics.FillRectangle(brsh,
New Rectangle(New Point(3, .Height \ 2 + 1),
New Size(.Width - 6, (.Height - 6) \ 2))) 'draw lower half of button.
Dim pn As New Pen(SystemColors.ControlDark) 'draw darker line near top for beveled look.
e.Graphics.DrawLine(pn, 3, 3, .Width - 6, 3)
If .Focused Then
pn.Color = SystemColors.Highlight 'border highlight if selected...
End If
e.Graphics.DrawRectangle(pn, New Rectangle(0, 0, .Width - 1, .Height - 1)) 'draw border of button.
Dim sz As Size = TextRenderer.MeasureText(.Text, .Font) 'get size of text.
Dim rct As New Rectangle((.Width - sz.Width) \ 2,
(.Height - sz.Height) \ 2, sz.Width, sz.Height) 'compute rect to center text in button.
brsh.Color = .ForeColor 'color to paint the text.
e.Graphics.DrawString(.Text, .Font, brsh, rct.Location) 'center the text in the button.
pn.Dispose() 'Dispose of the pen resource.
brsh.Dispose() 'dispose of the brush resource.
End With
End Sub

This is a lot of effort to do basically a mostly two-tone image with some beauty edging and support for
mouse-over enhancement. Even so, this result is much better than a boring ‘Standard’ Button control.
What we could do to get an easier and faster effect is to employ a simple
image of a 3D curved surface, and just assign it to the button’s background.
Suppose we had the full-sized image shown to the right, named Img3D:
 Page 761

With this image, we could assign it to the button’s BackgroundImage property and also set the button’s
BackGroundImageLayout property to ImageLayout.Stretch, so the image fills the button.
With Me.Button1
.BackgroundImage = Img3D 'assign 3D image to button background.
.BackgroundImageLayout = ImageLayout.Stretch 'make sure it is scaled to the button.
End With

This would be all we would have to do with all our buttons. But the next trick is where to get the image.
Taking advantage of the BuildImageCode method outlined in the Creating Single Base64 Image Data
subsection of Black Book Tip # 49 on page 639, I rendered such an image, which we can grab by
invoking the resulting InitializeImage function and assigning its returned image to local field storage in
our application, usually during a Form_Load event, such as the following:
Dim Img3D As Image = InitializeImage() 'build 3D image. Method defined in the next listing.
With Me.Button1
.BackgroundImage = Img3D 'assign 3D image to button background.
.BackgroundImageLayout = ImageLayout.Stretch 'make sure it is scaled to the button.
End With

Here is the definition of the InitializeImage function:


'*********************************************************************************
' Method: InitializeImage
'*********************************************************************************
Private Function InitializeImage() As Image
Dim strImg As String =
"iVBORw0KGgoAAAANSUhEUgAAAJwAAAAuCAIAAABS2OdkAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" &
"wwAADsMBx2+oZAAAAPxJREFUeF7t0TlOQwEUQ1Hvf1es4DNDGBo6xhAkqjQpvrOE+ClXPp0rW7IuCkf7" &
"wtF/4eivcHoqUE8F0q5weipQTwXSb5A7p3kNV/TUytA2yJ3TvIYrempl9FQg/QS5c5rXcOk7yJ3TvIYr" &
"empl6CtorXSok4meekY+j5zPirw9Yq10qJOJnloZ+igcvRWOXgtHL4Wj58LpqUA9FUhPQe6c5jVc2gS5" &
"c5rXcOkxyJ3TvIZLD0HunOY1XLoPcuc0r+GKnloZPRVIt0HunOY1XLoJcuc0r+HSdZA7p3kNV/TUyuip" &
"QLoKcuc0r+GKnloZuiwc3RWO3gtHS8EsywH82Th4DckCBQAAAABJRU5ErkJggg=="
Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab the Base64 data as a byte array.
Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert the byte array to a memory stream.
Dim Img As Image = Image.FromStream(memStream) 'construct an image from stream data.
memStream.Close() 'release and dispose of the stream resources.
Return Img 'return the image.
End Function

We can even render this image to different-sized targets, such as the tabs on a TabControl. Consider the
following example drawing method used if the TabControl had its DrawMode property set to
TabDrawMode.OwnerDrawFixed (this is the DrawOnTab event code, modified to use the above image,
featured in Black Book Tip # 13 on page 480):
'*********************************************************************************
' Method : DrawOnTab
' Purpose : Owner draw each individual tab on a TabControl.
' : This is actually a TabControl object method, but
' : it is used to render its tabs and their text.
' : Be sure that [TabControl].DrawMode = TabDrawMode.OwnerDrawFixed
'*********************************************************************************
Private Sub DrawOnTab(sender As Object, e As DrawItemEventArgs) Handles TabControl1.DrawItem
Dim TabControl As [TabControl] = DirectCast(sender, TabControl) 'get the tab control being processed.
Dim TabPage As [TabPage] = TabControl.TabPages(e.Index) 'get the tab page being processed.
Dim tabRect As Rectangle = TabControl.GetTabRect(e.Index) 'get the tab rectangle for the current tab.
e.Graphics.DrawRectangle(Pens.Silver, tabRect) 'draw the tab rectangle.

Dim bgBrush As Brush 'store to desired tab background color


Dim txtBrush As Brush 'store to desired font color
Dim txtFont As Font 'store the draw font as bold or normal

If TabControl.SelectedIndex = e.Index Then 'is the current tab to paint also the active tab?
With tabRect 'for line below to work, make sure TabPage.Backcolor is not set to Transparent, or will not seem to work.
Dim tPen As New Pen(TabPage.BackColor, 2.0!) 'create pen to cover bottom border.
e.Graphics.DrawLine(tPen, .X, .Y + .Height + 1, .X + .Width, .Y + .Height + 1) 'draw thick line beneath the tab.
tPen.Dispose() 'dispose of created pen resources.
End With
bgBrush = Brushes.White 'yes, so use White for the tab background...
txtBrush = Brushes.Navy 'and use Navy for its text...
txtFont = New Font(TabPage.Font, FontStyle.Bold) 'then define the Bold font...
e.Graphics.FillRectangle(bgBrush, tabRect) 'now, fill tab with selected background color...
Else
bgBrush = Brushes.LightGray 'otherwise, use a grayed background color...
txtBrush = Brushes.Black 'use our default text color...
txtFont = New Font(TabPage.Font, FontStyle.Regular) 'and ensure tab font is normal (in case it is not).
e.Graphics.DrawImage(Img3D, tabRect) 'render/stretch button in 3D.
 Page 762

End If
Dim txtSize As Size = TextRenderer.MeasureText(TabPage.Text, txtFont) 'get width and height of text to render in pixels.
Dim Y As Int32 = tabRect.Y + (tabRect.Height - txtSize.Height) \ 2 'computer vertical centering start location.
Dim X As Int32 = tabRect.X + (tabRect.Width - txtSize.Width) \ 2 'compute horizontal centering start location.
Dim cntrRect As New Rectangle(New Point(X, Y), txtSize) 'computer new centering rectangle with text.
e.Graphics.DrawString(TabPage.Text, txtFont, txtBrush, CntrRect) 'draw the tab text.
txtFont.Dispose() 'and finally dispose of the created resource.
End Sub

NOTE: For the blending of the selected tab to the background to work, remember to set the BackColor property of each
TabPage to wither White or to the actual background color that we will be using for the selected tab.
 Page 763

Black Book Tip # 61


Allowing a Form’s BackColor Property to Accept Transparency Colors.
This particular issue was first addressed in the much earlier article, Emulating VB6 Image Control
Features Under VB.NET, under the sub-heading Emulating What VB6 Image Controls Do Using a
Borderless Form on page 254. I address it again here because many readers may have skipped over that
article, but this issue is one prevalently discussed on the internet by users wanting to know how to work
around this issue.
By default, if we set a transparency color to
the BackColor property of a form, we will get
an exception error when we run the code.
There are actually two solutions to this
problem. One is documented, but the easy one
is not. However, the provided error message does not even mention the documented solution; it offers
some general advice that is too vague to provide any real help at all toward solving the issue. I am
starting to believe that their “General Help” option is a Microsoft inside joke, because all it does is tell
us that there was an error reported because there was an error.
The documented, though obscure solution is to modify the form’s Style to support transparency colors:
Me.SetStyle(ControlStyles.SupportsTransparentBackColor, True) 'setting True has a minuscule performance hit.

Afterward, we can assign a transparency color to the form’s BackColor property and it will be accepted.
The undocumented solution was one I noodled out in my noggin one day by just thinking about how
transparency can best be handled. The way I figured it, the system would pick up color values and just
compare their Red, Green, and Blue values (RGB), ignoring its Alpha component. If there was an exact
RGB match, then that color would be rendered transparent. As an experiment, I forced the Alpha
component, its transparency value, to 255 (opaque) so that the Form’s BackColor property would be
able to accept it without Style changes. For example, assuming variable Clr is a variable of type Color:
Me.BackColor = Color.FromArgb(&HFF, Clr) 'remove Clr transparency and assign to form's BackColor property.

NOTE: Images, such as Icons and PNG Images with Transparency, have a color, such as Black or Magenta, that is assigned
as its Transparency color. This is typically a normal RGB color, but with its Alpha component set to 0.
This works because the system will strip the Alpha component from BackColor and test the other colors
on its assigned surface for a matching RGB value, and render transparent any that match.
The ControlStyles enumeration is, as its name implies, designed for not just forms, but by controls
(which a form is actually derived from), even though we might notice that general controls do not sport a
SetStyle or GetStyle method, but forms do (as do User-Defined Controls, which the above Control
reference principally implies). This enumeration features many useful flags, as listed below, which I
derived from the MSDN website and added additional notes as needed. Notice that these are treated as
extended behaviors beyond the control’s defaults, though clearly many controls have these flags set or
reset by default to establish their behavioral characteristics. The reason for this is the addition of these
auxiliary behaviors also eats more CPU cycles, yet most controls require only a few of them.
Member Name Description
ContainerControl If True, the control is a container-like control. This can be used to make any control a container.
UserPaint If True, the control paints itself through its Paint event rather than the operating system doing so (default). If
False, the Paint event is not raised. This style only applies to classes derived from Control.
Opaque If True, the control is drawn opaque and its background color is not painted. This is useful if the control’s
background is not normally visible. This can speed processing and reduce flicker if there are a lot of these.
ResizeRedraw If True, the control is redrawn when it is resized. Normally, the control waits for normal intervals to resize, or
when resizing has completed before being redrawn. Doing a lot of forced image resizing introduces flicker.
FixedWidth If true, the control has a fixed width when auto-scaled. For example, if a layout operation attempts to rescale
the control to accommodate a new Font, the control's Width remains unchanged.
 Page 764

Member Name Description


FixedHeight If True, the control has a fixed height when auto-scaled. For example, if a layout operation attempts to
rescale the control to accommodate a new Font, the control's Height remains unchanged.
StandardClick If True, the control implements the standard Click behavior. This is its default behavior.
Selectable If True, the control can receive focus.
UserMouse If True, the control does its own mouse processing, and mouse events are not handled by the operating
system.
SupportsTransparentBackColor If True, the control accepts a BackColor with an alpha component of less than 255 to simulate transparency.
Transparency will be simulated only if the UserPaint bit is set to True (default) and the parent control is
derived from Control.
StandardDoubleClick If True, the control implements the standard DoubleClick behavior. This style is ignored if the
StandardClick bit is not set to True.
AllPaintingInWmPaint If True, the control ignores the window message WM_ERASEBKGND (erase/repaint background of control),
and a form’s OnPaintBackground and OnPaint events, if implemented, will be invoked directly from the
system’s WM_PAINT event processing for the control, to reduce flicker. This style should only be applied if
the UserPaint bit is also set to True.
CacheText If True, the control keeps a copy of its Text property rather than getting it from the Handle each time it is
needed. This style defaults to False. Setting it to True improves performance, but makes it difficult to keep
the text synchronized, though under most situations this might not ever be an issue, especially because we
seldom access a control’s Text property while concurrently altering it in some obscure background process.
EnableNotifyMessage If True, the OnNotifyMessage method is called for every message sent to the control's WndProc. This style
defaults to False. EnableNotifyMessage does not work in partial trust.
DoubleBuffer If True, drawing is performed in a buffer, and after it completes, the result is output to the screen. Double-
buffering prevents flicker caused by the redrawing of the control. If you set DoubleBuffer to True, you should
also ensure that both UserPaint and AllPaintingInWmPaint are also set to True.
OptimizedDoubleBuffer If True, the control is first drawn to a buffer rather than directly to the screen, which can reduce flicker. If you
set this property to True, you should also set the AllPaintingInWmPaint to True.
UseTextForAccessibility Specifies that the value of the control's Text property, if set, determines the control's default Active
Accessibility name and shortcut key.

Notice the DoubleBuffer style. Many VB6 developers moving to VB.NET have wailed over the “loss”
of this DoubleBuffer property in PictureBoxes, even though .NET PictureBoxes seldom need this
feature. This property was not actually lost, but was just moved to the form as its DoubleBuffered
property in order to support a wider field by being assigned to the form upon which these controls are
painted, so that other controls not previously supported can also take advantage of this feature. Thus,
they should simply set the form’s DoubleBuffered property to True to get the exact same effect as they
had under VB6. Note further that, apart from setting the property to the form causes the process to
consume far fewer resources because multiple PictureBox controls no longer need to maintain individual
DoubleBuffer caches, setting this Style also requires that the UserPaint (default = True) and
AllPaintingInWmPaint (default = False) Styles also be set to True. Note that setting the form’s
exposed DoubleBuffered property ensures that all these things are set for us in just one property.
The UserPaint Style, when set to True (default), allows us to provide painting services. Normally, the
operating system paints the control, except when we intercept it by providing our own Paint event for it
(if we want the OS to continue to provide default painting services for features we do not want to deal
with, we should first issue MyBase.Paint at the top of our Paint event code, though at the end if we are
using and in a OnPaint event, to ensure a Paint event fires afterward; either ours and/or the system's).
If set to False, any Paint event, even if we wrote a Paint method to support the event, is not raised and
the operating system, via the control’s base class, will solely handle painting services.
The AllPaintingInWmPaint Style, when set to True, will cause the background of the control not to be
cleared during a repaint of the control. This is useful if the background will not be seen, but will be
completely covered by whatever the control is drawing over the top of itself, such as an image that fills a
PictureBox. This can be useful on a form with a lot of such controls, greatly reducing or eliminating any
refresh flicker that might normally occur. This is similar to the Opaque Style, except that the Opaque
Style, apart from also not drawing its background, will render all its data fully opaque, even if it contains
transparency colors, by rendering all of its color’s Alpha components to 255; fully Opaque.
NOTE: We can use the GetStyle method to check a Style bit for being set or reset for a property.
 Page 765

Closing Remarks
The Visual Basic editor includes additional features, such as Snaplines, allowing easy alignment of form
controls, symbolic rename, which intelligently renames all occurrences of an identifier (for example, a
variable), Control Anchoring, which frees developers from writing code to deal with form resizing,
allowing controls on a form to be visually anchored so that they remain a fixed length from the edge of
the form and resize whenever the form resizes with no effort on the part of the developer, and built-in
support for connecting forms to XML services, plus, as before, ActiveX controls support.
Also available in VB.NET are code snippets, which are blocks of code for common tasks that we can insert
into the code editor from the context menu. Visual Studio includes quite a number of predefined snippets for
common tasks, and we can define our own snippets. Microsoft has also released snippet editors since
VB2005, relieving us of having to juggle complicated syntax and brackets, making editing snippets a breeze.
Their latest snippet editor is a Multi-Visual Studios version that can work with several different versions of
Visual Studio. It is free, available at https://visualstudiogallery.msdn.microsoft.com/B08B0375-139E-41D7-AF9B-FAEE50F68392. It even
works with the Visual Basic Express editions.
As I have mentioned elsewhere within this manual, I tend to use the DirectCast directive, instead of the
more common CType directive, a LOT. I use it everywhere where I know exactly what the generic
object being provided to me actually is. DirectCast is a whole lot faster than CType because DirectCast
does not actually generate even one byte of compiled code, but instead informs the compiler that it is to
treat this generic object (which internally does in fact store the object’s actual type and which we can
access through the object’s GetType property) as the type I provide it. Granted, if we have optimization
on, the CType directive can treat the process just like a DirectCast directive in that, finding that the
object is of the provided type, it will simply do the same as DirectCast, and which Microsoft says is
safer, though for me it is a pointless waste of compile time if I know exactly what type the object is.
Besides, from my perspective, if we do make a wrong assumption about the type when using DirectCast,
an error event is automatically triggered. This is immensely helpful in debugging that wrong assumption
within the code. So I see the DirectCast directive as not only a faster and optimal command, but one that
is a useful tool that helps me beef up the robustness of my code.
Please, do not set Option Strict to Off, Option Explicit to Off, Option Infer to On, or worse, lead any
complex code that should be parsed, with “On Error Resume Next”. This is a dated, indeed ancient technique
that has no place in modern code. I get a lot of flak (read as crap) from amateur and hobbyist coders
who claim this is OK, and who harrumph and whine that they are software professionals (if I hear one
more hobbyist claiming they are a software engineer because they can get MsgBox(“Hello, World!”)
to run, my head is going to pop!). No, they are not. Code should always have strict type checking
enforced, code should always provide explicit type definitions, code should never allow the compiler to
have to make any assumptions whatsoever about types, and code should always provide explicit error
trapping. Anything less is “pretend” wanna-be code. If we have code that does not care about the result
of an operation, and there are actually a number of situations where this is the required case, instead of
using the archaic, dinosaur-trodden “On Error Resume Next”, which should always be followed by “On Error
Goto 0” behind the ignorable code, instead use the Try...Catch...End Try structure, where the ignorable
code rests between Try and Catch, the Catch definition is blank and is in fact simply the command
Catch, and Catch is immediately followed by End Try:
If Value Is Nothing Then 'if the Value is set to Nothing...
Try
gAppBaseSettings.DeleteValue(Key) 'then simply delete the registry key. This key might not exist, so ignore error.
Catch 'this, with the Try block above, emulates On Error Resume Next.
End Try 'this emulates On Error Goto 0.
Else
gAppBaseSettings.SetValue(Key, Value) 'otherwise, Value contains data, so save it to the registry
End If
 Page 766

For more detailed help, open up either the MSDN Library for Visual Studio or Microsoft Visual Studio
Documentation and browse to the path Development Tools and Languages \ Visual Studio \ Visual
Basic \ Help for VB6 Users, and we will find a vast reservoir of help in converting VB6 code to .NET.
The optional Visual Basic Power Packs controls, which ships on VB2010 and after, include LineShape,
OvalShape, and RectangleShape controls that can be used to replace the VB6 Line and Shape controls.
In addition to duplicating the behavior of the VB6 Line and Shape controls, these controls add many
new capabilities. These include gradient fills, run-time selection, and even run time events (the Visual
Basic Power Packs are available online for free, without subscription restrictions, from The Microsoft
Developer Network (MSDN) at http://msdn.microsoft.com/en-us/vbasic/aa701257.aspx)..
To see how Pinvokes to the system are defined and used in .NET, and for excellent help in the
VB.NET and C# implementation of Pinvoke formats, visit Redgate Software’s Pinvoke.net,
www.pinvoke.net. This site is a massive knowledge-base of .NET Pinvoke information, providing
examples for VB.NET and C#. This is an interactive, user-supported site. If we have a Pinvoke
that is not yet documented, or is not presented for our development language, such as VB.NET,
here is our chance to contribute our knowledge to the hive.
Another place to get great upgrade information is VB Migration Partner. Although products and
services are sold here, it also features a wealth of freely available information (www.vbmigration.com).
I also recommend that we download the free (and massive) VB6 to VB.NET upgrade guide e-book,
“Upgrading Visual Basic 6.0 to Visual Basic .NET and Visual Basic 2005” from Microsoft Developer
Network (MSDN) at http://msdn.microsoft.com/en-us/library/aa480541.aspx. Pay particular attention to Chapter 7
through 9. We can also download the “Visual Basic 6.0 Upgrade Assessment Tool” from here, along
with before and after sample files. This guide was developed jointly by the Microsoft patterns &
practices team and ArtinSoft (www.artinsoft.com), a company with vast experience in Visual Basic upgrades
and the developer of the Visual Basic Upgrade Wizard and the Visual Basic Upgrade Wizard
Companion. This guide provides valuable information for organizations who are considering upgrading
their VB6-based applications and components to VB.NET. It provides proven practices to reach
functional equivalence with a minimal amount of effort and cost, as well as guidance for common
advancements after the application is running on the .NET framework.
Another huge e-book from Microsoft is “Upgrading Microsoft Visual Basic 6.0 to Microsoft Visual
Basic .NET” at http://msdn.microsoft.com/en-us/vbrun/ms788236.aspx. It is the complete technical guide to
upgrading Visual Basic 6 applications to Visual Basic .NET, covering all upgrade topics from APIs to
ZOrders. It shows how to fix upgrade issues with forms, language, data access, and COM+ Services, and
how to upgrade applications with XML Web services, ADO.NET, and .NET remoting. It also provides
big-picture architectural advice, a reference of function and object model changes, and hundreds of
before-and-after code samples.
Still another free Microsoft e-book is “Introduction to Microsoft Visual Basic 2005 for Developers”,
http://msdn.microsoft.com/en-us/vbasic/ms788235. Even though it is designed around VB2005, it is worth its
weight in gold. VB2005 is a giant step forward from earlier editions of VB.NET, and is in my view
almost VB2008, which is the starting platform we need to get really serious in VB development. If we
currently work with Visual Basic 6, these authors fully understand the adoption and code migration
issues we will encounter. They will step us through a quick primer on .NET Framework programming,
offering guidance for a productive transition. If we already work with .NET, we will jump directly into
what's new, learning how to extend our existing skills. From the innovations in rapid application
development, debugging, and deployment, to new data access, desktop, and Web programming
capabilities, we get the insights and code walkthroughs we need to be productive right away.
 Page 767

Though some of the information provided in the above Microsoft books are clearly dated, sometimes
offering solutions using older, more convoluted command paths than those offered by VB2008 and later
(often thanks to the “My” namespace), those older solutions are still valid.
If we have youngsters, or even a non-programmer friend who wants to learn how to develop their own
applications and games, a fantastic and easy avenue is Microsoft’s Small Basic, a free, easy pre-Visual
Basic development environment and compiler. It is immediately accessible, and just begs us to try
programming in it. Small Basic combines a friendly environment with a very simple language and a rich
and engaging set of libraries to make programs and games pop. In a matter of a few lines of code, a
beginner programmer will be well on their way to creating their very own game. They can even share
their programs with their friends; let them import their published programs and run them on their own
computers. Be sure to visit http://msdn.microsoft.com/en-us/beginner/ff384126.aspx.
Special thanks goes out to all the dedicated amateur programmers, developers, and software engineers
who have poked, prodded, and turned the VB.NET code upside down to stumble past sometimes
inadequate documentation in order to figure out how to do many of the things they wanted to do in
VB.NET. Between their often anonymous posts on blogs and support sites, their pushing Microsoft for
more functionality, and their drive to make VB.NET a powerful RAD platform that surpasses my much-
beloved C++, I humbly thank you for your unfettered and generous giving and sharing of knowledge,
the fruit of which makes this document more complete and more useful to those who are now struggling
to master the VB.NET platform.
David Ross Goben
Happy Computing!
 Page 768

About the Author


David Ross Goben is an independent researcher who is obsessive about details,
a professional software engineer, and author, living his life in glorious
anonymity. As a software engineer, he has been expected to think entirely out
of the box and use intuitive perception to develop solutions to problems that
were often assumed impossible. He says that he has been designing software
solutions since dinosaurs walked the Earth, being a systems designer, language
designer, and a compulsive developer (oh, and he has also painted houses,
built them, and moved people's furniture across the country). He has written professional code in
FORTRAN, C, C++, VB, Forth, COBOL, Pascal, various assembler languages, and others he wants to
forget, and some he has successfully forgotten.
Of Jewish descent, he has extensively explored Biblical history, ancient cultural thinking, and ancient
slang for over three decades, which had resulted in his seminal work: A Gnostic Cycle: Exploring the
Origin of Christianity. He has written numerous books, manuals, and magazine articles, many not
credited, or authored under pen names.
His interests include the bio-mechanical origins of life, analyzing patterns that are indicative of
intelligent construction, Climate Change Myths, The Electric Universe, Expansion Tectonics, Electron-
Positron Lattice Technology, Quartz Technology, the real truth of history, exploring the ancient practice
of Dream Walking, exploring the glaring flaws in current Darwinian theory, Mendelian Inheritance,
Cosmology, Quantum Physics, Particle Physics, Astrophysics, and Nuclear Physics. His goal is to
become as close as he can be to a Universal Scholar.
David Ross Goben
Kissimmee, FL, USA
david.ross.goben@gmail.com
 Page 769

Free Online PDF Documents Available by David Ross Goben

Four ready-to-read PDF documents are available for both online viewing and free downloading from
my public Google Docs folder. They are listed below and on the next few pages.
Open Letters Sent to Advocates for the Electric Universe and Expansion Tectonics
Theories, and Exploring How These Theories Connect (155 pages). Since their initial proposals,
gathered evidence has only strengthened the Electric Universe and the Expansion Tectonics
theories. In spite of this, each time additional evidence for either of them surfaces, or another of a
fast-growing body of scientists, especially those of important and augustly respected note, dares to
declare favor for one or the other, select advocates for presumed ‘standard’ theories pop up, like
South African meerkats alerted to an impending threat, and respond with typically scripted salvos
of protest, too frequently droning the same old and practiced slogan-laced retorts like Gregorian
Chants, as if their need to deny the public’s access to, or worse, their acceptance of such ideas
was a matter of personal or professional survival. And it may be no wonder. In these last few
decades, tenures, funding, and reputations have often been decided wholly upon which ideas, real
or imagined, are accepted by the general public. We will compare the evidence between the
Electric Universe and the Gravity-Based Universe, and between Expansion Tectonics and Plate
Tectonics. We will also explore the long-held Prime Matter (Aether) theory that can strengthen the
liquefacting sand upon which Particle Physics now finds itself, and which also strongly links the
Electric Universe with Expansion Tectonics and makes these two models all the more plausible.
So, prepare yourself for a Gnostic rollercoaster ride through an extremely thick ocean of
information and history, a great deal of which had in the past often been intentionally obscured.

View and download this PDF document for free at:


https://drive.google.com/file/d/1TWV3pNtp3-5l4tSodlysv5L6V80M9_44

Also available for free on Scribd.com, SlideShare.com, and through web searches.
 Page 770

Enhancing Visual Basic .NET Applications Far Beyond the Scope of Visual Basic 6.0 (742
pages). 10th Edition. Transitioning from Microsoft Visual Basic 6.0 (VB6) to Microsoft Visual Basic .NET
(VB.NET), at first glance, may look to be an intimidating endeavor. After all, you have likely heard
or read through copious magazines and blogs that there are huge differences between these two
developmental platforms. Though some of those differences are real, most others are simply
imagined, engendered by nothing more than unapprised conjecture. Of the real platform
deviations, most are simply due to them having to be expressed differently; plainly because VB.NET
strictly follows a stringent pattern of uniform language syntax, which is something VB6 was not
always good at. Hence, a programming language feature may have to be implemented under
VB.NET using a radically different access conduit than the way it may have been realized under
VB6. Other disparities, some seen as much more profound, actually end up being VB6 features that
VB.NET does in fact support, but, again, due to tight .NET platform architectural specifications,
VB.NET cannot support them in a like manner, but may by necessity have to utilize non-VB6-style
invocation rules. Nevertheless, by employing some simple user-defined helper functions, such as
will be demonstrated throughout this document, you can easily emulate “lost” VB6 commands, or,
in most cases, make their functionality more accessible through simpler syntax. Regardless, you
will find that, overall, VB.NET supports all these many differences, both major and minor, in but
different forms, and in all it also implements much more robust techniques to apply their
functionality. Many of the “major” differences bemoaned by many VB6 purists no longer exist;
having existed only in Beta releases of VB.NET, but being addressed by the time of the initial
product launch, or, in more complex cases, in later releases. The biggest problem here is that most new VB.NET developers still approach
problems the same way as they may have faced them under VB6, or had been trained by VB6-savy developers. This book helps you to not
only break yourself free of that mold, but to really make your VB.NET application shine by example after example of how to make what was
thought to be difficult or even impossible to be in fact very easy, and it also provides you with a toolkit you can build yourself, and customized
to your needs so that you can really make your applications stand out from the rest of the pack.

View and download this PDF document for free at:


https://drive.google.com/file/d/1Iti2vRdoeywVeLb9INE-TTh6qVWyLdlg

Source code is available in a RTF document, Code Excerpts for Enhancing Visual Basic .NET, at:
https://drive.google.com/file/d/1_9-FnqGRe8qpcSsHxc-WzQCrQkG0hk-h
 Page 771

Doom 3 Walkthrough and Strategy Guide (554 pages). This is an enhanced novelized exploration
into the dark horrors of the 2004 Doom 3 adventure, perhaps one of the scariest and most densely
detailed first-person-shooter escapades ever conceived, for both the original offering from Id
Software and their later Doom 3 BFG Edition. This walkthrough takes you through the Doom 3
adventure at the Veteran Difficultly level, describing in intimate detail this quest and the strategies
required to both beat this game easily and to explore areas and find treasures that you may have
never before thought existed. Some of the most arduous battles that gamers often wail and gnash
their teeth about can sometimes be won by using some of the simplest solutions imaginable, all
without cheat codes, but with quick thinking and actual military techniques. For example, many
players dread facing off with the six maggots (tall man-like demons with razor-sharp talons, two heads,
and panther-like ferocity) at the end of the Alpha Labs – Sector 1 level because they always seem
to come to great harm unless, through sometimes pure luck or random fortune, they can hole up in
the left far corner of that locked room and hold the massing maggot hoard at bay long enough to
take them all down in a blazing torrent of voluble gunfire, but did you know that it is actually stupidly
easy to defeat them pain-free, and on top of that you can do it in perfect safety and at your leisure?
Full descriptions of items, adversaries, strengths, weaknesses, locations, secrets and caches not
mentioned in any other guide, a dense listing of useful console command codes, and custom
modifications are covered, such as how to play at your monitor’s maximum resolution, add and/or
alter custom keyboard commands and toggles, and how to easily play Doom 3 BFG Edition on
otherwise uncooperative Windows 8.1 systems (not an issue under Windows 10), offering two fast
and very easy solutions that have worked on every system they have ever been tried.

View and download this PDF document for free at:


https://drive.google.com/file/d/1FmksQM2JNmiijpeGw3E6y-gNOJJO8-3o
 Page 772

Getting Fit After 40 - A Practical No-Nonsense Guide. “A Fitness Guidebook for Adults
Over 40, Yet Useful for Adults of Any Age, to Quickly Getting Fit, Lean, and Losing ‘Impossible-
To-Lose’ Fat.” (167 pages). This book explores exercise and diet facts and fictions, and
also the types of exercises and foods needed to EASILY defeat the “midriff bulge” most
people get as they grow older and more sedentary. After age 24, they might notice fat
slowly accumulating around their belly, hips, thighs and butt. This is largely due to steadily
diminishing Testosterone levels. Apart from making a man a man, both men AND women
employ it to maintain muscle and break down body fat to release its lipids into the
bloodstream, which the body’s cells then absorb for energy. As Testosterone slowly
dwindles, our ability to build and repair muscle and to burn fat likewise dwindles. By age
40, when our HGH, Human Growth Hormone (our “Fountain of Youth” hormone), begins a
slow decline as well, this can make hiding that hard-to-lose fat in time utterly impossible.

This book explains how we can EASILY restore diminishing Testosterone, HGH and many
other hormones to peak levels and quickly lose excess body fat naturally, and end up
looking, feeling, and PHYSICALLY BEING much younger, using just short exercises and a
healthy diet, extending our lives and getting the lean body we had, or should have had in
our 20s! This is based on actual scientific research and PROVEN results, not celebrity
opinions, rumors, or “fad” diets!

Instead of needing to spend endless hours at the gym, you can devote but 1/2-hour each
day, 4 or 5 days a week during a 12-week period following an inexpensive third-party
exercise and diet program ($39.95 to $97), with no equipment except for a pair of
dumbbells, in the privacy of your own home!

You will lose actual fat, not muscle-weight, as you normally lose during reduced-calorie or low-fat diets. Reduced-calorie and low-fat “fad” diets
actually force your body to stop burning fat and will in fact add more body fat, so your body is instead forced to burn off muscle for fuel! This
document will also explain exactly why this happens and how to correct it.

You will lose all this fat-weight without “miracle” supplements, “wonder” gizmos, expensive pills or therapies, without going on a low-fat diet,
without drinking tiny shakes, and without having to eat what looks like meals for hamsters. You will do it through simple and short anaerobic
resistance exercises and a more sober and educated understanding of your daily diet and how it and exercise actually acts upon your
physiology. You will learn how to properly fuel your body and your metabolism, which will give you the day-and-night-running fast metabolism
you may have previously just envied in others, allowing you once more to enjoy steaks, pizzas, hamburgers, and even cheesecake, as most of
us could do guilt-free in our 20s. You will in fact burn it off before it can get to your waist. And, by naturally elevating our Insulin and
carbohydrate sensitivities, the sugars and other carbs we consume will once again properly fuel and maintain muscle, as they should, instead
of being directed to our fat cells!

This book is free to everyone, and you are free to post it online and you can distribute free copies or links to it as you choose. It is stored as a
PDF file to minimize its size, so adding it as an email attachment will not take much time to upload or download. You can also used a PDF
version.

You can also download a PDF version, and you are free to link or distribute that, from:
https://drive.google.com/file/d/1HxhSddeBTOnmll68lO-nlksOxD3o-vdC
 Page 773

Also Available From the Author

A Gnostic Cycle: Exploring the Origin of Christianity (712 pages). In this book, you are
going to explore the bare-boned facts behind a broad range of Biblical mysteries, digging
deep beneath the thick layers of misunderstood traditions, semantic misconceptions,
invented religious and political dogma, and breath-taking, world-changing events in
history that had been willfully suppressed or misrepresented. Like any really good who-
done-it, you will read tales of sinister deception, glorious victory, hateful murder, intimate
love, rampaging armies, wholesale genocide, spiritual salvation, and, as you would
anticipate in such mysteries, the heroes and villains are not always who they seem to be.
Listed below is a short list of the hundreds of things you will herein unearth:

 The Hebrew Bible was written to build self-esteem, not to record concise history.
 King David was based upon Pharaoh Thutmosis III.
 King Solomon was based upon Pharaoh Amenhotep III.
 Moses was based upon Pharaoh Akhenaten (Amenhotep IV).
 Joshua (Ye-ho-shua) was based upon Pharaoh Tutankhamun (Amenhotep V).
 Joshua was the son of Moses, not of Nun, and lived only until he was 19, not 110.
 Jesus was named for Joshua, because it was believed he was his Second Coming.
 Jesus was a dynastic king; a direct descendant of the Scythian Anointed Kings.
 Jesus married Mary Magdalene, a Royal Princess, and their offspring live today.
 The Apostle John Mark was actually Mary Magdalene; the Beloved Disciple.
 Simon Zealot was of the Magi, and was one of the most brilliant men of all time.
 Lazarus raised Jesus from the grave after his crucifixion.
 Judas Iscariot was hung on a tree (ancient slang for a crucifix), not from a rope.
 Mary Magdalene was the first Pope of the Church Jesus personally established.
 Christianity and Judaism are polytheists, worshipping to this day multiple Gods.
 The Holy Spirit is the Jewish Shekinah (Presence of God); the Mother Goddess.
 The Father in Heaven (Hebrew Adon) and Yahveh are two competing deities, from ancient Mesopotamia, and are separate to this day in the
Hebrew Bible.
 Women are not half the value, but are in fact twice the value of a man.
 The Jerusalem Church was actually located in Qumrân, by the Dead Sea.
 Original Christian doctrine comes from Chapter 6 of Numbers, from the Hebrew Bible; rules governing the Qumrân Nazarite (pronounced
“Nazareth”) Order.
 The Apostolic Church has tried for centuries to exterminate Jesus’ Family Line.
 True royal blood has different genetic markers than the common population.
 The Eucharist is an ancient ceremony, using bread mixed with white powder gold.
 The reason we think non-precious metals like gold and silver valuable is spiritual.
 The Antichrist was born 8 years after Jesus, yet his sway is more powerful today.

This 712-pafe book is available from your favorite book seller, or directly from Authorhouse or Authorhouse UK
Ltd., for either $17.50USD or for £13.90UK:
www.authorhouse.com/Bookstore/ItemDetail.aspx?bookid=33204 (ph: 1-888-519-5121)
www.authorhouse.co.uk/Bookstore/ItemDetail.aspx?bookid=33204 (ph: 0800-1974150)
630-page free PDF download (complete, but compressed) is available at
https://drive.google.com/file/d/1flgr95WRtF6KW6qJGf_XXyvbYSHPcUvy
 Page 774

Recent Important Free PDF Public Posts on my Google Drive:

Exploring the Mark of Cain Connection to the Mark of the Beast:


https://drive.google.com/file/d/0B_Dj_dKazINlMUMzaExfdzFmeDg

What Separates Man from Angels and Demons:


https://drive.google.com/file/d/1QS_kBTOgdl8FdfIcrpmIVt3ScYlGq_VP

Gobalist Symbolism:
https://drive.google.com/file/d/0B_Dj_dKazINlY2ZYb3NVdnlLNDQ

Exploring Ancient Christian Mystery Traditions:


https://drive.google.com/file/d/1iIUMrhhRLpdOorgbsV343DGCsUp6a70P

You might also like