Enhancing Visual Basic .NET Beyond The Scope of Visual Basic 6.0
Enhancing Visual Basic .NET Beyond The Scope of Visual Basic 6.0
11 Edition
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 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
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
Foreword and
Introduction.
Getting to Know VB.NET and What Object-Oriented Programming Is All About.
Page 13
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
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;
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 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 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.
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
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 .
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.
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
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.
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
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.
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
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") ”.
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.
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.
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
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.
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”.
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
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!
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
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
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
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
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.
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.
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!!!”
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
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.
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
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.
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.
NOTE: See "Notes on Class Construction and Destruction – a Detailed Look" on page 117 for more details on New,
Finalize, and Dispose.
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
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.
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.
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).
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
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
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
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
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.
The preceding can easily be adapted to the following Try…End Try block:
Page 72
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
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 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
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
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
'-------------------------------
' 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))
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.
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.
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).
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
'******************************************************************
'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
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.
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
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!")
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
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.
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
'*******************************************************************************
' 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
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).
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
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.
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.
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
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.
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.
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.
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
'******************************************************************
' 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
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
Or modify the image of an existing menu item by declaring something like this:
Me.mnuFileOpen.Image = My.Resources.icnOpen.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
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.
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.
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.
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
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.
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.
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.
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
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
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 ‘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#.
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.
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
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 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.
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.
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 ”.
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
' 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.
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
<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.
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
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
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
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
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
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
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
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)
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)).
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.
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.
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
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.
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
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.
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
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
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 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
'--------------------------------------------------------------------------------------
' After upgrade to VB.NET...
'--------------------------------------------------------------------------------------
Private Structure MyType
Dim a As Short
End Structure
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
vbObject 9 Object.
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
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
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
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.
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
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
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.
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
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.
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
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
' 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
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.
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
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.
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
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
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.
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.
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
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.
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
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
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
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
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...?”
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
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
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'.
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
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
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
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
' 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
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.
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
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
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
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())
'*******************************************************
' 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
'*******************************************************
' 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
'*******************************************************
' 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
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
' 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:
' 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
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.
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
'*********************************************************************************
' 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
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
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
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
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
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
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
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.
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!
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
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
When we run this, by appearances we will see the text displayed exactly where
we had placed it on the form during development.
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
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.
'---------------------------------------------------------------------------
'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
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
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.
'*******************************************************************************
' 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
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.
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
'*******************************************************************************
'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
'*******************************************************************************
'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
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
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
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
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!
'*******************************************************************************
' 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
'*******************************************************************************
' 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
'*******************************************************************************
' 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
'*******************************************************************************
' 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
'*******************************************************************************
' 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
'*******************************************************************************
' 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.
'*******************************************************************************
'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
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
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
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.
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
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
' 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
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
End Class
Page 272
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
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.
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
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
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
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")
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
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
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?
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
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 Paste_Click() 'I know you were looking here to see if I had something snappy to say...
MsgBox("Paste")
End Sub
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
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
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!
.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 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
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
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.
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.
'*******************************************************************************
' 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.
'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
'*******************************************************************************
' 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.
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
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.
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
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.
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
bResize = True 'block the resize event that will occur if we resize the form below.
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
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
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.
' 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
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
'-------------------------------------------------------------------------------------
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)
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
'*************************************************************************************
'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.
'*************************************************************************************
' 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.
' 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
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.
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.
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)
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.
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.
'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!
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
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
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
'******************************************************************************
'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().
'*************************************************************************************************************
'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
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...
'******************************************************************************
' 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
'******************************************************************************
' 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)..
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
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
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.
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.
'**************************************************************************************
' GDI32 - GDI Support for .NET
'**************************************************************************************
'Hatch Styles
Public Enum HatchStyle As Integer
HS_HORIZONTAL = 0 '----- A horizontal hatch.
Page 350
'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
'*****************************************************************************************************************************
'-----------------------------------------------------------------------------------------------------------------------------
' 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
'-----------------------------------------------------------------------------------------------------------------------------
' 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
End Property
'-----------------------------------------------------------------------------------------------------------------------------
' 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
'-----------------------------------------------------------------------------------------------------------------------------
' 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
'-----------------------------------------------------------------------------------------------------------------------------
' 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
'**************************************************************************************
' 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
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.
'**************************************************************************************
' 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
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
Private Const CB_SHOWDROPDOWN = &H14F 'Message code used to display or hide the dropdown list of a ComboBox
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.
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.
'******************************************************************************
' 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 *.*
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
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.
'******************************************************************************
' 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 *.*
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
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
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.
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
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
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.
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
'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
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.
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
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.
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.
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.
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).
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.
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
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.
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.
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.
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
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.
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
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
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.
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 “<” to represent an
intentional “<”,“>” for an intentional “>”, “ ” (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
" " " quotation mark (?)
' ' ' apostrophe (’)
& & & ampersand (&)
< < < less-than (<)
> > > 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(" ", " ").Replace(""", """").Replace("'",
"'"), "<[^>]*>", "").Replace("<", "<").Replace(">", ">").Replace("&", "&").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 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("¡", "¡").Replace("¢", "¢").Replace("£", "£").Replace("¤",
"¤").Replace("¥", "¥").Replace("¦", "¦").Replace("§", "§").Replace("¨",
"¨").Replace("©", "©").Replace("ª", "ª").Replace("«", "«").Replace("¬",
"¬").Replace("­", "-").Replace("®", "®").Replace("¯", "¯").Replace("°",
"°").Replace("±", "±").Replace("²", "²").Replace("³", "³").Replace("´",
"´").Replace("µ", "µ").Replace("¶", "¶").Replace("·", "•").Replace("¸",
"¸").Replace("¹", "¹").Replace("º", "º").Replace("»", "»").Replace("¼",
"¼").Replace("½", "½").Replace("¾", "¾").Replace("¿", "¿").Replace("×",
"×").Replace("÷", "÷")
' replace ISO 8859-1 characters. Note that any matches will make the text 8-bit
Sb.Replace("À", "À").Replace("Á", "Á").Replace("Â", "Â").Replace("Ã", "Ã").Replace("Ä",
"Ä").Replace("Å", "Å").Replace("Æ", "Æ").Replace("Ç", "Ç").Replace("È",
"È").Replace("É", "É").Replace("Ê", "Ê").Replace("Ë", "Ë").Replace("Ì",
"Ì").Replace("Í", "Í").Replace("Î", "Î").Replace("Ï", "Ï").Replace("Ð",
"Ð").Replace("Ñ", "Ñ").Replace("Ò", "Ò").Replace("Ó", "Ó").Replace("Ô",
"Ô").Replace("Õ", "Õ").Replace("Ö", "Ö").Replace("Ø", "Ø").Replace("Ù",
"Ù").Replace("Ú", "Ú").Replace("Û", "Û").Replace("Ü", "Ü").Replace("Ý",
"Ý").Replace("Þ", "Þ").Replace("ß", "ß").Replace("à", "à").Replace("á",
"á").Replace("â", "â").Replace("ã", "ã").Replace("ä", "ä").Replace("å",
"å").Replace("æ", "æ").Replace("ç", "ç").Replace("è", "è").Replace("é",
"é").Replace("ê", "ê").Replace("ë", "ë").Replace("ì", "ì").Replace("í",
"í").Replace("î", "î").Replace("ï", "ï").Replace("ð", "ð").Replace("ñ",
"ñ").Replace("ò", "ò").Replace("ó", "ó").Replace("ô", "ô").Replace("õ",
"õ").Replace("ö", "ö").Replace("ø", "ø").Replace("ù", "ù").Replace("ú",
"ú").Replace("û", "û").Replace("ü", "ü").Replace("ý", "ý").Replace("þ",
"þ").Replace("ÿ", "ÿ")
' replace Math Symbols Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("∀", "∀").Replace("∂", "∂").Replace("∃", "∃").Replace("∅", "∅").Replace("∇",
"∇").Replace("∈", "∈").Replace("∉", "∉").Replace("∋", "∋").Replace("∏",
"∏").Replace("∑", "∑").Replace("−", "−").Replace("∗", "∗").Replace("√",
"√").Replace("∝", "∝").Replace("∞", "∞").Replace("∠", "∠").Replace("∧",
"∧").Replace("∨", "∨").Replace("∩", "∩").Replace("∪", "∪").Replace("∫",
"∫").Replace("∴", "∴").Replace("∼", "∼").Replace("≅", "≅").Replace("≈",
"≈").Replace("≠", "≠").Replace("≡", "≡").Replace("≤", "≤").Replace("≥",
"≥").Replace("⊂", "⊂").Replace("⊃", "⊃").Replace("⊄", "⊄").Replace("⊆",
"⊆").Replace("⊇", "⊇").Replace("⊕", "⊕").Replace("⊗", "⊗").Replace("⊥",
"⊥").Replace("⋅", "⋅") 'NOTE: certain characters have tall characteristics
' replace Greek Letters Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("Α", "Α").Replace("Β", "Β").Replace("Γ", "Γ").Replace("Δ", "Δ").Replace("Ε",
"Ε").Replace("Ζ", "Ζ").Replace("Η", "Η").Replace("Θ", "Θ").Replace("Ι",
"Ι").Replace("Κ", "Κ").Replace("Λ", "Λ").Replace("Μ", "Μ").Replace("Ν",
"Ν").Replace("Ξ", "Ξ").Replace("Ο", "Ο").Replace("Π", "Π").Replace("Ρ",
"Ρ").Replace("Σ", "Σ").Replace("Τ", "Τ").Replace("Υ", "Υ").Replace("Φ",
"Φ").Replace("Χ", "Χ").Replace("Ψ", "Ψ").Replace("Ω", "Ω").Replace("α",
"α").Replace("β", "β").Replace("γ", "γ").Replace("δ", "δ").Replace("ε",
"ε").Replace("ζ", "ζ").Replace("η", "η").Replace("θ", "θ").Replace("ι",
"ι").Replace("κ", "κ").Replace("λ", "λ").Replace("μ", "μ").Replace("ν",
"ν").Replace("ξ", "ξ").Replace("ο", "ο").Replace("π", "π").Replace("ρ",
"ρ").Replace("ς", "ς").Replace("σ", "σ").Replace("τ", "τ").Replace("υ",
"υ").Replace("φ", "φ").Replace("χ", "χ").Replace("ψ", "ψ").Replace("ω",
"ω").Replace("ϑ", "ϑ").Replace("ϒ", "ϒ").Replace("ϖ", "ϖ")
' replace Other Entities Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("Œ", "Œ").Replace("œ", "œ").Replace("Š", "Š").Replace("š", "š").Replace("Ÿ",
"Ÿ").Replace("ƒ", "ƒ").Replace("ˆ", "ˆ").Replace("˜", "˜").Replace(" ",
" ").Replace(" ", " ").Replace(" ", " ").Replace("–", "–").Replace("—",
"—").Replace("‘", "‘").Replace("’", "’").Replace("‚", "‚").Replace("“",
" ").Replace("”", " ").Replace("„", "„").Replace("†", "†").Replace("‡",
"‡").Replace("•", "•").Replace("…", "...").Replace("‰", "‰").Replace("′",
"′").Replace("″", "″").Replace("‹", "‹").Replace("›", "›").Replace("‾",
"‾").Replace("€", "€").Replace("™", "™").Replace("←", "←").Replace("↑",
"↑").Replace("→", "→").Replace("↓", "↓").Replace("↔", "↔").Replace("↵",
"↵").Replace("⌈", "⌈").Replace("⌉", "⌉").Replace("⌊", "⌊").Replace("⌋",
"⌋").Replace("◊", "◊").Replace("♠", "♠").Replace("♣", "♣").Replace("♥",
"♥").Replace("♦", "♦") '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
' 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("<",
"<").Replace(">", ">").Replace("&", "&").Replace(";;", ";")
End Function
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
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
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
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)
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.
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.
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
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.
'*******************************************************************************
' 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 "•", 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
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.
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
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.
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.
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
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
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.
'***********************************************************************************
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
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.
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
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”.
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.
'*******************************************************************************
' 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.
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.
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.
The Delete method will delete a specified message. Just provide the selected POP3Message object.
The Reset method will undelete emails that we selected with Delete during the current session.
Page 420
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.
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
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).
Mercedes
<body>
Page 424
<p>
<p>
Mercedes<p> ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ HTML forcing a leading space by inserting a non-breaking space.
</body>
</html>
------=_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
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
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
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.
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
'*******************************************************************************
' 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
'-------------------------------------------
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
'-------------------------------------------------------------------------------
' 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
'*******************************************************************************
' 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
'*******************************************************************************
' 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
'***********************************************************************************
'***********************************************************************************
'***********************************************************************************
'***********************************************************************************
'***********************************************************************************
' 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
'*******************************************************************************
' 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))
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 "•", 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(" ", " ").Replace(""", """").Replace("'",
"'"), "<[^>]*>", "").Replace("<", "<").Replace(">", ">").Replace("&", "&").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
'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 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("¡", "¡").Replace("¢", "¢").Replace("£", "£").Replace("¤",
"¤").Replace("¥", "¥").Replace("¦", "¦").Replace("§", "§").Replace("¨",
"¨").Replace("©", "©").Replace("ª", "ª").Replace("«", "«").Replace("¬",
"¬").Replace("­", "-").Replace("®", "®").Replace("¯", "¯").Replace("°",
"°").Replace("±", "±").Replace("²", "²").Replace("³", "³").Replace("´",
"´").Replace("µ", "µ").Replace("¶", "¶").Replace("·", "•").Replace("¸",
"¸").Replace("¹", "¹").Replace("º", "º").Replace("»", "»").Replace("¼",
"¼").Replace("½", "½").Replace("¾", "¾").Replace("¿", "¿").Replace("×",
"×").Replace("÷", "÷")
'replace ISO 8859-1 characters. Note that any matches will make the text 8-bit
Sb.Replace("À", "À").Replace("Á", "Á").Replace("Â", "Â").Replace("Ã", "Ã").Replace("Ä",
"Ä").Replace("Å", "Å").Replace("Æ", "Æ").Replace("Ç", "Ç").Replace("È",
"È").Replace("É", "É").Replace("Ê", "Ê").Replace("Ë", "Ë").Replace("Ì",
"Ì").Replace("Í", "Í").Replace("Î", "Î").Replace("Ï", "Ï").Replace("Ð",
"Ð").Replace("Ñ", "Ñ").Replace("Ò", "Ò").Replace("Ó", "Ó").Replace("Ô",
"Ô").Replace("Õ", "Õ").Replace("Ö", "Ö").Replace("Ø", "Ø").Replace("Ù",
"Ù").Replace("Ú", "Ú").Replace("Û", "Û").Replace("Ü", "Ü").Replace("Ý",
"Ý").Replace("Þ", "Þ").Replace("ß", "ß").Replace("à", "à").Replace("á",
"á").Replace("â", "â").Replace("ã", "ã").Replace("ä", "ä").Replace("å",
"å").Replace("æ", "æ").Replace("ç", "ç").Replace("è", "è").Replace("é",
"é").Replace("ê", "ê").Replace("ë", "ë").Replace("ì", "ì").Replace("í",
"í").Replace("î", "î").Replace("ï", "ï").Replace("ð", "ð").Replace("ñ",
"ñ").Replace("ò", "ò").Replace("ó", "ó").Replace("ô", "ô").Replace("õ",
"õ").Replace("ö", "ö").Replace("ø", "ø").Replace("ù", "ù").Replace("ú",
"ú").Replace("û", "û").Replace("ü", "ü").Replace("ý", "ý").Replace("þ",
"þ").Replace("ÿ", "ÿ")
'replace Math Symbols Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("∀", "∀").Replace("∂", "∂").Replace("∃", "∃").Replace("∅", "∅").Replace("∇",
"∇").Replace("∈", "∈").Replace("∉", "∉").Replace("∋", "∋").Replace("∏",
"∏").Replace("∑", "∑").Replace("−", "−").Replace("∗", "∗").Replace("√",
"√").Replace("∝", "∝").Replace("∞", "∞").Replace("∠", "∠").Replace("∧",
"∧").Replace("∨", "∨").Replace("∩", "∩").Replace("∪", "∪").Replace("∫",
"∫").Replace("∴", "∴").Replace("∼", "∼").Replace("≅", "≅").Replace("≈",
"≈").Replace("≠", "≠").Replace("≡", "≡").Replace("≤", "≤").Replace("≥",
"≥").Replace("⊂", "⊂").Replace("⊃", "⊃").Replace("⊄", "⊄").Replace("⊆",
"⊆").Replace("⊇", "⊇").Replace("⊕", "⊕").Replace("⊗", "⊗").Replace("⊥",
"⊥").Replace("⋅", "⋅")'NOTE: certain characters have tall characteristics
'replace Greek Letters Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("Α", "Α").Replace("Β", "Β").Replace("Γ", "Γ").Replace("Δ", "Δ").Replace("Ε",
"Ε").Replace("Ζ", "Ζ").Replace("Η", "Η").Replace("Θ", "Θ").Replace("Ι",
"Ι").Replace("Κ", "Κ").Replace("Λ", "Λ").Replace("Μ", "Μ").Replace("Ν",
"Ν").Replace("Ξ", "Ξ").Replace("Ο", "Ο").Replace("Π", "Π").Replace("Ρ",
"Ρ").Replace("Σ", "Σ").Replace("Τ", "Τ").Replace("Υ", "Υ").Replace("Φ",
"Φ").Replace("Χ", "Χ").Replace("Ψ", "Ψ").Replace("Ω", "Ω").Replace("α",
"α").Replace("β", "β").Replace("γ", "γ").Replace("δ", "δ").Replace("ε",
"ε").Replace("ζ", "ζ").Replace("η", "η").Replace("θ", "θ").Replace("ι",
"ι").Replace("κ", "κ").Replace("λ", "λ").Replace("μ", "μ").Replace("ν",
"ν").Replace("ξ", "ξ").Replace("ο", "ο").Replace("π", "π").Replace("ρ",
"ρ").Replace("ς", "ς").Replace("σ", "σ").Replace("τ", "τ").Replace("υ",
"υ").Replace("φ", "φ").Replace("χ", "χ").Replace("ψ", "ψ").Replace("ω",
"ω").Replace("ϑ", "ϑ").Replace("ϒ", "ϒ").Replace("ϖ", "ϖ")
'replace Other Entities Supported by HTML. Note that any matches will make the text 8-bit
Sb.Replace("Œ", "Œ").Replace("œ", "œ").Replace("Š", "Š").Replace("š", "š").Replace("Ÿ",
"Ÿ").Replace("ƒ", "ƒ").Replace("ˆ", "ˆ").Replace("˜", "˜").Replace(" ",
" ").Replace(" ", " ").Replace(" ", " ").Replace("–", "–").Replace("—",
"—").Replace("‘", "‘").Replace("’", "’").Replace("‚", " ‚").Replace("“",
" ").Replace("”", " ").Replace("„", "„").Replace("†", "†").Replace("‡",
"‡").Replace("•", "•").Replace("…", "...").Replace("‰", "‰").Replace("′",
"′").Replace("″", "″").Replace("‹", "‹").Replace("›", "›").Replace("‾",
"‾").Replace("€", "€").Replace("™", "™").Replace("←", "←").Replace("↑",
"↑").Replace("→", "→").Replace("↓", "↓").Replace("↔", "↔").Replace("↵",
"↵").Replace("⌈", "⌈").Replace("⌉", "⌉").Replace("⌊", "⌊").Replace("⌋",
"⌋").Replace("◊", "◊").Replace("♠", "♠").Replace("♣", "♣").Replace("♥",
"♥").Replace("♦", "♦") '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
'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
'strip remaining HTML text tags, replace < and > placeholders, convert ampersand, replace ;; with ;, then return result
Return RegularExpressions.Regex.Replace(NewText, "<[^>]*>", "").Replace("<",
"<").Replace(">", ">").Replace("&", "&").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
'-------------------------------------------------------------------------------
' 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
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
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
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
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
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.
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
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
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
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)
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
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).
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
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
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
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
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.
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
'*******************************************************************************
' Method Name : RefreshPicTextData
Page 473
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
'*******************************************************************************
' 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.
'*******************************************************************************
' 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
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
'*******************************************************************************
' 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
parameters, as shown to the right: /e Opens Windows Explorer in its default view.
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
into the primary string, “explorer.exe /e,/select,”, and Opens a window view of the specified share with TestApp selected.
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
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
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
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
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
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
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
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
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
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
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
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
' 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
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).
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
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.
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
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...
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
'*********************************************************************************
' 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
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
' 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.
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
The above code would yield the following result in the Debug Output panel of the IDE:
Bob
Judy
Ralph
Timmy
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
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.
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).
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.
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
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).
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
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
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.
'*********************************************************************
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
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
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
'*********************************************************************************
' 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
'*********************************************************************************
' 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
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
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
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
'*********************************************************************************
' 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
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
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
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
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
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
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
'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 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).
'' 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"))
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
' 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
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
'*******************************************************************************
' 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
'*******************************************************************************
' 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
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
<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
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
'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((
'-------------------------------------------------------------------------------
'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
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
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
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.
'-------------------------------------------------------
'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
' select our Pen and Brush and save the ones we are replacing
oldhPen = SelectObject(hDC, hPen)
oldhBrush = SelectObject(hDC, hBrush)
Page 567
' select our Pen and Brush and save the ones we are replacing
oldhPen = SelectObject(hDC, hPen)
oldhBrush = SelectObject(hDC, hBrush)
'*************************************************************************************************************
'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
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
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.
' 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
<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)
'*******************************************************************************
' 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)
'-------------------------------------------------------------------------------
'-------------------------------------------------------------------------------
'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.
'*******************************************************************************
' 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
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
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
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
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
'*******************************************************************************
' 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
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
And that is all there is to it. And this even works when checking against SystemColor values.
Page 590
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
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
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
'*********************************************************************************
'*********************************************************************************
' 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
'*********************************************************************************
' 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!
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 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
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
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
'*********************************************************************************
' 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
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
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.
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.
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
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.
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
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
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
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)”.
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
'***********************************************************************************
' 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
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.
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.
'*********************************************************************************
' 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
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
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.
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.
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.
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
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=
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 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.
Suppose we also added a few more converted images. Our new Form code might look like this:
Option Explicit On
Option Strict On
'*******************************************************************************
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%).
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
'*********************************************************************************
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
'*********************************************************************************
' 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
"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.
'*******************************************************************************
' 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
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
'*******************************************************************************
' 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
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
"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_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
'*******************************************************************************
' 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
'*********************************************************************************
' 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
'*********************************************************************************
' 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
'*********************************************************************************
' 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))
'*********************************************************************************
' 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
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.
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.
'*******************************************************************************
' 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
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
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 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
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:
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
'*******************************************************************************
' 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
'*******************************************************************************
' 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
'*********************************************************************************
' 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 : 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
'*********************************************************************************
' 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
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 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
'*******************************************************************************
' 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
'*******************************************************************************
' 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
'*************************************************************************************
'*************************************************************************************
' 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
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
'*******************************************************************************
' 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
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
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.
End Module
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
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
End Module
Page 706
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
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
'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.
'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
'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
'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.
'*******************************************************************************
' 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
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
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
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
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
'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
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
'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
'*******************************************************************************
' 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.
'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)
'*******************************************************************************
' 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
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
'*******************************************************************************
' 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
'*******************************************************************************
' 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.
'*******************************************************************************
' 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.
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!
'*******************************************************************************
' 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!).
'*******************************************************************************
' 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
'*********************************************************************************
' 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 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.
'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
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
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 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
'*******************************************************************************
' 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
'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
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
'*******************************************************************************
' 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
'*******************************************************************************
' 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
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
'*********************************************************************************
' 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.
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
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
'*********************************************************************************
' 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
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
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.
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
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.
'*********************************************************************************
' 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
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.
'*********************************************************************************
' 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
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
'*******************************************************************************
' 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
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
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.
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
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
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
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.
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.
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.
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
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
Gobalist Symbolism:
https://drive.google.com/file/d/0B_Dj_dKazINlY2ZYb3NVdnlLNDQ