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

March 25, 2018 | Author: David Ross Goben | Category: Visual Basic .Net, Email, String (Computer Science), Class (Computer Programming), Icon (Computing)


Comments



Description

Enhancing Visual Basic .NET Applications Far Beyond the Scope of Visual Basic 6.0 “How to do the things that you used to do under VB6 even better under VB.NET, and how to make your VB.NET applications accomplish so very much more.” Written and Edited by David Ross Goben Copyright © 2010-2016 by David Ross Goben All rights reserved. (Last updated February 24, 2016) (The above two logos are registered trademarks of Microsoft Corporation) This title was originally published as “Visual Basic .NET Compared to Visual Basic 6.0” in July, 2010. NOTE: You can download the latest update of this PDF document for free at Google Docs: https://drive.google.com/file/d/0B_Dj_dKazINlcmpucEFPeVBObjg/view?usp=sharing Source Listings are available, unbroken by page breaks, in “Code Excerpts from Enhancing Visual Basic .NET.rtf”: https://drive.google.com/file/d/0B_Dj_dKazINlMlA5UUx6R2JLOG8/view?usp=sharing Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Table of Contents Preface ..................................................................................................................................................9 Introduction.........................................................................................................................................11 Why Is VB.NET Not 100% Compatible with VB6? .....................................................................................12 Why .NET?..................................................................................................................................................13 What is Dot NET?................................................................................................................................................. 14 What is the .NET Framework? .............................................................................................................................. 14 What is a Namespace?......................................................................................................................................... 15 What is an Application Domain?............................................................................................................................ 16 What is a Solution/Assembly?............................................................................................................................... 17 What is a Project? ................................................................................................................................................ 17 Object-Oriented Programming....................................................................................................................18 Classes and Objects............................................................................................................................................. 18 Fields, Properties, Methods, and Events ............................................................................................................... 20 Encapsulation, Inheritance, and Polymorphism...................................................................................................... 20 Overloading, Overriding, and Shadowing............................................................................................................... 21 Scoping Rules ...................................................................................................................................................... 21 My Basic Guidelines for VB.NET Development .........................................................................................22 Conclusion to the Introduction ....................................................................................................................23 Noteworthy VB.NET Features That Differ from, or are New to VB6.................................................25 Notes Regarding the VB6 Compatibility Library.........................................................................................25 Notes Regarding All String Variables in VB.NET Now Being Reference Types........................................26 Notes Regarding Sending Structures to P/Invokes....................................................................................27 Notes Regarding Fixed-Length Strings ......................................................................................................28 A Note Regarding Passing Parameters ByVal ...........................................................................................29 A Note Regarding Returning Value Data via the Return Command from Functions.................................30 A Note Regarding Multi-Variable Declarations...........................................................................................30 Notes Regarding Collections Now Storing Objects, Not Simple Strings....................................................30 A Note Regarding VB.NET Default Properties That Cannot Be Parameterless........................................32 A Note Regarding VB.NET Having Dropped the 'Let' Keyword .................................................................32 A Note Regarding New and Wider Variable Definitions .............................................................................33 Notes Regarding Edit and Continue ...........................................................................................................33 Notes Regarding the VB.NET GET/SET Property Format.........................................................................33 A Note Regarding the Support of Using…End Using.................................................................................34 A Note Regarding the New 'IsNot' Keyword...............................................................................................34 A Note Regarding the New 'AndAlso' Keyword..........................................................................................34 A Note Regarding the New 'OrElse' Keyword ............................................................................................34 A Note Regarding Overloading, and Why We Should Welcome It ............................................................35 A Note Regarding New Variable Types SByte, UShort, UInteger, ULong, and UIntPtr ............................35 A Note Regarding Partial Classes and Structures .....................................................................................35 A Note Regarding Form-Linked Controls ...................................................................................................36 A Note Regarding Better IntelliSense.........................................................................................................36 A Note Regarding Accessing Common or All Options in IntelliSense Popups..........................................36 A Note Regarding Snippets ........................................................................................................................36 A Note Regarding the Exception Assistant ................................................................................................36 A Note Regarding the Immediate Window .................................................................................................37 A Note Regarding Debug Trace Enhancements........................................................................................37 A Note Regarding the Continue Statement ................................................................................................37 A Note Regarding Data Source Binding .....................................................................................................37 A Note Regarding the Conditional Operator...............................................................................................37 A Note Regarding Structured Exception Handling .....................................................................................37 A Note Regarding Type Inference ..............................................................................................................37 A Note Regarding Anonymous Typing .......................................................................................................37 A Note Regarding Enumerators .................................................................................................................38 A Note Regarding Optional Event Parameters...........................................................................................38 Notes Regarding the MY Namespace........................................................................................................38 Page –2– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Notes Regarding Class Construction and Destruction...............................................................................39 Notes Regarding New Style P/Invoke Signatures ......................................................................................42 A Note Regarding the VB.NET Form Event Firing Sequence....................................................................45 Notes Regarding Form Command Changes ..............................................................................................46 Notes Regarding MouseMove Event Parameters......................................................................................48 A Note Regarding Type Object as the New Universal Data Type .............................................................50 A Note Regarding the Instance Handle ......................................................................................................51 A Note Regarding Checking For a Previous Application Instance.............................................................51 A Note Regarding Getting the App Title .....................................................................................................52 A Note Regarding Getting the App Path and the App EXE Name..................................................................52 A Note Regarding Reviving the VB6 App Command .................................................................................52 A Note Regarding Collections Enhancements ...........................................................................................54 Notes Regarding Registry I/O.....................................................................................................................54 A Note Regarding Adjusting Form Opacity (Transparency).......................................................................54 A Note Regarding Literal Type Characters ................................................................................................55 Notes Regarding Loading Images..............................................................................................................55 Notes Regarding Loading Icons .................................................................................................................56 Notes Regarding Embedding Classes........................................................................................................57 Notes Regarding Accessing Private Class Members from a Sister Object ...............................................57 Notes Regarding the Checked State of a CheckedListBox........................................................................59 Notes Regarding New String Manipulation Features .................................................................................59 Featured Articles 60 VB.NET Generics Collection Classes ................................................................................................61 Generic Collections ......................................................................................................................................61 Generics ......................................................................................................................................................61 Generics Collection Classes.........................................................................................................................63 How to Perform Easy Font Manipulation at Runtime under VB.NET...............................................64 Easily Change Font Sizes at Runtime...........................................................................................................64 Easily Change Font Styles at Runtime (Bold, Italic, Underline, and Strikethrough) ........................................64 Easily Change Font Names at Runtime ........................................................................................................65 Building a Suite of Runtime Font Modification Tools......................................................................................66 Drawing Labels with Transparent Backgrounds over Images.........................................................68 Easily Placing Labels with Transparent Backgrounds over Controls..............................................................68 Painting Text Directly onto Controls with Ease..............................................................................................70 Painting Text across Multiple Controls with Ease ..........................................................................................71 Emulating VB6 Image Control Features in VB.NET ..........................................................................72 Painting Images on a Form...........................................................................................................................72 Image with Transparency Rendering over Multiple Controls..........................................................................79 Emulating What VB6 Image Controls Do Using a Borderless Form ...............................................................81 Room for Improvement ....................................................................................................................................... 83 Emulating What VB6 Image Controls Do Using a PictureBox........................................................................88 Emulating What VB6 Image Controls Do Using a Resource Icon ..................................................................89 Super-Fast No-Holds-Barred VB6 Image Emulation......................................................................................91 Emulating Mouse Interaction on Drawn Images ............................................................................................91 Conclusion ...................................................................................................................................................92 Implementing Unions in VB.NET .......................................................................................................93 Structures that use Sub-Structures and Union Sub-Structures ......................................................................97 Understanding VB.NET Imports.......................................................................................................100 Namespaces, Modules, and Non-Inheritable Classes: Siblings by Different Names......................100 Using Imports Aliases ...................................................................................................................101 Referencing Class Code without Importing or Instantiation ...........................................................101 Understanding VB.NET Delegates...................................................................................................102 Delegates in Events ................................................................................................................................... 102 Delegates and AddressOf........................................................................................................................... 103 Delegates and Callbacks............................................................................................................................ 104 Invoking Methods through Delegates.......................................................................................................... 105 Page –3– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Using a Delegate to Perform Super-Fast Shell-Metzner Sorts on Any 1D Array........................................... 106 Closing Notes on Delegates ....................................................................................................................... 110 VB.NET Structures Compared to VB6 User-Defined Types ...........................................................112 Releasing COM Objects from Memory in VB.NET ..........................................................................115 Changed Behavior of VB6 Functions in VB.NET ............................................................................116 Array Function........................................................................................................................................... 116 Dir Function ............................................................................................................................................... 116 Get#, Put# Functions ................................................................................................................................. 116 IsObject Function ...................................................................................................................................... 116 Mod Operator ............................................................................................................................................ 117 Return Statement ...................................................................................................................................... 117 SavePicture Statement.............................................................................................................................. 117 Str Function ............................................................................................................................................... 117 TypeName Function .................................................................................................................................. 117 TypeOf Function ........................................................................................................................................ 118 VarType Function ...................................................................................................................................... 118 Upgrading Win32 Data Types for VB.NET P/Invokes .....................................................................119 Fix-Length Strings ...................................................................................................................................... 119 Strings Used in Interfaces........................................................................................................................... 120 Strings Used in Platform Invoke.................................................................................................................. 120 Strings Used in Structures.......................................................................................................................... 121 Fixed-Length String Buffers ........................................................................................................................ 122 Dealing with "As Any" in P/Invoke Signatures ............................................................................................. 122 Using the AsAny Marshalling Parameter..................................................................................................... 123 List of All .NET UnmanagedType Members ................................................................................................ 124 Thread Creation, Queue Hooking, and Other Special P/Invoke Features .................................................... 126 String Formats Changes between VB6 and VB.NET ......................................................................127 Marshaling Memory and Passing Strings to P/Invokes in VB.NET................................................129 Upgrading Administrative Rights Checks.......................................................................................133 Running applications that require Administrative Privileges......................................................................... 133 Check for the User having Administrative Rights......................................................................................... 134 Running Applications Requiring Administrative Rights without Being Prompted .......................................... 134 Disabling User Disabling User Disabling User Disabling User Account Control through your User Account Settings ..................................................................................135 Account Control through the Control Panel ..................................................................................................135 Account Control through MSCONFIG ..........................................................................................................135 Account Control through Editing the Registry...............................................................................................135 Adding Run-Time Custom Menus and Cloneable ToolStripMenuItems to VB.NET......................136 Understanding Menus by Converting a MenuStrip to a MainMenu .............................................................. 136 Optimizing MainMenu Design Code............................................................................................................ 141 Building ContextMenuStrip PopUp Menus on the Fly .................................................................................. 144 Building MenuStrips on the Fly ................................................................................................................... 145 Cloning MenuStrip Items with a Cloneable ToolStripMenuItem Class.......................................................... 145 Using cToolStripMenuItem.CloneMenu to Create a New MergeMenuStrip Method ..................................... 148 Conclusion ................................................................................................................................................. 149 Adding Window and File Lists to VB.NET Menus.......................................................................... 150 Maintaining an Active Child Windows List............................................................................................... 150 Adding an Open Window List to a VB.NET MenuStrip menu ................................................................................ 151 Adding a MainMenu and ContextMenu control to your VB.NET Toolbox............................................................... 152 Maintaining a Most Recently Used File List ............................................................................................ 152 MRU File Support under VB6.............................................................................................................................. 153 MRU File Support under VB.NET........................................................................................................................ 157 Flicker-Free VB.NET Form Resizing ................................................................................................162 The Minimalist Solution for Form Resizing Flicker ...................................................................................163 Adding a Little Pizzazz to the Minimalist Solution ....................................................................................164 Adding More Pizzazz to Build a More Stable Solution .............................................................................164 Adding Alotta Pizzazz: Professional Results by Intercepting the Windows Message Queue .................165 Form Resize Subclassing under VB6 ............................................................................................................... 166 Form Resize Subclassing under VB.NET......................................................................................................... 168 Easy Ways to Draw Lines and Shapes, and to Paint in VB.NET....................................................172 Page –4– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben First, News of Free VB.NET Line and Shape Controls ............................................................................172 Drawing Lines from the Paint Event .........................................................................................................172 Drawing Rectangles from the Paint Event................................................................................................174 Drawing Ellipses (Circular Shapes) from the Paint Event ........................................................................176 Painting Backgrounds with Patterns.........................................................................................................176 Painting the Background of Odd Shapes Using a Flood Fill ....................................................................178 GDI FloodFill ..................................................................................................................................................... 178 GDI+ FloodFill................................................................................................................................................... 180 Drawing Other Shapes from the Paint Event ...........................................................................................184 Drawing Super-Fast Rounded Rectangles from the Paint Event.............................................................184 Drawing Shapes Outside of the Paint Event ............................................................................................189 Conclusion ................................................................................................................................................190 Adding Images to ListBoxes and ComboBoxes with Ease under VB.NET ...................................191 Principle features of an Owner-Drawn ListBox (and ComboBox)............................................................191 Using A Generic List Collection and an ImageListControl .......................................................................192 Using A Flag at the Start of the List Item Text..........................................................................................193 Using a Custom Class as a List Item to Index Graphics ..........................................................................194 Centering a Message Box on a Form under VB.NET......................................................................196 The Simple Explanation of Centering a Message Box on a Form ...........................................................196 Some Background Information .................................................................................................................196 A More Detailed Explanation of Centering a Message Box on a Form....................................................197 The Detailed Explanation of Centering a Message Box on a Form .........................................................197 Putting It All Together ...............................................................................................................................202 Restoring Raster Graphics and Rubber Bands to .NET ................................................................ 204 Getting an Image Graphics Object Without Going Through the Paint() Event........................................ 205 Restoring Raster Operations to .NET ...................................................................................................... 205 Getting One's Hands Dirty ................................................................................................................................... 206 The GDI32 Class ................................................................................................................................................ 208 Emulating a Selection Rubber Band under GDI ...................................................................................... 214 Emulating a Selection Rubber Band under GDI+.................................................................................... 215 Using the GDI+ DrawReversibleFrame() Method.................................................................................................. 215 Using the GDI+ DrawRectangle() Method and a Translucent Brush ...................................................................... 217 Extending VB.NET Controls Functionality ..................................................................................... 219 Demonstrating Control Extension Using the VB.NET ComboBox (and ListBox) .................................... 219 Win32 ComboBox Messages............................................................................................................................... 220 The CB_DIR Message (and how to make it work under VB.NET) ......................................................................... 221 The CB_SETTOPINDEX and CB_GETTOPINDEX Messages.............................................................................. 224 The CB_SETEXTENDEDUI and CB_GETEXTENDEDUI Messages..................................................................... 225 The CB_SETCURSEL Message.......................................................................................................................... 226 Adding a CheckBox to the Edit Field of a ComboBox ........................................................................................... 227 Sizing the ComboBox DropDown List Width......................................................................................................... 228 Sizing the ComboBox DropDown List Height........................................................................................................ 230 Adding the DriveListBox, DirListBox, and FileListBox Controls to VB.NET ............................................ 231 Send (SMTP) and Retrieve (POP3) Email with Ease under VB.NET ............................................. 233 Adding the VB6 MAPISession and MAPIMessage Controls to VB.NET................................................. 233 PART ONE ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Sending Email under VB.NET using Native Methods ....................................................................... 234 Quick and Dirty Email Senders .................................................................................................................... 234 TCP Ports, SSL Authentication, and Creating Credentials............................................................................. 236 An Email Sender with a Lot of Muscle .......................................................................................................... 237 Sending Email Messages as HTML ........................................................................................................................... 240 Sending Alternate Message Views .............................................................................................................................. 241 Sending a Plain Text Message Body and a RichText AlternateView ........................................................................... 244 Sending Alternate Message Views with Different Context Types and Transfer Encoding ........................................... 245 Typical Email Server Specifications ..................................................................................................................249 Page –5– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben PART TWO ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Encoding and Decoding Email Data.................................................................................................. 250 Allowing Users to Specify Content-Type and Content-Transfer-Encoding Options ......................................... 251 Determining if Text can be Sent Encoded As Quoted-Printable, Base64, or 7Bit............................................ 252 Converting 8-Bit HTML Data to 7-Bit for Sending without Loss of Integrity ..................................................... 253 Converting 8-Bit Text Data to 7-Bit for Sending without Data Loss................................................................. 253 Decoding Quoted-Printable Text .................................................................................................................. 255 Translating Base64 Data Back to Its Original Format .................................................................................... 255 Translating BinHex Data Back to its Original Format..................................................................................... 258 PART THREE ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Receiving Email under VB.NET using Native Methods..................................................................... 259 Connecting to a POP3 Server...................................................................................................................... 262 Checking for a POP3 Server Response........................................................................................................ 264 Checking for Being Connected to a POP3 Server ......................................................................................... 264 Getting a Response from the POP3 Server .................................................................................................. 264 Submitting a Request to the POP3 Server.................................................................................................... 266 Disconnecting from the POP3 Server ........................................................................................................... 266 Getting Email Statistics from the POP3 Server ............................................................................................. 267 Getting an Email Reference List from the POP3 Server ................................................................................ 267 Get an Email Header from the POP3 Server................................................................................................. 268 Retrieve an Email from the POP3 Server ..................................................................................................... 269 Deleting an Email from the POP3 Server...................................................................................................... 270 Reset (Undo) All Deletes from the POP3 Server........................................................................................... 270 Send a 'Keep-Alive' NOOP Command to the POP3 Server ........................................................................... 271 Disposing of Resources............................................................................................................................... 271 Using the Completed POP3 Class ............................................................................................................... 271 PART FOUR ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Email Data Blocks Made Easy .......................................................................................................... 273 Easily Extracting the Component Parts from an Email File............................................................................. 276 Compiling Everything into an Email Class Library ............................................................................ 279 Building the VBNetMail Class Library............................................................................................................ 281 Accessing your New VBNetEmail Class Library DLL from another Project...................................................... 281 The Complete SMTP Class File ........................................................................................................ 282 The Complete POP3 Class File......................................................................................................... 284 The Complete Utilities Class File....................................................................................................... 290 Conclusion.............................................................................................................................................. 298 Useful References 299 Comparing Visual Basic System I/O Commands......................................................................300 REALLY Looking under the Hood of Visual Basic .NET ..........................................................302 VB6 Changed Commands In VB.NET ........................................................................................304 VB6 Aficionado Complaint Department (116 VB6 User Complaints against VB.NET Answered) 316 Additional "Black Book" Tips 343 Black Book Tip # 1: Bypassing Click Events when a Context Menus is Not Displayed on a Right-Click ..... 346 Bonus Tip......................................................................................................................................... 347 Black Book Tip # 2: Creating an Association between a File Extension and Your Application.................... 348 Black Book Tip # 3: Get the Linked File Path from a Shortcut File............................................................. 351 Bonus Tip ........................................................................................................................................................352 Black Book Tip # 4: Create a Shortcut File within Your Code .................................................................... 353 Bonus Tip ........................................................................................................................................................354 Black Book Tip # 5: Adding a File to the Recent Documents Folder .......................................................... 355 Black Book Tip # 6: Sorting Any Column in a ListView Control in Ascending or Descending Order............ 356 Bonus Tip ........................................................................................................................................................359 Black Book Tip # 7: Sizing a Label or TextBox to Fully Contain a String for Display .................................. 360 Black Book Tip # 8: Set a New SelectedIndex for a ListBox or a ComboBox without Firing its Click Event. 363 Black Book Tip # 9: Display TextBox Text Format-Justified at Runtime..................................................... 365 Bonus Tip 1 .....................................................................................................................................................370 Bonus Tip 2 .....................................................................................................................................................370 Bonus Tip 3 .....................................................................................................................................................370 Page –6– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 10: Get The ListIndex of a ListBox Row that is Under the Mouse Cursor........................ 371 Black Book Tip # 11: Open File Explorer with a Target File Selected ........................................................ 372 Black Book Tip # 12: Determining if an array is Dimensioned.................................................................... 373 Black Book Tip # 13: Customizing the Display of TabControl Tabs ........................................................... 374 Bonus Tip ........................................................................................................................................................378 Black Book Tip # 14: Detecting a TabControl Tab on a Right MouseDown Event...................................... 379 Black Book Tip # 15: Prevent the User from Selecting a TabControl Tab .................................................. 380 Black Book Tip # 16: Hiding Tab Pages without Destroying the Tab Pages or their Resources ................. 381 Black Book Tip # 17: Passing a Parameter ByVal when the Invoking Function Specifies a ByRef Parameter ... 384 Black Book Tip # 18: Show and Hide Additional Information at the Bottom of the Form ............................. 385 NOTES ON IMAGES .......................................................................................................................................389 Black Book Tip # 19: Easily Recovering Crashed Menus and Toolbars under VB.NET.............................. 390 Black Book Tip # 20: Tracking ComboBox Items under the Mouse ........................................................... 396 Black Book Tip # 21: Greatly Simplifying P/Invoke Definitions of POINTAPI and RECT Structures............ 399 Black Book Tip # 22: Easily Sorting Strongly-Typed Generic Collections Lists .......................................... 405 Black Book Tip # 23: Dithering a Form's Background .........................................................................409 Black Book Tip # 24: Designing Intelligent Context Menus for TextBox and RichTextBox Controls ..........412 Black Book Tip # 25: Setting Context Menu Icon Image Transparency at Runtime.................................... 417 Bonus Tip # 1 ..................................................................................................................................................419 Bonus Tip # 2 ..................................................................................................................................................420 Black Book Tip # 26: Adding Background Transparency to Images on Toolstrips and Menus..................421 Black Book Tip # 27: Easily Do a Redim Preserve with Leftward Bounds of Multi-dimensioned Arrays....424 Black Book Tip # 28: Implementing a C-Style Bit Field Class in VB.NET ..............................................428 Black Book Tip # 29: Taking Advantage of, and Extending Application Events ......................................439 Black Book Tip # 30: Enable Built-In Justify-Alignment in a RichTextBox from VB.NET............................. 442 Black Book Tip # 31: Easily Replace Power Pack Shape Controls with Faster Paint() Event code............. 446 Black Book Tip # 32: Dealing with the Form Cursor not displaying over Form Controls ............................. 455 Black Book Tip # 33: Passing Scalar Values and Strings to and from a Byte Array.................................... 457 Black Book Tip # 34: Display Buttons Labels wider than the button normally allows .................................. 467 Black Book Tip # 35: Performing Selections or inserting RichText Data Off-screen without Scrolling......... 469 Black Book Tip # 36: Opening an Associated Application without a File .................................................... 471 Black Book Tip # 37: Comparing Color Values.......................................................................................... 476 Black Book Tip # 38: Use [Enum].GetValues() to Parse Enumeration Definitions ...................................... 477 Black Book Tip # 39: Detecting Transparency Colors................................................................................ 478 Black Book Tip # 40: Greatly Accelerate TreeView Reflection of Directory Trees ...................................... 479 Black Book Tip # 41: Sorting TreeView Directory Trees in an Orderly Fashion.......................................... 483 Black Book Tip # 42: Taking Advantage of VB.NET Drag and Drop Features............................................ 488 Bonus Tip ........................................................................................................................................................492 Black Book Tip # 43: Taking Advantage of the DateTimePicker Control................................................... 493 Black Book Tip # 44: Padding Strings and Filling Separator Lines in Proportionally-spaced Text............... 496 Black Book Tip # 45: Making Sense of Form Default Buttons and Their DialogResult Properties............... 499 Black Book Tip # 46: Quick and Easy Text-Justification for Text Boxes, Labels, and Dialog Boxes ........... 500 Black Book Tip # 47: Owner-Drawn Controls Made Easy.......................................................................... 506 Black Book Tip # 48: Owner-Drawn TreeViews Made Easy ...................................................................... 509 Black Book Tip # 49: Embedding Images within Your Source Code .......................................................... 513 Converting Between an Image and a Base64 String..........................................................................................513 Formatting Base64 Strings for Embedding Within Source Code.........................................................................514 ImageLists versus Resources...........................................................................................................................517 Creating BuildImageListCode(), an ImageList Initialization Source Code Generator ...........................................518 Creating Single Base64 Image Data .................................................................................................................523 Creating Single Base64 Icon Data ....................................................................................................................525 Black Book Tip # 50: Replace the BrowseFolderDialog Control with a Custom BrowserDialog Form......... 527 Black Book Tip # 51: Using, Creating, and Embedding ImageStrips with Ease.......................................... 544 Loading an ImageStrip .....................................................................................................................................545 Creating Your Own ImageStrip.........................................................................................................................545 Splitting an ImageStrip .....................................................................................................................................546 Updating Black Book Tip # 49 to Use ImageStrips.............................................................................................546 Black Book Tip # 52: Extracting Icon Images from Files and Displaying Them in a Directory TreeView ..... 550 Black Book Tip # 53: Determining If a Computer Has an Internet Connection ........................................... 581 Black Book Tip # 54: Manipulating Color Value Members with Ease ......................................................... 586 Page –7– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 55: Converting Byte Sizes to Formatted Byte, KB, MB, GB, or TB Strings...................... 589 Black Book Tip # 56: Getting, Enumerating, and Changing Screen Settings.............................................. 591 Black Book Tip # 57: Printing Plain Text and Formatted Rich Text with Ease ............................................ 599 Using VB6 Printer Functionality under VB.NET .................................................................................................599 Printing Simple Text under VB.NET..................................................................................................................599 Drawing Simple Text a Full Page at a Time............................................................................................602 Drawing Simple Text on a Page One Line at a Time...............................................................................604 Selecting a Printer............................................................................................................................................607 Selecting Page Setup Options..........................................................................................................................607 Selecting a Printer Font and Font Color ............................................................................................................608 Performing a Print Preview...............................................................................................................................609 Creating a Print Document Class......................................................................................................................610 Supporting the Help Buttons on Dialogs............................................................................................................614 Printing WYSIWYG RichTextBox Text ..............................................................................................................615 Black Book Tip # 58: Creating Fancier ProgressBar Marquees and Enhanced ProgressBars.................... 624 Enhancing Our Custom ProgressBar Marquee by Tiling It .................................................................................627 Emulating the Aero Glass ProgressBar Marquee ..............................................................................................628 Drawing Custom ProgressBars of User-Defined Heights ...................................................................................631 Emulating the Standard ProgressBar Using Images ..........................................................................................632 Custom ProgressBars With More Complex Images ...........................................................................................636 Black Book Tip # 59: Adding a Horizontal Scrollbar to a ComboBox DropDown List .................................. 640 Black Book Tip # 60: Restoring 3D Curved-Surface Appearances for Buttons and Tabs ........................... 643 Black Book Tip # 61: Allowing a Form’s BackColor Property to Accept Transparency Colors .................... 645 Closing Remarks ..............................................................................................................................647 About the Author ..............................................................................................................................649 Free Online PDF Documents Available by David Ross Goben..................................................................... 650 Open Letters Sent to Advocates for the Electric Universe and Expansion Tectonics Theories..................... 650 Navigating Your Way through Visual Basic 6.0 to Visual Basic .NET Application Upgrades ........................ 651 Enhancing Visual Basic .NET Applications Far Beyond the Scope of Visual Basic 6.0................................ 652 Doom 3 Walkthrough and Strategy Guide .................................................................................................. 653 Also Available from the Author...................................................................................................................... 654 A Gnostic Cycle: Exploring the Origin of Christianity ................................................................................... 654 NOTE: You can download this document’s source code listings for the code consisting of more than just a few lines, colorized, unbroken by page breaks, and with spacing fully intact, all wrapped within a RichText file named “Code Excerpts from Enhancing Visual Basic .NET.rtf” from my public folder at Google Drive: https://drive.google.com/file/d/0B_Dj_dKazINlMlA5UUx6R2JLOG8/view?usp=sharing NOTICE The opinions expressed herein are those of the author and are not opinions advocated or sanctioned by any other individual or body. These are the personal views of an independent software developer, and all criticisms, praises, endorsements and rejections are entirely his own; not those of any other. Page –8– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben PREFACE This tome was first released when VB2008 was still fresh. Now, VB2015 is VB.NET’s fresh face. This manual has been continuously updated and is applicable to all these releases. Sadly, the Upgrade Wizard referred to at times was last available to VB2008. Though you can still find the free VB2008 Express Edition floating around the web that features this wizard, as well as fee-based Migration Partners (search the Web for “convert vb6 to vb.net”), once you are proficient in VB.NET, it is easier to redesign your VB6 projects as totally reconstructed VB.NET projects, copying and pasting segments as needed, even whole code blocks, and editing each to update and enhance them as required. At first I imagined this would be a difficult task, but in writing fresh code using the more powerful features available to VB.NET, I realized it was much easier to really pump up its power. I highly recommend this approach. Though I think VB2015 is their best version yet, it also is my feeling that VB2008 had actually been their most effectual release, where Microsoft® best realized their vision for Visual Basic .NET. Although VB2005 was a monumental paradigm shift from the tentative VB2002/VB2003 releases, and it finally restored VB to its high standing as a RAD (Rapid Application Development) platform, I have always looked at VB2005 as a preparatory “Release Candidate” for VB.NET, because using the Upgrade Wizard to convert the linear, procedural language of VB6 to the object-oriented language of VB2005 was, even with all its incredible enhancements that were all greeted with loud and heavy sighs of relief, still an often gauche, sometimes long, and frustrating effort, seldom really worth all the time-consuming labor required to transition VB6 code, other than for simple or small applications. Besides, what was the typical result of this? It was, essentially, just a VB6 program that was able to run under VB.NET. VB2002 and VB2003 were awkward and problematic implementations that often required convoluted, impossible-to-remember workarounds to duplicate some quite simple VB6 functions. For example, VB6 could get the running application’s Major Version as an Integer using App.Major. Before VB2005, VB.NET required System.Diagnostics.FileVersionInfo.GetVersionInfo(System.Reflection.Assembl y.GetExecutingAssembly.Location).FileMajorPart. With the introduction of the My namespace in VB2005, this was simplified to a much more logical My.Application.Info.Version.Major (refer to “A Note Regarding Reviving the VB6 App Command” on page 52 to see how to emulate the much simpler App suite of commands under VB.NET, enabling you to once again specify App.Major and most other App functions). Truly, it is my feeling that Microsoft should have never bothered releasing any preVB2005 code, except to those who would diligently beta-test it, report verifiable bugs, and submit enhancement suggestions, though we must also fully appreciate that Microsoft also needed to offset the enormously expensive development assets they had invested to this long range game-changing venture. It is my strongest feeling that VB6 should have been aggressively supported throughout the release cycle of VB.NET, not just the marginal nodding support that it got, even though most objective people recognized by 1999 that VB6 was desperately floundering in the wake of developing technology, becoming more of an anchor chain around the neck of Visual Studio’s development (the main reason why VC++ programmers derided VB6). But until then, Microsoft should have more vocally supported VB6 extensions to access Dot NET forms, workspaces, and code until that time (as it was, they did in fact develop these assets, but few in the VB6 world seemed to be aware of it), easing the VB6 transition from its procedural code platform roots to the more powerful Object-Oriented world of Dot NET. Unlike claims made by critics from the “Classic VB” world, everything that can be done in VB6 can be easily accomplished using Dot NET as of VB2008, no longer requiring convoluted alternative solutions or intense, deep research to find a compatible alternative for a simple language feature, as had been the case for early VB.NET releases. However, many developers do not have, or may never have a need for object-based programming, and VB6 still fills that procedural language niche in the VB Community, though I would love for VB6 to have been upgraded to 64-bit and feature a massively updated IDE. Page –9– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben VB2010 added many welcome language features beyond VB2008 and pretty-much settled out the language, but the latest release, VB2015, blows all previous editions of the IDE out of the water! Its source code editing features alone are worth the upgrade. Also, it can be used without needing to upgrade source code from VB2010 forward. Further, the Visual Studio 2015 Community edition is nowhere near as limited as previous “Express” editions. VS2015 is now a full .NET development environment, save for enhanced MSDN subscriber support and privileges. Otherwise you have the full implementation of Microsoft’s Programming Language Suite, which now features multi-project solutions in VB. Remember, however, when installing, that you should select ALL the features you want, such as Desktop, Web, Phone, Storefront, or whatever. By default, the installation configuration, based upon how you reach the download point, might solely be for Web, Storefront, etc., so please take the time to read the option prompts and not simply sigh “Yeah, yeah. Whatever” before hitting the button labeled “Next”. Also, avoid the trial software options, to start, unless you really need to explore those avenues. Regarding Running VB6 on Recent Operating Systems Many developers, like myself, must still support legacy VB6 and VBA code. This is not really so surprising to those of us who have been in the business for any appreciable amount of time, considering that there are still masses of developers supporting legacy code for even the 36-bit DEC PDP-10 MiniComputer dinosaurs from the 1960s. As such, we still need to be able to run Visual Studio 6 and especially Visual Basic 6 from our development platforms. It is often said by many presumed authorities, especially online, that VB6 will not run on Windows Vista, Windows 8/8.1, or Windows 10, or any operating system that features a 64-bit environment. However, VB6, a 32-bit environment, will run on these systems if you simply Run as Administrator (the 64-bit systems will support it, and easily, through WOW64; Windows 32-bit On Windows 64-bit). Installation Setup for Visual Studio 6, or just VB6, should also be installed by running as an administrator. Still, be advised that the installation will report a single error if you are installing on an operating system later than Windows 2000, though it will not report any specific details on what that error actually was (it will report these details, however, if you install it in Safe Mode, but other than that, there is no differences or a real need for Safe Mode). Even so, you can safely ignore this error because it was ultimately caused by Setup not being able to over-write a critical system DLL, OLEAUT32.DLL (this is a shared file installed by the operating system as a service provider for 32-bit Object Linking and Embedding Automation) with a much older version that was actually designed to operate under Windows 95 through Windows 2000. But that, if it had actually succeeded in over-writing it, would have been a very bad thing for your system. Fortunately, operating systems now-a-days are more picky about what it allows to be upgraded in its system kernel. Microsoft currently maintains a updated VB6 Support Statement titled “Support Statement for Visual Basic 6.0 on Windows Vista, Windows Server 2008, Windows 7, Windows 8 and Windows 8.1, Windows Server 2012, and Windows 10”, which you can find at https://msdn.microsoft.com/en-us/vstudio/ms788708.aspx. In its Executive Summary, they state “The Visual Basic team is committed to “It Just Works” compatibility for Visual Basic 6.0 applications on the following supported Windows operating systems: Windows Vista, Windows Server 2008 including R2, Windows 7, Windows 8 and Windows 8.1, Windows Server 2012 including R2, and Windows 10.” Microsoft also provides support summaries for each operating system. For Windows 10, they state: Since the initial release of this support statement, the Windows 10 operating system has been announced. This document has been updated to clarify Microsoft’s support for VB6 on Windows 10. VB6 runtime will ship and will be supported in Windows 10 for the lifetime of the OS. Visual Basic 6.0 runtime files continue to be 32-bit only and all components must be hosted in 32-bit application processes. Developers can think of the support story for Windows 10 being the same as it is for Windows 8.1. David Ross Goben February 2016 Page –10– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Introduction Transitioning from Microsoft Visual Basic 6.0 (VB6) to Microsoft Visual Basic .NET (VB.NET1), 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 of them 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 more diligently follows a stringent pattern of language syntax, which is something VB6 was not always very good at. Hence, a programming language feature may have to be implemented under VB.NET using sometimes radically different invocation rules 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 Dot NET platform architectural specifications, VB.NET could not support them in an identical manner, but by necessity had to utilize non-VB6-style invocation rules. Nevertheless, by employing some simple user-defined helper functions, such as those that will be provided throughout this tome, you can easily emulate such “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 robust2 techniques to apply that functionality. Many of the “major” differences bemoaned by many VB6 purists3 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. See the article, “VB6 Aficionado Complaint Department (116 VB6 User Complaints against VB.NET Answered)”, on page 316 to examine these numerous charges many VB6 devotees held against VB.NET, and how all but a pittance of them are groundless. And of those few that are not groundless, they can still be quite easily resolved, rendering as petty the remainder of those complaints. Instead of simply adding some slick Dot NET features to VB6 and calling it VB7 (they basically did this with their Microsoft Interop Forms Toolkit 2.0, http://msdn.microsoft.com/en-us/vstudio/aa701257.aspx, allowing a step-wise transition of VB6 apps to VB.NET), after long debate Microsoft chose the much more difficult task of reengineering VB from the ground up, making it fully Dot NET- and OOP-compliant. Considering the monumental work required to accomplish such a task (consider the transition from VB.NET Beta to VB2008), this could not have been 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 featurerich language that also removes or replaces many legacy or ‘hacked’ commands that no longer had meaning or had a complicated or convoluted structure; greatly improved type safety; exposing low-level data that advanced developers need to access; and made it easier to write distributed applications. This bounty of new VB.NET features opened all the doors that had previously been closed to VB6 developers. 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 your executables and components from directory to directory, often referred to as ‘XCOPY deployment’, so 1 VB.NET is pronounced V-B-Dot-Net; just as .NET is pronounced Dot Net, and VB6 is pronounced V-B-Six. I have also heard them pronounced Vib-Net and Vib-Six, though this seems to be confusing and not as “cool” as their purveyors might think of it as being. 2 Robust: Bullet-proof; bug-proof. This usually implies sturdy code that is written to avoid errors, using strong types and error-trapping. 3 I find the term VB6 Purists to be humorous, because VB6 is not a pure language by any measure, but rather a mongrel mix of really cool features and concepts, added to by a hodge-podge of hacks and fixes, starting with VB1, chiseled together with brilliance, tricks, hacks and work-arounds to provide a GUI and windows forms to what was otherwise DOS-level QuickBasic. Even so, VB6 was for years my personal favorite. But the point remains that its language is structurally convoluted, often clumsy, and its program structure precluded much more modern programming concepts. Page –11– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben you no longer need to worry about DLL versioning issues. With VB.NET, this versioning danger and the resulting intense aggravation (often referred to as “DLL Hell4”) is a thing of the past. As importantly, VB.NET fully integrates with the other Microsoft Visual Studio .NET languages. You can even develop application components in different programming languages, and your classes can now inherit from classes written in those other languages, and visa versa, using cross-language inheritance. With a unified debugger, you can now debug multiple language applications, irrespective of whether they are running locally or on remote computers. Whatever Dot NET language you use, the Dot NET Framework provides a rich set of Application Programming Interfaces (APIs) for Microsoft Windows® and the Internet that are now, in the Dot NET world, commonly referred to as P/Invokes or Pinvokes; meaning Platform Invokes5 or Processor Invocations. Why Is VB.NET Not 100% Compatible with VB6? Why does VB.NET not fully support VB6 code, as written, just as VB6 and previous editions were generally able to support previous editions of code (with minimal redesign)? Because VB6 code is not supportive of OOP (Object-Oriented Programming). Many of the features VB6 users bemoan the loss of in VB.NET are features that: 1) are dangerous to the operating system, so safer means under Dot NET were implemented, 2) are not applicable to an OOP paradigm6, so Dot NET-compliant implementations were introduced, or 3) invited the development of less robust, bug-prone code (for example: As Any). VB1 through VB6 were procedural programming languages, which execute linearly, featuring simple procedural calls to functions and subroutines. VB.NET, however, is a fully Object-Oriented Programming Language (OOPL), which consists of objects that interact with one-another, each encapsulating their own methods and data. These paradigms, procedural and object-oriented, are vastly different. I find it amazing that VB.NET can in fact use as much original VB6 code as it does. Previous to VB.NET, an object-oriented language often required colossal source code re-tinkering to upgrade a procedural version of its language. A case in point is the procedural language C versus the object-oriented language C++. Granted, you could compile and execute straight C code, for the most part, using a C++ compiler and successfully execute it, but it would still run as a procedural program. To convert it to use objects as VB.NET requires, entailing form objects, control objects and such, necessitates monumentally exceptional feats of code restructuring. But the VB.NET Upgrade Wizard (available through VB2008, but sadly missing in later editions, though you can still install VB2008 Express to take advantage of it) does a dazzling job. See my free companion document, “Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades” (refer to the link on page 651), to learn how to address most every upgraded VB6 program code issue. There were two options that Microsoft had to consider when designing VB.NET: they could simply retrofit the existing code base to run on top of the Dot NET Framework, but it would still be stuck with being a procedural language, even though it would be able to run VB6 code right off the bat, or they could rebuild VB from the ground up, not taking any shortcuts, not sacrificing even one Dot NET feature, and also take complete advantage of a fully functional object-oriented platform. To deliver the features that were most demanded by their customers, such as inheritance and threading, to provide full and uninhibited access to the platform and to other platform languages, and to ensure that VB moved forward into the next generation of Web applications, the right, though vastly more difficult decision was to build VB from scratch on the Dot NET platform (and no, it is not a modified version of C#). 4 DLL Hell: When an older version of a DLL overwrites a newer DLL, and suddenly previously working applications start blowing up because they cannot find entry points in the DLL that had previously existed in the newer version of that DLL. This also includes newer DLLs that dropped older entry points, which clearly should be a developmental cardinal sin in professional-grade applications. 5 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 processor, which implies something outside of the 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 Delgates or Method Signatures, which are like C/C++ Prototype Declarations. 6 Paradigm: Organized method of performing a task. Page –12– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben For example, many, but not all the new features found in the Windows Forms package could have been added to the VB6 code base as new controls or as additional properties. However, this would have been to tremendous cost to all the other great features inherent to Windows Forms, such as security and visual inheritance, not to mention that VB.NET would have lost the powerful Web Forms package entirely, which allows your users to access and run code behind your forms directly from your web page without needing to install driver code onto their own machines first, as VB6 required. Also see ASP.NET. Once on this tract, one of Microsoft’s principal goals was to ensure that VB.NET code could fully interoperate with methods and classes written in other languages, such as Microsoft Visual C# or Microsoft Visual C++ (and visa versa), without caveats or being forced to implement hidden helper code. Another goal was to enable the VB.NET developer to harness the full power of the .NET Framework simply and with absolutely no impediments, and all without having to resort to internal support workarounds, which were traditionally required just to make Windows P/Invokes work with VB6. An example of such a hack was the code used to work around the inability of VB6 to employ method overloading (also known as function overloading), by introducing the messy and potentially dangerous “As Any” functionality to API Declarations. Method overloading, an essential feature common to Object-Oriented Programming, allows different methods to employ the very same method name, but each would utilize different parameter counts and/or types. As a result, when you invoke the method name, the compiler looks at your method parameter list, referred to as a signature or delegate, to determine which same-named method to invoke. This eliminates entirely the need to use an “As Any” cheat. Method overloading is easy and will be demonstrated in several examples within this document. VB.NET now has the same variable types, arrays, user-defined types, enumerations, classes, and interfaces as does Visual C++ and any other language that targets the .NET CLR (Common Language Runtime). As a result, they also had to redact features that did not play well with the .NET paradigm, such as fixed-length strings and non-zero-based arrays from the VB language, even though this document will show you with detailed examples how to still implement them (both of these features can be easily emulated using super-quick coding techniques, or by very simple user-defined classes). Why .NET? Though the .NET Common Language Runtime operates on Microsoft Windows® and Linux platforms, this will expand. As of this writing, development of support for other platforms such as UNIX (which Linux is modeled after) and iMac7 are either in the works, or are being conceptualized (considering that .NET’s F# runs on the Mac, it may already have a working CLR). The result of this will be that you can write code in .NET and run that code on various platforms and CPUs without changing the code or even needing to recompile. This is the core purpose behind Dot NET; to be a multi-platform development environment that can compile on one platform, but execute on many. This sounds almost like the much slower Java scripting language and its philosophy of “write once, run anywhere.” Unlike previous editions of Visual Studio, Dot NET compiles its code into an MIL (Machine Independent Language, and also referred to as Microsoft Intermediate Language, or just as IL). This MIL was carefully designed around advanced CPU (Central Processing Unit) architecture, crafted to be as close as possible to the instruction sets for most 32- and 64-bit CPUs. This MIL is technically an actual machine language, yet one for a hypothetical, though ideal CPU, but which can almost instantly be translated to native code with its built-in Just-In-Time Compiler. Soon you may be able to write an application on one platform and then execute it on another without change. This MIL is instantly compiled into native machine code on the target platform by the CLR’s just-in-time compiler. Hence, as 7 The iMac’s OsX is a Unix-based operating system modeled after Carnegie Mellon University’s Mach microkernel, which was in turn an upgrade/replacement for BSD’s FreeBSD and NETBSD implementations of UNIX. BSD is presently devoting heavily into research and development in the Dot NET direction. Page –13– Indeed. because its code is not being interpreted at all. which. we will clarify these points soon enough. Each more complex class object often inherits a simpler base class as its foundation. be installed on a system before one can use it. popular with many in-house development languages.NET CLR. the interfaces of all its ancestral classes. it also runs much faster than interpreting text source code because the code is already in an intermediate state between the source code and ready-to-run code. The great thing about it is that you can start developing code without having to understand every little stinking thing before you can write a program. even forms and modules. At the very core of this structure is a single root class named Object. Microsoft DOS BASIC was such a tokenized language. even our own. This is the first thing that you should know about Dot NET – everything in its environment. What is the . is actually a class or an instance of one.NET Beyond the Scope of Visual Basic 6. it is filled with nothing but classes. Variables are even class objects. to many this may almost appear to perhaps be an interpreted or a tokenized p-code8 language. As a result it became an instant monster hit among users. This is an intermediate. but nonetheless they can feature properties and methods. which might be unfamiliar to most new Dot NET programmers. though it might appear to us mortals as a tree-like structure that is filled with classes. But trying to absorb everything all at once will only result in you forgetting most of it. such as the above. and turn people into Dot NET experts.0 – David Ross Goben long as a platform has a Microsoft . and so on. this approach is no different – though nowhere nearly as complicated – as requiring someone to install a Windows Operating System prior to them being able to run applications designed for a particular Windows environment. I had just mentioned that the . and causing you anxiety due to information over-load. Microsoft BASIC80 in fact ran scores of times faster than non-tokenized versions. Normally. Basically. constructing most classes seems no different from constructing any other ordinary class. though you certainly can if you so desire. the entire Dot NET environment is wrapped within by a thing called the . like any technology. Understanding this terminology and what they imply in the scheme of things will go a long way toward making sense of it.NET Framework? In a nutshell. 8 P-code: Pseudo-code. tokenized version of a program. Granted. Presently. though still interpreted on many platforms.NET Framework is filled with classes. you might remember saving your pennies to get the Level II BASIC upgrade for the TRS-80 Model I or Apple computer so you could write “real” programs beyond their default and primitive Tiny BASIC. to start. It is even rumored that such institutions as Intel and AMD may even be playing with the idea of developing a CPU or an add-on hardware device that will be able to execute MIL code directly. your code can run on that platform. just as it is standard for computers to come with an Operating System installed.NET Framework. albeit a variation of it known as abstract classes. From this primordial class all other classes in Dot NET are derived.NET Framework is a runtime execution environment that manages applications.Enhancing Visual Basic . Obviously. Rest assured that you do not need to fully understand most of it when you first get started. If you have been using computers long enough. as a part of its bag of tricks. Before anyone complains. however. This framework must. What is Dot NET? The Dot NET environment is filled with terminology.NET Framework pre-installed (or you can download them free of charge). the . it most definitely is not. and the module is automatically internally imported so that you do not need to fully qualify a method or property within it by referencing the module class name in order to access it. Even your regular VB Module files are classes. This progression builds until you have truly complex classes the bear little resemblance to their ancestral roots. Picking this stuff up will also present you with some really cool tricks that you can use to spiff up your code. Still sound strange or confusing? No worries. all recent versions of Windows have come with the . without the need for just-in-time compiling. Page –14– . but none-the-less share. but you can safely relegate such comprehension to periods when you have time to do so. and you can most probably get along by knowing virtually nothing about it. and many of those sub-classes in turn are other Namespaces that are populated by more sub-classes. where each branch consists of a Namespace class (see later) that is populated by sub-classes. where all exposed members are treated as Shared. though we might not even notice it. a fuller understanding of it is immensely helpful. Dot NET Namespaces simply organize classes into groups of subclasses that are usually expected to work together. enumerations. which may be classes. and properties relating to VB. The VisualBasic namespace is a child namespace of the Microsoft namespace (there are basically two root namespaces – System and Microsoft – and everything else. branches from them as sub-classes (yes. In the most simple of terms: adding a namespace is just like adding a non-visible VB Module file to your application. methods. but they do clean up quite easily when they each go out of scope (accessibility). if you wish. This is on top of the fact that any class in this framework can also become a branch of its tree. Everything within the .NET Framework is comprised of terms like Namespaces. just by selected source files if the namespace had been specifically imported into them. you are thus prevented from instantiating a namespace to an object (this would be pointless. thus implying branches and leaves. all layered within this onionskin-like framework. a namespace is non-inheritable. are also wrapped inside a wrapper class known as a Namespace. etc. but point to them. you can declare classes within other classes in Dot NET. For examples. exactly like you can Structures. grouping of classes within a namespace is a bit of an esoteric art. It is like defining properties and methods for a class. Anything else is useless and poor design. When you import a namespace. where each broad branch. it is inaccessible from outside applications. and they also automatically clone themselves (make new copies) when assigned to a different variable. Namespaces do not follow any stringent organizational rules regarding the classes that can be defined within them. Indeed. When you create a new project or suite of projects. or methods. where certain of these classes comprise additional Namespace branches. for example. However. which can be accessed by the whole application. just like with a disc folder – sure. if.).NET Framework.NET Framework was like a tree. However. What is a Namespace? Namespaces contain classes that provide programming interfaces. the name given the namespace is the same as the first—and often the only—project. usually defined as a special non-inheritable class. A namespace is actually a non-inheritable class wrapper that envelops classes or a project.NET. and all of its non-private members.NET Beyond the Scope of Visual Basic 6. Within the VisualBasic namespace you will find most classes. It is formatted much like the directory structure of a disc drive. the Constants and ControlChars enumerations are children of the VisualBasic namespace. Then.0 – David Ross Goben NOTE: The difference between a Concrete and Abstract class is this: Concrete classes must be instantiated using the New keyword and associated variables do not actually contain their data. or. but it makes more sense to place things that have something to do with each other. anyway). that namespace is auto-instantiated. For example. constants. Likewise. Hence. The VisualBasic namespace holds classes relating to VB. Page –15– .NET Framework goes.Enhancing Visual Basic . one of their properties is a Namespace. Else. in-as-far as the .VisualBasic. This makes them a bit messy because they can make copies of themselves all over the place. By default. Abstract classes are not instantiated but contain their data. or is considered a part of a suite of solutions for something. is referred to as a Namespace. you normally define only properties and methods for a class that actually have something to do with that class. Again. The structure of the . Application Domains. Assemblies. but containing their own embedded child classes. a namespace would hold classes that have something to do with each other. It in turn envelopes a collection of related classes. Solutions. and Projects. are marked as Shared to referencing files. to include every program that you develop. you can put anything into them. It is simply a convenience for the developer that related classes can be declared in them. though most new Dot NET developers might not even be aware of it. if you have multiple classes defined in separate projects that are related to each other. enumerations. you might wish to change the default namespace for each to be identical if these projects will be working closely with each other. properties. such as If. which are Abstract Classes)). But keeping related classes together is just common sense. there is a namespace named Microsoft. your project is a DLL.NET (you therefore will of course not find language-specific syntax keywords there. Let us take a brief look as these from an outsider’s perspective. I said that the structure of the . During assignment they will transfer just a reference to that data. or a derived class. if you are writing code in C# or Managed C++. structures are classes. of which VB. are treated just like VB6 Modules. but is a very cool bit of trivia: VB.Enhancing Visual Basic .0 – David Ross Goben You can bring up the Object Browser from the View menu and explore these namespaces and classes for yourself. even though it was helpful beyond words). Conversely. each can be defined with a scope of Private. Structures. where you would change data through pointers (pointers are often used to target actual memory locations). Page –16– . Public members can be accessed from outside the class. As such. Dynamic Help should be one of your new best friends for VB2008. Instead of their values being generic. Friend members are treated by the application as if Public. declare all of the non-private variables and methods that they want to be accessible to the application as Shared. Structures. as will also be demonstrated later within this manual. Doubles. but as Private to all outside applications that could otherwise access its parent. in Object-Oriented lingo). but is directed at all CLR languages. Modules. such as a DLL. Public. but sadly gone as of VB2010 because apparently it ate too many resources.NET is now a full member. We will briefly discuss how to access namespaces (referencing and importing) when we take a look at the structure of code. Integers. to include by other applications that can access its parent. as opposed to reference-type objects (a concrete class) that most classes resolve to. Unlike Visual Studio 6.NET code needs to use something like pointer manipulation. A cool trick. However. or Protected. you can use the Dynamic Help to explore them in exhaustive detail.NET Beyond the Scope of Visual Basic 6. even though you can in fact do such from VB. Well. Private members will be visible only to within the structure or code block they are declared within. and can help you find answers to anything that you find of interest. Varianttype expressions. being treated as a noninheritable namespace. We will be looking deeper into these two types a little later. by appearance. Modules. can also include methods and properties. 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. Also. but a much-welcomed enhancement in Dot NET is the ability to declare them with a storage class (storage type). and includes VB examples for all related features. though this can never hurt in-as-far as self-documentation is concerned. unlike the very primitive User-Defined Types of VB6. you can declare the values of an Enum to provide Singles. unlike a regular class. and Enumerations.NET. if your VB. the Dot NET Help System is not mostly directed at C++. For certain. Suffice it to say for now that there are four types of objects that can be created at the namespace level: Classes. cannot be instantiated as an object simply because a module is auto-imported into a project. is that you can actually access the Visual Basic classes and invoke their functionality. slow. This is really handy when there is a VB-only method that you want to take advantage of from non-VB language code. so as you click items in the Object Browser.NET modules are actually classes. Friend. All they have to do is to declare a class as non-inheritable. These treatments were done so that VB6 developers migrating over to Dot NET would not suffer a grand mal seizure because they no longer had Modules available. Enumerations should already be familiar. non-private methods and variables in the module are handled within the application as Shared. there is an important internal difference that perhaps you need not worry yourself about. and such. The key distinction of a module is that it. A great hint for VB2008 users is to have the Dynamic Help tab open and in view (found in the VB2008 Help menu. technically they can. except that they are what is called a value-type object (an abstract class. then you can write such code in C# or C++ and invoke it from VB. I have heard a number of C# developers complaining that they cannot likewise define C# modules. Protected specifies that the item is accessible only from within its own.NET by using deeper Dot NET features and processor invocations. its list of resources. and the other optional projects could produce only nonEXE files. Frankly. A Solution is the final result. and forget about their real technical descriptions. Again. which is a great feature of Dot NET. in the end. which had previously been a nightmare for those who had multiple executables that shared data space. most of you will never have to worry about this. it also creates a thing that wraps around the Project called a Solution. You should set these in the creation dialog if you will in fact require them to be different from the default. the headaches have been marginalized. and maintain our software projects. suffice it to say that when we talk about a Solution or an Assembly. and within the solution(s) to finally create their project(s). when you go about creating new applications.0 – David Ross Goben What is an Application Domain? An Application Domain is a solution or group of solutions that work together. its auto-generated Globally Unique Identification number (GUID). because from the outside they seem to be the same thing. and I associate the idea of an Assembly as this single AssemblyInfo file. By default. but those who do write such complex programs. and its version number. a DLL. and such. defining the Solution's fingerprint. Each project contains only one compiled object. focusing instead on single executable applications. found in the Solution Explorer. most applications do not need to work this way or even to worry about it at all. as had been a “feature” under VB6. or some other object. and this latter approach would therefore be nothing more than a peevish bother than a help in most development projects. then to create solution(s) within it. guarantees that components cannot interfere with each other. It all comes down to perspective. such as an EXE and supporting DLL files. This process separation. the wrapper that can hold one or more projects. and to most users these terms appear interchangeable. usually the only time you personally need to be concerned with an Assembly is when you want to edit the application description. Instead of pulling out diagrams and cracking thick tomes to explain them. Granted. though their differences might appear a bit gray. we are talking about pretty much the same thing. This somewhat cut-to-the-chase approach is a well-received feature for most developers.NET releases and the command-line compiler can compile solutions with more than one EXE. Beside. I tend to put Solutions and Assemblies together. I think of the whole thing as a Solution. giving it the same name as the initial or only project (a Solution is simply an organizing wrapper around a project or group of projects – see below).vb file. When you create a new project in VB. Page –17– . which will also create a like-named Solution and Namespace. Indeed. What is a Solution/Assembly? Solutions are how we author. simply access it and look for yourself – it will then become quite obvious). the name of the solution will be the same as the name that you provide for the first. What is a Project? A project is the executable/control/DLL you are developing. Personally. or open the project properties and select the Assembly Information button to edit this file through a much safer dialog interface. and usually the only project you created. as well as a Namespace that can wrap itself around one or more Solutions (also see below) with the same name as the initial or only project. Although it would at first seem logical to first create a namespace. whether it is an EXE. Note that older VB. In this case you can simply open the AssmeblyInfo. but are completely separate from each other in managed memory by the CLR. but you can just as easily change them later. though you really do not have to bother your head with even knowing that it exists. An Assembly describes that Solution. title.Enhancing Visual Basic . this is a concept that. An Assembly is a collection of resources that forms a logical unit of functionality. The fact is that most of you could have gone through the rest of your careers without thinking about it even once. and modify the entries (if you do not understand what I mean. package. though there is generally little need for it. Later VB. such as DLLs. the solution and namespace can be redefined later with absolute ease.NET Beyond the Scope of Visual Basic 6. is quite helpful. version number. Dot NET will be prompting you for a new project. especially the pain of trying to quash circular references in released objects that somehow kept resurrecting themselves.NET Solutions could only hold one EXE.NET. sharing memory. Again. procedural languages. properties. predictable known output results are guaranteed. The class defines the fields. as opposed to VB6. but I keep running into many self-proclaimed advanced developers who have authored only a few classes. The VB2008 edition of VB. Objects.NET. the following terms and concepts will help you get a head start in gaining a basic understanding of the Object-Oriented Programming Language paradigm. which allows developers to use these convenient module classes just the as they did under VB6. when known data is input.NET are control and form containers. If new to object-oriented programming.NET. Each instance shares a single block of class code with all other instances of the same class. when grouped together. Just keep reminding yourself that it is a whole lot easier to do than it is to explain. most of this gobblety-gook will be invisible to you as you go about writing control events. and also lends itself greatly to a development team environment. tried to straighten this problem out. 9 Black Boxes: Predictable widgets. Another development in VB. will define the complex outer class that a primary program thread manages. for example. Everything in VB. which. allowing their working content to be ignored by all. or were not exactly sure how classes were actually handled internally. A class describes an object. where each object is a self-contained building block that is plugged into and link together by invoking each others’ program code. In VB.NET worked around most incompatibilities between an OOPL like VB. to include making the new Integrated Development Environment (IDE) reasonably familiar and compatible to that of VB6. The introduction of structured programming by languages such as Pascal. Because these objects are like black boxes9. the need for developing a design paradigm is critical to application development. is derived from class definitions. it does not contain a copy of its class’s source code. However. A full understanding of a Class and the Objects instantiated from it is critical to mastering Dot NET. is VB Modules. a tangible manifestation of the class that is accessible from code external to it. methods. even a basic introduction will clarify the tremendous advantages it has over conventional programming that had spawned the development of maddeningly confusing “spaghetti” code and “half-cocked” programming.NET. promote and almost force structured design.0 – David Ross Goben Object-Oriented Programming VB. the classes that describe them. such as VB6. this is old news to you. and how they are incorporated within Dot NET. An object is an individual instance of the class. and even each button and control that are contained by the form. these smaller classes are self-contained and can be separately tested. Each object instance contains only data. where each member of the team can work on a separate module. VB modules are actually classes that are automatically imported into the IDE (see the Imports command). ADA. such as the portion of your program that will use it. and some none at all. and C. which are files whose public contents were accessible from other files as though that code was written within the invoker’s file. Classes and Objects It is critically important that you fully comprehend the idea of objects. functions. before it is plugged into the main application.Enhancing Visual Basic . except for those whose job it is to develop.NET Beyond the Scope of Visual Basic 6. preferring instead linear. Each sub-class has a particular set of tasks to perform. and service those widgets. but failed miserably in that it was too easy to work around. on the other hand. Page –18– . just as they were used to doing under VB6. If you have written lots of classes. Truth is. Algol. is a full-fledged Object-Oriented Programming Language. and subroutines just as you did under VB6. The typical flow of an OOP project is to break each task or class down into more primitive classes or methods to consolidate common features. A great example of this. test it and debug it individually. and events by which the ‘personality’ of object instances of it is established. provide. as discussed earlier. allowing developers to access these containers like collections. though their inner workings may be unseen or not known except by its developer. Each module can be further broken down into simpler separate classes that in turn perform even simpler tasks. Anyone who has ever been involved in a large and complex project realizes (the unlucky ones realized it after the fact) that breaking a project down into smaller self-contained modules is highly beneficial.NET and a procedural language like VB6. Although people new to OOP design might not imagine much practical use for OOP. such as a form. to include simple variables. Also unlike classical objects. and events) that manage its data. This is called encapsulation. of course. which an OOP calls attributes). and therefore they do not have to be released to the Garbage Collector. such structures are called Abstract Classes. methods. and outside references that support the application comprise other objects.NET are similar to VB6 Type blocks. Objects and their class definitions are therefore developer-friendly. the data and code assigned to them are not simply pointed to by them. such as a Form. Even though they can be very messy. Its Data is the content of their procedure-level variables. More on this will be discussed later. an object is something that contains data and should always protect that data. but instead they are self-contained structures that actually encapsulate their data. Classes are quite clearly the heart of Object-Oriented development. An Object is anything you can think of. A class that contains only the methods that manipulate the data of its objects is much easier to debug. which the form manages interaction with through events and properties. We also learned that there are two types of class objects in OOP. their data is automatically released when they go out of scope (visibility/accessibility). and being Abstract Classes. although they are constructed very much like classical classes. and each object instance of the class storing its private data in each instance’s separate and unique allocated storage space on the Heap. which are stored in a separate memory pool. light bulbs.0 – David Ross Goben Classes wrap related methods and properties together. They also do not create classical objects. is an example of a Value Type Object. they differ in that. subroutines. They can in fact be initialized using the New keyword (this is used to concurrently initialize the fields of the structure during its creation. An Object is an instance of a Class. all objects are derived from a class definition and that class supports its objects with properties and methods. they follow object-oriented programming rules. leaving copies of themselves everywhere they go. not to instantiate it as a classical object). As mentioned previously. We have learned that in OOP. The buttons and other controls on the form are also objects with their own class code. they are like regular classes in that they can also contain properties and methods. imported classes. like classes. because their makeup is just like a simple VB. In computer terms. How can you make a program out of a bunch of that stuff? But in simpler terms. they also clean up very easily and very quickly by their simply going out of scope. When you define a variable as a new Integer. Value types are typically called structures. you do not define it as a new instantiation of an integer as you would with a user-defined class (you do not need to use the New keyword to create it). an object is a tangible representation of an abstract concept that we want to manipulate in our application.Enhancing Visual Basic . Classes are important to object-oriented programming because they allow you to group related items as a single unit.NET associates with objects. phones. Though Structures in VB. cars. Page –19– . under Dot NET. customers. with one copy of the actual program code of the class being shared by all object instances of that class. reports. In OOP. and everything in VB. except. or if they are specifically declared to be non-inheritable – meaning they can only be imported.NET Beyond the Scope of Visual Basic 6. and they enable you to fully control their visibility and accessibility to other procedures or objects. which should relieve any confusion you might still be feeling. What is important is that an object does not contain just data (properties. modules. Other aspects of the program. etc.NET Structure (a User-Defined Type in VB6). known as the Heap. allowing its data to consume space in the application’s memory reserved for object instantiation (often referred to as the Heap). an Object is the Form that comprising the user-interface of a VB program. It also references the programmed intelligence of its class to invoke related actions. We work with physical objects all the time: televisions. meaning that. Classes can also inherit and reuse code defined in other classes. such as a bill object being able to print itself if its class incorporates a print method (subroutine) that can be invoked (called). Many people new to OOP cannot make sense of objects being the encapsulation of data that includes methods to support that data. and events that it can reference in its class definition should support its data and be the communications conduit to users of each of its objects. such as an Integer. • Value Type Objects: A numeric variable. an Object contains data (storage fields and properties) and reference methods (functions. Ultimately. being Value Type and Reference Type. causing the program code of the class is be brought out from concept into manipulative tangibility. The properties. applications. you can create a new class named 10 Invoked: Summoned out of something else. one would store fields as Private. These types must always be instantiated using the New keyword. Properties. and often referred to as Members of the class. accessing a property member of the object acts just as if one were directly accessing a publicly declared field or variable. this is like a form and its code. You may have referred to executing these as calling them under VB6 (and Microsoft still does). “Public carColor As Color”). and are also called Concrete Classes. and other members are treated as a single unit. Encapsulation. Methods. As under VB6. An example of an event for the "Car" class would be a "Check_Engine" event. methods. but it is preferred in OOP to say that they are being invoked10. Inheritance. and events.NET. and can be customized. subroutines (Sub) and functions (Function). or transmits to other objects or applications. To the consumer (user) of the object. you could store its color in a public field named "Color" (for example. such as a mouse click or keyboard action) are only one part of the object-oriented programming equation. as abstract classes do. needs to do something. For example. or typical classes. which comprise the object’s data). • Events are notifications an object receives from. though they seem transparent to invokers under VB. and events (subroutines designed to react to event triggers. You define methods by adding subroutines or functions to your class. This layer of indirection between the value being stored and the procedures that use this value helps isolate your data and lets you validate values before they are assigned or retrieved. inheritance. and implemented through property Get and Set access attributes. to include overriding (superseding) methods existing in the base class with newer methods that exhibit different functionality. or from a joystick. Consumers of the object need not concern themselves with the inner gearing of the object." and "Stop" methods. events can come from other objects. Page –20– . classical.0 – David Ross Goben • Reference Type Objects: These are considered regular. The reason behind this is that to invoke something is to imply that it is something called out from within something else. Events enable objects to perform actions whenever a specific incident occurs. True object-oriented programming also requires objects to support three qualities: encapsulation.NET Beyond the Scope of Visual Basic 6. Methods are essential. • Methods come in two flavors." "Drive. a "Car" object could have "StartEngine. • Fields are data the object contains. if you have an object named "Car". Encapsulation makes it easier to change your implementation latter on by letting you hide implementation details of your objects. but are instead just a simple reference pointer to the actual encapsulated object that is stored in a protected memory pool called the Heap. Normally. For example. such as a form. properties (methods providing information about an object). a keyboard. or from an input device. For example. Fields are like variables because they can be read or set directly. such as from an object. The new class inherits all the properties and methods and events of the base class. clicking a form button executes code defined within a Click event method. • Encapsulation means that a group of related properties. Methods therefore represent actions that an object can perform on or with its data. For example. after all. and polymorphism. and Polymorphism Fields (variables declared within a class but at the method level. and actually access this private data through Public Properties. they simply use (consume) them. methods. • Inheritance describes the ability to create new classes based on an existing class. Variables assigned to these types of objects do not contain the data of the object. These attributes provide greater validation control over how field values are stored or returned within the class. Objects can control how properties are changed and methods are invoked. like a button control dropped on a form. and Events Classes consist of fields. In simple terms. an object can validate values before enabling property changes. and are in fact declared exactly like VB variables. a practice called data hiding. most objects. • Properties are methods that access and set information about an object as though they were fields or variables. or extended by apply additional properties and methods. because. properties. Because Microsoft Windows is an event-driven operating system. enhanced. Fields. such as a mouse. methods (subroutines and functions that provide actions to perform on an object).Enhancing Visual Basic . If an inner block declares a local variable with the same name as a local variable or field declared in an outer block. for example. a series of functions all named Cinteger that return type Integer. such as a block that has been declared at the very same level within an encapsulating block. • Shadowed members are used to locally replace a member having broader scope (visibility) with a member having a narrower or smaller scope. regardless of what type of object is in use at the moment." polymorphism allows a programmer to define different "StartEngine" methods for any number of derived classes. no matter what type of "Car" object is being used. Further. Other procedures or methods can use the "StartEngine" method of the derived classes in the same way. but accept a different number of parameters. cannot be inherited. such as subroutines and functions. just include Inherits myBaseClass under the heading of the new class.NET Beyond the Scope of Visual Basic 6. For example. then the inner variable takes precedence at its level and inward during referencing.0 – David Ross Goben "Truck" based upon a "Car" class. you can declare a property that shadows an inherited method declared as Public with a method of the same name that is declared as Friend. A VB. Overloading. like-named variable is declared. Page –21– . The "StartEngine" method of derived class "DieselCar" may be completely different from the method of the same name in the base class. Overridden members. Most of you are aware that if a method. one is a string. which is a self-contained block of code. and Shadowing Overloading. Although all three techniques enable you to create members with the same name. is declared Public. For example. but each exhibits a different type parameter. or Protected. but can also add its own additional properties such as "FourWheelDrive.NET rightly allows only the Dim keyword to declare private variables within methods and properties. Shadowed members. a Select block ends with a matching End Select. another is a short integer. Derived classes will inherit these overridden members. must accept the same data type and number of arguments. which is used to dimension additional space on the program stack. and often contain blocks within blocks. For example. A Class block is defined from the declaration of the class to the matching End Class declaration. given a base class of "Car. Any type can shadow any other type. prevent code in one block from directly accessing a variable in deeper. or in separate inner blocks declared at outer levels. Polymorphism is important to object-oriented programming because it lets you use items with the same names. or a lifetime. and another is double precision. • Polymorphism allows you to have multiple classes that can be used interchangeably. however. Scoping Rules The visibility or accessibility of an object is handled by what are known as Scoping Rules. however. The "Truck" class inherits the "Color" property from the "Car" class. and shadowing are similar OOP concepts that can be easily confused. overriding. it can be accessed by everything within the program. much like the code and data declarations within an If-Then-Else body. you want to inherit from. inner blocks (even though code at those inner locations can access variables declared in the outer.NET program is composed of code blocks. Scoping rules do. preexisting or a creature of your own creative genius. which is used for local variable storage). there are important differences: • Overloaded members are used to provide different versions of a property or method that have the same name. from the point of its declaration to the end of the block that it was declared within. At the end of the block it falls out of scope and is dissolved. however. A local variable has viability." Inheriting a class is as easy as tripping over invisible logs in public. • Overridden properties and methods are used to replace an inherited property or method that is not appropriate in a derived class. where myBaseClass is the class. parenting layers). an inner block can access local variables and fields declared in outward scope where naming collisions are not encountered. or in blocks where its own scope cannot reach. even though each class implements the same properties or methods differently. Private. unless a deeper. and Private methods are accessible locally to the file or method or block within which they are declared (VB. an If block ends with End If. where each block is a grouping of encapsulated code and data.Enhancing Visual Basic . or parameters with different data types. Overriding. and so on. myName”. but not in “F”. My Basic Guidelines for VB.Items. When the Dim keyword is used to declare a variable in the body of a Module or Class (which is a thing called a “Field”. it is so simply for backward compatibility with older versions of BASIC. Friend.0 – David Ross Goben From that gibberish we should have surmised that outer variables are visible to inner variables.C. if you process the data as members of the classes. Option Strict On forces strict type checking. This accessibility to inner blocks changes. but not directly in or within “B”. “E” and “F”. On the other hand. I have always preferred to declare variables outside of method bodies (functions and subroutines) as Public. In VB6. or Protected in the declaration. no objects but those declared within classes “B” and “C” could access “myName”. being able to specify myObject. This does not add additional code to your compiled program because it represents code that would otherwise be automatically added by the compiler. Code in “E” can access variables in “A” and “E”. It also goes a long way toward self-documenting the code and eliminating hard-to-find bugs that cast it to a type that might cause issues when used later on. as most people assume. scope visibility works in related blocks. This might sound a bit crass. default to Public. Although it is legal to use Dim instead of Private at this procedure level in a class. I would prefer to see the code perform an explicit CSng() or CType() function. And anyone who thinks that automatic type casting is great is a developer who I would not hire – Page –22– .Items(10) instead of myObject.Text = "Hello"? What does the second example show you that the first one does not? Anyone who complains about the loss of parameterless default properties needs to learn how to really develop code. it does not. or even “A. and “F” can access that string stored within class “D” by using the reference “B. where we can see each block being declared in concentric class circles from outer to inner. For example. Following are four fundamental rules that have helped me to stay out of a lot of trouble while using VB. implying a static data field that exists for the lifetime of the object). being siblings of “C” within class “B”). but not visa versa. but class “D” were declared Public. code in “A” cannot directly access variables declared in classes “B” through “F”. Conversely. I feel that anyone who likes the idea of automatic type casting or thinks it is no big deal is an amateur. being declared Private.B. Also. but Private to all external processes. and a publicly declared string variable named “myName” is declared within class “D”. a person reviewing your code will know you intentionally performed this task.Item(10). Private. it is worth mentioning that if you declare a subroutine or function without Public. temporary variables (their lifetimes last as long as control is within their code block). which is Public in its scope. the easier it is for someone else to read. and reserve using the Dim keyword exclusively to within a method for declaring local. “E”. For example. what makes more sense to you as someone who did not write the following code: myData = "Hello" or myData. 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”. not allowing automatic variable type casting. The more obvious one makes the code. if class “C” were declared Private. Option Explicit On requires that all variables be declared before being used.Enhancing Visual Basic . When you need to convert the value of a Long variable to a Single variable. unless an identical variable is declared locally. However.NET: • • Always set Option Explicit and Option Strict options to On. or within an intermediate block from the location trying to access the class “A” level variable. thus. however. Consider the illustration. this is the same as declaring it Friend. then code declared within classes “A”.NET Development Everyone has a set of rules that they have grown into or developed over time. Anything else should normally be taboo.C. Why? For reasons of self-documenting code. it actually defaults to Friend. The only default property that you should ever use is indexed items in lists.D. Private. Again. While on this subject.D. if classes “C” and “D” are declared Public. or Protected. Code in “F” can access variables declared in “A”. Friend. because class “C”. but I think anyone who leaves this option Off is an amateur who does not feel responsible enough to care about ensuring their code’s reliability. Variables declared in class “A” are visible to all other classes. or in or within “B”.myName”.NET Beyond the Scope of Visual Basic 6. Personally. Defaults sometimes change. or a participant in it. and might even declare with inner-conviction that VB is now broken. These are much more self-documenting than using any default behavior. When people examining your code make the wrong assumptions about it. which are. properties. or fields declared outside of these methods.NET Beyond the Scope of Visual Basic 6. such as indexed lists. The end result was that the VB1 through VB6 family had finally digitally painted itself into a corner. VB6. In that time. VB6 used 16-bit integers. worlds apart.cooper. last available in VB2008) cleanly from a previous version of VB. And every time. avoid at all costs taking advantage of non-self-documenting default behaviors. if anything was broken. In other words. awaiting one’s attention. it was VB6. Why I am called “the Father of Visual Basic”. and dinosaur technology like Direct Data Exchange (DDE) was no longer supported (well. or Protected goes a long way toward telling someone who is reviewing your code of what your intentions actually are. when in fact it actually defaults to Friend. Use the Dim statement to declare only variables within methods (subroutines and functions) or properties. and could upgrade code all the way to VB6 with little or no change. which are clearly flagged in special. Friend. It was having enough trouble coping in a 32-bit world.html). sleep-deprived howls of aggravation echo unrequited and muffled in the fog-shrouded hollows of the night. traceable comments within the resulting code. Explicitly declaring classes and methods as Public. when Alan Cooper demonstrated his original visual concepts to Bill Gates.com/alan/father_of_vb. or a VB6+1 (which Microsoft originally intended to develop. individual languages have changed significantly. After all. Please do not depend too heavily upon default scoping rules. it makes it much more difficult for them to locate the errors that a second set of eyes is normally capable of finding. Page –23– . was that VB keywords and syntax have changed. the upgrade process from one level of a language to the other has always been frustrating. probably shouted the loudest. Self-documenting code Rules. at http://www. and clenched fists have always been shaken at the dark sky as our forlorn. and released as Project Thunder. and expansion could now only be incorporated through additional hacks. It failed time and again to follow syntax rules when it realized new features.0 – David Ross Goben • • their code will most likely have hidden casting anomalies in them. Half the people I talk to think that declaring a class without such a scoping keyword defaults to Public. But in late 1999 they realized that VB was going to be too constricted in the rich Dot NET environment) could never have survived in a 64bit world. systems standards were 16-bit. A language’s integer size should normally comply with the architecture it operates upon. Plus. and merged with QuickBASIC to become Project Ruby. this was generally not the case for code within VB1 through VB5. Not every new developer can quickly answer the type of a method-level variable declared with Dim (it is Friend). on top of the fact that they are moving from a Procedural Programming Language to an Object-Oriented Programming Language. enhanced. We all should know that variables declared with Dim inside a method is local to the code block within which it is declared. Another complaint. typically requiring numerous additional after-upgrade edits. I have been a software engineer since 1978. except where they make sense. whereas a 32-bit platform uses a 32-bit integer. where even its base integer type did not match the CPU platform’s integer type. Private. technology had changed profoundly since its introduction in 1991. Use Private and Public (or Friend) declarations on methods. which usually required just a simple recompile and minor tweaks. VB’s roots began in 1988. Welcome to the REAL world of software engineering. as though they had been simply plugged in from other specifications. Hence. and device communications were hand-coded (refer to Alan Cooper’s online article. At that time. but this is a luxury that is amazingly unique to VB for computer languages. It all comes down to this: You can be a victim of change. VB1.NET will not upgrade (using the Upgrade Wizard. Project Tripod was later purchased. and in fact had long ago passed it up. actually it is – through COM references).Enhancing Visual Basic . they seem to feel cheated. This is the price we must pay to progress forward. Conclusion to the Introduction One of the biggest complaints I hear from VB6 users is that VB. Honestly. programmacly. Prior to VB2005. monumental paradigm shift should take so much time. native VB. Professionals sigh. this “nodding” support was in no way FULL support. but they are supposed to make software development easier with each successive generation of the development environment. Upgrades from VB6 to VB. I believe that Microsoft finally got Object-Oriented Visual Basic right. which requires a staff giving it FULL.NET was Beta until VB2005 was released. Microsoft did claim to provide “extended” (actually. rocky predicaments VB2002 and VB2003 placed us in.NET. FORTRAN (Formula Translation). not more complicated. 2008.NET have finally become easy. They have it easy compared to some language upgrades. Try translating between C (K&R C) and C99. VB2008’s debut was on November 19. When VB2008 was released.NET tools). Upgrades bite everyone.0 – David Ross Goben While most VB6 users feel they are alone in this dilemma and are the sole victim in this language upgrade predicament. But since VB2008. But everyone should have expected that such a necessary. And VB2010 throughVB2015 has only made this progression even better. it was a severely rocky path. and perhaps the whining and complaining I heard were to a great extent justified. 2007. and then hit the books. The pre-VB2005 . 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. or even C++85 or C++95. But every professional who has been in the business long enough has also gone through these cycles enough times to now just roll their eyes each time it happens.Enhancing Visual Basic . myself. and syntax is so-o-o-o much friendlier to VB6. I. Further. The introduction of the “My” namespace should have been introduced much earlier than VB2005 to address that situation. However. or better. I thought VB2008 was a more valid development environment release than the sad. I considered VB2005 to be a Release Candidate until VB2008 was released. was apprehensive about the upgrade from VB6 to VB. Advancements in high-level languages might require changes in coding habits. Goodbye. between upgrades of the very first high level language. In my mind.NET development has finally regained its highly coveted status as a Rapid Application Development environment. due to such changes. VB. proactive attention (but I will grant that they did develop some excellent leveraging-VB6-to-. If there is fault to cast on Microsoft. and by my determination. such as between FORTRAN57 and FOTRAN77. It is quite often not an easy pill to swallow. sometimes severely. I would call it “nodding”) support for VB6 until March 31. Amateurs whine and complain. upgrades have become easy.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. VB6… Page –24– . they most certainly are not. It has happened to me more than I care to mention. the thought of cycling from coveted ‘guru’ to rank novice. or even to FORTRAN95.NET Beyond the Scope of Visual Basic 6. However. "#. Because expressions result are value-types. Page –25– . ensure the Microsoft. or.VisualBasic. "Short Date")”. then until you are comfortable with VB. and so expression results. that is no longer true.NET. Personally. it would actually be much faster to take advantage of VB. and all without much editing. But regardless of that. Indeed. For example.NET Beyond the Scope of Visual Basic 6. we could change the above “S = Now. This allows you to import this namespace in each class or module file it is needed in.Compatability namespace has a check in its checkbox to force an application-wide import of the namespace. are upgraded by the Upgrade Wizard (this wizard is sadly lacking as of VB2010) to “myStr = VB6. Many have complained that although VB. where you can specify short or long date and time formats. Either way. "#. The functions provided in the VB6 compatibility namespace mimic VB6 behavior while remaining compliant to the Dot NET Common Language Specification (CLS).ToShortDateString” assignment to “S = Now. in the Imported Namespaces panel.NET’s built-in functionality and instead upgrade it to “S = Now.NET help using “vb6 compatibility library” for additional details.NET FormatDateTime() function and its DateFormat enumeration. Another example regarding the Format statement is in expressing dates and times as text.VisualBasic. Microsoft says that when a VB6 application is upgraded to VB.##0")”.NET without the need for extensive modification.NET has reserved to express dates and times in just about any format you wish. but I do not recommend this. VB6 statements using the Format command. "Short Date")” (which VB2008+ does support) and converts it to “S = VB6.NET. I prefer to take exclusive advantage of VB.ToString("d")”.##0")”.Enhancing Visual Basic . You should search the VB. the VB. or are New to VB6  Notes Regarding the VB6 Compatibility Library If you came from VB6.ToString("#. one can truly spread their wings and make their code really shine by embracing the liberating environs of the Dot NET Framework. a shortcut tag representing “M/d/yyyy”. where you can optionally specify date and time formats with very little typing. you can speed up this code even more by using instead “myStr = (TotalFolders – FolderCnt). But this is not your only option. But even so.NET. It has been my own experience thus far that I can duplicate all the “altered” VB6 functionality easily enough. such as "Short Date" and "Long Time". I find this a bit odd. or do web searches for “DateTimeFormatInfo” and “DateTime. and have yet to be unable to provide truly superior alternatives by employing the tremendous power offered by the . even for strings or functions. The “d” format tag. With VB2008.NET Upgrade Wizard will take the VB6 statement “S = Format(Now. it will behave exactly like any other variable or field.ToString Method”.ToShortDateString”.Format(TotalFolders . provide full support for an extensive array of method and property members. Another slick means of expressing dates and times is by taking advantage of a Date object’s ToString() method. because VB2008 fully supported the original VB6 statement. For example. as I will be demonstrating.##0")”.Compatability. it does not recognize some “classic” VB6 format shortcuts. For example.NET supports a wide range of Date and Time formatting tags. Still another option is to use the VB. like “myStr = Format(TotalFolders FolderCnt. First. by not trying to remain within the restrictive confines of the VB6 code specification.Format(Now. Search the Dot NET help. the conversion of some VB6 code is impossible due to syntactical or architectural differences.NET’s powerful muscles and bypass VB6 methodology altogether.FolderCnt. to include custom formats. is just one among many tags that VB.0 – David Ross Goben Noteworthy VB. this allows you to specify special VB6-compatible features using the VB6 command. Call me stubborn and bull-headed.NET Features That Differ from. For this reason functions in the VB6 Compatibility Library are used to allow code to run in VB. you may want to go to your References Tab on your application’s Project Properties and add a reference to Microsoft.NET Framework. which you could also use. that does not mean that their functionality was not already built into even older versions of VB. which are called Abstract Classes.ffffzzz”  Comment (long form definition) M/d/yyyy (Short Date Pattern) dddd. because the data that is being assigned is not being newly instantiated but it already exists (either in the command line or by variable indirection). Variables for reference types. yyyy (Long Date Pattern) h:mm tt (Short Time Pattern) h:mm:ss tt (Long Time Pattern) dddd. nor does the variable assignment “Dim S As String = myStrVar”.NET: Format "d" or "Short Date" "D" or "Long Date" "t" or "Short Time" "T" or "Long Time" "f" "F" "g" "G" "M" "R" "s" "u" "U" "Y" "o" "o" "o" “h:mm:ss. all arrays are reference types. 22 Jan 2010 09:53:21 GMT 2010-01-22T09:53:21 2010-01-22 09:53:21Z Friday.2512235-05:00 2006-01-22T09:53:21. the destination array variable will point to the exact same array data as the source. January 22. UTC (useful for sorting) Roundtrip. What this means to us mere mortals is that any newly-created data assigned to a reference-type variable must be declared using the New keyword because we are in fact assigning a not-previously-existing data object.NET does this cloning for us from “behind the green curtain”.Enhancing Visual Basic . are treated as reference pointers that are assigned the address of their data.NET.00 AM 22 Jan 2010 09:53:21. you should use the source array’s Clone method.0 22 Jan 09:53:21 Month: 1 09:53:21. MMMM dd.NET they are designed to act just like VB6 strings. 2010 2:22 PM 2:22:48 PM Friday. yyyy h:mm tt (Full Date Short Time Pattern) dddd. are reference types in VB. like "Testing". Conversely. “Dim S As New String(ChrW(0). 2010 1/22/2010 Friday. yyyy (Year Month Pattern) Roundtrip. such as Ary()). but the literal assignment “Dim S As String = "Testing"” does not. January 22. are structures that encapsulate (embody) their data. 2010 9:53:21 AM 1/22/2010 9:53 AM 1/22/2010 9:53:21 AM January 22 Fri. but this is all due to VB. constants. Unspecified (useful for sorting) (User-customized format) (User-customized format) (User-customized format) (User-customized format) (User-customized format) (User-customized format) Result for January 22.NET’s Rapid Application Development functionality. but VB. in the above literal and variable assignment examples. assigning existing data. local (useful for sorting) Roundtrip. dd MMM yyyy HH':'mm':'ss 'GMT' (RFC 1123 Pattern) yyyy'-'MM'-'dd'T'HH':'mm':'ss (Sortable Date Time Pattern) yyyy'-'MM'-'dd HH':'mm':'ss'Z' (Universal sortable) (invariant) Universal sortable MMMM. VB6 strings were value types. With explicitly declared Array reference types (those with appended parentheses “()”. which are called Concrete Classes. However. Page –26– . As a result. MMMM dd. which means it copies just the pointer address from the source to the destination. This is because strings are actually arrays of type Char. if you had a string array named Src() and you have a string array reference variable named Dst(). whereas variables for value types (numeric/scalar).0 – David Ross Goben Following is a brutally truncated table of just a few shortcut tags available under VB. which were value types. 64)” requires the “New” keyword because we are creating a brand new string.Clone”. it will not copy the array’s data. just as was done under VB6. yyyy h:mm:ss tt (Full Date Time Pattern) (General Date Short Time Pattern) (General Date Long Time Pattern) (default) MMMM dd (Month Day Pattern) ddd. NOTE: You may be quick to realize that although simple strings.NET (being an array of type Char). January 22. or string literals are assigned without the New keyword because they already exit and are not being newly instantiated.2512235Z 2010-01-22T09:53:21. you can copy just the pointer to the data Src points at to the Dst reference variable by using “Dst = Src”.0000-05:00 Notes Regarding All String Variables in VB. For example. but rather it will copy only the reference to the array. This is in strict compliance to Object-Oriented Programming rules. For example. MMMM dd. Under VB. under VB. This is done for much greater compatibility to the convenience of VB6 and also to support VB. 2010 9:53 AM Friday.NET Now Being Reference Types Strings variables under VB.NET performing automatic data cloning for such simple strings (internally carried out by an array object’s Clone method—covered below). January 22. 2010 2010-01-22T09:53:21.0000000 9:53:21. this means that when you copy one array to another. In order to actually copy an array.NET Beyond the Scope of Visual Basic 6. 2010 2:53:21 PM January. you must use the statement “Dst = Src. new copies of the data are in fact being instantiated and assigned to the string variables. if you want to copy the actual array’s data (you want to make another separate copy of the array data). Conversely.f” “dd MMM HH:mm:ss” “\Mon\t\h\: M” “HH:mm:ss.NET are reference types.ff tt” “d MMM yyyy” “HH:mm:ss. For example. For example: <StructLayout(LayoutKind. This way. Pack:=1)> _ Private Structure BrowseInfo Dim OwnerID As Byte 'owner identification tag (VB.Sequential)>”. String) 'apply copy to Dst (more work than required. but it is in fact easily addressed by Dot NET’s Runtime Interoperability Services namespace. just like VB6 actually did. If Option Strict is on. which on very rare occasions presented a problem to VB6. For example. you can then precede any structure you need to marshal for interoperability (interop. which will ensure members maintain byte-packed spacing (the next variable will start on the very next free byte). All this will be covered in much more detail as we progress. but working VB. but fully behind the scenes (under VB6. though most-all VB6-approved APIs are structured in such a way as to avoid this dilemma. but the process is still simple enough in VB. consider this unnecessary. For example. Under VB. under VB. End Structure NOTE: Note the “Pack:=1” in the above parameter list. not “First”: Module modTestStringCopy Sub Main() Dim Src() As String = {"First". even though simple strings are in fact arrays of type Char. The idea of having to marshal them to take advantage of interoperability processes might sound complicated.Enhancing Visual Basic .Clone”. Also note that the Clone method greatly simplifies the more complex copying process required by C/C++.NET will automatically handle their cloning for us. This special casting function does not generate any additional code).. just as it was done under VB6. copying arrays were in fact also automatically cloned for us). where each consecutive member will begin on a memory bounds equal to its size.Runtime.NET handles the cloning of simple strings.NET to not tarnish its RAD (Rapid Application Development) reputation.Clone. this would be aligned 3 bytes higher than OwnerID) '. they are handled as a special case (a cheat. Nonetheless. it will report "123". unlike the requirements for C/C++.NET will not be more complicated than it really needs to be.NET code using simple strings: Dim Src As String = "123" 'initialize simple source string (this is technically an array of type Char) Dim Dst As String = DirectCast(Src. such as ensuring that any 16-bit Unicode string member is passed as Page –27– .NET Beyond the Scope of Visual Basic 6. VB.InteropServices” in the file header).Clone. "Second".Print(Dst) 'check result. There is another consideration you may need to ponder. this could lead to data spacing gaps that can in turn lead to data corruption if the invoked process does not provide for natural spacing. VB. we must also cast it to a string array because the Clone method returns the result as an Object. “Dst = DirectCast(Src. a feature sadly unavailable to VB6. just as would be done under VB6. to give Dst a copy of the array. "Third"} Dim Dst() As String = Src Src(0) = "Zero" Debug. As previously noted. in other words) since VB2005. rather than on VB6’s explicit 8-bit boundaries. and in a different order. because memory access is much faster when separate objects are aligned on 32-bit or 64-bit boundaries. keep in mind that copying explicitly declared arrays will require us to use the Clone method. more efficient order) Dim hwndOwner As UInt32 'Handle to the owner (without Pack:=1. most string processing under VB.. Copying strings in C/C++ can sometimes be a bother.Sequential. the above declaration of Dst can become “Dim Dst As String = Src”.0 – David Ross Goben A case in point: the debug output from the following code sample will yield “Zero”. String())” (we can use DirectCast() here because the actual return type is known. even if we had used 'Dim Dst As String = Src' Because VB.NET you will need to marshal P/Invoked structures so that its members will be guaranteed to occupy contiguous memory. Just use 'Dim Dst As String = Src') Src = "ABC" 'change source data Debug. and still more tedious with complex arrays.  Notes Regarding Sending Structures to P/Invokes You may need to send a Structure (known as a User-Defined Type or UDT under VB6) to a P/Invoke (API).NET might store these in a different. otherwise fields in a structure will use natural spacing. By importing it (just place “Imports System. For such strings.NET still has it way too easy.NET they might not be. This method was brought into the open for reasons of cross-language interoperability. change “Dst = Src” to “Dst = Src.Print(Dst(0)) End Sub End Module 'init first string array 'declare second string array and assign the src data's address to it 'change the content of a src array data element 'now see if Dst reflects this change at its corresponding index (IT WILL!) As stated. for short) with the attribute “<StructLayout(LayoutKind. However. If not accounted for. SizeConst:=xxx)> instead of <VBFixedString(xxx)>. The VBFixedArray() modifier is not part of the language.VisualBasic. put your minds at ease. you will then be required to also programmacly dimension the needed array(s). If you had a fixed-length string declared within a UDT. The structure/class program code is stored in common application program code space.Compatability namespace. 128)”.ByValTStr. NOTE: The VBFixedString parameter value is a byte count for a string.NET structures cannot normally accept pre-sized strings or arrays.FixLengthString(). This is because the bracketed information cannot do that for us – it just tells the compiler to set aside space to accommodate the needed array pointers. for character members declared in Win32 as type TCHAR or BYTE. in-structure method that can be used to handle resizing members declared with VBFixedArray Public Sub Initialize() ReDim boxFaceImgIdx(5) 'note that the value in this parameter MUST match the VBFixedArray parameter. you could employ VB6. you must replace something like “Dim szCSDVersion As String * 128” with “<VBFixedString(128)> Public szCSDVersion As String” because VB. you can define the string instead as “<VBFixedString(128)> Public szCSDVersion() As Char”. it is strongly recommended that you take the simple route and just dynamically create strings of that specific length. though I will be the first to admit that it also makes VB6 to VB.NET upgrades a whole lot easier and smoother in the short run. consider a VB. and mind you that VB.NET app or if you are invoking a 16-bit Unicode P/Invoke. to keep data sizes correct. LPSTR. but rather a compiler directive. and abstract class data is stored Page –28– . not a character count – so if the structure will be for P/Invoke use. If your structures require strings of type Char. a member of the Microsoft. in the form “Public szCSDVersion As New VB6. The VBFixedArray documentation states “The VBFixedArrayAttribute is informational and does not allocate any storage. not instantiated. or by specifying that the data strings will be handled as another unmanaged type. you must prepend it with <MarshalAs(UnmanagedType. such as “Dim szCSDVersion As String * 128”. above End Sub End Structure NOTE: For those who may be concerned that adding methods to structures might affect its overall length. Keep in mind that this attribute does not convert a variable length array to a fixed array and that you must still allocate array storage using Dim or ReDim statements. As such. Pack:=1)> _ Structure BOXSTRUCT Dim boxHeight As Integer Dim boxWidth As Integer <VBFixedArray(5)> Dim boxFaceImgIdx() As Short 'set aside space for 6 (5+1) short integers 'safe. NOTE: Personally. ByValArray. where the VBFixedArray parameter specifies the upper bounds index of the array.0 – David Ross Goben an 8-bit ANSI string.Auto” in the StructLayout parameter list shown above. if the structure is to be used locally by your VB. ByValTStr. API functions. instead of Auto. but it cannot pre-dimension it to initialize that space. Structures (abstract classes) are treated just like standard classes in that they separately store the program code from data.Sequential. To show what this blather means. because many P/Invokes use 8bit ANSI text. To me. if need be. Or.NET. I prefer to avoid this last suggestion and sever explicit VB6 namespace connections – it is often more trouble than it is worth. However. after you have assigned the structure to a reference field. it just takes up more system space by having a heavier code overhead. you must double its size to account for 16-bit (2-byte) Unicode characters.Enhancing Visual Basic .FixedLengthString(128)” to create a string of fixed size that can be treated like a string. Don’t forget the header in the previous note. 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 VBFixedArrayAttribute. See the article “Marshaling Memory and Passing Strings to P/Invoke Signatures in VB. However.NET Beyond the Scope of Visual Basic 6. To convert fields or local string variables to VB. or whatever you require. in these cases. then match the byte count to the number of characters in the string.  Notes Regarding Fixed-Length Strings Many VB6 structures. you could emulate the above VB6 fixed-length string with this expression: “Dim szCSDVersion As New String(Chr(0). If you need fixed arrays within a structure. and file operations required fixed-length strings.NET does not allow dimensioned array declarations within a Structure. Also. they have no initialization process. you must declare it using something like “<VBFixedArray(1023)> Dim I() As Integer” or “<VBFixedArray(6)> Dim ch() As Char”. such as Unicode.” The reason for this is because structures are assigned. For example. to an operating system P/Invoke that is expecting (requiring) 8-bit ANSI character strings by including “CharSet:=Charset.NET” on page 129 of this tome for more detailed information and examples.NET structure that supports the above guidelines: <StructLayout(LayoutKind. ) . For example. Notice that we must exactly match the values in the VBFixedArray parameter in the ReDim statement.. above NOTE: Typically. you should also be sure to include the ‘Auto’ verb directly after the ‘Declare’ verb in a P/Invoke declaration. and can significantly help later in tracking down bugs. Note also that the dimension Rank must be included in the declared un-dimensioned variable (the commas). the result of the expression “Len(myBox)”. is not as confusing as it sounds. such as “<VBFixedArray(1023. As such. especially those invoked from VB6. NOTE: As just noted. will yield 20. this VB6-compatible functionality should have been that way all along! Page –29– . and thus it will pass it ByVal (and yes. For example. typically expected those strings to be 8-bit ANSI. which complies with stringent OOPL guidelines. but you see that the variable is passed in ByVal. Previously. A great many P/Invokes. After disagreeing with it. 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 values myBox. adjust the pertinent lines in the above structure to: <VBFixedArray(32. passes parameters by value (ByVal). such as 32x32. in actual practice. As such.) As Integer”.  A Note Regarding Passing Parameters ByVal VB.0 – David Ross Goben in the program stack space (data for concrete classes are also stored separately.NET strings are defined as 16-bit Unicode character arrays. I realized that because strings are in fact objects. which means that a reference to a copy of the string text. passing a string ByVal would strictly not allow the alteration of the string by it passing an immutable reference. 40. Finally.32) 'note that these parameters MUST match the VBFixedArray parameters. thus eliminating one more VB6 user complaint.. for a variable assigned to the above structure.Enhancing Visual Basic . if required. a 3-dimensional array such as “Dim I(20. is passed. or 8 + 12.NET will extract any information it needs to validate your declaration from the DLL's internal lookup tables during the compiling process and will automatically adjust data types in an attempt to satisfy your specifications. 40. And that. say myBox. This verb will force automatic type conversion to 8-bit ANSI arrays. which would be thrown away after the invocation. in most cases you should consider including the ‘Auto’ verb in your P/Invoke declarations when strings are passed to a P/Invoke that expects 8-bit ANSI characters. In the spirit of strong code self-documentation. Note. that passing a reference object within parentheses (such as “(objPtr)”) will create a copy. or any number of methods or properties to it. For example. 128)> Dim I(.. the length of the structure will not report any change in length at all if you add a method. This is in opposition to by reference (ByRef) that VB6 used by default. This makes it easier for someone else reviewing your code to get a clearer understanding of its intent. or 20. keeping public fields (variables) to a minimum and keeping data protected as much as possible enhances the robustness of code and minimizes the risk of corrupting data. if a routine is supposed to modify an integer variable. This will be more thoroughly demonstrated later.NET. which implies 3 dimensions. but rather a copy (a clone) of it is passed. VB. just as VB6 had done. NOTE: As noted above. Notice that the rank for “I” is changed to 2 commas. you are informing the system of the specific method within a DLL to invoke (the Alias parameter string). as will also be covered later. allowing it to be modified. except. which can be altered without changing the original. which is two 32-bit integers. when passing strings directly to P/Invokes as parameters. passing a simple string variable ByVal to a P/Invoke will now pass a reference to the base of the string text (or to an intermediate ANSI version of the string text).Initialize() 'initialize sizing of the yet-undimensioned boxName array using the Initialize() method NOTE: If you require multiple dimensions. this is in fact a means for easily creating a strongly-typed Clone!). ReDim boxName(32. 128) As Integer” would be declared as “<VBFixedArray(20.32)> Dim boxName(. it would be obvious why the variable is not changing when the method returns because you would know that the routine is in fact altering data. finally.NET Beyond the Scope of Visual Basic 6. it is stalwartly suggested that you explicitly reference each parameter ByVal or ByRef. plus 5+1 16-bit integers. when the P/Invoke processes either 16-bit Unicode strings (no conversion required) or if you will explicitly specify a method Alias. converted to ANSI or not. 11 Passing interop Strings variables ByVal: As of VB2005. if your P/Invoke is expecting 8-bit ANSI strings. you can specify multiple dimensions. If you wish to alter the passed variable. Regardless. because VB. by default. then pass it ByRef.) As Short 'set aside space for 33 rows of 33 -. When you use an alias in a P/Invoke declaration. A more in-depth look at P/Invoke (API) declarations will also be covered within this document. To pass parameters ByVal means the passed variable is not altered by the invoked routine11. 63)> Dim I(. you will do nothing but confuse it if you simultaneously include Auto in a declaration that will also specify an Alias verb.) As Integer”. but in a separate memory space commonly referred to as the Heap).note the Ranking (the comma) in boxName(. Z As Integer” would have declared variable X and Y as the VB6 Universal Type Variant. as it was under VB6 NOTE: Because we should always ensure that all return avenues are covered (protected). but which could add up for considerably substantial member counts.0# Then GetReciprocalValue = 1. during a VB6 to VB. a collection under VB. even though you can still continue to use just text strings as data if you want to.0# / Reciprocal 'directly return the result (Idea borrowed from OOP and C++) End If Return 0.0# / Reciprocal 'assign result to the method name (Idea borrowed from FORTRAN) End If End Function New VB.0# 'return default (optional if Option Strict is Off. You cannot expect VB6 code that simply looks like VB. For example.NET functions are able to return results more directly than was offered by VB6.NET upgrade. VB. Therefore. Not Simple Strings Collections. you had to separately declare like-typed variables. and even though default return values will be set. under VB6. even on the same line. Y. ComboBoxes. Internally. Y As Integer. such as “Dim X As Integer.NET Beyond the Scope of Visual Basic 6. For example.NET now must utilize the object’s ToString() method in order to obtain the text data that was. “Dim X. and types String and Object functions return an object set to Nothing. Value functions return a default value of zero. Z As Integer” will initialize variables X. destroyed) – an ObjectOriented process called Boxing. using “Dim X. are still allowed.  A Note Regarding Multi-Variable Declarations VB. provided by default to the collection by its text member. Under VB6. and Z to type Integer. as well as the data that would be used during comparisons. “Return iVal * 2” to return a value of twice the value of iVal.NET): Public Function GetReciprocalValue(ByVal Reciprocal As Double) As Double GetReciprocalValue = 0.NET.NET Universal Type). under VB6. the upgrade would assign X and Y to type Object (the VB. Default return values.0# in this case. you can still to do it that way. but recommended) If Reciprocal <> 0.  Notes Regarding Collections Now Storing Objects. Further. Previously. thus ignoring the VB6 rules that the upgrade application must fully respect in order to make any sense of an upgrade translation. you had to assign a result to the function name and then use the Exit Function command if it were not at the function’s end.NET Way: Public Function GetRecipricalValue(ByVal Reciprocal As Double) As Double If Reciprocal <> 0.NET will wrap each of them in a reference object shell and unwrap them when they are released (disposed. the following two function definitions will provide identical results: Old VB6 Way (this technique is still supported by VB.0# 'init default (optional. This change makes collections much more powerful. such as when you set the Sort property of these collection objects to True. though recommended) End Function 'Note that a default value will still be returned. Y. Z As Integer”. or you can use the Return statement to both specify a return expression and also to return immediately from the function. which are Abstract Classes.0 – David Ross Goben  A Note Regarding Returning Value Data via the Return Command from Functions VB.NET. if Return is not used. after all. be aware that it has a minuscule cost in time. though it is considered a “Best Practice” to always ensure that all return avenues are clearly addressed. Y. Under VB. the data that would be visibly displayed within a ComboBox or ListBox. Under VB. Although this boxing process is transparent to you.Enhancing Visual Basic . Under VB6. 0. it is a best practice to always specify a return value if the test fails. and ListBoxes now store reference objects as data instead of the simple text strings that were the only type used by VB6.NET syntax to be left untranslated during an upgrade. NOTE: You can also use structures and numeric variables. Page –30– .0# Then Return 1. which are objects.NET supports multiple-variable declarations using a single type specification. just like in VB6. 132)) ' Read Index like this : With Me. which is typically useless. just like VB6 Static Variables) Public Text As String 'default text of entry (Public so it can be directly edited by outside code) Private mKey As Integer 'Key value (Private so it is protected from direct manipulation from the outside) 'additional special private index that is shared by ALL clsData instances follows.Item(.) '------------------------------------------------------Public Class clsData 'Declare Field members (Fields are declared just like variables. and ComboBoxes.NET Shared = VB6 Static) '------------------------------------------------------'access readonly Key value (this. you must include a method declared as: “Public Overrides Function ToString() As String”.mKey = Key 'save key value MstrIndex += 1 'increment shared master index End Sub '------------------------------------------------------'used by many invokers. with the meaningful text such as had been provided to it under VB6 with strings.ToString ' End With ' NOTE: A ListBox/ComboBox will invoke the class’s ToString() method.0 – David Ross Goben However.Label1.1).ListBox1. which includes numerous methods. such as ListBoxes and Collections to retrieve displayable data '------------------------------------------------------Public Overrides Function ToString() As String Return Me. you may need to override its default ToString() method. ListBoxes. declared at the very least within its base class.NET class you write inherits the base Object class. 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() Me. such as a ListBox. This is an awfully diminutive price to pay for tremendously more powerful functionality. when not explicitly defined by you. empty VB. but Fields are persistent.Index. such as ToString()). could also be changed to a string. thus providing your own ToString() method that will supply the invoker.Text = String.Items ' Me.Text = "Current Index = " & DirectCast(. their default ToString() method. specifically roll-your-own classes. clsData). '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-initialize to zero by first instantiation (and shared by ALL instantiations). '**************************************************************************************** ' Add an entry like this: Me.Add(New clsData("Harry Potter". Consider the following sample class designed to be a minimal data storage class for collections: '**************************************************************************************** ' SIMPLE COLLECTION CLASS OBJECT FEATURING A TEXT STRING AND AN INTEGER KEY FIELD ' This can be used as a storage class in general Collections. depending on the class you use.Empty 'initialize blank data to "" Me.Text = Txt 'save text data Me.Items.Enhancing Visual Basic .Count . To override the ToString method of a base class (even a new. will simply return the class name of the object.Text = Txt 'save text data Me.Text End Function End Class Page –31– . (Try disabling the ToString() method and see what happens.NET Beyond the Scope of Visual Basic 6. So. keep in mind that for some objects. and the above integer.mKey = 0 'initialize null key MstrIndex += 1 'increment shared master index End Sub '------------------------------------------------------'Instantiate a New object with just the text property specified (function over-ride) '------------------------------------------------------Public Sub New(ByVal Txt As String) 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 over-ride) '------------------------------------------------------Public Sub New(ByVal Txt As String. ByVal Key As Integer) Me. ' 'NOTE: VB.ListBox1. where you can replace typing MyCol.NET still fully supports default properties.Items(3).Items. as opposed to explicitly spelling out all properties and methods. Try disabling the ToString() method in the clsData class and see what happens. But then.  A Note Regarding VB. worse. You will be presented with a lot of warnings to this effect in upgraded VB6 code if you were one prone to using them.SelectedIndex = .NET does not allow default properties that lack parameters (being parameter-less). Notice ListBox1 will automatically use the class’s ToString() method to display the text we had added for each entry.Load With Me. giving you no direct clue in many cases to what the code’s actual intent is. Next. 124)) .NET. which the base class ToString() method (MyBase. Parameterless default properties make code too indefinite. 126)) 'ADD returns VB6 NewIndex value Me. Let no longer has any relevance.Add(New clsData("Harry Potter".Add(New clsData("Gracie Allen".NET Beyond the Scope of Visual Basic 6.NET variables are abstract or concrete objects.Enhancing Visual Basic . The following VB6 code: Dim MyCar As CarClass Set MyCar = New CarClass 'declare the object 'instantiate the object This could be implemented under VB. Add the above class to the project. it operates as though its scope was at the current code block level). like this: “Dim MyCar or abbreviate it even further by using the short-form: “Dim MyCar As New CarClass”. you do not need to type a totally-redundant Set modifier when assigning an object to a variable because that is the only way by which any assignments can be made.Item(. e As System. which sometimes resulted in late-binding issues. both of which “obj” can accommodate? It is the default Text property.NET Having Dropped the ‘Let’ Keyword In VB.NET as the following code: Dim MyCar As CarClass MyCar = New CarClass 'declare the object 'instantiate the object (If you pre-pend 'Set' to this line.NET. VB. add the following Form Load event code: Private Sub Form1_Load(sender As System. Put a ListBox and a Label somewhere on the form. Principally. the consequentially redundant Set and Get verbs are always assumed and therefore they are never needed or required under VB. its default property.Add(New clsData("Sally Rae". what do we do if we actually want to pass the Item object to such a variable? We cannot! This is yet another extremely good reason why we should not allow parameterless properties.ToString) will return that is in fact the name of our data class object (when a base class method is invoked. Page –32– As CarClass = New CarClass”.Items .ListBox1.Add(New clsData("George Burns".clsData”. 125)) .NET Default Properties That Cannot Be Parameterless VB. Consequently. You will instead get 4 entries of something like “WindowsApplication1.Count . so Get and Set properties are all that are left.0 – David Ross Goben Create a new Windows Application project. clsData). What should be done in the following case if Text were the default property of class object Item: Dim obj As Object = Item Are we clear in an instant what is actually passed into “obj”? Is it the Item object or is it its Text data.  A Note Regarding VB.Object. but not with wild abandon as VB6 did. classes have dropped the Let property verb.NET. the only good place for you to define a default is in the Item property in an Items collection. debug your code.EventArgs) Handles MyBase.Text = "Current Index = " & DirectCast(. which still makes perfect sense to a person stuck with having to review or.Index. 123)) . VB. .Item(3) with MyCol. when you set a local variable to an object. Thus. Because all VB.Label1.ToString End With End Sub Execute the code.NET will automatically remove it) NOTE: You can also instantiate upon declaration under VB.1). Usually. Refer to the following table for converting VB6 variable types to VB.NET variables have taken on broader definitions to both modernize the language and align itself with current technology. it auto-reserves a varaible that is named as the property. this would generate a syntax error. please be sure to check for updates or else just uninstall and then reinstall Visual Studio 2015. you type the line with the ReadOnly modifier (conversely. but VB6 does not support them): VB6 Name Byte Integer Long Single Double Currency Variant VB6 Size in Bits/Bytes 8 bits / 1 byte. Int32 Single Double Decimal Object VB.NET projects.” Thomas Barthelemy addressed this issue at http://thomas-barthelemy. or use a different variable name. support Edit-and-continue debugging.NET GET/SET Property Format A big change in VB.NET Name(s) Byte. Also be aware that there was an issue when running code in debug on a 64-bit system under VB2012-VB2015. if you type the following line and then press ENTER: Public Property DoorCount() As Integer The following VB2008 code block will automatically be generated (you would have to type “get” or “set” after the above and press enter to view this expanded coding on VB2010 and later." If you have this issue. What you actually need to do is explicitly tell the program that the property is read-only. but 64-bit is forthcoming. although Update 1 for VB2015 from Microsoft has in fact fixed it. For example. just as we could do under VB6.NET.github. signed 32 bits / 4 bytes 64 bits / 8 bytes 64 bits / 8 bytes 128 bits / 16 bytes VB. because they will. UInt8 Short.NET Size In Bits/Bytes 8 bits / 1 byte. If you want to make the property read-only so that Get is the only verb supported. signed 32 bits / 4 bytes 64 bits / 8 bytes 96 bits / 12 bytes 32 bits / 4 bytes  Notes Regarding Edit and Continue VB.0 – David Ross Goben  A Note Regarding New and Wider Variable Definitions VB. unless you over-ride it by declaring an 'identical variable name.VsHub. no longer separately as they were in VB6.Enhancing Visual Basic . Int16 Integer. He reported that "With the help of the Microsoft Connect team we were able to locate the issue on System32/ASProxy64.  Notes Regarding the VB.NET (VB. that presently Edit and Continue is only supported in 32-bit applications. a feature dearly missed by many former VB6 developers in earlier releases of VB. unsigned 16 bits / 2 bytes.NET Beyond the Scope of Visual Basic 6.exe has stopped working”. unsigned 16 bits / 2 bytes. This feature allows you to add/edit/delete lines of code during an interactive debugging session. until then. you can also use the WriteOnly modifier for a Set-only property): Public ReadOnly Property DoorCount() As Integer And VB will generate the following result (less my comments): Public ReadOnly Property DoorCount() As Integer Get End Get End Property 'Vb2010 and later will not auto-generate the Get (or Set) structure unless you type Set or Get 'below it. signed 32 bits / 4 bytes. This way you can quickly set up 'a class initially without pounding away at a lot of property code to get to testing the class. such as unsigned integers.HttpHostx64.io/2015/05/01/visual-studiovshub/ in an article named “How to fix: HttpHostx64. auto-generate a default ‘backing field’ variable for value assignment): Public Property DoorCount() As Integer Get End Get Set(ByVal Value As Integer) End Set End Property Now fill in the highlighted blank lines for Get and Set properties. Page –33– .dll which belongs to the Astrill VPN software. This makes property processing much easier to manage.Server. you cannot simply delete the Set block. but with a 'underscore. as of VB2005. This is a great feature for fixing bugs or correcting/testing variable values on the spot.NET properties is that now the GET and SET verbs are embraced inside a single property definition. signed 32 bits / 4 bytes. Until then.exe has stopped working. It had to do with a crash reporting that “Microsoft.NET has more types. such as Protected _DoorCount As Integer. Note though. Hence.  A Note Regarding the New ‘OrElse’ Keyword VB. which avoids really weird syntax like “If NewCLass Then…”.NET now features an IsNot keyword. which will instantiate a temporary object and automatically disposes of it at End Using. even if your implementation does not do anything other than use the default code that is automatically inserted by your entering the “Implements System.IO. I recommend you forget all about using logical Or again and always use OrElse. It is similar to the And operator.0 – David Ross Goben VB2010 introduced shorthand code to automatically define Set and Get Properties with an initial assignment in one line of code. Indeed. vbCrLf) SR. Indeed.NET supports a logical AndAlso operator. This significantly speeds program execution.StreamReader(Path) Dim Ary() As String = Split(SR.IDisposable” command line above the field zone within your class. This works exactly like the regular “&&” logical And operator under C++ and C#. then any logical comparisons following an OrElse within that expression are not performed. For example. except that if a comparison is found to be False.Dispose End With 'use a temporary SreamReader object to open a text file 'read the file (specified by the Path string variable) into an array 'close the file 'display the file contents in the Immediate Window 'dispose of SR resources 'Dispose of the SR object using its Dispose() method NOTE: The object instantiated by Using…End Using is also required to implement System.ReadToEnd. otherwise the IDE will report an error. then any logical comparisons following an AndAlso within that expression are not performed. The Dispose() method is used to release any possible unmanaged interop resources the class instantiated or reserved while being processed. instead allowing a more natural “If ObjItem IsNot NewClass Then…” Not ObjItem Is  A Note Regarding the New ‘AndAlso’ Keyword VB.IDisposable.Print(Ary(Idx)) Next . It is similar to the Or operator. vbCrLf) SR.IO.NET Beyond the Scope of Visual Basic 6.Close() For Idx As Integer = 0 To UBound(Ary) Debug. but it is initiated with the form “Using nm As New objClass”. under VB2010 or later.ReadToEnd. being moot.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. This significantly speeds program execution. Built-in framework objects have this implementation.Enhancing Visual Basic .Close() For Idx As Integer = 0 To UBound(Ary) Debug. It is similar to With…End With.Print(Ary(Idx)) Next End Using 'use a temporary SreamReader object to open a text file 'read the file (specified by the Path string variable) into an array 'close the file 'display the file contents in the Immediate Window 'Dispose of the SR object using its Dispose() method This is the same as implementing the following code: Dim SR As New System. replacing it (with added comments by me): Private _Prop2 As String = "Empty" ' ┌────────────────────────────────────────────────────────────────────────────────────────┐ Property myProp As String ' │ │ Get ' │ │ Return _Prop2 ' │ │ End Get ' ▬▬▬▬▬▬▬▬▬▬▬▬▬▬►│ WOW! LOOK AT ALL THE GREAT SPACE FOR DEVELOPER COMMENTS IN HERE! │ Set(ByVal value As String) ' │ │ _Prop2 = value ' │ │ End Set ' │ │ End Property ' └────────────────────────────────────────────────────────────────────────────────────────┘  A Note Regarding the Support of Using…End Using VB. except that if a comparison is found to be True. This is called ShortCircuit Evaluation.  A Note Regarding the New ‘IsNot’ Keyword VB. I recommend you forget all about using logical And again and always use AndAlso. This is called Short-Circuit Evaluation. enter the following line: Property myProp As String = "Empty" 'declare a property with an initial string value And the following code will be automatically generated. but user-defined objects may need to update their class to support the Dispose() method by implementing System. This works exactly like the regular “||” logical Or operator under C++ and C#.NET supports Using…End Using code blocks.NET supports a logical OrElse operator. being moot. regardless of how the code actually exits this block.StreamReader(Path) With SR Dim Ary() As String = Split(SR. Page –34– . For example: Using SR As New System. and Why We Should Welcome It VB. there is also FrmAPIViewer. and also frmAPIViewer. which is the form’s XML-based resource file.Message) Return 0 End Try End Function 'convert 64-bit float to 32-bit integer 'usually an overflow or underflow error 'report description of error 'return an integer default of 0  A Note Regarding New Variable Types SByte.resX. Long. which is the Dot NET-generated source code that is used to support the form.New() 'This call is required by the Windows Form Designer. especially for Form classes. UShort. UInt32). such as String. If you select the “Show All Files” option in the Solution Explorer. you simply use MakeInt() and insert your parameter. UInteger. but each of the their parameter definitions will accept a different type value. allowing the developer to define object-related meanings for object-processing operators such as “+” or “–” or “=” for user-defined classes. Double. and UIntPtr (Unsigned IntPtr (Integer Pointer)). to be stored in an entirely separate file. UInt16).0 – David Ross Goben  A Note Regarding Overloading. NOTE: An IntPtr is a platform-specific type that can be used to represent a memory pointer or a window handle. it also supports Method Overloading.NET Beyond the Scope of Visual Basic 6.. thus allowing multiple source files to all be used to define a single class or structure.Dispose(Disposing) End Sub . enabling them to break up their own complex class or structure bodies into more than one file.Diagnostics. InitializeComponent() End Sub 'Form overrides dispose to clean up the component list.Designer. UShort (Unsigned Short.. and UIntPtr VB.Microsoft.  A Note Regarding Partial Classes and Structures A Partial class or structure is where a class or structure is defined with the new Partial verb.Enhancing Visual Basic . allowing different parameter counts and types for same-named methods. UInt64). that will all return type Integer.Designer. ULong.VisualBasic. Perhaps more importantly for most developers.CompilerServices.DebuggerNonUserCode()> Public Sub New() MyBase. Note that only one of the separate declarations.NET supports the new Integer value types SByte (Signed Byte). ULong (Unsigned Long.Dispose() End If End If MyBase. away from developer-generated Form code.vb.vb. this also allows the special Visual Studio-generated code. you will see additional files listed under each form. you can declare several separate class files for “myClass”. all identically named MakeInt. is allowed not to include the Partial operator. <System.Diagnostics. For instance. Consider these three overloads: Public Function MakeInt(ByVal Value As Short) As Integer Return CInt(Value) End Function 'convert 16-bitvalue to 32-bit integer Public Function MakeInt(ByVal Value As Single) As Integer Return CInt(Value) End Function 'convert 32-bit float to 32-bit integer Public Function MakeInt(ByVal Value As Double) As Integer Try Return CInt(Value) Catch ex As Exception MsgBox(ex. UInteger (Unsigned Integer.vb file.NET supports Operator Overloading. That way you will not need to stop and think about which method to actually invoke. you can declare separate functions.DebuggerNonUserCode()> Protected Overloads Overrides Sub Dispose(ByVal Disposing As Boolean) If Disposing Then If Not components Is Nothing Then components. or whatever. usually that which is developer-modified. specifying “Partial Class myClass” in all of them (but maybe optionally declaring “Class myClass” in the root class declaration). Significantly. you would see something like this in its heading: <Global. The compiler will figure out all the nitty-gritty details for you.DesignerGenerated()> Partial Class frmAPIViewer #Region "Windows Form Designer generated code " <System. the developer is also free to exploit this Partial verb. For example. all to enable signed bytes and unsigned integers. Page –35– . However. for a form named frmAPIViewer. For example. If you were to open the frmAPIViewer. Short. just use the ESC key to clear them). replacing old-style VB6 form-resident controls. For example. I have gained a major part of my VB.com/. featuring copious help tips and submenus to help you while typing commands (if they get in the way. See Microsoft’s CodePlex Open Source Project Community site: http://snippeteditor. do not have to get so elaborate as to need to add special system flags and properties (the “jargony” stuff embraced by angle brackets “< >”). you should certainly get the idea. such as this sample list of controls: o o o o o o WebBrowser MaskedTextBox MenuStrip (replaces VB6 main menu) StatusStrip (replaces VB6 StatusBar) ToolStrip (replaces VB6 Toolbar) Timers o o o o o OpenFileDialog (replaces CommonDialog) SaveFileDialog (replaces CommonDialog) ColorDialog (replaces CommonDialog) FolderBrowserDialog (replaces CommonDialog) FontDialog (replaces CommonDialog) NOTE: Most old VB6 controls and DLL services can still be implemented and freely used within VB. and it will sometimes even offer to fix the problem for you (I wish that option would pop up more often than it does).codeplex. displaying a list of the most commonly used options. where I show you how to add (and get) a free Microsoft VB6 Animation control to VB.  A Note Regarding Snippets The user can access and import dozens of pre-written code snippets via the right-click popup menu. if I do any form subclassing. Between these auto-dropping lists filled with options. Further. I like to place the subclassing code within its own separate Partial Class form file.0 to Visual Basic . VB. providing principal details regarding each option or statement or keyword. and also how to re-add old VB6-style toolbars that some miss.  A Note Regarding Accessing Common or All Options in IntelliSense Popups The user can access a subset of object members.NET projects.  A Note Regarding Form-Linked Controls New VB.NET program development for each pop-up menu option.NET features appreciably extensive and detailed help tips that are displayed during VB.  A Note Regarding Better IntelliSense VB.NET has significantly better IntelliSense. Page –36– .  A Note Regarding the Exception Assistant The new VB.Enhancing Visual Basic . which I do quite often. by adding a free Snippet Editor. VB2010.NET.net/davidrossgoben). in our own partial class files. which they sometimes do. This is one of the most powerful learning/reminder tools in the IDE. one for Common options.NET Beyond the Scope of Visual Basic 6. complete with source code. and are discussed in the companion document. The pop-up selector provides two tabs. “Navigating Your Way Through Visual Basic 6. you can add your own favorite code snippets to the collection without the usual need to write it using copious angle brackets.NET Application Upgrades”(www. to include debugging suggestions and Debugger Data Tips that displays parameter and variable values when the mouse is over an error.NET Exception Assistant provides more detailed help for unhandled runtime exceptions. and their Express Editions. VB2008. another for All options.NET savvy. This version fully supports VB2005.slideshare. The means to being able to do this is amazingly easy.0 – David Ross Goben Although we.NET form-linked controls make developing form features easy. and the help tips popping up next to each as I click the mouse pointer on them. leaving the programmer free to omit type annotations while maintaining some level of type safety. I. You can even ignore errors by implementing an empty Catch block. but with the type safety of a statically typed language.NET Beyond the Scope of Visual Basic 6.  A Note Regarding Data Source Binding Data Source Binding is available. It provides for evaluating expressions while debugging.Enhancing Visual Basic . cringe at this type of programming. and automatically skipping over any traces through libraries. This idea was one of my own personal contributions to the VB. Because the cross-language compiler is also continuously active in the background of the IDE. This expansion allows you to embed more than one Catch block to enable detecting more than one type of error that might have occurred within the wider Try block.Number = 53”.LastName = "Smith"}” creates a user-defined-type structure on-the-fly containing two members: FirstName and LastName.  A Note Regarding the Conditional Operator VB. Note that you can (and should) declare member types.NET features a debug option that allows you to debug trace through just your own code. .NET features a true conditional operator. allowing you to build structures seemingly “on the fly”. For example. such as “. trapped with the new “When” keyword.  A Note Regarding Structured Exception Handling VB2008 expanded the capabilities of the Try…End Try error trapping structure to feature Structured Exception Handling. duplicating the Continue statement in C#/C++. operating much like the IIF() operator used in spreadsheets. personally. “Dim Valu = 1234” will define Valu as a 32-bit Integer.NET features an Immediate Window (opened with Ctrl-G). for which you do not have source code. ExpressionIfFalse)”. Floats default to Double. ExpressionIfTrue.NET language. Such special Catch blocks would precede more general ones. “If(BooleanExpression. The ability to infer types automatically makes many programming tasks easier. and While loop. allowing program control to immediately pass to the loop control test of a Do. For example: “Dim person = New With {. This gives programmers much of the convenience of a dynamic language. otherwise the third. you can even evaluate expressions while not debugging.FirstName = "John".  A Note Regarding the Continue Statement The Continue statement was added in VB2005. which makes for much easier debugging. For example. which was added in VB2005.FirstName As String = "John"”. just as you could under VB6. refers to the ability to deduce automatically the type of a value in a programming language. then the result of the second expression is yielded. This also includes the ability to embed deeper Try…End Try blocks within Catch blocks. For.0 – David Ross Goben  A Note Regarding the Immediate Window VB. For example.  A Note Regarding Type Inference Type inference.  A Note Regarding Anonymous Typing Anonymous types allow data types to encapsulate sets of properties into a single structure without having to first explicitly define it. If the first expression results TRUE.  A Note Regarding Debug Trace Enhancements VB. in case my code forgot itself and had a bug in it. or implicit typing. Page –37– . It is the same as that for VB6. a Catch block beginning with “Catch When Err. because I always want to know my storage types at all times. such as “Catch ex As Exception” or even simply “Catch”. or changing variable values. easing database client/server application binding issues. This information is organized in a format that is accessible through IntelliSense and logically delineate according to use. One common task is showing an instance of a form.Close() End Sub NOTE: If a button event handles multiple buttons (the Handles declaration listing multiple objects. the My namespace. as was required prior to VB2005.Application. previously. their signatures. which are all now pre-instantiated. and it will still run OK: Sub btnClose_Click() Handles btnClose. if not explicitly assigned a value. and the current user.Location).Show”.Close() End Sub Can now be manually edited to be declared more simply as this. the following declaration is allowed (the first item. resource objects and strings. Further. to get an app’s major version.Assembly. This is now just “Dim Major As Integer = My. Instead of explicitly declaring a new instance of a form and instantiating it.Form2.NET button click event: Sub btnClose_Click(sender As Object. the computer.Version.Major”. Thus. the forms. they now auto-increment by an integer value of 1. it will instead simply take you straight to the top of the source code file. See later in this document for a more detailed look at this very powerful feature.NET Framework. These parameters are not typically used in simple events that handle only one object.Click Me. enables quick access to frequently used information that is normally buried deeply within the .Forms collection exposes a reference to each form declared in the application. Previous to VB2005. including Information about the program.Diagnostics. e As EventArgs) Handles btnClose. You can even abbreviate this further by leaving off the “My.Forms. because forms in Page –38– .Click Me. then checking the “sender” argument is essential if you need to know exactly which button was actually clicked. the following VB.  Notes Regarding the MY Namespace VB2005 introduced new features for Rapid Application Development that greatly improves productivity. e As EventArgs" in event handlers has become optional. It simplifies access to otherwise difficult-to-obtain information.0 – David Ross Goben  A Note Regarding Enumerators Enumerator member no longer have to be explicitly assigned an absolute constant value. for example).FileVersionInfo. The My namespace acts as a wrapper to “speed dial” common tasks. you can now simply invoke it like this: “My. For example.GetVersionInfo(System. although with a very different underlying implementation. program settings. you had to ‘jump through hoops’ and do it like this: “Dim Major As Integer = System. For example. such as a single button click.Info. The new auto-generated My.Show”. My also provides information and default object instances that relate to the application and its run-time environment. just as had been done under VB6.GetExecutingAssembly.NET Beyond the Scope of Visual Basic 6. This is because the modified declaration and the auto-generated Delegate for the event.Forms” prefix and just use “Form2.Fi leMajorPart”. One of these features.Enhancing Visual Basic . removing these parameters also disables the IDE’s ability to take you straight to the default event source code if you double-click a form object. defaults to a value of 0): Enum MyConstants As Integer 'optionally declare an explicit type (I STRONGLY recommend this option) myColors 'this defaults to 0 myShapes 'this defaults to 1 (auto-increment from previous) myBorders = 4 'explicit set a value of 4 myCorners 'this defaults to 5 (auto-increment from previous) End Enum  A Note Regarding Optional Event Parameters The inclusion of the standard "sender As Object. will not match (a Delegate is simply a method prototype template).Reflection. giving you the same syntax as VB6. 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. and so that unused but still allocated space can be immediately re-used and reinitialized extremely quickly. as stated. By delaying their destruction until the Garbage Collector (as a continuouslyrunning separate background thread) finds it and invokes an object’s Finalize method.NET CLR (Common Language Runtime) automatically invokes its Finalize method for objects that have defined a Sub Finalize procedure. so you should never explicitly invoke a class’s Finalize method. The Finalize method should contain code that must execute before an object is to be destroyed. being an instance of the defined form class. The Finalize destructor is a protected method that can be invoked only from the class it belongs to. As a consequence of these new features. This is because different unmanaged objects must be disposed of in different ways. Also note the slick trick to locally override the class name when the new form is instantiated to first instantiate an initial instance of the desired form class (this may have been hidden from you on simple single-form apps because the main form had always been automatically instantiated and shown from within the typically-hidden Assembly code).NET Beyond the Scope of Visual Basic 6. that object space is most-likely still around when we instantiate the new object. 12 VB6 also allowed a Static Sub New declaration. I never understood why Microsoft never got up in front of this uniformed user-complaint and simply clearly explained to them the sound logic for doing this.NET’s OOPL design.NET initialize and destroy objects. previous to issuing Form2. identical object. Before releasing objects. had to first be declared by the developer. This is addressed in a later example for closing nontypical MS Office COM objects (COM objects are unmanaged types because they all operate outside . a default instance of each form has already been declared in the My. VB2005 and later versions implicitly create a Sub New constructor at run time in your classes if you do not explicitly define a Sub New procedure in them.  Notes Regarding Class Construction and Destruction The Sub New and Sub Finalize procedures in VB. are often reused to instantiate another instance of a new. Page –39– . because. Unlike VB6’s Class_Terminate. such as for closing files and saving program state information. The system invokes Finalize automatically when an object is actually being destroyed. But due to VB. which was a special one-shot method that would execute the first time the class’s New method was invoked.Forms collection. which was the same as using “Load Form2” under VB6. especially unmanaged (non-CLR) objects explicitly if you have a large number of objects.0 – David Ross Goben .Enhancing Visual Basic . the Sub New constructor can run only once for each object as it is created.NET’s CLR).NET objects are not immediately destroyed when the user sets them to Nothing is because the reference variables that we typically set to Nothing. you can now use addressing of forms similar to the VB6 methods. involves the time-consuming process of releasing resources. and such. object method interfaces.NET are reference objects. this non-standard feature is not needed. being replaced by your being able to Share common variables among instances and so being able to assign them an initial value upon declaration (see the example on page 30). the code in the Sub New method always runs before any other code in a class12. Form2 in the above example. objects that the operating system executes directly. because object and interface table space already exist. NOTE: The . so you should define a Sub Finalize method only when you need to actually release resources. or from derived classes. NOTE: The reason that VB. it must be found in the documentation for the object. Totally obliterating an object. A class that uses unmanaged objects must dispose of them in its Finalize method. the . That information is not directly associated with the unmanaged object. which executed when an object is set to Nothing. you were required to also declare something like “Dim Form2 As New Form2”.NET garbage collector cannot dispose of unmanaged objects. For example. they replace the Class_Initialize and Class_Terminate methods used in VB6 and earlier. It cannot be invoked explicitly anywhere other than in the first line of code of another constructor from either the same class or from a derived class (a class that inherited the referenced class).Show. or the object space. which is automatically built into the Visual Studio Designer portion of the code for each form. There is naturally a tiny performance penalty for executing Sub Finalize. Furthermore. such as VB6 did. outside the CLR environment. Unlike Class_Initialize. The Finalize method should never reference any other objects. never Public. If an external resource is scarce or expensive. because under VB6. but it will also do it better. The consumer of the object should then invoke this method when it finishes using the object. and enabling them to dispose of resource immediately.0 – David Ross Goben VB2005 and later versions also allow for a second kind of destructor that is built into all current . if more than one reference to an object existed and one reference was set to Nothing. which can be quite often. which would just disable the connection to the target object). In fact. because an object exception must always reference an object. NOTE: Dispose can be invoked even if other references to the object still exist. a Finalize method should release only the resources that its actual object has held onto. such as device context handles. because a prerequisite for closure is the destruction of an object. Finalize provides a backup to prevent any unmanaged resources from permanently leaking whenever the programmer fails to invoke Dispose. which can be explicitly invoked at any time. to include applications that will not terminate when they are “supposed” to. This is where VB6 objects often got into a lot of trouble. The Finalize method could. The following rules explain the usage guidelines for the Finalize method: 1) Implement Finalize only on objects requiring it. Be aware that the Dispose method should be written in such a way that it can be invoked multiple times. such as COM objects) that the object owns. where allocated memory for the application is not released when the application terminates abnormally.NET objects. but it should actually de-allocate resources only the first time through. you might still need to provide implicit cleanup using a Finalize method.NET Beyond the Scope of Visual Basic 6. the object would suddenly be resurrected. 3) Do not make the Finalize method more visible. consider implementing IDisposable to allow users of your class to avoid the cost of invoking the Finalize method. just as many developers were used to doing under VB6. In special cases you may need to give developers the ability to explicitly release external resources before the garbage collector frees their encapsulating object. This might ultimately cause “memory leaks”. Therefore. 4) An object's Finalize method should free any external resources (unmanaged space. You provide implicit control by implementing the Finalize method on an object. called Sub Dispose.Enhancing Visual Basic . totally obliterating the object. which it keeps detecting as existing. you should finish the Dispose method by invoking GC. Also. if you accessed the other reference by invoking one of its methods or accessing one of its properties (not simply also setting it to Nothing. and therefore in extreme cases it can cause the application to terminate. 5) Do not directly invoke a Finalize method on an object other than the object's base class. allocated memory space via P/Invokes. and clogging available resources until the system is rebooted. the Dispose method does what setting an object to Nothing did under VB6. which is treated as the actual object. 2) If you think you need a Finalize method. There is a slight performance cost to Finalize methods. and allows you to immediately release resources. better performance can be attained if the developer explicitly releases resources when they will no longer be used. you can add your own Dispose method in your objects by implementing the IDisposable interface in your class. Note that even when you provide explicit control using Dispose. to avoid the Garbage Collector from invoking any defined Finalize method after you have released the object through a Dispose method. 6) Be sure to invoke the base class's Finalize method as the last line in your object's Finalize method. This is because such exceptions cannot be handled by the application due to the object being destroyed. Further. as it did under VB6. sometimes causing all sorts of headaches. Page –40– . 7) A Finalize destructor should not throw exceptions. you should provide both an explicit and an implicit way to free those resources. This is possible because an object’s program code always exists during the lifetime of an application. To provide explicit control. and so on. though not its data. It should be Protected. ideally. database connections.SuppressFinalize for it. The garbage collector invokes this method when it finds that there are no longer active references to the object. simply invoke the Dispose method if it has not run. Class instances sometimes reference resources that are not managed by the CLR. 8) Propagate invokes to Dispose through the hierarchy of base types. to prevent it from trying to free resources that would have already been freed by Dispose. a flag should be implemented to inform Finalize if the Dispose method had been invoked. Finalize should be implemented only in any derived types that actually introduce resources that require cleanup. Place your actual resource cleanup code in the “Protected Overridable Sub Dispose(ByVal disposing As Boolean)” implemented method. 9) Consider not allowing an object to be usable after its Dispose method has been invoked. to the brutal consternation of VB6 developers.0 – David Ross Goben 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. even if the base class does not. The Dispose method should free all resources held by this and any object owned by this object. “Public Sub Dispose()”. Users can free these external resources by invoking the public Dispose method.NET Beyond the Scope of Visual Basic 6. Unmanaged resources owned by a type should also be released in a Finalize method in the event that Dispose is not invoked. What follows is an empty class with IDisposable implemented. In such cases. The Dispose method should simply do nothing after its first invocation. 10) Allow a Dispose method to be invoked more than once without throwing exceptions. so you can see its auto-created code: Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' EmptyClassWithIDisposable Class ' 'Sample class to expose IDisposable implementation '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Public Class EmptyClassWithIDisposable Implements IDisposable #Region "IDisposable Support" Private disposedValue As Boolean ' To detect redundant calls ' IDisposable Page –41– . If the base class features a Close method.Enhancing Visual Basic . When you invoke the Dispose method on the TextReader. For example. This is automatically added to your code. prevent it from executing by invoking GC. such as unmanaged Interop memory allocations. In such cases. anyway. 2) Implement the dispose method on a base class that commonly has derived classes that hold onto resources.SuppressFinalize within the Dispose method. do not implement a Finalize method on the base type. though VB6 did it with wild abandon. this often indicates the need to implement Dispose. and not of any real sensible use. 6) Do not simply assume Dispose will be invoked. The exception to this is in the rare instance where work must also be done in Finalize that is not covered by Dispose. like memoryhungry images and large tracts of text (simply add “Implements IDisposable” within the top bracing of your class – see the example at the end of this list). this exception. Use a Boolean field in the class to indicate if the Dispose method has been invoked. both of which are created by the TextReader without the user's knowledge. and raised havoc with memory leaks in VB6). So be sure to implement rule 4 in your Dispose method. COM objects. 4) After Dispose has been invoked on an instance. if a Finalize method also exists. both the Stream and the Encoding object can acquire external resources. 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. Furthermore. Re-creating an object that has already been disposed is a difficult pattern to implement. 3) Free any disposable resources a class owns in its Dispose method. 5) It is a best practice to always invoke the base class's Dispose method if it implements IDisposable. it will in turn invoke Dispose on both the Stream and the Encoding objects. or even large local resources. In reference to rule 7 of the usage rules for the Finalize method. causing them in their turn to release their own external resources (its Close method auto-invokes its Dispose method). which will automatically write the skeletal base code for you when you implement IDisposable. or any exception should not be thrown from the Finalize method either. This allows multiple references to free their connections to the object. you can create an object such as a TextReader that in turn internally instantiates a Stream and an Encoding object. 7) Throw an ObjectDisposedException from instance methods on this class (other than Dispose) when resources are already disposed. InteropServices.Enhancing Visual Basic .disposedValue Then If disposing Then ' TODO: dispose managed state (managed objects). but I like the VB6 method).DllImport("shell32.0 – David Ross Goben Protected Overridable Sub Dispose(disposing As Boolean) If Not Me. if you prefer.Runtime.DllImport("shell32.dll")> _ Public Shared Function SHGetSpecialFolderLocation( _ ByVal hWndOwner As Integer. Even though the VB6 method of declaring P/Invoke Signatures (called API Functions or API Declarations under VB6) is still fully supported as legacy code in VB.NET): Public Declare Function SHBrowseForFolder Lib "shell32" ( _ ByRef lpbi As BROWSEINFO) As Integer Public Declare Function SHGetPathFromIDList Lib "shell32" ( _ ByVal pidList As Integer.Runtime. _ ByVal nFolder As Integer. End If ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below. _ ByVal nFolder As Integer. 'Protected Overrides Sub Finalize() ' ' Do not change this code. Rather than describe how to convert VB6-style P/Invoke Signatures to the new VB. _ ByRef ListId As Integer) As Integer End Function You can abbreviate “System. End If Me.Runtime.Dispose ' Do not change this code. _ ByRef ListId As Integer) As Integer New .InteropServices. Put cleanup code in Dispose(ByVal disposing As Boolean) above.NET has a new technique for declaring P/Invoke Signatures. _ ByVal lpBuffer As String) As Integer End Function <System. ' TODO: set large fields to null.dll")> _ Public Shared Function SHBrowseForFolder( _ ByRef lpbi As BROWSEINFO) As Integer End Function <System.NET Beyond the Scope of Visual Basic 6.InteropServices. The above results in: <Dll("shell32.Runtime.Finalize() 'End Sub ' This code added by Visual Basic to correctly implement the disposable pattern.NET format examples of the same P/Invoke Signatures: <System.InteropServices.dll")> _ Public Shared Function SHBrowseForFolder( _ ByRef lpbi As BROWSEINFO) As Integer End Function Page –42– . ' Dispose(False) ' MyBase. the following examples probably explains it much more clearly: VB6 format examples that have been upgraded to VB.Runtime.DllImport("shell32. Public Sub Dispose() Implements IDisposable.NET format. _ ByVal lpBuffer As String) As Integer Public Declare Function SHGetSpecialFolderLocation Lib "shell32" ( _ ByVal hWndOwner As Integer.NET. or. Dispose(True) GC.dll")> _ Public Shared Function SHGetPathFromIDList( _ ByVal pidList As Integer.DllImport” to just “DllImport” if you include “Imports System.InteropServices” in the heading of the class file. it is recommended by Microsoft that you adopt the .disposedValue = True End Sub ' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources. Put cleanup code in Dispose(ByVal disposing As Boolean) above.Runtime.SuppressFinalize(Me) End Sub #End Region End Class  Notes Regarding New Style P/Invoke Signatures VB.DllImport”. cut it down more using an alias like “Dll”: “Imports Dll = System.NET format for current development projects (they can suggest all they want.NET (which still work under VB.InteropServices. and would force the wide (MoveFileW) method invocation) ExactSpelling:=True. where the invoked function should marshal the data back to the invoker. that Shared member cannot be shared with other outside code that might try to access the shared item (directly.Unicode. _ ByVal nFolder As Integer. Module classes are automatically imported.0 – David Ross Goben <Dll("shell32. ' The DLLImport attribute forces invocation to the alias moveFile to be forwarded to MoveFileW ' in KERNEL32. or provide other additional attributes in the declaration. _ <[In](). Were you to wish to use these special tags. Page –43– . Were you to look at MSDN help for a P/Invoke. you can do something like the following (exclude the InteropServices parameters you do not need to specify): <DllImport ("kernel32. End Function A different and legal way to declare this P/Invoke Signature using the VB. which I have yet to find a situation that really required them. it details more options.NET Beyond the Scope of Visual Basic 6. otherwise. _ ByRef ListId As Integer) As Integer End Function Further. InOut (the function both reads from and writes to the buffer. but not back to the invoker. not only because its definition is terser than the VB. Under VB. if you want to define the actual function with an alias name. forcing Unicode) SetLastError:=True. if used on the return value. _ (Unicode is the default. Note that the parameter specifier ':=' allows us to specify only used ' parameters.dll". the function provides the buffer and initializes it.dll")> _ Public Shared Function SHGetPathFromIDList( _ ByVal pidList As Integer. They are also useful for self-documenting the code.LPTStr)> ByVal lpExistingFileName As String. _ (the actual invoked method that MoveFile will be forwarded to. not all possible. Leave this function body empty. or if a parameter will receive a result [Out].NET. MarshalAs(UnmanagedType. the invoker provides the buffer and initializes it). ByVal dst As String) As Boolean ' This function copies a file from the path src to the path dst. or even apply them out of their originally declared order. _ (True is the default) CharSet:=CharSet. MarshalAs(UnmanagedType. But in plainer language: to avoid syntax errors. the invoker provides the buffer and initializes it). where the data should be marshaled from the caller to the callee.DLL. This should not be confused with the fact that the Shared keyword can also 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.NET format is as follows: <DllImport("kernel32")> _ Public Shared Function MoveFile( _ <[In]().Enhancing Visual Basic . MSDN states that if an item is In (the function reads from the buffer. which can help you to determine how you should implement it. whereas DllImport format functions presently cannot. unless you remember to remove (or never add) the Shared declaration when adding them to Modules (because they are automatically shared). in most cases you will use [In].dll")> _ Public Shared Function SHGetSpecialFolderLocation( _ ByVal hWndOwner As Integer. _ ByVal lpBuffer As String) As Integer End Function <Dll("shell32.StdCall)> _ (StdCall is the Default. I must admit that I greatly prefer and use the VB6 format. specify the character set of the string to be sent. which will require it. Out (the function writes to the buffer.NET format. _ (the DLL containing the method we need to access) EntryPoint:="MoveFileW". the invoker provides the buffer and the function initializes it).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]. or what?). _ (True is the default) CallingConvention:=CallingConvention. do not bother tagging module members as Shared unless you are in fact creating a non-inheritable class. and imported member functions cannot also be declared as Shared because they are already shared publicly (the automatic importation of them causes their public/frient members to be treated as Shared Friend and as a non-inheritable class). Very FEW P/Invokes specify CDecl) Public Shared Function MoveFile(ByVal src As String. but also because the VB6 “Declare” format can also be defined without modification within Modules. anyway) without first qualifying it through an object that is in fact an instance of the encapsulating class (was that a mouthful. or Opt (the parameter can be NULL). The following list shows the implementation of the Cdecl calling convention: Element Argument-passing order Argument-passing convention Stack-maintenance responsibility Name-decoration convention Case-translation convention Implementation Right to left (last is first to be popped. _ ByVal nSize As Integer) As Integer 'you can replace the whole ByRef line with 'ByVal lpBuffer As String' to get the EXACT SAME RESULT. In that case the ByRef declaration should be marshaled by preceding it with “<MarshalAs(UnmanagedType. as StdCall uses significantly less code). was use the managed modifier as shown above. whether it is the address of the actual string (ByRef). on page 119. it can support variable count arguments methods (ParamArrays). Consider the following: Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" ( _ <MarshalAs(UnmanagedType. you can also append “Auto” (or “Ansi”. An underscore (_) is prefixed to the method name. first is last to be popped from the stack (parameters are pushed onto the program stack in FILO – First-In/Last-Out – order)). passing strings either ByVal or ByRef in VB. which is the usual case. unless a pointer or reference type is passed (ByRef). or “Unicode”) after the VB6-style Declare verb to duplicate that feature. 12 bytes) None (hence. However.NET Beyond the Scope of Visual Basic 6. Calling function pops the arguments from the stack. For managed memory safety. unless otherwise explicitly declared. and GetCurrentDirectory() come to mind). which will be discussed below.VBByRefStr)> ByRef lpBuffer As String. Same as StdCall. case sensitive). 260) 'init receiving buffer Dim I As Integer = GetSystemDirectory(S. By value (ByVal).Enhancing Visual Basic . The following list shows the implementation of the StdCall calling convention: Element Argument-passing order Argument-passing convention Stack-maintenance responsibility Name-decoration convention Case-translation convention Implementation Right to left (last is first to be popped. Same as StdCall. a function declared as “int func( int a. except when exporting __cdecl functions that use C linkage. Same as StdCall. because the callee cannot predetermine argument counts. Further. first is last to be popped from the stack (parameters are pushed onto the program stack in FILO – First-In/Last-Out – order)). By value (ByVal). the only way around that string parameter issue. so even if they were sent ByRef. the callee cleans the stack.NET would send the address of the start of the string to unmanaged code. you may wish to. Therefore. and more fully in a later article. simply for self-documentation purposes. At that time. as though you had simply sent it as ByVal. unless you want to modify that exact string.0 – David Ross Goben Though the Charset parameter makes the DllImport format valuable. Cdecl (the invoker will clear the stack) is used for invoking function written in C++ and C# (unless their functions are explicitly declared using the StdCall verb. the strings still would never be modified by unmanaged code. variable count argument (ParamArray) methods must be declared Cdecl. Page –44– . pre-2005 VB.VBByRefStr)> ByRef myString As String” (this is available by importing System. flushing it of all arguments to the method.Runtime. The StdCall convention is used to summon Win32 P/Invokes. Methods using this calling convention require a function prototype. NOTE: Even though the ByVal and ByRef references in the above tables may indicate to you that perhaps you should pass strings to P/Invoke Signatures ByRef. in the very rare instances where the P/Invoke would pass back a string via one of its parameters (the P/Invoke Signatures for GetWindowsDirectory(). Same as StdCall.NET had always sent managed strings as unalterable Constants to P/Invokes. are to be considered StdCall. The name is followed by the at-sign (@) and then followed by the number of bytes (in decimal) in the argument list. please refrain from that. None (hence. double b )” is decorated as follows: _func@12 (int=4 bytes + double=8. The default calling convention for C++ and C# programs is Cdecl. flushing it of all arguments to the method. Called function pops its own arguments from the stack. Because the stack is cleaned up by the caller. or the /Gz command line compiler parameter is present. as of VB2005 Dim S As String = New String(Chr(0). which means all methods.VBByRefStr)>” in the form “<MarshalAs(UnmanagedType. entitled “Upgrading Data Types for Win32 P/Invokes”. VB2005 introduced simpler techniques. case sensitive). The Cdecl calling convention creates larger executables than StdCall because it requires each function to provide stack cleanup code. which means that you do not really need to explicitly specify it. 260) 'now get system directory to String S and length to I Above. we marshaled a ByRef string using the VBByRefStr modifier. unless a pointer or reference type is passed (ByRef). or a copy of it (ByVal).InteropServices). although StdCall is the default calling convention to unmanaged methods with Platform Invoke (StdCall indicates that P/Invokes will clear the parameters off the stack). GetSystemDirectory(). Underscore character (_) is prefixed to method names. Prior to VB2005. NET event firing sequence. if you have TextChanged and/or Resize event methods defined for a form. Load. considering the subject we are now on. . _ ByVal nSize As Integer) As Integer Dim sb As New StringBuilder(260) Dim I As Integer = GetSystemDirectory(sb. but this is impractical because the form can be displayed by the user before it has finished running it Load event. SetLastError:=True)> _ Public Function GetSystemDirectory( _ <MarshalAs(UnmanagedType. be it as it may be.Auto. Internally.NET is now TRLAP: TextChanged. and Paint.Auto. Resize. This eliminated one more VB6 user complaint. which at its end invokes PerformLayout.ToString 'use StringBuilder to get API return data 'get system directory to StringBuilder and length to I 'aquire returned text as string For completeness. Indeed.NET will copy the original string to an 8-bit array and pass a reference to it to the P/Invoke. if the form is not yet loaded. has a different order than VB6. _ ByVal length As Integer) As Integer 'or 'ByVal S As String. Activate. and Paint. ByVal length As Integer' if VB2005+ End Function <DllImport("kernel32. Under VB6. and the marshalling tags would no longer be needed.Capacity) Dim S As String = sb. then translate it back to the native type upon return if the 8-bit array was modified. because a string is in fact an object. This just goes to show that if users scream loud enough…  A Note Regarding the VB. named mFormLoaded (Private mFormLoaded As Boolean = False).NET Form Event Firing Sequence The VB. you can be sure that those events will fire before the form is fully loaded. because. we used the I Love (or Loathe) RAP slogan to remember the order: Initialize. if necessary. you can bypass the whole ByRef string attribute hodgepodge and instead simply pass the string ByVal to the P/Invoke. which many online gurus usually recommend. Resize. Previously. _ ByVal length As Integer) As Integer End Function NOTE: As of VB2005. such as TextChanged when object text properties are assigned. CharSet:=CharSet. sb. and even before the form’s Load event is processed. This actually makes it comply with OOPL standards.0 – David Ross Goben A StringBuilder variation of it. SetLastError:=True)> _ Public Function GetSystemDirectory( _ ByVal sb As StringBuilder.NET Beyond the Scope of Visual Basic 6.VBByRefStr)> ByRef S As String. CharSet:=CharSet. Activate. following are the new VB.dll". We can eradicate this frustration by declaring a Boolean flag at the procedure-level of any form. the Initialize process no longer fires an event (it is replaced by an internal invocation to InitializeCompoents. Trouble Really Loves A Programmer? I wish the Load event would fire first. This means that the above ByRef string examples can now simply be passed ByVal. we can insert the following simple statement: If Not mFormLoaded Then Return 'do not process this event until the form has been flagged as having been loaded Page –45– . passing a value ByVal would strictly not allow alteration of the string. Load. exactly as you would have done it under VB6. Then. the exact firing order of initial form events under VB. is shown below: Declare Function GetSystemDirectory Lib "kernel32" Alias "GetSystemDirectoryA" ( _ ByVal lpBuffer As StringBuilder.Enhancing Visual Basic . as the first statement in each existing TextChanged and Resize event we have on a form. if they are defined). This can result is some frustration on your part and errors on VB’s part. as it does internally within our own non-P/Invoke methods that specify ByVal string parameters. to pass a reference pointer to it. so the original string will not be altered). and so it will by default pass a reference pointer to the first character of its string (pass it within parentheses if you want to create a clone.NET.NET formats for the above two GetSystemDirectory() P/Invoke Signature variations: <DllImport("kernel32. and any declared TextChanged and Resize events would simply be ignored until the form has been fully loaded.dll". and Resize events when form objects are dimensioned. when a form is loading. Under VB. However. because the internal PerformLayout method would naturally invoke other events. which in turn will naturally invoke the main form's TextChanged and Resize events. these events can trigger an exception error. This should be set to True (mFormLoaded = True) at the bottom of the form’s Load event code. But so I dream. You can prevent the Page –46– . otherwise not neeeded) The VB. vbModal Unload Form2 'declare a new reference pointer to type Form1 (not really needed if the Show command is used) 'declare new instance of form and assign to the reference pointer (not really needed) 'show the new form as a dialog via the reference and make the current form its parent 'remove the current instance of the form The vbModal command allowed the new form to be displayed like a dialog box. If you look at the documentation for the Close() method (search Help for Form. You can get by without invoking the Dispose() method if you use the Show() method. I found that the way to handle these functions under VB. Dispose() must be explicitly invoked if the ShowDialog() method was used because in this case it will not automatically invoke Dispose() in its Close() method. one thing you can still do in VB. and then you need to additionally invoke its Dispose() method.ShowDialog(Me) Form2.NET was the way that VB6 had previously done it for us.NET. VB6 Forms were different in that you could also create new instances of a form and assign them to a reference variable. But as I researched them and experimented. Under VB. we could instantiate it to a reference variable like this: Dim Form2 As Form2 Set Form2 = New Form2 Form2.NET Beyond the Scope of Visual Basic 6. Mind you. Load. Load. Indeed. The Close method removes the form from memory.Show Me. For VB6 controls. not hogging input focus. Also. I felt uncomfortable because so many familiar VB6 form-processing commands I depended on seemed to be missing. we used the I Love (or Loathe) RAP slogan to remember the order: Initialize. where most everything was hidden from you.0 – David Ross Goben  Notes Regarding Form Command Changes Form commands have changed. actually they still do exist.NET. where control of the application cannot continue until the new form is closed. VB. unless it is displayed as a dialog. it states “When a form is closed. but no longer needed as of VB2005) 'show new form in modal/dialog format. and Paint. Indeed.NET is now TRLAP: TextChanged. the exact order in VB. controlling program focus until it closes (exactly like vbModal). Resize. Well. The above VB6 code would be written in VB.Enhancing Visual Basic . but just in a form more open to your inspection than they were defined under VB6. because its Close() method will automatically invoke Dispose(). Resize. For example.Close() Form2. I noticed that events that I depended on were either not present.NET. However. these changes were not put in place to frustrate you. plus to also fully support cross-language interoperability. Activate. and ShowDialog() displays the form as a dialog. the TextChanged event and then the Resize event fires at the end of the initialization process.NET Close() method acts very much like the VB6 Unload command. not partial control over them. make current form its parent 'close form and release components (works like VB6 Unload command) 'mark form's components for deletion (expected if ShowDialog used. You will quickly notice that some of the standard form processing commands you may have used in VB6 no longer exist under VB.Close). Trouble Really Loves A Programmer? The Load command under VB6 was simply a way to instantiate objects. the Initialize process no longer fires an event. Under VB6.NET is to simply load the form and address it through its object name. and Paint. or they fired in a different sequence. but from “behind the curtain”. as stated previously in the previous note. though maybe sometimes they do. and the Query_Unload events. all resources created within the object are closed and the form is disposed. Unload. you typically referenced them through loading them into an indexed array.Dispose() 'declare new instance of prototype form (still valid. if we had a form named Form2.NET like this: Dim Form2 As New Form2 Form2. they were put in place to enable you to have complete. We could also ignore the Load command in VB6 and go directly to Show.NET has two primary display methods: Show() will show the form as an ordinary window (exactly like vbModeless). The reason for this is because you may want to obtain the form's DialogResult value before disposing of its resources. The vbModeless parameter told it to display like a regular form. such as the Load. when this form result resource will be lost. However. Activate. When I started using VB. using a simple module: Option Explicit On Option Strict On Module modShowForm Public Enum FormShowConstants As Integer Modal = 1 Modeless = 0 End Enum '************************************************************************* ' ShowForm emulates the VB6 method of displaying a form.” Further.” Elsewhere. the less obvious VB6 “Load Form1” statement was being used in place of the much clearer VB6 “Set Form1 = New Form1” statement. such as setting a timer with 10 a milisecond delay once the main form parts have loaded. in perfectly logical order. That never settled well with me. though that would be better served by a background task. should not fire until the Load process (and the other events) have finished doing their thing. and then display the form once it is set up.NET. It makes sense to me in the new VB. which is a handy place to do that sort of thing. but you must invoke Dispose() to fully remove it End If End Sub End Module To use it. if we cannot live without it.Modeless. If another method such as ShowDialog is used. _ Optional ByVal Modal As FormShowConstants = FormShowConstants. it states the point more directly: “Dispose will be called automatically if the form is shown using the Show method. and the optional parent window. VB.Modal. the FormClosing event will not fire. The weird thing about the VB6 Load command was that it was not really necessary with forms. A dynamic label or textbox therefore should certainly establish its data bounds before a form Resize event fires.Show() ' assume form closes itself. and selecting a display mode (Modal or Modeless) '************************************************************************* Public Sub ShowForm(ByVal Form As Form.NET should be more stringent in this issue. Alternatively.Modeless) Then Form.Enhancing Visual Basic . we could initialize it.ShowDialog() ' assume form closes itself. especially when a whole lot of things that take time are rumbling around in the Load event. and invokes Dispose() in its Close event Else Form. During the Load event of a form. we provide the method with our form.0 – David Ross Goben closing of a form at run time by handling the Closing event and setting the Cancel property of the CancelEventArgs passed as a parameter to your event handler. In these cases.NET Beyond the Scope of Visual Basic 6.NET TRLAP sequence that the TextChange events fires before the Resize event because of the anchoring capabilities VB. and (2) you have displayed the form using ShowDialog. but then we could also display it using the Show() method while we are still executing the Load event. which will then launch the burdensome stuff while we typically sit there staring at the screen for a few seconds. I just never saw the logic in the ILRAP/TRLAP sequences being able to be processed out of their defined orders. We can emulate the VB6 Show method easily. the optional modal setting (default is Modeless). your application ends. And Activate and finally Paint. If the form you are closing is the startup form of your application. in the form: ShowForm(Form1. you are able to invoke Dispose from within the FormClosed event if you want to.NET forms have regarding form resizing. It is still legal under VB. Me). you will need to call Dispose manually to mark all of the form's controls for garbage collection. The two conditions when a form is not disposed on Close is when (1) it is part of a multiple-document interface (MDI) application. if you invoke Dispose before Close.Owner = OwnerForm End If If (Modal = FormShowConstants. unless you wanted to load a form without displaying it. or the form is never shown at all. This last point brings us to one of the issues I have with VB. specifying a ' parent. and the form is not visible. This way you could load the form. Hence. Everything must have an order to it. The final thing to do is always painting. you must call Dispose yourself within your application. FormShowConstants. It is also in this last event that you should do any special Page –47– . though I do understand why it is done. _ Optional ByVal OwnerForm As Form = Nothing) If (OwnerForm IsNot Nothing) Then Form. initialize some control fields. The VB6 documentation indicated that the Load command simply instantiated the form. Y As Single) This header provided us with the ability to check which button is being held down.NET initially lacked native shaping controls because VB.NET form processing. I might write my FormClosing event like this: Private Sub Form1_FormClosing(ByVal sender As Object. It would maybe be ideal if Microsoft had adopted an LTRAP sequence.NET was the VB6 QueryUnload event. but sadly.com/en-us/vbasic/bb735936.NET Beyond the Scope of Visual Basic 6. _ ByVal e As FormClosingEventArgs) Handles Me. X As Single.NET FormClosed event).aspx). NOTE: VB. closing all forms End Enum Therefore. which deliver new controls for your IDE Toolbox. featuring line and shape controls that can draw lines. eliminating the need to draw shapes manually in the form’s Paint event (these controls come pre-installed with VB2010 and later). 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 if the form owner was closing by testing for vbFormOwner. or they had pressed ALT-F4. and “CloseReason”.UserClosing) AndAlso CriticalProcessesRunning = True Then e. But during the Load event. then the user was blowing out.Forms namespace: Public Enum CloseReason As Integer None = 0 'the closing reason could not be defined WindowsShutDown = 1 'the operating system is shutting down MdiFormClosing = 2 'an MDI child form is closing UserClosing = 3 'the user is closing the window TaskManagerClosing = 4 'Windows Task Manager is closing the window FormOwnerClosing = 5 'the form's owner is closing and is therefore closing this form ApplicationExitCall = 6 'Application. emulated printer I/O from VB6. Ctrl.FormClosing If (e. a PrintForm component to allow you to print forms as you did in VB6. if the Shift. 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. There was the answer right in front of me. One event I once thought lacking in VB.0 – David Ross Goben shaping commands.Windows. Shift As Integer. as otherwise data could be corrupted for example). Page –48– . You could also check in a Multi-Document Interface form for a value of vbFormMDIForm to see if a child form was closing.NET from what they were in VB6. Finally. which would have resolved every sequencing frustration we have had with VB. which provides a value that indicates why the form is closing. I took a much closer look at the FormClosing event. the TextChanged and Resize events have little choice but to fire during the Initialization process.CloseReason = CloseReason. or Alt keys are held down by checking the Shift parameter. by checking the Button parameter. They are defined in the following enumeration within the System. I simply had to check the UnloadMode parameter for a value of vbFormControlMenu. unless there was flagging to inhibit them before a Load is processed. This presented me with two properties: “Cancel”.NET cannot work with windowless (lightweight) controls. but it can work around this by employing Microsoft’s free Visual Basic Power Packs (http://msdn. the second parameter. If I decided I did not want the form to close. before the Load event. The pack also includes a printer control and collection. I would set the Cancel parameter to a non-zero value (usually 1). a Boolean flag that I could use to check or set whether the form should be closed. In VB6.microsoft. and we can obtain the local X and Y mouse coordinates from the like-named parameters.Enhancing Visual Basic . such as draw form-surface lines and circles and such. and a really neat data repeater that allows you to display rows of data in a scrollable container. Apart from the usual “ByVal sender As Object” parameter. If this test was true. This event had fired before the VB6 Unload event (the VB. e. rectangles and ovals at design time. was of type FormClosingEventArgs. you were provided with a somewhat excessively informative event header like this: Private Sub Form1_MouseMove(Button As Integer.Exit() method was invoked.Cancel = True 'cancel form close if user-defined critical processes running flag is set End If End Sub  Notes Regarding MouseMove Event Parameters The parameters for mouse events as MouseMove() have changed in VB. So in the body of the method I typed ‘e’ and then the dot. changes made to labels and fields can cause the TextChanged and Resize events to fire again. if any. EventArgs.Windows.Drawin. 1. containing only X and Y integer members representing pixel values. 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.0 – David Ross Goben But under VB. ByVal dy As Integer) Public Sub Offset(ByVal p As Point) Page –49– . But if your pre-written re-usable code uses the old VB6 values of 0. 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. for example).Form. As with the Form Closing event. as in “ByVal e As EventArgs”. which comprise its data footprint. The MouseButtons enumeration is not as primitive as the VB6 value was. However.Point structure containing the integer X and Y mouse location during the generating mouse event.Drawing. notice that in this case the event arguments are defined as “ByVal e As MouseEventArgs”. this is where you should really pay attention to the parameters you are provided. 4. However. ByVal right As Point) As Boolean Public Shared Function Add(ByVal pt As Point.NET Beyond the Scope of Visual Basic 6. as you can see below: Public Enum MouseButtons None = 0 Left = &H100000 Right = &H200000 Middle = &H400000 XButton1 = &H800000 XButton2 = &H1000000 End Enum As Integer '1 '2 '4 '8 '16 * * * * * &H100000 &H100000 &H100000 &H100000 &H100000 Of course. ByVal right As Point) As Boolean Public Shared Operator <>(ByVal left As Point. allowing you to access special properties such as the Cancel and CloseReason properties. Clicks. the MouseEventArgs (you do not need to specify System.MouseButtons that indicates which mouse button was pressed. Delta. and 16 (XButton1 and XButton2 represent auxiliary buttons you can find on newer Microsoft or Logitech mice.Windows. we can cut to the chase and check the built-in enumerations. Following is a prototype list of its capabilities: 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.Forms with this because that namespace is already loaded in a form) provides us with several useful properties: Button. and Y.NET. 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). ByVal sz As Size) As Point Public Shared Operator -(ByVal pt As Point.Left or MouseButtons.MouseMove Many new developers will throw their hands up in frustration. but is in fact an abstract class that happens to also include the X and Y integer properties. even with Interop!!! Public Sub New(ByVal x As Integer. ByVal sz As Size) As Point Public Shared Function Subtract(ByVal pt As Point. ByVal e As MouseEventArgs) Handles MyBase. relative to the top-left corner of the control beneath it (the form in the above example’s case).Enhancing Visual Basic . which has an EventArg defined as FormClosingEventArgs. Gets an integer value having the number of times the mouse button was pressed and released. then you can add the following line at the start of the event: Dim Button As Integer = e. Typically. Location.Button \ &H100000 'Note the Backslash for Integer Division The Location structure is unlike the simplistic point structure we were used to working with under VB6. 8. Gets an integer pixel value having the y-coordinate of the mouse during the generating mouse event. ByVal sz As Size) As Point Public Shared Operator =(ByVal left As Point.Point. They are described in the following table: Property Button Clicks Delta Location X Y Description Gets an enumerator of type System. 2. which means you can Private y As Integer 'actually use the simpler VB6 Point Structure or this one interchangably. Returns a System. Gets an integer pixel value having the x-coordinate of the mouse during the generating mouse event. such as MouseButtons. the event header will look something like this: Private Sub Form1_MouseMove(ByVal eventSender As Object.Right. the event arguments are just that. X. There is a lot you can do with a System. This returns an integer that will represent the data type stored within an Object variable. “NumLock”.Print(VarType(GenObj).Alt. “CtrlKeyDown”.0 – David Ross Goben 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 you need to complete the features you expected from your VB6 event is the old Shift parameter. as in: If Not My.NET Beyond the Scope of Visual Basic 6. defining a general type as Object is like using Variant. but with the added benefit of being able to process them much faster. Left Shift Key (LShiftKey).NET platform. Because all variables are derived from classes or structures. Further. and by one that is compatible all across the . Right Shift Key (RShiftKey). Media buttons. Consider the following subset of ModifierKeys values I whipped up: Public Enum SpecialKeys As Integer Shift = 1 '1 * &H10000 Control = 2 '2 * &H10000 Alt = 4 '4 * &H10000 End Enum Of course. be aware that you can also check “My.Computer. for example.ModifierKeys \ &H10000 NOTE: In form code. etc. or “CapsLock”. nor should it really be expected to be found there. you can instead simply test ModifierKeys against Keys. you need only specify ‘ModifierKeys’ and not the whole namespace/class path.GetType. or Keys. For example: “If ModifierKeys = Keys. but Microsoft wisely chose to adopt the naming convention of the CLR to avoid confusion for cross-language development. “Debug.NET. vbArray.Keyboard” for the Boolean result of its properties “AltKeyDown”. This can be used to very quickly identify the type of object actually referenced there. if it had been retained.NET platform. “ShiftKeyDown”. which means that.Control. This integer value corresponds to values listed within the new VarientType enumeration. In addition.NET could have continued to use Variant for its universal data type. etc.Keyboard. such as vbInteger.NET uses type Object as its universal type because all objects and scalars inherit from it.Computer. then you will want to examine the ModifierKeys value unaltered. vbString. Caplock. VB. if you want to check for other keys. Page –50– . Keys.NumLock Then Debug. VB.Name”. such as the function keys. Simply add another line at the beginning of your event as follows: Dim Shift As Integer = Control. Caps. Furthermore. the resulting text will be expressed as the text name of the type.Enhancing Visual Basic . instead of worrying about defining a local Shift variable for VB6 compatibility. as of VB2005. But it is still extremely easy to gather. because it has to do instead with the Keyboard.BrowserHome OrElse ModifierKeys = Keys. Finally.Print("NumLock key is NOT ON!")  A Note Regarding Type Object as the New Universal Data Type VB. it would have rendered VB incompatible with the rest of the . a flag that is preset within the object.. The type system is also simplified by having only a single universal data type declared. if you were to use the ToString method of the VarType result (for example. and also faster. now provides the VarType() function (similar to the VB6 VarType method that only worked with Variants).ToString)”). Granted. There is no place for it in the MouseEventArgs class.H Then”. This is the same as using “GenObj.Shift. such as “Integer” or “Double”. Module) But this is much too restrictive. by embedding the code assigned to m() directly as the parameter for GetHINSTANCE(). we obtained it by simply specifying App. or non-zero (indicating two or more instances): If UBound(pArray) = 0 Then Return False 'only one instance running (this one) Else Return True 'two or more instances running.InteropServices. However.NET Beyond the Scope of Visual Basic 6. sometimes we will allow multiple instances of an application to run. If we can generalize it even more. it is not so direct.InteropServices. all we have to do is go to the project properties.NET miss is getting an Application’s Instance Handle. and in the Make single instance application checkbox. Under VB6.GetCallingAssembly.Diagnostics.Reflection.Runtime. if we simply want to ensure than only one instance is running.GethInstance method).Enhancing Visual Basic . and on the Application page.Diagnostics.0 – David Ross Goben  A Note Regarding the Instance Handle Another thing that VB6 developers moving to VB. As it happens. it is a form class). we can acquire an array of class modules in our assembly by invoking System. and we can obtain the application’s instance handle from that. Under VB.Process. requiring us to provide it with an active class name.GetCurrentProcess().GetProcessesByName(pName) All we need to do next is to simply check to see if the UBound of the array is zero (indicating a single instance).GetExecutingAssembly.With this knowledge.Marshal. placing it all within the body of a couple of lines of code: Dim m() As System.Runtime. we can obtain the current process name using this command line: Dim pName As String = System.Reflection.GetModules() (you can provide a parameter of True if you want to gather the application resources as well).Diagnostics.GetHINSTANCE(GetType(frmView). Because of the managed structure of VB. as the free Integrated Development Environment editor enhancer. System. All that is left is to simply take the first entry in the array (m(0)) as the parameter to GetHINSTANCE().Reflection Module modHInstance '******************************************************************* ' HInstance: Get the application instance handle '******************************************************************* Public Function HInstance() As IntPtr Return Marshal.InteropServices.Marshal. We can actually reduce all this down to just a single line of Visual Basic code.devexpress.NET.ProcessName We can also gather an array of all like-named processes like this: Dim pArray() As System.GetHINSTANCE(m(0)) But even this is far too complicated. For example.GetHINSTANCE(Assembly.com/Products/Visual_Studio_Add-in/CodeRushX/).Runtime. make sure a checkmark is in the Enable application framework checkbox. but with the addition of a rather simple function.Reflection. Code Rush! Express informed me (visit www. First. and we are good to go.Reflection. In VB6.Module”.GetModules() Return System. you simply needed to check the Boolean value of App. but we want to check how many are running. I wrote the following module and function for my re-use library: Imports System. we might execute this line: Return System.Process = System.Process. including this one End If Page –51– . you can make it that way (note that the VB6 Compatibility Library does feature a VB6.Module = System. I am all for it.HInstance.GetExecutingAssembly.PrevInstance to tell if a previous instance of an application is running.GetModules()(0)) End Function 'Note that the above (0) grabs the zeroeth member of the returned Module array End Module  A Note Regarding Checking For a Previous Application Instance Checking for the previous instance of an application has changed. We can store it into an array that has been declared like this: “Dim m() As System.NET. What we need to do is obtain any instantiated class module in our assembly.Assembly.Assembly. to get the module for a class named frmView (OK. With VB. the App Path. for example.ProcessName)) <> 0) End Function End Module  A Note Regarding Getting the App Path and App EXE Name To get the Application Path under VB6. because the data instance would be null. get the application’s version number exactly the same as we did it under VB6. to obtain the full path.NET. However. nor can we instantiate a new ' instace of the class. And if you wanted to combine them. you can use something like the following in VB. to include the executable. it is a little more involved.NET. As a base. 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Page –52– .Application. and then appended App. ' Dim S As String = App. we can. By including the following static App Class in your Project (no need to instantiate).EXEName to it.Major) & ". we can implement a simple non-inheritable class and obtain this information just as easily as we did under VB6." & CStr(App. but the principle ones are.Application.Path value had a trailing slash. and we are left with extracting its many useful application-related values by other.NET to obtain the same thing: Dim AppTitle As String = My. you would use a statement like this: Dim AppEXEName As String = GetFileName(My. though platform-necessary means.Application.Process Module modHasPreviousInstance '******************************************************************* ' HasPreviousInstance: Return True if a previous instance is running '******************************************************************* Public Function HasPreviousInstance() As Boolean Return (UBound(GetProcessesByName(GetCurrentProcess().ExecutablePath)  A Note Regarding Getting the App Title If you used the VB6 App.Application. which we can access thru the "App" name.Title ' 'NOTICE: The more obscure (or phased out) VB6 App features are not supported.DirectoryPath To get just the executable’s filename with extension.Revision) Following is my App class. you used App. ' DO NOT INSTANTIATE AN INSTANCE OF THIS CLASS! USE IT AS-IS.0 – David Ross Goben We can put this all together into a single module and function like so: Imports System. To get the Application Executable Name.NET Beyond the Scope of Visual Basic 6. you can easily write them as simpler functions to operate much as VB6 did. featuring a number of the old App statement’s many useful methods: Option Explicit On Option Strict On 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' Class App ' This static class is used to expose VB6-style App functionality in order to easily access application information.Path. the code for the class will 'exist without instantiation.Info.ExecutablePath To get just the folder path to it. you can use the following statement: Dim FullPath As String = My. as well as many more directions in which you can go with these functions.Info.EXEName. but it also entails providing you with many more options. ' 'NOTE: Because the App class does not contain data. ' ' These function emulate the VB6 App command. to get the current app's title.Title command a lot (I constantly used it for registry value saving/loading in tandem with the GetSetting and SaveSetting persistent storage registry functions). we do not need to.Diagnostics.Title  A Note Regarding Reviving the VB6 App Command The App object in VB6 is completely missing in VB. you checked to ensure that the App. For example. if that point was not already obvious from a few previous points. sometime more complicated. you used App. However.Minor) & ".Enhancing Visual Basic . you would use a statement like this: Dim AppPath As String = My. However." & CStr(App. using: Dim Version As String = CStr(App. Info.GetCurrent. in VB. it is the 3rd part End Function '---Friend Shared Function Path() As String 'return the path the full directory path text of the application Return My. Revision is the 4th part of the version number.Path.DirectoryPath End Function '---- Page –53– .REVISION) Return My.NET.Info.GetVersionInfo(FullPath()).UserName 'return the logged on user name End Function '---'new.Application.GetModules()(0)). used in DotNET but not in VB6 Friend Shared Function Build() As Int32 'return the Build ID of the application version (Major. it is the 4th part End Function '---Friend Shared Function Revision() As Int32 'return the Revision ID of the version (Major.Revision) Return My.Application.Security.Info.Info. used in DotNET but not in VB6 Friend Shared Function Version() As Version 'return the full application version Return My.Application.Application. System.Enhancing Visual Basic .Application.Version End Function '---Friend Shared Function Major() As Int32 'return the Major ID of the version (MAJOR.Application.GetHINSTANCE(GetCallingAssembly.Build.Trademark End Function 'duplicate the above vb6-style method name using the more recent name Friend Shared Function Trademark() As String Return My.Copyright End Function 'duplicate the above vb6-style method name using the more recent name Friend Shared Function Copyright() As String Return My.Process.GetHINSTANCE(GetCallingAssembly.Description End Function '---Friend Shared Function hInstance() As Int32 'return the Instance Handle of the application Return Marshal.Minor.Revision) Return My. in VB.Application.Reflection.Info.Info. used in DotNET but not in VB6 Friend Shared Function Comments() As String 'return the Comments text assigned to the application header Return FileVersionInfo.NET Beyond the Scope of Visual Basic 6.Revision 'In VB6.Build. not used in VB6 Friend Shared Function UserName() As String Return SystemInformation.Principal.GetModules()(0)).Assembly. System.Build.Info.Minor.Comments End Function '---Friend Shared Function CompanyName() As String 'return the Company Name text assigned to the application header Return My.NET.Application.Diagnostics. not used in VB6 Friend Shared Function DomainUserName() As String Return System.ExecutablePath End Function '-------------------------------------------------------------------------'MAIN FEATURES '-------------------------------------------------------------------------'new.Major End Function '---Friend Shared Function Minor() As Int32 'return the Minor ID of the version (Major.Info.IO.InteropServices Friend NotInheritable Class App Friend Shared Function FullPath() As String 'Support function Return Application.Application.Info.ToInt32 End Function 'duplicate the above vb6-style method name using the more recent name Friend Shared Function Instance() As Int32 Return Marshal.MINOR.Info.Description End Function 'duplicate the above vb6-style method name with the more recent name Friend Shared Function Description() As String 'return the Description text assigned to the application header Return My.Version. Build is the 3rd part of the version number.Runtime.Application.Info.Minor.Application.Info.BUILD.WindowsIdentity.Minor End Function '---'new.Application.Revision) Return My.Version.Info.Info.Application.Name() 'This will return 'domain and logged on user name End Function '---Friend Shared Function AssemblyName() As String 'return the assembly name assigned to the application header Return My.CompanyName End Function '---Friend Shared Function EXEName() As String 'return the applications executable filename Return GetFileName(FullPath()) End Function '---Friend Shared Function FileDescription() As String 'return the Description text assigned to the application header Return My.Trademark End Function '---'new.Copyright End Function '---Friend Shared Function LegalTrademarks() As String 'return the Trademark text assigned to the application header Return My.Version.ToInt32 End Function '---Friend Shared Function LegalCopyright() As String 'return the Copyright text assigned to the application header Return My.Build 'In VB6.Application. System.AssemblyName End Function '---'new.Version.0 – David Ross Goben Imports System. Cint(Me.00")”. if you have a ListBox named ListBox1 and you want to get the string you assigned at the index stored in the integer variable Idx. but do not alter anything unless you actually do know what you are doing – not just think you know).ListBox1.  Notes Regarding Registry I/O The Registry can be accessed under VB.0. to let pending painting operations and timers do their thing Friend Shared Sub DoEvents() My. All we have to do is the following: 'save the WindowState key (SetValue automatically converts numeric keys to text by invoking their ToString method) '(use Cint() to cast the WindowState to a numeric value.##0.0. which is fully opaque (solid).ListBox1. under VB.Items(Idx). you can early-bind this and make it operate much faster by changing this command to “Me. “Acme Inc”.ToString”. For example.Items(Idx). This means that you will require a hive path named “HKEY_CURRENT_USER\Software\Acme Inc\MyOLAPpro”.Opacity = 0. The range is from 0.NET empowers Collections.NET Beyond the Scope of Visual Basic 6. Although the code will actually run.DoEvents() End Sub End Class  A Note Regarding Collections Enhancements VB. that is). where you had no direct access to the Registry or to other hives except through the API. suppose you wanted to save your form’s WindowState to a key value “WindowState” using a sub-key from the CurrentUser\Software hive path that specified your company name. It is also much easier to remember when you are writing native VB. Therefore. this pre-testing/pre-creation is no longer necessary under VB. Additionally. This exposes features that are more powerful than those offered by VB6.Items(Idx)”.Computer.  A Note Regarding Adjusting Form Opacity (Transparency) To adjust the Opacity (solidness) of a form (we might think of this feature inversely as more as a Transparency feature) is as simple as “Me.Application. Also notice that in an upgrade from VB6.Title End Function '---'simplify invoking the DoEvents process. “MyOLAPpro”.Registry class. ListBoxes. Under VB6. all keys were stored under the “HKEY_CURRENT_USER\Software\VB and VBA Program Settings” hive key path (they still are if you use the GetSetting and SaveSetting functions). Default is 1. default is otherwise worded text.Info. this same statement may caution you with a warning that the default value cannot be determined.ProductName End Function '---Friend Shared Function Title() As String 'return the Title text assigned to the application header Return My.ListBox1. representing 0 to 100 percent (divided by 100. to 1.Application. Unlike VB6. VB6 was restricted to using only simple strings.Registry. you can format the text by applying an optional format parameter to the ToString method.ProcessName)) End Function '---Friend Shared Function ProductName() As String 'return the Product Name text assigned to the application header Return My. we had to first test for the SubKeys “Acme Inc” and “MyOLAPpro”.GetItemString(Me. or 100 percent opaque (meaning fully non-transparent). under VB.ListBox1. However. and ComboBoxes with the ability to use objects as data. before we could write the “WindowState” value.Computer. or invisible.WindowState)) Page –54– .ToString("$#.Info. in succession. creating them if they do not exist. such as “Me. in VB6 you would have used the command “Me.0. it operates much faster if you simply used the original VB6 command in tandem with its new “ToString” property. as shown above.SetValue("HKEY_CURRENT_USER\Software\Acme Inc\MyOLAPpro".Enhancing Visual Basic .NET with Option Strict turned on.NET. Under VB6.Application.NET you can access the entire registry with ease (try running the RegEdit utility to browse the registry if you have some basic understanding of it. such as "Normal" or "Maximized") My. this line will be upgraded to “VB6. and then a sub-key from that for the application. However. as stated in a previous note.NET through the My. Although this works.0 – David Ross Goben Friend Shared Function PrevInstance() As Boolean 'Return True if a previous instance of this application is running Return GetProcessCount() <> 0 End Function '---Friend Shared Function GetProcessCount() As Int32 'return the number of processes running in this application Return UBound(GetProcessesByName(GetCurrentProcess.NET code. "WindowState".5”. Idx)”. such as 123US for Unsigned Short.FromFile(PicPath)” (under VB.NET that you can use just like the old VB6 function: Page –55– . and always to disasterous results.NET Example(s) String(" "c.FromFile(PicPath)” or “Dim img As Image = My. or 123UI for Unsigned Integer. For compatibility. a PictureBox control no longer supports both an Image and a Picture property. FormWindowState. you can either use DirectCast or the ToString method to acquire a text value (DirectCast is faster because it does not require helper methods. significantly reducing resources).NET Beyond the Scope of Visual Basic 6.NET. such as “Ofst += 1”: if I have Option Strict turned On (which I always do).” Use instead Chr(32) or ChrW(32) or CChar(" ") or " "c.Computer.Resources. it warns me that Option Strict will not allow casting from Integer to Short.CurrentUser.  Notes Regarding Loading Images Loading images into PictureBox controls involves little work.Computer. NOTE: If your Long. we can use DirectCast to cast it to the type that we know that it is actually formatted to (Integer). You can use the My.NET. and then assign it to a PictureBox when it is needed. such as: “Dim img As Image = Image. And how is that? Under VB6. FormWindowState) Because the GetValue method always returns a generic Object.Normal).  A Note Regarding Literal Type Characters One thing that bugs me is when I am using Short Integers and I add 1 to it. VB.WindowState = DirectCast(My. you had a number of shortcut symbols. which some have accidentally done via P/Invoke access to the Registry. The answer is to append “S” to the 1 digit. Integer.GetValue("HKEY_CURRENT_USER\Software\Acme Inc\MyOLAPpro". it now features only an Image property. but which may be necessary if the data cannot directly cast to the target type (type Object stores the actual storage type internally)). including intermediate sub-keys.NET also supports these. as CType does.45D NOTE: It may seem redundant to use a ‘C’ tag because a single character of a string is a Char.45F 123. 123UL for Unsigned Long.DeleteSubKey() method to do the opposite and delete a Key. it is actually an element in an array of 1 of type Char. you can also define an image object that would hold the graphic for later use. 128)” will generate the error “Option Strict On disallows conversions from 'String' to 'Char'. 128) 123S 123% or 123I 123& or 123L 123.NET Symbol(s) c S % or I & or L ! or F (Float) # or R (Real) @ or D VB.45@ or 123.NET. under VB6 you could load an image into a PictureBox using a statement such as this: “Picture1. here is a simple LoadPicture function for VB. so the values will not promote to Integers.45# or 123. you would do it like this: “Picture1.Image = Image. _ "WindowState". its registry SetValue() method can be used to automatically build a full path. you cannot accidentally delete the primary hive members HKEY_CURRENT_USER or HKEY_LOCAL_MACHINE.Enhancing Visual Basic . You can retrieve the above saved WindowState value like this: Me. plus a few more.45! or 123. that you could append to values to directly cast them to a particular storage type. For example. then precede the Literal Type Character with a U. With VB. Notice further that by allowing you to use DeleteSubKey() (and CreateSubKey()) only from the Hive Key members CurrentUser and LocalMachine. If the value to retrieve must be of type String. but it can be compressed into a simple function for compatibility to VB6.myImage”. For example.Image = img”.45R 123. hence “Ofst += 1S”. Or.Registry. even if they do not already exist.0 – David Ross Goben Under VB. “Dim s As String = New String(" ". called Literal Type Characters. Consider the following Table: Value Type Char Short Integer Long Single Double Decimal VB6 Symbol Not Available Not Available % & ! # Not Available VB. like this: “Picture1. or Short values are Unsigned.Registry.Picture = LoadPicture(PicPath)”. The IDE’s automatic Syntax Parser will help you keep these things straight. Under VB. which I highly recommend for reasons of speed (adding icons. and enabling the one feature that Image controls had that a PictureBox does not have: being able to display icons with transparent backgrounds.0 – David Ross Goben Public Function LoadPicture(ByVal PicturePath As String) As Image Try 'init error trapping Return Image.FromFile(IconPath)”. this was true only for Windows versions prior to Windows NT). so their formats are compatible).Resources. which will now go out of scope and will be picked up and its resources released during garbage collection. etc.ToBitmap”. Icon) 'get the resource pointer through the resource name Page –56– .NET LoadPicture method will also load Icon files. Windows 2000. will automatically convert the Icon to Image (Bitmap) format by merging the PictureBox’s blank background color with the icon.NET Beyond the Scope of Visual Basic 6.Resources.Icon = My.Icon = LoadPicture(IconPath)”.NET has taken care of this in the PictureBox control. allowing the background color to replace the icon’s transparency color (you may have noticed the reference to icon files in the above code’s comments).GetObject("Shadow"). this was only true when you had a very large number of controls on a form. so it is not necessary for you to declare it as a New Image. See the later article. jpeg. and so the advantages that the VB6 Image control could offer to VB. and 2) they persisted their images without having to enable the AutoRedraw property of a PictureBox.NET does not support windowless controls. That article also explains how to easily extend this transparent background capability to images as well. But the primary benefit of using windowless controls had been to reduce resource consumption. where IconPath is the file path to the icon file.ResourceManager. this command has changed to “Me.Picture1. Note further. such as the current form.NET are minimized (but not yet moot).NET upgrades. Under VB6.Shadow 'acquire a reference pointer to the icon stored in the resources.NET. as indicated by the appended “. png. during VB6 to VB. Windows Vista. If your Icon will be displayed in an Icon object.  Notes Regarding Loading Icons Loading Icon images has also changed slightly.Picture1. images. which ate more resources. and Windows 7/8 do not have these resource constraints. If the image is stored in the application resources. Icon. Emulating VB6 Image Control Features in VB.Icon = New Icon(IconPath)”. However. Windows XP. as shown in the above examples. because the icon resource is newly instantiated. and so lightweight controls offered absolutely no benefit for these more recent platforms. to an application’s resources is almost too easy under VB. you can load it using either of the following two methods: Me. People used Image controls in VB6 for two reasons: 1) they used fewer resources (actually.Enhancing Visual Basic . Windows NT. audio files. you could load an icon file as an Icon like this: “Me.Image = Image.Image = New Icon(IconPath).NET. VB.Icon = DirectCast(My. This is because an Image object is actually being redefined as a Bitmap object (a Bitmap object inherits from an Image object.) Catch 'catch ANY error End Try 'close error trapping Return Nothing 'return no object if the file format is unrecognized End Function VB.NET). or even “Me. As far as the benefit of image persistence without loss of resources. named Shadow Me. this benefit applied only to Windows 95 and Windows 98. then you can access it in one of two ways. Further. Image controls are changed to PictureBox controls.FromFile(PicturePath) 'load a image file (bitmap. NOTE: The FromFile() Image method will internally instantiate a new bitmap object and return it. like VB6 PictureBox controls. on page 72 to see several ways of emulating the features of the old VB6 Image controls under VB. the above VB. replacing the previous icon or image. or “Me.ToBitmap” method invocation. that when loading an icon file or resource to an image object requires that it also be converted to Bitmap format.NET. which. Because of this. Note the use of “New” in most of these statements. etc.NET does not support the VB6 Image control because VB. so there is no real cause to be concerned about when to implement the New keyword. and when not to. One more thing: In case you did not catch it. NOTE: In case you want to load an image as an icon. If your Icon will be stored in a 32x32 PictureBox named PictureBox1. or just close the project properties and you will be asked if you want to update the resources. This situation typically crops up only in linked lists.  Notes Regarding Accessing Private Class Members from a Sister Object One big complaint VB6 users wail and gnash their teeth about is that under VB. private fields or methods within a class are private to its class instance if they are declared Protected. a pointer actually always pointed to an object. You can even declare classes within Modules. Icon). or Dim. In older days. and thank all that is good for it! My question is. Now they are sometimes replaced by Handles. This is used extensively under . In like manner. Private. which I find myself in all the time. This is a very important feature that can often be worth its weight in gold. you really should consider either creating their own class file.0 – David Ross Goben NOTE: Just in case you forgot.Enhancing Visual Basic . this is not possible from objects of other classes.Resources. Navigate to your icon storage location and select all the icons you want to load. and it is clearly not likely to be self-malicious. all object variables are actually pointers to separate resources. However. you can actually define that subordinate class right within your main class. Mind you. If you find yourself in such a situation. except in the very specific case where an instance object of a class is referencing another instance of that exact same class! Because each member of the same class shares the exact same code. and the 32x32 pixel icon’s name is Shadow.Image = My.FromHandle(myPicImage.Resources.Image = DirectCast(My. use “Me.Icon = Icon.NET Beyond the Scope of Visual Basic 6. at the bottom. through scoping rules you can in fact declare instances of these classes from outside the master class or module by referencing the encapsulating class. or right between the declarations of two methods.GetHIcon)”. why are these people even wailing and gnashing their teeth? This is standard Object-Oriented fare that anyone who Page –57– . These embedded classes are not limited in scope.NET. 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. defining another class after the closing of the previous or main class? I have been doing this kind of thing since the days when I was wring code in Pascal. even while they seem to have absolutely no problem declaring Structures. Select File \ Save or hit Ctrl-S to save the resource file (Shift-Ctrl-S saves all files that have been modified). where objects will reference the consecutively next and sometimes previous members in its chain. you can load it from the resources using either of the following two methods: PictureBox1. because the Garbage Collector is constantly cleaning out discarded objects from the program Stack and Heap. Choose Icons from the Resource menu. and this single body of code must also be able to access every member that is instantiated by this class. but on several occasions we do not need to access these classes except from within the main class. or declaring them outside the module or class body of a file.ToBitmap NOTE: To add icons to the application’s resources. in a similar manner within their source files.ResourceManager. either at the top.ToBitmap 'An Icon format object must be converted to BitMap/Image format PictureBox1. private members are not private to the class instance by other instances of that exact same class. which are actually non-inheritable static classes. Very often we need to access a class or two from another class. What this means exactly is that an instance of a class is capable of accessing private or protected data members of another instance of its class. then from the Add Resource option select Add Existing File… This opens a browser. and then select Open to load them. as in any true Object-Oriented Programming Language.NET. but if you find yourself needing to do something like that. were you aware that you can also declare more than one class within the body of the same source file. Granted. Some developers are surprised to hear this.  Notes Regarding Embedding Classes I make the point in a couple of articles in this document that you can embed classes within other classes. of course it can access them. You can place it anywhere at the field and method level. except for the caveat that they can only be accessed from within the class body they are declared in. and they might need to parse through that chain in order to find a specific member. go to the project’s properties and select the Resources tab.GetObject("Shadow"). but only if they have a reference to it. which are actually abstract classes.Shadow. 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. shared codespace must be able to access all members that encapsulate that code._Next 'recurse forward through it (we actually access private members of identical instances) 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 the above example. their code-space is actually the very same shared code-space. or even Protected members might defeat the protected status of this data. making note of this class’s use of private members of other instances of this class in a doubly-linked list: Option Explicit On Option Strict On Friend Class myLinkedClass '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private _Next As myLinkedClass 'next data cell is this cell's chain Private _Prev As myLinkedClass 'previous data cell is this cell's chain '----------------------------------Protected _Data As String 'actual data to defines this object's Personality '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : NextSibling -. whose code-space must be able to access all objects instantiated only by its class. sData = sData. Granted. Even so.. sData = sData._Next End Get End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : PriviousSibling -. to reiterate. If the class were ignorant of itself._Prev IsNot Nothing 'while a previous sibling exists in the chain. Dim.0 – David Ross Goben has experience in object-based programming knows about and does not even think twice on because this is a logical consequence of an OOPL. it would not be able to do the above.Public member acessed by other members of this project ' Purpose : Return the DataCell's Previous Sibling 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend ReadOnly Property PriviousSibling As myLinkedClass Get Return Me. which is simply faster than using the more public members NextSibling and PrevSibling. 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)... you would think that being that two objects of the same class are uniquely able to access Private. a class that inherits from this class does in fact lack the ability to access these private members of its base class so those members are in fact hidden. To put it succinctly. Page –58– .NET Beyond the Scope of Visual Basic 6._Prev End Get End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : FirstSibling ' Purpose : Find the first member in the current sibling chain ' NOTE : Use private members of this and other sister object to loop to the start of the chain 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend ReadOnly Property FirstSibling As myLinkedClass Get Dim sData As myLinkedClass = Me 'start search with this DataCell Do While sData. Consider the following example. which is to access the private _Next and _Prev members of the provided Sibling. but because.Public member acessed by other members of this project ' Purpose : Return the DataCell's Next Sibling 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend ReadOnly Property NextSibling As myLinkedClass Get Return Me. such as the protected _Data member._Next IsNot Nothing 'while another DataCell exists after it in the chain._Prev 'recurse back through it (we actually access private members of identical instances) Loop Return sData 'return the myLinkedClass object that is at the start of this sibling chain End Get End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : LastSibling ' Purpose : Find the last member in the current sibling chain ' NOTE : Use private members of this and other sister object to loop to the end of the chain 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend ReadOnly Property LastSibling As myLinkedClass Get Dim sData As myLinkedClass = Me 'start search with this DataCell Do While sData.Enhancing Visual Basic .. 0 – David Ross Goben  Notes Regarding the Checked State of a CheckedListBox There seems to be a bit of hair-pulling out there when developers are trying to figure out how to properly monitor the checked state of an item in a CheckedListBox control. insert. Note that the seqence "\"c specified a single character.Text. To find the first occurrence of a character or string. then there was no match. there is little to be confused about. InstrRev(). "-"c) to trim dashes and spaces from either side of the string. Actually. myString.GetItemCheckState(clbOptions. or CheckState.TrimStart. whether it is CheckState. as just mentioned. and the like are still supported as a legacy part of the language.Remove(offset. If you do not set the state. What is also great about them is that you can pass them to functions and even P/Invokes just like an ordinary string.6) that make working with strings easier.LastIndexOf("\"c.SubString(offset).Replace(""""c. yet it is waiting for you to determine that checked state in this event. by you setting the desired state using the SetItemCheckState() method. myString. offset). You can change the character case using myString. Although VB6 commands like Instr().Contains("\"c) will return True if the string. but is about to. Thus. You can replace characters. You can use myString. CheckState. And that is all there is to that.ToLower. you can use myString. such as remove all quotation marks using myString.IndexOf(":"c). if all you want to use this event for is to see if the item at the SelectedIndex is checked or unchecked. Count) to grab that count of characters.SubString(0. You can determine if a string starts with another string using myString.Checked”. except from within the ItemCheck() event. For example: “Dim IsChecked As Boolean = clbOptions. or specify an offset and a character count using myString. then you need to flip its reported state.LastIndexOf("\"c) or myString. when it comes to reading documentation. For just a short list of amazing new featured offered by VB. it seems.0 (latest is 4. To look at the last character of a string. it will simply flip it. the new state has not yet been set. You can grab the rest of the string starting at an offset using myString. You can find subsequent occurences using myString. I am constantly stumbling on some now-old features as enhancements I was not previously aware of all the time. It is here that the interface allows you to determine what that checked state presently is.Last. that can also be used.Unchecked. Why? Because in this event. as opposed to a string. Page –59– .ToUpper or myString. such as StringBuilder objects defined within System. because. You can also search backward for a character or string using myString. which would be "\". it is simply preparing to set its checked state. Normally.Indeterminate.NET for string manipulation. you can use Dim Result As String = myString.TrimEnd. More robust string processing tools are available. Suppose you want to return the text preceding a backslash. I will type a dot after a command or variable and explore what pops up.NET Beyond the Scope of Visual Basic 6. and myString.Trim(" "c.Unchecked”.Checked. Count).SubString(offset. You can remove a number of characters from an offset using myString. consider the following incomplete list of enhancements:           myString. delete and concatenate strings up to 200 times faster than regular string manipulations features. Sometimes.IndexOf(":"c.Enhancing Visual Basic . contains a backslash character. you need to use “Dim IsChecked As Boolean = clbOptions. except. You also specified character parameters in the trim commands.GetItemCheckState(clbOptions.StartsWith(sndString).IndexOf("\"c)).  Notes Regarding New String Manipulation Features New string manipulation commands are exposed as of Dot Net Framework 4. you can check myString. myString. If the returned value is -1. The frustration arises when a user is trying to monitor the checked state of the selected item in the list using the GetItemCheckState() method during a ItemCheck() event. this statement works as advertised. It is a powerful tool that allows you to manipulate. Nothing). However.SelectedIndex) = CheckState. such as myString.SelectedIndex) = CheckState.Trim to trim a string. for fun. much more efficient zero-indexed commands are now available. offset). Mid(). NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Featured Articles Page –60– .Enhancing Visual Basic . you normally must recast them to their original types.NET Collection class. structures. as if they were generic Objects. “Dim customerList As New List(Of Customer)”. known as Generics. for type Object is not quite as generic as we think of it as being. I strongly recommend using DirectCast(). you may not get an error report as you otherwise would. Use Push and Pop to add and recover/remove items. This is important if TKey is a custom class. they are extremely powerful. an item of type Object actually does know exactly what type it is. use DirectCast() instead of CType(). because type Object is just a generic wrapper around that entry. NOTES: As indicated. With VB2005 you had no choice but define your own custom Generics classes in order to implement strongly typed collections. they return their data as generic Objects. As such. just like the default VB6-style Collection class. basing them on pre-defined templates. That could be if you are mindful enough to ensure that the objects are exactly of the type that you know them to be. if you know the object’s actual type. such as a string or class instance. they are still limited. It only acts that way. and also why it will snap at you if you specify the wrong type. A zero-based list supporting a “first in. This is because.GetType. This is why you can test an item of type Object for being of another type. Just like the default VB. A zero-based list that is optimized to store a very tightly packed array of Boolean (True/False) values (bits of 1 or 0). Among them were Edit-and-Continue.Enhancing Visual Basic . whether they are variables. the DirectCast() command will let you know if you must use the CType() instruction through an error message at compilation.Equals(Me) Then…”. A zero-based list of Values and Keys that is most like the VB6 Collection class. with a user-defined Generics collection class named List and a user-defined class named Customer. if you accidentally specified the wrong type. Page –61– . you will always retrieve the items in sorted order. But used correctly.Collections namespace. “If TypeOf Obj Is ListView Then…”. unlike the VB6 Collection class. and to top it all off. This means that if you use For…Each to iterate through its collection. and use CType() whenever you actually do want to convert one type to another. When VB.NET Generics Collection Classes The VB. Use Enqueue and Dequeue to add and recover/remove items. For example. The advantage to doing this was the introduction of the “Of” keyword. the My namespace. partial classes. internally.Name = "ComboBox" Then…”. I say as if. But.Collections namespace are: ArrayList BitArray HashTable Queue SortedList Stack A zero-based list that uses an array whose size can be dynamically adjusted as required. The built-in structures provided by the System. they are not strongly typed. the following statement. except that the keys are always sorted. which could result in a shadowy bug that can be much more difficult to track down. but they do accept any type of object as data. saying that it is safer. or “If Obj. A zero-based list similar to HashTable. Besides. This class allows you to look up Values by using a Key.0 – David Ross Goben VB.NET took its first major step forward with the release of VB2005. You can also supply it with your own comparison class. because it will not generate a single additional machine code instruction as Ctype() sometimes does. you can often expect to encounter errors at run-time due to binding issues. “If Obj.NET Collection control returns all entries. first out” model. Strongly Typed means that you can access its members as the type they are assigned as without casting. so you could write list classes yourself. which is used to enable the strong typing and force faster.NET can now be of any type. if you do get the type wrong. the Key and the Value under VB. and why you can use the DirectCast() instruction to tell the compiler to treat it as its original object type. unsigned integers. operator overloading. To use these returned objects. or classes. Strongly-typed collections can take on numerous forms. Generics VB2005 also allowed you to create your own strongly typed collections. data source binding. it also introduced many new and powerful features. design-time expression evaluation. it introduced Generic Collections and Generics. could create a strongly typed list collection of type Customer (notice the new Of operator. If not. Likewise.NET Beyond the Scope of Visual Basic 6. Though CType() will not generate additional code if you do specify its actual type. Microsoft recommends that you use CType() instead of DirectCast(). Generic Collections Generic Collections are accessed through the System. as in “If Obj Is MyCustomClass Then…”. first out” model. A zero-based list that supports a “last in. regardless of the type delivered to it as. to late-binding issues if used in reckless code. As such. early type binding.GetType. the Using and Continue keywords. implying “Of Type”). but this specification fails because at least one of the parameters must be of the type “GenericStack(Of itemType)”.ToString = elements(0).NET Beyond the Scope of Visual Basic 6. we must first declare a class that supported stack-like operations. such as “(Of Employee)” or “(Of TempEmp)”. Increase by 10 End If ' NOTE: PRESERVE modifier used to preserve previous content data element(pointer) = item ' All is well. etc. You can also declare it using “Dim stackA As New GenericStack(Of Integer)” if you want to use the default initial stack depth of 10. which could be a structure or class. For example. For example: “Dim stackA As New GenericStack(Of Integer)(20)”.. ByVal Item2 As itemType) As Boolean”. Double. if we wanted to implement a Stack class (Last In. or even using their ToString methods. In this situation. the Comparison is flagged as an error and reports “Operator ‘=’ is not defined for types ItemType and ItemType”. This creates a 20-element-deep stack that only accepts type Integer data. such as would be required to compare two items of Itemtype in the above stack class. during instantiation of the class. TempEmp)”.") 'force a trappable error Else pointer -= 1 ' else back stack index down Return element(pointer) ' and return last-pushed item End If End Function End Class The use of “itemType” is actually an arbitrarily-named Type-placeholder (I could have as easily just declared it as “T”). the stack will automatically expand if a deeper stack is needed. first Out) that also allowed at instantiation to provide strong support for a specific data type. ReDim Preserve element(UBound(element) + 10) ' expand the stack size to accomodate a deeper depth..Enhancing Visual Basic . cannot be implemented because it is too generic. Throw New Exception("Stack is empty.ToString Then…” Page –62– . add more constraints by separating them with commas: “Public Class GenericStack(Of itemType As Employee. which. However. This is due to the nature of the Generics class type. so stuff new item into the stack pointer += 1 ' and bump the cuurent stack index up End Sub ' '--------------------------------------------------' POP (pop an item from the stack and return it to the invoker) '--------------------------------------------------Public Function Pop() As itemType ' Pop an item off the custom stack If pointer = 0 Then ' If the current stack index is at botom of stack. In this case. a very easy solution to this for basic types (Integer. you would need to declare the instance of the class to be ‘Of’ a type. “Public Shared Operator =(ByVal Item1 As itemType.1) pointer = 0 ' Initialize stack location to the bottom of the stack End Sub '--------------------------------------------------' PUSH (push an item onto the stack) '--------------------------------------------------Public Sub Push(ByVal item As itemType) ' Push an item onto the custom stack If pointer > UBound(element) Then ' If stack index is out of bounds. we can declare our Generic class using “Public Class GenericStack(Of itemType As Employee)”. Or.) is to perform a CompareTo() comparison. For example: Dim X As itemType: X = elements(0): If X = elements(0) then Debug. some have tried to define an operator. in case you push more that 10 items onto the stack.0 – David Ross Goben For instance. For example.Print(“Compare OK”).. you would not be able to declare the object to be “Of” any other type. but instead you would instantiate it like this: “Dim stackA As New GenericStack”. something that could be as simple as the following primitive class: 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' Generic Stack – Strongly-Typed Stack Class that can support any type 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Public Class GenericStack(Of itemType) ' itemType is a placeholder for the actual type to be 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) ' Initialize the stack size. such as “If X. In this example. but only by specifying the allowed types.CompareTo(elements(0)) = 0 Then…”.. Regardless. say of type Employee. You can further impose constraints on a user-defined Generic type. if we want our Stack class to only process objects of a certain type or types. optionally defining its initial stack 'depth' ReDim element(size . This will be assigned an actual object type during instantiation. sadly. such as “If X. such as “=” to compare these two variables of type ItemType by adapting the typical template solution. IMPORTANT NOTE: Bypassing Operator Comparisons Between Generic Types: You might notice that you are not able to override Operator functions in your Generics objects as you were able to do with regular classes (those not featuring the “Of” specification). Therefore. This is a variable-sized stack of last-in-first-out (LIFO) zero-based collection of strongly-typed objects.Collections. Tvalue) Stack(Of T) This is a zero-based collection of keys and values. Page –63– .Collections.Generics. Methods are provided to search.Enhancing Visual Basic . Tvalue) SortedList (Of Tkey. The following pre-defined collections are supported in the System.Collections. Manage the lists through LinkListNode objects. Lamda expressions. This is important if TKey is a custom class.Collections. This is a strongly-typed zero-based list of objects that can be accessed by an index. We could also take the optional parameter out of our custom New() method and initially issue a “ReDim element(9)” to instantiate an object with a startup depth of 10 (0-9) elements. allowing you to declare your collection by implementing the new System.Generic namespace. sort.Icomparer(Of T) implementation. because System. Now. Use Enqueue and Dequeue to add and recover/remove items. This is a doubly-linked zero-based list. You can also supply it with your own comparison class if TKey is not a general type. Notice that these Generic classes are designed to be fully dynamic. you can declare everything you need for a strongly-typed stack collection by entering “Dim MyStack As New System. such as string or numeric. This is a zero-based collection of key/value pairs that are sorted by key based upon an associated System. you are able to implement any of several built-in strongly-typed collection classes. You can also supply it with your own comparison class if TKey is not a general type. But it also introduced Generics Collection Classes. It also eliminated the need for you to create custom class-definitions for commonly used types in order to enjoy strongly-typed collections. LINQ support. This is a queue of first-in-first-out (FIFO) zero-based collection of strongly-typed objects. This is a zero-based collection of key/value pairs that are sorted by key. shown above. and manipulate the list. This is important if TKey is a custom class. Anonymous types. such as string or numeric. Use Push and Pop to add and recover/remove items.Generic namespace. Actually.Stack(Of String)”.Generic is automatically imported in a VB project. and Extension methods.0 – David Ross Goben Generics Collection Classes VB2008 upped the ante by introducing many new features. though you certainly still can do so in situations not supported by the collection types provided by the System.Generic. instead of needing to first create needed Generic collection classes. without needing to write a custom class anymore. Type Inference.Generic namespace. Tvalue) LinkedList (Of T) List(Of T) Queue(Of T) SortedDictionary (Of Tkey. such as a true conditional operator.Collections. XML Literals. Generics Collection Classes enabled the use of type-safe zero-based collections that avoided the otherwise necessary task of also casting their results.NET Beyond the Scope of Visual Basic 6. NOTE: You are strongly encouraged to explore the method and property support for each of these Collection types. just as we could actually do in our much simpler example. where “Of T” represents the object type to strongly cast this collection to: Dictionary (Of Tkey. meaning that we do not need to declare a “depth”.Collections. you need only specify “Dim MyStack As New Stack(Of String)”. NET is to do it literally. I can just do this: Me. We can even do this: “Me. Suppose I wanted to change the font for Label1 to have a Bold style.Label1.Enhancing Visual Basic . on the other hand. 12. especially if we want to toggle a single style option.Name.Font. the move is much smoother because this process is exactly what you would have to perform in those environments. The best approach is to test to see if a change is actually required. causing livid consternation among many new users. the only way to change fonts at runtime under . which is usually not what we want. CurrentFont. Under VB.Font = FontChangeSize(Me. 2)) Then Return CurrentFont 'no need for change. We can accommodate this with a helper function that will check for that (be sure to also import the System. you may not want to change it if it is already set to the desired size. ByVal NewSize As Single) As Font If (Math. employs leaner. and you want to change it to 12 points. unless you do not want these embellishments. When a real change is actually needed should be the only time that we apply the desired state.Label1.Label1. underlined. Let us begin at the beginning… Easily Change Font Sizes at Runtime To change font sizes at runtime. but alter its style This works great.Name. Thus. Under VB6 you could toggle font properties during runtime at will. strikethrough.Drawing namespace in the heading of the file if you need it. we simply issue the command “Me.0 – David Ross Goben How to Perform Easy Font Manipulation at Runtime under VB.NET Beyond the Scope of Visual Basic 6. you can do something like the following: Me.Label1.Font = New Font(Me. so return a new font End Function To use it. but we used a new point size (12).Font.NET? It is actually quite easy.Font = New Font(Me. We can do this easily with another helper function. 12)”.Style) 'the font size changed. in case the text was specifically bolded. The reason for this is that VB6 used bulky font classes that contained all the functionality needed to manipulate a font’s properties. NOTE: We can also borrow font settings from other controls that are similarly set. Typically. these are easy enough to set at development time. An advantage of a leaner class is that you now not only see and know exactly what is going on. 2) = Math. we want Label1 to actually own its font).Font” (If we did not use the “New” verb. but at runtime some claim that this is now impossible.Label1.Set/reset Selected Font Size ' CurrentFont = font reference to change ' NewSize = point size to set '**************************************************************************************************** Public Function FontChangeSize(ByVal CurrentFont As Font. it is too often very important to also retain the original Font Styles. but if you transition your code to C# or C++. all you need do is create a new font based on the one you are replacing and then assign the new font where you need it.NET. but toggling styles might seem a bit of a problem. For example. and Strikethrough) To change the font’s Bold. underline. so return the current font settings End If Return New Font(CurrentFont.Font. making you think it was still the same font object.Font. although you can likewise specify “Me. or Strikethrough state is even easier. Also. more efficient objects that lack a cumbersome code overhead that lay idle most of the time.Label1.Font. Me. Underline. italic. 12)”. Underline. Easily Change Font Styles at Runtime (Bold. internally creating a new font instance whenever you changed one of its properties. Italic. or italicized. or set the font family. such as bold. suppose Label1 has a size of 10 points (10/72 Inch).NET A big change for VB6 users to get used to when moving to VB.Font = New Font(Me. then Label1 will simply piggyback a reference to the form’s font. NewSize.Label1. How do we perform on-the-fly runtime font manipulation under VB. Dot NET.Label1. but we can make it even easier with the addition of a few helper functions.Style) NOTE: This works because all parameters are processed before the new font is actually instantiated. As mentioned. For this. What we did was to simply create a new instance of a font using the old font’s name and style. FontStyle. Of course. like the one below: Page –64– . which handles drawing fonts): '**************************************************************************************************** ' FontChangeSize . this was what VB6 did for us using its heftier font class.Round(CDbl(NewSize). set a font’s size.Label1. Italic.Name.SizeInPoints).Round(CDbl(CurrentFont. leaving any others intact.Font = New Me.Label1.Bold) 'base the font on an existing font. or already set to the desired format.NET is Font Manipulation. Label1. The great thing about this flag is that we can set multiple styles at once. _ ByVal SetStyle As Boolean) As Font If StyleFlag = FontStyle. CompareMethod. newStyle = FontStyle. We then check it against the SetStyle flag. based on current font style settings Dim newStyle As FontStyle = CurrentFont. CurrentFont. but bold is already set (flag = True). we set the state of flag to True if the style we selected. _ ByVal StyleFlag As System.Regular Then SetStyle = True ' 'Force Setting if Regular specified 'mask desired style against current (do this in case of multiple selections) Dim fntStyle As FontStyle = (CurrentFont. We can do this by using the statement “Me. we should refer to this as the Font Family). with current selection set/reset Catch Try Return New Font(CurrentFont.Font.Font. we define a new font based on the old one and apply the new style change to it.Style And Not StyleFlag If SetStyle Then 'are we setting the new style(s)? If StyleFlag = FontStyle. so return current font End Try End Try End Function In the above function.Italic.Regular 'force all to Regular Else newStyle = (newStyle Or StyleFlag) 'else appy new type to new font End If End If Try Return New Font(CurrentFont. We could do this: Me. then simply return the current font Return CurrentFont End If 'define a new style value minus the current selection(s). Easily Change Font Names at Runtime The last thing we might want to do is change the actual font (in point of fact.Regular Then 'if Regular (0).SizeInPoints.0 – David Ross Goben '**************************************************************************************************** ' FontChangeStyle .Regular) Then flag = Not SetStyle If (flag = SetStyle) Then 'if nothing will change. We first define a local variable that will contain the current style flag.. CurrentFont.Style Or FontStyle.Name.Bold.Font = New Font("Courier New".Bold) Catch Return CurrentFont End Try End Try End Function Page –65– . Me. If we want to set it (SetStyle = True). Suppose we want to set both Bold and Italic styles to a label.. FontStyle.Style) But this is a ton of work if we have to do a load of it on our forms.Font. Finally.Text) = 0 Then Return CurrentFont End If Try Return New Font(NewName. and if it is set to True we will apply the selected style to the flag value. Me.Label1. then FORCE a change If Not flag AndAlso (fntStyle <> FontStyle. Italic.SizeInPoints.Style And StyleFlag) '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. True)”. then we have something to do. newStyle Or FontStyle. If the state of SetFlag does not equal flag.Bold) 'try applying bold Catch Return CurrentFont 'odd error (unlikely). in case we will be toggling it off (SetStyle = False). Suppose we wanted to change the font of our label to Courier New.Bold Or FontStyle.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. but without our selected style. To toggle a selected style off.NET Beyond the Scope of Visual Basic 6. ByVal NewName As String) As Font If StrComp(CurrentFont. is already set. NewName.Drawing.Label1. then there is nothing to do and a reference to the old font is returned. CurrentFont.FontStyle.Label1. we simply change the state of the SetStyle parameter to False. such as FontStyle.Support method for Bold. StrikeThrough set/reset '**************************************************************************************************** Private Function FontChangeStyle(ByVal CurrentFont As Font.Size.Style) Catch Try 'some fonts require Bold to be set Return New Font(NewName. CurrentFont. We can instead create a simple little function that will do all the dirty work for us.Font = FontChangeStyle(Me.Enhancing Visual Basic . We then check the SetStyle value.Label1. newStyle) 'return new font based upon current. such as the following: '**************************************************************************************************** ' FontChangeName . Underline. Style) Catch Try 'some fonts require Bold to be set Return New Font(NewName. Here is my complete module. Bold) End Function '************************************************************************************************ ' FontChangeItalic .Label1. Building a Suite of Runtime Font Modification Tools But suppose we want to set an entirely new font.Strikeout. but in the end.Drawing Module modFontChanges '************************************************************************************************ ' FontChangeBold .Set/reset Selected Font Strikeout ' CurrentFont = font reference to change ' Strikeout = True:Set.Set/reset Selected Font Bold ' CurrentFont = font reference to change ' Bold = True:Set.NET Beyond the Scope of Visual Basic 6. We could write. NewName. else Reset '************************************************************************************************ Public Function FontChangeBold(ByVal CurrentFont As Font. ByVal Italic As Boolean) As Font Return FontChangeStyle(CurrentFont.Font. then a full function would be in order.Style) End Function '************************************************************************************************ ' FontChangeName . All I would have to do is this: Me.Label1. as I have. the point size to 12. all we have to do to change the font to Courier New is execute this statement: “Me.Name. and the style to Bold and Italic. CompareMethod. ByVal Strikeout As Boolean) As Font Return FontChangeStyle(CurrentFont. CurrentFont. FontStyle.Style Or FontStyle. for small jobs.SizeInPoints. 12.Bold Or FontStyle. For example. else Reset '************************************************************************************************ Public Function FontChangeStrikeout(ByVal CurrentFont As Font. a function to do all this.Set/reset Selected Font Size ' CurrentFont = font reference to change ' NewSize = point size to set '************************************************************************************************ Public Function FontChangeSize(ByVal CurrentFont As Font. CurrentFont. CurrentFont. CurrentFont.Label1. 2) = Math. FontStyle. NewSize.Name.Italic) Or. such as the font size and the style. which was originally based on the font support provided by the VB6 Compatibility Library: Imports System. CurrentFont.Underline. ByVal Bold As Boolean) As Font Return FontChangeStyle(CurrentFont. "Courier New")”.SizeInPoints.Text) = 0 Then Return CurrentFont End If Try Return New Font(NewName.Bold) Catch Return CurrentFont End Try End Try End Function Page –66– . Italic) End Function '************************************************************************************************ ' FontChangeUnderline .0 – David Ross Goben With the above FontChangeName() function. suppose I want to change the font of Label1 to Courier New. Underline) End Function '************************************************************************************************ ' FontChangeStrikeout . FontStyle. FontStyle. We can then copy a reference to this new font to any other control we need to assign it to from Me.Round(CDbl(NewSize).Enhancing Visual Basic . probably the easiest method is to simply define a new font on the spot. ByVal NewName As String) As Font If StrComp(CurrentFont. else Reset '************************************************************************************************ Public Function FontChangeUnderline(ByVal CurrentFont As Font.Set/reset Selected Font Underline ' CurrentFont = font reference to change ' Underline = True:Set. just as had been demonstrated at the beginning of this article. ByVal Underline As Boolean) As Font Return FontChangeStyle(CurrentFont. ByVal NewSize As Single) As Font If (Math. or change more than one property.SizeInPoints).Font = New Font("Courier New".Bold. else Reset '************************************************************************************************ Public Function FontChangeItalic(ByVal CurrentFont As Font.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.Font. FontStyle. 2)) Then Return CurrentFont End If Return New Font(CurrentFont.Font = FontChangeName(Me.Label1. Strikeout) End Function '************************************************************************************************ ' FontChangeSize . if we will be doing a lot of this.Round(CDbl(CurrentFont.Set/reset Selected Font Italic ' CurrentFont = font reference to change ' Italic = True:Set.Italic. Support changing multiple properties '************************************************************************************************ Public Function ChangeFont(ByVal CurrentFont As Font. try returning new font If Changes Then Try Return New Font(Nam. _ Optional ByVal NewName As String = vbNullString.Round(CDbl(CurrentFont...Text) <> 0 Then 'if name actually changes Nam = NewName 'set new name Changes = True End If End If If NewSize <> 0 Then 'if we will change the point size. 'define a new style value minus the current selected 'type.Regular Then 'if we are setting a style flag 'set flag to true if selected style is (or all selected styles are) already set flag = (fntStyle = StyleFlag) 'if not EXACT match. so return current font End Try End Try End Function '************************************************************************************************ ' ChangeFont .Size 'get current point size Styl As FontStyle = CurrentFont. so set new style to flag End If Changes = True End If End If 'if there are changes. newStyle Or FontStyle. and difference is big enough to impact it visually. based upon the current font style settings Styl = DirectCast(CurrentFont. If (Math.Enhancing Visual Basic . with current selection set/reset Catch Try Return New Font(CurrentFont.. then FORCE a change If Not flag AndAlso (fntStyle <> FontStyle. try bolding Try Return New Font(Nam.Support method for Bold.Bold) 'try applying bold Catch Return CurrentFont 'odd error (unlikely).Regular Then 'if Regular (0). 2)) Then Siz = NewSize 'set new size Changes = True End If End If If StyleFlag <> FontStyle. StrikeThrough set/reset '************************************************************************************************ Private Function FontChangeStyle(ByVal CurrentFont As Font.Regular) Then flag = Not SetStyle If (flag = SetStyle) Then 'if nothing will change.FontStyle.SizeInPoints).FontStyle = FontStyle. Siz. _ Optional ByVal StyleFlag As System. _ ByVal SetStyle As Boolean) As Font If StyleFlag = FontStyle. Siz.Regular 'force all to Regular Else newStyle = (newStyle Or StyleFlag) 'else appy new type to new font End If End If Try Return New Font(CurrentFont. NewName. but something IS different.Round(CDbl(NewSize).Style 'get current style flags fntStyle As FontStyle = (Styl And StyleFlag) If CBool(Len(NewName)) Then 'if we will change the font family name..Style And StyleFlag) '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. _ ByVal StyleFlag As System. Italic..Bold) Catch End Try End Try End If Return CurrentFont 'return current if nothing to change.Style And Not StyleFlag.Regular) Then flag = Not SetStyle If (flag <> SetStyle) Then 'if something will change. newStyle = FontStyle. Styl) Catch 'on error. based on current font style settings Dim newStyle As FontStyle = CurrentFont. then FORCE change If Not flag AndAlso (fntStyle <> FontStyle. _ Optional ByVal NewSize As Single = 0. newStyle) 'return new font based upon current.Name 'get current name Siz As Single = CurrentFont.0 – David Ross Goben '************************************************************************************************ ' FontChangeStyle .Style And Not StyleFlag If SetStyle Then 'are we setting the new style(s)? If StyleFlag = FontStyle.Drawing. _ Optional ByVal SetStyle As Boolean = False) As Font Dim Changes As Boolean = False Dim flag As Boolean = False Dim Dim Dim Dim Nam As String = CurrentFont. Underline. Styl Or FontStyle..Drawing. 2) <> Math. CompareMethod. or error End Function End Module Page –67– . then simply return the current font Return CurrentFont End If 'define a new style value minus the current selection(s).NET Beyond the Scope of Visual Basic 6.Regular.. FontStyle) If SetStyle Then 'are we setting the new style? Styl = (Styl Or StyleFlag) 'yes.Regular Then SetStyle = True 'Force Setting if Regular specified 'mask desired style against current (do this in case of multiple selections) Dim fntStyle As FontStyle = (CurrentFont. If StrComp(Nam.. and the main form itself is a child of the desktop.0.NET because it is no longer needed.Transparent. it had to do a lot of things in the background. Later. VB. After all. in the hour of their most desperate need. which in turn provides any child control with its local coordinate system.Transparent. but it also had a cost to execution speed because it was not written just once to the form’s background. but it is something that has always been available to all knowledgeable programmers all the way back to when Microsoft Windows was first introduced. the actual home location relative to the top-left corner of the screen is seldom physically 0. When they upgraded to VB. is also a child of at least a form. VB6 supported certain clearly defined containers. though in genuine screen coordinates. but individually to each control it overlapped. when they tried using it on the label… nothing seemed to happen. the label just seemed to jog over to somewhere else. it allows a label to present a transparent background primarily to its parent control. which is treated as coordinate 0. not to metion the time spent parsing all controls on the form to check to see if they overlapped (as you will see. Easily Placing Labels With Transparent Backgrounds Over Controls VB. a parent control is always considered to be a container for the child control. they would have discovered that this property had been rendered moot by a new color value. such as in front of a picture.0 to the PictureBox. they found out that the background will only be transparent to the label’s parent.0. To allow for the fastest overall execution speed. But under VB. if it did not disappear from view altogether.NET does what VB6 should have done. the first thing they may have discovered was that a label’s BackStyle property no longer existed.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Drawing Labels with Transparent Backgrounds over Images Many new VB. Page –68– . doing so seemed to be as easy as someone zapping their groin as they straddled an electric fence out on their grandparent’s farm. and then with images near the end of the next article. usually after a few web searches. it was very convenient. this schema greatly simplifies and accelerates the code. Because in most typical cases its parent is the only surface it will ever need to be transparent to. or Form. and then you moved on to the next task on your list. There. drawing all or a portion of its text to each surface that would be even partially covered by it. such as a PictureBox.0. DOH! If they were lucky. Mind you. always down and right. though even the client area is a child of the main form.NET. Anything drawn to it is subject to a relative location from its top-left corner. which is typically a rectangular area with a defined pixel length and width. GroupBox.NET treats every surface like a canvas.NET. For VB6 to make this process appear so simplistic. because this exact same process will be demonstrated in code later in this article. or. and all these linked controls have a “home” location that their child controls all treat as 0. all properly offset. Ooh! Again! But after the initial flush of relief. you set the label’s BackStyle13 to 0 (Transparent) if it was not set to that value already.Enhancing Visual Basic . after some quick checking of Microsoft’s upgrade notes in the MSDN library. It had to draw itself on the form’s background and onto each control on the form affected by the label. on page 72). every control that is declared as a parent to another control automatically becomes a “container”. when they tried to assign the label’s Parent property to the control it is in front of. they lowered themselves to actually reading some documentation. such as a PictureBox. where the PictureBox’s location is relative to the top-left corner of the form’s client area.NET. which to subordinate contols is always assumed to be local coordinates 0. The BackColor property has absorbed it simply by adding one additional color value to its repertoire: Color. you just placed a label where you wanted it on a form. Color.NET programmers have complained of the trouble they are having in just trying to display some stupid label without its background painting a gray box around its text. This is not a new concept freshly introduced by Dot NET. this seemed to happen with perfect ease under VB6. Ooh! However. It was not rocket science. God forbid. 13 Note that the VB6 BackStyle property is not supported in VB. Emulating VB6 Image Control Features in VB. But the surface itself. after some livid grumbling and a smidgen of soul-searching. NET becomes quite simple to do.Me.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.BackColor = Color. in most cases you will not want to center the label over its new parent. You can do this quite easily by simply subtracting the image’s top and left offsets from the label’s top and left offsets. though it might appear to be centered over the PictureBox at design time.Enhancing Visual Basic . at development time you would want to position the label and the image to the locations where you will want to see them at runtime. and you must ensure its origin offset is relative to its new parent. Typically. will simply disappear from view. if a 64x64 pixel PictureBox has its top-left corner at coordinate 100.Parent = Me. You must set the parent. to the image you want displayed.100. PictureBox1. This offset. its display data will simply be discarded because the image would have been drawn outside the defined display range of the canvas.500. will consequentially become its offset under its new parent’s relative home location. To demonstrate this. then a 80x20 label. 2) Set the background color of the label to Transparent. then that image will not be exposed through the parental viewable ‘window’ at all. 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.0 – David Ross Goben NOTE: To be technical. I am all too familiar with this process. and it will simply center the label’s text over the PictureBox: Me. representing 90 degrees in a 360 degree circle. Microsoft chose to invert the vertical axis in order to make the coordinate system simpler for users. so anything that is “drawn” outside the displayable range of its parent object (its container) is simply clipped off.Height) \ 2) As you can see. Having been for almost a decade a software engineer at an industry standard CAD/CAM company. 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). that is exactly how the Graphical User Interface (GUI) of Windows works. the “home” location for a graphical system would be the bottom-left corner of the screen. an IDE property) Me. 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 surface range of its new parent. just so you will not lose track of where you placed it). just as it did not do so even under VB6).Location = New Point((Me. Consider the following code. then move Label1 to where you want it displayed (you may have to select Format / Order / Bring to Front to also display Label1 on top of PictureBox1. 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. just add the following block of code in your Form Load event: Page –69– . Position PictureBox1 where you want it. just like you may have done under VB6.PictureBox1. Indeed. So. Set the Label. but you are drawing an image onto it that starts at coordinate 500.140. which will never automatically update (nor should it ever be expected to. the process of placing a label with a transparent background over an image under VB. which you can place in the Form Load event of a new project with a PictureBox and a Label present. Now. These coordinates.PictureBox1. internally.Label1.Width) \ 2. such as “Which is X and Z?” Set the PictureBox. such as 100.100. you need to set these lines at runtime.Width . to the text you want displayed.PictureBox1 'Required: this must be done at runtime (there is no. can push its new relative display location completely off the PictureBox’s canvas. if you have a parent canvas that is sized to 300 by 300 pixels. who usually think in terms of starting at the top of the page and moving downward. rightward and downward.Height .Label1. nor should there be.Me. from an engineer’s point of view. because its top-left offset to its old parent object (the form) was set to 110. For example. if not corrected. (Me.Label1. place a PictureBox and a Label on a form. However. What is important here is that an object’s canvas does not go on to infinity.Label1. instead of upwards.Label1. you must set the background color to Transparent. whose parent is presently the form. being a circle’s upper-right quadrant. unless you 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. In understanding this. Label1.NET Beyond the Scope of Visual Basic 6. We then draw the text directly to the bitmap using the text from the label. because all the above prep work was not really necessary. and will await disposal by the background-running GarbageCollector process that is always running. Once completed. Brush. but then you have to come up with font information.Left) 'Adjust to relative coordinates within the target Bitmap object Dim Y As Single = CSng(Lbl.Text.Width = picBox.Left -= . and much faster version of the above code: Page –70– .Bitmap(picBox. Lbl. 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. Y) 'update the PictureBox image object by assigning to it the new.BackColor = Color. Consider the following (bloated) method from my very first attempt at doing this. then dispose of what objects we can.Top) 'NOTE: We can actually use Integers instead of Singles here 'Draw the Label Text on the Bitmap thru the Graphic interface using text.Location = .Height '(NOTE that System.Image. ByRef Lbl As Label) 'Read and save the Image Dimensions of the PictureBox to a Size structure Dim TmpSize As System.Size 'these 3 lines can be shorted to Dim TmpSize As Size = picBox.Top End With '(too bad we cannot do this: .DrawString(Lbl. it removes superfluous processes. exactly where you had placed it on the form during development. all you need.ForeColor) 'set the color of the text (the color to paint the text with) 'compute how to relatively position the label text on the bitmap Dim X As Single = CSng(Lbl.Image.Image.Transparent 'set Label1 background color to transparent .Drawing.Image. In this case. sizing it by employing the PictureBox image size and current graphic contents. a brush.Subtract(.Font.Left . X.Enhancing Visual Basic .Drawing. Mull over this much terser. plus the brush and the computed coordinates to paint and position the text.Left 'adjust to relative coordinates of new parent .PictureBox1 'set Label1 parent to Picture1 .0 – David Ross Goben 'Set Label1 over Picture1.Dispose() End Sub Supplied with a PictureBox.Drawing. and so I instead allow the graphical interface to draw directly to the PictureBox’s image. You can also do this with raw text. now unreferenced. This results in fewer created objects.Label1 . or borrow it from another object.Drawing.Parent.Parent = Me. we assign the new bitmap to the PictureBox image.Dispose() 'destroy resources that we no longer need Graphic. though you could easily cook it from scratch (see How to Perform Easy Font Manipulation at Runtime under VB.Parent. I am always looking to make my code run as fast as possible and using the fewest requisite instructions.Location)) When you run this. and local starting coordinates Graphic.Height = picBox. However. more direct.Top -= . its font definition. We then define a color brush to draw the text with and compute how to position the label on the image. updated bitmap copy picBox.Size TmpSize.NET Beyond the Scope of Visual Basic 6.picBox.Graphics. such as a control or the form. we first create a new bitmap.Image = NewBitmap 'set the Image reference pointer to the new Bitmap object 'dispose of what we do not need (we do not dispose of the Bitmap object because the PictureBox image is now referencing it) Brush. such as a PictureBox Image or even to the form itself. The above listing is along the lines of how most gurus will show you how to perform this task. In reviewing the above first draft.Drawing namespace does not need to be specified) TmpSize. relative to their current form-based positioning With Me.Top . I rewrote the method to skip setting aside objects to hold dimensions and copies of bitmaps because all of this is actually redundant. and executes considerably faster.Width 'Create a new Bitmap object of a size defined by the Size structure and also grab a copy of the image data at the same time Dim NewBitmap As New System.FromImage(NewBitmap) 'set up a color 'brush' to use for drawing text through the graphical interface object and set it to Black Dim Brush As New SolidBrush(Lbl. you will see the text displayed. implementing all the rules as defined by Microsoft and dutifully echoed by most gurus out on the web: Private Sub DrawTextOnImage(ByRef picBox As PictureBox.NET on page 64).picBox. 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. a font. again. is a PictureBox with an image and a hidden or invisible Label.Graphics = System. TmpSize) 'TmpSize parameter not really needed (no rescaling) 'Create a Graphical interface object that can be used to directly interact with the new Bitmap image Dim Graphic As System. Painting Text Directly onto Controls with Ease A faster-executing approach is to simply draw the text directly on a control.Parent. by appearances.Location. However. 2) A color brush for painting the text. brsh.Text. .Visible = False 'make sure label covering controls is not visible End Sub '--------------------------------------------------------------------------'Paint form and layer Label on top of it and any child controls '--------------------------------------------------------------------------Private Sub Form1_Paint(sender As Object.DrawString(. .Image) 'Draw Label Text directly onto the Image in the PictureBox. All the images were huge and were auto-stretched.Location.Left. such as the form.picBox. so remove End With End Sub End Class Page –71– . Lbl.ForeColor) 'create brush to draw text on control e. ' 1) The Font information associated with the Label.Y)) 'compute offset location to Ctl With Me.Dispose() 'dispose of the objects we no longer need Graphic.Dispose() 'clean up resources For Each ctl As Control In Me. as opposed to using the earlier described methods. on a form and across form controls '------------------------------------------------------------------------------Public Class Form1 'initialize by rendering label that will cover controls to not paint its background Private Sub Form1_Load(sender As System.Top .X.DrawString(.Object. using.Visible AndAlso ctl. .Load Me.Location.Left . such as PictureBoxes..FromImage(picBox.Font. e As EventArgs) Handles MyBase. ByRef Lbl As Label) 'Obtain a reference to the existing Graphical interface for the PictureBox image data Dim Graphic As Graphics = Graphics.Label1. e As PaintEventArgs) With CType(sender.Refresh() 'allow control 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..ForeColor) 'set the color of the text (the color to paint the text with) 'Note: Automatic Csng() done within drawing method.Graphics. the text is extremely persistent. you will need to invoke the above method again for each image update.Text.Paint. below. AddressOf Ctl_Paint 'no longer needed. file. whether refreshing an image from an ImageList. Lbl. Label1.Label1.Bounds) Then 'if label intersect with Ctl. ' and 3) Computing how to relatively locate the label text onto the PictureBox Image Dim Brush As New SolidBrush(Lbl. .Enhancing Visual Basic . Painting Text across Multiple Controls with Ease If you have a label that must be displayed across not just its parent.Label1 Dim brsh As New SolidBrush(. '------------------------------------------------------------------------------'Demonstrate drawing a Label. offsetLoc) 'draw text at computed offset brsh. Brush..IntersectsWith(.0 – David Ross Goben Private Sub DrawTextOnImage(ByRef picBox As PictureBox.Graphics. for integer coordinate calculations Graphic. if you require overlaid text labels when you change images. but also across one or more other controls.NET Beyond the Scope of Visual Basic 6.Paint With Me.Text. resource pool.Dispose() End Sub NOTE: Because the text is now embedded in the run-time image (but not within the design-time image).ForeColor) 'create brush to draw text on form e. you can adapt the following code to enable this functionality. New Size(.Font. Control) Dim offsetLoc As Point = Point. AddHandler ctl. brsh.Dispose() 'clean up resources End With RemoveHandler .Paint.Font. Lbl.Top) Brush.picBox.Subtract(Me. e As PaintEventArgs) Handles Me.Location) 'draw text on form brsh.DrawString(Lbl.Location. or because the images are changing to simulate animation.Controls 'check each control on the form If ctl.. AddressOf Ctl_Paint 'allow label to draw over control ctl.Label1 Dim brsh As New SolidBrush(.Bounds. This example assumes that Label1 is the label that will be drawn across the form and to one or more controls: NOTE: Form1 is Label1’s parent to make calculations easier. they did not actually occupy space on the form as objects. we should find ourselves with the ability to bring about a . or Windowless controls. it was not actually there. we must endeavor also to make any emulation work swiftly. this means that if we want their functionality we will have to emulate it by doing what we Visual C++ developers always had to do – we drew the images ourselves. will in turn adjust the opacity of its client area. It was much like we were seeing a ghost on the screen. even if it overlaid one or more other controls. its client area can be drawn to. Labels. Thus. This is the perfect test bed for me because it involves floating cursors and round marbles that move across graphic objects over a triangular board. Image controls did not exhibit a solid presence on the form because they were simply painted images. What this means is that they possessed no Windows Handle and therefore had no object-based presence on a form. but the graphics are not presently as clean and robust as the version running under VB6. This was great for creating impressive graphics and animation. I would prefer that the red marble not have a square corner overlapping the pit icon. their SizeMode parameter is set to StretchImage. I will simply try to first address these overlapping icons. if you loaded an icon to an Image control. but the round objects and fake cursors look like they are floating around on their own personal flying carpets. because they presently appear to be gliding about on what are obviously square-shaped backgrounds. we need to start simple in order to test and verify the things we want to do. but it sure did look like it.NET.NET version of Image controls. which. This made them unusable to event-handling mechanisms in an objectoriented environment. So. There is no point to doing it if it is slow or results in a lot of screen flicker. Black. It just so happens that I have two 32x32-pixel icons in an About Box that I want to overlap for a game program I have been tinkering with. if we were to set a form to be borderless and if its transparency key was set to that of the desired transparency region of an image. if set to a color value.NET. but this was all done behind the curtain. Unlike a more conservative PictureBox. by setting a PictureBox’s BackColor property to Transparent and making sure its image features transparency).NET should feature object-based Image controls (in a limited way. So. Everything works. at 6. we should first look at drawing images on a form in order to perform the core feature of Image controls. to ensure that we can actually accomplish these things.ico.ico. My very first consideration toward achieving this goal had been to use a VB. I say VB. though this is not really necessary due to the icon size already matching their PictureBox containers. Some fancy coding allowed us to click where Images were being painted to and get a reaction from an event handler. they were simply painted onto it. creating a logo for my game. as moving VB6 Image controls tended to do. It was just as well. Image controls.20).0 – David Ross Goben Emulating VB6 Image Control Features in VB. Painting Images On A Form Before embarking on such a project. and of course they are displayed as flat with a BorderStyle set to None.NET One thing VB6 users miss in their transition to VB.NET is Image controls. and. like VB6-style Frames. because forms can be borderless so that its client area will cover its whole surface to be like image controls. at pixel location 20. Here is what it looks like in design mode (a red marble overlapping a round pit): Of course. they hindered program execution speed and users over-used them. and Shapes are what are known as Lightweight. which clearly ruins the intended effect. Sadly. and graphical zoom. their design-time images were just placeholders so that you could visualize their placement and specify where on the form they would be drawn. importantly. But as importantly.NET Beyond the Scope of Visual Basic 6. featuring animation. Red. For safety. it does. each with their icon images already loaded to them. shadows. Because we cannot use lightweight controls under VB. Presently they are defined on two 32x32 pixel PictureBox controls (formerly VB6 Image controls). but I have ported it over to VB. The controls are named Image1_0 (the pit image. Page –72– . And that was exactly what Lightweight controls did for VB6 users.Enhancing Visual Basic .3).NET Form. Everything is working great. the background bled through its transparent regions. Lines. I had originally written the game for VB6. it also features a property named TransparencyKey. and Image1_1 (the red marble. Thus. Simply remember that it starts with “Protected Overrides”.Enhancing Visual Basic . 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. though. though before any form image or form text is rendered. DownArrow. Please note the lack of a sender parameter. Developers 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.OnPaint(e)_ End Sub NOTE: Though the example I am building here will in fact later use the form’s Paint event. sometimes the reasons may seem mysterious. there is a reason why there are these two events. which form background clearing and painting definitely does to them. This method is useful for changing the form’s background image. to select “Overrides” from a dropdown menu. then Space.. The choice of the event through which to paint them might seem to be a matter of personal preference to many developers. both will fire before controls on the form are painted. then type “OnPai(”. Me. because at run-time I will want them completely out of the way. such as drawing contained controls that must be displayed (OnPaint by itself will not do this!). which is typical. exactly like that. Space. Once the Paint event exits. or works better in some situations than others. Besides.Visible = False 'set both logo PictureBoxes invisible. Also. And my cursor is blinking at the end of the statement ending in “(e)”. So all you have to do is type “Prot”. 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).NET Beyond the Scope of Visual Basic 6. However. thus rendering their drawing areas “invalidated” (this means that its screen-painted image no longer validly reflects its current embedded image). or its font before they are painted. but still within the body of the Form. There is just cause for using either. but it is worth the time to construct the OnPaint event when you want to initialize its form image or text at the start of the event. Image1_1} 'NOTE: referencing the objects through Image1() accesses the very same objects that are pointed to by Image1_0 ahd Image1_1. before any of its child controls have been painted. as I said. After all. DownArrow.Paint() method at the end of your OnPaint code. there is nothing to it. By implementing it. but only if you will also be expecting Paint() event duties to be performed. Me.Image1(1) = Image1_1 'You could have just done this: Dim Image1() As PictureBox = {Image1_0. it will also cause the usual form Paint event to be ignored. 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). to use either the Paint event or the OnPaint event. I will also render them invisible: Me. however. its text. All that is left to do is to paint them as needed. then “Ov”. which is not necessary because the sender is already known: the form class the ON-event is tied to.Visible = False 'we will be drawing them to the form in a Paint event The preliminary groundwork is now complete. This just makes them easier to reference later (we will be adding more controls soon): Dim Image1(1) As PictureBox 'local image collection for two items: 0 and 1 Next. I will use the formbased OnPaint event for now. otherwise you can forgo that invocation. Unlike form Paint. then DownArrow.Image1(0) = Image1_0 'build an PictureBox reference container list (collection) Me.Image1(0). though to a great many developers.Image1(1).. 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. visually anyway. and before a Paint event would fire. and suddenly I see all the following appearing in my code: Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) MyBase. somewhere within my Form_Load event I am going to assign these two controls to the array. 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). unless you also invoke the MyBase. just to familiarize you with it.0 – David Ross Goben Because I tend to group objects to keep them in order. the form-based OnPaint event will fire immediately after the form background is erased. or it just seems that one somehow works. Page –73– . Location) 'draw the 'lower' image onto the form (icon background matches form) Dim Bmp1 As New Bitmap(Me. Fine. Using just the following code renders them overlapped perfectly: Public Class Form1 Dim Image1() As PictureBox = {Image1_0.0 – David Ross Goben Notice that the event argument. 0)) '(re-)render a transparent border on the upper image e.NET Beyond the Scope of Visual Basic 6.. Image(0) I simply drew the image on the form using the DrawImage method exposed by the “e” PaintEventArg.GetPixel(0. is defined as PaintEventArg. form text. which is associated with the form being operated on.Dispose() 'be sure to clean up created resources that are no longer used to Bmp1. Finally. Me.Paint() 'Add this line if your form contains visible controls that must be painted. let us try that to start.Image) 'create a new bitmap.Image) 'create a new bitmap. Place a couple of small PictureBox controls on a form.Image1(0). this process involves created objects that must be disposed of. Consider the following initial version of my OnPaint event.Visible = False 'set both logo PictureBoxes invisible.Dispose() ' avoid memory leaks and bypass waiting for garbage collection.DrawImage(Bmp1. End Sub Above.Graphics. overlap Image1_1 above Image1_0.Image1(1). much like VB6 did by assigning “Nothing” to an object – well… VB6 did not actually destroy it right away…).Graphics.Load For Each img As PictureBox In Me. The way most on-line gurus tell you to paint images involves creating a Bitmap object.Location) 'draw the 'lower' image onto the form (icon background matches form) Dim Bmp1 As New Bitmap(Me. This is perfect.Dispose() 'be sure to clean up created resources that are no longer used to Bmp1. which I knew to be set to its transparent border color.GetPixel(0. I created another bitmap from Image(1).Graphics.Dispose() ' avoid memory leaks and bypass waiting for garbage collection. copied from the 'lower' image e. and then drawing that bitmap onto the form. Hence. MyBase. copied from the 'lower' image e. Me. Notice that with the first image.Image1(0). form text. 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.MakeTransparent(Bmp1. which will grab the color value from a specified pixel coordinate on the bitmap (it works like the VB6 Point command). applied a transparency color to it by grabbing the color of the pixel at coordinate 0. Try it..DrawImage(Bmp1.Image1 img.Image) 'now grab a copy of the 'upper' image (new instance for disposal purposes) Bmp1. name them Image1_0 and Image1_1. but before form image. I disposed of (destroyed) the resources for my created Bitmap objects. Me. Next. I then drew that image to the form at the coordinates defined for the source PictureBox.Image1(0).Location) 'draw the 'upper' image over the 'lower' image. e. Next End Sub '******************************************************************************* ' Subroutine Name : OnPaint event (takes place immediately after background painting. or controls) ' Purpose : paint graphics that overlap with transparency '******************************************************************************* Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) MyBase.0 of the bitmap.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).Location) 'draw the 'upper' image over the 'lower' image. ByVal e As EventArgs) Handles MyBase.Image1(0). End Sub End Class Page –74– . Also notice that the “e” event argument parameter enables access to the graphical interface to the form (e.Image1(1).Graphics. grabbing a copy of an image into them. The biggest advantage to using a Bitmap object is that it has full access to an invaluable MakeTransparent method and the GetPixel method.Paint() 'Add this line if your form contains visible controls that must be painted. minus transparent regions Bmp0. or controls) ' Purpose : paint graphics that overlap with transparency '******************************************************************************* Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) MyBase.DrawImage(Bmp0. but before form image. just to settle our curiosity about what those gurus rant about.DrawImage(Bmp0. following the guru recommendations: '******************************************************************************* ' Subroutine Name : OnPaint event (takes place immediately after background painting. because we can destroy their resources immediately. MyBase. then apply the coding that I have laid out below. Me. 0)) '(re-)render a transparent border on the upper image e.Graphics). because we can use this argument to invoke the DrawImage method we need to render a bitmap image onto the form’s surface. minus transparent regions Bmp0.OnPaint(e) 'do some required housekeeping first Dim Bmp0 As New Bitmap(Me.Enhancing Visual Basic .Image1(1).Image) 'now grab a copy of the 'upper' image (new instance for disposal purposes) Bmp1. Image1_0} 'local image collection for two items Private Sub Form1_Load(ByVal sender As Object.OnPaint(e) 'do some required housekeeping first Dim Bmp0 As New Bitmap(Me.MakeTransparent(Bmp1. Me. we do not actually need to assign a brand new object to it. where one is obsessed with repetitive tasks that might even to themselves seem ridiculous. were we suffer a brain fart and dispose of it. The upper image required access to a Bitmap. What we did instead was define a bitmap reference that simply has access to the Image1(1) image. even as they believe themselves to being more “socially connected” through their social media apps on their devices. bitmap references. Why not simply reference the original? We must remember that to use a reference variable. thus 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 data footprints. creating social barriers around themselves.Image1(1). because that would be all that would be required to be done. Thus. but that does not mean we must instantiate new data.ico and Red. not to mention still using more resources than is needed. as icons in my program’s resources and then just rendered them directly to my form during its paint event. Why must we create a bitmap copy of an image just to draw it.NET Beyond the Scope of Visual Basic 6. but that does not really mean that we have to create a new instance of the second image to get it.Drawimage. therefore working with it directly. I then browse 14 OCD: Obsessive Compulsive Disorder. This way we can actually use the PictureBox image’s data. With that in mind. 0)) 'render a transparent border on the 'upper' image e. Page –75– . Bitmap) 'now point a Bitmap REFERENCE to the 'upper' image Bmp.Image1(1). a real tragedy of our time. Black. but they do let the PictureBox’s background color bleed through its transparency regions when the PictureBox is displayed). which also runs much faster: '******************************************************************************* ' Subroutine Name : OnPaint event (takes place after other painting completes) ' Purpose : paint graphics that overlap with transparency '******************************************************************************* Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) MyBase. 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). consider my revision to our OnPaint event.MakeTransparent(Bmp.Graphics.Graphics. Even obsessive game-play or smartphone use can lead to this tragic disorder. totally ignoring the original image that that we copied it from. 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. when we paint icons using the DrawIcon() method. why not simply define a reference pointer and have it point to the master image.Enhancing Visual Basic .Paint() 'Add this line if your form contains visible controls that must be painted. we can just as easily assign a pre-existing object to the variable. When I take a really good look at even this shorter and faster code. they are not rendered when added to a PictureBox’s Image property.Graphics. which would not be a new instance.Image.DrawImage(Me. Me. so we do not need to clean up Notice I rendered the lower image directly to the form using e.ico.Image1(0). Indeed. Unlike images. I would reduce my OnPaint method to just four lines of code (two if I used the Paint event instead).Image1(0).Image. selecting Icons from the first dropdown list.Location) 'draw 'lower' image DIRECTLY from original image Dim Bmp As Bitmap = DirectCast(Me. we do not have to do any cleanup afterward. we are actually accessing and drawing directly from the PictureBox image. I could have simply stored the icons. our image would disappear because its bitmap data is presently assigned to the Image1(1) PictureBox. Instead. when we render the Bmp variable to the form using DrawImage.DrawImage(Bmp. choose the drop triangle on Add Resources. and I would not have to worry at all about transparency colors. And since the Bmp variable is simply a reference variable without a locally instantiated resource that must be disposed of. Therefore. where one tends to blow up on others for the most trivial reason. but they find themselves incapable of not doing them. and select Add Existing File.GetPixel(0. we might need a reference to a Bitmap to gain access to the MakeTransparent and GetPixel methods. End Sub 'We did not create new resources. their transparency color is not rendered (actually. I saw lot of waste (I wonder if I have OCD14 sometimes). let alone the additional time required to poke and prod and finally render their images. to include constantly imagining they hear their phone ring or heard a phone message chirp at them. I see that I am still doing far more programming than I really need to.Location) 'draw the 'upper' image over the 'lower' image.0 – David Ross Goben When I reviewed this. If I were to paint them to the background directly from the program resources. I add them to my Icon resources by selecting Project / Properties. which just sits there like a bump on a log? In the second instance. minus transparent regions MyBase. but we just treat it like a Bitmap. or anything! The first thing I would need to do is place my two icons into the program’s resources. and even lead to PTSD (Post Traumatic Stress Disorder).OnPaint(e) 'do some required housekeeping first e. choose the Resources tab. it will invoke a validation on the control’s rectangular area to tell Windows that everything is still hunky-dory and that any drawing we did will be considered valid. it will have already drawn its own image data. would look something like this: Private Sub Form_Paint(ByVal sender As Object. Page –76– .ICO extension. this circle being displayed only when the mouse moves over the marble or pit in question. I add it to my Icon resources just as I did for my Black and Red icons. the main goal of this experimental endeavor is starting to look achievable. The black pit should have a top/left pixel coordinate set at 20.ico and Red. 6. 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. I then browse to my game’s icon storage location and select the Highlight. Next.DrawIcon(My. By instead drawing it to the actual PictureBox’s Paint event. Notice that it will automatically name the resources by using the file names. which is grossly inefficient. 20) 'render the black icon at the old Image1(0) coordinates: 20. Black. You can afterwards rightclick them in the resource display and rename them if you so desire.Resources.Enhancing Visual Basic .Paint e. to boot. ByVal e As PaintEventArgs) Handles Me. However.ico. choose the Resources tab. I will know that when the control’s Paint event fires.Red.Graphics.Graphics. and I moved it to the exact location of the marble/pit in question. The way I had accomplished this task under VB6 was to use an Image control that contained a simple circle Icon. that is. Afterward. I set its visibility to True.Black. but I saved an incredible amount of execution time.NET Beyond the Scope of Visual Basic 6. Painting on Controls I have a 16-color 32x32 Icon named Highlight.ico. What I really should have done was to simply draw a circle there. the only thing left to do is to render the icons. and the Red marble should be at coordinate 6. overwriting a portion of it (these were the old PictureBox locations for these marbles).ico. Highlight.DrawIcon(My. and select Add Existing File.20. I select Project / Properties. but… Boy. selecting Icons from the left dropdown list. the results would seem to be invisible. and also delete the two PictureBox controls. When you close the Properties tab. where a cyan circle is drawn around a location if it is considered a valid move that will be guaranteed to also lead to victory (well. We have just performed the core task of a VB6 Image control – to paint its image and keep it persistent. and so my alternatives are what they should have been in the first place. The way I did it was to place a Cyan circle around a marble or pit if a choice at that location can lead to victory. 20. but because we are looking at ideas on how to emulate the things that a VB6 Image control did. the cheat using an Image control and Icon was just too easy. If I try this in the form’s Paint event. This option may be out of the question. replacing the deleted OnPaint event. My game also includes what I call a Training Wheels Mode (OK – a Cheat Mode). above and left of the Black icon. Now. 3) 'render the red icon at the old Image1(1) coordinates: 6. Finally. and so it will render them behind the PictureBox controls that I really want to draw on top of.NET I find myself in a situation where I can no longer us a VB6 Image control to do this. Closing the Properties tab updates resource references. sleeker. delete all the current code except the form class shell. I then hit the Open button to load it into my application resources. It now allows us to do our own thing on its canvas. overlapping the black icon End Sub Running this worked perfectly! Not only did I dramatically reduce the application’s resource load.3. There is no need to separately compile it.0 – David Ross Goben to my game’s icon storage location and select my two icons. as long as you actually do choose the various paths available that it will offer to you). You know. I have to hang my head when I admit that I had done it that way. as was the case with VB6.Resources. because these controls are placed in front of the form’s surface since they are painted after the form’s surface is painted. the images references are automatically saved to the program’s XML-based ResX resource file. Finally. behind the curtain. A newer. did it work great!!! But now under VB. This is because this event provides persistence only on the form’s primary surface. let us start by trying to display an icon over the top of an image.3. choose the drop triangle on Add Resources.20 e.ico file. This is done automatically at compile time. and much shorter Paint event. Black and Red. I hit the Open button to load them into my application resources. less their . Graphics. Image1_8.. This is nothing magical. which is preceded by an underscore. if help can be shown on that control. MouseMove.Resources. 0) 'draw the icon on the 32x32 sender image stating at its home location.Name.DrawIcon(My. I simply name them with a trailing index value. I have a global integer variable named ShowHelpIdx that will be set to a location value. 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. 32) 'so aquire the circle resource icon (named HighLicght. Image1_12 has ID # 12) using a custom GetNameIndex() method (shown soon).Enhancing Visual Basic . Instead of attaching controls to the event using Handles. whose local coordinate system we are using at this point.StartsWith("Image1_") Then 'if a PictureBox and its name starts with Image1_.0. 32x32..ico). which will extract the ID number from the end of the selected control’s name.NET Beyond the Scope of Visual Basic 6. For these PictureBox controls I have been featuring here. and see if it matches ShowHelpIdx. I can write a generic paint event that will handle all 15 of the needed 32x32-pixel PictureBoxes. Image1_6. '******************************************************************************* Private Sub Image1_Paint(ByVal sender As Object.. Image1_10. like this: 'create and fill our collection of primary images on game (pits) Public Image1() As PictureBox = {ImgShadow. I like ordering similar controls together in reference arrays. Image1_14. Paint. Image1_5. the event already “knows” which control to draw to through its “sender” object and also its “e” parameter.. This way I can keep sequential track of them and I can collect them into a common array. We first extract the ID of the control from the trailing digits in its name (i. I will instead attach the image controls to it in-code using the AddHandler command. although I have 15 image controls being supported by the above Image1_Paint event. and perhaps any other required events: Page –77– . Image1_3. Image1_4. halo. which matches it in size. Image1_13. Image1_11.Dispose() 'and finally remove the created resouces End If End Sub Here. Take Image1_1 or Image1_15.. since all we are doing is rendering the icon. 1-15.. Notice within the above code that I drew the icon at X and Y coordinates 0. Image1_7.. And speaking of easy. If so. I declared their common array like this: Public Const MaxSlots As Integer = 15 Public Image1(MaxSlots) As PictureBox 'set number of pits on the board 'set aside room for storing the marble pit images I then fill the array with my controls in my Form_Load event: 'fill our collection of primary images on game (pits) Image1(0) = ImgShadow '(Black marble I use for a shadow effect) For Each Ctl As Control In Me. 32. Image1_1. As you now know. End If End Sub Also.0 – David Ross Goben Because I will be handling all the pits and marbles identically. PictureBox) 'assign array element with it End If Next You can alternatively both declare and initialize the array directly. Though this topic is covered in the free companion manual to this tome. you may have noticed that there was no Handles verb declared in the header of the method. 0) 'draw the icon on the 32x32 sender image stating at its home location. In this case.0 to Visual Basic . ByVal e As PaintEventArgs) If ShowHelpIdx = GetNameIndex(sender) Then 'is this choice valid? (the trailing value of sender object is _1 through _15) e. 0.DrawIcon(halo. Image1_9.. e. If TypeOf Ctl Is PictureBox AndAlso Ctl.Controls 'check each control. Normally. This makes our work almost too easy. This is because we are laying it over the top of a 32x32 borderless PictureBox.. 0. which are named Image1_1 through Image1_15: '******************************************************************************* ' Subroutine Name : Image1_Paint ' Purpose : Support Image1() collection ' Comment : Event handlers for this method will be applied seperately. we will paint a help circle atop this image. Image1_15} I next assign event handlers for Click. Image1(GetNameIndex(Ctl)) = DirectCast(Ctl. I will briefly explain this useful technique here.HighLight.HighLight. Image1_12.. 32 pixels wide by 32 pixels high.e.. Image1_2. we extract our Highlight icon from our resources and draw it onto the control. ByVal e As PaintEventArgs) If ShowHelpIdx = GetNameIndex(sender) Then 'is this choice valid? (the trailing value of sender object is _1 through _15) Dim halo As New Icon(My.Graphics. “Navigating Your Way through Visual Basic 6.Resources. for instance. Before you ask (if you are new to this).NET Application Upgrades” (refer to the web link at the end of this document). Name Nam = DirectCast(sender.0 – David Ross Goben 'Assign event handlers by taking care of OOPL object referencing and For-Each looping. If it were a Bitmap. AddressOf Image1_Click End If Next This small bit of work does two important things. Image1(Index). but in that case you use the DrawImage function.ClientRectangle) 'draw a circle within the client rectangle bounds (32x32) pn. such as “Label” Nam = DirectCast(sender.DrawEllipse(pn. if you prefer them over arrays. a PictureBox background will bleed through a stored icon’s transparency regions. 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. return trailing value (this works due to 0-based Substring method) End If '(for Substring. Although we can specify a default pen right within the DrawEllipse method that will not cost us any resources. if I need to (also note that I actually do keep something in Image(0). which is my Shadow image to create a cool 3D effect when a ball is picked up from one pit.. If it were an Icon. 2. Drawing Image resources is just as easy as icons.Name 'Get the Object control type name Case GetType(Label). Button). and then dropped into another pit). When I tested this code. AddHandler pic. Image1(). 'You can also apply this technique to members of a strongly-typed Generics List. as well as the proper way of doing the graphic work we just did. 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.LastIndexOf("_"c) + 1 'find trailing underscore and add 1 If I <> 0 Then 'found one? Non-zero means Yes (it was -1 if it had failed. Get name 'If you need them.. and it is not sufficient enough to leave a significant enough visual impact for me on our image.Cyan. Page –78– . it will draw a solid image. Get name Case GetType(PictureBox). AddressOf Image1_Paint 'assign even handlers to each item in the Image1 array AddHandler pic. unless you also set the PictureBox’s BackColor property to Transparent.. as shown earlier. but this time by simply drawing a Cyan circle from within the Image1_Paint event. '******************************************************************************* ' Subroutine Name : Image1_Paint ' Purpose : Support Image1() collection '******************************************************************************* Private Sub Image1_Paint(ByVal sender As Object. which was also shown earlier. we got the index of the control from its name through our GetNameIndex method and saved it. leaving only a Cyan circle to be drawn onto the target control. End Select '--------------------------------------------------------------------------------------If Nam IsNot Nothing Then 'something to process? (this is the reason we initialized it to vbNullString) Dim I As Integer = Nam. it renders only its non-transparent portions of the image. PictureBox). This effect ended up working and looking much better than my VB6 Image version. And the best part is.NET Beyond the Scope of Visual Basic 6. let us take a look at an alternative. which uses even less time and resources. moved.Enhancing Visual Basic . Sadly.Name 'Using GetType().0!) 'set up a 2-pixel-wide Cyan pen (we uses a Single value in case of scaling) e. First. in case you thought my simple little GetNameIndex function did anything mysterious. I found that the icon is drawn to the form magnificently.Dispose() 'release resources of our created pen End If End Sub Above.MouseMove. because we will also use it when we draw our circle via DrawEllipse.Name Nam = DirectCast(sender..Click.Name 'Handle PictureBoxes. before we added 1) Return CInt(Nam.Name <> "ImgShadow" Then 'do not apply events to the ImgShadow picturebox AddHandler pic. 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. What we have just done is what Image controls did in VB6.Substring(I)) 'yes. Finally. that pen is only 1 pixel wide. you can add more Case tests here. Get name Case GetType(Button). it keeps my event code looking tidy. the transparent border and center associated with the icon is preserved and rendered transparently. You can also designate a transparency color.Name 'Handle Button. 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.GetType. Label). Second.Graphics. AddressOf Image1_MouseMove 'other methods you might want to also include.Paint.Name is safer than using a text string name.Name 'Handle Labels. For Each pic As PictureBox In Image1 If pic. I am able to index and enumerate (do a For…Each) through my control array. Child objects are always painted after their parent object is already painted.NET such effects still perform magnificently if we keep image control emulation to a minimum. and we will also have to render it on each of its controls that it might be moving over by ensuring that a refresh will be sent to each control that the image will be crossing. What all this boils down to is that we may have to render the image more than once. though I have personally noticed no conspicuous difference in operation so far that would actually merit taking such action. and any additional times for each of the controls the rendered image covers or only partially covers. The control will automatically clip any portion of the image that cannot be rendered to its own visible drawing canvas. which is namely displaying persistent images to the run-time screen. it does involve time to check all other controls to see if it overlaps them. wherever the image will cover the background surface. 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. This allows the system to update only these small regions of the screen.0 – David Ross Goben So far. NOTE: Alternatively. raised.NET Beyond the Scope of Visual Basic 6. it will just draw only on the form’s surface. under VB. 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). behind all the other controls. Although this is an ideal strategy. Instead. and long delays can cause flicker.Enhancing Visual Basic . time-wise. We will not have to do anything but let the system repaint over the ‘old’ location. for reasons of code simplicity. which is what my marble images must do when one is selected. which will pass a rectangle structure for the area to invalidate and the window handle of the form to the system. and dropped into a new slot. Before going on. to create the illusion that it is moving. Even so. Image with Transparency Rendering Over Multiple Controls One thing that the VB6 Image control also did that we have not yet done is to draw an image with a transparent border across multiple controls simultaneously. both where it was and where it will be moved slightly to. Page –79– . we could simply force it by instead sending an UpdateWindow() P/Invoke along with the handle of the form to make it update (Paint) immediately. sliding across other slots as needed. This can be accomplished using the form’s Invalidate() method. we want to focus only on where we are moving the image to. between the “upper” controls. as the form’s children. we have duplicated the core of what a VB6 Image control did. even if its rendering is hidden behind a control. which is why a VB6 Image control slowed a program’s over-all execution speed. maintaining persistence on the rest of the screen and greatly reducing the possibility of image flicker. Even great animation techniques can look like ejected matter from the south side of a north-bound horse if even marginal image flickering is involved. moved. because we want to erase our image from the place where it is currently rendered anyway. 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. we will have to compute the image’s relative position for each refreshed control and render the image to that computed location. We must first invalidate both the original region and the destination region so the paint event can update them. I must point out that drawing can be expensive. which is fortunately insanely easy to do. and to draw itself to each of them if they do. like two frames of a movie. but always to the form background. In the Paint event for these controls. even partially. properly offset to cover any covered portion. to craft a transitional “movie effect”. actually layered in front of it. These experiments also demonstrate that employing a resource image or icon is a superior alternative to a VB6 Image control. We will of course have to render the object in the background form’s Paint event. But if you think you 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. that drawing them directly to the form or onto another control is a preferable alternative to overlapping more resource-hungry VB6 controls. you are going to find that when the image is rendered. Spending too much time doing something force delays. and the system will in turn add this rectangle into its internal Update Region cache that will accumulate all pending changes until the form’s Paint event is fired. which is layered behind the other controls that are. Visible AndAlso . ByVal e As PaintEventArgs) 'if the control intersects with the current location of the painted image.Enhancing Visual Basic . previously added by Form_Paint End With End Sub This method works extraordinarily well. and finally invalidate that new area as well (we could just invalidate the entire form. the absolute best choice for an animation control is not a control at all. and a Bitmap resource stored in the Image region of the project resources returns an Image object). Increase values for faster animation.Resources. relatively. 0) Private imgSize As Size = My.Visible AndAlso .Resources. which afterwards chains into our event.red. XYoffset) 'Update current location to its new location Me.NET Beyond the Scope of Visual Basic 6. below: Private Sub myForm_Paint(ByVal sender As Object.Bounds.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.Invalidate(imgRect) 'invalidate the rectangular region at animXY for 32x32 pixels imgRect.Size Private XYoffset As New Size(4. imgRect) 'Draw the image to animate (Red..Paint. I want to examine another strategy.ico) from the resources to the form background 'Now scan for each control to see if they will interects with the present image's location For Each Ctl As Control In Me. Next.Y .Refresh() 'Refresh will invalidate the control if they intersect.0 – David Ross Goben The process of executing this task is amazingly simple.DrawIcon(My. imgRect.Graphics. ByVal e As PaintEventArgs) Handles Me. 4) Private imgRect As New Rectangle(imgXY. Here.ico marble stored in program resources 'icon image size 'Animation increment values. 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. if for nothing more than as an academic exercise. If . imgRect. This will cause the animation image to be drawn on it End If End With Next End Sub In our common Ctl_Paint event. assuming Point animXY holds a moving 32x32-pixel image’s current top-left coordinate: Me.Graphics. we must invalidate the area where the object is presently.Invalidate()).IntersectsWith(ImgRct) Then 'Bounds is the control's bounding rectangle e. we would check to see if the image we are moving will also cover any of our controls.DrawIcon(My.NET option (Me.Controls With Ctl 'If the control is visisble and its Bounds. AddressOf Ctl_Paint '(we get an error here until the Ctl_Paint event code is actually written) . The Refresh method will invalidate a control’s canvas.Left. it fires after the control’s primary image is already painted by its base class. and I have yet to ever have need of it in any of my code. firing only for the controls that were issued a refresh. There has been a lot of internet chatter about how “all one has to do is write a new image control using a form. If so. then paint the image onto the control Dim Ctl As Control = DirectCast(sender.Paint e.Resources. although this should seldom. unless you preempt it and force the Paint event to run immediately by issuing an UpdateWindow() P/Invoke (Private Declare Function UpdateWindow Lib "User32. Control) With Ctl If . imgSize) 'Animation image top/left location for 32x32 Red. before we progress much further.Invalidate(imgRect) 'invalidate the destination rectangular region A control’s paint event is processed once the internal message queue is empty (idle). because the entire form will then be repainted). update its coordinates to its new location.red. using that as our Image “control” (an Icon resource stored in the Icon region of the project resources returns an Icon object.Bounds. However. decrease for slower 'Set aside initial rectangle reference variable The next thing to do is that each time we will move the object. For example.. and because I have been thinking on this very concept as well. but rather an icon or an image stored in a local variable or in the program’s resources. Even so. which is also a VB. but that is. and thus queue the control for updates through a Paint event we assign to it.. intersects with the target. it is worth investigating.Location.Location = Point.dll" (ByVal hWnd As IntPtr) As Boolean).” But this is a lot more complicated than one might imagine.red. Page –80– . if ever be necessary. 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). Also. AddressOf Ctl_Paint 'remove this handler from the control.Paint.X . we must be certain to issue a Refresh for each overlapped control. and I will demonstrate it later in this article with complete code. For example. which is the control's bounding rectangle. a slower process.IntersectsWith(imgRect) Then 'the following AddHandler line runs faster if we simply add it in a for-each for all form controls in the form's Load event: AddHandler Ctl.Add(imgRect. consider the following header data in a form: Protected imgXY As New Point(0.. Struct pt will afterward contain the new Screen coordinates But VB. and that was that a form’s BackColor property will not. I will outline the steps I used. 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).Parent. and is readily at hand. which is the best icon editor I have ever used.NET. Under VB. and inexpensive.X = Image1(CurIndex).BackColor = Color. the VB6 code would require the following translated declarations: Private Structure POINTAPI Dim X As Integer Dim Y As Integer End Structure 'NOTE: You can actually use a VB. the icon editor within Visual Studio is acceptable. and I am off to the races.SupportsTransparentBackColor. except for an MDI child form. in relation to the background form or window). VB. The only thing we must consider when using this technique. However. sized to the same dimensions as my form (I use a PictureBox instead of the form’s background image. I had to also set the form’s background color to our image border color.Handle.NET Point rather than this POINTAPI structure – their footprints are IDENTICAL 'horizontal point from left edge 'vertical point from top edge Private Declare Function ClientToScreen Lib "user32" (ByVal hWnd As IntPtr.Top ClientToScreen(Me.FromArgb(&HFF. load an image or icon to it. Now all I need to do is add a borderless form. This initially prevented me from using icons with transparent borders. that would inevitably expose my form’s background. To use it and replace all the above is as easy as this: With Image1(CurIndex) Dim pt As Point = . NOTE: I use IconWorkshop from Axialis (www. pt) 'set aside a structure variable (I use New to avoid uninitialized variable warnings.Location) 'compute local pt structure values to screen coordinate End With 'reference control's parent in case it is placed on a container other than the form Easy-Breezy. which you are free to use. and impressive. one as icons. The best part is that the resulting animation application is smooth. though I soon found two ways to correct it. Later.PointToScreen(. fast. I recall that I had used a P/Invoke in VB6 that could quickly convert local to screen coordinates named ClientToScreen(). which I did by successively reducing the PictureBox height and width by 2 pixels and increasing its left and top coordinates by 1. providing the window handle of my form that the local coordinate is relative to. its PointToClient() method. set the form’s transparency color to match the transparency color for my image. because I will often resize the PictureBox within the bounds of the parent form. In a matter of a couple short hours I went from wondering if it would actually work to putting the finishing touches on it. Clr)” will also work (internally. though obviously their quality is unknown to me.Location) 'convert to screen-local coordinates. For example: Dim pt As New POINTAPI pt.Y = Image1(CurIndex). and I personally discovered that “Me. This is because a form. A quick web search for “icon editor free download” also yields plenty of free editors. had been my initial idea in this article. it was surprisingly easy and fast to develop. Before the fixes. but you must adapt them to your needs.0 – David Ross Goben Emulating What VB6 Image Controls Do Using a Borderless Form I had seen promise in the idea of using borderless forms to stand in for controls.SetStyle(ControlStyles. though Dim pt As Point is OK) 'fill with top/left corner coordinate of a PictureBox control 'You could also have just used: Dim pt As New Point(Image1(CurIndex).Left pt.axialis. I ran into a minor problem. features transparency. though you are free to use icons or images with transparency if you use either of the above two BackColor solutions. which is normally quite impressive. it does so by shrinking to nothing. and the other as images (ack!).NET. to emulate zooming or gaming “impact” effects). However. the system ignores testing Alpha component values).NET has this functionality build right in to a form or control’s PointToScreen() method. by default. which. is that we must think in terms of screen coordinates rather than local coordinates (client coordinates. To avoid that effect. What made this initially worse was that one of my game’s effects is that when a marble is removed from the board. we normally get a program exception if the image border is transparent.NET makes this even easier! This is an example of something that you will discover a lot as you snoop around VB. ByRef lpPoint As POINTAPI) As Integer To use it. which is displayed as a persistent 32x32 square (ugh!). is always subordinate to the screen. Page –81– .NET Beyond the Scope of Visual Basic 6. which I will accomplish by using a borderless PictureBox control. thus keeping it centered on its form. even if it set its own Owner property to another form. I realized that using “Me.com). I would fill a variable of type POINTAPI to a coordinate. When I finally decided to implement this idea. True)” would solve this problem. accept transparency colors. and then invoke ClientToScreen. if you recall.Enhancing Visual Basic . The down side of this first workaround was that I would have to maintain two sets of images. and conversely. would have actually eliminated the need to use a PictureBox on the form and allowed us to use the form’s BackgroundImage instead! .ShowInTaskbar = False 'do not display this form in the taskbar With Me. but the 'TransparencyKey property ignores a color's Alpha transparency setting.Img. 4.Image.Show() 'make visible. The steps to creating our custom Image class are as follows: 1.FormBorderStyle = FormBorderStyle. Add a new form to your project. we create our image “control”.) End Sub 5.Enabled = False 'turn off timer frmImage. 3. Note also that I will keep this “control” as simple as possible.Height = .Height .Enabled = True 'enable animation timer End Sub Private Sub Timer1_Tick(ByVal sender As Object. in case the frmImage Form_Load event fires) .. add a PictureBox control. Add a PictureBox to that new form and name it Img. ByVal e As EventArgs) Handles Me. Set the PictureBox so that it completely fills the borders of the Form (you can simply set its Dock property to Fill if you want to. we can assign it without a transpaency value.Tick frmImage. and you should see your image/icon track left-to-right across the screen.Top = -. Next.1) 'get sample color to use for transparency key If (Clr..Hide Me.TransparencyKey = Clr 'set transparency to the same (this also ignores amy transparency assigned to the color) End With m_FormLoaded = True 'tell the world we are now loaded (not used here.. you can instantiate as many of them as you need from it.ToArgb And &HFF000000) <> 255 Then 'if color contains transparency.None 'make sure we are borderless .PictureBox1.Load Dim Clr As Color = Me. I will walk you through a very quick and very easy project where you will be able to see the results of this technique by your own hand using minimal code.Select() 'IMPORTANT! Keep focus on the main form.Refresh() 'refresh its display when image moves to avoid delayed-update jerkiness If frmImage. Add the following code to the Form1 code page: Public Class Form1 Dim frmImage As frmImage Private Sub Form1_Load(ByVal sender As Object. and though from it I could develop an actual control.None 'make sure we are borderless . Bitmap).Icon = Nothing 'remove things that cause sizing to change . I will lay out the types of images I am using so you can easily adapt these instructions to instead suit your own images or icons.32) \ 2 'set it halfway down the background form End With Me. Clr = Me. but avoid 0!) frmImage.Width 'ensure image form size matches the picturebox Me. and ensure that its Interval property is set to 10.Width = . but usefull in more involved apps.Hide() 'hide image (same as frmImage.NET Beyond the Scope of Visual Basic 6.Top = 0 Me. transparent.Select is actually not required in this situation. Page –82– . so it will still render the form's background transparent! This workaround. exhibiting a common border color. I put it here to highlight its importance during normal processing. Add a timer named Timer1 to Form1.BackColor = Color.. in an application that might continue operating after the animation is completed… Now just run the program.Height . Me. which will be showing through the transparency End If End With 'because a form's backcolor property will choke on a transparency color an icon might have. Finally. ByVal e As EventArgs) Handles MyBase.Width Then 'if image has scrolled off the screen. with transparency regions… well. which will cause it to fill the form). Start by creating a new Windows Form Application named ImageEmulate.AutoSize 'in case we animate the picture control . 2.Owner = Me 'IMPORTANT! This will ensure frmImage will always display IN FRONT OF the owner form . launch Form_Load event first time.0 – David Ross Goben To get your feet wet. representative of the images you will be using..Height Me..Close() 'dispose of resources Me.Img . . naming it frmImage. onto the default Form1 and then load a really large image file to PictureBox1. You should see the main Form1 fill the screen. 6.Close() 'close and auto-dispose current form/application End If End Sub End Class NOTE: Although the last Me.Left = 0 'set image to left size of form (must position AFTER Show. Note that once you create it.Left = 0 'fit picture control tightly into the form .Left = -. if you application requires several. Open the code for the frmImage form and add the following: Private Sub frmImage_Load(ByVal sender As Object.BackColor 'then grab PictureBox background color.GetPixel(0.Top = (Me. Ensure the Img Image property is loaded with a bitmap or icon with either a solid or transparent border.Height ' brief initial flicker. Desktop background images do this pretty well.Enhancing Visual Basic . set the form’s WindowState property to Maximized.Select() 'prevent your form from being pushed down on display stack after the Hide() frmImage.Load frmImage = New frmImage 'define new frmImage Control With frmImage . I will simply lay out the raw data for now and treat it as a form class of the whole project.Text = Nothing 'this caption may cause headaches if not blank .Timer1.Width 'hide form off-screen to avoid once-in-a-blue-moon Me. All this is just to provide a canvas to play with the “Image” control we are about to make.SizeMode = PictureBoxSizeMode. Clr) 'set form background color to a border color sample (avoid any icon Alpha transparency issue) .Left += 4 'move image rightward by 4 pixels each time (increase or decrease for speed. Next.BackColor 'keep a copy of the form background color With Me . size Form1 and PictureBox1 to accommodate the large image.FromArgb(&HFF. or it could simply be the only image you will be using. 'figured out later.Left > Me. Clr = DirectCast(. otherwise its border will flicker . so position AFTER Show() Me.Timer1. PictureBox1. Next.BorderStyle = BorderStyle.Visible = False) Me. ByVal e As EventArgs) Handles Timer1. which will be showing through End If Else Clr = Me.Image IsNot Nothing Then Clr = DirectCast(Me.Icon = Nothing 'remove things that cause sizing to change . that it in turn automatically issues the Select() method on its owner form. all will look good. For example. because sometimes the main application form gets pushed down the display stack. and to auto-refresh the form upon such a move.Img . so it will still render the form's background transparent! This workaround. TopMost will simply cause layering issues. Room for Improvement Improvements to the form can be vaste.Img.Left = -. so that the position and bitmap could be set.Left = 0 'fit picture control tightly in the form . we can assign it without a transpaency value. 'figured out later. to avoid once-in-a-blue-moon flas End With Dim Clr As Color If Me.Select() from the background form immediately after showing your image. By always setting the Owner of the image form.Top = 0 Me. which resets things by hiding the image in the clipped area. or better.New() InitializeComponent() ' This call is required by the Windows Form Designer. and all continues to look great.Width = .AutoSize 'in case we animate picture control .Img. though providing absolute screen coordinates should also be an option. Clr = Me..SizeMode = PictureBoxSizeMode. behind any other applications that might happen to also be up. then the move should occur after the image set. which would reset the image to its default. Setting frmImage’s Owner property to the primary form will ensure that our image will always display above the form.Text = Nothing 'this caption can cause headaches if not blank . then immediately do a image form Refresh. Always position the Image form after you have selected Show().Width 'ensure form size set to 32x32 Me.Image control emulator for VB. Always Refresh the Image after any move to avoid jerkiness (the refresh will auto-cascade to Img). you could define a function that will allow you to load an image and specify if it should be treated as a bitmap or as an icon.. Clr) 'set form background color to a border color sample (avoid any icon Alpha transparency issue) .Height Me. ByVal e As EventArgs) Handles MyBase. 3.0 – David Ross Goben Important Tips (this is why I had said that this process was not as easy as it might first appear): 1. and then display the form by setting its Opacity to 100. You could add a different function to sample either a default or a specified point on the image to access its transparency color. or provide it yourself. You could also set it up so that when it performs Show() or Hide().NET – Written January 2010 by David Ross Goben '**************************************************************************************** Friend Class frmImage Private m_FormLoaded As Boolean = False 'flag that can be used by resize and change events to determine if the form has loaded yet '******************************************************************************* ' 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.FormBorderStyle = FormBorderStyle. As long as you follow this with a form Refresh.Img. 0) 'get sample color to use for transparency key If (Clr. but also set it to a new image and move it. Do a Me. as we have done previously. 5. Do not try instead to set frmImage’s TopMost property to True.Img. this keeps related windows local to its display level. but. and then do a Refresh.Activate) after hiding the last Image form. though it would be simpler to just move the form initially off the window. set the new image. Concerned about flicker elsewhere on the screen? I have never seen it as long as I position it immediately. each consecutive shown image will have higher display precedence.GetPixel(0.BackColor 'then grab picturebox background color.None 'make sure we are borderless .BackColor = Color.. position it. If you want to display/load a new image in the form upon Show().TransparencyKey = Clr 'set transparency to the same (this also ignores amy transparency assigned to the color) End With m_FormLoaded = True 'tell the world we are now loaded (not used here. but the 'TransparencyKey property ignores a color's Alpha transparency setting.Enhancing Visual Basic . 4. Following is a new frmImage implementation that features most of these ideas. 2. This is in case its Form_Load event initially fires upon Show(). You could define a positioning function where you give it the coordinates on its owner form and it will compute screen coordinates for you. All this keeps the message queue busy. If you use multiple images.Image. I typically Show it.ToArgb And &HFF000000) <> 255 Then 'if color contains transparency. 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). and painting will not start until the queue is idle. End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad of the above) ' Purpose : Instantiate and set owner Page –83– . Please note that if you plan to show the image. You could set the form up to sets its Opacity property to zero prior to a Show. Example: Label1_13 for ID=13 '******************************************************************************* Private Sub frmImage_Load(ByVal sender As Object. would have actually eliminated the need to use a PictureBox on the form and allowed us to use the form’s BackgroundImage instead! . because the move functions (both local and absolute) will also issue a form Refresh: '**************************************************************************************** ' frmImage .. 7.None 'make sure we are borderless With Me.BackColor 'else grab picturebox background color. Bitmap).Select() (faster than Me. 6. actually set the new image after Show(). Do a Me.NET Beyond the Scope of Visual Basic 6.) End Sub '******************************************************************************* ' Subroutine Name : New ' Purpose : Default object instantiator '******************************************************************************* Public Sub New() MyBase.BorderStyle = BorderStyle.Width 'hide form off-screen.Load With Me .Height = . which will be showing through End If 'because a form's backcolor property will choke on a transparency color an icon might have.FromArgb(&HFF. in case its Form_Load event fired. just let it to load an icon or bitmap with equal ease. Me. TransparencytSampleX. _ Optional ByVal TransparencytSampleY As Integer = 0) MyBase.NewImage(NewImg. Set transparency '******************************************************************************* Public Sub New(ByVal Owner As Form. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad) ' Purpose : set owner. ByVal NewImg As Image.New() InitializeComponent() ' This call is required by the Windows Form Designer. Me.Owner = Owner Me. _ ByVal TransparencyColor As Color) MyBase. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad) ' Purpose : set owner. Me.New() InitializeComponent() ' This call is required by the Windows Form Designer. set transparency '******************************************************************************* Public Sub New(ByVal Owner As Form. ByVal NewImg As String. Set transparency '******************************************************************************* Public Sub New(ByVal Owner As Form. TransparencytSampleX. load Bitmap. _ Optional ByVal UseTransparency As Boolean = False. UseTransparency. _ ByVal TransparencyColor As Color) MyBase. UseTransparency. _ Optional ByVal TransparencytSampleY As Integer = 0) MyBase. _ Optional ByVal UseTransparency As Boolean = False.NewImage(NewImg. UseTransparency. Set transparency '******************************************************************************* Public Sub New(ByVal Owner As Form.Owner = Owner Me. Me.New() InitializeComponent() ' This call is required by the Windows Form Designer.Owner = Owner Me. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad) ' Purpose : set owner.NewImage(NewImg. UseTransparency. set transparency '******************************************************************************* Public Sub New(ByVal Owner As Form. _ Optional ByVal TransparencytSampleX As Integer = 0. _ Optional ByVal TransparencytSampleX As Integer = 0.Owner = Owner Me. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad) ' Purpose : set owner.Enhancing Visual Basic . load Bitmap.NewImage(NewImg. _ Optional ByVal TransparencytSampleY As Integer = 0) MyBase.NewImage(NewImg. ByVal NewImg As Bitmap. ByVal NewImg As String.New() InitializeComponent() ' This call is required by the Windows Form Designer. load Image. _ Optional ByVal UseTransparency As Boolean = False. ByVal NewImg As Bitmap. UseTransparency. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad) ' Purpose : set owner. TransparencytSampleX. _ ByVal TransparencyColor As Color) MyBase. ByVal NewImg As Icon. _ ByVal UseTransparency As Boolean.Owner = Owner Me. ByVal NewImg As Image. ByVal NewImg As Icon.Owner = Owner End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad) ' Purpose : set owner. TransparencytSampleX. _ Optional ByVal TransparencytSampleX As Integer = 0.NET Beyond the Scope of Visual Basic 6. Set transparency '******************************************************************************* Public Sub New(ByVal Owner As Form. load Image.NewImage(NewImg. _ ByVal UseTransparency As Boolean.New() InitializeComponent() ' This call is required by the Windows Form Designer.New() InitializeComponent() ' This call is required by the Windows Form Designer. UseTransparency.Owner = Owner Me. UseTransparency. load file image. load Icon. _ Optional ByVal UseTransparency As Boolean = False. _ Optional ByVal TransparencytSampleY As Integer = 0) MyBase. load file image. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad) ' Purpose : set owner. Me.NewImage(NewImg.New() InitializeComponent() ' This call is required by the Windows Form Designer. Me. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : NewImage Page –84– . _ ByVal UseTransparency As Boolean. load Icon. UseTransparency. Me. _ ByVal UseTransparency As Boolean. _ ByVal TransparencyColor As Color) MyBase. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : New (OverLoad) ' Purpose : set owner. Set transparency '******************************************************************************* Public Sub New(ByVal Owner As Form.0 – David Ross Goben '******************************************************************************* Public Sub New(ByVal Owner As Form) MyBase.Owner = Owner Me.Owner = Owner Me. Me. _ Optional ByVal TransparencytSampleX As Integer = 0.New() InitializeComponent() ' This call is required by the Windows Form Designer. Set transparency '******************************************************************************* Public Sub New(ByVal Owner As Form.NewImage(NewImg. Me.New() InitializeComponent() ' This call is required by the Windows Form Designer. Height Catch Exit Sub End Try End Try UpdateImage(X.Image = DirectCast(NewImg.Width Y = Img. _ Optional ByVal TransparencytSampleX As Integer = 0.Img. _ Optional ByVal UseTransparency As Boolean = False. set this before moving (moving does refresh) '******************************************************************************* Public Overloads Sub NewImage(ByRef NewImg As Image. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : NewImage (OverLoad) ' Purpose : Set new image from File. _ ByVal UseTransparency As Boolean.Img. NewImg.Image = New Bitmap(NewImg) 'load the image into the picturebox UpdateImage(NewImg. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : NewImage (OverLoad) ' Purpose : Set new image from Bitmap.Img. _ ByVal TransparencyColor As Color) If NewImg Is Nothing Then Exit Sub Me.Width.Width Y = Img.FromFile(NewImg) 'try to load as a bitmap X = Img.0 – David Ross Goben ' Purpose : Set new image.Image = New Icon(NewImg).Height Catch Exit Sub End Try End Try UpdateImage(X.Image = DirectCast(NewImg. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : NewImage (OverLoad) ' Purpose : Set new image from Bitmap.Height. optionally set transparency key ' : If also moving control.Image = New Bitmap(NewImg. _ Optional ByVal TransparencytSampleY As Integer = 0) If NewImg = vbNullString Then Exit Sub Dim X As Integer = 32 Dim Y As Integer = 32 Try Me.Width. _ Optional ByVal UseTransparency As Boolean = False.Clone. _ Optional ByVal TransparencytSampleY As Integer = 0) If NewImg Is Nothing Then Exit Sub Me. _ Optional ByVal UseTransparency As Boolean = False.Img. UseTransparency. optionally set transparency key ' : If also moving control. optionally set transparency key ' : If also moving control. _ ByVal UseTransparency As Boolean. Y. Image) 'load the image into the picturebox UpdateImage(NewImg.Height.Image = Image.Img.Clone. UseTransparency.FromFile(NewImg) 'try to load as a bitmap X = Img. UseTransparency. NewImg.Image = Image. UseTransparency. set this before moving (moving does refresh) '******************************************************************************* Public Overloads Sub NewImage(ByVal NewImg As String. _ ByVal TransparencyColor As Color) If NewImg Is Nothing Then Exit Sub Me.Width.Width Y = Img. TransparencytSampleX. optionally set transparency key ' : If also moving control. _ ByVal UseTransparency As Boolean. optionally set transparency key ' : If also moving control. _ ByVal TransparencyColor As Color) If NewImg = vbNullString Then Exit Sub Dim X As Integer = 32 Dim Y As Integer = 32 Try Me.Img. _ Optional ByVal TransparencytSampleX As Integer = 0. NewImg. Image) 'load the image into the picturebox UpdateImage(NewImg. set this before moving (moving does refresh) '******************************************************************************* Public Overloads Sub NewImage(ByRef NewImg As Image.ToBitmap) 'load icon to picturebox Page –85– .Image = New Icon(NewImg). set this before moving (moving does refresh) '******************************************************************************* Public Overloads Sub NewImage(ByRef NewImg As Bitmap. _ ByVal UseTransparency As Boolean. UseTransparency. _ ByVal TransparencyColor As Color) If NewImg Is Nothing Then Exit Sub Me.NET Beyond the Scope of Visual Basic 6. optionally set transparency key ' : If also moving control. NewImg.ToBitmap 'try to load as an icon X = Img. set this before moving (moving does refresh) '******************************************************************************* Public Overloads Sub NewImage(ByRef NewImg As Icon. UseTransparency.Img. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : NewImage (OverLoad) ' Purpose : Set new image from Icon.Height.ToBitmap 'try to load as an icon X = Img. _ Optional ByVal TransparencytSampleX As Integer = 0. TransparencytSampleX. optionally set transparency key ' : If also moving control.Height.Img.Img. Y. TransparencytSampleX.Enhancing Visual Basic . _ Optional ByVal TransparencytSampleY As Integer = 0) If NewImg Is Nothing Then Exit Sub Me. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : NewImage (OverLoad) ' Purpose : Set new image. set this before moving (moving does refresh) '******************************************************************************* Public Overloads Sub NewImage(ByVal NewImg As String.Image = New Bitmap(NewImg) 'load the image into the picturebox UpdateImage(NewImg. set this before moving (moving does refresh) '******************************************************************************* Public Overloads Sub NewImage(ByRef NewImg As Bitmap.Width. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : NewImage (OverLoad) ' Purpose : Set new image from File.Width Y = Img.Height Catch Try Me.Height Catch Try Me. Height = NewHeight Me.Height = NewHeight CheckForTransparency(UseTransparency.Image = New Bitmap(NewImg.Image IsNot Nothing AndAlso UseTransparency = True Then NewTransparencyKey(TransparencytSampleX.Height.Width. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : UpdateImage ' Purpose : Set a new image to the form.Img.PointToScreen(New Point(X. Optional ByVal TransparencytSampleY As Integer = 0) Dim Clr As Color If Me. _ ByVal TransparencyColor As Color) If Me. _ ByVal TransparencyColor As Color) Me.GetPixel(TransparencytSampleX.Width = NewWidth Me. TransparencytSampleX. 0) 'use default transparency key End If Try 'this might fail if the image has a transparency color Me. get it '******************************************************************************* Private Overloads Sub CheckForTransparency(ByVal UseTransparency As Boolean.Enhancing Visual Basic .Img. NewImg. UseTransparency. with refresh '******************************************************************************* Public Overloads Sub MoveToLocal(ByVal X As Integer. 0. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : UpdateImage (OverLoad) ' Purpose : Set a new image to the form.Img.Height = NewHeight CheckForTransparency(UseTransparency. optionally update transparency '******************************************************************************* Private Overloads Sub UpdateImage(ByVal NewWidth As Integer. _ ByVal UseTransparency As Boolean.Width = NewWidth Me. ByVal NewHeight As Integer.Width = NewWidth Me.BackColor = NewTransparencyKeyColor 'set form background color to a border color sample Catch End Try Me.Img.Width.X.Image IsNot Nothing AndAlso UseTransparency = True Then NewTransparencyKey(TransparencyColor) End If End Sub '******************************************************************************* ' Subroutine Name : CheckForTransparency (OverLoad) ' Purpose : If transparency option selected.Img.TransparencyKey = Clr 'set transparency to the same End Sub '******************************************************************************* ' Subroutine Name : MoveToLocal ' Purpose : Set new location for form using local coordinates.Img. _ Optional ByVal TransparencytSampleY As Integer = 0) Me.Image IsNot Nothing Then Dim bmp As Bitmap = DirectCast(Me.Img. _ Optional ByVal TransparencytSampleY As Integer = 0) If NewImg Is Nothing Then Exit Sub Me. optionally set transparency key ' : If also moving control. optionally update transparency '******************************************************************************* Private Overloads Sub UpdateImage(ByVal NewWidth As Integer. ByVal NewHeight As Integer. _ Optional ByVal UseTransparency As Boolean = False. ByVal Y As Integer) Dim pt As Point = Me. TransparencytSampleY) End Sub '******************************************************************************* ' Subroutine Name : CheckForTransparency ' Purpose : If transparency option selected. UseTransparency. NewTransparencyKeyPoint. get it '******************************************************************************* Private Overloads Sub CheckForTransparency(ByVal UseTransparency As Boolean.BackColor = Clr 'set form background color to a border color sample Catch End Try Me. 0. Y)) 'convert local to screen coordinates Page –86– . NewImg.Image. Bitmap) 'set a bitmap reference to image Clr = bmp. TransparencyColor) End Sub '******************************************************************************* ' Subroutine Name : NewImage (OverLoad) ' Purpose : Set new image from Icon. _ Optional ByVal UseTransparency As Boolean = False.FromArgb(0.ToBitmap) 'load icon to picturebox UpdateImage(NewImg.0 – David Ross Goben UpdateImage(NewImg.Height = NewHeight Me. _ TransparencytSampleY) 'get sample color to use for transparency key Else Clr = Color.Img. TransparencytSampleY) End If End Sub '******************************************************************************* ' Subroutine Name : NewTransparencyKey ' Purpose : Set new TransparencyKey value '******************************************************************************* Public Overloads Sub NewTransparencyKey(ByVal NewTransparencyKeyColor As Color) Try 'this might fail if the image has a transparency color Me.Y) End Sub '******************************************************************************* ' Subroutine Name : NewTransparencyKey (OverLoad) ' Purpose : Set new TransparencyKey value '******************************************************************************* Public Overloads Sub NewTransparencyKey(Optional ByVal TransparencytSampleX As Integer = 0.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. _ Optional ByVal TransparencytSampleX As Integer = 0.Img.NET Beyond the Scope of Visual Basic 6.Height. _ ByVal TransparencytSampleY As Integer) If Me. TransparencytSampleX.Width = NewWidth Me. _ ByVal TransparencytSampleX As Integer. set this before moving (moving does refresh) '******************************************************************************* Public Overloads Sub NewImage(ByRef NewImg As Icon.Owner. _ Optional ByVal TransparencytSampleX As Integer = 0. If it does.0 – David Ross Goben Me. once things are working.NET Beyond the Scope of Visual Basic 6.Owner IsNot Nothing Then Me.Owner.Owner IsNot Nothing Then Me. and moves can be in either local or absolute (screen) coordinates.X. with refresh '******************************************************************************* Public Overloads Sub MoveToAbsolute(ByVal X As Integer.Select() End Sub 'locate form 'refresh form 'ensure focus on the owner '******************************************************************************* ' Subroutine Name : MoveToLocal (OverLoad) ' Purpose : Set new location for form using local coordonates.Close() Me.X. ' as long as it does not profit you in return. as you can see. whether Image. ' Just keep this tag at the bottom.Show() End Sub '******************************************************************************* ' Subroutine Name : Hide ' Purpose : replace form Hide function '******************************************************************************* Public Overloads Sub Hide() MyBase.Show() If Me. I have tested the above form. Notice especially that I have addressed the issues regarding performing a select of the owner form after a Show or Hide.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.Location = pt Me. in writing your own controls you will want to start simple.Select() 'ensure focus on the owner End If End Sub '******************************************************************************* ' Subroutine Name : MoveToAbsolute (OverLoad) ' Purpose : Set new location for form to absolute Screen location. Page –87– . This also gives you some idea how various options are applied. or File. ' You are free to use and modify this in any way without compensating me.Close had invoked Dispose) End Sub '****************************************************************************** ' Copyright © 2010-2014 David Ross Goben. and then. to avoid once-in-a-blue-moon flash If Me.Y) End Sub '******************************************************************************* ' Subroutine Name : Show ' Purpose : replace form Show function '******************************************************************************* Public Overloads Sub Show() MyBase.Y) End Sub '******************************************************************************* ' Subroutine Name : MoveToAbsolute ' Purpose : Set new location for form to absolute Screen location. to include your own update notes.Hide() Me. Note finally that Hide moves the form off-screen to avoid minor flickering when things get really busy.Owner = Owner Me.Refresh() 'refresh form If Me. with refresh '******************************************************************************* Public Overloads Sub MoveToLocal(ByRef pt As Point) MoveToLocal(pt. as I had demonstrated earlier. It is now a standard part of my 2D game software bag of tricks. pt.Left = X 'locate form Me. even with differentsized icons.Top = Y Me.Dispose() 'then dispose of resources (this will work even if MyBase. ByVal Y As Integer) Me. and the point you want to sample. with refresh '******************************************************************************* Public Overloads Sub MoveToAbsolute(ByVal pt As Point) MoveToAbsolute(pt.Owner. such as loading images.. quite an abundance of instantiation and update options.Left = -Me. a form Refresh and Owner form Select is performed after a move.Width 'hide form off-screen. Obviously.Owner.Enhancing Visual Basic . then we will talk. to ensure the owner is not covered by another app.Owner. pt.. All rights reserved.Owner IsNot Nothing Then Me.Select() 'ensure focus on the owner End If End Sub '******************************************************************************* ' Subroutine Name : Show (OverLoad) ' Purpose : replace form Show function '******************************************************************************* Public Overloads Sub Show(ByVal Owner As Form) Me. and.Refresh() Me. to begin adding and testing enhanced variations (Bells and Whistles) as desired. Also. and so far it has worked perfectly. Bitmap. '****************************************************************************** End Class This code represents almost a complete user control. Icon. if you want transparency. add the following code to your form: Public Class Form1 '******************************************************************************* 'Method Name: Form1_Load 'Purpose : Initialize project '******************************************************************************* Private Sub Form1_Load(ByVal sender As Object.MakeTransparent(Bmp. AddressOf Ctl_Paint End If Next Me. Actually. to the form and set its Interval parameter to 10 (default). unique color that we will in turn render transparent (an icon stored in a PictureBox Image property retains its transparency. and reflect at the form’s bounders. it is presently not ideal. the more interesting the object movement will end up being. Next. Indeed. add a timer control.Load With Me. size and position them however you want to. it detracts from its visual intent. 0)) 'render a transparent border on the image if not transparent already End With For Each Ctl As Control In Controls 'add the Ctl_Paint event handler to each control on the form If Ctl.GetPixel(0.Image). Just quickly “eyeball” it. bouncing around it. However. For this example I will simply draw an image that will travel at an angle across the form and any controls that it will encounter. I think it bears repeating here. even though it works quite impressively in such situations as a game. though displaying the PictureBox itself will cause its background color to bleed through the icon’s transparency regions)..Image = New Bitmap(. before we attacked the borderless form.Animation . But speaking of ideal… Emulating What VB6 Image Controls Do Using a PictureBox Now that we have constructed a control class for a borderless window. and especially in a form that features complete code that you can copy and paste to your code to demonstrate its functionality.NET Beyond the Scope of Visual Basic 6. Timer1..Visible Then '(except for the invisible Animation image control) AddHandler Ctl. though you are free to size it however you want to.Paint. ByVal e As EventArgs) Handles MyBase. I sized it to about one quarter my screen size. As such. The next step is to add a final PictureBox. You will probably need to set each PictureBox’s SizeMode parameter to StretchImage or AutoSize to fill each PictureBox with its assigned image. most of this code has already been shown earlier in this article. named Animation.Enabled = True 'Enable the animation timer End Sub '******************************************************************************* 'Method Name: Form1_Paint 'Purpose : Process painting of current form '******************************************************************************* Page –88– . it can run into trouble if the form beneath it is moved by the user while the image control is still moving. Add as many images as you want to.Enhancing Visual Basic .Clear(. Just try to keep the client area relatively square.0 – David Ross Goben The biggest problem with this class is that. .Timer1. Though it is no disaster.Visible = False 'turn off object to be painted If .Width. but being perfectly square is not important or desired.BackColor) 'clear image using the background color of PictureBox1 End If Dim Bmp As Bitmap = DirectCast(.Image. The first step is to create a new project that I named TestImageControl1. and load them with pictures or icons you have stored on your system. let us take another go at this by using the techniques employed by VB6 Image controls. The second step is to add PictureBoxes. or else a solid.Height) 'create a blank image (of background color) as a background to draw onto Graphics. Bitmap) 'get a reference to the animation PictureBox image Bmp. Finally. and fill it with a bitmap image or icon with an image border that is either transparent. the less square it is.FromImage(.Image Is Nothing Then 'if an image is not loaded (oops!). . Page –89– . The small bitmap image should be seen bouncing around the form. ByVal e As EventArgs) Handles Timer1. then change its direction If . lower for slower Private Sub Timer1_Tick(ByVal sender As Object. Red.Paint e. as before.Left <= 0 Then XYoffset. crossing all the other images displayed upon it.ico. the more varied the bouncing. ByVal e As PaintEventArgs) Dim Ctl As Control = DirectCast(sender. then draw the image onto the control If Ctl.ClientRectangle.Graphics.Left + .Invalidate(.Resources. I sized it to about one quarter my screen size.Animation.Add(.Width = -XYoffset. Timer1.Location = Point. Control) 'if the control intersects with the animation image.Top <= 0 Then XYoffset.Top) End If End Sub '******************************************************************************* 'Method Name: Timer1_Tick 'Purpose : Move the Animation image across the screen '******************************************************************************* Private XYoffset As New Size(4.0 – David Ross Goben Private Sub Form1_Paint(ByVal sender As Object.Bounds) 'allow old location to be refreshed to original data . again.Animation. XYoffset) 'apply movement offsets (higher values for faster.Red”.Ctl.Location.Tick With Me.Image. NOTE: If you do not want to go through all this work again. Me.Animation. we will simply add an icon to our project’s resources. or even an ImageList. 4) 'increase values for faster. then simply use the previous project.Ctl.Left. Emulating What VB6 Image Controls Do Using a Resource Icon As demonstrated earlier in this article. Me.Height = -XYoffset.Left.Width >= Me. This requires only minor changes to our program. you can now use an ordinary icon with a transparent border.Refresh() End If Next End Sub '******************************************************************************* 'Method Name: Ctl_Paint 'Purpose : Paint the animation image to each intersected control '******************************************************************************* Private Sub Ctl_Paint(ByVal sender As Object. The next step is different.Visible AndAlso Ctl.IntersectsWith(Me.Bounds) 'tell form Paint event to also update the new target region 'if the animation may image slide out of sight.Animation.Invalidate(.Enhancing Visual Basic . but unlike the previous examples.Visible AndAlso Ctl.ClientRectangle) Then e. This will cause ' the animation image to be drawn on it through the control's paint event. Instead of adding a final PictureBox named Animation.Width End If If .ClientRectangle. I will again use my red marble icon.IntersectsWith(Me.Graphics. which I called TestImageControl2.Height End If End With End Sub End Class Now run it to test it.ClientRectangle) Then 'Refresh the control if it intersects with the animation image.Image. expressed in the following code as “My.Animation. but you should of course change this to use your own preferred icon. you are free to size it however you wish to. Next. and load them with images or icons you have stored on your system.ClientRectangle. Me. Add any icon of your choosing. remove the Animation PictureBox.Width OrElse .DrawImage(Me. the second step is to add PictureBoxes. to use in place of a VB6-type “Image control”. Add as many as you want. to the form with its Interval parameter to 10. The less square the form is. As before.Height OrElse . Me.Left . You will again probably need to set each PictureBox’s SizeMode parameter to StretchImage or AutoSize to fill each PictureBox with its assigned image. though.DrawImage(Me. Just try to keep the client area relatively square (but not quite – for better bouncing effects).Animation Me.NET Beyond the Scope of Visual Basic 6. lower for slower) Me.Top .Top + . Ctl.Top) 'draw image on form background 'scan for each control that interects with the animation control For Each Ctl As Control In Controls If Ctl. and add an icon or image to your project resources.Height >= Me. ByVal e As PaintEventArgs) Handles Me. as we did previously. add a timer control.ClientRectangle. it was recommended that you use an icon or bitmap stored in your resources.Animation. size and position them however you want. The first step is to simply create a new project.Animation. Again.Animation. Visible AndAlso . ImgWH) 'tell form Paint event to also update the new location Me. animXY.IntersectsWith(ImgRct) Then AddHandler Ctl.Load ImgWH = Img..Add(animXY.Size 'gather dimensions of image to paint ImgRct = New Rectangle(animXY.Paint.IntersectsWith(ImgRct) Then Ctl. lower for slower '******************************************************************************* 'Method Name: Form1_Load 'Purpose : Initialize project '******************************************************************************* Private Sub Form1_Load(ByVal sender As Object. which is the controls bounding rectangle.Resources. ByVal e As EventArgs) Handles MyBase.Invalidate(ImgRct) 'if the animation image slides out of sight. intersects with the target.Bounds.Width = -XYoffset.Y) 'scan for each control that interects with the animation control For Each Ctl As Control In Controls With Ctl 'If the control is visisble and its Bounds..Height End If End Sub End Class Page –90– . AddressOf Ctl_Paint . 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.X.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Finally.X <= 0 Then XYoffset.X + ImgWH.Height OrElse animXY..Left. This will cause 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..Bounds.Visible AndAlso .Tick 'allow old location to be refreshed to original data Me.IntersectsWith(ImgRct) Then 'Bounds is the control's bounding rectangle e.DrawIcon(Img.Top) 'draw the image offset from the control End If RemoveHandler DirectCast(sender. XYoffset) 'update Img positioning ImgRct = New Rectangle(animXY.Y + ImgWH.Width OrElse animXY. lower for slower) animXY = Point.Paint. 0) 'start X/Y coordinate for Img 'Animation movement rate Protected XYoffset As New Size(4. Control).Timer1.Graphics.Height >= Me.X . ByVal e As PaintEventArgs) 'if the control intersects with the animation image. ByVal e As EventArgs) Handles Timer1.Paint 'draw the image on the form background e. add the following code to your form (or replace the existing code with it): Public Class Form1 Protected Img As Icon = My.Bounds.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.ClientRectangle.Paint.Width >= Me. then draw the image onto the control With DirectCast(sender.Y <= 0 Then XYoffset. animXY. animXY..Invalidate() End If End With Next 'apply movement offsets (higher values for faster.Y ..Graphics.DrawIcon(Img.ClientRectangle.Visible AndAlso .Enhancing Visual Basic . 4) 'increase value for faster. If . If .Height = -XYoffset. animXY.Width End If If animXY.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. Control) If . ByVal e As PaintEventArgs) Handles Me. AddressOf Ctl_Paint Next Me.Refresh() 'Refresh will invalidate the control if they intersect.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 visisble and its Bounds intersected with the target. then flip its direction If animXY. AddressOf Ctl_Paint End With End Sub '******************************************************************************* 'Method Name: Timer1_Tick 'Purpose : Move the Animation image across the screen '******************************************************************************* Private Sub Timer1_Tick(ByVal sender As Object. then this is almost no work at all.Top End With 'Set aside a bitmap interface for the image control 'Render its background transparent (0. where we displayed a label with a transparent background in front of an image control: With Me.0 – David Ross Goben Super-Fast No-Holds-Barred VB6 Image Emulation If all you want to do is to have a borderless PictureBox with a transparent background resting on a form’s background. An alternative to this is to check for a click on the faux Image area through the Click events for the form and for its other controls. then you must render the PictureBox background transparent by also setting its BackColor property to Transparent: Me.Top -= . However. we must ALSO render the PictureBox’s background transparent. AddressOf Ctl_MouseMove 'Support the mouse moving over controls that are covered by a moving image AddHandler Ctl.BackColor = .Parent. We can simply combine the above method with the technique we used in the previous article. just add code like these two lines: 'render the background of an image control transparent Dim Bmp As Bitmap = DirectCast(Me.0 is a known transparent location) Because the PictureBox assumes the background color of its parent. If one is found. For example.Parent. even if it is on another control. if you have a background image for the form. the form. Bitmap) 'set aside a bitmap interface for the image control Bmp.Transparent . The only thing really missing is your ability to click on them and react to those click events.PictureBox1 Dim Bmp As Bitmap = DirectCast(.BackColor .Image.Left -= .Location 'keep track of mouse movement End Sub '******************************************************************************* Page –91– .Enhancing Visual Basic .BackColor = Color. aside from rendering the actual image’s background transparent. you can probably be fairly safe in assuming that these images will be displayed in front of all other controls. AddressOf Ctl_Click 'Support click the mouse on a control that is covered by a moving image Next You could then add the following methods: '******************************************************************************* 'Method Name: Form1_MouseMove 'Purpose : keep track of mouse movement '******************************************************************************* Protected MseLT As Point 'local storage for mouse movement Private Sub Form1_MouseMove(ByVal sender As Object.NET Beyond the Scope of Visual Basic 6.Image1.Left .MakeTransparent(Bmp.PictureBox2 . but it is also within the bounds of our faux Image control. 0)) 'render its background transparent to the form (0. named PictureBox1.GetPixel(0. Bitmap) Bmp. but you will need to make PictureBox1 Image1’s parent and also render its background transparent. AddressOf Ctl_Paint 'Support Refreshes of controls AddHandler Ctl. the actual PictureBox background is not apparent.MakeTransparent(Bmp. that we can trigger event notifications for our Image control (respecting the layering of image controls adds another level of complexity to the code). and so a pick.Image1. ByVal e As MouseEventArgs) Handles Me.0 is a known image background location) 'Set image's parent control 'Note we must ALSO render the PictureBox background transparent (differs from the image) 'Also set the parent's picturebox BackColor to transparent 'Adjust offsets to remain in same relative location over new parent (old was Form) NOTE: As you can see.Paint.MouseMove MseLT = e. This would eliminate any need to add any nervous message queue hooking and unhooking. but that aspect of it usually requires hooks into the thread’s message queue. you can modify the Form Load event to add handlers for Mouse Move and Mouse Click events that will be used: For Each Ctl As Control In Controls AddHandler Ctl.MouseClick.MouseMove. on page 68.Transparent 'note we must ALSO render the PictureBox background transparent (this differs from the image) But what if you want to display the image in your PictureBox.BackColor = Color. 0)) . intercepting it and checking for clicks that will occur in the client rectangle area of our faux Image control. invoke a faux Image click event and exit the regular event without processing it. on top of another PictureBox’s image named PictureBox2? This is also easy to do.GetPixel(0.Parent = Me. Emulating Mouse Interaction on Drawn Images All the above code provides the entire display functionality of VB6 Image controls.Parent. Drawing Labels with Transparent Backgrounds over Images. If you were to do this.Image. otherwise the PictureBox background color will remain visible. In the form’s Load() event. For example. suppose you have a PictureBox that contains an image or icon named Image1. Local Y=" & (MseLT. but once we master it. rendering them on your form and controls is extremely fast. based upon the code I have provided.Y – Ctl. several such controls could slow the program down significantly.NET that I would not want to go back to VB6.Visible Then 'keep track of mouse movement on controls (compute form offsets) MseLT = Point. Page –92– . for the most part. But most importantly. that Microsoft will one day instate regular Image controls under VB. and sadly. to only throw a faux click event if a MouseUp event occured over the same control as a MouseDown event. Indeed. we tend to fall in love with it. many. many. Control) If Ctl. we used an Image control under VB6 to simply display stationary images on a form (where we should have used a PictureBox).Ctl.Ctl. I hope that you will see that by VB. ByVal e As MouseEventArgs) With DirectCast(sender. many VB6 programmers over-used Image controls because their transparency feature made them preferable to work with over faster PictureBoxes.ClientRectangle.ToString) End If End Sub Now you can detect clicks on images without needing message queue interception hooks. in the end.ClientRectangle. and an image can be rendered with just a single line of code from within a Form or control Paint event. Control) If .Contains(MseLT. ByVal e As EventArgs) Handles Me.Click If Me. though you must appreciate the fact that VB6 Image controls slowed program execution speed down.Print("Clicked on Image control" & Ctl. very difficult.Enhancing Visual Basic . I hope that users will not then begin to over-use them.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.NET Beyond the Scope of Visual Basic 6. Even so. All that you need to do to complete this code is write methods to support clicks on these images.NET allowing us to get into the guts of our project.Contains(MseLT) Then 'if the two indexes intersect upon the form Debug.NET. or.Left. thus slowing the over-all execution speed of their programs once again.Y . though maybe checking for MouseUp events. I hope.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. I am so impressed with what I can now do with images under VB. Personally. Even the code I had provided for the complete Image class.NET that many of us had under VB6 with Image controls. Conclusion I realize that some of you will see the work laid out here as a whole lot of unnecessary effort that one did not have to do in VB6. it has also given us much greater control over the operation of it. considering how easy the technology is to implement.ToString & ". The updates are clean and crisp and with no lags or flicker as would often happen using a VB6 Image control.Location.Top) Then 'if the two indexes intersect upon the control Debug. it is a small step away from the shape controls that now come standard as of VB2010. as I have demonstrated. .NET project resources or in an ImageList. would have also been rather short and to the quick. many variations for the overloaded methods. Beside. nor is creating an Image control or classes. New Size(.X – Ctl. Even so. if you had removed the many. If you store these images in your VB.Left). The initial learning process might make us grumble. to be more robust. MseLT.Name & _ ": Local X=" & (MseLT.X .Left. But I hope that most of you can also see that it is not at all difficult to add the functionality to VB.Add(e. ByVal e As EventArgs) Dim Ctl As Control = DirectCast(sender. instead would actually be more productive for an application.Top).0 – David Ross Goben 'Method Name: Ctl_MouseMove 'Purpose : keep track of mouse movement over controls '******************************************************************************* Private Sub Ctl_MouseMove(ByVal sender As Object. To demonstrate this point. but the process of supporting this functionality was a bit clunky. '******************************************************************************* '-----------------------------------' Local space to hold union value ' Make enough room for largest value '-----------------------------------Private mValue As String * 8 'can hold a Double at full length '******************************************************************************* ' Subroutine Name : Class_Initialize ' Purpose : Initialize Union Class '******************************************************************************* Private Sub Class_Initialize() Me. Long. especially if it was compared to VB.Enhancing Visual Basic . add: ' Set MyUnion = Nothing '---' You CAN shorten this in the module header to: ' Public MyUnion As New Union ' but I simply like to keep it cleaner and instantiate and uninstantiate classes. but these ' are slow. It was adequate for what we needed it for.123 ' MyUnion. Previously. ' ensure that the Instancing in the class properties is set to MultiUse. it did exactly what was expected of it. ' ' The methods offered by this class are: ' Clear: Reset the class data to null ' SetUnionDbl: Set Double Value to Union ' SetUnionSng: Set Single Value to Union ' SetUnionLng: Set Long Value to Union ' SetUnionInt: Set Integer Value to Union ' SetUnionByt: Set Byte Value to Union ' GetUnionDbl: Get Double Value from Union ' GetUnionSng: Get Single Value from Union ' GetUnionLng: Get Long Value from Union ' GetUnionInt: Get Integer Value from Union ' GetUnionByt: Get Byte Value from Union '******************************************************************************* ' To use the Union class.GetUnionInt() '---'NOTE: If you are including this Class module (Union.cls) as a part of your VB ' project. 0) End Sub Page –93– . ' you can speed up variant numeric processing.NET functionality.SetUnionInt 123 ' ' Examples extract values from it: ' Dim MyDbl As Double ' Dim MyInt As Integer ' MyDbl = MyUnion. Integer. or Byte using the rtlMoveMemory Win32 API to and from that string space medium.0 – David Ross Goben Implementing Unions in VB. following is the code for that VB6 class: Option Explicit '******************************************************************************* ' Union. the Instancing parameter in the Union class properties will be ' auto-set to Private. and it worked quite well for sharing common space between variables of different sizes with the same memory starting point. A Union allows different sized numeric ' values to occupy the same space. '---' Examples assign values to it: ' MyUnion. ' ' To Create a Union space: ' Public MyUnion As Union 'you can also make this Private (or Dim) ' 'to make it local to the containing module ' In your Form_Load event.dll).Clear() End Sub '******************************************************************************* ' Subroutine Name : Clear ' Purpose : Reset the class data to null '******************************************************************************* Public Sub Clear() mValue = String$(8. and keeping track of the variable type. This is especially handy when a numeric ' value will be assigned to a parameter.SetUnionDbl 23456789.Cls for VB6 ' This class implements unions in VB6.GetUnionDbl() ' MyInt = MyUnion.NET (inspired by an article on FieldOffsets by Paul Kimmel) A feature many VB6 users have always wanted was Unions. It moved data to and from cardinal value types. where variable address spaces overlap each other. I had once written a crude yet involved Union class for VB6 that served this task. add: ' Set MyUnion = New Union ' ' In your Form_Unload event. It employed an 8-byte fixed-length string as a generic storage medium.NET Beyond the Scope of Visual Basic 6. Using this union class. VB6 addressed this by using Variant variables. you must add a refrence in your project to vb6Union. Single. If you are using it to build a DLL (vb6Union. whether Double. but its type is not known ahead of ' time. mValue. Offset is zero-based. The module was originally written to mimic functions that were intrinsic parts of DOS BASIC.Enhancing Visual Basic . Here is that module: Option Explicit 'VB6 Copy binary numeric data to and from strings '**************************************************************************** ' modMkStrings: Copy numeric data to and from strings. 0) End Function '******************************************************************************* ' Subroutine Name : GetUnionSng ' Purpose : Get Single Value from Union '******************************************************************************* Public Function GetUnionSng() As Double GetUnionSng = StrMkSng(mValue. mValue. mValue. used all the way through QuickBasic and QBasic. modMkString. so position 1 in a string is position 0 ' in the offset. 0) End Sub '******************************************************************************* ' Subroutine Name : SetUnionSng ' Purpose : Set Single Value to Union '******************************************************************************* Public Sub SetUnionSng(ByVal Value As Single) SngMkStr(Value. 1) = Chr$(Value) End Sub '******************************************************************************* ' Subroutine Name : GetUnionDbl ' Purpose : Get Double Value from Union '******************************************************************************* Public Function GetUnionDbl() As Double GetUnionDbl = StrMkDbl(mValue. 1. All rights reserved. 0) End Sub '******************************************************************************* ' Subroutine Name : SetUnionInt ' Purpose : Set Integer Value to Union '******************************************************************************* Public Sub SetUnionInt(ByVal Value As Integer) IntMkStr(Value. 0) End Function '******************************************************************************* ' Subroutine Name : GetUnionByt ' Purpose : Get Byte Value from Union '******************************************************************************* Public Function GetUnionByt() As Double GetUnionByt = Asc(Mid$(mValue. which was to move values to and from strings. 0) End Sub '******************************************************************************* ' Subroutine Name : SetUnionByt ' Purpose : Set Byte Value to Union '******************************************************************************* Public Sub SetUnionByt(ByVal Value As Byte) Mid$(mValue. specifying an offset within ' a string where the data is located. 0) End Function '******************************************************************************* ' Subroutine Name : GetUnionLng ' Purpose : Get Long Value from Union '******************************************************************************* Public Function GetUnionLng() As Double GetUnionLng = StrMkLng(mValue. 0) End Function '******************************************************************************* ' Subroutine Name : GetUnionInt ' Purpose : Get Integer Value from Union '******************************************************************************* Public Function GetUnionInt() As Double GetUnionInt = StrMkInt(mValue.0 – David Ross Goben '******************************************************************************* ' Subroutine Name : SetUnionDbl ' Purpose : Set Double Value to Union '******************************************************************************* Public Sub SetUnionDbl(ByVal Value As Double) DblMkStr(Value. '****************************************************************************** The above condensed class used a single module that allowed it to do its magic. ' ' The following functions are provided to extract values from a string buffer: ' StrMkInt(): convert binary data from string to integer(returns 2 byte int) ' StrMkLng(): convert binary data from string to long (returns 4 byte lng) ' StrMkSng(): convert binary data from string to single (returns 4 byte sng) ' StrMkDbl(): convert binary data from string to double (returns 8 byte dbl) ' The following subroutines are provided to insert values into a string buffer: ' IntMkStr(): convert integer to binary string (write 2 byte int to string) ' LngMkStr(): convert long to binary string (write 4 byte lng to string) Page –94– . not ' 1-based as a VB string is. Useful for reading/writing blocks of data in files.NET Beyond the Scope of Visual Basic 6. 0) End Sub '******************************************************************************* ' Subroutine Name : SetUnionLng ' Purpose : Set Long Value to Union '******************************************************************************* Public Sub SetUnionLng(ByVal Value As Long) LngMkStr(Value. 1)) End Function '****************************************************************************** ' Copyright 1990-2011 David Ross Goben. 1. mValue. Posn + 1. Writes from Source to txt '************************************************* Public Sub SngMkStr(ByRef Source As Single. ByVal ptr. Posn + 1. Returns Int '************************************************* Public Function StrMkInt(ByVal Txt As String. easy process is not widely documented. Unlike VB6. 8)”. ByVal ptr. ByVal Txt As String. Writes from Source to txt '************************************************* Public Sub DblMkStr(ByRef Source As Double. Posn + 1. even though this simple. 4&) 'copy 4 bytes StrMkSng = dst 'return value to user End Function '************************************************* ' StrMkDbl(): convert binary data from string to double.NET Beyond the Scope of Visual Basic 6. such as changing “Dim ptr As String * 8” to “Dim ptr As New String(" "c. Returns Dbl '************************************************* Public Function StrMkDbl(ByVal Txt As String. Posn + 1.NET. 4) 'grab character to convert Call CopyMemory(dst. Source. 4) = ptr 'stuff new string to master End Sub '************************************************* ' DblMkStr(): convert double to binary string. ByVal Txt As String. Posn + 1. All rights reserved. which was all quite quick and easy. 4) 'grab characters to convert Call CopyMemory(dst. ByVal Posn As Long) Dim ptr As String * 2 'temp string to receive intial bytes Call CopyMemory(ByVal ptr. 4&) 'copy single to temp string Mid$(Txt.NET is very Union-friendly. Writes from Source to txt '************************************************* Public Sub LngMkStr(ByRef Source As Long. but all this effort would in the end have been to provide functionality that is already built right into VB. ByVal Posn As Long) Dim ptr As String * 8 'temp string to receive intial bytes Call CopyMemory(ByVal ptr. 2) 'grab character to convert Call CopyMemory(dst. Posn + 1. 2&) 'copy 2 bytes StrMkInt = dst 'return value to user End Function '************************************************* ' StrMkLng(): convert binary data from string to long. Returns Sng '************************************************* Public Function StrMkSng(ByVal Txt As String. 8) = ptr 'stuff new string to master End Sub '****************************************************************************** ' Copyright 1990-2011 David Ross Goben.NET. _ ByVal Source As Any. Source. 8&) 'copy 8 bytes StrMkDbl = dst 'return value to user End Function '************************************************* ' IntMkStr(): convert integer to binary string. 4) = ptr 'stuff new string to master End Sub '************************************************* ' SngMkStr(): convert single to binary string. Source. Returns Lng '************************************************* Public Function StrMkLng(ByVal Txt As String. ByVal Txt As String. ByVal ptr. 2) = ptr 'stuff new string to master End Sub '************************************************* ' LngMkStr(): convert long to binary string. ByVal Posn As Long) As Integer Dim ptr As String 'copy of source string for data to convert to integer Dim dst As Integer 'temp integer to receive 2 string bytes ptr = Mid$(Txt. and it has readily-accessible functionality to support them. ByVal Posn As Long) As Double Dim ptr As String 'copy of source string for data to convert to double Dim dst As Double 'temp double to receive 8 string bytes ptr = Mid$(Txt. _ ByVal Length As Long) '************************************************* ' StrMkInt(): convert binary data from string to integer. I would need to build various overloads to the CopyMemory P/Invoke to address the “As Any” issue and change the fixed-length strings to literals. VB. ByVal Txt As String. Writes from Source to txt '************************************************* Public Sub IntMkStr(ByRef Source As Integer. ByVal Posn As Long) As Single Dim ptr As String 'copy of source string for data to convert to single Dim dst As Single 'temp single to receive 4 string bytes ptr = Mid$(Txt. ByVal ptr. 8) 'grab character to convert Call CopyMemory(dst. ByVal Posn As Long) Dim ptr As String * 4 'temp string to receive intial bytes Call CopyMemory(ByVal ptr. 8&) 'copy double to temp string Mid$(Txt.Enhancing Visual Basic . 4&) 'copy long to temp string Mid$(Txt. ByVal Posn As Long) Dim ptr As String * 4 'temp string to receive intial bytes Call CopyMemory(ByVal ptr. Source. Posn + 1. Posn + 1. Page –95– . 4&) 'copy 4 bytes StrMkLng = dst 'return value to user End Function '************************************************* ' StrMkSng(): convert binary data from string to single.0 – David Ross Goben ' SngMkStr(): convert single to binary string (write 4 byte sng to string) ' DblMkStr(): convert double to binary string (write 8 byte dbl to string) '**************************************************************************** '***************************************************** ' API call used to copy memory to/from string/variable '***************************************************** Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal Destination As Any. 2&) 'copy integer to temp string Mid$(Txt. ByVal Posn As Long) As Long Dim ptr As String 'copy of source string for data to convert to long Dim dst As Long 'temp long to receive 4 string bytes ptr = Mid$(Txt. '****************************************************************************** Had I chosen to upgrade this class DLL to VB. This is very small.Enhancing Visual Basic . which seems obvious by the need to specify a byte offset value at all.NET Union is simply a modified version of a Class or Structure. you will need to employ the System. This is very large (1. though you cannot mix reference types (objects or arrays) with cardinal types (integer and char).NET takes just 5 very easy steps: 1) 2) 3) 4) 5) Add an “Imports System. as I mention periodically in this tome. and would likely indicate the user had supplied a floating point value.0 – David Ross Goben A VB. it would return the value “A”. For example: if the floating-point sMember was set to 2.0!). Precede each field member with “<FieldOffset(0)>”. If we assign a value of 65 to the Char member. The key to this whole functionality is the FieldOffset attribute preceding each class/structure member.Explicit)> Public Structure Integer_Field <FieldOffset(0)> Private IntValue As UInteger <FieldOffset(0)> Private ShortLeft As UShort <FieldOffset(2)> Private ShortRight As UShort <FieldOffset(0)> Private Byte0 As Byte <FieldOffset(1)> Private Byte1 As Byte <FieldOffset(2)> Private Byte2 As Byte <FieldOffset(3)> Private Byte3 As Byte End Structure 'Integer value (32-bits) 'Left (lower) 16 bits 'Right (upper) 16 bits 'First 8 bits of 32-bit field 'Second 8 bits of 32-bit field 'Third 8 bits of 32-bit field 'Fourth 8 bits of 32-bit field Page –96– . Precede the definition of the structure with the “<StructLayout(LayoutKind. You can instead apply differing offsets. it would return the value 65.824). the union would assign the integer the same value. Apply the required field members to the structure.073. which is exactly what we need to support Unions. because a union will always minimally be the size of its largest component.741. 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. we can use this simple structure to detect if the user had supplied an integer or floating-point value. the floating-point sMember would reflect a value of 2.NET at what byte offset within the class or structure the field is located.NET Beyond the Scope of Visual Basic 6. each initialized to a parameter of 0 to define a Union. This FieldOffset attribute tells VB.InteropServices” statement to the top of the source file. each field is stacked sequentially after the previous field on natural boundaries (a natural boundary is where each consecutive member begins on a memory bounds equal to its size. A common application for a Union in Dot NET is for interop. unless you add a Pack:=1 parameter to the StructLayout list so that they would be separated on single-byte boundaries). By informing VB that each field will begin at offset 0 within the class/structure.Runtime. To implement a union in VB. When accessed through its integer member. Alternatively. which is to implement code that had not been written in Dot NET. indicating the third byte of the 32-bit (4-byte) field: <StructLayout(LayoutKind. the Integer iMember would reflect a value of 1073741824.Explicit)> Public Structure isUnion <FieldOffset(0)> Public iMember As Integer '32-bit Integer value <FieldOffset(0)> Public sMember As Single '32-bit Floating-Point value End Structure NOTE: You are not limited to using just integers and singles. yet through attributes it permits all its fields to share the very same starting address. you are not confined to specifying simply a 0 offset for each field. Consider a union that contains a character and an integer field. much as you might notice that Graphics functions that expect floating-point values can auto-promote supplied integer values. It also contains many other features that you should most definitely explore on your own. such as isUnion. When accessed through the character member.Explicit)>” attribute.InteropServices namespace. Change one member and the other changes. and would likely indicate the user had supplied an integer value. such as an Integer and a Char in the above example. A union is commonly used to map more than one cardinal field to the same address. When you use Unions or interop code in general.802597E-45. cardinal fields (numeric fields) happen to agreeably overlap. Such a union structure would be 32 bits in size. In the above example. Define a named structure.Runtime. After all. If the integer iMember was set to 2. By default. Now consider the following example structure: <StructLayout(LayoutKind.0F (or 2. You can use other types too. This namespace contains the StructLayout attribute. for example. they will simply overlap each other. short PaperWidth. I have had need for that ability in VB. short PrintQuality. which provide you with tools that you can use to write your own CPU emulator. and of course this equally applies from the other directions. and tools such as the F# Interactive enable exploring data interactively. and then in the main structure.NET language suite was through a process called Co-Evolution. they have borrowed so many features from each other that they now are incredibly alike. In fact.microsoft.NET. also does not openly document this simple union structure. but using C++ syntax. sciences and machine learning. which is why C# looks so much like VB. This functionality also allows you to emulate CPU register structures. Structures that use Sub-Structures and Union Sub-Structures Some structures need to employ additional structures defined within them. declare a member field as that sub-structure.NET were actually developed separately. regardless of the many false rumors that VB.” F# comes installed as of Visual Studio 2010. from VB. NOTE: F#. in Microsoft’s own words. consider this imaginary Win32 structure: typedef struct _prnInfo { char PrinterName[32].NET managed version of Visual C++. parallel and data-rich areas. yet I noticed that C#. Implementing Unions under VB. short Scale. long PrinterID. short DefaultSource. which is a .com/en-us/um/cambridge/projects/fsharp/. //assign this inner structure to PRNINFO.DETAILS //assign structure to PRNINFO For us to use this structure in VB. analyzing. for example (I often used a Z80 CPU emulator to run older-than-dirt 8-bit TRS-80 Model 4 software on my PC). so when a feature is added to C#. visualizing and testing against live data sources… F# is a productive language for developers working in technical. } PRNINFO. NOTE: C# and VB. but which also happens to implement and fully support unions in the exact same manner that we have demonstrated here.NET is almost insanely simple. I wish that this also applied to C#’s ability to declare Unsafe blocks and use actual pointer processes within them. But by explicitly specifying them.NET was based upon C#. short PaperSize. it is also added to VB.NET is to first declare any sub-structure as a stand-alone structure. short PaperLength. The best way to handle them under VB.NET and F#. simply not displayed.NET (depending on who you are listening to). through its support for immutability and asynchronous programming. However. For example.NET and F#. and you can download it for free at: http://research. short Copies. games. we would need to declare the inner DETAILS structure separately from the PRNINFO structure. struct { short Orientation. data analytics. “enables users to write simple code to solve complex problems… F# has strong support for parallelism and concurrency. For example: Friend Structure DETAILS 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 Friend Structure PRNINFO <VBFixedString(32)> Friend PrinterName As String Friend PrinterID As Integer Friend PaperLength As Short Friend Dtls As DETAILS End Structure Page –97– .NET.NET Beyond the Scope of Visual Basic 6.NET many times. but it also works with Visual Studio 2008. in 2009 Microsoft decided that the best way to advance their .Enhancing Visual Basic . } DETAILS. we can manually declare where their starting offset should be assigned. algorithmic.0 – David Ross Goben NOTE: All field members of a class or structure actually have a FieldOffset value assigned to them internally. Normally they are defined in such a way to lay them one after the other on natural boundaries. or that C# was based upon VB. This has included applications in domains such as financial services. }.. you would have to reference them through the Dtls member of Pinfo.0 – David Ross Goben You would declare a variable of type PRNINFO using something like “Dim Pinfo As PRNINFO”. #if (WINVER >= 0x0500) || (_WIN32_WINNT >= 0x0400) DWORD dmPanningWidth. The Unicode version is WCHAR //DWORD = Integer or Int32 //This union will allow the following two structures to use the same space //this first structures declares 8 short integers. we can construct our DEVMODE structure properly. This will also automatically create space ad mapping the embedded DETAILS structure. DWORD dmPelsWidth. DWORD dmDisplayFixedOutput. DWORD dmNup.. short dmYResolution.Runtime. 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. TCHAR dmFormName[CCHFORMNAME]. }. DWORD dmFields. WORD dmDriverVersion. selects the size of the paper to print on <FieldOffset(4)> Friend dmPaperLength 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=. DWORD dmDitherType. selects the orientation of the paper <FieldOffset(2)> Friend dmPaperSize 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. union { struct { short dmOrientation.NET. and long pointer DEVMODE If we understand that WORD and short represent VB Short integers (Int16). *LPDEVMODEA. Be sure to also include “Imports System. DWORD dmICMIntent. short dmDuplex. DWORD dmDisplayFrequency.NET Point structure (two-Int32 values. short dmPaperSize.NET Beyond the Scope of Visual Basic 6.. DWORD dmBitsPerPel.. Following is listed the corrected DEVMODE structure for VB.Scale” to assign or obtain the Scale member of the embedded DETAILS structure. that TCHAR (same as BYTE) should be interpreted as an unmanaged ByVal Fixed-Length String. #if (WINVER >= 0x0400) DWORD dmICMMethod. *PDEVMODEA. vbFixedString works with CHAR. WORD dmSize. short dmCopies. DWORD dmPanningHeight.. //Declare a Structure of type _devicemode //TCHAR = Unmanaged Type ByValTStr.2. Now consider this more elaborate Win32 DEVMODE structure that is designed to support both Printer Devices and Display Devices. DWORD dmReserved1.Dtls. To access any of the members of the embedded DETAILS structure. // the following declarations are used only by a printer device.. DWORD dmReserved2.Explicit)> Friend Structure DEVMODE_union1 ' struct { <FieldOffset(0)> Friend dmOrientation As Int16 'For printer only.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. short dmTTOption. union { DWORD dmDisplayFlags. WORD dmSpecVersion. //presently must be set to 0 //presently must be set to 0 //if Window98/WinNT40 or greater. using 8 bytes) //this union allows dmDisplayFlags and dmNup to occupy the same space //if WinNT40 or greater. #endif #endif } DEVMODEA. DWORD dmDisplayOrientation. and that Union allows more than one set of variables to occupy the same space.Enhancing Visual Basic . For example: “Pinfo. and just like under C. DWORD dmMediaType. C++. POINTL is a VB Point structure.InteropServices” at the top of your class or module. //presently must be set to 0 (used by printers) //presently must be set to 0 (used by printers) //assign structure to DEVMODE. short dmPaperLength.1.) <FieldOffset(14)> Friend dmPrintQuality As Int16 'Specifies the printer resolution ' } ' struct { Page –98– . consuming 16 bytes //this second structure also consumes 16 bytes //POINTL is the same as a . which employs both embedded structures and Unions: typedef struct _devicemodeA { TCHAR dmDeviceName[CCHDEVICENAME]. DWORD represents VB Integers (Int32). WORD dmDriverExtra. but not with this //WORD = Short Integer or Int16 //NOTE: ANSI TCHAR is equivalent to BYTE. }. short dmColor.. short dmCollate. DWORD dmPelsHeight. short dmScale. }. WORD dmLogPixels. short dmDefaultSource. struct { POINTL dmPosition. pointer PDEVMODE. short dmPrintQuality. short dmPaperWidth. and C#. 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)>. but not both. course.NET Beyond the Scope of Visual Basic 6. in bits per pixel. for example. this version of it actually works. Enumerating. in bytes. in dots per inch. 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. or intent. which would clearly show that all the seemingly duplicate settings are not in fact duplicates at all. of the visible device surface Friend dmPelsHeight As Int32 'Specifies the height. of the display device in a particular mode 'All the following are for Image Color Management For printers Friend dmICMMethod As Int32 'For ICM applications. should be used by default Friend dmMediaType As Int32 'Specifies the type of media being printed on. 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 you might have seen spread around on the web.0 – David Ross Goben <FieldOffset(0)> Public dmPosition As Point <FieldOffset(8)> Friend dmDisplayOrientation As Int32 <FieldOffset(12)> Friend dmDisplayFixedOutput As Int32 ' } End Structure 'For display only. 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 '--------------------------------------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. For a practical example of using this structure to get a list of display modes available to your computer. in hertz (cycles per second). dmNup for a printer) <StructLayout(LayoutKind. and for both. Must be ByValTStr) <MarshalAs(UnmanagedType. such as standard. in pixels. SizeConst:=CCHFORMNAME)> Friend dmFormName As String Friend dmLogPixels As Int16 'The number of pixels per logical inch. glossy.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. fine.ByValTStr. of the display device Friend dmPelsWidth As Int32 'Specifies the width. please be sure to refer to Black Book Tip # 56: Getting. must be zero Friend dmReserved2 As Int32 'Not used. in pixels. the system examines this member to determine how to handle ICM support Friend dmICMIntent As Int32 'Specifies which color matching method. of the visible device surface '--------------------------------------Friend u2 As DEVMODE_union2 '--------------------------------------Friend dmDisplayFrequency As Int32 'Specifies the frequency. to include properties that most implementations of it miss. lineart. how it presents a low-res mode on a higher-res display 'create a 4-byte union of two overlapping int32 values (dmDisplayFlags for display. and Changing Screen Settings. the orientation at which images should be presented 'For fixed-resolution displays. 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. though all lacking unions and being configured for exclusive used by either printers or display devices.ByValTStr. such as none. "Letter" or "Legal" <MarshalAs(UnmanagedType. on page 591. Page –99– .Enhancing Visual Basic . or grayscale Friend dmReserved1 As Int32 'Not used. or transparency Friend dmDitherType As Int32 'Specifies how dithering is to be done. Printer drivers do not use this member Friend dmBitsPerPel As Int32 'Specifies the color resolution. NET Framework or from your own library. Page –100– . Modules.Enhancing Visual Basic . and Non-Inheritable Classes: Siblings by Different Names An example of local instantiation of a class is a VB Module file. all Public/Friend members of a noninheritable class. Thus. Even so. though they must declare it as a non-inheritable class with its Public/Friend members declared Static. If your project contains Imports statements for namespaces that contain members with the same name. it is probably best to go ahead and specify the parent module. these were used to place code for global application access. Note further that all classes that are intended to be imported are also declared NonInheritable. To emulate this under VB.NET. VB. you must fully qualify that member path when you use it. internally. C# users can use them just like VB modules. Finally. and all its non-private members are treated as Shared Public or Shared Friend. NOTE: You may be interested to know that VB.NET globally imports any module files present in the project so that we have effectively imported these classes. though it does not generate any additional compiler code. However. it will not cause your application to generate any additional code. This special property is assumed as a behind-the-scenes default for Module classes.NET Beyond the Scope of Visual Basic 6. it is actually a class (the body of the module is embraced by a Module class body). when a Module file is loaded. ByVal Y As Integer) As Integer Return X * Y End Function End Class You can then import and use this class right within the project like this: Imports MyApplication. which helps self-document the code. which is automatically app-globally imported. whether from the . reference this class using the Imports command. provided they are also unique to the project. in the headings of other class files that will use it. actual Class files. Namespaces. Under VB6. 63) 'invoke shared method End Sub End Class If you use the Imports statement as described above. You can easily emulate this by declaring Public/Friend methods within a standard class file as Public Shared. fully emulating VB usage. For example. you can use all of its exposed members declared within that namespace without qualification.NET module files are. But they can. for direct access without instantiation. consider this non-inheritable class declared within a project: Public NotInheritable Class Class1 Public Shared Function Multiply(ByVal X As Integer. and declare the class as NotInheritable. However. which explains why imported namespaces can be treated just as though they were modules included locally in any file where they are marked as imported. by importing it. Then.0 – David Ross Goben Understanding VB.NET Imports The Imports command. a local instance of the class is created and you can invoke any member of that class without having to also qualify it with its class name (though this can never hurt). allows you to import. even though you do this. NOTE: Some new C# developers have expressed jealousy that they cannot declare VB-style modules. Full-pathing or assumed-pathing (also called short-circuit pathing) will still generate the exact same compiled code with no additional overhead.Class1 'import local instance of this project’s application class Public Class Form1 Private Sub Test() Dim Result As Integer = Multiply(7. which is placed in the heading of a class file before the actual class declaration. declare Dimmed or Friend methods as Friend Shared. after you import a class. as you may have guessed. DLL" () As Integer Friend Const LockedWorkStation As String = "Workstation locked from unauthorized use. delegates (see the next topic for in in-depth look at Delegates). when you import several namespaces. These items must be declared as members of a namespace body. For example: Public Class Form1 Private Sub Test2() WinPinvoke. Also.Forms. and other namespaces. even if you never instantiate an instance of that class. variables and events at the namespace level.Information.InteropServices. or on one line. For example. You can even use aliases to more easily create object reference variables.NET Beyond the Scope of Visual Basic 6. classes. add a number of P/Invoke or even Constant declarations within it. procedures.LockWorkStation() 'lock the local workstation MsgBox(WinPinvoke.ListBox. interfaces. Consider the following class declaration: Friend Class WinPinvoke ' The LockWindowUpdate function disables or enables drawing in the specified window.ListBox The above statement creates a short-cut reference to System. This is because the code for a class is always resident in the program’s code space. enumerations. MsgBoxStyle.Forms. structures. separated by a dot. Only one window can be locked at a time. "Locked") End Sub End Class Page –101– . For example: Imports System. the following code example creates an alias called LB for a fully qualified path: Imports LB = System.Windows.Windows. you can do so on individual lines.LockedWorkStation.VisualBasic Versus: Imports System. this subject is often lumped into the mix.Runtime.0 – David Ross Goben Using Imports Aliases When namespaces have members whose names may conflict because they share the same name. which is required to use any data members of a class.VisualBasic Referencing Class Code without Importing or Instantiation Although not directly related to importing." End Class You can invoke the declarations within this class without importing it or instantiation it by simply specifying the class name and the desired member. such as the following: Dim LBreference As LB Within a namespace you can define items such as modules. However.OkOnly Or MsgBoxStyle. separating each consecutive namespace from the previous with a comma. you cannot define items such as properties. often called namespace collision or namespace pollution. and that is that you can create a class. Friend Declare Function LockWorkStation Lib "user32. and then access them without having to either import or instantiate the class.Runtime.InteropServices Imports VB = Microsoft. ' Unlock a locked form or control by passing a value of Zero to the function.DLL" Alias "LockWindowUpdate" ( _ ByVal HandleToLock As IntPtr) As Integer ' Locks the workstation's display. VB = Microsoft. one can employ aliases to create abbreviated names to use in the place of these long paths.Enhancing Visual Basic . Friend Declare Function LockWindowUpdate Lib "user32. Locking a workstation protects it from unauthorized use. This is very valuable since the method provided to VB6 after an AddressOf keyword was by name only. program crashes would likely occur. AddressOf CmdGo_Click”.NET’s most important features. By defining a unique prototype for a specific instance of use for a method.Click. I do not recommend it – it is a major waste of editing time): Private Sub cmdGo_Click() Handles cmdGo. with just a little understanding of them. They are also described as type-safe function pointers because they are similar to pointers to functions used in other programming languages. a similar method is expressed like this. However. In this particular case. Page –102– . to handle the event through the provided CmdGo_Click method.NET of the manner in which it should handle the method definition the Delegate represents. where parameter mapping can get confusing. In such classic cases. A Delegate can be used as a roadmap that lays out the parameter and return type characteristics of the method structure it is used to represent (often referred to as its Signature). in order for each to associate a particular event with the same handler method. Button)”. however. This longhand expression. The cmdGo. by the developer under VB. adding separate lines in the Form Load event could apply the AddHandler keyword to each control concerned with the target event. which informs the compiler that this method will be used to handle the Click() event for the control. and if parameters were misunderstood and not handled correctly. so you could have a test of something like this: “If sender Is Image1(3) Then…”. This is even more important when one also factors in overloaded methods. so this parameter can be ignored because we already know which control is associated with it. slipping by you completely unnoticed. due to it being automatically declared WithEvents.NET Beyond the Scope of Visual Basic 6. Were we to have multiple controls attached to this event to emulate what was done in a VB6 Control Array (something done manually.Enhancing Visual Basic .NET). Delegates are used to invoke the methods of other objects.NET: the Handles keyword. ByVal e As EventArgs) Handles cmdGo. such as the code used to support a button click. exposing the control/event linkage: Private Sub cmdGo_Click(ByVal sender As Object.Click This code makes clearer a major difference between VB6 and VB. then a list of control event handler delegates could follow the Handles token. because it is used only to support the cmdGo button. under VB. Delegates are also able to reference both shared methods (methods invoked without a specific class instance. we need to only know that a Click() event occurred. NOTE: Perhaps the simplest explanation of a Delegate is that it is can be used as a Method Prototype (If you understand prototypes as defined in C/C++. Delegates are quite benign and are usually constructed invisibly right under your nose. This is also where the sender object becomes very important. separated by commas. such as those implemented in C/C++ and VB6. referencing a PictureBox array named Image1() containing a list of PictureBoxes. Thus. like module methods) and instance methods. VB. Unlike function pointers.0 – David Ross Goben Understanding VB. because it will identify the actual control being processed. rendering the following (though it is permitted. has various event members internally defined that can be accessed via a Dot operator). along with its shorthand version when attached to the event handler header. In most cases.Click reference noted after it is shorthand for what would otherwise be the need for a separate coded line consisting of “AddHandler CmdGo. then this concept is clear). code confusion can be easily eliminated. The e parameter exposes additional features available to the sender control. for example. but easily. Or.NET function pointers require Delegates that are of a reference type. we could actually manually edit out (remove) the two parameters from the above declaration to clean up its appearance. a Delegate safeguards against such crashes by pre-informing . In that case you can cast sender to the object’s type. specifies that when a Click() event occurs on the CmdGo control (the CmdGo control. you might see some eventhandling code that looks like the following. you can perform virtual coding miracles. Delegates in Events In a typical VB6 event. yet they are thinly documented. similar to control arrays in VB6. such as “DirectCast(sender.NET.NET Delegates Delegates are one of VB. which hides the actual event/control linkage from you: Private Sub cmdGo_Click() However. if we would choose to. used best when multiple likecontrols are assigned to the event.Click The sender parameter can be used to reference the control being processed. ByVal attr As Integer. the method of the specified instance is invoked. the VB. but with .NET AddressOf operator extracts the address of the method declared after it.NET. ByVal msg As Integer. when the specified method is an instance method (a method declared in an instance of a class) then the method delegate refers to both the instance’s data and its method. Suppose our SubClassProc() method had been declared like this: Private Function SubClassProc(ByVal hwnd As Integer.DLL" Alias "SetWindowLongA" _ (ByVal hwnd As Integer. the AddressOf operator not only provides the method address to a P/Invoke. such as btnSend_Click. AddressOf SubClassProc) '◄———————————————┛ End If End Sub The VB. ByVal lParam As Integer) As Integer To create a Delegate fully compatible with the above method. Let us assume that we want to call this Delegate SubClassProcDelegate: Private Delegate Function SubClassProcDelegate(ByVal hwnd As Integer. This is because under VB. ByVal lVal As Long) As Long ' ▲ Sub SubClassWindow(ByVal hwnd As Long) ' ┗ ———————————————┓ If PrevProcPtr = 0 Then ' ▼ PrevProcPtr = SetWindowLong(hwnd. In this case. ByVal attr As Long.NET upgrade of it: ' VB6 Declare Function SetWindowLong Lib "USER32. and when passing classes or functions that are expected to match a pre-defined format.DLL" Alias "SetWindowLongA" _ (ByVal hwnd As Integer.DLL" Alias "SetWindowLongA" _ (ByVal hwnd As Long. _ ByVal wParam As Integer. Just like VB6. This can be as simple as copying the SubClassProc declaration line. Basically. In our example’s case. this is the last parameter of the SetWindowLong P/Invoke definition. though in most cases it is not (I have suggested to Microsoft how to auto-declare such delegates so the user is not burdened by the process). ByVal lParam As Integer) As Integer Once declared.0 – David Ross Goben Delegates and AddressOf The AddressOf operator can expose the existence of Delegates. for instance.NET AddressOf operator requires a Delegate reference for signature verification when it generates a memory address. adding the “Delegate” verb ahead of the “Function” (or “Sub”) verb. AddressOf SubClassProc) End If End Sub ' The above VB6 code after being upgraded to VB.Enhancing Visual Basic . it needs to know that the method address provided to lval in SetWindowsLong() is as is expected. NOTE: The reason a Delegate must be created is in case the signature is used by more than one method. except for Event methods. we simply emulate its P/Invoke Signature in our Delegate declaration. provided by AddressOf: Declare Function SetWindowLong Lib "USER32. then place it before any references to it or its target(s). _ ByVal wParam As Integer. when the delegate is referenced. such as comparator method for a sort. and the instance’s data is used. but VB. Consider the following VB6 code and the warning issued in a VB. as VB6 had done. For More: BLAH-BLAH-BLAH │ PrevProcPtr = SetWindowLong(hwnd. Also. GWL_WNDPROC.NET Beyond the Scope of Visual Basic 6.NET: Declare Function SetWindowLong Lib "USER32. Note that when using the AddressOf operator that you must also manually pre-declare a Delegate for the target method. we must also refer to the Delegate within the Signature where the AddressOf operator is to be employed (we do this so that the compiler will understand the actual provided method’s addressing). and providing a unique (different) name for this new Delegate method (I ‘cheat’ by simply appending “Delegate” to the originating method’s name). ByVal attr As Integer. ByVal lVal As SubClassProcDelegate) As Integer 'replaced: ByVal lVal As Integer Page –103– . ByVal msg As Integer. which is to actually employ the referenced subclassing method’s address. ByVal lVal As Integer) As Integer ' ▲ Sub SubClassWindow(ByVal hwnd As Integer) ' ┗ —————————————————————————————————————┓ If PrevProcPtr = 0 Then ' │ ' UPGRADE_WARNING: Add a delegate for AddressOf SubClassProc. in this case SubClassProc. GWL_WNDPROC. 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.NET it often requires an explicit Delegate (a Signature) be defined to verify that the provided method’s format matches the expected prototype.NET must also internally understand the target’s parameter list and optional return type in order to more robustly trap errors that had too often plagued countless VB6 users of AddressOf. To declare this needed Delegate is stupidly easy: we simply supply our Delegate with the parameter and type declarations associated with our target method. NET so that it can auto-generate delegates when AddressOf is used. So even though the structure member or function parameter may need to be declared as the Delegate type in the P/Invoke Signature prototype. exactly as VB6 had done. perhaps after timed intervals while it is processing data. AddressOf SubClassProc) End If End Sub 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.Enhancing Visual Basic . GWL_WNDPROC. 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 P/Invoke. if you compare the rather smooth upgrade process between VB6 and VB2008. but made safer by allowing only methods that match the prototype’s signature. Under VB6. the AddressOf still provides. their types. within our code. This is especially useful in situations where a P/Invoke will need to inform you of periodic progress updates. I can imagine a time will come when upgrading will require little attention at all by the developer. it is still physically just a single method address that is 32-bits wide. though it is possible to overcome even this possible issue through target method verification (clarification through prototype specification whenever naming collisions occur). Callbacks are hooks into prewritten code that allows the P/Invoke being summoned to actually in turn invoke (call back) one or more of your own custom-written methods. ' : ' : This workaround is needed because you cannot assign ' : AddressOf directly to a member of a user' : defined type. except in only the most extreme upgrade situations. detailing parameters. what they represent. web browsers can often take some time to load a busy web page and so may need to send back progress updates so that you can reflect that progress within a progress bar that you may wish to display on your user interface. Now. and what information you in turn need to provide and sometimes send back as a result when your method terminates or as the P/Invoke task progresses. Regardless.cbFn = FARPROC(AddressOf myFn)”: '****************************************************************************** ' Function Name : FARPROC (VB6) ' Purpose : A dummy procedure that receives and returns ' : the value of the AddressOf operator. I can only hope this slightly annoying requirement for additional developer-generated code can be addressed in a future edition of VB. but you can assign it to another ' : long and use that (as returned here) '****************************************************************************** Private Function FARPROC(ByRef pfn As Long) As Long FARPROC = pfn End Function Page –104– .0 – David Ross Goben Notice that the last parameter. at the compiled machine code level. such as “myStruct. or even when a background task has completed. though conflicts could crop up if that method name has overloads defined. The P/Invoke documentation for these special system methods provide you with a method prototype for exactly how each of your callback methods should be declared. 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 PrevProcPtr = SetWindowLong(hwnd. this upgrade process has become progressively easier and leaving fewer and fewer “ToDo” items and upgrade warnings for the developer to deal with after an upgrade.NET Beyond the Scope of Visual Basic 6. from VB2005 on. compared to the very brutal upgrade process to VB2002 or VB2003. Delegates and Callbacks Another powerful way to employ Delegates is to solve a problem that in VB6 prohibited it from being able to directly assign a method address to a user-defined type member. in the end. specified by the AddressOf operator. For example. In numerous Windows P/Invokes there is a requirement for a thing called a Callback Function. a 32-bit integer address of the procedure. and how with each succeeding generation. because the compiler does have knowledge of the target method. this callback was worked around in shared methods by adding a simple generic FARPROC function declared within a module so you could assign a method address to a structure member. which was previously expecting just a simple 32-bit Integer). Page –105– .NET because the AddressOf operator will also be looking for a Delegate (invocation prototype) to associate the addressed procedure with.NET Beyond the Scope of Visual Basic 6. the P/Invoke will still find just a 32-bit function address at the member location we declared as the Delegate type. I tend to just use the “CharSet:=CharSet. then declare an exposed field to be of the Delegate type and assign the address of the method to it. but instances of classes would also try to use shared methods in instance-specific space. A silly idea. the above generic VB6 solution does not work at all in VB. such as those declared within Modules or Imported Classes. we can simply invoke GenericSub() anywhere. unless you wrote numerous overloaded FARPROC methods that each specified the parameter type as one of the desired Delegates.0 – David Ross Goben But this did not solve a similar issue when one must supply an instance method address to a structure member within class instances. storing there the address of the actual target function. which sometimes required some mighty fancy workarounds within separate modules. We would assign it like this: GenericSub = AddressOf SelectedMethod 'assign a method that uses the GenericMethod Signature format to the referencer Thereafter. we use a Delegate at the declaration level to allow the structure to accept the needed 32bit method address without complaint. For our first example we will keep it mind-numbingly simple. within the code that has scoping access to it to in turn invoke the actual method to be executed. being methods without a specific instance of a class.Enhancing Visual Basic . Use “CharSet:=CharSet. to use the above function. because you might well argue that it would simply be easier to just invoke the actual method. we can assign its address to GenericSub. within the setup code. you can declare the delegate and all its required parameters. Be sure to also include “CharSet:=CharSet. and only one of them will be invoked variously throughout an application based upon a flag. However. our generic Delegate could be declared just like this: Delegate Sub GenericMethod() 'this method delegate can also include parameters. Not only do Delegates provide an elegant solution to this problem. Suppose we have a number of functions to choose from. and that the Structure is also marshaled with the “<StructLayout(LayoutKind. or a return type if a Function We must then declare an object reference variable for the GenericMethod prototype. you would simply declare a Delegate for the Callback procedure that you want to supply the structure member or P/Invoke parameter with. assuming that each of the alternative methods is a simple subroutine with no parameters. eliminating the need for a workaround function as that shown above for VB6.Ansi” if the structure specifically requires 8-bit strings.Auto” in the attribute list if string members are declared within the structure. and then you just declare the structure member or parameter type as that Delegate instead of as an Integer (in the end. But also remember that when passing Structures to P/Invokes that they should be passed ByRef.Unicode” if they specifically require Unicode strings. this may seem pointless. Delegates come to the rescue. but they also fully solve the problem VB6 had when such was required in class instances. Hence. As described previously. as needed. Invoking Methods through Delegates Another advantage of Delegates is that you can actually use a Delegate to invoke a method. once we determine which method is to be variously invoked.Sequential)>” attribute. However. At first glance. consider a scenario where there is a list of methods to choose from. 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.Auto” parameter because it will adjust for the requirements of the system automatically. However. The structure member or P/Invoke parameter that is to hold the required callback address is declared as a 32bit Integer. whose signature must be reflected in each of the alternative methods. which was illegal). or “CharSet:=CharSet. because when the structure is in turn passed to the summoned P/Invoke. which acts exactly like a Callback Hook that a pre-written method can use to invoke your custom method: Dim GenericSub As GenericMethod 'declare an instance of a referencer to objects with a signature of GenericMethod Finally. they will still supply the very same Integer data). The first thing we do is declare the Delegate. though many of these scenarios fell against coding dead-ends that could not be made to work at all (VB6 only allowed shared methods. and only one method will be used based upon a particular situation and it will be invoked from numerous locations within the code. Therefore. Conversely. if any. but its one comparison line must be changed to compare the actual types provided to it. Although a QuickSort is said to be extremely small and fast. but it does makes sense that they should make sense to the methods using it as a prototype. and like the results of CompareTo() and StrComp() comparisons. it must return an integer value of -1 if the first object is less than the second object. so report failure End If Page –106– . and return 1 if the first object is greater than the second object. and any return type. Doubles.NET Beyond the Scope of Visual Basic 6. we can cast them as needed to their proper types when we absolutely must have such knowledge in our custom method. For years. 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. we can provide the sorting method with a Boolean flag that can be used to indicate if we want to perform a Descending Sort or not. By defining a project-accessible Delegate prototype for the comparison. ByVal RightItem As Object) As Int32 'prototype for comparer Friend SortComparer As SortComparerDelegate 'assign the address of your comparison method to this hook before invoking the Sort() method '--------------------------------------------------------------------------------- Next. Adapting it to use Delegate prototypes. it can be used to sort anything. we need to write our generic Sort() method that will invoke the SortComparer callback hook: '********************************************************************************* ' Method : Sort ' Purpose : Sorting reference-type arrays Method (Strings. cannot normally use this technique. whether they are passed ByRef or ByVal. By declaring the interface to use Object types. Following that. You can name your parameters whatever you like. matches the types specified in the Delegate. We will declare our Delegate prototype to accept two parameters. To simplify it. All this really means is that the number of parameters. What is more. Suppose we want to use a Shell-Metzner Sort method to sort arrays. sorting so fast that you might think a QuickSort is actually the famously super-slow Shell Sort. we should declare a field that will be assigned the Delegate as a type. which we can place at the start of the module body that will also include our Sort() sorting method: '--------------------------------------------------------------------------------Friend Delegate Function SortComparerDelegate(ByVal LeftItem As Object. and also defining a project-accessible field defined as that Delegate type. I have used this ShellMetzner algorithm to sort strings and numbers.0 – David Ross Goben Using a Delegate to Perform Super-Fast Shell-Metzner Sorts On Any 1D Array But what about invoking methods that contain parameters or even return types? All you need to remember is that the methods you write using a Delegate must be identical to the used Delegate’s signature. ' This method invokes SortComparer. or numeric types. A ShellMetzner Sort is astoundingly faster that the default QuickSort method used by Dot NET. or of class objects (scalar. which are to hold the two items to be compared against each other. not being concrete objects. which should first be assigned the ' address of a custom item comparsion method that is defined to match ' the parameter definitions in the ComparerDelegate() declaration '********************************************************************************* Friend Function Sort(ByRef SortArray() As Object. starting on TI-59 Programmable Calculators in 1979. first examine our first two required declarations. This sounds more complicated than it really is. even though their names in the Delegate are purely arbitrary. to include arrays of custom-written classes. The trick to adapting a sort method to use any type object is to make the conduit to it generic enough to accept any array. whether it be an array of Strings. return 0 if the two objects are equal. their storage types.Enhancing Visual Basic . The sorting routine can be written generically enough. and also assign to it the address of our custom-written comparison method that uses the Delegate as its prototype. a ShellMetzner Sort leaves QuickSort in its settled dust. The second thing we need to write is a comparison method to actually compare two objects that will be provided to it by the sorting method. we can assign a custom-written comparison method to it. Here is where Delegates really pay off. class objects). but it simply keeps a cleaner appearance if the parameter names match those in the Delegate. if a function. but I will show you how to easily work around that restriction). it is sorted in Ascending order (SortDescending = False). By default.. CompareMethod. 'NOTE: string compares are best served by the StrComp() method.Name) 'compare class object members if both LeftItem and RightItem are not Nothing. it will sort the array in Descending order. What follows is a generic method template that we can use to write our own comparer support.. If RightItem Is Nothing Then 'and RightItem is ALSO Nothing. or choose a variable type.Text) Case 1 Return 1 'LeftItem>RightItem ( 1)(reflect CompareTo result) Page –107– . which ' will be provided to the method as XX and YY. String). ' 4) Next assign your Compare method and invoke the sort as follows: ' SortComparer = AddressOf MyCustomCompare ' Sort(MyArrayOfClass) '********************************************************************************* Friend Function CustomComparer(ByVal LeftItem As Object. ' 3) Create a version of the Compare() method below that will compare on that member... By providing the SortDescending parameter with a True value. sorting on an object's Title field. returning the standard StrComp() or CompareTo() result of -1 for left is less than right. for that member of two array elements. For 'example. ' 2) choose which member field in the custom class is to be used to perform sort comparisons on. where we would rewrite the following line to: 'Select Litem.. Exit if this is not an array Dim NumberofItems As Integer Try NumberofItems = UBound(SortArray) + 1 'number of items to sort Catch SortComparer = Nothing 'error. ByVal RightItem As Object) As Integer Try If LeftItem Is Nothing Then 'if LeftItem is Nothing. the Sort() method is expecting to receive the array as an array of type Object: “ByRef SortArray() As Object”.NET Beyond the Scope of Visual Basic 6..HalfDown 'look in upper half Dim IncIndex As Int32 = 0 'init index to start of array Do While IncIndex < HalfUp 'do while we can index range Dim IndexLo As Int32 = IncIndex 'set base Do Dim IndexHi As Integer = IndexLo + HalfDown 'invoke the sort comparer method If SortComparer(SortArray(IndexLo). SortArray(IndexHi)) = AscDecFlag Then 'check comparison result Dim Tmp As Object = SortArray(IndexLo) 'swap data if the first is greater than the second SortArray(IndexLo) = SortArray(IndexHi) SortArray(IndexHi) = Tmp IndexLo = IndexLo . 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.Name. so we will be doing descending End If ' now perform the sort Dim HalfDown As Integer = NumberofItems 'number of items to sort Do While CBool(HalfDown \ 2) 'while counter can be halved HalfDown \= 2 'back down by 1/2 Dim HalfUp As Int32 = NumberofItems .0 – David Ross Goben ' get number of elements to sort. Return 1 'LeftItem>RightItem because RightItem is Nothing and LeftItem is not Else 'Perform required numeric/text comparison.CompareTo(Ritem. DirectCast(RightItem.Enhancing Visual Basic . 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. especially if you do not care about character case: Select Case StrComp(DirectCast(LeftItem.HalfDown 'back up index Else IncIndex += 1 'else bump counter Exit Do End If Loop While IndexLo >= 0 'while more things to check Loop Loop SortComparer = Nothing 'make sure comparer is reset Return True 'return success End Function As you can see.. String). The highlighted line is where the SortComparer() method is invoked. The following template example is defined to compare two Strings.. so make sure comparer is reset Return False 'in dicate and error (the Array was not dimensioned) End Try ' determine if we are sorting in Ascending or Descending order Dim AscDecFlag As Integer = 1 'default sort direction to ascending If SortDescending Then 'Descending flag specified? AscDecFlag = -1 'yes. It also contains an optional Boolean parameter that specifies how to sort the array. Specify the same target field to sort on as needed. We must assign it our own custom comparison method before we can actually invoke the Sort() method. RightItem>LeftItem. 0 for equal. Return 0 'then they are equal Else Return -1 'otherwise. "Rick"} SortComparer = AddressOf CompareStrings 'set our custom compare method to sort with Sort(DirectCast(MyArray.Name.. RightItem>LeftItem.. and simply name it CompareStrings. Object())) 'sort it in ascending order (default) as an object-type array For Each str As String In MyArray Debug.Length) 'compare lengths 'Return 0 'x = y for most numeric types. 'For strings. Object()).NET Beyond the Scope of Visual Basic 6.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.Print(str) Next 'display the data in sorted order Of course. 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.0 – David Ross Goben Case -1 Return -1 'LeftItem<RightItem (-1) (reflect CompareTo result) Case Else 'LeftItem=RightItem ( 0) 'If you are not comparing strings. DirectCast(RightItem. "Rick"} SortStringArray(MyArray) 'sort it as an object-type array by way of a helper method For Each str As String In MyArray Debug.CompareTo(Ritem. 'Return Litem.Enhancing Visual Basic . Return 1 'LeftItem>RightItem because RightItem is Nothing and LeftItem is not Else Select Case StrComp(DirectCast(LeftItem. in our preparation code we would assign the address of the CompareString() method to the SortComparer field (SortComparer = AddressOf CompareStrings).SortDescending) 'sort it as an object-type array (you can use CObj() instead of DirectCast()) End Function Page –108– .CompareTo(DirectCast(RightItem. Optional ByVal SortDescending As Boolean = False) As Boolean SortComparer = AddressOf CompareStrings 'set our custom compare method to sort with Return Sort(DirectCast(SortArray.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. such as if X was shorter than Y.Length) ‘x = y. ByVal RightItem As Object) As Integer Try If LeftItem Is Nothing Then 'if LeftItem is Nothing.Length. we would first create a copy of it. String). "Zed". 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". or forget to cast our array to type Object so that the compiler has an excuse to scold us.Print(str) Next 'display the data in sorted order Here is the SortStringArray() helper method. which we could insert above our CompareString() method: '********************************************************************************* ' Method : SortStringArray ' Purpose : Sort a string array '********************************************************************************* Friend Function SortStringArray(ByRef SortArray() As String. 'so that exact matches with X might actually have different lengths. If RightItem Is Nothing Then 'and Y is ALSO Nothing. String). For example: Dim MyArray() As String = {"Bob". String).CompareTo(DirectCast(RightItem.Length. String). cut out the alternative notes.. "Allen". String). Return 0 'then they are equal Else Return -1 'otherwise. we will further want to compare the lengths of the strings.. and then pass the array of strings that we want sorted to the Sort() method by recasting it to an array of type Object. we would not want to make it messy. "Allen".. Or..Length. unless we compare string fields of object. 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. "Zed". Return DirectCast(LeftItem. 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.Name. CompareMethod. if the comparison matches. you may want to replace the following line with a simple "Return 0". String). Integer). but it needs less testing. The most interesting part is the SortIntegerArray() helper function.. The problem. However. Finally..ToString) Next 'display the result With just a few modifications to the above. we are able to use a variable of type Object to hold a scalar value. 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. For example.CompareTo(DirectCast(RightItem. Optional ByVal SortDescending As Boolean = False) As Boolean 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.Print(IntV. we can create an Object array that contains scalar values. We then populate it with the values of the Integer array. we repopulate the Integer array with the sorted list and return to the invoker. Consider the following SortIntegerArray() and CompareIntegers() declarations: '********************************************************************************* ' Method : SortIntegerArray ' Purpose : Sort an integer array '********************************************************************************* Friend Function SortIntegerArray(ByRef SortArray() As Integer. but we are not so lucky with arrays. such as Integer and Double? If we were to try to cast them as Object arrays and send them to the Sort() method. This is a bit confusing to me. Return 1 'LeftItem>RightItem because RightItem is Nothing and LeftItem is not Else 'compare Integers Return DirectCast(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 0 'then they are equal Else Return -1 'otherwise. I suppose. There. we create an array of Type Object that is sized the same as the delivered Integer array. because a scalar array cannot be boxed like individual scalars can. Next. well… it. ByVal RightItem As Object) As Integer Try If LeftItem Is Nothing Then 'if LeftItem is Nothing. we can likewise create a SortDoubleArray() method and a CompareDoubles() method (or for any other scalar): Page –109– . where it will automatically “box” a class wrapper around it. We can either copy each scalar to an Object array. 14.NET all arrays are in fact Objects. or we can create a tiny 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.. If RightItem Is Nothing Then 'and RightItem is ALSO Nothing.NET Beyond the Scope of Visual Basic 6. 26. RightItem>LeftItem... Granted.. we assign our CompareIntegers() method to the SortComparer Delegate callback hook. can be presumed to have something to do with the embedded boxing of individual scalars. 17} SortIntegerArray(Ary) 'create an integer array to sort 'sort the array For Each IntV As Integer In Ary Debug. and then pass that to the Sort() method.0 – David Ross Goben But what about scalar arrays. the compiler will snap its fingers in our face and inform us that we. test it with the following: Dim Ary() As Integer = {5. we simply invoke the sort. 3. To work around this issue only involves a small bit of work. Next.Enhancing Visual Basic . 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. because in . 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). cannot cast abstract Arrays to type Object. CustomButtonText2 As String.. As already indicated. Integer) 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. If RightItem Is Nothing Then 'and RightItem is ALSO Nothing.06. RegistryKeyForCheckBoxValue As String = Nothing. Title As String. Prompt As String. but you must supply all of them. all optional parameters are specified as “required” parameters. For example. Page –110– ...32} 'create a Double array to sort ' SortDoubleArray(dAry) 'sort the array ' ' For Each DblV As Double In dAry 'display the result ' Debug. so internally the function is always invoked as you see in the above delegate. CustomButtonText1 As String = Nothing. is invoked. be aware that optional parameters are not permitted in a Delegate declaration or when invoking a method through a Delegate.3.ToString) ' Next '********************************************************************************* Friend Function SortDoubleArray(ByRef SortArray() As Double. RightItem>LeftItem. Double)) End If Catch Return 0 'default to EQUAL if not of proper types End Try End Function CLOSING NOTES ON DELEGATES: In case you have not thought of it. 16.Print(DblV. Title As String = Nothing.17. when you define a Delegate for such a method. CustomButtonText2 As String = Nothing. when you create a delegate for methods that features optional parameters. Prompt As String. 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. 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). you cannot skip optional parameters. if the original method heading is: Friend Function myMsgBox(ByVal ByVal Optional ByVal Optional ByVal Optional ByVal Optional ByVal Optional ByVal Optional ByVal ParentForm As Form. 14. Double). What this means is that when using a delegate.. myMsgBox in this case.NET Beyond the Scope of Visual Basic 6. when the main method. Return 1 'LeftItem>RightItem because RightItem is Nothing and LeftItem is not Else 'compare Doubles Return DirectCast(LeftItem. simply specify any optional parameters instead as absolute parameters. 3. CustomButtonText3 As String) As MsgBoxResult As you can see. which is one of the main reasons why you must provide default values for optional parameters you declare. What the compiler does is simply add a small bit of code to supply any parameters to the function that you did not. 17. CustomButtonText3 As String = Nothing) As MsgBoxResult The Delegate you create for the above function can be formatted like the following: Friend Delegate Function myMsgBoxDelegate(ByVal ByVal ByVal ByVal ByVal ByVal ByVal ByVal ParentForm As Form.0 – David Ross Goben '********************************************************************************* ' Method : SortDoubleArray ' Purpose : Sort a double array ' 'EXAMPLE: Sorting a Double array: ' Dim dAry() As Double = {72.Enhancing Visual Basic . all optional parameters are always fed to it. CustomButtonText1 As String. Optional ByVal SortDescending As Boolean = False) As Boolean 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. RegistryKeyForCheckBoxValue As String. MsgBoxFlags As MsgBoxStyle. MsgBoxFlags As MsgBoxStyle = MsgBoxStyle. ByVal RightItem As Object) As Integer Try If LeftItem Is Nothing Then 'if LeftItem is Nothing.0.OkOnly Or MsgBoxStyle..Information. Return 0 'then they are equal Else Return -1 'otherwise.CompareTo(DirectCast(RightItem. Therefore.. "Testing Optional Parameters".0 – David Ross Goben For example. Nothing. whereas it will return 0 if it is set to String. Besides. Nothing. Granted. Nothing. I personally do not like unspecified default values.Information.OkOnly Or MsgBoxStyle. MsgBoxStyle. On top of that. Further. but by providing them. Nothing. Nothing. if you had created a Delegate reference and assigned your myMsgBox method to it.NET Beyond the Scope of Visual Basic 6. Nothing) This exercise also highlights one of the reasons why providing default values are so important. this is because Dim textLength As Integer = myString. because they can vary between platforms and languages. MsgBoxStyle. you must specify ALL parameters: delMsgBox(Me. Nothing. a reviewer then knows what the default is. Nothing. such as: Dim delMsgBox As myMsgBoxDelegate = myMsgBox When you invoke myMsgBox by way of delMsgBox. if you invoked the myMsgBox function like so: myMsgBox(Me.Enhancing Visual Basic . it is submitted like this: myMsgBox(Me. Nothing. In large part. "Testing Optional Parameters". I much prefer a default value for string text being String.OkOnly Or MsgBoxStyle. numbers default to 0.Empty (technically.Length can trigger an exception error if myString is set to Nothing. this specifies ""). Nothing) Page –111– .Empty rather than Nothing or VbNullString. strings do have a default value of Nothing. "Testing Optional Parameters") Internally.Information. and Booleans to False. the structure’s memory is expected to be. especially when the P/Invoke is not of your design.Enhancing Visual Basic . VB6 stored and passed UTDs using natural alignment. such as VB6 and Visual Fortran. The best solution to this problem is to place the variables with the smallest size (such as byte and string) after the larger sizes. you saw a byte or 16-bit integer member declared ‘Undefined’. In more practical terms. short b. most windows-based P/Invokes exhibited explicit padding so that they were fully compatible with languages that use natural alignments. with no gaps. Member fields (variables) were laid out in sequence and stored within memory in that exact same sequence. which was perfect for handling fixed-sized records in random access file input/output (I/O). } userType { // 1-byte unsigned char // 2-byte integer // 4-byte integer // 4-byte unsigned byte array ([0] – [3]) // 8-byte signed byte string (ANSI) With its single-byte packing. or arrange them so that each consecutive field will be placed on the appropriate boundary naturally. because it always uses natural alignment. laid out like this: abbccccddddeeeeeeee = 19 bytes of data Now. they also had a fixed size. VB. would store itself in memory and would pass itself to a P/Invoke 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. straightforward.NET Structures. must align on even boundaries following natural boundary rules. though. 32-bit values align on 4-byte boundaries. Apart from the most obvious difference of needing to declare the storage class of each data field under VB. meaning that each field type will align itself on multiples of its own size. and 64-bit values appear on 8-byte boundaries. This potential VB6 incompatibility for the most part was invisible to the VB6 user because the structures expected by P/Invoke Signatures are also laid out in such a way that each field would naturally fall on its expected boundary. but also inevitable incompatibility with VB6. Most P/Invoke Signatures expect that each field will also be stored in their declared sequence. a 2-byte integer. to address natural boundary issues. VB6 User-Defined Types gave way to VB. though they did do what they were meant to do. meaning that each field is laid out in memory immediately after the previous. VB6 does not have an unsigned byte integer integer array (element 0-3) string (ANSI) The above VB6 UDT. but 16-bit (2-byte) values are aligned on even-byte boundaries.0 – David Ross Goben VB. on rare occasions. BYTE d[4]. Char e[8].NET Structures Compared to VB6 User-Defined Types VB6 User-Defined Types were basic. which is why string fields were laid out in fixed sizes. and also rather primitive. more memory-efficient order (to avoid boundary gaps) than they are specified in your source code. consider the following imaginary C++ P/Invoke structure: Typedef struct BYTE a. but on byte boundaries (called single-byte packing).NET. and with the name change came many powerful features. another important difference is that the fields of the structures may actually be stored internally in a different.NET Beyond the Scope of Visual Basic 6. and is. not single-byte packing. were we to convert this structure to a VB6 User Defined Type such as the following: Type userType a As Byte b As Integer c As Long d(3) As Byte e As String * 8 End Type ' ' ' ' ' 1-byte 2-byte 4-byte 4-byte 8-byte signed byte. But this is not always practical. all so there will be no inadvertent gaps in memory. This is because the next entry. By employing fixed-length strings when strings were present. byte fields followed or filled in other fields. Further. Also. Page –112– . What this boils down to is that bytes can of course appear anywhere. int c.NET does not actually support fixed-length strings or fixed-size arrays without being specially instructed to do so. VB. such as the above or in a DllImport-style declaration). such as “Dim A As New MyStruct("Initial String")”. even with a “<VBFixedString()>” prefix. they must to be declared with parameters.ByValTStr. Because VB6 Win32 ANSI P/Invokes use 8-bit ANSI characters rather than the 16-bit Unicode used by VB. CharSet:=CharSet. SizeConst:=8)> _ Private e As String ' 8-byte string. 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. not the standard 8 2byte Unicode characters long. we need to declare a fixed-length string that is 8 1-byte characters long. the fixed-length string. I will not hesitate.NET will internally construct a properly sized memory block for transport. OR pre-pend with the easier <VBFixedArray(4)> <MarshalAs(UnmanagedType. More. For that we can also turn to marshaling commands (alternatively. All this yields “<StructLayout(LayoutKind. considering the following Structure: Public Structure myStruct Private m_myStr As String Public Property myStr As String Get Return m_myStr End Get Set(ByVal value As String) m_myStr = value End Set End Property ' actual field data for structure ' properties to manipluate our field data within our structure Public Sub New(ByVal InitialString As String) ' optional constructor to initialize _myStr string. Pack:=1)>”. to include constructors (Sub New). There is never such a thing as an empty Structure. Thus. Use marshaling to allow fixed arrays. can be addressed by pre-pending it with the much simpler attribute “<VBFixedString(nnn)>” to specify a byte length). as shown previously in this document. SizeConst:=4)> _ Private d() As Byte ' 4-byte array. Putting this all together.Sequential.InteropServices namespace is the solution to these incompatibilities.Auto. were I send a string directly as a parameter in a P/Invoke. because the default constructor is reserved and system-defined. defining them in either of the following manners will generate the same structure. if needed.NET structure is fully compatible with the previous structure: <StructLayout(LayoutKind. Even with Structures. Although Structures are an abstract class. because VB. nor can they inherit from other classes or implement interfaces. though the first will have un-initialized content. a Structure can also contain methods and properties. Structures are Abstract Classes.Sequential. Like a Reference Class. There are even more powerful differences between Structures and UDTs.ByValArray. the following VB. pre-pend with the easier <VBFixedString(8)> End Structure NOTE: I have not had trouble passing strings directly to P/Invokes because normally they are convert between 16-bit Unicode and 8-bit ANSI. CharSet:=CharSet.NET warns you 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. However. because fixed-sized arrays are not normally allowed in structures.Enhancing Visual Basic . By prefixing “<StructLayout(LayoutKind.NET’s Unicode Strings can convert to 8-bit ANSI or Unicode as required by any P/Invoke using the structure.NET. Structures are Class-like. 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. and as such they cannot be inherited. and as “CharSet:=CharSet. passing dynamic strings in structures that have attributes such as the above to an API can be relied upon to do the proper conversions as needed.0 – David Ross Goben The System. we can resort to marshalling again to address that issue.Auto” to the attributes. the fields are passed to a Win32 P/Invoke in the structure-declared order. By including “CharSet:=CharSet.Sequential)>” to the structure declaration.Auto” anywhere in an attribute parameter list. a feature that these VB. when passing to/from a P/Invoke as long as I remember to include the “Auto” keyword in the P/Invoke Signature (immediately after “Declare” in VB6-style declarations. even though a structure can have constructors. not to instantiate a new reference but rather to initialize its content to zeroes or nulls. Unlike Reference Classes.NET attributes have that VB6 lacked was the ability to specify that the structure should be stored and passed using single-byte packing by including “Pack:=1” in the attributes. Alternatively. otherwise _muStr will be set to Nothing m_myStr = InitialString End Sub End Structure Page –113– . and the second will use the New verb.Auto.NET Beyond the Scope of Visual Basic 6.Runtime. Finally. Variable C remained blank. Finally. Variable A is subsequently set to “Testing”. Next. principally because each structure variable contains it own data copy of an entire structure. Debug.NET Beyond the Scope of Visual Basic 6. whereas a Class’s primary purpose is to group data and methods to operate on that data. so all other reference variables assigned to it will also ‘see’ the change. and assign it new text 'C' 'D'. but two Structure variables are always isolated from each other. Had they been. you can copy an individually modified copy of a structure. variable A is declared with no initial value and variable B is declared with “Hello” as its initial text. and variable D is set to “GoodBye”.Print("B={0}". Classes are reference types.myStr) C. where differing parameter types could be used with samenamed methods.myStr) ' Display individual results The output from this short program is: A=Testing B=Hello C= D=Goodbye What this little experiment clarifies is that Structures are value types. VB6 users have complained to no end that VB. Consider the following code. Two Object variables can point to the same Class instance. Structures. then you must use a class.NET finally allows them to perform function overloading. being abstract classes.InteropServices namespace. variable C is set to A. ' ' ' ' create create create create a a a a copy copy copy copy of of of of myStruct myStruct myStruct myStruct as as as as variable variable variable variable 'A' 'B'. A Structure’s primary purpose is to group data. whereas class variables are actually reference types. the above structure should simply be initially set to a particular fixed value. Structure variables are not reference types. VB2005 quietly reintroduced this parameter modifier. However. then Variables B and C would have reflected what had been assigned to Variables D and A. Variable C was set to Variable A.Print("D={0}". you can instead alter the declaration of the m_myStr field using something like this: “Private m_myStr As String = "Initial String"”. holding individual copies of their data. users are complaining that they no longer have their work-around cheat hack. not reference types.Runtime.NET. the resulting contents of these 4 variables are printed to the Debug Output. Next variable C is declared just like A. But now that VB. though practically.myStr) B. As you can see. Notice the output. so changing one reference will in fact change their object target. If you need the ability to change the data through one variable and access the changed values through another variable. Even though Variable D was set to B. referenced earlier. Debug. See the later article. but Structures are value types. Likewise.NET “lacks” the ability to pass parameters as any type to a P/Invoke.0 – David Ross Goben NOTE: If. unlike a Field Dim in a module.Print("C={0}".myStr = "Goodbye" Debug. Marshaling Memory and Passing Strings to P/Invoke Signatures in VB. but remained being set to “Hello”. employing the above myStruct Structure definition: Dim Dim Dim Dim A B C D As As As As myStruct New myStruct("Hello") myStruct myStruct = B C = A A. but when Variable A was changed to “Testing”. Page –114– . are treated as value types.myStr) D.myStr = "Testing" D. respectively. However. is AsAny. Variable B did not change when variable D was later changed to “Goodbye”. Debug. the above would be best served by a Class. and so a change to one copy will in no way affect any other copies. and assign it a copy of the data in 'B' ' copy data from 'A' to 'C' ' Assign new data to 'A' ' Assign new data to 'D' A.Print("A={0}". on the other hand. The whole point to “As Any” was as a cheat to get around the fact that VB6 lacked the ability to implement function overloading. which is treated as Friend). 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. ONE FINAL NOTE: One of the Unmanaged Types provided by the System.Enhancing Visual Basic . What this shows us is that unlike Classes. on page 129 for more details. then variable D is declared and set to B. however. which can accept various types. The above shows some of the possibilities of structures. to Private Sub btnClose_Click(ByVal sender As Object. Thus.Worksheet) 'we must be sure to first add a . before it closes.Excel' 'create a new Excel instance 'add a workbook to the Excel object 'Add a worksheet to the workbook 'display the Excel spreadsheet and then fill it with some data Private Sub btnOpenExcelApp_Click(ByVal sender As System.NET Framework. 'Be sure to do MyBase.Quit() 'exit the Excel application Catch 'ignore any errors End Try Me. By placing the above lines of code after we have finished using our COM objects. which function outside the managed space of the .Object.Marshal.Runtime.Marshal.Click user had already closed the Excel app (you will be prompted for saving changes) When our application exits.InteropServices.Close() 'close the workbook object objEx.Enhancing Visual Basic . “Set Obj = Nothing” released memory allocated to an object.NET? It is because our VB. ByVal e As System. but afterward we discover that the Excel application actually hangs around in memory once we've finished and our main application had closed.NET application might be referencing COM objects.Interop Public Class Form1 Dim objEx As New Excel. because the Garbage Collector will always make a final run at the end of an application.NET interacts with COM objects. it automatically wraps all unmanaged COM references within something called an RCW (Runtime Callable Wrapper).Workbooks.NET reference to “Microsoft. because each of their reference counts will become zero. the runtime releases all its references to the COM object: Protected Overrides Sub Finalize() System.Runtime. When the reference count reaches zero. it will also automatically remove the RCW code from managed memory.NET Garbage Collector (GC) will not be responsible for. This is because the RCW maintains a reference count that is incremented every time a COM interface pointer is mapped to it.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. in front of Excel. To address this issue. the .Today.Finalize() last.Workbook = objEx. If you examine the RCW documentation. As such.InteropServices namespace. but because the actual COM objects are operating in unmanaged memory. you will see that an RCW must be explicitly released within our code as part of a developer-written cleanup process through the "Marshal.ToString).ReleaseComObject(objWB) System.Marshal. if you have MS Office installed: Imports Microsoft.Finalize() End Sub 'Decrement reference counts to each COM object.Click objEx.exe is still running. The ReleaseComObject method decrements the references count of a RCW.Office. available in the System. Consider the following coding situation.NET reference to 'Microsoft.Add(). this code forces the release of the objects from the system space. ByVal e As EventArgs) Handles Try 'Overlook errors in case the objWS. at least in a Dispose or Finalize method.InteropServices. the memory they occupy is not released.Interop.Range("A" & intRow. as you may discover if you happened to open up the Task Manager and look in the Process list.Office.0 – David Ross Goben Releasing COM Objects from Memory in VB. Excel.Close() 'close this application End Sub do this.Worksheet = _ DirectCast(objWB.Excel”. we must first understand how . nor can it handle this kind of memory management. but unmanaged resources often specifically require explicit. Is it really such a big issue? YES! In VB6.Delete() 'delete the worksheet object objWB. So why does this not always work in VB. developer-written cleanup. each time you close an external COM application.Worksheets. the Garbage Collector will release the RCW construct data from the memory used by VB.EventArgs) Handles btnOpenExcelApp.Value = Date.Add() Dim objWS As Excel.ReleaseComObject" method. in order to access the form's Close button) btnClose.NET application. where you will find to your surprise that Excel.Interop.NET (based on information from Microsoft sources) We open an instance of Excel in our VB. which it will also require a . 'Decrement order is not important. to find a solution. your code should invoke the "ReleaseComObject" Marshal command with each COM object. We can always rely on the Garbage Collector to perform all the requisite memory management tasks for our managed code.NET Beyond the Scope of Visual Basic 6. Page –115– .Office.AddDays(intRow).ReleaseComObject(objWS) System.Runtime. When referencing COM objects from our VB.Application Dim objWB As Excel.ToString("dddd") Next End Sub 'close down our Excel application (bring this app back up.NET application and later close it.ReleaseComObject(objEx) MyBase.NET. But I like ' to back out in the reverse order of going in.Runtime. Also. This is technically called a memory leak.InteropServices. " syntax has different behavior. Dim myStr As New String(ChrW(0). the function still returns True. indicating that a length descriptor is not to be used. then. well. What to do next: Review your code. the Array function is not supported (this differs from the unrelated. which stores members as Objects). the IsObject function returned True for an object." and ". when opening the file in Random mode. strings in VB. and all will also return True. be sure to set the StringIsFixedLength argument to True. Because of this minor difference. 2. A test using the Vartype() method may be required. using specific types are even faster than using generic Objects.. a two-byte length descriptor is by default added prior to each array/string entry so to specify its data length. if the variable contains a string. and assign the old Array values like: “Dim i() As Integer = {1. A best practice is that you should simply check to see if the left-most character of a directory name starts with "." strings were used in the path argument list to represent the current and the parent directory." and ". it is important that you be aware of those differences and you should also check your code to ensure that your usage of them is compatible.NET. it is an object reference. Page –116– . consider using the much faster System. and ignore those that do. preventing length descriptors from being read or written that would specify the number of dimensions and their lengths.NET. However.NET does support. these values are used to tell VB. Put# Functions The VB6 Get# and Put# functions were used to read and write data to a file. Array Function The VB6 Array statement returned a Variant array.NET act slightly differently from similar VB6 features. Under normal circumstances this has absolutely no effect on program execution. Likewise.32)). What to do next: Examine the variable(s) being evaluated by the IsReference function and make sure the logic is still valid. IsObject Function The VB6 IsObject function was used to determine if a Variant variable was of type vbObject. Because Variants are not supported under . The VB.." would be the first two entries.NET. Get#. If you want the same results for strings as under VB6. 3}”." Then”. Under VB.. such upgraded code will skip the first two files or directories. longer file length. respectively. because. it returned False. When these functions are used to pass dynamic arrays or strings to a file opened in Random mode. as are arrays of any type. Variant variables are converted to the Object data type. allowing you to assigning values to that array in a single line of code.NET. these are upgraded to the FileGet and FilePut functions. Dir Function The VB6 Dir function was used to return the name of a file or directory. If the variable was empty. But this is no longer true under VB. but False for a Variant. What to do next: VB6 Code that parsed directories often assumed that ". Even better. If an object is empty. The returned ".".IO namespace.NET are in fact objects.Enhancing Visual Basic . also be sure to set the optional ArrayIsDynamic argument to False with the FileGet and FilePut functions. If you want the same results for arrays as under VB6.NET Beyond the Scope of Visual Basic 6. but similar Array Class that VB. as in “If Left(dirValue. which means the arrays being passed predetermines these parameters. When a VB6 project is upgraded to VB." and ". but the use of ".0 – David Ross Goben Changed Behavior of VB6 Functions in VB.NET. Though using Objects is several times faster than using variants. resulting in a different. and so skipped them. 2. so the upgraded code will return an array of type Object.e.NET IsReference function should be used instead to determine if a variable contains an object.NET the length of the array/string to be read. When reading. and that the passed strings are already set to their required size (i. What to do next: 1. 1) <> ".. The VB. You may prefer to use a value-type array in place of the object array.NET Dir function is the same. In addition.NET (adapted from Microsoft sources) Various important features in VB. any Variant variables are converted to the Object data type. When a VB6 project is upgraded to VB. What to do next: Modify the upgraded code to use the Save method. use the Format function instead. 2. modify the data type of the result.NET. see Format Function in the MSDN Help. What to do next: If the Str function uses a date variable or date literal as an argument. date variables or literals were treated as valid numeric expressions. as was expected by the VB6 code Picture1. What to do next: 1.ImageFormat.Drawing. For example. SavePicture Statement The SavePicture statement under VB6 was used to save a graphic from the Picture or Image property of an object to a file.NET Str function takes an Object containing any valid numeric expression.Image. System.NET code if necessary.0 – David Ross Goben Mod Operator The VB6 Mod operator accepted any numeric expression and its result always returned as an integer. the result will be a floating-point number (the remainder value). If either of the operands is a floating-point number. and some controls return a different type. see the VB TypeName Function. 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. Under VB.bmp". regardless of the file extension Picture1. and if either operand is a floating-point number. emulating its functionality in C#/C++.NET. a much more efficient format than the dated “BMP” format. Alternatively.NET no longer accepts variants. The data type text returned by the TypeName function may have different values under VB.Save("MyPicture. Date variables and literals are no longer treated as valid numeric expressions. Graphics for the Image property were always saved as bitmaps regardless. and modify your VB.NET. but attains the same result as Format().Save("MyPicture. it was saved as a bitmap file (BMP). For more information.Imaging.NET.Save method saves the files in different formats. Return Statement The Return statement under VB6 was used to branch back to the code following a GoSub statement. NOTE: You may also choose to use the variable’s own GetType.NET Beyond the Scope of Visual Basic 6.Name method instead.Enhancing Visual Basic . and the default output format is now “PNG” (Portable Network Graphics).Bmp) Str Function The VB6 Str function takes a Long data type containing any valid numeric expression as an argument. will actually save the image as a PNG (Portable Network Graphics) file. the default behavior of the Image. The Mod operator under VB. the GoSub statement is not supported. as in the following example: ' Default. any Variant variables are converted to Objects. Under VB. manually convert it to the appropriate numeric data type.ToString("<format>") method. If a variant has been converted to an object during upgrade. use their x. For more information. If the format of the graphic for the Picture property was either GIF or JPEG (JPG). Page –117– . What to do next: Review any lines of code that use Return in conjunction with a GoSub statement. or modify your code to convert the operand(s) using the CShort or CInt function before using the Mod operator. Save takes an ImageFormat enumeration as an optional parameter that allows you to specify the format to use. strings are of type Object in VB.Image. it will save as a Bitmap. The VB. The Return statement is used to return control to an invoking code from a Function or Sub method. TypeName Function The TypeName function was used by VB6 to return a string specifying the actual data type of a variable. which is faster.bmp") ' Using this optional parameter instead. NET.Object.NET Beyond the Scope of Visual Basic 6. Under VB. VarType Function The VarType function under VB6 was used to return an enumeration representing the underlying data type of a variable declared as a Variant. though class objects work just as they did before. an unassigned variant returned 0 (Empty).Else statement to determine whether an object reference is of a specified object type. Variant variables are converted to the Object data type.NET.0 – David Ross Goben TypeOf Function The TypeOf function under VB6 was used in an If. instead: ' Modified code Dim m As MyType Dim mTest As MyType If m. ByVal eventArgs As System.Load Dim m As MyType ' UPGRADE_WARNING: TypeOf has a new behavior.NET Private Structure MyType Dim a As Short End Structure Private Sub Form_Load(ByVal eventSender As System. In this context.. What to do next: Determine if the VarType function is returning 9 and.NET. an unassigned object returns 9 (Object).EventArgs) Handles MyBase. if so.. user-defined types (now called Structures) are not object types and cannot be evaluated by the TypeOf function as they were under VB6.GetType Then . modify your code to correctly interpret the new result. not TypeOf. In VB6. Refer to the following constant table: Constant Value vbEmpty 0 Description Uninitialized (default) vbNull 1 Contains no valid data vbInteger 2 Integer subtype vbLong 3 Long subtype vbSingle 4 Single subtype vbDouble 5 Double subtype vbCurrency 6 Currency subtype vbDate 7 Date subtype vbString 8 String subtype vbObject 9 Object vbError 10 Error subtype vbBoolean 11 Boolean subtype vbVariant 12 Variant (used only for arrays of variants) vbDataObject 13 Data access object vbDecimal 14 Decimal subtype vbByte 17 Byte subtype vbArray 8192 Array Page –118– . If TypeOf m Is MyType Then MsgBox("MyType") End If End Sub What to do next: Modify your code to do a type comparison with GetType.GetType Is mTest... a user-defined type is considered an object type. in VB.Enhancing Visual Basic ...Then. When a VB6 project is upgraded to VB. The following example demonstrates the use of TypeOf with a user-defined type: ' VB6 Private Type MyType a As Integer End Type Private Sub Form_Load() Dim m As MyType If TypeOf m Is MyType Then MsgBox("MyType") End If End Sub ' After upgrade to VB. If you upgrade them manually. VB6 has a fixed-length string data type (String * CharCount) that VB. 25) ' the 'c' character post-fix forces type Char. ByRef nSize As Integer) As Integer”. these changes are made automatically for you and most-all P/Invokes work just as they did under VB6.NET Beyond the Scope of Visual Basic 6. And. Consider the following original VB6 code: Private Declare Function GetUserName Lib "advapi32. Ret) MsgBox(UserName) End Sub NOTE: When string parameters are involved. ByRef nSize As Integer) As Integer Sub GetUser() Dim Ret As Integer Dim UserName As String Dim Buffer As String Buffer = New String(" "c.NET. ByRef nSize As Long) As Long Sub GetUser() Dim Ret As Long Dim UserName As String Dim Buffer As String * 25 Ret = GetUserName(Buffer. Ret) ' consider replacing this with UserName = Buffer.Enhancing Visual Basic . you should ensure to include “Auto” directly after the “Declare” verb. Ret) MsgBox(UserName) End Sub ' set Buffer to 25 16-bit Unicode characters ' the API requires 25 8-bit ANSI character string. then simply rename every Integer to Short. the above P/Invoke could have alternatively been declared as “Declare Auto Function GetUserName Lib "advapi32. so " "c is exactly like using Chr(32) Ret = GetUserName(Buffer. See the note below for additional details. 25) UserName = Left$(Buffer. except when the Declare also specifies an Alias. which is often 8-bit ANSI. for any parameters that are strings. During a VB6 program upgrade to VB. we did not use the Auto verb because Alias is already specifying the ANSI version of the method. you should not include Auto because you are already informing the compiler of the method within the DLL to employ. For example.dll" (ByVal lpBuffer As String.NET does not support. Also notice that even though we are passing a string. with the exception that you must adjust your integer data type naming accordingly.NET Integer data type. 25) UserName = Left(Buffer.SubString(0.dll" Alias "GetUserNameA" (ByVal lpBuffer As String.0 – David Ross Goben Upgrading Win32 Data Types for VB. as indicated by the “A” post-fixed to the alias. consider the following VB6 code: Private Declare Function GetVersion Lib "kernel32" () As Long Sub GetVer() Dim Ver As Long Ver = GetVersion() MsgBox("System Version is " & CStr(Ver)) End Sub This is upgraded to VB. The Auto verb forces converting VB. so autoconvert types NOTE: Notice that we are passing a malleable string parameter using ByVal. and the VB6 Integer data type is now the VB. instead of leaving it to the compiler to determine it automatically. due to an Alias also being declared.NET’s 16-bit Unicode text to the platform-specified format. which is a beloved VB6 technique that we have been able to again employ since VB2005. Page –119– . you are advised to insert Auto between Declare and Function. For example. In most cases.dll" Alias "GetUserNameA" (ByVal lpBuffer As String.NET Short data type. in VB6 you can perform a more compatible action using a normal string filled to a particular length. and then every Long to Integer. " ") The above VB6 code will now upgraded cleanly and without warnings to VB.NET as follows: Declare Function GetUserName Lib "advapi32.NET as follows (note that Long becomes Integer in this example): Private Declare Function GetVersion Lib "kernel32" () As Integer Sub GetVer() Dim Ver As Integer Ver = GetVersion() MsgBox("System Version is " & Str(Ver)) ' this can be shortened to: MsgBox("System Version is " & GetVersion().ToString) End Sub Fixed-Length Strings On top of numeric data type upgrades. However.NET P/Invokes (adapted from Microsoft sources) Most P/Invokes can be used just as they were by VB6. 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 = String$(25. The VB6 Long data type is now the VB. which will already declare the required text conversion of “A” (ANSI) or “W” (Unicode). For these cases. NOTE: As of VB2005. the only allowed options are UnmanagedType. All later Windows platforms used Unicode. Under VB. A pointer to a null-terminated array of ANSI (8-bit) characters (. thus tightening VB6/VB.Unicode)>”. If that issues an unhandled exception warning. LPTStr. For virtually all P/Invoke declarations.LPWStr UnmanagedType. I would recommend that you instead simply resort to declaring your P/Invoke using Declare Auto.NET better handles passing strings to P/Invokes because you can also optionally declare how you want strings to be passed. and LPWStr. However.AnsiBStr UnmanagedType. as described in the opening paragraph of this article. but in cases where you do. For StringBuilder buffers. as in “<MarshalAs(UnmanagedType. below.LPWStr. In this case.0 – David Ross Goben In some cases VB. if you are not sure. NOTE: The above table applies only to strings. strings were treated as immutable and were not copied back from unmanaged memory to managed memory when the invocation returned. The BSTR format will be covered later in the article “String Formats” on page 127. If that fails. the only options allowed are LPStr. Page –120– . A pointer to a null-terminated array of Unicode characters (this is the format used by . A COM-style BSTR with a 32-bit prefixed length and 16-bit Unicode characters (already refrenced).TBStr UnmanagedType. The MarshalAsAttribute attribute provides three UnmanagedType enumeration values that can be used to marshal string support for unmanaged COM interfaces: Enumeration type UnmanagedType.NET strings). such as in “<MarshalAs(UnmanagedType. The MarshalAsAttribute attribute provides several UnmanagedType enumeration values to marshal such strings (note that type BSTR will be discussed in the later “String Formats” article): Enumeration type UnmanagedType. it is probably due to the very rare instances where the P/Invoke is trying to modify a string pointed to by one of its parameters. Be sure to also specify the associated string ByRef. as shown in the examples. resort to invoking it ByRefStr or ByVal.BStr UnmanagedType. Strings Used in Interfaces If an interface uses multiple string parameters that require more than one string format. A pointer to a null-terminated array of platform-dependent (platform-defined) characters. the following table shows the marshaling options used for string data types when passed as unmanaged interface arguments. ANSI. except when used with the VBByRefStr option.ANSI)> ByVal myString As String”. This is typical for most P/Invokes that uses 8-bit text characters (already refrenced). you can also specify ANSI or Unicode in place of the Auto verb.BStr (default) UnmanagedType. Indeed. then the usual solution for that is to try LPWStr. converting from the . A value that enables VB. ANSI was the default on Windows 95/98 and Windows Millennium Edition. you are usually best served by simply declaring the P/Invoke with the Auto verb. duplicating the exact same functionality performed by VB6.NET. Strings Used in Platform Invoke Platform invoke. or resort to a ByVal StringBuilder buffer.LPStr UnmanagedType.ANSI)>” or “<MarshalAs(UnmanagedType.VBByRefStr Description of unmanaged format A COM-style BSTR with a 32-bit prefixed length and 8-bit ANSI characters. This value type modifier is supported only for platform invoke (there is no need for it in in-app fully managed processes).NET platform compatibility. This is typical for P/Invokes that employ 16-bit text strings (already refrenced).NET to change a string in unmanaged code and have the results reflected in managed code. the following table lists the marshaling options for strings when marshaled as a method argument of a P/Invoke Signature. simply passing a string ByVal to a P/Invoke will also perform this same task. and let the platform determine how to configure it. Hence. A COM-style BSTR with a 16-bit prefixed length and platform-dependent characters.LPWStr Description of unmanaged format A COM-style BSTR with a prefixed 32-bit integer length and 16-bit Unicode characters.LPStr and UnmanagedType. copies string arguments. A pointer to a null-terminated array of 8-bit ANSI characters. such as one being ANSI and another being Unicode.NET type Byte). or in a platformdependent manner.NET type Char).VBByRefStr)> ByRef myString As String”.LPTStr (default) UnmanagedType. the technique used to consume unmanaged DLL functions in an unmanaged DLL library from the . Prior to VB2005. the characters within the string (by default 16-bit Unicode) can optionally be specifically marshaled to a P/Invoke as Unicode. you may seldom.LPStr UnmanagedType.Enhancing Visual Basic . if ever have to worry about the following special marshalling functionality.NET framework (also referred to as API Calls).NET Framework format (Unicode) to the platform unmanaged format.NET Beyond the Scope of Visual Basic 6. A pointer to a null-terminated array of Unicode (16-bit) characters (. you can properly marshal those rare situations by prefixing each parameter string variable in the appropriate format using a “<MarshalAs(UnmanagedType. NOTE: This table applies to strings. But for StringBuilder buffers. _ ByVal nSize As Integer) As Integer Dim S As String = New String(Chr(0). though not) to ANSI. This should be used for Win32 Types TCHAR and BYTE. the MarshalAsAttribute attribute provides several UnmanagedType enumeration values to marshal strings to a field in a specific manner: Enumeration type UnmanagedType.Ansi)> _ Structure StringInfoA ' ANSI (8-bit) <MarshalAs(UnmanagedType. but by also specifying the ANSI method to invoke using the Alias. this would also tell it to compress the text from its present state (assumed to still be Unicode.LPStr)> str As String) PassLPWStr Lib "StringLib. the type (8-bit ANSI or 16-bit Unicode) is determined by the character set of the containing structure. CharSet:= CharSet.Sequential.TBStr)> str As String) NOTE: The "Auto" verb is used to let the target platform determines the suitable character width (ANSI or Unicode).ByValTStr.Dll" (<MarshalAs(UnmanagedType. Also note that the SizeConst marshalling parameter actually specifies the number of Bytes in the string. Default is 16-bit Unicode.LPTStr)> Public f1 As String <MarshalAs(UnmanagedType. SizeConst := 128)> Public f2 As String End Structure Page –121– . Strings Used in Structures Strings are valid members of Structures.LPWStr)> str As String) PassLPTStr Lib "StringLib. CharSet:= CharSet. This is because Auto would tell the compiler to compress the 16-bit Unicode string to 8-bit ANSI. SizeConst := 256)> Public f2 As String <MarshalAs(UnmanagedType.AnsiBStr)> str As String) PassTBStr Lib "StringLib.BStr)> Public f3 As String End Structure <StructLayout(LayoutKind.BStr)> str As String) PassAnsiBStr Lib "StringLib.LPWStr)> Public f1 As String <MarshalAs(UnmanagedType. A pointer to a null-terminated array of platform-dependent characters (already referenced).Dll" (<MarshalAs(UnmanagedType. The CharSet argument of the StructLayout attribute that is applied to the containing structure determines the character format of strings in structures. we may seldom need to worry about implementing this information because in most-all instances such specialized massaging is not needed. Again. 260) Dim I As Integer = GetWindowsDirectory(S. Unicode.ByValTStr. the byte count should be double the character count for 16-bit Unicode if you require Wide strings. and platform-dependent characters.Sequential. A pointer to a null-terminated array of Unicode characters (default. or as a result from a P/Invoke. A fixed-length array of characters. already referenced).BStr UnmanagedType.LPWStr UnmanagedType.VBByRefStr)> ByRef lpBuffer As String.LPTStr UnmanagedType. The table below shows the options available for managing string data types of fields that may be required by certain P/Invokes. as well as ANSI.Sequential.Auto)> _ Structure StringInfoT ' Generic (platform-defined bit size) <MarshalAs(UnmanagedType.0 – David Ross Goben The following type definition shows the correct use of the MarshalAsAttribute for platform invokes: Class StringLibAPI Public Declare Public Declare Public Declare Public Declare Public Declare Public Declare End Class Sub Sub Sub Sub Sub Sub PassLPStr Lib "StringLib.ByValTStr Description of unmanaged format A COM-style BSTR with a prefixed 32-bit length and 16-bit Unicode characters (already referenced). The following example structures contain string references and inline strings. 260) 'init receiving buffer 'now get Windows directory to S and its data length to I If we were to insert Auto after Declare in the above P/Invoke Signature. However.ByValTStr. NOTES: The ByValTStr type is used for inline. Other types typically apply to string references contained within structures that are passed by reference.Dll" (<MarshalAs(UnmanagedType.Dll" (<MarshalAs(UnmanagedType. “GetWindowsDirectoryA” in this case.LPTStr)> str As String) PassBStr Lib "StringLib. hence. though StringBuilder buffers are not. My rule of thumb is this: If Strings are passed to. then just add the “Auto” verb to the declaration and ignore adding marshaling commands. as shown above. CharSet:= CharSet. Here is how to use the MarshalAsAttribute attribute to define the same structure in other formats: <StructLayout(LayoutKind. Consider this: Declare Function GetWindowsDirectory Lib "kernel32" Alias "GetWindowsDirectoryA" ( _ <MarshalAs(UnmanagedType.NET Beyond the Scope of Visual Basic 6.LPStr)> Public f1 As String <MarshalAs(UnmanagedType.Unicode)> _ Structure StringInfoW ' Unicode (16-bit) <MarshalAs(UnmanagedType. the ‘S’ string variable would contain garbage after the P/Invoke execution.Enhancing Visual Basic . fixed-length character strings within a structure. A pointer to a null-terminated array of 8-bit ANSI characters (already referenced). But if we ever do.Dll" (<MarshalAs(UnmanagedType. I tend to beat this topic to death in this tome because it has caused a great many developers uncounted hours of frustration.LPStr UnmanagedType. not Characters. so garbage will inevitably result. you should avoid using the Auto keyword in declarations that will also specify the actual method to invoke by an Alias.Dll" (<MarshalAs(UnmanagedType. SizeConst := 128)> Public f2 As String End Structure <StructLayout(LayoutKind. This is much easier than employing sometimes complex marshalling commands on each individual string. a member of the System. The fast solution is to pass the string ByVal. _ ByVal lpReturnedString As String. _ ByVal lpReturnedString As String. you could replace this VB6 usage by declaring two separate forms of the same P/Invoke. which sends a pointer to its text buffer ' Convert StringBuilder array to String text using its ToString() method Dealing with “As Any” in P/Invoke Signatures Using the As Any type in a Declare-style signature is not “directly” supported by VB.ToString() End Function End Class ' Friend handle to a Window. sb. and when you pass an object as a parameter. _ ByVal lpFileName As String) As Integer Page –122– . as shown below: Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpApplicationName As String.NET Beyond the Scope of Visual Basic 6. it is in the nature of an object to pass a pointer to the base address of its data (I consider this confusing ByRef/ByVal interop issue a major flaw in VB.Dll" ( _ ByVal hwnd As IntPtr. the VB6 GetPrivateProfileString signature has a parameter lpKeyName declared as type As Any. _ ByVal nSize As Long. _ ByVal lpKeyName As Any. you should account for one character of this capacity being reserved for a null string terminator. whether ByRef or ByVal.NET. ByVal nMaxCount As Integer) End Class Public Class Window Friend hWnd As IntPtr = Form1. so it passes the string as immutable (it in fact passes a disposable clone). but I understand their intent). _ ByVal nSize As Integer. _ ByVal lpDefault As String. but it makes perfect sense in OOP because of the fact that a string is an object (a reference type). _ ByVal lpFileName As String) As Integer Declare Auto Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpApplicationName As String. what is passed is a pointer to its data. The following code example demonstrates how a StringBuilder can be initialized to a fixed length. A StringBuilder can also be initialized to a fixed length. passed to a modifying P/Invoke.NET. as the argument instead of a string. _ ByVal lpKeyName As Integer. and how to copy the returned modified text to a string variable: Imports System. _ ByVal lpDefault As String.Text namespace.GetWindowText(hWnd.NET. When passed to a P/Invoke. _ ByVal lpDefault As String. _ ByVal lpKeyName As String. Variables of type As Any were used in VB6 as a way to pass a variable that was typically either a String or an Integer value of Zero. sb. one with longs and one with strings. and another that that will accept an integer: Declare Auto Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpApplicationName As String. ' define string buffer of 260 characters for P/Invoke ' send sb object ByVal. one that will accept a string. you can remove the error-generating "As Any" by replacing the above P/Invoke Signature with two separated overloaded versions.Handle Public Function GetWindowTitle() As String Dim sb As New StringBuilder(260) Win32API. which is its string. which all P/Invoke strings require. ByVal sText As StringBuilder. _ ByVal nSize As Integer.0 – David Ross Goben Fixed-Length String Buffers Sometimes we must send a fixed-length string to unmanaged code to be modified. A StringBuilder’s data can be modified by the callee because it is a class. emulating VB6 P/Invoke behavior.Enhancing Visual Basic . _ ByVal lpFileName As String) As Long Under VB. For example. Simply passing a string ByRef does not work with interop strings because the CLR cringes at passing managed text to unmanaged code. Microsoft recommends you pass a StringBuilder. _ ByVal lpReturnedString As String.Text ' enable easy access to the StringBuilder member Public Class Win32API ' Note below that ByVal for Strings sends the text pointer Public Declare Auto Sub GetWindowText Lib "User32. and so when passing a string or any class object as a parameter.Capacity) Return sb. _ ByVal lpKeyName As String. _ ByVal lpReturnedString As String.Runtime. NOTE: Actually. but this is not allowed in non-instantiated (non-inheritable) classes. This is primarily why I much prefer the overloading method. as shown below: <DllImport("kernel32. _ ByVal nSize As Long. you might just differ with me in this philosophy. you could get by using only the above string version. If you wanted to define them separately under VB6.NET formats for declaring this P/Invoke Signature: <DllImport("kernel32. _ ByVal lpReturnedString As String. following are the official VB.0 – David Ross Goben NOTE: You are allowed to optionally include the “Overloads” term at the start of the above two declarations.dll".VBByRefStr)> ByRef lpBuffer As String) As Integer End Function Or using a StringBuilder.Enhancing Visual Basic . as an unmanaged type in the System. GetPrivateProfileStringStr and GetPrivateProfileStringInt: ' version using a String Declare Function GetPrivateProfileStringStr Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpApplicationName As String. _ ByVal nSize As Integer. the compiler will automatically select the version of GetPrivateProfileString that will accept an Integer without you needing to think about it. here is how to pass lpKeyName in the GetPrivateProfileString P/Invoke As Any (be sure to declare “Imports System.AsAny)> ByVal lpKeyName As Object. because they can be declared as the above in all cases.NET Beyond the Scope of Visual Basic 6. _ <MarshalAs(UnmanagedType.NET. it is in fact possible to pass parameters AsAny (note the lack of a space in the term). such as Modules. _ ByVal lpDefault As String. When you use a Null (zero) as the lpKeyName parameter. and the overloading technique also reduces bugs and the chance for your code to fail. is in fact the danger of As Any. passing any reference or value to the above lpKeyName parameters will be accepted. being the base type of all objects and value types can be automatically corralled into an object wrapper. Hence. _ ByVal lpKeyName As Long. you can pass VbNullString or Nothing (same value) instead. _ ByVal lpBuffer As StringBuilder) As Integer End Function Page –123– . _ ByVal lpFileName As String) As Long Using the AsAny Marshalling Parameter Now that I have said that “As Any” cannot be used in P/Invoke Signatures. however. for completeness. for completeness. Finally. I tend to simply forget about the “Overloads” term. _ ByVal nSize As Long. so I will have one less warning issued to me.dll". Although I much prefer the proper overloading solution. This. _ ByVal lpDefault As String. under VB. because when you want to pass a null value rather than a string. SetLastError:=True)> _ Public Function GetCurrentDirectory(ByVal length As Integer. However. _ ByVal lpFileName As String) As Long ' version using an Integer Declare Function GetPrivateProfileStringInt Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpApplicationName As String. and I have also gone through all the time and trouble to explain the proper ways to address this problem through overloading (the “classic” VB As Any solution was actually a hack that allowed it to address overloaded methods declared in a DLL in the first place. and doing it this way will also mean that the functions will upgrade to VB. _ ByVal lpFileName As String) As Long NOTE: Because type Object is a generic type in VB. _ <MarshalAs(UnmanagedType. and why it should be completely dropped from usage: it is able to pass anything unchecked. simply because it did not directly support method or operator overloading).NET. you can define two separate declarations with two separate names.NET without a warning. SetLastError:=True)> _ Public Function GetCurrentDirectory(ByVal length As Integer. which also has the significant added benefit of extremely strong type-safety.Runtime.InteropServices” at the top of the file): Private Declare Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" _ (ByVal lpApplicationName As String. _ ByVal lpReturnedString As String. _ ByVal lpDefault As String.InteropServices namespace. which accomplishes the same thing. When the MarshalAsAttribute. You can use this member on the System.NET.Runtime.MarshalCookie field can be used to pass additional information to the custom marshaler.MarshalTypeRef field.String data type. A 1-byte signed integer.Decimal to marshal the decimal value as a COM currency type instead of as a Decimal. A currency type. You can use this member on the String data type. Specifies the custom marshaler class when used with the MarshalAsAttribute. A 2-byte signed integer. the SizeConst field must be set to indicate the number of elements in the array.InteropServices. A Unicode character string that is a length-prefixed double byte. false = 0).MarshalType or MarshalAsAttribute. A native type that is associated with an I4 or an U4 and that causes the parameter to be exported as an HRESULT in the exported type library. A COM IDispatch pointer (Object in Microsoft Visual Basic 6.NET Beyond the Scope of Visual Basic 6. A dynamic type that determines the type of an object at run-time and marshals the object as that type.CharSet argument of the System. You can use this member. This member is valid for platform invoke methods only. An 8-byte signed integer. You can use this member to transform a Boolean value into a 1-byte. C-style bool (true = 1.Enhancing Visual Basic .Value property is set to ByValArray. A 4-byte signed integer. which is the default string in COM. Always use the MarshalAsAttribute.NET UnmanagedType Members The following list features all the UnmanagedType enumerations used by .StructLayoutAttribute attribute applied to the containing structure. A Windows Runtime string. Using these members of UnmanagedType in a <MarshalAs(UnmanagedType)> declaration marshals parameters or fields to unmanaged code as described on MSDN (search for “unmanagedtype enumeration msdn”): UnmanagedType Member Description AnsiBStr An ANSI character string that is a length-prefixed single byte.InteropServices. An integer that can be used as a C-style function pointer. A Windows Runtime interface pointer. AsAny Bool BStr ByValArray ByValTStr Currency CustomMarshaler Error FunctionPtr HString I1 I2 I4 I8 IDispatch IInspectable Page –124– . You can use this member on a Delegate data type or on a type that inherits from a Delegate. You can use this UnmanagedType only on an array that whose elements appear as fields in a structure. on the String data type.Runtime. Used on a System.SizeConst field to indicate the size of the array. The MarshalAsAttribute. false = 0). Used for in-line. The ArraySubType field can optionally contain the UnmanagedType of the array elements when it is necessary to differentiate among string types. You can use this member on any reference type. The character type used with ByValTStr is determined by the System. This is the Win32 BOOL type. You can use this member on the Object data type. fixed-length character arrays that appear within a structure.0).0 – David Ross Goben List of All . A 4-byte Boolean value (true != 0. false = 0).IUnknown when you apply it to the Object data type. When marshaling from unmanaged to managed code. A 2-byte. The Guid of the interface is obtained from the class metadata. OLE-defined VARIANT_BOOL type (true = -1. optionally followed by the unmanaged type of the elements within the array when it is necessary to differentiate among string types. A 4-byte floating-point number. A single byte. An 8-byte unsigned integer. You rarely use this BSTR-like member. A pointer to a C-style structure that you use to marshal managed formatted classes. This value is only supported for platform invoke. the length of the array is determined by the length of the managed array.StringBuilder data types. An 8-byte floating-point number. A platform-dependent character string: ANSI on Windows 98.SizeParamIndex fields. 8 bytes on 64-bit Windows.Enhancing Visual Basic . unsigned integer: 4 bytes on 32-bit Windows.NET Beyond the Scope of Visual Basic 6. A pointer to the first element of a C-style array. signed integer: 4 bytes on 32-bit Windows. A platform-dependent. platform-dependent char string: ANSI on Windows 98. which is a self-describing array that carries the type. and bounds of the associated array data.String and System. You can use this member with the MarshalAsAttribute. null-terminated Unicode character string. the length of the array is determined from the MarshalAsAttribute. and Unicode on Windows NT and Windows XP. Use this member to specify the exact interface type or the default interface type if you apply it to a class. This member produces the same behavior as UnmanagedType. When marshaling from managed to unmanaged code. You can use this member on the System.0 – David Ross Goben UnmanagedType Member Description Interface A COM interface pointer. A length-prefixed. null-terminated ANSI character string. A platform-dependent.Text. Unicode on Windows NT.SizeConst and MarshalAsAttribute. which is used to marshal managed formatted classes and value types. You can use this member on the Object data type. 8 bytes on 64-bit Windows. because exporting a string of type LPTStr is not supported. A 4-byte unsigned integer. A 1-byte unsigned integer. A 2-byte unsigned integer. rank. A 2-byte. This value is supported only for platform invoke and not for COM interop. This member is valid for platform invoke methods only. A VARIANT. A value that enables Visual Basic to change a string in unmanaged code and have the results reflected in managed code. A SafeArray. A COM IUnknown pointer. IUnknown LPArray LPStr LPStruct LPTStr LPWStr R4 R8 SafeArray Struct SysInt SysUInt TBStr U1 U2 U4 U8 VariantBool VBByRefStr Page –125– .SafeArraySubType field to override the default element type. 2012.NET's Platform Invoke (PInvoke) functionality. Copy & Paste your way to productivity Certain things just can't be done in pure . this should not stop you from simply using the site itself to find the information you need. it is user driven and acts as a repository where developers can contribute or retrieve information as they wish. From the PInvoke.NET or in the . Access PInvoke. It is often reported in many dated blogs that it does not work with VB2008 or later (though you could edit its data file to do so). Obviously. Most P/Invokes feature equivalent methods in VB. They also provide a free Visual Studio add-in that will allow you to insert P/Invokes directly into your code. derive from Windows classes. you will have to fix these on a case-by-case basis. so that you don't have to spend time writing them from scratch.net. and any other information related to calling Win32. PInvoke. Page –126– .net. type definitions.pinvoke. which requires declarations to be supplied by the developer.NET. perform message queue hooking.NET Beyond the Scope of Visual Basic 6. The very best advice that I can offer is to become a regular visitor to Redgate’s www.NET Framework and you can normally find them and their . were you to decide not to install this add-in. or other unmanaged APIs in managed code (languages such as C# or VB. and so on.net supplies you with tried and tested signatures and type definitions. Queue Hooking. and 2013. but the add-in has since been updated on their website to support VS20010 and after.net website. and the developer has to drill down to the Windows API.net directly from within Visual Studio Download the PInvoke. Some of the VB6 methods will throw run-time errors in VB.Enhancing Visual Basic . You can now insert a PInvoke signature with the click of a button while you're working on your application in Visual Studio. meaning Platform Invoke. As a wiki. Even so. This is achieved through .NET developers a month to find and contribute PInvoke signatures (also known as Declare statements in VB). and include example code to show you how to implement them.0 – David Ross Goben Thread Creation.000 .NET solutions by performing a search of the on-line help (the best help is in the “Help for VB6 Users” article). is a wiki used by around 50. This is a user-supported Wiki site that provides P/Invoke formats that really do work.Net Website: PInvoke.NET).NET. and save yourself the effort of having to open up a web browser to search for the PInvoke.net add-in for Visual Studio 2010. Manually defining and using PInvoke signatures is an error-prone process that can introduce extremely subtle bugs. and Other Special P/Invoke Features The final area where you may need to make some changes in your VB6 code upgrades is if you are using P/Invoke Signatures that perform Thread creation. though it does not use it. not the number of characters. was returned by the StrPtr function. This makes it clearer why BSTR's are null terminated.Enhancing Visual Basic .NET (adapted from Microsoft sources) By understanding how VB6. to remain clear on this. but is requisite for defining data of type BSTR) and is always terminated by a single null 2-byte character (ANSI = 0). a Unicode character array and a BSTR should not be confused as being the same thing. at least as far as VB6 is concerned. or as a VB string. VB6 and other COM-based applications employ a string format known as BSTR (Bee-String). like all pointers. so we cannot rely on a null character to signal the end of its string. to allocate space for in the character array. and . The reason is that the Unicode version of a Win32 string (denoted by LPWSTR. Further. We should also emphasize that an embedded null Unicode character is a 16-bit zero (2-bytes). the BSTR is actually a separate Pointer variable that points to its string data. Since the array is Unicode. NOTE: Under VB6. COM. Long Pointer to Wide–16-bit– String) is defined as a pointer to a null-terminated Unicode character array. When you define a string under VB6.0 – David Ross Goben String Format Changes between VB6 and VB.NET. the actual character count is always one-half its byte count. In other words. which is in turn led by a 32-bit integer length descriptor. not an 8-bit zero (1-byte). however.NET because the antiquated OLE2 technology is not used by VB.NET. or IntPtr. A Unicode character array is an array of 16-bit Char values. we should not refer to a BSTR simply as a string––we should refer to it by its unequivocal name––Bee-String.NET Beyond the Scope of Visual Basic 6. the address of a string’s BSTR pointer field was returned by the VarPtr function. Thus. As such. being an unsigned 32-bit Integer. Access to the BSTR pointer. which is defined in the OLE 2. However. though the BSTR points beyond this 32bit length descriptor and directly to the very first 16-bit Char in the Unicode character array. such as this: Dim str As String Str = "hello" This string’s data is stored in memory like this: In the above diagram. Also be mindful that the 32-bit descriptor field in a marshalling command always specifies the number of bytes. VC++ and Win32 use the string data types LPSTR (8-bit ANSI) and LPWSTR (32-bit Unicode). you can better understand how to deal with them when developing and porting applications from VB6 to VB. even though you would be technically correct to refer to a BSTR as a string. is not allowed to contain embedded null characters. by the way.0 specification and which is also a part of the Microsoft ActiveX specification (MS Marketing’s new name for what had been OLE2). Additional null characters can be embedded anywhere within the BSTR’s Unicode character array. you would be incorrect to consider that the Unicode character array is also a string. However. is not possible under VB.NET store string data. excluding the terminating null bytes. because a BSTR may contain embedded null characters. beyond its special 32-bit length field. not by Char. the length descriptor field is vital. but as separate entities. A BSTR with no embedded nulls can otherwise be considered like an LPWSTR. the terminating null is not of much use. and the address of the characters of the Unicode array. and it in turn points to a Unicode character array. Page –127– . which. Be mindful of this when testing for null characters in Unicode strings when indexing by Byte. its presence is extremely important for Win32. The Unicode character array that is pointed to by a BSTR must always be preceded by a 32-bit (4-byte) length descriptor field (this descriptor is not typical for Unicode arrays. which also made it fully compliant to OOP. you can once again. and converts it back to Unicode if it returns or modifies the string. or when passed ByVal for full VB6 (and OOPL) compatibility. If the P/Invoke will be modifying it on rare occasions. for more details on sending strings to P/Invokes. This also eliminated a lot of confusion that people had with passing StringBuilder strings ByVal and passing strings ByRef using the VbByRefStr parameter. Although internal bookkeeping can figure this out virtually all the time. Under VB. Page –128– . an LPCWSTR is just like LPWSTR.NET methods will work exactly as expected.NET are 16-bit Unicode. NOTE: You may encounter the data types specified in some documentation as LPCSTR and LPCWSTR. Hence. On the other hand.VbByRefStr)>” modifier (available by importing the System. Similarly. When you send a string of text to a P/Invoke. forget about passing strings ByRef when using interop methods and just use ByVal. you may notice that it is usually sent ByVal. for self-protection. The embedded C means Constant and indicates an instance of this data type cannot be changed by any P/Invoke Signature that uses this type. or prefixing the ByRef string with the “<MarshalAs(UnmanagedType.NET”. as a rule of thumb. See the following article. Otherwise. passing the string address when sent ByRef.Enhancing Visual Basic . you should marshal it in the P/Invoke declaration accordingly. except that pointer to a disposable clone of the string is passed as a parameter by the compiler. pass a string ByVal to a P/Invoke in order for it to be modified by the P/Invoke method. These string data types are shown below. as you were able to do under VB6. then. passing a string ByVal or ByRef as a parameter to . similarly. Strings in VB.NET Beyond the Scope of Visual Basic 6. except when preceded by the special VBByRefStr prefix shown above. just as it had been under VB6. the Unicode string may need conversion to 8-bit ANSI if the target P/Invoke expects it. LPSTRs are not allowed to contain embedded null characters. rather than the actual string itself. If you are confused by this. as of VB2005. But even so. if you must pass or retrieve character data that is 8-bits wide. However.0 – David Ross Goben An LPSTR string is defined as a pointer to a null-terminated ANSI character array. “Marshaling Memory and Passing Strings to P/Invokes in VB. and. an LPCSTR is just like LPSTR.Runtime. However.NET.InteroServices namespace). When sending a string to a P/Invoke. the system does need your help and your permission to operate on your string this way. sending a string ByRef to unmanaged interop P/Invokes serves no purpose because the CLR will still pass them as immutable. considering that StringBuilder text manipulation is 200 times faster than a regular string. be sure to either resort to using a ByVal StringBuilder buffer in place of string. it is strongly recommended that one should look into them when performing time-critical string manipulation. because the only way that we can tell when an LPSTR string ends is by the location of the terminating null. or make this conversion transition easy by simply inserting the ‘Auto’ verb after the ‘Declare’ P/Invoke declaration verb to trust internal bookkeeping to get it right. and as an immutable string when ByVal. an LPWSTR is a pointer to a null-terminated Unicode character set with no embedded nulls. You should follow this using VB. and it also does it all within managed memory without requiring the use of unmanaged methods. select Project. such as a Structure. 6. starting with a new Console Application. Now. From the Main Menu ribbon. This is a technique that most VB. 4. select Project. and then use a managed method to marshal that data into the actual target structure in Heap memory. They do so because it does in fact work. an exception error will occur because what we would be trying to do is to use an unmanaged COM function to write to the protected managed Heap memory of a class (I discovered that this is allowed in an abstract class. but passing unmanaged data directly into managed . A solution is to transfer the data with an unmanaged method to a neutral. To avoid wandering off topic. We will walk through this whole process. 5.NET-gurus will advise you to use. Type CopyMemorySample in the Name box. 5. 3.0 – David Ross Goben Marshaling Memory and Passing Strings to P/Invokes in VB. 3. just so you will understand this popular concept. Type clsTest in the Name box. 2. the second 20 characters long (Name). This article will show you how to use the Windows P/Invoke RtlMoveMemory (normally expressed in VB code function declaration by the more common name of CopyMemory) to populate Class data using a string as input. the first 10 characters long (ID). processing them as fixed-length records is typically the fastest and most straightforward manner of processing these files because of the simple fact that the data does have a known size. Step 1: Create a Test Project named “CopyMemorySample” 1. To copy data to the Heap. Note that all variables are ByVal.NET structure fields presents us with a very rare and rather unique problem: if we attempt to use RtlMoveMemory to write to the data members of a .Enhancing Visual Basic . please be sure to read their comments. not on the Heap). consisting of three strings. Open Visual Studio. less resources. ByVal pSrc As String. It is normally a good idea to give descriptive names to our code components. but for this small project we will just leave Module1 alone. Select 'Add Class' from the Project menu.NET (Inspired from an article by James Mimeault) Frequently developers have to cope with data file formats containing fixed length records. Step 2: Add a “WINAPI” Class to Store Our CopyMemory P/Invoke Signature Declaration 1. we must work around the problem.”). 3. non-Heap buffer accessible by both managed and unmanaged methods. Likewise. From the Main Menu ribbon. You have to go somewhere else first. We do not normally need to perform a seemingly redundant task of first copying data to an intermediate location before we can place it where we really want to put it (this is like getting directions in Maine: “You can’t get there from here. The project template will open and you will see the module 'Module1' in the Code Viewer. which is stored on the program Stack. add the following Class declaration that contains our fixed-length data. and the third 30 characters long (m_chAddress): Page –129– . 2. 4. Select Visual Basic Projects from the Project Types list if it is not already selected. Copy or type in the following code so that your WINAPI Class will now look like this: Public Class WINAPI ' Declare the Windows P/Invoke Signature CopyMemory. and NOT alter the IntPtr value itself.NET class. ' pDst is passed ByVal because we want CopyMemory to go to that location and modify ' the data that is pointed to by the IntPtr. Select Console Application from the Templates list. We will try this useful method first. Click the Open button or just hit the Enter key. Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal pDst As IntPtr.NET and select the 'New Project' option. In most situations the RtlMoveMemory P/Invoke method is the perfect tool for these cases because it virtually eliminates the alternative of having to parse the data.NET Beyond the Scope of Visual Basic 6. 5.NET on your system. ByVal ByteLen As Integer) End Class Step 3: Add a “clsTest” Class Formatted to Receive a Fixed 60-Byte Record of Test Data 1. Click the Open button or just hit the Enter key. Type WINAPI in the Name box. But even so. but then I will afterward show you a much better solution that will use less time. Select 'Add Class' from the Project menu. when we get to the code blocks. feel free to simply copy and paste them into your program. 4. Click the OK button or just hit the Enter key. 2. If Target Is Nothing Then Return Nothing ' Declare an IntPtr that will hold a memory address. p_objTarget = Marshal. This would be useful for P/Invokes that are expecting to receive PRE-FILLED structure members.Enhancing Visual Basic .ByValArray. 'Marshal.. still populated. unless you had 100s of thousands or millions of these). we will trap any exceptions. because only the field members will get the data.ByValArray.Sequential) dictates that the values within the structure/class will be ' guaranteed to be stored sequentially in memory.SizeOf(Target)) ' this will copy only DATA members ' Now. ' SizeConst specifies the data as the count of Char array elements. coincidentally is the same ' size as the source string. Marshal. Name second. 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() P/Invoke. ReportExError(ex) End Try ' send the results back to the invoker for printing Return Target End Function 'CopyStringToClass What this Code Does So Far     The Function CopyStringToClass takes a String and a Class Object instance as parameters.ByValArray. this could have been a Structure 6. Target) ' Copy the data at the pointed address to the Target class data ' Always free the intermediate memory that was allocated.StructureToPtr(Target. into our .InteropServices ' Namespace used ' StructLayout(LayoutKind. and ' Address third in one sequential block of memory (otherwise. as used in the following line of ' code. immediate return nothing. WINAPI. <MarshalAs(UnmanagedType. use StructureToPtr.AllocHGlobal(Marshal. We declare a variable of type 'IntPtr' to hold a memory address of the data space set aside in intermediate memory.NET instance of the Target class object (of type clsTest) on the Heap.NET might try to use a more efficient layout).Sequential)> _ Public Class clsTest ' MarshalAs informs the Marshaler of the intended type and size of the data we are trying to work with.NET Class) is nothing. ID first. SizeConst:=30)> Private m_chAddress As Char() '<. otherwise we will create a memory leak. . SizeConst:=20)> Public Name As Char() '<. Marshal.second field (20-bytes) ' Private storage for property values.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. ByVal Target As Object) As Object ' If the Target (a pointer to a .NET Beyond the Scope of Visual Basic 6. Do not try to copy a string from unmanaged memory into ' a class property using the Win32 CopyMemory() P/Invoke because this WILL trigger an exception error.third field (30-bytes) ' no problem with properties either. We will just use its defined P/Invoke. Notice that we did not need to declare a variable of class 'WINAPI' and initialize it using the New keyword.. Page –130– . ' pointed to by p_objTarget. p_objTarget.SizeOf(Target)) ' To copy Target to p_objTarget on the local heap. Switch the Code View back to Module1 by clicking the Module1 tab at the top of the Code View window. This is because this class does not have Procedure-Level Fields (variables) defined. not bytes as <VbFixedString()> will do. AllocHGlobal() will return a memory pointer to this new storage space.Runtime. Private Function CopyStringToClass(ByVal Source As String.Catch block. Using a Try. ' 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. NOT its program code. tell the Marshaler to copy the data that is now in intermediate memory (the results of CopyMemory()).first field (10-bytes) <MarshalAs(UnmanagedType. Source. Dim p_objTarget As IntPtr ' used 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. The SizeOf() ' function will only return the size of the data members of the Target class.PtrToStructure(p_objTarget. In other words. SizeConst:=10)> Public ID As Char() '<. True) ' copy a structure to the p_ObjTarget address. ' however.CopyMemory(p_objTarget. Marshal. <StructLayout(LayoutKind.0 – David Ross Goben Imports System. 7. 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. <MarshalAs(UnmanagedType. keep in mind that marshalling these unmanaged types will ASSUME 8-bit ANSI characters. We had defined set-aside sizes for members in our clsTest class definition). ByVal Value As clsTest) Console. for strings that are declared as strings.ID = " & CType(Value. This is the reason why we are instead using Char Arrays (As Char()) in our Class and Structure examples instead of simple strings.NET is kind enough to account for treating Char Arrays as String objects. " ' Sampling index --------> 123456789012345678901234567890 ' declare the Target parameter for the CopyStringToClass function.Name = " & CType(Value.InteropServices.WriteLine(ControlChars. internally.Address = " & CType(Value.Runtime.Marshal. press <Enter> to close the program.Runtime.Exception) Console.Write("Press <Enter> to continue.WriteLine("clsTest. We use the member function CopyMemory() of the WINAPI object to copy the string into the intermediate memory that we set aside for clsTest (the 'Target'-sized block of heap memory). and VB.WriteLine() ' Prompt for user input PromptForEnter() End Sub 'ReportExError End Module Step 5: Running the CopyMemorySample Program 1.Marshal class.WriteLine("clsTest. When you are done. it will result in a memory leak.Address. NOTE: Be aware that although the MSDN documentation for “<MarshalAs(UnManagedType.Enhancing Visual Basic .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 area. String)) Console. We invoked the System. 2.WriteLine("Source String = " & Source) Console.0 – David Ross Goben      Using the System. String)) Console. string are just arrays of type Char)... We return the now-populated class back to the invoker. and then 30 character segments Dim strSource As String = "0123456789" & _ "Abraham Lincoln " & _ "1234 Pennsylvania Blvd. consisting of 10 (ID).ReadLine() End Sub 'PromptForEnter '---------------------------------------------------------------------------' let's see what we got from our Source '---------------------------------------------------------------------------Private Sub PrintResultsOfCopy(ByVal Source As String.") Console. Step 4: Finishing Up With Some Startup Code: Now add the following code to Module1. One character will be “lost” for every string in your class or structure simply because.WriteLine("Exception Caught: " & e. 3.InteropServices. it does not work exactly as we might expect for Classes and Structures (for our purposes.NET Beyond the Scope of Visual Basic 6. prompt and return '---------------------------------------------------------------------------Private Sub ReportExError(ByVal e As System. we allocate a 'Target'-sized block of intermediate memory and assign the returned pointer to that memory to p_objTarget (which is an IntPtr object).Name. A Console window will appear and show you that our code works if you entered it exactly as shown above.NET IDE. its final character position will be overwritten by a null terminator to mark the end of each string.ByValTStr)>” shows that it can be used to marshal strings for Classes and Structures. then 20 (Name). In the Visual Studio. 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.Runtime. but without null terminators.WriteLine() Console.InteropServices.InteropServices Module Module1 Sub Main() ' 60 char string for sample data. String)) ' Prompt for user input PromptForEnter() End Sub 'PrintResultsOfCopy '---------------------------------------------------------------------------'exception display.Marshal. which are directly compatible with strings (because.Message) Console. anyway). We invoked the System.WriteLine("clsTest. Page –131– . 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. to declare our Sub Main method and its dependent subroutines: Imports System. If this is not done. clsTarget).ID.WriteLine() Console. press the <F5> key. clsTest) 'cast returned object to type clsTest PrintResultsOfCopy(strSource.CrLf & "Results of CopyMemory for clsTest:") Console.FreeHGlobal() method to free the block of intermediate memory that we allocated for the copy.Runtime. StrPtr. If we do this. and consequentially also completely eliminate the need to use the CopyMemory P/Invoke. ByVal Target As Object) As Object If Target Is Nothing Then Return Nothing 'If the Target is nothing. and arrays – there is no need for differently-named functions. nor does it employ non-heap allocation/de-allocation.UTF8. then 20. 'convert unicode string to UTF8 8-bit string of bytes. This new code also shows you a practical reason for using the VB. For example. It occurred to me at this point that we could instead use just a simple Byte array in place of the allocated neutral storage. which is superior to ANSI. and the . only using 16-bits as needed) and also how to perform C-style pointer-based memory access from VB. and we allocated 3 strings (Char arrays) that totaled 60 characters.PtrToStructure(VarPtr(Bytes). strings. as VB6 required. consisting of 10. structures. and then 30 character segments. This would be a means of standing in for the 8-bit ANSI text data that the PtrToStructure method is expecting to work with. Target) Catch ex As Exception ReportExError(ex) 'An exception could occur if the system is out of memory (not very likely) End Try Return Target 'Send the results back for printing End Function 'VB.NET compiler has extensive Win32 API knowledge and of the RtlMoveMemory method.Text. This little 3-line function can be used to return the base address of value types. GCHandleType. we must keep in mind that we are dealing with unmanaged strings. and each of its bytes can usually fully absorb and reflect most common 16-bit Unicode characters within an 8 bit structure without loss of information.GetBytes(Source & ChrW(0)) '(Use UTF7 if you really require 7-bit text) ' get the address of the Byte() array as an IntPtr value and tell the Marshaler to copy ' the data in the Bytes array into our instance of the Target class object (clsTest). but it eliminates a lot of overhead and runs faster. we may discover that we are allocating a memory buffer that is actually sized to 60 8-bit ANSI characters of space.Alloc(o. and knows to automatically convert the 16-bit characters to 8-bit characters for its use. if you added this test line. because PtrToStructure will expect 8-bit characters.Encoding. Now consider the following replacement CopyStringToClass function that does not use the CopyMemory P/Invoke. The SizeOf method returns the number of bytes of the data members. but only as needed to get the memory address of the Bytes array): ' copy a string into a class's properties WITHOUT using CopyMemory Private Function CopyStringToClass(ByVal Source As String. and even though the SizeConst attribute parameters in the clsTest class designates an array element count. which is prior to conversion. immediate return of nothing. we might expect 120 bytes. etc. doing that service for us automatically).AddrOfPinnedObject GC.NET-emulated VB6 VarPtr function.Enhancing Visual Basic . and we can keep everything within managed memory. Marshal.0 – David Ross Goben Optimizing the Code to Run Much Faster In reviewing this code. and the internal bookkeeping will assess this data to actually be 8-bit ANSI strings. “Dim Cnt As integer = Marshal.NET Beyond the Scope of Visual Basic 6.Free() End Function 'Use Object as a 'catch all' universal data type 'Get a trackable handle and pin the obj address 'Get the memory address of the pinned object (the variable's data address) 'Free the allocated space used This version may not appear to run differently. It also demonstrates direct VB-based conversion of a string to 8-bit data (actually UTF8. not the 60 16-bit Unicode characters we might assume (120 bytes).NET version of VB6 VarPtr. this one template handles them all. 'and the marshalled string in the Target's class are actually allocated as 8-bit ANSI strings. ObjPtr. Page –132– .NET CLR (the RtlMoveMemory method in Kernel32 expects and processes characters that are 8-bit. Public Function VarPtr(ByVal o As Object) As IntPtr Dim GC As GCHandle = GCHandle. at the head of the Try block in the CopyStringToClass() function. This results in a much faster process (it does use a form of internal allocation/de-allocation in the added VarPtr() function. RtlMoveMemory will internally receive the 60 Unicode characters as 60 8-bit characters by the .NET. However. we can completely eliminate the nonheap memory allocation and de-allocation process. We then copy our 60-character string (120-bytes) to that 60-byte memory buffer. Bytes = System. classes. the returned byte count in Cnt would already report Target’s size to be 60 bytes. Dim Bytes() As Byte 'Buffer to hold 8-bit ANSI version of our 16-bit Unicode string Try '60 char string for sample data. We finally marshal it to the structure and then release our allocated storage resource.Pinned) VarPtr = GC.SizeOf(Target)”. objects. having said that. to even set this option. Further. based upon the suggestions in the comments. You can very quickly modify your Assembly information so that the little window that asks you if you trust the application and so allow it to run with administrator privileges will always come up. it should also be sure to check to see if the current user logged on actually has administrative rights. you yourself must already have Administrative privileges. You will see three different options offered within the comments: <requestedExecutionLevel <requestedExecutionLevel <requestedExecutionLevel level="asInvoker" uiAccess="false" /> level="requireAdministrator" uiAccess="false" /> level="highestAvailable" uiAccess="false" /> If you look below this comment area. and make sure the Application tab is displayed. when your application launches it will request that you enable administrative privileges. Doing so then launches Visual Studio. and in-code. or “View Window Settings” under VB2010 and later. You can do this by right-clicking the Visual Studio icon and selecting “Run as administrator”. after you have done this. Page –133– . A quick way to ensure an application that requires Administrator rights will prompt for it. Visual Studio will not start right up.0 – David Ross Goben Upgrading Administrative Rights Checks Running applications that require Administrative Privileges One thing that was driving me crazy when I first started using Visual Studio. When you do this. This is easy to do.NET on Windows Vista (and this applies to subsequent operating systems). but will first greet you with a User Account Control prompt to select the Continue button to resume.Enhancing Visual Basic . but it is one I prefer to avoid as long as possible. instead of expecting your users to perform this application properties adjustment with their own copies of your program. in the Privilege Level box. However. Of course. all without your user having to do anything. you will see the first non-comment line: <requestedExecutionLevel level="asInvoker" uiAccess="false" /> All you need to do. you can also bypass this by simply starting Visual Studio with Administrative rights. especially in protected directory branches. One of the buttons on this tab is labeled “View UAC Settings” (User Account Control) under VB2008. you can do it for them. because otherwise you will have to restart Visual Studio the first time you test-run you app from the IDE (simply restart with Administrative Privileges). is change “asInvoker” to “requireAdministrator”. was that some applications that must access the protected parts of the registry or else add or delete protected files. such as a registry-update utility. However. Go to your project’s properties (Project / Properties from the menu). Cancel of course exits without launching Visual Studio. in order for your own code to even consider running if it requires such administrative rights. Of course. you will not be re-prompted until after you leave VS and start a new VS session with your app. NOTE: This is one of the last things that you should do. is to right-click the file. It is not really a big pain. On its Compatibility tab. In the middle of the displayed text file is a large comment section (by default it is Green) headed by “UAC Manifest Options”.NET Beyond the Scope of Visual Basic 6. and then select Properties. or a shortcut to it. place a checkmark in the “Run this program as an administrator” option. Select it. required administrator rights to do so. Once you have saved and compiled this change. such as installing software.CurrentPrincipal. if opened.NET Beyond the Scope of Visual Basic 6.User.Principal or instantiate any objects. you could actually transpose the function expression right to within your test. you are in fact able to turn this dialog off so that it will always assume that your answer is YES or ALLOW and that it does have administrator permission. so get to it! How do I do it?” There are four ways of doing it: through the Control Panel. such as “If My. the test has been reduced to a single expression: Module modCheckAdminPrivilages 'Add a terminating backslash to a drive/path if required. My original code follows here: Option Strict On Option Explicit On Imports System. Having read the above.IsInRole("Administrators") Then MsgBox("You have privilages!")”. will be more than happy to offer you the option to allow it to turn UAC back on for you. “OK. through you User Account Settings. they also suggest that you turn it back on as soon as possible. If you do turn it off. you will see a constant reminder that it is off in the notification area of your status bar. you may already be asking the question. Running Applications Requiring Administrative Rights without Being Prompted User Account Control (UAC) can help you prevent unauthorized changes to your computer. Microsoft does not recommend you turn UAC off. you had to do a few things to check to see if the current user had administrator privileges. if dealing with the UAC dialog box gives you too much stress. As a result. or simply is the most annoying thing you have ever encountered in your life on your computer.IsInRole("Administrators") End Function End Module As you can see. changing settings that affect other users. Having said that. Page –134– . NOTE: If you do turn UAC off. or writing to protected memory. Also remove '******************************************************************************** ' modCheckAdminPrivilages: check current User for Administative Privilages ' ' CheckAdminPrivilages(): return True if User has Administrators Privilages '******************************************************************************** Public Function CheckAdminPrivilages() As Boolean Return My. which.Principal Module modCheckAdminPrivilages 'Add a terminating backslash to a drive/path if required.Security.IsInRole("Administrators") End Function End Module However.0 – David Ross Goben Check for the User having Administrative Rights Previous to the introduction of VB2005’s gratefully welcomed “My” Namespace.CurrentPrincipal.Security. It works by prompting you for permission when a task requires administrative rights.Enhancing Visual Basic . through MSCONFIG. or through editing the Registry using RegEdit. I had written a small module that I include in my applications that needed to check this. with the introduction of the “My” namespace.GetCurrent()) Return principal. describing it as some sort of problem that must be fixed (I thought the UAC popping up all the time WAS the problem). It does this by verifying that confirmation is coming from your physical keyboard or mouse.User. However. all without needing to import System. Also remove '******************************************************************************** ' modCheckAdminPrivilages: check current User for Administative Privilages ' ' CheckAdminPrivilages(): return True if User has Administrators Privilages '******************************************************************************** Public Function CheckAdminPrivilages() As Boolean Dim principal As New WindowsPrincipal(WindowsIdentity. 5.NET Beyond the Scope of Visual Basic 6. you would be taken to the User Account Control Settings dialog box. then scroll down and put a checkmark in the Run command option). 3. Click User Accounts and Family Safety (or click User Accounts. Click User Accounts. 6.Enhancing Visual Basic . Go to the location: HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System 4. (this will take you to the above dialog box). 5. Clear the check box for Use User Account Control (UAC) to help protect your computer.1 and later User Account Control dialog is: “Change how these notifications appear”. Page –135– . select Properties. That being said. 6. 5. stop right here and use one of the above methods. Or. and then click OK. Disabling User Account Control through the Control Panel On MSDN. Click the Tools tab. Click the Start button 2. select the lowest setting and choose OK. Microsoft usually recommends that you do it through the Control Panel: 1. 5. If Windows 7/8. Reboot your system. Click Control Panel. Disabling User Account Control through Editing the Registry The last resort is to modify the registry using the RegEdit utility (Warning – for advanced users only). NOTE: MSCONFIG also allows you to easily reboot your system. Click your user icon. Exit RegEdit. 2. Launch RegEdit. Select the Customize button. 3. 3. this should be done by people who know what they are doing. Enter MSCONFIG. If you were to select it. Click the 4. right-click the Start Button.0 – David Ross Goben Disabling User Account Control through your User Account Settings I think the easiest way to do it is through your User Account Settings: 1. an option in the lower-right corner on the Windows 8. 3. and click the Disable UAC (Windows Vista) or Change UAC option. Disabling User Account Control through MSCONFIG An easier way to do it is to use MSCONFIG (sadly lacking on Windows XP). . Scroll down to. simply type -R. Click the Start button 2. 7. Reboot your system. 1. Reboot your system. Click the 6. you may want to be sure that you have made a valid backup of your registry. 4. If you do not know how to do this. Turn User Account Control on or off option. Like I said. if you are connected to a network domain). Reboot your system. Bring up the Run menu from the Start menu (you must have this option enabled in the Customize Start Menu options – if not. Click the Launch button. Do the following: 1. and then click OK. Change the value of the EnableLUA entry from 1 to 0. 2. . Turn User Account Control on or off option. where you can safely change its priority level. Clear the check box for Use User Account Control (UAC) to help protect your computer. Before making any changes. 4. such as to or away from Safe Mode without having to tap away on the F8 key like a woodpecker during a reboot in order to bring up the system boot options. 15 If you have not placed a shortcut to NotePad onto your QuickLaunch bar. The first step is to create a new Windows Form Application. much as a TreeView’s Node can be used as a root.0 – David Ross Goben Adding Run-Time Custom Menus and Cloneable ToolStripMenuItems to VB. with the default and more powerful strip versions.NET.NET (though this article will show you how to easily do exactly that). We will explore both of these methods. which you can actually paste directly into the File Explorer Address Bar and it will be accepted! Page –136– .NET by default uses a much more powerful MenuItem object to represent each member of a menu tree. a branch. and doing it all in just a matter of seconds! Understanding Menus by Converting a MenuStrip to a MainMenu I am going to first show you how to go through the troublesome task of converting a MenuStrip menu into a MainMenu and then show you how much easier it is to instead create Main Menus and Context Menus from scratch. Even so.NET. However. but from it I was able to gain invaluable experience and wholly abandoned this process to instead create my menus from scratch without bothersome pre-built menu templates. My first. a singular Menu object was used for every member in a menu tree. you are not able to do so with the more robust MenuStrip and ContextMenuStrip. that is not quite true. and it could even be used as a main menu or as a context menu.NET main menu and context menu construction. a context menu was actually a menu item.NET ‘incompatibility’ with VB6 by using the CloneMenu() method of a MenuItem object to create a copy of it and its children so you can share a full menu construct between a MainMenu and a ContextMenu.NET Beyond the Scope of Visual Basic 6. you will be able to take full advantage of the CloneMenu method. much as you might copy strings or clone arrays. The solution to this dilemma is through either copying or cloning them. you will have to go through typical menu construction channels to construct them (we will return to address this issue soon. Also. unlike the ancient Menu object. In fact. After all. where we will eliminate this supposed ‘difficult’ tasks). which can be done quite simply at run-time. This process ended up being a bit complicated. We will also be grabbing data from it and storing it in NotePad15. Please also consider placing a copy of its shortcut into your SendTo folder so that you can right-click a file and send it directly to Notepad by using the Send To menu.NET cannot use top-level menus as context menus in any way similar to how we might have done it under VB6. the default menu interfaces offered by VB. but not both at the same time. Name it MenuTesting. A MenuSrip and ContextMenuStrip are entirely different objects. we can minimize and even erase issues with on-the-fly VB. a ToolStripMenuItem can only be rooted to a MenuSrip or to a ContextMenuStrip object. their members cannot be encapsulated by more than one object because data safety would otherwise be compromised. the very same Menu control instance could be used simultaneously in a main menu and in a context menu.NET Online gurus (and especially VB6 critics of VB. By first learning how to convert a MenuStrip into the simpler MainMenu format. as you are about to discover. shortly. With this we do not have to bother with cloning – it will so it for us – and we can employ main menu items as context menus just as easily as we did under VB6. NOTE: If we read the menu documentation.NET) will tell you that VB. So please follow along in VB. CloneMenu and MergeMenu. VB.Enhancing Visual Basic . and MenuItem objects. Otherwise. we can much better understand general menu mechanics. Also. Were you to fabricate your menus using MainMenu. It is located at %APPDATA%\Microsoft\Windows\SendTo. From this I could grab ready-made menu members and experiment with how to employ them as popup menus. clumsy attempt to resolve this perceived huge problem was to manually convert an exiting MenuStrip to a MainMenu. However. or a leaf. It is proclaimed by many gurus that you can get around such VB. you will find NotePad at Start / All Programs / Accessories (or Programs / Windows Accessories). there. which is not possible in a well-behaved OOPL like VB. Also. we will discover the MergeMenu method of a MainMenu and ContextMenu. Using top-level menus as context menus under VB6 was a by-product of the much simpler design employed by early versions of Microsoft Windows. because we will be using that project later in this article. With existing and created tools from our digital toolbox. ContextMenu. Enhancing Visual Basic .EditToolStripMenuItem = New MenuItem Me.PasteToolStripMenuItem = New MenuItem Me. For “Exit”.vb.MenuStrip 'change this to: Me.AddRange(New System.DesignerGenerated()> _ Partial Class Form1 Inherits System.mnuMain.AddRange" 'Change "New System.Forms.Windows. place a check in the Alt checkbox. “Ctrl+C” for Copy.Diagnostics. 20) Me.Text = "&File" ' 'EditToolStripMenuItem ' 'Change "Me.0 – David Ross Goben Next.Forms.Size = New System. Me.FileToolStripMenuItem = New System.mnuMain.EditToolStripMenuItem.Dispose() End If Finally MyBase.Text = "MenuStrip1" ' 'FileToolStripMenuItem ' 'Change "Me.AddRange" 'Change "New System.ComponentModel.NET Beyond the Scope of Visual Basic 6.Windows. Under “Edit”. Click the Plus sign beside Form1 in the Solution Explorer to open up its now-visible ‘directory tree’ and open Form1.EditToolStripMenuItem = New System.VisualBasic.Items.DebuggerNonUserCode()> _ Protected Overrides Sub Dispose(ByVal disposing As Boolean) Try If disposing AndAlso components IsNot Nothing Then components.Forms.Windows.Forms. I have highlighted and commented it heavily in orange to clarify the edits that we will need to perform in Notepad.EditToolStripMenuItem.Designer.Diagnostics. I like to use “Alt+F4”.PasteToolStripMenuItem}) Me.Windows.ToolStripItem()" below to "New MenuItem()" Me. Then.Size = New System.EditToolStripMenuItem.AddRange" below to "Me.ToolStripItem()" below to "New MenuItem()" Me. at least for now. Next.Windows.mnuMain = New MainMenu Me. Along the menu ribbon. Save it. 0) Me. 24) Me.Windows.AddRange" 'Change "New System.mnuMain.Size(37. add a MenuStrip to the form.CopyToolStripMenuItem. you can add Shortcut keys that will display on the right side of the menu.Forms.PasteToolStripMenuItem = New System.AddRange(New System.AddRange" below to "Me.Size(284.mnuMain = New System.Location = New System.Forms.mnuMain.Windows. <System.SuspendLayout() Me. 'Do not modify it using the code editor.ToolStripItem() _ {Me. and “&Paste”.ToolStripMenuItem 'change this to: Me.mnuMain.Name = "FileToolStripMenuItem" Me. So in the ShortcutKeys property of the “Exit” entry (actually automatically named ExitToolStripMenuItem).Windows.Drawing. and I also struck through lines to delete): <Global.EditToolStripMenuItem. and from the dropdown list select F4.TabIndex = 0 Me. I also like to add “Ctrl+X” for Cut. <System.DebuggerStepThrough()> _ Private Sub InitializeComponent() Me.DropDownItems. in the Solution Explorer.FileToolStripMenuItem. and “Ctrl+V” for Paste. Rename the default MenuStrip1 to mnuMain.DropDownItems.ToolStripMenuItem 'change this to: Me.Forms.ToolStripMenuItem 'change this to: Me.ToolStripItem() {Me.ToolStripItem()" below to "New MenuItem()" Me.Windows.ToolStripMenuItem 'change this to: Me.Forms.Point(0.DropDownItems. and then close the Shortcutkeys property’s dropdown. add “E&xit”.CompilerServices.CopyToolStripMenuItem = New MenuItem Me.Windows.FileToolStripMenuItem.ExitToolStripMenuItem}) Me.mnuMain.Drawing.Forms.Name = "mnuMain" Me.Windows. We will use it later when I show you how to create a ContextMenuStrip from a MainMenuStrip. Under “File”.mnuMain.mnuMain. Me.FileToolStripMenuItem = New MenuItem Me. add “&Cut”. What you will see is a slew of code that should similar to the following (well.Form 'Form overrides dispose to clean up the component list.ExitToolStripMenuItem = New MenuItem Me.NET automatically generates for us. the code that VB.EditToolStripMenuItem}) Me.CutToolStripMenuItem.ToolStripMenuItem 'change this to: Me. we will add some menu items.CutToolStripMenuItem = New MenuItem Me.Microsoft. add “&File” and “&Edit”.AddRange(New System.Forms.Dispose(disposing) End Try End Sub 'Required by the Windows Form Designer Private components As System.CopyToolStripMenuItem = New System. “C&opy”. Me.Forms. We have all that we need from this project.ToolStripItem() {Me.MenuItems.Drawing.Name = "EditToolStripMenuItem" Page –137– .Items.MenuItems.ToolStripMenuItem 'change this to: Me. NOTE: Save this project.DropDownItems. Optionally.Windows.IContainer 'NOTE: The following procedure is required by the Windows Form Designer 'It can be modified using the Windows Form Designer.FileToolStripMenuItem. the application close command.Forms.CutToolStripMenuItem = New System.Forms.FileToolStripMenuItem.AddRange" below to "Me. make sure the “Show All Files” button is selected so we can access our Form Designer code.FileToolStripMenuItem.Windows.MenuItems.FileToolStripMenuItem.FileToolStripMenuItem.Forms.ExitToolStripMenuItem = New System.SuspendLayout() ' 'mnuMain ' 'Change "Me.Windows. Notice that “Alt+F4” now appears alongside “Exit” in the menu.mnuMain. Windows.FileToolStripMenuItem. System.CopyToolStripMenuItem.Size(152.Text = "C&opy" ' 'PasteToolStripMenuItem ' Me.Size = New System. Finally.Windows.ResumeLayout(False) Me.mnuMain) ' change the following line to: Me.ShortcutKeys = _ CType((System.CutToolStripMenuItem = New MenuItem Me.V).CutToolStripMenuItem.Keys.Shortcut = Shortcut.mnuMain Me.Windows.Text = "&Paste" ' 'Form1 ' Me.Windows.mnuMain.Windows.Drawing.ExitToolStripMenuItem.Drawing.Drawing.CopyToolStripMenuItem. perhaps better.MenuItem() {Me.Forms.Text = "&Cut" ' 'CopyToolStripMenuItem ' Me.Shortcut = Shortcut.Forms.Keys.MainMenuStrip = Me..Windows.Forms..Enhancing Visual Basic .EditToolStripMenuItem}) ' 'FileToolStripMenuItem ' Me.Windows.CtrlC Me.Windows.Drawing.CtrlX Me.MenuItem() {Me.AutoScaleDimensions = New System.Windows.Forms.Windows.Forms. in Notepad.Name = "Form1" Me.Forms.EditToolStripMenuItem = New MenuItem Me.Forms. or.Add(Me.CutToolStripMenuItem.PasteToolStripMenuItem.Text = "Form1" Me.PasteToolStripMenuItem.ExitToolStripMenuItem.ToolStripMenuItem CopyToolStripMenuItem As System.Forms.Forms..EditToolStripMenuItem.Windows.PerformLayout() End Sub Friend WithEvents Friend WithEvents Friend WithEvents Friend WithEvents Friend WithEvents Friend WithEvents Friend WithEvents End Class mnuMain As System. 22) Me.ShortcutKeys = _ CType((System. System. 22) Me.Windows. 22) Me.Size(152.ToolStripMenuItem PasteToolStripMenuItem As System.Font Me.PasteToolStripMenuItem = New MenuItem ' 'mnuMain ' Me.MenuStrip" to "MainMenu" 'change "System.CutToolStripMenuItem.EditToolStripMenuItem.mnuMain.ShortcutKeys = _ CType((System. 22) Me. apply the edits I have annotated in the highlighted areas.Windows.Forms.ExitToolStripMenuItem}) Me.Windows.Drawing.ExitToolStripMenuItem.CopyToolStripMenuItem.Size(39.ExitToolStripMenuItem.Forms.AddRange(New System.Control Or System.Size(284.Control Or System.MenuStrip FileToolStripMenuItem As System.ResumeLayout(False) Me.Text = "&Edit" ' 'ExitToolStripMenuItem ' Me.Size = New System.CutToolStripMenuItem.Shortcut = Shortcut. simply copy my resulting data.NET Beyond the Scope of Visual Basic 6.Text = "&File" ' Page –138– .MenuItems.Forms.Name = "ExitToolStripMenuItem" 'Change the whole long line below to: Me.Forms.ToolStripMenuItem ExitToolStripMenuItem As System.PerformLayout() Me.0!.Size = New System.Forms.Forms.Controls.Keys) Me. Me.Keys.Keys.Windows.Keys.PasteToolStripMenuItem.Windows.0!) Me.Shortcut = Shortcut. shown above.Size = New System.0 – David Ross Goben Me.AddRange(New System.Size(152.Name = "PasteToolStripMenuItem" 'Change the whole long line below to: Me. shown below: Me. System.FileToolStripMenuItem = New MenuItem Me.Forms.X).PasteToolStripMenuItem..ToolStripMenuItem 'change "System.Menu = mnuMain Me.Name = "CutToolStripMenuItem" 'Change the whole long line below to: Me.Drawing.Keys) Me.Forms.AutoScaleMode.mnuMain. 13. System.Size(152.CopyToolStripMenuItem.ExitToolStripMenuItem = New MenuItem Me.Alt Or System.FileToolStripMenuItem.Windows.AltF4 Me.Keys) Me.Windows.Forms. delete all the StrikeThrough lines.ShortcutKeys = _ CType((System.MenuItems.Windows.ToolStripMenuItem EditToolStripMenuItem As System.C).Windows.CopyToolStripMenuItem = New MenuItem Me.ToolStripMenuItem CutToolStripMenuItem As System.Keys) Me. 264) Me.Forms.CopyToolStripMenuItem.CutToolStripMenuItem.SizeF(6.Keys.Drawing.Text = "E&xit" ' 'CutToolStripMenuItem ' Me.FileToolStripMenuItem.Forms.Windows.ExitToolStripMenuItem.ClientSize = New System. Copy this entire file and paste it into NotePad.Keys. 20) Me.PasteToolStripMenuItem. Next.Keys.Name = "CopyToolStripMenuItem" 'Change the whole long line below to: Me.Control Or System.AutoScaleMode = System.Windows.mnuMain = New MainMenu Me.ToolStripMenuItem" to "MenuItem" 'ditto 'ditto 'ditto 'ditto 'ditto Notice I have added quite a number of editing instructions in Orange Text to the above raw form data (I also added continuation tags for the lines that ran over).CtrlV Me.Forms.F4).Size = New System.Forms. Our Form1.EditToolStripMenuItem.Windows.mnuMain = New MainMenu 'instantiate new event-supported objects for use in our app Me.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.CopyToolStripMenuItem.CutToolStripMenuItem = New MenuItem 'Friend WithEvents FileToolStripMenuItem As New MenuItem.FileToolStripMenuItem. EditMenu.Windows.CopyToolStripMenuItem.Shortcut = Shortcut. but is not necessary.FileToolStripMenuItem. double-click the form to go to the Form1_Load event. Me. if we exercise our gained knowledge.PasteToolStripMenuItem. and PASTE items to the Edit menu item) ' Me. I use names such as FileMenu. as well as to show you that building MainMenu objects is much simpler and requires less code than MenuStrip objects (much less.CutToolStripMenuItem.MenuItem() _ {Me.PasteToolStripMenuItem = New MenuItem ' 'mnuMain (attach File and Edit menu items to the Main Menu) ' Me.Forms. A WithEvents declaration CAN be made.CutToolStripMenuItem.Enhancing Visual Basic . Me. copy the rest of the other data above and paste it to within the Form1_Load event code.PasteToolStripMenuItem. Friend WithEvents CopyToolStripMenuItem As MenuItem 'and the like. Friend WithEvents mnuMain As New MainMenu.PasteToolStripMenuItem}) Me. Try it! Me. Me. and paste them above the Form1_Load event. though perhaps a bit tiring.Text = "E&xit" ' 'CutToolStripMenuItem ' Me.ExitToolStripMenuItem}) Me.MenuItems.Text = "&File" ' 'EditToolStripMenuItem (attach submenu CUT. for Friend WithEvents PasteToolStripMenuItem As MenuItem 'every MainMenu and each main menu item or submenu item.mnuMain.EditToolStripMenuItem = New MenuItem 'NOTE: if we had declared each of the above 'Friend WithEvents' declarations Me.PasteToolStripMenuItem}) Me. the edits we applied to the MenuStrip data to convert them to MenuItems was not too difficult.Text = "&Edit" ' 'ExitToolStripMenuItem (define the additional features for the Exit submenu item) Page –139– . Private Sub Form1_Load(ByVal sender As System.0 – David Ross Goben 'EditToolStripMenuItem ' Me. or Me.FileToolStripMenuItem.EditToolStripMenuItem. so we can take Friend WithEvents FileToolStripMenuItem As MenuItem 'advantage of associated events from the drop-down menus.MenuItem() {Me.FileToolStripMenuItem = New MenuItem Me.Shortcut = Shortcut.AddRange(New System.CutToolStripMenuItem.MenuItem() {Me.Forms. Friend WithEvents EditToolStripMenuItem As MenuItem 'When I construct main menus from scratch.ExitToolStripMenuItem.MenuItems. but NOT for context Friend WithEvents ExitToolStripMenuItem As MenuItem 'menus.AddRange(New System.CtrlC Me. COPY.NET Beyond the Scope of Visual Basic 6.Object. ByVal e As System.ExitToolStripMenuItem = New MenuItem 'AS NEW (for example.Windows. In the above listing.AltF4 Me.CopyToolStripMenuItem.CopyToolStripMenuItem = New MenuItem 'instantiation declarations would have been totally unecessary.MenuItems. Me. we are ready to create a new project to take full advantage of our work.Forms.Shortcut = Shortcut.MenuItem() _ {Me.AddRange(New System. Create a new Form App.Text = "&Edit" ' 'ExitToolStripMenuItem ' Me.CtrlV Me. noted shortly). but it does clearly show what is required for MainMenu construction.EditToolStripMenuItem}) ' 'FileToolStripMenuItem (attach EXIT submenu items to the File menu item) ' Me. Next.CutToolStripMenuItem.CopyToolStripMenuItem. as you will soon learn.Windows. the WithEvents verbs in the lines below are not really necessary Friend WithEvents mnuMain As MainMenu 'These declarations are REQUIRED for form-level construction.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 you can see.EventArgs) Handles MyBase.Forms.Text = "&Cut" ' 'CopyToolStripMenuItem ' Me. Me. copy the last 7 lines (all beginning with “Friend WithEvents”).MenuItems. We could have also declared each of these'As New'.Load Me.EditToolStripMenuItem. then these latter Me.Shortcut = Shortcut. I use much simpler names than those Friend WithEvents CutToolStripMenuItem As MenuItem 'created for us by the MenuStrip. Once loaded.CtrlX Me.EditToolStripMenuItem.ExitToolStripMenuItem. Armed with the above text.Text = "C&opy" ' 'PasteToolStripMenuItem ' Me.Text = "&Paste" ' 'Form1 ' Me.AddRange(New System. Exit()”. So we type “Handles”.Enhancing Visual Basic .Shortcut = Shortcut. Copy. ByVal e As EventArgs) Handles CopyToolStripMenuItem. so type into the body “Me. then select from its context menu the event we want the new subroutine to handle.ExitToolStripMenuItem. 3 parts to build the event declaration (name.Text = "&Cut" ' 'CopyToolStripMenuItem (define the additional features for the Copy submenu item) ' Me. such as “Exit_Click”. parameters. we will name our events Exit_Click. ByVal e As EventArgs)”. add code for other required menu-selection events (Cut.Close() End Sub Next. You should see the form come up.Text = "&Paste" ' 'Form1 (finally attach our newly constructed Main Menu to the form) ' Me.0 – David Ross Goben ' Me. so do not be concerned).PasteToolStripMenuItem.ExitToolStripMenuItem. and the 4th part to build the event body.AltF4 Me. run the program as-is.Click End Sub NOTE: Because these controls are declared “WithEvents”.Close()” or even “Application.Click MsgBox("Paste") End Sub Now test the above code.Shortcut = Shortcut.CutToolStripMenuItem. complete with a menu.Shortcut = Shortcut. The empty body of our event code should look like this: Private Sub Exit_Click(ByVal sender As Object. We usually begin an event declaration with “Private Sub”. press the Return key (this events recognition is thanks to declaring the menu members “WithEvents”). type a dot. which you can open and move around in (we do not have any event code to support them yet. Cut_Click.Shortcut = Shortcut. ByVal e As EventArgs) Handles CutToolStripMenuItem.CtrlC Me.CtrlX Me. Finally we decide what event this code should handle. Now we need to enter some code to react to the click. then the event name. and we could even name the Exit_Click event Bob. Copy_Click. We are usually concerned with only Click events. and then select the “Click” event. Lastly. Our event code will now look like this: Private Sub Exit_Click(ByVal sender As Object.Text = "C&opy" ' 'PasteToolStripMenuItem (define the the additional features for the Paste submenu item) ' Me.CutToolStripMenuItem. Please note that the “_Click” part is not required.CopyToolStripMenuItem. to make sure everything is working properly. For this event we simply want to exit the program.NET Beyond the Scope of Visual Basic 6.Text = "E&xit" ' 'CutToolStripMenuItem (define the additional features for the Cut submenu item) ' Me.Click MsgBox("Copy") End Sub Private Sub Paste_Click(ByVal sender As Object. To make things simple.PasteToolStripMenuItem.Click MsgBox("Cut") End Sub Private Sub Copy_Click(ByVal sender As Object. we could have written the handing code with the MenuStrip project). and Paste_Click. We will add event code manually (well. and Paste). such as “ExitToolStripMenuItem”. without event handler code for the menu items.CopyToolStripMenuItem.CtrlV Me. type a space. but the VB6-style name simply makes the most sense and is also descriptive of what its purpose is. then the standard parameter list “(ByVal sender As Object. and optional handlers).Click Me. 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. ByVal e As EventArgs) Handles PasteToolStripMenuItem. We construct such events in 4 parts. ByVal e As EventArgs) Handles ExitToolStripMenuItem. If no syntax errors were reported. Just display a message: Private Sub Cut_Click(ByVal sender As Object. Page –140– .Menu = mnuMain End Sub End Class We are now ready to test the code. ByVal e As EventArgs) Handles ExitToolStripMenuItem. and much more compact version that provides everything that the previous code did: Private Sub Form1_Load(ByVal sender As System.MouseButtons.AddRange(New MenuItem() {ExitMenu}) EditMenu.Object. Building ContextMenus from MainMenu Items Now suppose we want to right-click the screen and pop up the Edit menu contents. If we use a MouseDown event.CtrlV) 'Link the parent menus File and Edit to their associated submenu items.MenuItems. ByVal e As MouseEventArgs) Handles Me.EventArgs) Handles MyBase. select “(Form Events)” and “MouseDown” from the two menus at the top of the code form.Forms. With our code for Form1 up. and a second at the end of the click event methods. not required 'Build Edit menu items Dim EditMenu As New MenuItem("&Edit") 'Edit header item Dim CutMenu As New MenuItem("&Cut".CloneMenu) 'clone the EDIT menu (Cloning makes new duplicates.MenuItems. we will display our Edit menu. New EventHandler(AddressOf Copy_Click).Show(Cursor.AddRange(New MenuItem() {CutMenu. enter the following code. otherwise the events will actually fire the event code twice in a row.MouseDown If e. we will check to see if the right mouse button is down. New EventHandler(AddressOf Cut_Click).0 – David Ross Goben Optimizing Menu Design Code Be aware that we could have made our code much shorter. ByVal e As EventArgs) Me. Shortcut.Menu. ByVal e As EventArgs) MsgBox("Paste") End Sub Run it after updating the code to test the results. Let’s try it using the MouseDown event.Right Then 'if the mouse-down was with the right button. CopyMenu.Close() End Sub Private Sub Cut_Click(ByVal sender As Object. then when this event is triggered. if we want to. totally eliminating ' the need to maintain references to these now-unused original objects in the more persistent form class body as we did before. Shortcut.. which we now no longer need: Private Sub Exit_Click(ByVal sender As Object. because the events are now assigned their event handlers from within the above new code. and all in one instruction line. because when we instantiate MenuItem objects we can also simultaneously give them text data. If so. handlers. End Sub Page –141– .Menu. storing these added references in presistent memory.NET Beyond the Scope of Visual Basic 6. one in the menu item declaration. New EventHandler(AddressOf Exit_Click). 'This also clones all the locally-created objects. eliminating the need for one or two extra lines of code as had been done in the above translated program.AddRange(New MenuItem() {FileMenu. Dim cMenu As New ContextMenu 'declare a new context menu cMenu. an optional image. With an empty Form1_MouseDown code block opened up. So here are our event methods without their trailing handlers being assigned directly to them. rather than mnuMain) Me. because there are presently two handlers declared. 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. much shorter.Menu. ByVal e As EventArgs) MsgBox("Cut") End Sub Private Sub Copy_Click(ByVal sender As Object. making the event code look like this: Private Sub Form1_MouseDown(ByVal sender As Object. PasteMenu}) 'Finally. FileMenu.Position) 'show the EDIT menu (MenuItem(1)) with top-left at the mouse pointer location End If 'NOTE: The above cMenu.Menu = New MainMenu 'Notice that we can construct a new main menu right within the form’s Menu reference.MenuItems. ByVal e As System. 'Build File menu items Dim FileMenu As New MenuItem("&File") 'File header item Dim ExitMenu As New MenuItem("E&xit". an optional shortcut key. EditMenu}) 'all locals are now referenced & linked to the main menu structure End Sub Also. and any submenus) cMenu.AltF4) 'shortcuts are optional.Add(Me. link the main menu to the two subordinate menu headers File and Edit (note that we now use Me. In order to do that we will either need to add a MouseDown event on Form1 or construct the context menu and apply it as the form’s current default context menu. Shortcut.CtrlX) Dim CopyMenu As New MenuItem("C&opy".Button = Windows. Shortcut.MenuItems.Enhancing Visual Basic .CtrlC) Dim PasteMenu As New MenuItem("&Paste".. ByVal e As EventArgs) MsgBox("Copy") End Sub Private Sub Paste_Click(ByVal sender As Object. Consider the following new.Load Me.MenuItems(1).Show() example will work even if you have form controls under the cursor (specified by sender). New EventHandler(AddressOf Paste_Click). an event handler. elsewhere. including event handler) Next 'do all of them With the above change.Menu = New MainMenu” or “Me.Add("Paste". but you can instead place it anywhere that you really need it. New EventHandler(AddressOf Cut_Click)) Me. changing it as often as you require to suit the needs of your application.Menu.MenuItems(1)) Me.ContextMenu = cMenu 'declare a new context menu 'merge Edit menu's items into the context menu (EditMenu reference still active here) 'set as default form context menu NOTE: Be sure to also remove or comment out the Form1_MouseDown event code if you do the above. Otherwise. if you have already written your handling subroutine methods. This is OK.ContextMenu.0 – David Ross Goben This “on-the-fly” approach copies the edit menu item itself.Menu.ContextMenu. Consider the following quick context menu construct.Add("Cut". saving resources.Menu. being more of a “roll-your-own” version. Still another way to create context menus is to construct them on the fly and directly to the form’s menu controls.MenuItems. With it you can reduce the above replacement code back to a single line again.ContextMenu.ContextMenu = New ContextMenu 'Notice that we can create the form’s new context menu right within it’s ContextMenu reference Me. It only involves replacing one line with three very easy lines of code.ContextMenu = Nothing. or “Me.Clear()”.MenuItems(1). New EventHandler(AddressOf Copy_Click)) Me. is to employ the MergeMenu method. However.Menu. you can instead simply replace the “Me. Plus.ContextMenu = New ContextMenu” with “Me.Clear()”.MenuItems.Menu.Menu = New MainMenu” with “Me.MergeMenu(Me. to avoid conflicts.Add("Copy". it is super-easy. such as in your Form_Load event.ContextMenu. then you should also be sure to set Me. consisting of: cMenu. not requiring the presence of a main menu at all: Me. All you need to do is add the following code to the bottom of the Form1_Load event code: Dim cMenu As New ContextMenu cMenu. but most people instead want the sub-items of the menu only.NET.Add(itm. a more elegant way to display this same result. but with a slight increase in speed. New EventHandler(AddressOf Paste_Click)) NOTE: If you have already added a new menu or context menu to your form. instead of creating a new MainMenu or ContextMenu object each subsequent time.MergeMenu(Me.ContextMenu.MenuItems. Notice further that you do not actually need to set this short snippet of code in the Load event. when you want to add a new menu or context menu. Page –142– . to emulate exactly what VB6 had done when using main menu items as a context menu. when you show your mouse-triggered menu.MenuItems 'scan each sub-item in the Edit menu (Me. NOTE: If you had added a context menu via Me. along with its subordinate objects.Menu. because you had simply cleared the main menu and context menu objects without removing them and replacing them with newly instantiated ones.CloneMenu) 'add an item (Cloning creates a new duplicate. and to bypass manually implementing the cloning process altogether. 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. or even added a blank menu or context menu by placing only “Me.ContextMenu but later wish to use context menus from right-mouse click events.MenuItems. a context menu previously assigned to Me.Clear() or set Me. the “Edit” portion of the menu will not be displayed.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.MergeMenu(Me. and there is likely some use for that. which you can place anywhere in your code where you need it: Me.ContextMenu will also be displayed. we could easily enumerate through the sub-menu items and clone them.MenuItems(1)) 'merge/clone the Edit menu's submenu items into the new context menu Or try this. It is so easy that it is hardly worth all the fuss that so many VB6 users charge against VB.ContextMenu = New ContextMenu 'Note that we can create the form's new context menu right within it's ContextMenu reference Me.MenuItems.ContextMenu = New ContextMenu” within your Form_Load event. You can then go on about adding menu items as before.MenuItems(1)) cMenu.NET Beyond the Scope of Visual Basic 6.MenuItems.Enhancing Visual Basic . To do that.ContextMenu. significantly cut down on processing time. ByVal e As EventArgs) MsgBox("Cut") End Sub Private Sub Copy_Click(ByVal sender As Object. but you certainly get the idea by now: 'Note: Either write the following method first.Button = Windows. You 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.MenuItems.ContextMenu. All that is left for you to explore is adding the bells and whistles.ContextMenu.MenuItems.ContextMenu.MenuItems. on-the-fly. New EventHandler(AddressOf Context_Click)) 'use new Context_Click event handler Me.EventArgs) Handles MyBase.Show(Cursor.Object. and icon images.MenuItems. and they are not even confined to being saddled with any explicit Handles verb at design time.ContextMenu = New ContextMenu Me.Add("Cut". Private Sub Context_Click(ByVal sender As Object.MenuItems. and after the user has already made a right-click on a form. ByVal e As MouseEventArgs) Handles Me. then see how they did their magic in the Designer code in order to add such features to your manually-constructed menus. but instead creating the data for the context menu entirely from scratch. New EventHandler(AddressOf Paste_Click)) ' context menu entries even more. New EventHandler(AddressOf Context_Click)) 'use new Context_Click event handler End Sub If you have not yet realized it. much as shown earlier with the CloneMenu examples. Page –143– .Add("Cut".Add("Paste". cMenu. you may just find that runtime menu construction is the fastest. Further. item visibility.Load Me.Forms. If all else fails.0 – David Ross Goben Alternatively. New EventHandler(AddressOf Context_Click)) 'use new Context_Click event handler Me.Position) 'show the menu with top-left at the mouse pointer location End If End Sub You now have all the requisite knowledge you need to create menus and context menus that do not exist in your application in any other form. being simple subroutines of any name with optional special parameters. New EventHandler(AddressOf Paste_Click)) End Sub End Class Once you have a few custom menus. ByVal e As EventArgs) MsgBox("Paste") End Sub Private Sub Form1_Load(ByVal sender As System. New EventHandler(AddressOf Cut_Click)) 'The Add() method returns an index into the MenuItems cMenu.ContextMenu.. or even an existing menu to create context menus. ByVal e As System.MenuItems. both full and context under your belt. before adding the lines referencing it to the Form1_Load event. build the menus using a test MenuStrip control.Load Me. ByVal e As EventArgs) MsgBox("Copy") End Sub Private Sub Paste_Click(ByVal sender As Object. Just instantiate a ContextMenu object and add the menu features you want to add.. checks. and finally process that ContextMenu. the following raw code can be added to a fresh form and immediate run and tested: Public Class Form1 Private Sub Cut_Click(ByVal sender As Object. For example. easiest solution to your application needs. New EventHandler(AddressOf Copy_Click)) Me.ContextMenu.Add("Copy".Text 'Note: Use DirectCast whenever there is no actual conversion. such as the declaration of a CutToolStripMenuItem object (which are required to construct a main menu). and all constructed in just moments. ByVal e As System. ByVal e As System.Enhancing Visual Basic . Dim cMenu As New ContextMenu 'declare a new context menu cMenu.Add("Cut".MouseButtons.Add("Copy".Add("Paste". ' or wherever you actually need to set up the context menu. you could define a single separate handler method to take care of your context menus. RadioButton checks (a single check allowed for various options). you can also construct context menus on-the-fly.MenuItems. MenuItem).ContextMenu = New ContextMenu Me.Add("Paste". New EventHandler(AddressOf Copy_Click)) ' collection that you could capture and use to enhance cMenu.MenuItems. it is also clear that you do not need to have specific menu objects.EventArgs) Handles MyBase. New EventHandler(AddressOf Cut_Click)) Me.ContextMenu. You can probably also see that event handler code is nothing at all magical or mysterious.Object. to greatly speed processing Case "Cut" MsgBox("Cut") Case "Copy" MsgBox("Copy") Case "Paste" MsgBox("Paste") End Select End Sub Private Sub Form1_Load(ByVal sender As System.MouseDown If e. In this simple example I will in turn just invoke our current behavior.NET Beyond the Scope of Visual Basic 6.Add("Copy". or just its declaration.MenuItems.Right Then 'if the mouse-down was with the right button.EventArgs) Select Case DirectCast(sender. such as shown below: Private Sub Form1_MouseDown(ByVal sender As Object. such as item enabling/disabling. ByVal e As MouseEventArgs) Handles Me. New EventHandler(AddressOf CutMenu_Click))) Cmenu. to construct a ContextMenuStrip that emulates our main menu’s Edit dropdown list of members. we will first resort to building our context menus from scratch. ByVal e As EventArgs) Handles PasteToolStripMenuItem. Nice. MenuTesting.ContextMenuStrip = New ContextMenuStrip. Nothing. Copy. Unlike the ContextMenu object.Add(New ToolStripMenuItem("Cut". and ToolStripMenuItem objects. and then use the Form’s ContextMenuStrip member to build the new ContextMenuStrip. that we would use Me.. Dim Cmenu As New ContextMenuStrip 'The NOTHING below can optionally specify an Image object or Image resource Cmenu.NET Beyond the Scope of Visual Basic 6. It is possible to write code to add cloning into a new object that inherits ToolStripMenuItem. just as we had just done previously with the ContextMenu objects. However. New EventHandler(AddressOf PasteMenu_Click))) cMenu. Copy.NET MenuStrip. But it would be really nice if we could simplify the developer’s job of adding entries to a menu.Items. as shown. Cut.Click Me.Add(New ToolStripMenuItem("Paste". let us look back to the default VB.Right Then 'if the mouse-down was with the right button. We will address that.Click MsgBox("Cut") End Sub Private Sub CopyMenu_Click(ByVal sender As Object. but most examples I have seen will also warn us that they do not always work perfectly. with ToolStripMenuItem objects we will have to construct wholly new controls (rest assured that we will soon eliminate that issue by easily deriving a new ToolstripMenuItem class that can be cloned). ByVal e As EventArgs) Handles CutToolStripMenuItem. ContextMenuStrip. but this is simply due to menu cascading (dropdowns) not being accounted for. ContextMenu. unlike with the much simpler MenuItem object. New EventHandler(AddressOf CopyMenu_Click))) Cmenu..Items. the ContextMenuStrip object lacks any sort of MergeMenu method. and MenuItem objects.ContextMenuStrip instead of Me. as we will soon do. Notice also. but this is because a CloneMenu method is also lacking from ToolStripMenuItem objects. and add the following methods to the form’s code to apply support for its Exit. But I only say that as an enticement.ContextMenu. Nothing. Reload your saved MenuStrip project. and Paste. Nothing.Button = MouseButtons.Add(New ToolStripMenuItem("Copy". and Paste menu items: Private Sub ExitMenu_Click(ByVal sender As Object. constructed “the hard way” (well.0 – David Ross Goben Building ContextMenuStrip PopUp Menus on the Fly Now that we have exploited the MainMenu. Even so.Show(Cursor. and the ContextMenuStrip lacks a MergeMenu method.MouseDown If e. Notice that we specified the 3 ToolStripMenuItem controls for Cut. you could alternatively use Me. ByVal e As EventArgs) Handles CopyToolStripMenuItem. at least I think it is still pretty easy): Private Sub Form1_MouseDown(ByVal sender As Object. 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.Click MsgBox("Paste") End Sub Because our ToolStripMenuItem controls lack a CloneMenu.Click MsgBox("Copy") End Sub Private Sub PasteMenu_Click(ByVal sender As Object. much as we have been doing in the previous examples.Items.Enhancing Visual Basic .Position) 'display and support the new context menu End If End Sub NOTE: Like with ContextMenu objects. because we will soon be doing exactly that! Page –144– . ByVal e As EventArgs) Handles ExitToolStripMenuItem. it is still easy enough to follow the lead of our last several examples and construct context menu strips on the fly.Close() End Sub Private Sub CutMenu_Click(ByVal sender As Object. Nothing. but you may want a diff. Besides.Top 'doc to top of form (default.DropDownItems.NET Beyond the Scope of Visual Basic 6. the very reason for this statement is because it could not cascade dropdown lists for cloned items. and PASTE members EditItem. this does not explain WHY you must add it to the collection. the menu will not display without doing so. Nothing.” However. you must add main menu items to a MenuStrip’s DropDownItems collection. And now you know.Add(myMenuStrip)”. This can sound like a pain. The first thing is that instead of adding your ToolStripMenuItem objects to a DropDown collection as you will with a ContextMenuStrip or to another ToolStripMenuItem. such as “Me.Enhancing Visual Basic . be sure to add your new MenuStrip to the form’s Controls collection.C)) EditItem. which we can then use as a member of a new context menu. thus providing all the full-property fidelity that Microsoft could not somehow guarantee! Page –145– .Events)”.Controls. assign the new MenuStrip to the form’s MainMenuStrip. but there is a small catch.MainMenuStrip = mMenu 'assign to Main Menu Strip Me.0 – David Ross Goben Building MenuStrips on the Fly To build a MainMenuStrip is just as easy as building a ContextMenuStrip.Events. Without adding your MenuStrip to the Controls collection.Control Or Keys. but now that you know how to easily construct menus on-the-fly.Control Keys. AddressOf CutMenu_Click. and so the 3 remaining lines are able to recurse through however many children and generations to fully clone the complete object. because what we need to do is just assign the clone’s Events list from the Events list of the item being cloned. you must add the MenuStrip control to the Controls collection of the form. But that statement strikes me as really bizarre. even though the documentation for the MainMenuStrip clearly reports in its terse remarks. EditItem}) 'Add FILE and EDIT menu items to the Main Menu . Keys.Control Or Keys.Add(New ToolStripMenuItem("Cut". as usual.AddRange(New ToolStripMenuItem() {FileItem. Were we just able to do that. this is more of a “roll-your-eyes and heave a sigh” sort of pain. and two of those lines are the IF-block to determine if the item has dropdown items. For example: Dim mMenu As New MenuStrip 'create a new MenuStrip With mMenu Dim FileItem As New ToolStripMenuItem("&File") 'Build the FILE member of the Main Menu FileItem. you will not see it! This is another extremely important point that has aggravated a lot of programmers. Nothing. add it last for proper Z-Order display Cloning MenuStrip Items with a Cloneable ToolStripMenuItem Class We can implement a class that will create a clone of tool strip menu items for us. Jessica at the Microsoft Dev Center explained “ToolStripMenuItem is quite a bit richer than MenuItem. Currently we are not able to clone event handlers contained within a ToolStripMenuItem because it’s Events list property is unexposed (protected) except to a derived class. Nothing. we could easily clone a ToolStripMenuItem without any need whatsoever for constructing a derived class to access it. but you must also do two simple things: First. What this means is that we cannot clone a ToolStripMenuItem directly. which is just this simple line of code: “clonedItem.Add(New ToolStripMenuItem("Copy". Second. It bugs me that we cannot access the Events list of a ToolStripMenuItem. such as “Me.Controls. but it does say that you must do it. yet we are able to do so in a derived class.Add(mMenu) 'add to form controls so we can actually see it! Also. “In addition to setting the MainMenuStrip property.Items. but with a couple different steps.X)) EditItem. and we couldn't guarantee full-property fidelity when cloned. AddressOf ExitMenu_Click)) Dim EditItem As New ToolStripMenuItem("&Edit") 'Build the EDIT member of the Main Menu 'Add CUT. assigning your new MenuStrip to the form’s MainMenuStrip property is not enough to actually expose it. because it in fact requires only 5 additional lines of code to enable this cascading.Add(New ToolStripMenuItem("Paste".V)) . AddressOf CopyMenu_Click.Add(New ToolStripMenuItem("E&xit".MainMenuStrip = myMenuStrip”.Dock = DockStyle. This second point is very important.” Sadly. unlike simply assigning a MainMenu object to the form’s MainMenu property to bring it to life.AddHandlers(Me. but then we are stuck with needing to construct our menus manually using that derived class.DropDownItems. COPY. Second.DropDownItems. AddressOf PasteMenu_Click. Keys.DropDownItems. Keys. side) End With Me. MergeAction .Text . However.BackgroundImage . we will also need to easily create the new items so we can create a menu effortlessly. listed below: Page –146– .AutoToolTip = Me.Overflow .AutoSize = Me.DoubleClickEnabled .DropDownItems.AllowDrop = Me. Of course.Anchor .Enhancing Visual Basic .ImageAlign = Me.Events) '----------------------------------------------------------------------.AccessibleRole = Me.com/b/jfoscoding/archive/2005/09/28/475177.CheckState .AccessibleRole .RightToLeft . assuming that we named our cloneable ToolStripMenuItem class as cToolStripMeniItem.ImageTransparentColor .Image ..Overflow = Me.Alignment .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. we need only add the following before the above “End With”: '----------------------------------------------------------------------'if this menu item in turn has dropdown menu items.Name .TextImageRelation .BackColor = Me.TextDirection .BackgroundImageLayout .Padding = Me.Margin .Available End With Return clonedItem 'return cloned ToolStripMenuItem and any dropdowns End Function The above function is a direct VB.BackgroundImage = Me.ForeColor .ShowShortcutKeys .ShowShortcutKeys = Me. The only thing is.AccessibleName .ImageTransparentColor = Me.BackgroundImageLayout = Me.Alignment = Me. For Each mItem As cToolStripMenuItem In . to make it robust so that it will include any cascaded sub-items in all and however-many generations of dropdowns from the object.Enabled = Me.ToolTipText = Me.Checked .CheckState = Me.ImageScaling = Me.MergeIndex = Me. our added CloneMenu() function could initially be defined like this: '******************************************************************************* ' Method Name : CloneMenu (thanks to jfo's coding at MSDN Blogs) ' Decription : Return a Clone of the selected item ' : using the syntax used for instantiating a ToolStripMenuItem.TextImageRelation = Me.Image = Me.Text = Me.Checked = Me.Font .AddHandlers(Me.Dock .Name = Me. '----------------------------------------------------------------------If .Anchor = Me.Font = Me.Padding .TextDirection = Me.TextAlign = Me.DisplayStyle = Me.AllowDrop .DoubleClickEnabled = Me.AutoToolTip . we must remember that all members of this menu must be of type cToolStripMenuItem rather than ToolStripMenuItem.MergeIndex .CheckOnClick = Me.NET Beyond the Scope of Visual Basic 6.ShortcutKeys .ImageAlign .Tag . Consider first the full cToolStripMenuItem class definition.Enabled .MergeAction = Me.AccessibleName = Me.ImageScaling .aspx).0 – David Ross Goben For example. '******************************************************************************* 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) .BackColor .ToolTipText . then clone them as well.msdn.TextAlign .AutoSize .Margin = Me.NET translation of the C# Clone mentod provided by JFO at jfo's coding at MSDN Blogs (http://blogs..RightToLeft = Me.ShortcutKeys = .DisplayStyle .ForeColor = Me.Tag = Me.Events.Dock = Me.HasDropDownItems Then 'if this item have children.CheckOnClick .DropDownItems 'process each child item .Available = Me. ImageScaling = Me. '******************************************************************************* 'RAW instantiation Public Sub New() MyBase.AutoSize = Me.ImageTransparentColor = Me.Events. Keys)) End Sub 'Instantiation with Text. Keys)) End Sub '******************************************************************************* ' Method Name : CloneMenu ' Decription : Return a Clone of the selected item ' : using the syntax used for instantiantiong a ToolStripMenuItem.Image = Me. name) End Sub 'Instantiation with Text. Image.Image .Enabled . and Shortcut keys Public Sub New(ByVal text As String.New(text) End Sub 'Instantiation with Text and Image Public Sub New(ByVal text As String.BackColor .Events) '----------------------------------------------------------------------. CType(shortcutKeys.DisplayStyle = Me. ByVal shortcutKeys As Keys) MyBase. ByVal name As String) MyBase.ForeColor = Me.ImageAlign .New(text. Image.CheckState .New(text. Image) End Sub 'Instantiation with Text.BackgroundImageLayout = Me. 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Public Class cToolStripMenuItem Inherits ToolStripMenuItem '******************************************************************************* ' Method Name : New (various overloads) ' Decription : the overloaded constructors are used to instantiate a ClonableToolStripMenuItem ' : using the syntax used for instantiating a ToolStripMenuItem. ByRef Image As Image) MyBase. Image.New(text.AllowDrop . onClick event reference. ByRef Image As Image. ByRef Image As Image.Margin = Me.Checked .Dock = Me. ' I added all the constructors and read the tea leaves to figure out how to implement it.Font .ImageAlign = Me. onClick. onClick event reference.ImageTransparentColor .Anchor = Me.NET Beyond the Scope of Visual Basic 6. and Shortcut keys (add easy shortcut feature of MenuItem objects) Public Sub New(ByVal text As String.Anchor .AutoToolTip = Me.ForeColor . ByRef image As Image. image. onClick) End Sub 'Instantiation with Text. onClick event reference. onClick.CheckOnClick . onClick.AllowDrop = Me.Dock . ByRef onClick As EventHandler.AddHandlers(Me.BackgroundImageLayout .BackColor = Me.AccessibleRole = Me.Checked = Me. '******************************************************************************* 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) .Alignment .Enhancing Visual Basic .ImageScaling . ByVal shortcutKeys As Shortcut) MyBase. and onClick event reference Public Sub New(ByVal text As String.msdn.AccessibleRole . Image.AccessibleName = Me.AutoToolTip . ByRef Image As Image.Alignment = Me.New(text.CheckState = Me. ' I also added a recursive method to include dropdown items in a clone to make it richer. and menu item control name Public Sub New(ByVal text As String.CheckOnClick = Me.AutoSize . Image.New() End Sub 'Instantiation with just Text Public Sub New(ByVal text As String) MyBase.Font = Me.Margin Page –147– .DisplayStyle .BackgroundImage = Me.com/b/jfoscoding/archive/2005/09/28/475177. Image.BackgroundImage .DoubleClickEnabled . ByRef onClick As EventHandler.AccessibleName . Image.aspx ' ' jfo's code featured only the CloneMenu function and did not explain how to use the class.New(text.DoubleClickEnabled = Me. CType(shortcutKeys. ByRef onClick As EventHandler) MyBase.Enabled = Me.0 – David Ross Goben 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' cToolStripMenuItem Class ' Clone() function derived from C# source at: jfo's coding at MSDN Blogs: ' http://blogs. ByRef onClick As EventHandler. Tag .CtrlX)) EditItem.TextImageRelation . This will also clone any child menu items. Nothing.0 – David Ross Goben . Using cToolStripMenuItem.NET Beyond the Scope of Visual Basic 6.Add(New cToolStripMenuItem("Copy".Add(mMenu) 'add to form controls so we can actually see it! Also.TextAlign .Add(mItem. AddressOf ExitMenu_Click)) Dim EditItem As New cToolStripMenuItem("&Edit") 'Build the EDIT member of the Main Menu 'Add CUT. Shortcut.MergeAction . '----------------------------------------------------------------------If .DropDownItems.TextDirection . AddressOf PasteMenu_Click. With the above cToolStripMenuItem derived class with its added Clone() method available to use as support.MergeIndex .CloneMenu to Create a New MergeMenuStrip Method Of course.Controls.Name .Tag = Me. and PASTE members EditItem. except that you 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.ShortcutKeys = . Nothing.ToolTipText = Me.Add(itm.. AddressOf CopyMenu_Click.DropDownItems. then clone them as well.Available = Me.Add(New cToolStripMenuItem("E&xit".CtrlV)) .RightToLeft = Me.TextAlign = Me.Overflow = Me. as we were able to do using the MergeMenu method of a ContextMenu object.MergeIndex = Me. AddressOf CutMenu_Click.ToolTipText . Nothing.CloneMenu) 'add each to the new context menu strip Next Return Cmenu 'return the newly constructed context menu strip End Function Page –148– .DropDownItems 'process each child item ..DropDownItems.TextDirection = Me. EditItem}) 'Add FILE and EDIT menu items to the Main Menu .Text .Dock = DockStyle.CloneMenu”.Add(New cToolStripMenuItem("Cut".Name = Me. you could clone it like this: “Dim clonedItem As cToolStripMenuItem = cTSMI. one of my goals is to be able to construct a new context menu from a MainMenuStrip member (or simply a ToolStripMenuItem that has dropdown members).Text = Me.DropDownItems. Nothing.MainMenuStrip = mMenu 'assign to Main Menu Strip Me. For Each mItem As cToolStripMenuItem In .RightToLeft .Overflow .ShowShortcutKeys .CtrlC)) EditItem.CloneMenu) 'recurse childen (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 you look to the previous example for building MenuStrips on the fly.TextImageRelation = Me.Available '----------------------------------------------------------------------'if this menu item in turn has dropdown menu items.MergeAction = Me.HasDropDownItems Then 'if this item have children.Add(New cToolStripMenuItem("Paste". Shortcut.Top 'dock to top of form End With Me. we can now easily define yet another helper method in our form that will clone a menu item’s dropdown objects to a new context menu strip.Padding = Me. add it last for proper Z-Order display To create a clone of a cToolStripMenuItem object is easy.Items. COPY.AddRange(New cToolStripMenuItem() {FileItem.ShortcutKeys .Enhancing Visual Basic .DropDownItems. you would do exactly that. Consider the following new MergeMenuStrip method: '******************************************************************************* ' Method Name : MergeMenuStrip ' Decription : Use this method to quickly convert main meu items in a ToolStripMenu into ContextMenuStrip objects '******************************************************************************* Private Function MergeMenuStrip(ByVal StripItem As cToolStripMenuItem) As ContextMenuStrip Dim Cmenu As New ContextMenuStrip 'declare a new context menu strip object For Each itm As cToolStripMenuItem In StripItem.DropDownItems 'process all sub-items from the MainStrip Item Cmenu.Padding . such as if cTSMI contained a dropdown list. If cTSMI were of type cToolStripmenuItem.ShowShortcutKeys = Me. Shortcut.Items. Items(1).Position) 'show the menu with top-left at the mouse location End If End Sub Conclusion With the above examples as a guide to getting you started.MouseDown If e.Right Then 'if the mouse-down was with the right button. and my ability to work around these problems with no difficulty. and use them. However.. and a MergeMenuStrip method for MainMenuStrip and ContextMenuStrip objects.MainMenuStrip.Button = MouseButtons.Show(Cursor.DropDownItems 'process all sub-items from the MainStrip Item Cmenu. clone them. I have yet to find anything to spit vitriol about. cToolStripMenuItem)) Cmenu. you could simplify our invocation to this: Cmenu = MergeMenuStrip(Me. as you can see above. except for the lack of an actual CloneOnClick or CloneMenuStrip method for ToolStripMenuItem objects. ContextMenu. 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.Enhancing Visual Basic . you can add more power to your form menu needs than you may have ever dreamed possible.Items(1)). the catch here is that we cannot reference sub-items from a MenuStrip without casting it to a cToolStripMenuItem..0 – David Ross Goben We would use it must 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. MergeMenuStrip(Me.CloneMenu) 'add each to the new context menu strip Next Return Cmenu 'return the newly constructed context menu strip End Function With this overload in place.NET’s new.Add(itm. However. Page –149– . more powerful menu system. or yourself capable.NET Beyond the Scope of Visual Basic 6.. cToolStripMenuItem) 'cast item to its actual type For Each itm As cToolStripMenuItem In si. with the power afforded me with what I do have. and MenuItem objects and the speed at which I am able to construct them..MainMenuStrip.Items(1)) 'build a new context menu and assign it to our object reference And.MouseDown If e. ByVal e As MouseEventArgs) Handles Me.Items. we could write an additional overload of MergeMenuStrip() to take care of that for us so that we will not need to bother about the recasting issue ourselves: '******************************************************************************* ' Method Name : MergeMenuStrip (overload) ' Decription : Use this method to quickly convert main meu items in a ToolStripMenu into ContextMenuStrip objects '******************************************************************************* Private Function MergeMenuStrip(ByVal StripItem As ToolStripItem) As ContextMenuStrip Dim Cmenu As New ContextMenuStrip 'declare a new context menu strip object Dim si As cToolStripMenuItem = DirectCast(StripItem.Position) 'show the menu with top-left at the mouse pointer location End If End Sub Of course.Button = MouseButtons. because the Items collection of a MenuStrip returns an object of type ToolStripItem. plus being armed with MainMenu.Show(Cursor.Right Then 'if the mouse-down was with the right button. you can simplify the 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. I am not lamenting very loudly. For all the lamenting that I have heard over VB. ByVal e As MouseEventArgs) Handles Me.MainMenuStrip. NET menus.MDIForm definition attribute) that is displayed only when no MDI Child Forms are present. On the other hand. its own menu will replace the default MDI Parent menu on the MDI Parent Form by reassigning its menu to its parent. was displayed when MDI Child Forms were shown. Maintaining an Active Child Windows List Under VB6. developed for Windows 1.NET.NET’s more modern approach to MDI menu processing. NOTE: You would think that setting the MainMenuStrip property would be the way to go. to my thinking. and then setting the Visibility property of these menus as needed. used by VB1-VB6. such as a “&Window” main menu entry on a MDI Child Form by simply placing a checkmark in the WindowList checkbox of the Menu Editor when the target entry was selected (see the illustration to the right). Go with Visibility. I model my VB6 menus after VB. by defining multiple menus on the parent. as many rumors. Page –150– . I think it really has to do with the fact that they have simply grown accustomed to emplementing the VB6 method. In fact. Unfortunately. not reading documentation. a VB.0. or even better. you must maintain two separate menus on two separate forms. it does not. You could easily add a list of currently open forms (windows) to a dropdown menu. when you add an MDI Parent Form to your project. current developmental trends favor VB. making this schema often very frustrating to work with. this looked to be impossible because the open window list stopped working in their custom ventures. not stolen from the Macintosh (who also licensed from Xerox). complete with a leading separator.NET MDI Parent Form will maintain the menu interface for both when an MDI Child Form is or is not displayed. The MDI Child Form would of course be displayed without that menu. But. This told the background system to automatically add a list of presently open child windows to the bottom of that menu’s submenu list. you will need to modify the MDI Parent menu for additional options that should be available only when an MDI Child Form is present.Enhancing Visual Basic .NET. but there was a lot of concurrent programming required to support identical menu options for both the main and child menus. comprehensive windows/menu environment for the PC (the original interface was licensed from Xerox. nor did they want to have to face the major modifications to it that would be needed to personalize the interface. This streamlines the entire process and simplifies a ton of code. many developers do not want to start with a partially constructed form with basic menu and toolbar options already declared. as opposed to the original form. They most often need to start with a fresh blank form.NET Beyond the Scope of Visual Basic 6. as a consequence. and a lawsuit. This is easily accomplished by rendering certain menu entries invisible when no MDI Child Forms are displayed (easy). Conversely. when using a Multiple Document Interface (MDI).0 – David Ross Goben Adding Window and File Lists to VB. especially if menu options keep changing throughout the run of an application. no matter how complicated they can often make it. However.NET method is far superior and much easier. the open window list is taken care of for you. and another. fortunately. which is defined on a MDI Child Form (a form with its MDIChild property set to True). had it). But to many of them. which provided the first complete. Also. The first is assigned to the MDI Parent Form (a form declared with a hidden VB. the VB. that was Microsoft’s first venture into fully integrated window design.NET supports menu creation. such as some sort of secret code hidden within the system-provided MDI Parent Form template. Many of them.NET Menus Adding and removing custom menu items from form menus has changed between VB6 and VB. Under VB. NOTE: When a VB6 MDI Child Form is displayed. This approach worked. even though they may have added a “&Window” or “&Windows” main menu item. but it is not. Although many VB6 aficionados loudly complain that they do not like the way VB. simply assumed that inaccessible features would automatically be set behind the curtain by the MDI Parent Form template. finding it easier to simply construct their own menus and interfaces from scratch. such as ChildForm. Page –151– . This approach makes much more sense than being able to potentially set an MdiList property on every single menu item. not simply in front of the parent form as the Show(Me) property would specify. if you searched the MSDN Help System for “Create MDI form”. 'the above option is only available to runtime code. It also tells you that you can create a VB. my “&Windows” menu item is named WindowsMenu. based upon a form named frmChild. m_ChildFormNumber += 1 'Increment our custom child form naming index variable. you have to set that menu item’s MdiList property to True (this was also how it was done under VB6). by default. '<. there is in fact an easy solution to both problems. a solution for MenuStrip controls. You can add a MenuStrip from the Toolbox.MdiParent = Me 'Convert the new form into an MDI Child Form of this MDI Parent Form before showing it. The new form will now no longer be a top-level form..NET MDI Child Form by simply setting a standard Window Form’s MDIParent property to an MDI Parent Form at runtime. is not provided.> ChildForm. If we did do this. Hence. for as frustrated as many people get over it.NET MenuStrip menu Adding an open windows list to a MenuStrip-type menu. we would select the Menustrip object and then simply select WindowsMenu from its MdiWindowListItem dropdown list. but it is found only on a MenuItem control. ChildForm. which causes the Child Form to be displayed within (bounded by) the MDI Parent Form’s MDI Window Client Area. the big problem here is that you will not find an MdiList property assigned to a MenuStrip. the new form is no longer a top-level window. which. For example. you will not see a list of open windows displayed under it (this should be a big DUH!).NET Toolbox. except programmically. or else an exception error event will be triggered. is really easy. but it does not support an MdiList property. which are in the Toolbox. The MenuStrip contains a listed property named MdiWindowListItem. This property should be set to the menu item that we want to use to show the list of open MDI Child Forms. in that MSDN Help. which supports a MenuItem and its MdiList property. ChildForm. is to be displayed on the MDI Parent Form: Private m_ChildFormNumber As Integer = 0 'keep track of child index for unique naming purposes '*********************************************************** 'Sub : mnuNewChild_Click 'Description: Add a new blank child form based upon frmChild '*********************************************************** Public Sub mnuNewChild_Click(ByVal sender As System.. This part works great. Any similar option also cannot be found.Show(Me). The exception event therefore helps us to avoid such functional ambiguity. suppose the following menu event code was placed within an MDI Parent Form to react to when a new standard form. DO NOT also apply a parent as a parameter to Show()! 'NOTE: DO NOT designate an owner form. do not appear in the VB. Though this seems to be the making of a complex paradox that could potentially destroy the electromagnetic underpinning of the Universe. not on a MenuStrip. you cannot add a MainMenu control to a form from the Toolbox without adding it (see the next page to learn how). If you add a menu to the form and further add “&Window” or “&Windows” to the main menu. Worse.EventArgs) Handles mnuNewChild.Place any additional parent-side setup code here. However. by default.Enhancing Visual Basic . there is an MdiList property available.NET MDI Parent Form by setting a standard Window Form’s IsMdiContainer property to True. For example. Yet.Click Dim ChildForm As New frmChild 'Create a new instance of the child form (presently.ToString 'Give a default title to the form based upon this index. Truth is.0 – David Ross Goben Worse. as we might typically do. it does tell you that you can create a VB. Adding an Open Window List to a VB. the child form is just a normal top-level form). the Help system supposedly “saves the day” by telling you that for the menu item that you want the window display list shown under (“&Window” or “&Windows” in our case). an exception error will be triggered because an MDI Parent for the form is already established.Show() 'Display the child form within its MDI Parent's MDI window..Object. ByVal e As System. This part also works great. these woes seemed to be abated… until you realized that the instructions are for MainMenu controls. on my MDI Parent Form. End Sub NOTE: When you set the a newly instantiated form’s MdiParent property to the MDI Parent Form (usually Me). look instead to the MenuStrip object. Instead of looking to a ToolStripMenuItem to find a property to display a window list. However. However. and therefore we cannot also specify a parent form as a parameter in its Show() method.NET Beyond the Scope of Visual Basic 6.Text = "Window" & m_ChildFormNumber.. Apart from saving and loading entries in persistent memory (typically the Registry via the SaveSetting() and GetSetting() methods).Enhancing Visual Basic .0 – David Ross Goben Adding a MainMenu and ContextMenu control to your VB. My Vista system had three. My own approach for VB6 menus has been to borrow from both methods: to use predeclared dynamically created index entries. Place a checkmark in its selection box as well. you can drop them on your forms. which you can immediately begin to use.NET Toolbox Although you can easily build menus using a MainMenu object. And there is also the user selecting an entry from the MRU list. However. Maintaining a Most Recently Used File List Adding an MRU (Most Recently Used) file list to a menu. you must do so programmacly. we are interested in the one that is not selected and is also the only one located within the Global Assembly Cache directory.NET Beyond the Scope of Visual Basic 6. which you can use in place of the default MenuStrip and ContextMenuStrip controls. ensuring that this selected file still exists. 4. you might possibly see multiple entries. or loading them under the File menu. others like them because they think these menus look better. you might see multiple entries again. Others added entries dynamically. 5. To Add MainMenu and ContextMenu controls to your toolbox. Select OK and you will subsequently see a ContextMenu and a MainMenu entry added to you Menus & Toolbars list in your Toolbox. and then to finally save the updated list to the registry. Place a checkmark in its selection box. or they prefer to use them because they also have a very powerful MergeMenu() method available so that you can instantly duplicate an existing submenu branch from the main menu to a new ContextMenu (we created a clonable ToolStripMenuItem class and a MergeMenuStrip() method in the previous article. 2. When the dialog is finally up. or they prefer them for the simple fact that its MenuItem objects do have a MdiList property available (the whole point to the above exercise). there is a fast and easy way to add both the older-style MainMenu and ContextMenu controls to your Toolbox. this is a very good idea). Page –152– . Some people prefer to use them because they require much less code to support. adding the most recent file to the topmost entry of the list. 3. This technique is fast and also makes offset indexing extremely easy. adding entries and removing them via the Load and Unload commands. and then rendering them invisible until they were needed (actually. Wait for the “Choose Toolbox Items” dialog box to come up (this may take some time on some systems. right-click the Menus & Toolbars group heading within the Toolbox and select the “Choose items…” menu option. Mine also had three. we are interested in one that is not presently selected and is also the only one that is located within the Global Assembly Cache directory. plus deleting it from the list if it does not. However. However.Net Framework Components” tab is selected. typically to a File menu dropdown list. which involved deleting the entry from the list if it is already there. Scroll down the list until you see a “ContextMenu” entry. Once you have these in your Toolbox. Under VB6. because it is enumerating a lot of system components). With a form up on the screen so that Toolbox controls are displayed. but creating the maximum count all at once when the form loads and render those that are not being used invisible. shifting other entries down in the list. As with the ContextMenu entry. some developers resorted to adding the maximum number of file entries allowed. and shifting it up to the top of the MRU list as the most recently opened item if it did already exist in the list. using an indexed menu entry. and again shifting any others down. Fact is. Adding Run-Time Custom Menus and Cloneable ToolStripMenuItems to VB. inserting the current entry to the top of that list. there was always the task of limiting the number of files that could be listed (anywhere from 4 to 9 recent files is traditional). was often a bit of a pain under VB6. be sure the “.NET on page 136). Scroll down the list until you see a “MainMenu” entry. and saving that result to the registry. all you have to do is the following: 1. Title.Visible = False 'render MRU separator invisible For mnuIdx = 0 To Maxcount With Me. "MRUFiles". I next retrieve the current MRU list stored in the registry for the application. and allow selecting from it.Caption = vbNullString 'erase its displayed text End With Next mnuIdx 'Now get the current MRU file count Fcnt = CInt(GetSetting(App. save it. but user may have selected fewer somewhere) Maxcount = CInt(GetSetting(App. When I save the MRU list to the registry. I pre-build a list of file entry slots. in my MDI Main form.Visible = False 'and also the first MRU entry Dim Idx As Integer 'now pre-build MRU menu entries in File menu For Idx = 1 To MaxFileListCnt .Visible = False 'initislly hide the recent file list separator. Fcnt As Integer.NET Beyond the Scope of Visual Basic 6. is at the top of the list. If it is already at the top of the list.1 'First render the MRU separator and all possible MRU file entries invisible Me. but we must create 1-8 (this counts as 9 entries) Load mnuMRU(Idx) 'load a new instance of the indexed mnuMRU entry Next Idx 'Get a user-selected flag indicating if full paths or just the filename should be displayed in MRU list ShowFullPath = CBool(GetSetting(App. "MRUFiles". When I add to the MRU list. Idx As Integer Dim Tmp As String 'Get the maximum number of file entries (default is MaxFileListCnt. if it is not already at the top of the list.mnuMRUSep. or the currently listed one.Item(0).. Consider the following VB6 code for an MDI Parent Form that performs all these various tasks: Public MaxFileListCnt As Integer Private ShowFullPath As Boolean 'stores the maximum files that can be displayed under the File menu 'flag indicating if a full path should be shown in the MRU list. but the user can select fewer) MaxFileListCnt = CInt(GetSetting(App. it is of course left alone. If it does.Title.Visible = False 'make an entry invisible . "MRUFiles". which means the list must afterward be saved. Below that I add a menu entry named mnuMRU that has its Index property set to 0.mnuMRU. the entry must be moved to the top. and the filename (or full path) to the Caption mnuIdx = 0 'init the actual MRU entry index to zero Page –153– .Title. so make the leading menu separator visible 'Read entries. "0")) 'False (0) indicates filename-only 'Now load the MRU list to the File menu Call GetMRUFiles End Sub '************************************************************************* ' GetMRUFiles(): Get MRU File list. or just the filename '************************************************************************* ' MDIForm_Load: Set up instal state of form. "Count". "ShowFullPath". MRU File Support Under VB6 Under VB6. "MRUFiles". though I contrarily think that the VB. because we want to shift the selection to the top of the list. mnuIdx As Integer. when the user selects a file from the list. which means that we also need to save the updated list. In the form’s Load event. ' Initialize MRU list '************************************************************************* Private Sub MDIForm_Load() 'Get the user-selected maximum number of files we can display (default is 9. which also requires the MRU updating and saving processes. I shift any items higher than it down. CStr(MaxFileListCnt))) . which I name mdiMain. and these results must be propagated (reflected) to all child forms so they display identical lists.Title. and so I propagate the results to the menus of any child forms.0 – David Ross Goben Because many people are telling me the VB6 method is easier. I check to see if it already exists within the list.mnuMRU(mnuIdx) . let us look at both. load it. lpIdx As Integer.Enhancing Visual Basic .. "MaxCount". Me. '************************************************************************* Public Sub GetMRUFiles() Dim Maxcount As Integer. "MaxCount". "9")) Me. We will initialize the MRU list.mnuMRUSep. it is because I altered it. ensuring that the new file. 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. "0")) If CBool(Fcnt) Then 'Do MRU files currently exist? mnuMRUSep. Further. I typically add a menu separator somewhere under my File menu and always name it mnuMRUSep. we must additionally propagate these updated results to all child form menus so that they will also be up-to-date and reflect identical data. Set the full path to the TAG property. update it.Visible = True 'files exist.NET method is easier (which is why I model all my VB6 menus after it).1 'index 0 already exists. Finally. "\") 'find the start of the filename If ShowFullPath Then 'False if we should show only the filename Idx = 0 'if flag is True.Tag 'shift things up If CBool(Len(mnuMRU(mnuIdx .Caption Next mnuIdx Else 'entry is not found IN the list. then show full path. not just its filename filename (mnuIdx<>0) Page –154– .Tag.1).Tag.1). so just return the number of items in list If Fcnt = 0 Then AddMRUFile = CInt(GetSetting(App.1 With mnuMRU(mnuIdx) Tmp = GetSetting(App.1). but user can select fewer) Maxcount = CInt(GetSetting(App. vbNullString) If CBool(Len(Tmp)) Then . "MaxCount".Title.Visible Then 'is the current entry viable? If so.Caption = Mid$(.Caption = mnuMRU(mnuIdx .1).Tag) Fcnt = Fcnt + 1 'count one file saved by bumping the naming index End If Next mnuIdx 'Save file count Call SaveSetting(App. save index 'we are done with the search 'else check next entry 'If a match is found at the very top (index 0).1 'See if the file is in the current list Fcnt = -1 For mnuIdx = 0 To Maxcount With mnuMRU(mnuIdx) If . "MaxCount".Caption = Mid$(FName.Title.Visible = True And LCase(.Title.Visible = True End If Next End If 'Now set the file at the top of the list With mnuMRU(0) . save it and bump the index Call SaveSetting(App. Idx + 1) . "MRUFiles". "File" & CStr(lpIdx).Visible = True 'make the new menu entry visible mnuIdx = mnuIdx + 1 'bump the menu MRU entry index End If End With Next lpIdx 'process all mnuMRU entries If Fcnt <> mnuIdx Then Call SaveMRUFiles() 'update MRU list if it changed End If End Sub '************************************************************************* ' SaveMRUFiles(): Save current list of MRU files '************************************************************************* Public Sub SaveMRUFiles() Dim Maxcount As Integer. "Count". show full path.Title.1).Caption)) Then mnuMRU(mnuIdx). so move all others up For mnuIdx = Maxcount To 1 Step -1 mnuMRU(mnuIdx). "MRUFiles". then there is nothing to do.Tag = mnuMRU(mnuIdx . "File" & CStr(Fcnt).0 – David Ross Goben For lpIdx = 0 To Fcnt . CStr(MaxFileListCnt))) . "MRUFiles". CStr(MaxFileListCnt))) .Caption = mnuMRU(mnuIdx . Fcnt As Integer. mnuMRU(mnuIdx).Title.Tag) = LCase(FName) Then Fcnt = mnuIdx Exit For End If End With Next mnuIdx 'init for not found 'check each entry 'found match? (we will check the full path) 'yes. "MRUFiles". mnuIdx As Integer 'Get the user-selected maximum number of file entries (default is MaxFileListCnt. then shift things around to sqeeze it up to the top If Fcnt > 0 Then For mnuIdx = Fcnt To 1 Step -1 'slide backward through list from matching file mnuMRU(mnuIdx). "0")) Exit Function End If 'If a match found elsewhere in the list. "Count". "MRUFiles".Tag 'shift things up (selected file is over-written) mnuMRU(mnuIdx). "\") If ShowFullPath Then mnuIdx = 0 End If 'prepend the numeric shortcut to the filepath (mnuIdx=0) or .Enhancing Visual Basic . but user can select fewer) Maxcount = CInt(GetSetting(App. "MRUFiles". CStr(Fcnt)) Call PropagateMRUs() 'propagate MRU list to MDI Child menus End Sub '************************************************************************* ' AddMRUFile(): Add a file to the MRU list under the File menu '************************************************************************* Public Function AddMRUFile(ByVal FName As String) As Integer Dim Maxcount As Integer.1 'Init current count to null Fcnt = 0 'Save off all visible mnuMRU menu entries For mnuIdx = 0 To Maxcount If mnuMRU(mnuIdx). mnuIdx As Integer 'Get the user-selected maximum number of file entries (default is MaxFileListCnt.Tag = mnuMRU(mnuIdx . Fcnt As Integer.Tag = FName mnuIdx = InStrRev(FName.Title.Tag = Tmp 'stuff full file path to the Tag property Idx = InStrRev(. mnuIdx + 1) 'stuff the full path to the file 'find the start of its filename 'if flag is set.NET Beyond the Scope of Visual Basic 6. not just filename End If 'add the filepath (Idx=0) or filename (Idx<>0) .Caption mnuMRU(mnuIdx). set focus to it Path = mnuMRU(Index).NET Beyond the Scope of Visual Basic 6. "0")) 'return number of entries End Function '******************************************************************************* ' Subroutine Name : PropagateMRUs ' Purpose : propagate MRU list to children to modify their MRU lists '******************************************************************************* Public Sub PropagateMRUs() Dim Frm As Form On Error Resume Next ' ignore forms without the MDIChild property For Each Frm In Forms If Frm.Tag = Path FileIndex = FileIndex + 1 frm..Title.GetChildMRUList() ' reload Child MRU lists to match the Master list in the Parent Next End Sub '******************************************************************************* ' Subroutine Name : mnuMRU_Click ' Purpose : Process user MRU File selections that they click on '******************************************************************************* Public Sub mnuMRU_Click(ByVal Index As Integer) Dim Path As String Dim RdOnly As Boolean Dim frm As Form 'See if the file is already loaded.Text1 = Ts. you could use code similar to the following: '******************************************************************************* ' Subroutine Name : LoadFile ' Purpose : Load a text file into a new MDI Child Form and display it '******************************************************************************* Private Function LoadFile(Path As String) As Boolean Static FileIndex As Integer 'static refernce used for unique child form names (initial value is 0) Dim FSO As FileSystemObject 'file system object (reference to 'Windows Scripting Host Object Model' needed) Dim Ts As TextStream 'textstream object for file I/O Dim frm As mdiChild 'Child Form reference If CBool(Len(Dir(Path))) = False Then LoadFile = False Exit Function End If 'if the file does not exist.Caption = "Window" & CStr(FileIndex) 'otherwise instantiate a new Child Form 'save the full path to the file 'bump the static file naming index 'apply a new title to the Child Form Set FSO = New FileSystemObject Set Ts = FSO..0 – David Ross Goben 'make the menu item visible . will have to be custom-designed to address the needs of for your particular application. vbOKOnly Or vbExclamation.Visible = True 'make sure the leading menu separator is also visible Call SaveMRUFiles() 'save the updated MRU file list AddMRUFile = CInt(GetSetting(App. (invoke custom user routine to load file to a new child window) Call AddMRUFile(Path) 'add it to the MRU list if the file loaded OK End If End Sub The LoadFile entry.Close Set FSO = Nothing frm. For example.SetFocus 'so set focus to it Exit Sub 'and we are all done End If Next frm 'Create a new child window (The Loadfile() routine will save the full filepath to the new child form's Tag property) If LoadFile(Path) Then 'if the file loaded.Tag = Path Then 'if paths match.Enhancing Visual Basic . If so. if you were simply loading a text file to a TextBox on a Child form. False) frm.ReadAll Ts. 'indicate failure 'and leave the function Set frm = New mdiChild frm. "MRUFiles". then this file is already loaded frm.Tag If Not CBool(Len(Dir(Path))) Then 'does the file still exists? No if False Call MsgBox(Path & " no longer exists.. shown above in highlight. "File Not Found") mnuMRU(Index).OpenTextFile(Path.Visible = False 'tag invisible Call SaveMRUFiles() 'save only visible MRU files Call GetMRUFiles() 'refresh list Exit Sub End If 'See if the file is already loaded. If so. then just set focus to it For Each frm In Forms If frm.Show LoadFile = True End Function 'instantiate our File System Object 'open the filepath for reading 'read its contents into a textbox 'close the file 'remove our file system object 'display our child form 'indicate success Page –155– .MDIChild Then Call Frm. "Count".Visible = True End With mnuMRUSep. This method simply designates the process of loading data and constructing the look of your MDI Child Form.. ForReading.". Caption 'duplicate the displayed text for each entry from the MDI parent Me.mnuMRU(Idx).txt" 'set up the file filter to only allow text files .FileName) Then Call AddMRUFile(.Visible 'duplicate the visibility flag for each entry from the MDI parent Next Idx End Sub '******************************************************************************* ' Subroutine Name : mnuMRU_Click (example) ' Purpose : reflect file list selection '******************************************************************************* Private Sub mnuMRU_Click(Index As Integer) Call mdiMain.Item(0).Enhancing Visual Basic .Width = Me.Text1 .mnuMRUSep.mnuMRU(Idx).CommonDialog1 'using a common dialog interface .DefaultExt = ".Visible 'Duplicate all MRU menu entries in the parent MDI form to this form For Idx = 0 To mdiMain.mnuMRU.DialogTitle = "Open Text File" 'set a title for the dialog .Left = 0 .Visible = mdiMain.MaxFileListCnt . the following reacts to an “Open…” menu entry.FileName = "*.ScaleWidth .Visible = False Me.mnuFileOpen_Click End Sub '******************************************************************************* ' Subroutine Name : Form_Resize (example) ' Purpose : assume the textbox (Text1) fills the Child form '******************************************************************************* Private Sub Form_Resize() With Me.ScaleHeight End With End Sub Page –156– .ShowOpen() 'show the open file dialog interface If Err.mnuMRU(Idx). ' Initialize MRU list for this child form '************************************************************************* Private Sub Form_Load() Dim Idx As Integer 'Now pre-build MRU menu entries in File menu Me. but we must create 1-8 (this counts as 9 entries) 'load a new menu entry 'get MRU list from MDI parent '******************************************************************************* ' Subroutine Name : GetChildMRUList ' Purpose : set MRU list in this child form to match main form '******************************************************************************* Public Sub GetChildMRUList() Dim Idx As Integer 'duplicate our separator visibility to match the MDI parent's separator Me.Visible = mdiMain.mnuMRUSep.mnuMRU(Idx).Top = 0 .mnuMRU_Click(Index) End Sub '******************************************************************************* ' Subroutine Name : mnuMRU_Click (example) ' Purpose : reflect File Open selection '******************************************************************************* Private Sub mnuFileOpen_Click() Call mdiMain.Visible = False For Idx = 1 To mdiMain.Flags = cdlOFNExplorer Or cdlOFNFileMustExist Or cdlOFNPathMustExist .txt" 'just for show..Number <> 0 Then Exit Sub 'if the user had hit the Cancel button on the dialog On Error GoTo 0 'clear error trapping 'Create a new child window If LoadFile(.0 – David Ross Goben To complete this example code for the MDI Parent Form.Filter = "Text Files *.FileName) End If End With End Sub 'if the file loaded.mnuMRUSep.mnuMRU(Idx)) Next Idx Call GetChildMRUList End Sub 'erase file list separator 'and first entry 'index 0 already exists.MaxFileListCnt ..Caption = mdiMain. because Cancel generates an error .CancelError = True 'allow user to cancel out of the dialog On Error Resume Next 'avoid crashes.NET Beyond the Scope of Visual Basic 6. to highlight that only text files are allowed . (invoke user routine to load file to a new child window) 'add it to the MRU list if the file loaded OK The only thing left to do is to support propagating the MDI Parent Form’s MRU list to its child forms by adding the Parent Form-invoked GetChildMRUList method to the frmChild code: '************************************************************************* ' Form_Load: Set up initial state of form.1 Load(Me.Height = Me.1 Me.txt" 'set up default file extension . so that the user can specify a text file to load to a TextBox on our MdiChild form: '******************************************************************************* ' Subroutine Name : mnuFileOpen_Click ' Purpose : let the user select a file to open '******************************************************************************* Public Sub mnuFileOpen_Click() With Me.txt|*. Also. To minimize all this typing. I tend to always name my MDI Parent Form as mdiMain. All this code still does not address the issues of setting up the look and feel of a child form or properly naming child forms. to move an entry from one location in the list up to the top. NOTE: Although VB6 users may claim that setting an object reference to Nothing would immediately remove the object from memory.NET Supporting MRU File lists under VB. but at the same time it also introduced features. MRU File Support under VB. but the code may appear to some to be almost as complicated as the VB6 code.NET attribute that will immediately remove the object’s actual resources. Under VB6. it actually runs faster and is much more straightforward. which was especially aggravating when the closing of an application depended on that object not existing. If it finds that the flag is False. we can eliminate a lot of code that was required to shift the file entries around in the list. it should go ahead and dispose of any resources the object had created. We do not even have to reconstruct the entry in the new location because of the fact that VB. The Garbage Collector walks the program Stack (to find local. and the MRU item list item as a zero-indexed entry named mnuMRU. but it did not truly eliminate the object. we insert the reference at the start of the list. which each might. VB. if it is crucial that these unmanaged resources be released. 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. Even so. thus declaring some sort of superiority over VB. most often by accident. Hence. temporary variables) and the Heap (for static/shared/global variables/fields) and determines which objects are no longer being referenced by anything. I tend to stick with consistently naming the MRU menu separator to mnuMRUSep. because as long as a reference to it still exists (even through the reference variable that you just set to Nothing no longer references it).Enhancing Visual Basic . but only removing a reference to it. Because we are using instantiated objects. By also adding a Boolean IsDisposed flag to the object. and my MDI Child Form as mdiChild.NET is fully object-oriented. Although I included sample code. It may have removed data resources. and realizing that an object reference variable is not the object itself. Finally. either through our temporary reference variable or through a reference to it in the menu list. all we have to do under VB. not waiting for the Garbage Collector to do it. because references to an object can be held by more than one object (and the source of much VB6 frustration). when you set an object reference variable to Nothing. to put your personal stamp on your application. we are able to insert and remove MRU file entries from the File menu. 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. you are not erasing the object itself. you could still manipulate the object.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). which caused uncounted major headaches in the VB6 world. set to an initial False value. As long as a reference to the actual object exists. as long as a reference to an object exists (our local reference variable in this case).0 – David Ross Goben As you can see. thus instantly re-instantiating its data (which was actually the only thing removed). For example. This is why it can be very important to know if an object implements the IDisposable interface and thus features a Dispose() method. regardless of how efficient and feature-rich it is. because some objects just refused to die. this can be a lot of typing. which makes this process even easier. then all I have to do is simply paste the above code from a template file to significantly speed VB6 application development. such as to pass opening a selected file entry and opening a new file on to the mdiMain form. Because of this single language enhancement. if you even tried to test for an object being released. try to invoke its Dispose method. your custom Dispose method can first check this flag to see if it should actually do anything. That is all up for you to do. preventing unwanted accidental object reinstantiation. With that. We then remove the reference from the list. that eliminated all that frustration.NET Beyond the Scope of Visual Basic 6. and eliminates all such objects no longer in use. even though you should enable the method to be invoked more than one time. and should. we can simply use a MenuItem reference variable to point to the object to move.NET (do not get me started on that one – and this is coming from one of VB6’s biggest advocates) – this is not quite true. Unlike the VB6 version. Once this task has completed it then sets the IsDisposed flag to True. you should design the Dispose method to actually perform its task of releasing resources only once. which is a VB.NET is easy. such as the Dispose method.NET is to remove the MenuItem object from its current location and reinsert it at the beginning of the list. Also. Page –157– . but is just a reference to the actual object. the slightest reference checking on it immediately resurrected it. NOTE: Be aware that if you implement the IDisposable interface into your own classes. the object will stay in scope. you will almost certainly need to add numerous others. ' Initialize MRU list '************************************************************************* Private Sub Form1_Load(ByVal sender As System.Application. "ShowFullPath". but the user can select fewer) MaxFileListCnt = CInt(GetSetting(My.mnuMRUSep. But unlike the VB6 model.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. but you 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. the main departure from the VB6 version is the manner in which file entries are registered on the main menu’s File dropdown list. CStr(0))) 'get application title 'number of entries If CBool(MaxCnt) Then 'anything? Dim LoadingIndex As Integer = 0 'init loading index Dim mnuMRUSepIdx As Integer = Me. which everyone always set to False anyway. then you should also invoke the Dispose method from the Finalize method. somewhere on the File menu dropdown I will place a separator name mnuMRUSep. so add to File menu Dim Path As String = GetSetting(Ttl.Info. Other than that. "Settings".Application.Title. 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. was derived from the model I designed for my VB. '************************************************************************* Public Sub GetMRUFiles() Dim Ttl As String = My.Title Dim MaxCnt As Integer = CInt(GetSetting(Ttl.Info. Indeed. "File" & CStr(Idx). "Settings". and the entries are a menu array ' named mnuMRU. CStr(MaxFileListCnt))) ' erase file list separator Me. You will also notice that in VB. Assume a separator line named mnuMRUSep ' precedes these entries. "MRUFiles".Info. such as a mnuMRU control. then save the new list SaveSetting(Ttl.Load ' Get the user-selected maximum number of file entries (default is 9. you can do it right within the MDI Parent. CStr(LoadingIndex)) 'save new count SaveMRUFiles() 'save new list End If End If End Sub Page –158– . as we had to do for the VB6 model. that is all I need do. so add it to the file list LoadingIndex += 1 'bump actual offset index End If Next Idx If LoadingIndex <> MaxCnt Then 'if menu menu count is different from old. and that you have a separator named mnuMRUSep under your File menu.EventArgs) Handles MyBase. You will also notice that because the MDI Parent form maintains all menus. Like the VB6 model.Title. ByVal e As System.FileExists(Path) Then 'does file exists AddMRUFile(Path) 'yes. if you do need to propagate the file lists to other menus. "MaxCount". with an initial index entry set to zero. for example.mnuFile. except for maybe language refinements. it is almost like a built-in feature. because not doing so was a big useless pain. "FileCnt". "MRUFiles".NET there is no longer an AutoShowChildren property.IndexOfKey("mnuMRUSep") 'get index to MRU separator under mnuFile For Idx As Integer = 0 To MaxCnt . "FileCnt". "0")) 'False (0) indicates filename-only ' Now load the MRU list to the File menu GetMRUFiles() End Sub '************************************************************************* ' GetMRUFiles(): Get MRU File list. such as “Fcnt += 1” instead of “Fcnt = Fcnt + 1”. As with the VB6 version. there is little difference between the VB6 and VB. All we need is the separator named mnuMRUSep. my current VB6 template.Enhancing Visual Basic .NET projects.1 'yes.Application. With that. File lists will always be a lot of work. vbNullString) 'get a saved file entry If Path <> vbNullString AndAlso FileIO. Public Class mdiMain Public MaxFileListCnt As Integer = 9 Private ShowFullPath As Boolean = False 'maximum number of files for list under File menu 'flag indicating if a full path should be shown in the MRU list '************************************************************************* ' MDIForm_Load: Set up instal state of form.0 – David Ross Goben as otherwise a memory leak can result from not deleting them.NET Beyond the Scope of Visual Basic 6.DropDownItems. As indicated above. this code is designed to be fully reusable so that you can simply plug this code into your MDI project. listed above.FileSystem. I do not have to add any File entry placeholders in the list ahead of time.Object.NET versions. "Settings". unless you had the rare ready-to-run child forms. The following MDI Parent Form code assumes that your MDI Parent Form is named mdiMain. which is named mnuFile. IndexOfKey(mnuMRUSep. mnuItem) Else 'if we can open it.Tag Is Nothing Then Exit Do 'avoid exception errors FoundIt = StrComp(Me.Info.IndexOfKey("mnuMRUSep") 'get index of mnuFile separator Dim Fname As String = Path If Not ShowFullPath Then Fname = Mid(Path.Application.IndexOfKey(mnuItem.RemoveAt(Me. "FileCnt". If so.Name)) 'reinsert reference after the MRU separator Me.mnuFile.Insert(mnuMRUSepIdx + 1.DropDownItems Dim mnuMRUSepIdx As Integer = . .Count OrElse _ Me. "Settings".Tag.Info.Tag.Tag = Path 'save the path Me. mnuItem) 'insert at beginning of list If Idx > MaxFileListCnt Then 'if we exceeded the limits (it would be by 1 if so) Me. then file is loaded 'so set focus to it 'and we are all done 'try loading it.Title 'get application title Dim mnuMRUSepIdx As Integer = mnuFile.DropDownItems. "Settings".DropDownItems.mnuFile. "FileCnt".Insert(mnuMRUSepIdx + 1. CompareMethod. ToolStripMenuItem) Dim Path As String = mnuItem.Name)) Dim mnuMRUSepIdx As Integer = Me.Info.DropDownItems. mnuItem) End If Else 'entry not found.DropDownItems(Idx). CStr(Idx)) 'save new file list count SaveMRUFiles() 'then update update list End If Me.mnuFile.mnuFile.Application.Name <> "ToolStripMenuItem" OrElse _ Me.RemoveAt(mnuMRUSepIdx + Idx) 'remove current last entry in MRU list Idx = MaxFileListCnt 'set the counter to within bounds (Idx – 1) End If SaveSetting(Ttl.DropDownItems.Focus() Exit Sub End If Next frm 'if paths match. "Settings".Visible = True 'ensure separator is visible Return Not FoundIt 'True if an entry was actually added.mnuFile.mnuFile.mnuFile.mnuFile. then simply set focus to it For Each frm As Form In Me. so just return the number of items in list If FoundIt Then If Idx .mnuMRUSepIdx > 1 Then 'if not the first entry.Tag.1) 'update search key SaveSetting(Ttl.mnuFile.ToString.mnuFile. "Settings". "\") + 1) End If 'get full path to file 'not displaying full path? 'no.mnuFile.EventArgs) Dim Ttl As String = My.ToString 'get application title 'get reference to menu item selected 'save the display data 'See if the file is already loaded.mnuFile. 'remove its entry from the file list 'point to MRU separator 'reinsert after the MRU separator Page –159– .1 Dim FoundIt As Boolean = False 'flag indicating if an item was found Do Idx += 1 'bump index offset If Idx > Me.DropDownItems.mnuFile.Item(mnuMRUSepIdx + Idx). for count .DropDownItems(Idx).DropDownItems.Name = "mnuMRU" & CStr(Idx .DropDownItems(Idx).Application.Title Dim mnuItem As ToolStripMenuItem = DirectCast(sender. so get just the filename Dim Idx As Integer = mnuMRUSepIdx 'init base of search ..DropDownItems(mnuMRUSepIdx).IndexOfKey(Me. Nothing. 'get menu item to move Dim mnuItem As ToolStripMenuItem = DirectCast(Me.GetType. Path.DropDownItems(Idx).1).ToString) 'save new entry Next End With PropagateMRUs() 'propagate MRU list to other possible menus 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.mnuFile.1) 'add a unique search key to it mnuItem.Text = Fname 'add a name to the entry mnuItem.0 – David Ross Goben '******************************************************************************* ' Subroutine Name : SaveMRUFiles ' Purpose : Save MRU File List '******************************************************************************* Private Sub SaveMRUFiles() Dim Ttl As String = My. 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 System.NET Beyond the Scope of Visual Basic 6..Name = "mnuFile" & CStr(Idx .Insert(mnuMRUSepIdx + 1. then there is nothing to do.Name) Me..DropDownItems. ByVal e As System. InStrRev(Path. CStr(Cnt)) 'save update End If With Me.Tag. "0")) 'get number of file entries If Cnt > MaxFileListCnt Then 'do not exceed max file list count Cnt = MaxFileListCnt SaveSetting(Ttl. "File" & CStr(Idx . New EventHandler(AddressOf mnuFileList_Click)) Idx -= mnuMRUSepIdx 'drop menu offset count mnuItem.Item(mnuMRUSepIdx + Idx).ToString = Path Then frm.Text) = 0 If FoundIt Then Exit Do 'found a match Loop 'If a match is found at the very top (offset index 1).DropDownItems.mnuFile.mnuFile.DropDownItems.MdiChildren If frm.IndexOfKey(mnuItem. and process the result If LoadFile(Path) Then Me.DropDownItems. so insert this new entry into the MRU list immediately after the MRU separator Dim mnuItem As New ToolStripMenuItem(Path.RemoveAt(Me.mnuMRUSep.Enhancing Visual Basic . ToolStripMenuItem) 'remove the entry from the file list Me. "FileCnt".Title 'get application title Dim Cnt As Integer = CInt(GetSetting(Ttl.DropDownItems.Object.Name) 'get index of list separator For Idx As Integer = 1 To Cnt 'process each menu item.. mnuFile. keeping only the relevant one set to True.Show() 'show that we are busy 'otherwise instantiate a new Child Form 'make the form an MDI Child of this form 'save the full path to the file 'bump the static file naming index 'apply a new title to the Child Form 'display our child form Dim TS As New System.NET and are really easy to use.WaitCursor Dim frm As New mdiChild frm.Close() TS. which are built right into . With the VB..SelectionStart = 0 . which will assume that the child form. except for. but it is really quite easy to do. and both being viable. 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.StreamReader(Path) With frm. set their Visibility properties as needed to be set to True or False.FileExists(Path) Then Return False End If 'if the file does not exist. and then set the Visibility property for any menu that you will not be using at any one time to False. if you are indeed using a TextBox control to load files into.FileSystem. as stated before. you should do it here End Sub End Class The above block of code requires no complementary code to be placed in the MDI Child Form as we had to do for the VB6 version. which by comparison makes FSO seem slow. that is because.SelectionLength = 0 End With TS.1)) Me.MdiParent = Me frm. By instead setting it to Off and also setting its ScrollBars property to Both.RemoveAt(Me.Dispose() Me. This is because it is tremendously faster than the basic VB6 file I/O functions. named mdiChild.Tag = Path FileIndex = FileIndex + 1 frm. Under VB6. 'report failure Me.Name)) End If SaveMRUFiles() End Sub 'else get number of file entries 'make it 1 less 'remove the entry from the file list 'now update the file list regardless '******************************************************************************* ' 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. "FileCnt". This has the advantage of allowing for more than one menu to be present on a form. However. which it will not be if you build them in-code). to display or hide relevant main menus.ReadToEnd .0 – David Ross Goben Dim Idx As Integer = CInt(GetSetting(Ttl.Enhancing Visual Basic . This method will perform the file I/O to process the selection from the file menu. You simply place as many menus on your mdiMain form as you need.Text = "Window" & CStr(FileIndex) frm. Like with the VB6 version. "Settings".NET Beyond the Scope of Visual Basic 6. I prefer to use Filestreams.NET version. As such.DropDownItems. I have noticed that setting its WordWrap property to On when you are loading really large files can significantly slow things down whenever the TextBox must resize. Page –160– .Text = TS. is designed to load text files to a TextBox control: '******************************************************************************* ' Subroutine Name : LoadFile ' Purpose : Load a text file into a new MDI Child Form and display it '******************************************************************************* Private Function LoadFile(ByVal Path As String) As Boolean Static FileIndex As Integer = 0 'static refernce used for unique child form names If Not FileIO. "FileCnt".TextBox1 . meaning that it will have both horizontal and vertical scroll bars.DropDownItems. the above code invokes a Boolean method name LoadFile(). I tend to use the File System Object interface for my File input/output.mnuFile.IndexOfKey(mnuItem. as you can see in the code immediately above. this will improve your program’s processing speed to a considerable degree. the PropagateMRUs() method may require custom expansion by your code to propagate the file list to any other menu that requires them. CStr(Idx . NOTE: The idea of maintaining more than one menu on a form sounds complicated. construct them each as individual menus. "0")) SaveSetting(Ttl. Consider the following version. all menu control is exclusively maintained within the MDI Parent Form. But the best part is.Default Return True End Function 'open the file 'read its contents into a textbox 'close the file 'dispose of resources 'report success NOTE: By the way. this is not the case.Cursor = Cursors.IO. they are profoundly faster than the already fast FSO interface..Cursor = Cursors. "Settings". all in one line of code. (invoke user routine to load file to a new child window) AddMRUFile(.Enhancing Visual Basic .FileName) 'add it to the MRU list if the file loaded OK End If End With End Sub Page –161– .Dispose()” commands.IO. 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 System. we can simply replace “.CheckPathExists = True 'ensure path exists .DefaultExt = ".0 – David Ross Goben NOTE: Also notice the four highlighted lines in the above listing. and we will have accomplished the very same thing.txt" 'set default extension . to complete this example code for the MDI Parent Form. ByVal e As System. as with the VB6 version.FileSystem.. Lastly. notice that we could have also opened our StreamReader using the more traditional command “Dim TS As System.Filter = "Text Files|*. Second. if we are in fact going to read the entire text file in one go.FileSystem.CheckFileExists = True 'ensure the file exists .ValidateNames = True 'only valid Win32 filenames .txt" 'define filter and flags .txt" 'just for show. to highlight that only text files are allowed If .Close” and “TS.ReadAllText(Path)”.Object. First.Text = TS.ShowDialog() = DialogResult.Title = "Open API Text File" 'title for dialog box . and without instantiating an object that we will afterward have to dispose of.ReadToEnd” with “. the following reacts to an “Open…” menu entry. we can delete the declaration of the TS StreamReader altogether.Click With New OpenFileDialog1 . where we are reading the whole text file in.NET Beyond the Scope of Visual Basic 6.OpenTextFileReader(Path)”.FileName = "*.EventArgs) Handles mnuFileOpen.Cancel Then Exit Sub 'if the user hit the Cancel button on the dialog 'Otherwise.StreamReader = FileIO.Text = FileIO. create a new child window If LoadFile(.. After doing that.FileName) Then 'if the file loaded. along with the “TS. save the current form size to establish its minimum size.Height = MyFormHt End If 'below minimum width size in pixels? 'set to minimum width if so 'below minimum height size in pixels? 'set to minimum height if so But if we implement such a primitive solution. ByVal e As System. then all seems well. The quick and dirty solution was to check for the form’s width going below a predetermined minimum size. They are referred to as magic numbers because. these numbers may end up doing seemingly magical things to your application. However. you should replace all instances of such hard-coded values with labels. until the user finally abandons the resizing attempt.Load 'place other initialization code here 'At the end of the Load event. such as MyFormWd and MyFormHt. to simply set the form’s width back to the minimum size. Page –162– . when you modify and expand your code later on.Width 'save form's initial display width MyFormHt = Me. the form begins to bounce like a crazed Tasmanian Devil with hiccups as the form’s Resize event continuously tries to re-inflate its dimensions to the minimums. Hence. Under most circumstances you should declare them as constants.Width < MyFormWd Then Me.0 – David Ross Goben Flicker Free VB. you really should not add “magic numbers” to your programs.Height = 500 End If 'below minimum width size in pixels? 'set to minimum width if so 'below minimum height size in pixels? 'set to minimum height if so Of course.NET has actually solved the whole VB6 issue of maintaining a form’s minimal/maximal size by implementing the simple-to-use MinimumSize and MaximumSize Form properties. we get an unprofessional result: If the user sizes the form larger than the minimums.Enhancing Visual Basic .NET Form Resizing Although VB.Height < 500 Then Me. You would do likewise for the form’s height. If you needed to maintain a minimum size to a sizable VB form.Height < MyFormHt Then Me. which usually requires frequent revision as a project develops. MyFormWd = Me. except maybe during early developmental testing. You could of course write code for this in a flash. like this: Private MyFormWd As Integer Private MyFormHt As Integer 'stores minimum form width in pixels (startup width) 'stores minimum form height in pixels (startup height) '*********************************************** ' myForm_Load '*********************************************** Private Sub myForm_Load(ByVal sender As System. I am maintaining this article. 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. and if it did. which are applicable to a broad range of uses. because it clearly demonstrates the differences between VB6 and VB. where for either you can set a Width and/or Height subproperty to defined a form’s minimum size and/or maximum size. the previous primitive form size limit processing code can be updated to the following: If Me. traditionally you placed code in the form’s Resize event to ensure that it had not been sized below specific minimum dimensions (but excluding when the user might minimize the form to the taskbar. due to your maybe having imposed different sizing limits in more visible code regions elsewhere. such as the heading of your application. assigning them the startup size of your form at the end of the Load event.NET subclassing. The core of such code generally looked something like the following: If Me.Width = 300 End If If Me.NET Beyond the Scope of Visual Basic 6.Width < 300 Then Me.Height 'save form's initial display height End Sub Either way. of course).EventArgs) Handles MyBase.Object.Width = MyFormWd End If If Me. is to instead declare them as variables. such as the above form boundary dimensions 300 and 500. if they size the form below either of the minimums. You should also declare these constants in an easy-to-find location. first written for VB6. NET)..Height = MyFormHt 'set to minimum height if so (this also spawns another Resize event) End If End Select 'other control arrangement code goes here. we will ignore other form size processing while the form is too small with an Exit Sub instruction.NET Beyond the Scope of Visual Basic 6.Minimized 'if the form is minimized. This act forces another invocation of the Resize event for the simple reason that the form has been resized again.Normal 'if the form is in a sizable state. ByVal e As System..Height < MyFormHt Then With Me..Width is checked for being below the minimum width (If Me. ByVal e As System. Furhter. We finally re-enable (hence. the form will be constantly forced to internally resize to the user-selected dimensions. suppose the user had sized the form to where its dimensions are below both the minimum width and height ranges.EventArgs) Handles Me. Page –163– . When Me. naming it something like tmrResize.Resize Select Case Me. 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. If Me. the flicker is made all the worse by the fact that additional invocations to the Resize event are made. thus repeatedly imposing a fresh Resize event. then do nothing Exit Sub Case FormWindowState. 'if the user sizes the form below desired dimensions. change the above Resize event code to the following (the updated code is flagged by the highlighting): '*********************************************** ' myForm_Resize ' React to the form dimensions changing '*********************************************** Private Sub myForm_Resize(ByVal sender As Object. which will do the same thing. End Sub From code such as this.WindowState Case FormWindowState..Width < MyFormWd Then 'is the form is below the minimum width size in pixels? Me.. If Me. For now. We then let the display update with a DoEvents.0 – David Ross Goben The Resize event would contain code similar to the following. then the form width is reset (Me.Enabled = True 'restart timer (this resets its timing interval) End With Exit Sub 'let timer event code perform the actual resizing work End If End Select 'other control arrangement code goes here. but at this point we have not even got to the form’s height test. while a single Resize event is still being processed.Enhancing Visual Basic .Width < MyFormWd) and it is found to be below MyFormWd.EventArgs) Handles Me. Notice also that we have removed the size correction code.Width < MyFormWd OrElse Me. reset) the timer. but potentially by both. or one quarter of a second) and ensure that it is initially disabled (by default it should already be disabled under VB.Resize Select Case Me. until the user at last restores sanity by releasing the mouse button.Width = MyFormWd 'set to minimum width if so (this also spawns another Resize event) End If If Me. We will be moving that to the Timer’s Tick event.Normal 'if the form is in a sizable state. The Minimalist Solution for Form Resizing Flicker The simplest solution to the form flicker problem is for you to add a Timer control to your form. Set its interval to 250 (250 milliseconds.tmrResize 'smooth w/timer .Enabled = False 'turn timer off Application.Width = MyFormWd).WindowState Case FormWindowState. then do nothing Exit Sub Case FormWindowState. End Sub We first disable the timer.DoEvents() 'screen catch up . not only because we had invoked it by resizing the form below the minimum in one direction. in case it was previously enabled by our Resize event. Next. which is also the source of the flickering: '*********************************************** ' myForm_Resize ' React to the form dimensions changing '*********************************************** Private Sub myForm_Resize(ByVal sender As Object.Height < MyFormHt Then 'is the form below the minimum height size in pixels? Me. we can end up re-invoking the event up to two more times! For example.Minimized 'if if form is minimized.. such as not forcing proper control refits until after the mouse pick button is actually released by the user. as long as the mouse pick button is held down. 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 System. even if you pause moving the mouse while you are resizing the form to be too small. due to us resetting the timer every time the resize even detects that the form is less than minimum. This next enhancement needs to have a Boolean flag declared at the top of the form. form resize handling suddenly appears to be a whole lot more professional-looking. the Right Mouse Button is internally treated as the Left Mouse Button at the point that our P/Invoke is used to look at it. MyFormWd = Me.Height < MyFormHt Then 'is the Me.Load 'place other initialization code here ' 'At the end of the Load event. then do nothing If Me.EventArgs) Handles tmrResize. as needed If Me. our P/Invoke code will work for both of these cases. but this is deceiving.Object. To do that. As long as the cursor is moving during a user resize.Width < MyFormWd Then 'is the Me.Height = MyFormHt 'set to End If End Sub form below the minimum width size? minimum width if so form below the minimum height size? minimum height if so And that is all there is to the minimalist solution. Hence. Following are our updated Tick and Resize event methods (the new enhancements are flagged by darker shading): Private MyFormWd As Integer Private MyFormHt As Integer 'store minimum form width in pixels (startup width) 'store minimum form height in pixels (startup height) Private Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Integer) As Short Private Const VK_LBUTTON As Integer = &H1 'Left button virtual key (you can also use Keys. 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.Width 'save form's startup width MyFormHt = Me. we will need to add a single P/Invoke to our program that will make the transition appear to be even smoother. NOTE: The so-called Mouse Pick Button is usually the Left Mouse Button.EventArgs) Handles MyBase. Adding a Little Pizzazz to the Minimalist Solution If you want to add a little pizzazz to this. the form will not automatically spring out to its minimally defined sized after a quarter of a second.Object. for a left-handed or right-handed mouse configuration. Now.Minimized Then Exit Sub 'resize to minimum dims.Height 'save form's startup height Page –164– . 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.0 – David Ross Goben Next. ByVal e As System. Adding More Pizzazz to Build a More Stable Solution Another enhancement can jazz this up by making it more stable than with just the above refinement.WindowState = FormWindowState. ByVal e As System.Width = MyFormWd 'set to End If If Me. You now have a smoother resizing form. this is exactly the same as using Keys. save the current form size to establish its minimum size. which will inform the Resize event when the timer event alters the form. the quarter-second timer will not trigger its event.Enhancing Visual Basic .Enabled = False 'if the form is minimized.LButton instead) To take advantage of it. You can declare the new P/Invoke at the top of your form. On a mouse oriented for left-handedness. named something like bResize. all you have to do is make a minor modification to the above code.NET Beyond the Scope of Visual Basic 6.Tick 'turn the timer off Me.tmrResize. without change. The GetKeyState() P/Invoke can be used to quickly check to see if the mouse pick button is pressed or not.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 System. implementing advanced P/Invoke invocations. it is even easier.Enhancing Visual Basic . far beyond what we have previously demonstrated. End Sub Anything beyond these enhancements is normally outside Beginner or Intermediate levels. except that there you will always need to remember to save any modified code before running it.EventArgs) Handles tmrResize. But having said that.Object.Width < MyFormWd OrElse Me. and other topics.Minimized Then Exit Sub bResize = True 'block the resize envent that will occur if we resize the form below 'resize to minimum dims. then this also means that an lParm parameter will.NET. Page –165– . which is why it is always accompanied by the additional parameters wParam and lParam.Height = MyFormHt 'set to End If bResize = False form below the minimum width size? minimum width if so form below the minimum height size? minimum height if so 'safe now.Width = MyFormWd 'set to End If If Me. New System.Resize If bResize Then Exit Sub 'if the timer is resizing the form. 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. then do nothing Exit Sub Case FormWindowState.WindowState Case FormWindowState.Width < MyFormWd Then 'is the Me. and other Resize event processes have not yet been handled.Height < MyFormHt Then 'is the Me. ByVal e As System. even if we did not actually resize it here. or if the user sizes the form below desired dimensions.0 – David Ross Goben End Sub '*********************************************** ' Form_Resize ' React to the form dimensions changing '*********************************************** Private Sub Form_Resize(ByVal sender As Object. by implementing a technique such as the above.WindowState = FormWindowState. However. and its final look is truly professional-grade..NET Beyond the Scope of Visual Basic 6. 'if the left mouse button is down.Height < MyFormHt Then With Me. we will tag the message as handled by setting the function’s return value to zero. 'We invoke it here. and then copy the memory back.tmrResize .. then ignore handling the resize event Select Case Me. then do nothing If Me. Indeed..Enabled = False 'disable timer Application. so unblock the resize event from being processed Form_Resize(Me. modify its contents.Minimized 'if the form is minimized. 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. as needed If Me.WindowState 'other control arrangement code goes here. If GetKeyState(VK_LBUTTON) < 0S OrElse Me. this process may be more complicated than I just described. I suppose it is not as complicated as one might suppose. End Sub '*********************************************** ' tmrResize_Tick ' Check form for being too small '*********************************************** Private Sub tmrResize_Tick(ByVal sender As System.. 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 in other respects. If the WM_GETMINMAXINFO message is found.DoEvents() 'let screen catch up . which can involve pixel math.Normal 'if the form is in a sizable state.Tick 'exit this Tick cycle if the mouse pick button is still being held down If GetKeyState(VK_LBUTTON) < 0S Then Exit Sub 'turn the timer off Me.EventArgs) Handles Me. Because we are handling this message for the form.tmrResize. But note that the system can also deliver a message that is accompanied by additional data. system structures.EventArgs()) 'now force processing the form resize event (processes OTHER than OUR resize processing).Enabled = False 'if the form is minimized.Enabled = True 're-enable timer End With Exit Sub End If End Select 'Me. because a Resize event had 'instigated this timer process. We will copy this data to a local duplicate structure. especially with VB. though only for this message.. ByVal e As System. copying memory.. be pointing to a systemresident MINMAXINFO structure. even if not used). unhooking it. The method must be declared using a format prototype to support a Windows Processor. wParam. If it matches a sought message ID for the matched form or control. The VB6 AddressOf keyword was essential to enabling us to reroute Windows messages to a custom-written message processing method. Under VB6. because each would be identified by a Windows Handle that could be checked against the hWnd parameter value. or in extreme cases. and so there is no need for further processing of it.NET. consequentially over-writing and detaching our own hook. and some VB6 history on subclassing… Under VB6. or WndProc for short. We do this by reinserting the previous hook into the system. and also so we can restore it to the stream when our application is finished to sever our connection to the queue. At the end of our application.NET Beyond the Scope of Visual Basic 6. But it all ended with you losing any unsaved work you might have done. Once the actual object (our form. We had gathered that previous hook when we had hooked our own WndProc method into the queue through the SendMessageLong() P/Invoke that had also returned the previous hook as a result to us. there was an additional inconvenience that you had to work around. we will process it. hang. _ ByVal uMsg As Long. even if you did not want it to. it is usually a 32-bit pointer to data associated with the Message ID. The solution to this problem was very simple: just place your interface code in a Module. but typically not in object-oriented environment like VB. _ ByVal wParam As Long. and another Select block under each hWnd match to check for individual uMsg matches that were specific to each individual form or control.NET: '*********************************************** ' VB6 WindProc Handler (Long integers are 32-bit) '*********************************************** Private Function WndProc(ByVal hWnd As Long. AddressOf targets work well from within modules. if used. usually) is identified. 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() P/Invoke. or worse. _ ByVal lParam As Long) As Long     Windows Handle identifying the form or control the current message is for. So we would declare our WndProc using a format similar to the following declaration. you can further check the actual message value provided by the uMsg parameter. it usually provides a 32-bit value associated with the message ID. By intercepting these messages. or if the message was not one that we were looking for. NOTE: Most VB6 WndProc methods implement a Select block to wrap the hWnd tests. we must unhook our WndProc method from the queue.0 – David Ross Goben Form Resize Subclassing Under VB6 But first: a review. as we will see later.Enhancing Visual Basic . VB6 subclassing also involved hooking a message processing method into the Windows Message Queue and when finished. VB6 would typically crash. or if none of our window handles match hWnd. you would get an “Invalid use of AddressOf operator” error. it would automatically perform that reboot for you. If we provide the service for that message. then we must pass those messages on down through the message stream. Such multi-object handler processing is especially applicable to procedural language environments like VB6. we could declare a function that can process several window objects. uMsg. If you tried. which is designed to pass the hWnd. if used. some classic VB6 code. noting that the VB6 Long integers are type Integer under VB. Page –166– . Using this header. If we do not handle it. Subclassing using AddressOf under VB6 was very efficient. we must return a value of zero that will duly inform the system that we have handled the message. which we must store for downstream use. subclassing was a valuable technique that enabled you to intercept Windows messages being sent to a form or control. If the form being subclassed received a message while VB6 was in Debug Pause. it would either require a system reboot to recover. and that was that you could not specify a target of the AddressOf operator that was within the body of a form. The Message ID. and lParam parameters to a specified address (the old hook) in a format compatible to the WndProc prototype. you could write your own code to change or extend the behavior of that form or control. but it also made debugging a VB6 project extremely difficult. POINTAPI). do not use ptMaxSize As PointAPI 'The maximized width (x member) and the maximized height (y member) of the window ptMaxPosition As PointAPI 'The left (x member) and top (y member) of the maximized window ptMinTrackSize As PointAPI 'The minimum tracking width (x member) and the minimum tracking height (y member) of the window ptMaxTrackSize As PointAPI 'The maximum tracking width (x member) and the maximum tracking height (y member) of the window End Type ' ' 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. into a module file.ptMinTrackSize 'set min form dimensions in pixels . like the following. Otherwise. ByVal uMsg As Long. lParam) 'then let the system handle this message for now Exit Function End If Dim MnMxInfo As MINMAXINFO 'else set aside a local copy of the MINMAXINFO structure Call CopyMemory(MnMxInfo. This variable will be used to safely store the old message hook address (covered shortly by the hooking method.Height \ Screen.Height \ Screen.ptMaxPosition 'Top left when maximized .Enhancing Visual Basic .y = MyFormHt \ Screen.x = 0 . ' Place this method.x = MyFormWd \ Screen.y = 0 End With With . listed later. _ Page –167– . This will be filled by 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. into a Module file. wParam.Width \ Screen.0 – David Ross Goben In more practical terms. which will address all the above requirements: Public mPrevHook As Long Public MyFormWd As Integer Public MyFormHt As Integer 'store previous Windows Message Queue hook here (default value is zero. Hence.TwipsPerPixelY 'form height in pixels End With With . wParam. to support it. we would start to flesh out our VB6 subclass code by writing a WndProc method. and a system message constant. but if the form dimension limits have not been set yet.TwipsPerPixelY 'screen height in pixels End With With .TwipsPerPixelX 'allow work desktop area width for max .TwipsPerPixelX 'form width in pixels (\ = integer division) . ' pass the message on through the message queue MyWndProc = CallWindowProc(mPrevHook. in turn. hwnd.Width \ Screen. the above MyWndProc() function also needs a structure.y = Screen. we must also add the following short block of code above the previous WndProc code. lParam. and its support code. so check for the message being one we want to trap Case WM_GETMINMAXINFO 'sizing message for this form? If MyFormWd = 0 Then 'yes. 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. '*********************************************** Private Function MyWndProc(ByVal hwnd As Long.hwnd 'is it our form? (be sure to reflect the ACTUAL name of your form) Select Case uMsg 'yes.x = Screen. LenB(MnMxInfo)) 'now copy system MINMAXINFO structure to our local copy With MnMxInfo With .TwipsPerPixelY 'allow work desktop area height for max End With End With Call CopyMemory(lParam. lParam) End Select 'uMsg End Select 'hwnd End Function As you can see.x = Screen. require yet another structure. uMsg. _ ByVal uMsg As Long. MyWndProc = CallWindowProc(mPrevHook. WM_GETMINMAXINFO. separate from our form: '*********************************************** ' VB6 Subclassing WindProc Handler ' NOTE: This method will not work under VB. ByVal lParam As Long) As Long 'handle messages Select Case hwnd 'check which form the message is for Case Form1. MnMxInfo. uMsg. HookWin()).NET Beyond the Scope of Visual Basic 6. it will call for a procedure-level Long variable named mPrevHook.y = Screen. a function named CallWindowProc() (another P/Invoke declaration). for our WndProc to work. hwnd. This will be filled by by Form_Load event 'store minimum form height in pixels (startup height). _ ByVal hwnd As Long. NOT into a form.NET. an error will be generated. inactive) 'store minimum form width in pixels (startup width). ByVal wParam As Long.TwipsPerPixelX 'screen width in pixels .ptMaxTrackSize 'set max form dimensions in pixels . MINMAXINFO (which will itself.ptMaxSize 'Max size . Our WndProc also requires a function named CopyMemory() (a P/Invoke declaration). Lastly. PrvhWnd) PrvhWnd = 0 'disable previous handle address End If End Sub After adding all this code.NET Beyond the Scope of Visual Basic 6. with VB. you now have subclassing into the Windows Message Queue under VB6. Also.hwnd. which I usually name HookWin() and UnhookWin()(insert these methods below the above SetWindowLong() method): '***************** ' HookWin(): Subclass hwnd into Windows Message Queue processing '***************** Public Sub HookWin(ByVal hWnd As Long.Enhancing Visual Basic . it will not fatally crash. Add the following line to your form’s Unload event: '*********************************************** ' Form_UnLoad '*********************************************** Private Sub Form_Unload(Cancel As Integer) Call UnhookWin(Me. unlike Windows Message Queue processing under VB6. save previous hook for later End Sub To unhook our WndProc is just as easy. Second. overwritng ours End Sub And with all that.NET is actually more accommodating to this task. and later unhook it when we are preparing to exit our application.NET you can debug step through subclassed code without a lot of special coding or conditional compilation. using a constant index named GWL_WNDPROC: Private Const GWL_WNDPROC As Long = (-4) '(insert this constant adjacent to the WM_GETMINMAXINFO constant.NET will further provide us with a ready-built interface to the Windows Message Queue. mPrevHook) 'Unhook our WndProc from the Windows Message Queue and put the old one back. offering you several additional service features through System.Runtime. it will report an error or simply hang in a wait state. we will also need to write two additional methods.NET Subclassing under VB.NET is less cumbersome than VB6. GWL_WNDPROC. _ ByVal dwNewLong As Long) As Long To use the above. We perform both of these tasks with another P/Invoke declaration named SetWindowsLong(). _ ByVal cbCopy As Long) All that is left is to hook MyWndProc() into the Windows Message Queue. Re-enable it when you are finished*** Call HookWin(Me. Page –168– . In our Form’s Load event. and VB. into the Windows Message Queue.Height 'save form's startup height in twips '***Disable the following line when you are editing and testing other application features. Either way.hwnd. _ ByVal nIndex As Long. we would add the following lines of code: '*********************************************** ' Form_Load '*********************************************** Private Sub Form_Load() MyFormWd = Me. and right within the form’s code. _ ByRef hpvSource As Any. VB. to actually hook our WndProc method into our application is fairly straightforward. First. _ 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. if the code is incorrect. ByRef PrvhWnd As Long) PrvhWnd = SetWindowLong(hWnd.0 – David Ross Goben ByVal wParam As Long. you can easily stop the program and return to IDE control. MyWndProc. Form Resize Subclassing Under VB. ByRef PrvhWnd As Long) If CBool(PrvhWnd) Then 'if there is something to disable Call SetWindowLong(hWnd. 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. mPrevHook) 'Hook our WndProc method.Width 'save form's startup width in twips MyFormHt = Me. AddressOf MyWndProc) End Sub '***************** ' UnhookWin(): remove subclass hook '***************** Public Sub UnhookWin(ByVal hWnd As Long.InteropServices that make subclassing a form a lot easier. GWL_WNDPROC. under VB. making WndProc development under VB6 a long and arduous task. and its StructureToPtr() method to safely copy our local structure back to system memory.. We will of course need to declare our constant to identify WM_GETMINMAXINFO.asp). but it was very difficult to debug that code in the meantime until Microsoft released DBGWPROC.org/tools/ControlsAndComponents. A characteristic VB. which gave VB6 capabilities that were precursors to those already built into VB. We do not need to worry about unhooking it because our override is automatically unhooked when the application ends by the base class.NET.Forms.mvps. but without adding any unnecessary overhead to your finished product or distributing an extra component. because in our situation we are parsing for a message ID named WM_GETMINMAXINFO (integer value &H24. except for the two minimum sizing variables. Case Else MyBase. Peterson’s Classic Visual Basic website (http://vb. VB. Typically.Result = IntPtr. Therefore. As such. Here is the full. Peripherally. or 36 decimal).Msg 'check current message in queue for this form Case WM_MISCMSG1 'a match for a sought message? 'message support code goes here.WndProc(m) 'let the system handle messages we did not trap End Select End Sub As you can see. but be sure to name the form myForm. We will use the System Runtime Interop Services memory marshalling method PtrToStructure() to safely copy a system structure to our local copy.Enhancing Visual Basic .. '*********************************************** Protected Overrides Sub WndProc(ByRef m As System.NET subclassing project does not require any of the code we had previously visited in this article. The second thing we need to do is to define support structures and add a Windows Message Queue processor.NET Beyond the Scope of Visual Basic 6. We will also need to declare the structure MINMAXINFO.DLL for VB6..NET does not require us to perform any hooking or unhooking. However.NET we never have to worry about setup or takedown at all. each form provides us access to its own WndProc interface. basic form code to fully implement subclassing form dimension limit setting under VB. as well as a local copy of the MINMAXINFO structure. But even with all that. ' We will check our form for specific system' defined messages and handle them ourselves..Zero 'inform system that we handled this message 'Insert other CASE parsers here. it amounts to very little additional code. we have cut out all the P/Invokes (VB6 APIs) that were previously needed for subclassing under VB6.Windows. You can still find this tool at Karl E. because this new VB. It is a slight pain to use and rather frustrating for the beginner. First.. So it had to run correctly. below. which had caused plenty of crashes under VB6 when code was not yet perfected. VB. for compatibility to our form class code. A program shell for a typical VB. NOTE: Microsoft eliminated the VB6 crashes in 1997 by providing a free debugger DLL for VB6 called DBGWPROC.NET.NET WndProc method is as follows: '*********************************************** ' WndProc() ' Override MyBase. Hence. Unlike VB6.Zero 'inform system that we handled this message Case WM_MISCMSG2 'a match for another sought message? 'message support code goes here. We can simply get to the business of parsing for the required form messages.NET: Page –169– . 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).Message) 'establish our hook into the message queue 'Do processing for messages for current form Select Case m.0 – David Ross Goben Under VB. or it did not. we also need our variables MyFormWd and MyFormHt to store our minimal form sizing values.. the following block of code must be added to each of your forms. you could not single-step debug through the WndProc code.NET WndProc code is simple.NET WndProc method for a form eats up very little program code.WndProc with our own method.DLL that actually allows you to do what I just said you could not do – single-step through VB6 code that is subclassed. this part is so easy – it is as easy as tripping over your own feet when you are trying to look ‘cool’ when a gorgeous member of the opposite sex enters the room. you should simply start a brand new project. especially because while you had a WndProc method subclassed into the Windows Message Queue.Result = IntPtr. m. This DLL enables you to debug VB6 normally while a subclass is active. m. we will need to apply the additional code to support it. True) 'Return 0 to tell the system to ignore further processing on this message because we have handled it m. we instead use a VB.LParam) Marshal. Although the VB.WndProc(m) ' then let the system handle this message for now Exit Sub End If Dim mMinMaxInfo As MINMAXINFO 'we can handle it.Screen.Forms.Computer. notice that there was no need to hook or unhook our WndProc method. Notice also that we do not have to pass the Page –170– .WndProc(m) 'let the system handle messages we did not trap End Select End Sub End Class First.NET Point instead of POINTAPI) Dim ptMaxSize As Point 'The maximized width (x member) and the maximized height (y member) of the window Dim ptMaxPosition As Point 'The left (x member) and top (y member) of the maximized window Dim ptMinTrackSize As Point 'The minimum tracking width (x member) and the minimum tracking height (y member) of the window Dim ptMaxTrackSize As Point 'The maximum tracking width (x member) and the maximum tracking height (y member) of the window End Structure '*********************************************** ' myForm_Load '*********************************************** Private Sub myForm_Load(ByVal sender As System.Top End With With .y = My.Zero Case Else MyBase.Message) 'Do processing for messages for current form Select Case m.Msg Case WM_GETMINMAXINFO 'is it our sought message? If MyFormWd = 0 Then 'yes.ptMaxTrackSize 'set max width and height for form .Enhancing Visual Basic . but if the form dimension limits have not been set yet.Windows.x = MyFormWd 'set minimum width .ptMaxSize 'Max size .y = My.Screen. ' This is much easier than the way we had to do it under VB6.Screen. we do not need to test for a Window handle for the form.Object.NET Point.InteropServices Public Class myForm Private MyFormWd As Integer = 0 Private MyFormHt As Integer = 0 'BE SURE TO IDENTIFY YOUR FORM NAME HERE 'store minimum form width in pixels (startup width).Computer.Screen.Height 'allow work desktop area height for max End With With . Init to null 'store minimum form height in pixels (startup height).Computer.WndProc) ' NOTE that this is a SUB.WorkingArea. not a FUNCTION '----------------------------------------------' This WndProc is designed for exclusive use by the current form. the base process has already done that for us.LParam.WorkingArea. to_structure) 'Update the current data with our new form minimum and maximum With mMinMaxInfo With .WorkingArea.Computer.Height 'save form's startup height End Sub '*********************************************** ' VB.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. MyFormWd = Me.Result = IntPtr.GetType) 'parameters (from_address. Init to null 'message to look for in the Windows Message Queue Private Const WM_GETMINMAXINFO As Integer = &H24 'Structure used to define a form's minimum and maximum sizing (Supports WM_GETMINMAXINFO message) Private Structure MINMAXINFO Private ptReserved As Point 'Reserved.Width 'save form's startup width MyFormHt = Me. True) 'parameters (from_structure.x = My.EventArgs) Handles MyBase.Computer. Additionally. save the current form size to establish its minimum size. where we had to ' hook and unhook the WndProc method.0 – David Ross Goben 'ADD SUPPORT FOR BLOCKING THE USER FROM RESIZING THIS FORM BELOW ITS STARTUP DIMENSIONS ' Option Strict On Option Explicit On Imports System.Runtime. its data footprint is in fact identical to its more primitive POINTAPI cousin. do not use (NOTE: we can use a VB.WorkingArea. mMinMaxInfo.Width 'allow work desktop area width for max .ptMinTrackSize 'set min size for form .x = My.y = MyFormHt 'set minimum height End With With .Screen. so set aside a local copy of the MinMaxInfo structure 'safely copy system MinMaxInfo data from lParam into our local copy of the structure Marshal.NET version of this structure features innumerable methods and constructors.Left .NET WndProc Intercept method ' (overrides default MyBase. ByVal e As System.Screen.LParam.NET Beyond the Scope of Visual Basic 6. MyBase.StructureToPtr(mMinMaxInfo. as we would have had to do under VB6 using P/Invokes. because we are overriding the default WndProc method already declared for the form through its base class (MyBase.WorkingArea.Width 'allow work desktop area width for max . and crashes were common during development '*********************************************** Protected Overrides Sub WndProc(ByRef m As System.PtrToStructure(m. Also. to_address.WorkingArea.ptMaxPosition 'Top left when maximized .x = My.Computer. m. notice that instead of using the POINTAPI structure to declare a Point coordinate.y = My.Load 'place other initialization code here ' 'At the end of the Load event.WndProc()). you may choose to keep just myForm_Load in the main form). Notice further that in order to pass something on through the Windows Message Queue. Conclusion And that is all there is to it. also be sure to declare these items as Public or Friend instead of as Private. With these parameters. Finally.NET WndProc is Heaven compared to the medieval torture we had to go through while developing subclassing under VB6. we need only pass the single message packet structure (m). Next.NET Framework safely do that for us). if my form file was named myForm. Partial Class myForm End Class And I would then add my subclassing code to this partial class file. Lastly. Indeed. declared as type Message to the base class via MyBase. on top of that.NET form sports two new properties not available to VB6 named MinimumSize and MaximumSize. because we are overriding one of its base class methods. the structure declared in the form. I simply create new Module file. MinMaxInfo. you can completely forego the above examples. where you 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 you require. I might name my new Module file myFormSubclass. Further. notice that this code must be placed within our Form class. you can create an auxiliary form file with a different name. But take my word for it. I would completely replace the default Module myFormSubclass code block added to the Module with the following block. An alternative is to just stow the subclass code away in a #Region block within your form code (Declare #Region "Subclass".0 – David Ross Goben address of our WndProc method to the system. for example). sight unseen. naming it similar to my form. NOTE: If you do want to place this code within a separate file (typically. remember that a VB.vb. because all those things are already done for you.Enhancing Visual Basic . this interface did not require the direct use of any P/Invokes on our part at all (we are letting the . converting it into an additional myForm class file (the Partial verb tells the compiler to include this code in the primary myForm class code): 'Create an extension to Class myForm that will be integrated as a component of the main body of the myForm Class.WndProc(m). But in that case.vb. For example. conveniently and safely storing my subclass code in a separate file that will be merged with the default myForm class code during compilation. as well as the WM_GETMINMAXINFO constant can be declared in a separate module file where all of your forms can share them. Me. rather than having to use the CallWindowsProc() P/Invoke. this exercise in exploring form subclassing is a useful example to demonstrate how other subclassing tasks not supplied by VB. To some.NET Beyond the Scope of Visual Basic 6. Page –171– . However.NET can be added. VB. this WndProc code might at first seem a bit more intimidating than the initial techniques offered at the start of this article. NET Until VB2010.Enhancing Visual Basic . But that. For example. run-time selection. you will find its Location and the Size members. I maintain a PictureBox control throughout the development of my application named picTemp with its Visibility property set to False. To that end. drop it on your form.com/en-us/vbasic/aa701257. Line and Shape controls. Under Location. and even run-time events. the advantage of the VB6 shape controls was that you could simply visually place lines. for free and without subscription restrictions. if installed).microsoft. and circles on a form and forget them. size it. The hard part. and RectangleShape that can be used to replace VB6 Shape controls (the VB6 Upgrade Wizard even used them. is in figuring out coordinates and dimensions. But because I actually want to draw a line with a beveled 3D effect. VB. Even so. this package added many new features. News of Free VB. I move the top-left corner of picTemp to the start point. This version of the Visual Basic Power Packs also includes updated versions of the previously released Visual Basic Power Packs. you have a new Toolbox group called “Visual Basic PowerPacks”. and go on to the next thing. To determine this information really fast.” Once installed. In addition to duplicating the behavior of the old VB6 Shape controls. giving you more flexibility and customization than standard grid controls. roll up your sleeves. the only value I need is its Width. is the easiest part. Many developers will cringe. The DataRepeater control enables you to use standard Windows Forms controls to display rows of your data in a scrollable container. From it. These include gradient fills. but what looks to be a 3D engraved line in the location shown for a line in the above image. and 121 is the Top position for its top-left corner. suppose I need to draw not just a line.NET did not ship with shape controls like those that VB6 featured. and then drag its bottom-right corner across the screen by using its shape grabbers (the tiny boxes) to fine-tune its location. and to Paint in VB. the Visual Basic Power Packs feature the controls LineShape. Now. the value 19 represents the Left pixel position. To the right I am demonstrating using the LineShape control.aspx. squares. which is set to 513. but it is actually quite easy. Drawing Lines from the Paint Event But even with all the power offered by the Visual Basic Power Packs. In the Size properties. This is the pitch from Microsoft: “Microsoft Visual Basic Power Packs 3. for me.0 now includes a new DataRepeater control. First. The second line will be rendered one pixel below the first with a lighter color. I started at the upper-left of the displayed rectangle. though the GDI+ 2D graphics that came with VB. you can just grab a control. from the Microsoft Developer Network (MSDN) at http://msdn. Page –172– . or so I am told. I now have all the information I need in order to draw a simple line on my form.NET that allowed placing shapes on a form much as you did under VB6. placing a horizontal line on a form.NET Line and Shape Controls Even though pre-VB2010 releases did not ship with shape controls. Consider this illustration: As you can see. I still would rather roll my own graphics for the simple reason of significantly faster rendering. I will actually draw two lines. OvalShape.NET were a good substitute.NET Beyond the Scope of Visual Basic 6. set parameters. The Visual Basic Power Packs are available online for pre-VB2010 users. even as I did in VB6. Microsoft did offer free shape controls for VB. If we look at the properties for this control. aghast at the idea of having to draw their own lines and shapes.0 – David Ross Goben Easy Ways to Draw Lines and Shapes. and shaped it over the form. making them even easier to use and redistribute with your application. the PrintForm Component and the Printer Compatibility Library. All are included in a single assembly. Red) Dim pn2 As New Pen(Color. 531. which is not the usual eventArgs. or X2=531. thanks to auto-promotion. Still another is DashStyle.DrarkGray' here) 'draw the first line at Left. 121. This opens up an entire world of drawing functionality. The Paint() event. White (we could have just used 'Pn = Pens. But first. 513. The compiler optimizer would convert them to a single integer. or have meaningful comments (or any comments) in their code to help explain what they are doing.Drawing namespace. One Pen property is Color. read-only. 121 (we will eliminate all note-taking and math soon!). the default. For drawing lines. Y1=121. Fortunately. we are saved by the fact that most any object has properties.0 – David Ross Goben To draw these new lines. Dim pn As New Pen(Color. The second are also easy. 19. The result speaks for itself. creating a Pen is also very easy. 122. which drawing functions provide). For an engraved effect. Although we could create separate pens for both. its most important member is e. which allows us to go bug-nuts and draw with fancy patterns. Left+Width-1. we need something to draw them with. However.White e. lower line is actually rather subtle.DrawLine(pn. Top 'set pen to the second color. system-defined pen (DO NOT use pn3. we can compute the first X/Y values quite easily: X1=19. not to mention that the “line” would no longer have an engraved appearance as it does in the image to the right: NOTE: Some people prefer LightGray to White. To draw.Dispose to release its resources when finished) 'Use a 1 pixel thick pre-existing. this is wasting resources. In my form code. its loss would be quite apparent if it was missing. Fortunately. In using the Location and Size properties noted earlier for our non-visible PictureBox. provides us with an interface to all the object’s GDI+ drawing capabilities through its “e” parameter. and we did not even have to set things up in the project properties or import any namespaces.DrawLine(pn. The only thing left to do is to have a means to draw the lines. that you only can modify an instantiated (“As New”) Pen. the X1 and Y1 offsets for the start point of the line and the X2 and Y2 offsets for the end point of the line. we will compute X2=19+513-1.Graphics (basically. ready-to-use pens are immutable (read-only). 19.Paint End Sub To draw. They know that the components they need to use are in the System. I select the form’s Events item in the left dropdown list at the top of the code page. but rather a PaintEventArgs parameter. Note. Well. we come to the part a great many new VB. I will be performing this simple task from within my form’s Paint event. Consider these three examples: Dim pn1 As New Pen(Color. Page –173– . Top.Color = Color. we need to use a Pen object.Paint 'create new pen object. set to Dark Gray (we could have just used 'Dim Pn As Pen = Pens. we need a hook into the form’s client area. and Y2 will be set the same as Y1.NET users assume is difficult. For us. 3) Dim pn3 As Pen = Pens. Another is Width.DarkGray. ByVal e As PaintEventArgs) Handles Me. but I can use integers instead. 1) e.White' here) 'draw the second line at same X coordinates.Enhancing Visual Basic . We now enter the following lines into our form paint event: Private Sub Form1_Paint(ByVal sender As Object.ForestGreen 'create a red pen that is 1 pixel thick (Use pn1. This almost sound like it might be easy. We usually do it in one of three ways – depending on how thick (how many pixels wide) we want the line to be.Graphics. to draw.Blue.Dispose to release its resources when finished) 'create a blue pen that is 3 pixels thick (Use pn2. but usually they do not bother spending much time explaining what it is that they did.Dispose() End Sub ByVal e As PaintEventArgs) Handles Me. Wow. it is. 531. the Graphics object offers us a DrawLine() method (among many). I will use DarkGray and White. but 1 pixel down in Y (Top+1) 'dispose of resource-allocated Pen object (do not do this is you used built-in Pens objects) NOTE: You could have specified 19+513-1 rather than 531. Although the white.Graphics. being a veritable playground for form or control drawing.Dispose to remove it!) All I need is a 1-pixel pen (the thickness parameter specifies a single-precision value. They want to draw stuff.NET Beyond the Scope of Visual Basic 6. I require two colors. with the Width of the object. The DrawLine() method expects 5 parameters: the Pen object and 4 values. It does not help that most online gurus might show code that does it. but they are not quite sure how to proceed to actually do a drawing. this is the object’s Device Context). 121) pn. and then choose the Paint event from the right dropdown. especially because we should dispose of them after use if we declared them “As New”. 122) pn. This presents me with the following event block: Private Sub Form1_Paint(ByVal sender As Object. however. we can benefit immensely from monitoring the values stored in this control’s Location (Top/left) and Size (Width/Height) properties.Paint 'declare a pen that is three pixels wide 'set up bounding rectangle 'draw a rectangle using a Pen object and a Rectangle structure 'dispose of used object What is more.Location. you can also leave the offset math intact and the compiler will optimize it to an integer value for run-time execution.Y. which creates a pointer to the read-only object. 3) Dim rec As New Rectangle(89.picTemp. referenced in the Pens collection.Print("Dim rec As New Rectangle({0}. Still. suppose we took our development picTemp PictureBox control (again. 531.Paint Dim pn As New Pen(Color.Print(Nothing) 'add a blank line to the Immediate Window display Dim rec As New Rectangle(89. 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.Size.picTemp.DrawRectangle(Pens.DrawRectangle(pn. 531.NET Beyond the Scope of Visual Basic 6. was that easy.Size. we can greatly simplify this entire process by inserting temporary code that will take full advantage of our hidden temporary picTemp PictureBox control. We can jump right into our paint event and use these exact same values without change. for the most part. we can take full advantage of the rectangle that our hidden temporary PictureBox. rec) 'draw a rectangle using a Pen object and a Rectangle structure pn.Paint e. and the Location and Size parameter values End Sub Now. rec) pn. In fact. which we must be sure to Dispose of afterward. either by themselves (Pens. picTemp. ByVal e As PaintEventArgs) Handles Me. Taking advantage of this.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.Enhancing Visual Basic . Me. we can shorten the above code to the following: Private Sub Form1_Paint(ByVal sender As Object. ByVal e As PaintEventArgs) Handles Me. To draw a simple rectangle only requires a Pen and the above four values as parameters to the DrawRectangle() graphics method provided by e. the advantage of using created Pen objects (Dim pn As New Pen(Color. _ Me. An advantage to using them is that you will not need to invoke a Dispose method to release their resources.Dispose() End Sub ByVal e As PaintEventArgs) Handles Me. X2. Yet.Height) Debug. 122) 'draw second line at same X coordinates. 83. {2}. the best option is to employ the 1-pixel-wide read-only pens provided by the system.Graphics. 89. Just use the left or right vertical side of your temporary PictureBox object as a target instead of a top or bottom horizontal line. Me. 89. or what? But we are getting ready to make it a whole lot easier! If you want a thicker border. 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. In my experiment.Red. you will of course have to declare a Pen along with a thickness value. 82) e. Dim pn As New Pen(Color. with its visibility set to False) and set it up as a rectangle in the middle of a form.0 – David Ross Goben NOTE: As indicated in earlier examples. 19.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. 89. {3})". we did not really need to specify a new pen object. occupies to make our work almost too easy. 82) 'set up bounding rectangle (replace this as needed by the debug result) e. ByVal e As PaintEventArgs) Handles Me.DrawLine(Pens.Graphics.X.ForestGreen).picTemp.ForestGreen)) is that you can alter its color. 89. but 1 pixel down in Y End Sub Drawing vertical lines is just as easy. 121. 121) 'draw first line at X1. {1}. 83.ForestGreen). Its Size properties report Width=89 and Height=82. Y1. To reiterate an earlier note.Graphics. 83. For example. Drawing Rectangles from the Paint Event Rectangles are even easier to draw than rendering individual lines. if you are going to use 1-pixel-wide solid lines.Print("'Prototype for target Rectangle declaration:") Debug. or as variables (Dim pn As Pen = Pens. and DashStyle as needs require. Me.Paint e. I noted that its Location properties reported X=89 and Y=83. 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. we could have used systemprovided pens.White. Y2 e.DarkGray. because these contents are exactly what we must provide to a rectangle to establish its top-left location and size.Location. 19.Width.DrawLine(Pens.Graphics. width.picTemp. 122. 82) 'use built-in Red pen.Dispose() 'dispose of created object End Sub Page –174– .Graphics.Red.DrawRectangle(pn. DrawLine(pn. 129) Dim sz As New Size(260.1 : Y1 = rec. Me.DrawRectangle(pn. rec) 'Runtime: draw ellipse '------------------------------------------------------------------------------------------------------------' TESTING. 89.Print(Nothing) 'add a blank line to the debug output display Dim ptH As New Point(.DrawLine(pn. and then paste right into your own code. Y2 As Integer: X1={0}: Y1={1}: X2={2}: Y2={3}". ptH) 'Horizontal line (top line of picTemp) e. Me. X1. we simply invoke the FillRectangle() method after invoking DrawRectangle(): Private Sub Form1_Paint(ByVal sender As Object.Graphics. X2.Width . X2.Height) Debug.Location. 82) But what if we want a filled rectangle? That is also as easy as spilling red wine on a white silk tablecloth. Y. Y. {2}. Y2) 'e.Graphics. {2}.Height):") Debug.Location.Location. which you can select. 83. ByVal e As PaintEventArgs) Handles Me. Y1. Point)) 'Runtime: rec using rectangle 'e. 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. sz)") Debug.Location.Size.Height .FillRectangle(Brushes.Width. Y2) 'disable in release Debug. rec) 'Rectangle e.DrawEllipse(pn.picTemp.Print("'Prototype for target Rectangle declaration:") Debug. X2.Graphics.Print(vbNullString) 'add a blank line to the Immediate Window display Dim rec As New Rectangle(89.X.Print("Dim sz As New Size({0}. Y2 As Integer : X1 = rec.Width. and Y2 declarations: Dim X1.Y. Me. Y1. Disable whole WITH block for Release code '------------------------------------------------------------------------------------------------------------With Me.DrawLine(pn. Y1.Height) Debug..Width .DrawRectangle(pn.1 'disable in release Debug.Y) and Size (Width.Location. rec) 'fill the rectangle with yellow pn.Yellow.Top : Y2 = Y1 + rec.Location.Print("Dim rec As New Rectangle({0}.picTemp. 'disable in release Dim X1. rec) 'draw a rectangle using a Pen object and a Rectangle structure e.DrawEllipse(pn.Y.Location.Dispose() 'remove created resource Sample Output: 'Prototype for Rectangle X1.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.Y) and Size (Width.Y + . instead of manually transcribe it all the time as we had done up until now.Height) Debug. .Print("Dim lc As New Point({0}. Because the Location and Size properties of our picTemp control are also Point and Size structures.Print("Dim rec As New Rectangle(lc. {1})". rec.Y) Debug.Size.Size. . sz) Page –175– code code code code code . I keep the following block of code in a text file. 89. _ Me.picTemp. Me. Height) 'Runtime: manually modify run-time values (X. . by specifying Location (X. 60) Dim rec As New Rectangle(lc.Size. by specifying Location (X.Size.Size. 82) 'set up bounding rectangle (replace this as needed by the debug result) e.Location.Graphics.1. Y2) 'Runtime: using absolutes (manually modify values X1.picTemp. Y1.Location.Black. .NET Beyond the Scope of Visual Basic 6. . .Graphics..Location. Because I always name my hidden temporary PictureBox “picTemp”.Print("Dim X1.picTemp. 260. we can specify them like so: Dim rec As New Rectangle(Me.Graphics.Print("'Or.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.Height . . . X1.Size.Print("'Prototype for Rectangle X1. {1})".Graphics. {1}.Size.X.X. 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.Graphics. ptV) 'Vertical line (left side of picTemp) e.DrawLine(pn.Size. . copy. . {3})". Height) 'e. CType(rec. and Y2 declarations:") 'disable in release Debug. we can fine-tune the shaping of the rectangle and test the results immediately.picTemp.Size) 'set up bounding rectangle based on picTemp.0 – David Ross Goben This will display the following helpful declaration in the Debug Output.X.Height): Dim lc As New Point(12. 83.X + . . 60) 'Or..picTemp.Location. {1}.Size) 'set up testing bounding rectangle based on picTemp Using temporary code like this (replace “Dim rec” in the above code with it). rec) 'Runtime: draw a rectangle 'e.Left : X2 = X2 + rec. Y1. Width. Width. . X2. rec) 'Ellipse (circle) End With pn. For that. Y1.Graphics.picTemp.Location. X2. X2.1) 'end location (bottom) of vertical line 'disable the following lines that are not needed for testing e.Paint Dim pn As New Pen(Color. Me.Enhancing Visual Basic .Location. Y1.Print("'Prototype for target Rectangle declaration:") Debug. 'Prototype for target Rectangle declaration: Dim Rec As New Rectangle(89. Y2 As Integer: X1=12: Y1=129: X2=259: Y2=188 'Prototype for target Rectangle declaration: Dim rec As New Rectangle(12.picTemp 'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G) Debug. X2.Print("Dim rec As New Rectangle({0}.Graphics. Y1.Location.Y) 'end location (right) of horizontal line Dim ptV As New Point(. 129.Width. X2.Location.Print(Nothing) 'add a blank line to the debug output display 'disable in release '------------------------------------------------------------------------------------------------------------Dim pn As New Pen(Color. {3})"..DrawRectangle(pn. have the same spacing as hatch style ForwardDiagonal. 89.Print("'Prototype for target Rectangle declaration:") Debug. we can also take advantage of the rectangle structure to support drawing ellipses. Specifies a 75-percent hatch. I can take the previous Paint event and change DrawRectangle() to DrawEllipse(). The ratio of foreground color to background color is 50:100. Specifies a 20-percent hatch. as you may have ascertained by examining the above code. Specifies a 80-percent hatch. The ratio of foreground color to background color is 20:100. And to fill it. picTemp. Specifies horizontal and vertical lines that cross. rec) 'draw an elipse that will use the rectangle to mark its bounds e. {1}.Print("Dim Rec As New Rectangle({0}.DrawEllipse(pn. are spaced 50 percent closer together than BackwardDiagonal. Specifies a 60-percent hatch. The ratio of foreground color to background color is 80:100. The ratio of foreground color to background color is 70:100. but are not antialiased. The most intriguing property of the brush is probably HatchStyle. picTemp. The ratio of foreground color to background color is 75:100.Enhancing Visual Basic . 82) 'set up bounding rectangle e. Specifies a 50-percent hatch.Graphics. Specifies a 10-percent hatch.X. Specifies a 90-percent hatch. {2}. This type of Brush feature a ForeColor property.Y.Red. The ratio of foreground color to background color is 25:100. which is the actual Brush color to paint the background. once we understand drawing rectangles with the DrawRectangle() method.FillEllipse(Brushes. Consider the following huge table of possible patterns: hatchstyle Patterns Description Horizontal Vertical ForwardDiagonal BackwardDiagonal Cross DiagonalCross Percent05 Percent10 Percent20 Percent25 Percent30 Percent40 Percent50 Percent60 Percent70 Percent75 Percent80 Percent90 LightDownwardDiagonal A pattern of horizontal lines. using DrawEllipse() to draw ellipses is just as easy (a perfect circle is drawn when the width and height parameters are identical).Height) Debug. A pattern of crisscross diagonal lines.Location.Graphics. The ratio of foreground color to background color is 90:100. In fact. Specifies diagonal lines that slant to the right from top points to bottom points. and a BackColor property. The ratio of foreground color to background color is 30:100. through which you can select your fill pattern. 83. rec) 'fill the ellipse pn. but they are not antialiased. Specifies diagonal lines that slant to the right from top points to bottom points. picTemp.Drawing2d namespace.Print(vbNullString) 'add a blank line to the Immediate Window ddisplay Dim rec As New Rectangle(89. change FillRectangle() to FillElipse(). Specifies a 40-percent hatch. A HatchBrush is found in the System.Size. are spaced 50 percent closer together than. and are twice the width of ForwardDiagonal.0 – David Ross Goben Drawing Ellipses (Circular Shapes) from the Paint Event Drawing ellipses is just as easy. ByVal e As PaintEventArgs) Handles Me. LightUpwardDiagonal DarkDownwardDiagonal DarkUpwardDiagonal WideDownwardDiagonal Page –176– . The ratio of foreground color to background color is 5:100. A pattern of lines on a diagonal from upper left to lower right. Now that we have played around with Rectangle structures to support drawing rectangles.Width. we use the DrawEllipse() method. and we will have just as suddenly mastered ellipses: Private Sub Form1_Paint(ByVal sender As Object. The ratio of foreground color to background color is 40:100.Location.Drawing. This hatch pattern is not antialiased.Yellow. In fact.Size.Paint Dim pn As New Pen(Color. and are twice its width.Dispose() 'dispose of used object End Sub Painting Backgrounds with Patterns You can paint (fill) your background with far more than just solid-color brushes. The ratio of foreground color to background color is 10:100. and are triple its width. 3) 'declare a pen that is three pixels wide 'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G) Debug.NET Beyond the Scope of Visual Basic 6. You can also specify special patterns through a HatchBrush. noting the obvious references to Ellipses. Specifies diagonal lines that slant to the right from top points to bottom points and are spaced 50 percent closer together than ForwardDiagonal. but are not antialiased. A pattern of lines on a diagonal from upper right to lower left. Specifies a 25-percent hatch. _ picTemp. Specifies a 70-percent hatch. {3})". but the lines are not antialiased. The ratio of foreground color to background color is 60:100. which specifies the Pen color to draw the pattern. A pattern of vertical lines. which is an enhanced Brush. Specifies a 5-percent hatch. we use the FillEllipse() method. Specifies diagonal lines that slant to the left from top points to bottom points and are spaced 50 percent closer together than BackwardDiagonal. To draw an ellipse. Specifies diagonal lines that slant to the left from top points to bottom points. Specifies a 30-percent hatch. Enhancing Visual Basic . have the same spacing as hatch style BackwardDiagonal. Specifies dashed horizontal lines. ByVal e As PaintEventArgs) Handles Me. Specifies vertical lines that are spaced 50 percent closer together than Vertical. and are triple its width.HatchStyle. Specifies horizontal lines that are composed of zigzags.ClientRectangle) Brsh. drawn with black over a yellow background Dim Brsh As Brush = New Drawing2D.Dispose()'dispose of created HatchBrush End Sub Page –177– . Specifies forward diagonal and backward diagonal lines that cross but are not antialiased. each of which is composed of dots. we have declared a new HatchBrush that is using a diagonal brick style. that cross.Yellow) In this example.HatchStyle. and is composed of larger pieces than SmallConfetti. Color. Specifies dashed vertical lines. that cross. which slants to the left from top points to bottom points. Specifies a hatch that has the appearance of horizontally layered bricks. Specifies hatch style SolidDiamond. The pattern is drawn in Black over a Yellow background. Specifies a hatch that has the appearance of spheres laid adjacent to one another.DiagonalBrick. Specifies vertical lines that are spaced 75 percent closer together than hatch style Vertical (or 25 percent closer together than LightVertical). that slant to the right from top points to bottom points. Specifies a hatch that has the appearance of a plaid material. Specifies horizontal lines that are spaced 75 percent closer together than hatch style Horizontal (or 25 percent closer together than LightHorizontal). Specifies a hatch that has the appearance of confetti. Me. Specifies vertical lines that are spaced 50 percent closer together than Vertical and are twice its width. Specifies a hatch that has the appearance of a checkerboard with squares that are twice the size of SmallCheckerBoard. each of which is composed of dots. Specifies a hatch that has the appearance of a checkerboard.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben hatchstyle Patterns Description WideUpwardDiagonal Specifies diagonal lines that slant to the left from top points to bottom points. LightVertical LightHorizontal NarrowVertical NarrowHorizontal DarkVertical DarkHorizontal DashedDownwardDiagonal DashedUpwardDiagonal DashedHorizontal DashedVertical SmallConfetti LargeConfetti ZigZag Wave DiagonalBrick HorizontalBrick Weave Plaid Divot DottedGrid DottedDiamond Shingle Trellis Sphere SmallGrid SmallCheckerBoard LargeCheckerBoard OutlinedDiamond SolidDiamond LargeGrid Min Max To take advantage of these many styles. Color. such as FillRectangle() or FillEllipses().Black.Yellow) 'flood fill the form background. To use the new brush. Specifies a hatch that has the appearance of a trellis.Black. For example: Private Sub Form1_Paint(ByVal sender As Object. Color. Specifies a hatch that has the appearance of a woven material.HatchBrush(Drawing2D. you need to declare them as a new HatchBrush. that slant to the left from top points to bottom points.HatchBrush(Drawing2D. but are not antialiased. Specifies a hatch that has the appearance of a checkerboard placed diagonally. Specifies a hatch that has the appearance of layered bricks that slant to the left from top points to bottom points. Specifies horizontal lines that are composed of tildes.FillRectangle(Brsh. Specifies horizontal lines that are spaced 50 percent closer together than Horizontal. Specifies the hatch style Cross.Graphics. Specifies horizontal and vertical lines that cross and are spaced 50 percent closer together than hatch style Cross. Specifies a hatch that has the appearance of confetti. No need for it here e. Specifies dashed diagonal lines. Do not bother with drawing a rectangle. Specifies horizontal and vertical lines. Specifies dashed diagonal lines. Specifies a hatch that has the appearance of diagonally layered shingles that slant to the right from top points to bottom points. For example: Dim Brsh As Brush = New Drawing2D. Specifies horizontal lines that are spaced 50 percent closer together than Horizontal and are twice the width of Horizontal.DiagonalBrick.Paint 'Declare the HatchBrush using a DiagonalBrick pattern. Specifies a hatch that has the appearance of divots. Specifies forward diagonal and backward diagonal lines. you use it in place of the standard brush within your Fill method. Specifies hatch style Horizontal. Color. we want to replace one color with another. until that color is blocked by any other color.Enhancing Visual Basic . or the extent of the Device Context is reached. and X and Y point to a relative pixel location on that surface.dll" (ByVal hObject As IntPtr) As Boolean Page –178– . ByVal Y As Integer) As Integer 'Create a solid-color Brush using an RGB color. and even the printer and screen meet muster here. 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. this is easy to create using the CreateSolidBrush() P/Invoke. But this is easily solved by the GetDC() P/Invoke. This P/Invoke also returns the old Brush handle.Y coordinate outward in all directions.0 – David Ross Goben Painting the Background of Odd Shapes Using a Flood Fill Sometimes you have to render shapes. curves. 2. forms.dll" (ByVal hDC As IntPtr. which we will want to save. or you simply want to fill in the border that surrounds a number of shapes with a particular color. we will need to delete the resources we created with our Brush. The border color will stop painting from spilling over this border as long as the border is sealed. and wFillType is set with one of its two constants. they do have easy access to the Operating System. such as stars. There are two principle conditions we face where we need to paint a portion of a surface with a color: 1. For example. We want to paint over a specific color.NET do not have a built-in solution to this problem. 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. or the extent of the Device Context is reached.NET Beyond the Scope of Visual Basic 6. expansion being contained by the border color being encountered. We also have to think about a Brush to paint the background. Finally. because we will want to restore it when we are finished. When the FLOODFILLBORDER constant is used as a fill type. All these additionally required declarations can be handled by this small block of declaration code: 'Obtain the Device Context from the Window Handle (if zero is returned. such as buttons. The above two criteria can be easily met by the ExtFloodFill() P/Invoke found in GDI32. then the border color is set to the rgbColor parameter. Return its handle Private Declare Function CreateSolidBrush Lib "gdi32. Most VB. ByVal X As Integer.dll" (ByVal crColor As Integer) As IntPtr 'Select a new object. ByVal wFillType As Integer) As Boolean Private Const FLOODFILLBORDER As Integer = 0 'wFillType constant to fill to border (and to DC limits) Private Const FLOODFILLSURFACE As Integer = 1 'wFillType constant to flood the surface of a specific color with another color When the hDC holds the handle to the Device Context to an object. Here is the declaration for GDI’s ExtFloodFill method: Private Declare Function ExtFloodFill Lib "gdi32. All other colors will be treated as though they are border colors.dll" (ByVal hDC As IntPtr.DLL. GDI FloodFill Although VB6 and VB.DLL" Alias "ReleaseDC" (ByVal hwnd As IntPtr. triangles. freeing it for use by other applications Private Declare Function ReleaseDC Lib "user32. we also have to think about obtaining the DC handle from an object.NET controls. which returns its integer handle to us. ByVal hObject As IntPtr) As IntPtr 'delete resources of an object Private Declare Function DeleteObject Lib "gdi32. then the object does not have a DC) Private Declare Function GetDC Lib "user32. Fortunately. This GDI method is able to paint to any device that has a Windows Handle and contains a Device Context (anything that can have a Paint() event will have a DC).dll" (ByVal hDC As IntPtr. from the X. We want to cover all colors except for a color delimiter that is referred to as the Border Color. This is solved by the SelectObject() P/Invoke. or you want to fill the shape itself. this method can instantly paint-fill a large swath of territory using a Brush set to a color. Of course. and other such non-rectangular imaging. ByVal Y As Integer. This is solved by the DeleteObject() P/Invoke. but we want to leave all other colors alone. When the FLOODFILLSURFACE constant is used. We further need to select the Brush into the system so that we can paint with it. ByVal rgbColor As Integer. return the old one Private Declare Function SelectObject Lib "gdi32. picture boxes. ByVal X As Integer.dll" Alias "GetDC" (ByVal hwnd As IntPtr) As IntPtr 'Releases a device context (DC). and the surface begins being painted by the color set in the current system Brush (which we will also have to set). rgbColor is set to an RGB color value. and you need to paint their backgrounds with a color. dll" (ByVal crColor As Integer) As IntPtr 'Select a new object.White) ' End If 'End Sub '****************************************************************************** ' P/Invoke stuff '****************************************************************************** 'Obtain the Device Context from the Window Handle (if zero is returned. e. then the Clr value is filled on the surface ' until a border of BorderClr is encountered. 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. If the BorderClr value is set. ByVal Y As Integer. freeing it for use by other applications Private Declare Function ReleaseDC Lib "user32.dll" (ByVal hDC As IntPtr.Handle.Y 'restore original brush 'release DC (BE SURE TO DO THIS) 'destroy resourced foor now-detached new brush 'report success '************************************************************************************************************* '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.The GdiFloodfill() function will paint a region of any object with a Windows Handle and a DC with ' a slected color. OldBrush) ReleaseDC(hWin. 'if the X.Y point color is the same as border color? 'yes. If the border Color is Transparent. X. 'then there is no need to continue 'else set target color to the original Color value 'and fill with new color while OrgClr encountered or DC limits 'X.Button = Windows.dll" (ByVal hObject As IntPtr) As Boolean 'perform Flood Fill Private Declare Function ExtFloodFill Lib "gdi32.X.dll" (ByVal hDC As IntPtr.dll" (ByVal hDC As IntPtr. then the object does not have a DC) Private Declare Function GetDC Lib "user32. X and Y coordinates define the starting location. flooding out in all directions as long as the color originally ' found at X and Y is found.Control Then ' GdiFloodFill(Me..MouseDown ' If e.DLL" Alias "ReleaseDC" (ByVal hwnd As IntPtr. so nothing to do because we are on a border 'else set border color 'fill any color with Clr up to the border or DC limits 'define a color brush 'set new brush. (vARGB >> 8) And &HFF. Y) Dim Brsh As Integer = ARGBtoRGB(BrushClr) Dim Brdr As Integer = ARGBtoRGB(BorderClr) If BorderClr = Nothing OrElse BorderClr = Color. ByVal Y As Integer.NET Beyond the Scope of Visual Basic 6. or printer with a color '****************************************************************************** ' modGdiFloodfill . ByVal hObject As IntPtr) As IntPtr 'delete resources of an object Private Declare Function DeleteObject Lib "gdi32.MouseButtons. or the DC limits are met. e. 'EXAMPLE: ''do a flood fill at the point of the mouse click.Zero Then Return False End If Dim OrgClr As Integer = GetPixel(hDC. Return its handle Private Declare Function CreateSolidBrush Lib "gdi32. ByVal X As Integer. NewBrush) ExtFloodFill(hDC. ' The color selected will be painted..Y point's color is already the brush's color. save old 'flood fill surface with brush color. Optional ByVal BorderClr As Color = Nothing) As Boolean Dim FillStyle As Integer 'storage for fillstyle Dim iClr As Integer 'color to send to ExtFloodFill() Dim hDC As IntPtr = GetDC(hWin) If hDC = IntPtr. ByVal BrushClr As Color. iClr. Y. vARGB And &HFF) 'return RGB color End Function End Module Page –179– . starting at X.Transparent Then If OrgClr = Brsh Then Return False End If iClr = OrgClr FillStyle = FLOODFILLSURFACE Else If OrgClr = Brdr Then Return False End If iClr = Brdr FillStyle = FLOODFILLBORDER End If Dim NewBrush As IntPtr = CreateSolidBrush(Brsh) Dim OldBrush As IntPtr = SelectObject(hDC. It operates extremely fast and is easy to use: Module modGdiFloodFill 'Paint a region of a picturebox. but only if the Cntrl key is ALSO held down 'Private Sub Form1_MouseDown(ByVal sender As Object.Left AndAlso Control. ByVal rgbColor As Integer.Forms. ByVal wFillType As Integer) As Boolean 'wFillType constant to fill to border (and to DC limits) Private Const FLOODFILLBORDER As Integer = 0 'wFillType constant to flood the surface of a specific color with another color Private Const FLOODFILLSURFACE As Integer = 1 '****************************************************************************** ' GdiFloodFill '****************************************************************************** Public Function GdiFloodFill(ByVal hWin As IntPtr. form. hDC) DeleteObject(NewBrush) Return True End Function 'get hDC for window 'does the object have a Device Context? 'no. Color.dll" Alias "GetDC" (ByVal hwnd As IntPtr) As IntPtr 'Releases a device context (DC). return the old one Private Declare Function SelectObject Lib "gdi32.ToArgb 'convert color value to AARRGGBB Return RGB((vARGB >> 16) And &HFF. ByVal X As Integer.ModifierKeys = Keys..Enhancing Visual Basic . ByVal Y As Integer) As Integer 'Create a solid-color Brush using an RGB color. ByVal X As Integer. X.. ByVal e As MouseEventArgs) Handles Me.0 – David Ross Goben Following is my module to perform GDI Flood Fills. so we cannot do anything 'get RGB color at X/Y point on DC 'get RGB rendering of new brush color from its ARGB format 'get RGB border color from its ARGB format 'if border is Nothing or else it is Transparent.Y. FillStyle) SelectObject(hDC. If it has already been painted or if its breaches boundaries. Each of those invocations will check a saved target point to see if it needs to paint it. GDI+. it is assumed that we want to begin painting over any color. such as Color. the GDI fill is drawn to the Form’s or PictureBox’s display area. It also has overflow and bounds checking. plus a few added protections and much easier (saner and less cryptic) formatting. when its container refreshes. but rather a progressive gradient fill that supports shading. overflow errors are among the most common. you may need to specify a border color. painting the location and outward the color Orchid. because VB.0 – David Ross Goben To use this GDI-based FloodFill method. but VB. This is because we used the handle of the image’s container. Therefore. which our image had previously been drawn to. then you will have to paint instead to the container’s Image object. it is left up to us to write our own.Y coordinate and outward until any other color is encountered.Orchid. This might seem like a silly idea.Button = Windows. Thus. if it is still within the bounds of the image. ByVal e As MouseEventArgs) Handles Me. it still does not provide FloodFill support. For example. They may not realize it. unlike the ExtFloodFill() method provided by GDI32.Left Then GdiFloodFill(Me. until we find the border color or the image boundary. being tied directly to your video graphics hardware controller.DLL does not contain a FloodFill method. If it is not provided or set to Nothing. but if you want persistence.Enhancing Visual Basic . you need to have the handle of an object. the image will return to its original state). you will need the color for the brush to use for the FloodFill. e. Because the image itself was not altered. This way the changes will endure in the image. the image data will over-write anything written to the display area of its container.Y coordinate and fanning out from there. and most frustrating problems facing C-based development – this is one of many reasons why one can develop code in 2 hours using VB that would take 2 weeks to write with equal robustness in C++). but most rendering to games have no use for a single-color fill.Handle. it will in turn check 4 other locations relative to that location. and lastly. GDIPlus. starting at the X.Y. the image shown to the left will become the image to the right if we click the mouse anywhere on the white background. The best solution to this issue is to write the code using a recursive method.X.NET is modeled after managed C++ code using Visual Basic syntax. right. and below it. GDI+ FloodFill The above GDI FloodFill method works very well. This is for the simple reason that the video contoller hardware itself does not support FloodFill. Page –180– . it simply returns. If the border color is set to a color. You will also need the local X and Y coordinates of where you want the flood fill to begin. and so it will instead paint only to the color that is presently at the X. This is because when the target point is painted. etc. then it is clear that you do not want to paint to a border color.Orchid) End If End Sub This event will perform a flood fill anywhere on the surface of Form1 we click. This last option controls how things are going to be painted. which you can actually turn off in the compiler options (indeed. Most of my fellow C++ developers will scoff at writing such code in VB. For the current form. This also makes it very easiy to save the changes to a file directly from the image itself using its Save() method.Forms.MouseButtons. and if the current point has already been painted the new color. because. not just painted on it’s container’s display area. suppose we have the following MouseDown() event: 'do a flood fill at the point of the mouse click Private Sub Form1_MouseDown(ByVal sender As Object. declaring that it is too slow compared to C++.NET Beyond the Scope of Visual Basic 6. e. but it is not persistant (if you did a Me.Handle”. Thus. or the handle of the PictureBox that you are drawing to.DLL.. above. not the image itself.MouseDown If e. this would be “Me. for as powerful as it is. distance perspective. as long as the color we clicked on is available and is not blocked by any other color (this is because we set the the border color to Nothing (or Transparent)). Color. In some cases this is desired. which the OpenGL or DirectX software drivers provide support for. Otherwise. and so on.Refresh().NET runs just as fast as their C++ code. the method should invoke itself 4 times so to check each adjacent point that is left. I have often been asked how to easily clear a PictureBox or a Form (a form would use its BackgroundImage property instead of Image).NET’s stack space. .NET Beyond the Scope of Visual Basic 6. this method will persist in the Image object itself. If we were to use a Point structure. He did offer a more memory-efficient means to get and set points (so simple and small they are absolute genius. however. With that. pop the stack. you could use something like this: With Me. which is quite sizable to begin with. 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.Clear(. and because the recursion method is the absolutely best and shortest means for resolving this issue. Testing for a border to emulate the FLOODFILLBORDER option is much trickier.0 – David Ross Goben The problem with this solution is that a bitmap contains an absolutely collosal amount of pixel positions.Image = New Bitmap(. and if so. you may need to make sure that the image contains a sized bitmap. save one by Bob Powell at www. this is almost too easy to do. we check neighboring points in the 4 primary directions.BobPowell. those are in turn shoved onto the stack. Fortunately.White)”. where even a tiny 256x256-pixel image contains 65.net (specifically. As such. Also.BobPowell. 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).PictureBox1 Graphics. within a loop. I checked the web. which emulates the FLOODFILLSURFACE option offered by the GDI ExtFloodFill() P/Invoke. Page –181– . and if we set it. and so on. unless it was the specified border color. My solution to that. Therefore. check a point. you must make sure the Image object actually exists! When you drop a PictureBox on a form. was actually brain-dead simple. where it pops the last-saved point off the stack. not its methods or properties. All solutions offered used the GDI ExtFloodFill P/Invoke. this would simply shove two 32-bit integers onto that stack (only a Point’s data. For example: With Me. and then. we will need to write our own stack code that will not be limited by the default .536 pixel locations. because there were quite a number of people looking for a solution to this issue. because it can easily keep testing back and forther until even your computer’s memory runs out of space.NET stack restrictions (granted.Clear(Color. Just use “Graphics. after hours of testing. we need not test it further. at www.Enhancing Visual Basic . because it normally ignores the color of the target point. The looper then checks to see if anything is still on the stack.FromImage(Img). using code as simple as “Img = New Bitmap(NewWidth. it loops back. which is to say that it will crash (this is like saying “dying” is “exceeding one’s physiological limits”). With a PictureBox control. NewHeight)”.Image).net/floodfill. and only required a moment to implement: I simply checked to see if the target point is already set to the Flood Fill color. its embedded Image control is actually set to Nothing. but doing this is just one more consideration to trudge through when we develop code).BackColor) End With 'clear image using the background color of PictureBox1 NOTE: You normally need to clear a freshly-created image so that drawing on it will not have issues.Width.FromImage(. If any meet muster.aspx). will be added to the stack). and I was quick to snag them both up). we can simply test to see if we need to save each point on the stack. A large image can fill . we are able to adjust this stack space allocation. If so. He implemented a similar solution to what I came up with. His solution performed only color replacement. Unlike the GDI ExtFloodFill() P/Invoke. at your program startup. This can be performed non-recursively by placing the target point on this stack. As such. After developing my solution.PictureBox1 . That is also also very easy. and so your code will suffer a general protection fault.Height) 'create a blank image (of background color) as a background to draw onto End With And since we are on the subject of processing bitmap data. '****************************************************************************** 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.net http://www.Refresh 'display updated image to user ' End If 'End Sub '****************************************************************************** ' This is a total rewrite.X. we will fill to a border color or boundaries 'if we are already at the border. or the DC limits are met.ModifierKeys = Keys. form. e. not just color-replacement. 'then we have nothing to do 'set color to test for 'Fill the first pixel and recursively fill all it's neighbors '****************************************************************************** ' Portions of the following is based on original work by Bob Powell at ' BobPowell.Enhancing Visual Basic .Contains(Pos) Then 'if the position is within bounds.. If the BorderClr value is set...PictureBox1. but only if the Cntrl key is ALSO held down 'Private Sub Form1_MouseDown(ByVal sender As Object. Bitmap) imgBounds = Bmp. Pos.0 – David Ross Goben Following is my modGdipFloodFill module code that performs persistent FloodFills in Image objects: Option Strict On Option Explicit On '****************************************************************************** 'Paint a region of a picturebox.. GDI+ ' does not support FloodFill. ByVal X As Int32. ByRef Bmp As Bitmap) As Color If imgBounds. ' allow floodfill to borders.White) ' Me. or printer with a color '****************************************************************************** ' modGdipFloodfill . ByVal Clr As Color) If imgBounds.Equals(BrdrClr) Then Return End If TestClr = BrdrClr End If FillPixel(Pos. 'if we will replace a color.net/floodfill.aspx.Control Then ' FloodFill(Me. 'the we have nothing to do 'otherwise. ' reduce his complicated TestBounds position testing to a simple in-code test (If imgBounds.Image.Y) 'the return the color at the pixel position Else Return Nothing 'if we failed End If End Function '****************************************************************************** ' Method : SetPixel ' Purpose : Sets a pixel at a nominated point to a specified color ' pos : The pixel position on the bitmap ' bmp : The bitmap ' Clr : The color to set '****************************************************************************** Private Sub SetPixel(ByVal Pos As Point. it replaces the starting point's color '****************************************************************************** Friend Sub GdipFloodFill(ByRef Img As Image. Sadly.SetPixel(Pos.Contains(Pos) Then).bobpowell. The color selected will be painted. ByVal NewClr As Color. Return Bmp. ByVal e As System.X..MouseEventArgs) Handles Me. X and Y ' coordinates define the starting location.MouseDown ' If e. Bmp) Dim ReplaceClr As Boolean = BrdrClr = Nothing OrElse BrdrClr = Color..NET Beyond the Scope of Visual Basic 6.Forms. Pos. ' speed it up using a strongly-typed stack rather than a generic stack.Button = MouseButtons.X. '****************************************************************************** '****************************************************************************** ' 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.The GdipFloodfill() function will paint a region of any object ' with a Windows Handle and a DC with a slected color. My original FloodFill exploted ExtFloodFill and was ' GDI-based..PictureBox1. then ' the Clr value is filled on the surface until a border of ' BorderClr is encountered. I enhanced it to: ' process Bitmaps instead of BitMapData objects. Color.GetPixel(Pos.Equals(NewClr) Then Return End If Else If TestClr.Y. This is the original.Transparent If ReplaceClr Then If TestClr. Clr) 'set the pixel End If End Sub Page –182– . e.Pixel) Dim Pos As New Point(X. Bmp. If Not. If the border Color ' is Transparent.. flooding ' out in all directions as long as the color originally found at ' X and Y color is found. Optional ByVal BrdrClr As Color = Nothing) Dim Bmp As Bitmap = DirectCast(Img. ByRef Bmp As Bitmap. 'if it is already that color.Contains(Pos) Then 'if the position is within bounds.Windows. TestClr. 'EXAMPLE: ''do a flood fill at the point of the mouse click.. ReplaceClr) End Sub 'Ensure the bitmap is in the right format 'eliminate need for complicated TestBounds() test 'init testing position 'get the color under the point. ByVal Y As Int32.Y. Y) Dim TestClr As Color = GetPixel(Pos.. NewClr.Left AndAlso Control. but did not support persistent imaging via GDI+.. Bmp.GetBounds(GraphicsUnit. 'True if we will replace a color.. iTestClr. You will also need to have the local X and Y pixel coordinates of where you want the flood fill to begin within the image.PictureBox1.X + 1. then it is clear that you do not want to paint to a border color. Bmp).Y coordinates and fan out from there until we encounter the selected border color or the image border. Bmp.Push(Pos) 'push current position on the stack Dim INewClr As Int32 = NewClr.Y). starting at the X.. whichever comes first '****************************************************************************** Private Sub FillPixel(ByVal Pos As Point.ToArgb '----------------------------------------------Do Pos = stack. ReplaceClr) Loop While CBool(stack. Pos. ReplaceClr) TestPixel(New Point(Pos.BackgroundImage” or “Me. INewClr.X and e.Pop() 'get a position SetPixel(Pos.X. ByVal ReplaceClr As Boolean) Dim iOrgClr As Int32 = GetPixel(Pos.Push(Pos) 'else save point for later scrutiny End If End Sub End Module In order to use this GDI+ FloodFill method. NOTE: Be sure to refresh the image’s container after a GDI+ flood fill. iTestClr.Y . being the color to replace. ByRef Bmp As Bitmap. Bmp.Y from a MouseDown() event.Image”. you have to provide a target Image control for whatever object you are writing to. up. otherwise filling to' ' : a border color or to the the bitmap boundary.ToArgb 'get the color at the specified pixel position If OrgClr = Nothing Then 'if we went out of bounds.Y). so we are done without really starting End If ElseIf OrgClr = iTestClr Then 'otherwise.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.X .. ByRef Bmp As Bitmap.. ByVal TestClr As Int32.PictureBox1.Count) 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. Bmp.1). ByVal NewClr As Int32. whether it is a form’s BackgroundImage or a PictureBox’s Image.Transparent. ReplaceClr) TestPixel(New Point(Pos.ToArgb 'grab integer version of test color If ReplaceClr Then 'are we replacing a specific color? If OrgClr = NewClr. but do the colors already match? Return 'yes. Pos. You will also need the color you want to use for the fill. being the color to replace. and so it will instead paint only to the color that is presently at the X. This last parameter controls how things are going to be painted.Refresh”. such as Color.ToArgb Then 'yes. set to Nothing.NET Beyond the Scope of Visual Basic 6.Orchid. Pos. or the border to stop at ' ReplaceClr : True if we are replacing a specific color. 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. For example: “Me. and down TestPixel(New Point(Pos. right. Bmp).Enhancing Visual Basic . so we are done End If stack. Return 'we we are done End If End If stack. and l any other color is treated as a border. INewClr. iTestClr. then it is assumed that we want to begin painting over any color.Y + 1).. NewClr) 'set pixel 'now check pixels left. If the border color is set to any other color. If it is not provided.X. otherwise filling to' ' : a border color or to the the bitmap boundary. iTestClr. such as e. INewClr. Lastly. ReplaceClr) TestPixel(New Point(Pos. INewClr. ReplaceClr As Boolean) Dim OrgClr As Int32 = GetPixel(Pos. ByVal NewClr As Color. Pos. Return 'then we have nothing to do End If '----------------------------------------------Dim iTestClr As Int32 = TestClr. Bmp.1. Bmp. ByVal TestClr As Color. or set to Color.0 – David Ross Goben '****************************************************************************** ' Method : FillPixel ' Purpose : Fills a pixel and its un-filled neigbors with a specified color ' pos : The start position in the bitmap ' bmp : The bitmap ' NewClr : The color to fill with ' TeStClr : The testing color. whichever comes first '****************************************************************************** Private Sub TestPixel(ByVal Pos As Point. or the border to stop at ' ReplaceClr : True if we are replacing a specific color. you need to specify a border color IF you are not going to do a color-replacement. such as “Me. have we met the border or the bitmap boundaries? Return 'yes. Page –183– .Y coordinate and outward. X. CInt(r. d.Height .Left. If anyone is familiar with HP/GL (Hewlett-Packard Graphics Language). a corner radius value of 10 pixels is sufficient. with a tip of the hat to http://stackoverflow.X + d / 2).Width .Y + r. in all. taking advantage of the GraphicsPath collection defined in the System.Height . 90) g.Drawing2D Namespace. Drawing Super-Fast Rounded Rectangles from the Paint Event For all the amazing power we have at hand under GDI+. along with the many others I found online.DrawLine(p. . CInt(r. As such.Width .d / 2)) g.Drawing. it is much like creating plotter source code files.DrawArc(p. {2}. . d. as most others.Height .Top.X. but this is because there is so much drawing going on in the background to produce the desired shape. used for drawing blueprints and driving sheet metal cutting machines. r. the rest of these functions are child’s play. My own little adventure into VB-based rounded rectangle drawing uses less drawing instructions. For example.X + r. pn) 'render rounded rectangle with a 15-pixel corner radius pn.Graphics. such as DrawPie().Y. . {3})". especially in process-heavy code. CInt(r. Even the Visual Basic Power Packs features a CornerRadius property in its RectangleShape that will enable rectangles to feature rounded corners.Y + r.Y + d / 2).Y + r. if we add an arc for the top-left corner and next define an arc for the top-right corner. r.Height .Y + r. .Width.Dispose() 'dispose of used object End Sub Though it works. 90.d. But the important point is – it works.d / 2). Although you might think Microsoft stupid to ignore Rounded Rectangles in GDI+. we do not need to add connecting lines between corner definitions. CInt(r. cobbling together a rounded rectangle.d. the plotting function will draw a connecting line from the last drawing point to the start of the next. All the solutions I found online did the same thing: they drew.Width . The above cryptic code. and then draw 4 separate 90-degree arcs to render the four corners. d. r.X.Location. DrawArc(). r.DrawArc(p.d / 2).Y.Width. . ByVal e As PaintEventArgs) Handles Me. 0.Location.Y) g. r.Print(vbNullString) 'add a blank line to the Immediate Window ddisplay Dim rec As New Rectangle(.Width). For example. Page –184– . CInt(r.X + r.DrawLine(p. though it is not brain surgery to figure that out.Height)) g. r. even this powerful package has to submit the Rounded Rectangle shape by rendering 8 individual parts (4 sides plus 4 corners). you should be mindful that Rounded Rectangles sometimes eat a lot of process time because. this solution. This collection has become popular because it is very easy to add drawing instructions to it. 90) g.Y. 3) 'adjust Pen color and width as needed With Me.Y + d / 2). d.d / 2)) g. d.Graphics. ByVal p As Pen) g.Height) 'set up bounding rectangle using my picTemp helper frame End With DrawRoundedRectangle(e.DrawLine(p.NET Beyond the Scope of Visual Basic 6.X + r. consider the following typical online solution. we are not able to draw straightforward Rounded Rectangles (some people in the machining industry like to refer to these as Obrounds). CInt(r. 90) g. d. r. DrawCurve(). and wished that we had under GDI32. 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. DrawBeziers(). 4 individual lines and 4 individual 90-degree arcs. Another great thing about it is that in defining our drawing path.picTemp 'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G) Debug.DrawArc(p. .d.Size.Enhancing Visual Basic .Y + r. such a method needs to individually render 4 separate lines. though some prefer a larger value of up to 30.Y + r. rec. 180. r. 270.com: Public Sub DrawRoundedRectangle(ByVal g As Drawing. 90) End Sub And this is not even getting into the versions I have seen that fill-paint a rounded rectangle.X + r. CInt(r. ByVal r As Rectangle. d.X.Size.DrawLine(p. CInt(r.X + d / 2). ByVal d As Integer. CInt(r. However. is not documented or commented (Sinners! Repent!).X. r. r. CInt(r. internally.Width .DrawArc(p. DrawPolygon(). ran very slowly in comparison to the other solutions we have so far explored.Paint Dim pn As New Pen(Color.Black.Height) Debug.Y. {1}. and noting the provided tool tips. CInt(r. and the impressive number of other drawing features offered. .d.0 – David Ross Goben Drawing Other Shapes from the Paint Event By exploring the other features offered by the Graphics object. Many programmers have made valiant attempts to solve this problem in VB-only code.Print("Dim Rec As New Rectangle({0}. You are often not even told how to use it. you should use Rounded Rectangles sparingly. r. CInt(r.Height). r.Print("'Prototype for target Rectangle declaration:") Debug. 15. d.Width).X + r.X + r. CornerRadius . we supply it with the rectangle definition..BackColor) 'fill canvas with background color (init image) End If Dim Pen As New Pen(Brushes.Location.EventArgs) Handles Me.GetHdc. .FromImage(. the radius of a corner. gPath) gPath.Width.50) DrawRoundedRectangle(rec. 'close the drawing from end to start 'draw path using supplied pen 'dispose of created object To use it. .Graphics. 90) .Width . Here is code demonstrating this from a form’s Load() event: '****************************************************************************** ' Method : Form_Load ' Purpose : Initialize background image and draw a rectangle on it '****************************************************************************** Private Sub Form_Load(sender As Object.Blue. 10..CornerRadius .50.BackgroundImage) 'create Graphics interface to image 'RoundRectangle(g. For example.Clear(.Height . e As PaintEventArgs) Handles Me.Height) 'create canvas to draw onto (if no image loaded) Graphics. It is not a disaster. 10. '(specify bottom-left quadrant) 'bottom-left arc and draw to. . We lastly provide it with the graphics interface to the surface we are drawing to.Blue.X + 20. 100.ClientRectangle 'draw within the form's client area Dim rec As New Rectangle(. 5)”.Blue.X = BaseRect.Enhancing Visual Basic .ClientRectangle 'draw within the form's client area Dim rec As New Rectangle(. ByRef g As Graphics) Dim gPath As New Drawing2D. e As System.Dispose() End Sub 'create graphics path object 'CornerRadius * 2 'Bounds for corner arcs 'add top-left arc and draw to. New Size(CornerX2.Dispose() End With End Sub Page –185– .50. 5) 'pen to render with With ..BackgroundImage = New Bitmap(.Dispose() 'release created resources End With End Sub For more persistence. I know that most examples on the web demonstrating the GraphicsPath collection typically use too many instructions.Height) 'create canvas to draw onto (if no image loaded) e. 90. '(specify bottom-right quadrant) 'bottom-right arc and draw to.FromImage(.AddArc(CornerRect.BackgroundImage Is Nothing Then 'if no image assigned to background. CornerX2)) .BackColor) 'fill canvas with background color (init image) End If Dim g As Graphics = Graphics. if we are drawing the rectangle from the Paint() event of a form.. 0.0 – David Ross Goben Having programmed these plotters and cutting machines. New Rectangle(10. e. ..BackgroundImage Is Nothing Then 'if no image assigned to background. 90) CornerRect. 180. Color..AddArc(CornerRect.Graphics) 'draw rounded rectangle End With Pen.Y + 20. 90) CornerRect. .50) DrawRoundedRectangle(rec. Pen. _ ByVal CornerRadius As Integer.Bottom ..X = BaseRect. 10) Dim Pen As New Pen(Brushes.Dispose() 'release created resources g. 10. 5) 'pen to render with With .NET version of a DrawRoundedRectangle() method: '****************************************************************************** ' Method : DrawRoundedRectangle ' Purpose : Quickly draw a rounded rectangle using a GraphicsPath '****************************************************************************** Public Sub DrawRoundedRectangle(ByVal BaseRect As Rectangle.X + 20. 90) CornerRect.Height .. .Right .BackgroundImage = New Bitmap(... 270.Y + 20. such as 10 pixels. we could draw it once directly to the image and forget about it.. . whether a default pen or one such as “Dim Pen As New Pen(Color. What following is my VB. but it wastes more time. g) 'draw rounded rectangle End With Pen. a Pen.Width .. Pen.Left . _ ByRef Pen As Pen.DrawPath(Pen.White.AddArc(CornerRect. '(specify top-right quadrant) 'top-right arc and draw to.Load With Me If .Width.NET Beyond the Scope of Visual Basic 6.BackgroundImage). we might invoke it so: '****************************************************************************** ' Method : Form_Paint ' Purpose : Initialize background image and draw a rectangle on it '****************************************************************************** Private Sub Form_Paint(sender As Object.Blue. . .Clear(.Paint With Me If .Y = BaseRect.CloseFigure() End With g.AddArc(CornerRect. . Color. 100).GraphicsPath With gPath Dim CornerX2 As Int32 = CornerRadius * 2 Dim CornerRect As New Rectangle(BaseRect. there is no CornerRadius parameter featured. up through Windows 3. provided GDI support for Win16 operating systems.White.NET Int32 Integer that is typically used as a pointer. This is because the end point actually points one pixel beyond the drawing when we are working at the P/Invoke Level. and it works several dozens of times faster than other solutions.GetHdc. we will be able to add the Location.Y1 and X2. because the P/Invoke requires starting and end points (X1. and the Location. If you make them both of equal value. these Pen and Brush objects are selected and reset by invoking the also-required SelectObject() P/Invoke. and a Brush is used to paint-fill its background). New Rectangle(10. of which only one quarter of it will actually be painted to display a needed rounded corner image). At the brass tacks level. X2 As Integer. We will write a reusable method that we can invoke any other time we need it in any of our future projects. although there are two others that seem to be related to it: CornerWidth and CornerHeight. e As System. CornerHeight As Integer) As Boolean NOTE: GDI32. most developers who use this method generally set both of these parameters to the same value. 100. Y1 As Integer. filled with white g. The CornerWidth and CornerHeight parameters represent the dimensions of a bounding rectangle for rendering each of the four corners of the Rounded Rectangle. Consider the following root declaration: 'Draw a rounded Rectangle.Y2). you can get the hDC for the image being painted using e.Y1 represent the top-left corner of the rectangle within the image. As such.0 – David Ross Goben However.GetHdc() method provides this to us as an IntPtr value. twice the horizontal and vertical radius (the bounding rectangle must encompass the width and high of a full ellipse/circle. If you are drawing this somewhere else. we need only think our way though this whole process just once. GDIplus. Return non-zero for success Private Declare Function RoundRect Lib "gdi32.DLL. the Windows Operating System also delivers persistent Rounded Rectangle support through a RoundRect() P/Invoke found in GDI32.x.DLL is the base Graphical Display Interface for Win32 that has served us so faithfully since Microsoft Windows began Win32 support. The hDC integer represents the Windows handle to the Device Context of a target image. 10) 'draw a blue rounded rectange. And since we are creating (allocating) resources. which in turn require a PS_SOLID constant. You may optionally require the use of the GetStockObject() P/Invoke if you want to select a transparent Brush (NULL_BRUSH constant). For example: Private Sub Form1_Load(sender As Object. the e.NET Beyond the Scope of Visual Basic 6. we actually do not need to know much about the required Device Context. which is a . Additionally. to use the above P/Invoke. you can get it by temporarily creating a Graphics interface for the image. Page –186– .EventArgs) Handles Me. and X2. we can in fact easily use the start point and width/height values used by a Rectangle structure. VB. drawing to it.Height values together in order to compute the needed “draw-until” endpoint without needing to additionally subtract a 1 from each. We essentially only need access to its Handle. as well as the process of selecting and resetting Pens and Brushes (a Pen is used to draw the border of a shape.Enhancing Visual Basic . X1.Dispose() 'dispose of created resource End Sub Of course. Y2 As Integer. such as the above DrawRoundedRectangle() methods. 10.Graphics. These two parameters define a bounding box for rendering each of its rounded corners. GDI. Color. as you can also see in the above RoundRect P/Invoke.BackgroundImage) 'we will draw to the form's background image RoundRectangle(g.dll" Alias "RoundRect" (ByVal ByVal ByVal ByVal ByVal ByVal ByVal hDC As IntPtr. which also provides support for Win64.Y2 represent the bottom-right corner. and you will also need the CreatePen() and CreateSolidBrush() P/Invokes to obtain the Windows Handles of the needed Pen and Brush objects. the first incarnation. and then disposing of the created object.DLL. establishing a “draw-until-encountered” limit.Width values together. Moreover. Color.NET uses a brand new GDI. such in the form’s Load() event. we will also need the DeleteObject() P/Invoke to responsibly release them. X1 As Integer. 100).DLL. Although it does not seem apparent. If you are in a Paint() event. then this effectively becomes a Corner Radius (x2) value. we may need to know about things like a Device Context. Fortunately. CornerWidth As Integer.X and Size.Blue.GetHdc. As such. Fortunately.FromImage(Me.Y and Size.Load Dim g As Graphics = Graphics. dll" Alias "CreateSolidBrush" (ByVal crColor As Integer) As Integer 'Select/replace an object through its handle. The next thing is to select them and store the old Pen and Brush away for later recovery. Luckily. we need to store these values away so that we can restore them (if we will be in fact replacing them. so we restore the old Pen and Brush. this is old hat conversion that I have been doing since my original Assembly Language days. and each two hex values form an 8-bit BYTE value. Returns its handle Page –187– . and then we release the resources of our locally created Pen and Brush. Blue).0 – David Ross Goben The additional P/Invoke requirements can be neatly collected into a block like the following (we will ignore the NULL_BRUSH stock object. or DWORD in C parlance). the code is hardly worth all the trouble I just went through to describe it. BackColor. Rect.dll" Alias "SelectObject" (ByVal hDC As IntPtr. Internally. which are declared as Integers (Int32. is not directly compatible to an Integer (we cannot cast it either through Cint.NET’s Argb() method). RGB colors are formatted 00BBGGRR. Yet.ToArgb Return RGB((vARGB >> 16) And &HFF. A value of 0 indicates this color is fully transparent. and Height of the shape in pixels ' CornerRadius: The radius of the rounded corner in pixels '************************************************************************************************************* ' P/Invoke Stuff '************************************************************************************************************* 'solid pen constant Private Const PS_SOLID As Integer = 0 'Pen Style for solid pen (used by CreatePen()) 'create a solid pen. which I named modRoundRect : Module modRoundRect '************************************************************************************************************* 'RoundRectangle (hDC. Red. As such. ARGB is formatted AARRGGBB. (vARGB >> 8) And &HFF. After all this theory. a Color object fortunately has a member property called ToArgb that will return a 32-bit Integer to us. When we specify a pen or a brush. After that. _ 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 The only thing that may look tricky to someone first looking at the above declarations is the color values. vARGB And &HFF) End Function 'convert color value to AARRGGBB 'return RGB color With that. Thus. the color values expected by our P/Invokes require colors formatted to RGB (note that this has nothing to do with VB. we must be mindful that we are replacing a pen or a brush that the current device context may be using.Enhancing Visual Basic . However. CType or DirectCast). _ ByVal nPixelWidth As Integer. Unfortunately. Green. A Color value is an ARGB value (Alpha. 0-F) value. Return the old handle for the type of object Private Declare Function SelectObject Lib "gdi32. What follows is the complete module. Finally.Y). We then must declare our own Pen and Brush. we now have all the tools we need to render fast Rounded Rectangles. although 32-bit.Transparent) ' Rect : Rectangle structure containing the start location (X. Pn. 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. where each letter represents a 4-bit hexadecimal (0-15. Typically it has a value of 255. able to store values from 0 through 255 (00-FF hex). meaning that the color is fully opaque (solid). CornerRadius): ' Draw a Rounded Reactanle.NET Color value.NET Beyond the Scope of Visual Basic 6. and is very easy to resolve with a little helper function I will name ARGBtoRGB: Private Function ARGBtoRGB(ByVal clr As Color) As Integer Dim vARGB As Integer = clr. but we should never just ignore them or casually toss them to the side – this is how memory leaks start). ARGB and RGB might appear similar. A . Returns its handle Private Declare Function CreatePen Lib "gdi32. we will set aside two variables to store these Integer values.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. _ ByVal crColor As Integer) As Integer 'create a solid brush object. NOTE: The Alpha component of an ARGB value is an Int8 Alpha Blend value. we actually draw our Rounded Rectangle. because Color. then we should delete the old ones using the DeleteObject method. but they are not. Returns its handle Private Declare Function CreateSolidBrush Lib "gdi32.dll" Alias "CreatePen" (ByVal nPenStyle As Integer. we need to clean up. Width. So RGB colors are actually stored in reverse order from the way they are ‘advertised’. rec) 'Runtime: draw ellipse RoundRectangle(e. _ 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.DrawEllipse(pn. Y1. Me. Returns its handle Private Declare Function CreateSolidBrush Lib "gdi32.Enhancing Visual Basic .Location.Print("'Prototype for target Rectangle declaration:") Debug.Width). Width.Color)) Dim hBrush As Integer = CreateSolidBrush(ARGBtoRGB(BackColor)) 'create our Pen and save its handle '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.X + rect. but this way is less convoluted. {2}. ByVal e As PaintEventArgs) Handles Me.Location. Color. Return non-zero for success Private Declare Function RoundRect Lib "gdi32. _ ByVal CornerRadius As Integer) Dim oldhPen As Integer 'store old Pen handle Dim oldhBrush As Integer 'store old Brush handle 'Declare our local Pen and Brush Dim hPen As Integer = CreatePen(PS_SOLID. . hBrush) 'select new Brush and return old Brush 'draw rounded rectangle RoundRect(hDC.Y. Height) 'Runtime: manually modify run-time values (X.dll" Alias "RoundRect" (ByVal hDC As IntPtr. X1. _ ByVal crColor As Integer) As Integer 'create a solid brush object.DrawLine(pn.Size.Print("Dim Rec As New Rectangle({0}. _ ByVal X2 As Integer. OldhPen)). X2. _ rect.Location.ToArgb 'convert color value to AARRGGBB Return RGB((vARGB >> 16) And &HFF. {1}.Height. rect.Y + rect.X. oldhPen) SelectObject(hDC. CType(rec.Graphics. Y2) 'e. _ CornerRadius * 2.ToString.Paint Dim pn As New Pen(Color.dll" Alias "SelectObject" (ByVal hDC As IntPtr. oldhBrush) 'reselect old Pen and toss away returned value 'reselect old Brush and toss away returned value 'finally.Graphics. 1) 'adjust Pen color and width as needed '************************************************************************************************************* 'Disable the lines you do not require for final runtime Paint event (disable ALL during initial testing) '************************************************************************************************************* Dim rec As New Rectangle(Me. Point)) 'Runtime: rec using rectangle 'e. ARGBtoRGB(Pn. rec) 'Runtime: draw a rectangle 'e.0 – David Ross Goben Private Declare Function CreatePen Lib "gdi32.Size. DeleteObject(hBrush) 'such as DeleteObject(SelectObject(hDC.Size. pn. _ ByVal CornerHeight As Integer) As Boolean '************************************************************************************************************* 'RoundRectangle Method '************************************************************************************************************* Public Sub RoundRectangle(ByVal hDC As IntPtr.Y.NET Beyond the Scope of Visual Basic 6.dll" Alias "CreateSolidBrush" (ByVal crColor As Integer) As Integer 'Select/replace an object through its handle. X2. '************************************************************************************************************* 'ARGBtoRGB Method 'Helper function to covert Color ARGB value (AARRGGBB) to RGB (00BBGGRR) '(This function should eventually find its way to its own module. 15) 'render Rounded Rectangle '------------------------------------------------------------------------------------------------------------' TESTING. _ ByVal rect As Rectangle.GetHdc.Graphics. _ rect.Red. CInt(Pn. (vARGB >> 8) And &HFF. delete the resources of our local Pen and Brush DeleteObject(hPen) 'NOTE: We could have simply wrapped each of the above two lines witin DeleteObject().DrawLine(pn.Location.Width.Size. because the returned values will End Sub 'be the handle values for our hPen and hBrush. _ ByVal X1 As Integer.Location. _ ByVal BackColor As Color. Disable drawing methods you will not be using in this Paint event '------------------------------------------------------------------------------------------------------------With Me.Location. Return the old handle for the type of object Private Declare Function SelectObject Lib "gdi32.White.Height.Location. restore the old Pen and Brush SelectObject(hDC. {3})". Disable all when not in use. _ ByVal Pn As Pen.ToString) Debug.picTemp 'Display current Location and Size values of picTemp in the Immediate Window (Ctrl-G) Debug. Y.Size. CornerRadius * 2) 'Corner bound rectangle is twice the radius on both sides 'next. Y1.X. Width. _ ByVal Y2 As Integer. _ ByVal CornerWidth As Integer. Height) 'e. vARGB And &HFF) 'return RGB color End Function End Module We can invoke our new RoundRect() method like this: Private Sub Form1_Paint(ByVal sender As Object. _ ByVal nPixelWidth As Integer. . .Location.Size) 'set up bounding rectangle 'Dim rec As New Rectangle(X.ToString.ToString.Print(vbNullString) 'add a blank line to the Immediate Window ddisplay Page –188– . hPen) 'select new Pen and return old Pen oldhBrush = SelectObject(hDC. Y. rec.dll" Alias "CreatePen" (ByVal nPenStyle As Integer. _ ByVal Y1 As Integer.DrawRectangle(pn.Graphics. Y2) 'Runtime: using absolutes (manually modify values X1. _ rect.Width. and be declared Public) '************************************************************************************************************* Private Function ARGBtoRGB(ByVal clr As Color) As Integer Dim vARGB As Integer = clr.picTemp.Graphics.PicTemp. _ . _ rect. _ ByVal BackColor As Color. or first try saving off the Graphics object for a form or control before giving up.White. restore the old Pen and Brush SelectObject(hDC. Color.Red. . oldhBrush) 'reselect old Pen and toss away returned value 'reselect old Brush and toss away returned value 'finally.Y.DrawLine(pn. ptV) 'Vertical line 'e.1. because the returned values will End Sub 'be the handle values for our hPen and hBrush. oldhPen) SelectObject(hDC.Size. _ ByVal rect As Rectangle.Y + . 270.Paint Dim pn As New Pen(Color. I would take the debug text printed: 'Prototype for target Rectangle declaration: Dim Rec As New Rectangle (56. and for that we simply need to use that object’s CreateGraphics() method. _ ByVal Pn As Color. Drawing Graphics Outside of the Paint Event All the examples in this article were drawn from the Paint() Event of a Form or Control. _ rect.Width .Graphics. all we really need is a Graphics object that is associated with the form or control we want to draw to. If I was satisfied with this layout.Location. ByVal e As PaintEventArgs) Handles Me.X + rect. DeleteObject(hBrush) 'such as DeleteObject(SelectObject(hDC.Location. 1) 'adjust Pen color and width as needed Dim rec As New Rectangle(56. As you can see.CreateGraphics Page –189– . 1. A suggested method would allow a simple parameter of type Color in place of the Pen object (you will also have to change the heading of the current RoundRectangle() method to “Public Overloads Sub RoundRectangle”): Public Overloads Sub RoundRectangle(ByVal hDC As IntPtr. .Location. .Graphics provided by a PaintEventArgs object) Dim eg As Graphics = Me.Height.1) 'end location (bottom) of vertical line ' 'disable lines not needed ' 'e. .Dispose() 'remove used resosurces End Sub NOTE: Unlike the non-persistent result of GdiFloodFill() P/Invoke in Gdi32. _ rect.GetHdc.X.Graphics.Width.DrawEllipse(pn. 57. _ CornerRadius * 2. _ rect.Size.Dispose() 'remove used resosurces End Sub You may want to create an additional overloaded version of the RoundRectangle method.X.Location.Size.PictureBox1. pn.Graphics.Size. 99) And build my final rendering code: Private Sub Form1_Paint(ByVal sender As Object.Location. OldhPen)). 15) 'render Rounded Rectangle with CornerRadius of 15 pixels pn. Some new programmers are so stuck in trying to figure out how to do this and so they give up.Height . rec. However.DrawRectangle(pn. its RoundRect() P/Invoke is persistent because it is drawing to the actual image object through its Device Context (hDC). _ ByVal CornerRadius As Integer) Dim oldhPen As Integer 'store old Pen handle Dim oldhBrush As Integer 'store old Brush handle 'Declare our local Pen and Brush Dim hPen As Integer = CreatePen(PS_SOLID.NET Beyond the Scope of Visual Basic 6. hBrush) 'select new Brush and return old Brush 'draw rounded rectangle RoundRect(hDC. For example: 'generate a graphical interface to a picture box image (same as e. 99) 'Runtime: set up run-time bounding rectangle RoundRectangle(e.Location.X + . delete the resources of our local Pen and Brush DeleteObject(hPen) 'NOTE: We could have simply wrapped each of the above two lines witin DeleteObject(). rec) 'Ellipse End With pn.DrawLine(pn. However. hPen) 'select new Pen and return old Pen oldhBrush = SelectObject(hDC.Enhancing Visual Basic .Graphics.Location. but this way is less convoluted.Y + rect. 270. I am using my testing code.Y) 'end location (right) of horizontal line 'Dim ptV As New Point(. ARGBtoRGB(Pn)) Dim hBrush As Integer = CreateSolidBrush(ARGBtoRGB(BackColor)) 'create our Pen and save its handle 'create our color brush 'select our Pen and Brush into current use and save the ones we are replacing oldhPen = SelectObject(hDC. rec) 'Rectangle 'e. _ rect. 57.0 – David Ross Goben 'Dim ptH As New Point(.Location. ptH) 'Horizontal line 'e. CornerRadius * 2) 'Corner bound rectangle is twice the radius on both sides 'next.Graphics.dll. you can draw these shapes just as easily outside these events.Location.Location. I hope that by exploring graphical rendering here in much more detail. that you should not also create a local copy of a graphics object for that same control.Enhancing Visual Basic .Drawing is usually already loaded). What this really means is that what is drawn is to the PictureBox image surface. they do. there is one more important thing to remember. and being sure to dispose of the local instance if it is created. take much more processing time. Granted. but would afterward like to translate them to much faster and less memory-consuming Paint() event drawing instructions. your implementation decision to draw your own graphics or use line and shape controls will now be based upon understanding rather than hearsay. It is brain-dead simple to do if you use my modShapeConver module described in Black Book Tip # 31: Easily Replace Power Pack Shape Controls with Paint() Event code on page 446 of this document. What this really means to ordinary mortals is that we cannot mix different device contexts with each other without them stepping on each other’s toes. but remembering not to dispose of the graphics object if it is provided to you. Using the above graphics interface is non-persistent. The moral of this is that if you are using the Graphics object in a Paint event. and developing them is not the great burden that many may shock you into believing. But what if you want to draw to the Image object of the PictureBox? Doing this will make the drawing persistent. but rendering them many times faster than these Power Pack controls. RectangleShape. Conclusion There is much that you can do with roll-your-own lines and shapes. we keep the code extremely lean and extremely fast. Page –190– . What you should do in this case is determine if a Graphics object is passed to the function. or invoking a method that uses the Graphics object from a Paint event. eventually calling out their armies and bringing about Armageddon on your computer’s memory. and as such they will maintain their own local copies of the rules and what they claim control over. locking the system up and stalling everything as they do battle in endless cycles. just like VB6 line and shape controls. 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. because your drawings will actually be recorded directly into the image object. and create a local instance only if one is not provided.Image) However. and OvalShape controls. By whichever route you choose to take.FromImage(Me. Although the line and shape controls in the Visual Basic Power Packs are fantastic.NET Beyond the Scope of Visual Basic 6. NOTE: All that said. such as through a parameter from a paint event. but they are instantiated as separate objects. and that is that a Graphics object is actually a reference to what systems developers might refer to as a Device Context. and the fact is their identities might look to be the same. you would think that they are the same.0 – David Ross Goben And that is all there is to it (Note that System. How do you get the graphics object for that? EASY: Dim eg As Graphics = Graphics.PictureBox1. if you would prefer to design your forms using the quick and easy Power Packs Shape controls. But that is not the end of it. such as a PictureBox. this is really very easy to do. which exposes a ConvertShapes() method that will generate the Paint() event equivalent code needed to duplicate Power Pack LineShape. By rendering such simple lines and shapes from a form or control’s Paint() event. easily. Next. which was found in the Microsoft Common Controls Library 6. but if you have tried some of the easy features covered earlier in this document. Indeed. TreeView. and then ran the application. I know.NET. you could do owner-drawn graphics in VB6… But did you really want to? It was a major pain in the caboose. However.NET this has all changed and in more ways than one. Even a rank amateur can manage professional-looking results in a matter of a minute or two. and painlessly. First.NET with Ease I have seen a lot of internet chatter on how to add icons to a ListBox. drawing text directly to a PictureBox. To support Owner-Drawn ListBoxes and ComboBoxes you have to write some code within the control’s DrawItem() event. rather than the control as a whole. and simply because you did not yet perform your responsibilities in the Owner-Drawn part of the application (you. the previously often-dreaded idea of owner-drawn controls under VB6 is now almost brain-dead simple under VB. And that is in a matter of seconds for seasoned VB developers. as we have examined earlier in this tome. owner-drawn ListBox items and such. What it does is offer us a DrawItem() event for each member in a ListBox or ComboBox that it must display or update. nor does it have plans to provide an ImageCombo control. it also provides us with easy access to every tool we will need to easily process those items in short. Principle Features of an Owner-Drawn ListBox (and ComboBox) The idea of performing owner-drawn. if you wanted to add icons to a VB6 ComboBox. But the good news is: there is no real need for one! That is because of how easy VB. Luckily. Under VB. Yes. all you had to do was use an ImageCombo control in place of a ComboBox. this also provided the ToolBar. controlling the Owner part). ImageList. which has icon support built right in. to the consternation of legions of programmers. except that a DrawItem() event addresses each displayed line of a ListBox or ComboBox. I am going to show you what you need to know to draw an icon and some text into a ListBox. and how to draw a selection rectangle. such as drawing icons on a object’s surface.OCX).0 (MSCOMCTL. The OwnerDrawVariable option is a special form that indicates that all the elements in the control are drawn manually and can differ in size. If you had a ListBox on a form that contains data and you set the DrawMode to OwnerDrawFixed right now. This event can be looked upon just like a Paint() event for a control. ProgressBar. the “supposed” bad news: VB. StatusBar. we will tack together some easy software tools so you can draw different graphics for different kinds of data on a line from either the application resources or an ImageList control. That was what we typically did under VB6. but you might notice that now there is no data displayed within the ListBox. it would run normally. ListView.Enhancing Visual Basic . To enable owner-drawn graphics on either a ListBox or a ComboBox. For our purposes. all you have to do is change the control’s DrawMode property from Normal to either OwnerDrawFixed or OwnerDrawVariable. It was a cheat. but you simply do not yet realize it. or manually-drawn List Items may sound a bit scary to some. But along with it. you may even be an old hand at it. Well… 4 lines if you want to also draw a selection rectangle. it was harmlessly discarded. though VB. First. quick lines of code.0 – David Ross Goben Adding Images to ListBoxes and ComboBoxes under VB.NET Beyond the Scope of Visual Basic 6. or drawing shapes on a control or form. I think many of these gurus offer this advice because they remember all too clearly the difficulty we all had doing owner-drawn graphics under VB6. How professional did it look? Well… Many others advise us that this can only be done under C# because it involves derived classes (it does?). you can perform owner-drawn graphics and text on a ListBox or ComboBox quickly. Page –191– . which is as easy as remembering to say “Yikes!” when you fall into a snake pit.NET does not. Indeed. You should also be aware of how easy they are to actually do. which goes ditto for ComboBoxes. In just 3 simple lines of code.NET diligently processed that data. we will only concern ourselves with the OwnerDrawFixed option. and Slider controls. then you should already be familiar with owner-drawn graphics. Most times people are advised to simply resort to using a ListView control. and advise them to examine a C# application example somewhere. but it worked.NET has made doing owner-drawn controls. is the holder.Resources. you may want to change icons based upon the type of file being listed. The second parameter. It also provides the Index within the Items() collection of the sender control. How to select them will of course be a matter of personal choice. we need to draw the text for the list item being drawn. Also. is an object of type DrawItemEventArgs that is a Graphics object hooked to the list item. which is usually not very professional. be aware that the line height typically specified by the control’s ItemHeight property is set to 14 (pixels).Font. e. AddressOf lstBox1_DrawItem”). starting at its left plus 20 (16 pixels for the icon and 4 for a separator space). as absolute numbers have a strange way of later introducing bugs.DrawIcon(My. and in this case a simple 16x16 Icon. For example. Using A Generic List Collection and an ImageList Control One method to determine which icon to draw for each line is to maintain a non-visible secondary ListBox or a Collection. we could simply add an offset of 20 (16+4).Index). we drew the text to the bounding rectangle. or by manually adding them in the Form Load() event.X + My. With that. or “Dim colListItem As New List(Of Integer)” for mapping indexes into an image list or into a custom routine that will return resource icons based upon an integer flag value or index. Brushes. such as “AddHandler Me. it would be REALLY nice if we could display appropriate icons for each of our entries. Page –192– . Using the ListBox font and a black brush.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. “e”. we can now draw the icon: e. because you can maintain a 1-to-1 index offset for ListBox and Collection items. Although we can get fancy and select an icon from a list in our application resources or an ImageList.ToString” used the index provided by e. if the line we are drawing is the currently selected line. The strongly-typed zero-based Generic Collections work great for this. and that I will assume is stored in our resources for now.Items(e. I also like to point 4 pixels beyond that.Bounds.ListBox2.myIcon.0 – David Ross Goben Here is the skeleton for a DrawItem() event: Private Sub LstBox1_DrawItem(ByVal sender As Object. Check this out: Dim Lb As ListBox = DirectCast(sender. ByVal e As DrawItemEventArgs) Handles ListBox1. just to give a nice buffer between the icon and the text.NET Beyond the Scope of Visual Basic 6. Because we will be drawing 16x16 icons to the line. And so we first issue the following command: e. but I have seen three methods that work quite well. which is a size I strongly recommend you provide to your own ListBox and ComboBox controls because this size will fit neatly on a ListBox line. 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.SelectedIndex = e. This is just like the automatic background clearing that a Paint() event performed. then we will need to also highlight it: If Lb. We will have to point beyond the icon to avoid over-writing it. but these functions auto-promote Ints! Here. we will just use a single generic icon for now. _ e.ToString.DrawFocusRectangle() 'draw focus rectangle if this line is selected Of course.DrawItem End Sub The generic sender parameter. and from its top. Also.Index). Lb. the command “Lb. Because our icon is 16x16. use “Dim colListItem As New List(Of String)” for mapping icon names. or index into an ImageList control for each entry.Index Then e.Graphics.Bounds.DrawString(Lb. basing that decision upon its extension. or container object for the list item.Black. such as the names of icons you may have placed in the application resources.Width + 4.Index to grab the text of the entry (. e.Resources.Graphics.DrawItem. and you can map an icon resource name.myIcon. we will want to change the control’s ItemHeight property to at least 16. allowing us to easily identify them. defined as the source of the event. In our case it is the ListBox.Location) 'draw icon (draw starting at top-left of the bounding rectangle for list item) Finally. The first thing we will want to do is refresh the item’s background.Enhancing Visual Basic . which we will review below.Y) 'Single values expected. For example. ListBox) 'get a reference to the listbox being used e.ToString).Bounds.Items(e. e. For example.Index Then e. 1))) DrawItemEventArgs) Handles Me. When a file is added to lstFiles.DrawBackground() 'redraw listbox line's background (blank it out) e.Bounds.Graphics.ico 'SystemDir.Black. For example.Graphics. LB.ico e.Width + 4.Y) If LB.Resources.SelectedIndex = e.DrawString(s. LB.SystemDir Case "h" Icn = My.Black.Images(glcFiles(e.Resources.Resources.HiddenDir Case "D" Icn = My.Directory Case Else ' "F" Icn = My.X + Icn.lstFiles. Brushes.Graphics.Resources.DrawImage(Img.DrawBackground() 'redraw listbox line's background (blank it out) e. e.Graphics. e.Bounds.ico 'HiddenFile.Items(e.Substring(0.SystemFile Case "S" Icn = My.Bounds.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item End Sub Page –193– . e.DrawIcon(Icn.Bounds.Location) 'draw image at start of line 'draw the text after the bitmap e.HiddenFile Case "H" Icn = My.Index)) e As 'get 'get 'get DrawItemEventArgs) Handles Me.Index).Index). we parse each file path and load the ListBox and Collection with the data: Me.DrawItem 'get the listbox pointed to by this event 'get string in listbox 'get the image stored in the ImageList e.SelectedIndex = e. which contains a series of icons.Y)) If LB.lstFiles.Bounds.Black. ListBox) 'get the listbox pointed to by this event S As String = LB.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item End Sub A variation on this is where the first character is “0” through “9”. we might find: Dim glcFiles As New List(Of Integer) 'new collection for mapping ImageList indexes When we are building the list.Bounds. and added to glcFiles. LB.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item End Sub Using A Flag at the Start of the List Item Text Another method I have seen is where people place a single character code as the first character of the string. that represent different file types.DrawItem the listbox pointed to by this event string in listbox the image stored in the ImageList (Icons are turned into Images) e.Items(e. Brushes.ico 'Normal Directory. CSng(e.Items(e.SubString(1).Y) If LB.Graphics. e.ico 'Normal File.lstFiles.Add(GetIconType(Path)) 'get the icon index for this type file and save to the collection thru a unser-defined GerIconType() method Finally.Enhancing Visual Basic . which is trimmed off during the display of the text.Location) 'draw icon at start of line 'draw the text after the icon.Substring(0. e.Font. in our DrawItem() event for LstFiles. but do not display the first character e.SubString(1).Index Then e. ListBox) S As String = LB.Font. This might be implemented as the following: Private Dim Dim Dim Sub lstFiles_DrawItem(ByVal sender As Object.DrawString(S. Brushes.Resources.X + Img.lstFiles. in the Form Load event.ToString Img As Image = ilstFiles. ListBox) S As String = LB.Width + 4.0 – David Ross Goben Consider this example: A ListBox named lstFiles contains a list of files.DrawImage(Img. but the character is matched to an index.Items.Graphics. ByVal LB As ListBox = DirectCast(sender. CSng(e. consider the following possible implementation of that: Private Dim Dim Dim Sub lstFiles_DrawItem(ByVal sender As Object.Add(path) 'add an item to the file list (NOTE: This code depends on the Boolean Sorted parameter being set to False) glcFiles. ByVal e As DrawItemEventArgs) Handles Me. we would put this all together: Private Dim Dim Dim Sub lstFiles_DrawItem(ByVal sender As Object. A Generic List Collection of type Integer named glcFiles has indexes into an ImageList control named ilstFiles.ToString Img As Image = ilstFiles.ToString 'get string in listbox Icn As Icon = Nothing 'init icon Select Case S.Index).DrawString(S. 1) Case "s" Icn = My.File End Select 'check left-most character 'SystemFile. ByVal e As LB As ListBox = DirectCast(sender.Font.DrawBackground() 'redraw listbox line's background (blank it out) e.Bounds.ico 'HiddenDir.Location) 'draw image at start of line 'draw the text after the bitmap e.SelectedIndex = e.DrawItem LB As ListBox = DirectCast(sender. where an index value is gathered by acquiring the integer value of that first character. e.Bounds.X + Img.Bounds.Resources. its type was checked.Width + 4). offset from 0.NET Beyond the Scope of Visual Basic 6.Images(CInt(S.Index Then e. Optional ByVal Index As Integer = 0) strItem = Text 'save string intItem = Index 'save integer End Sub 'used by ListBox and ComboBox to return text to an invoker Public Overrides Function ToString() As String Return strItem End Function End Class With this.Items(e.Y)) If LB.ToString”. we can do the following: Me.X + Img.Index Then e.Bounds.Index).lstFiles. Notice that if we had used a command like “Dim S As String = LB.Graphics.Graphics. the following is a no-holdsbarred bare-boned no-error-trapping two-bit son of a cussin’ custom class that we can add as data to a ListBox or ComboBox to store both a String and an Integer as data members: '******************************************************************************* ' Class ListItem ' VERY simple class to service a ListBox or ComboBox '******************************************************************************* Public Class ListItem Public strItem As String 'text data Public intItem As Integer 'Index data 'declare a string and an optional index Public Sub New(ByVal Text As String.Items.Bounds.Images(lstItm. that the lstFiles ListBox would have obviously employed the class’s ToString() method to acquire the text.Bounds.Y) 'draw image at start of line 'draw the text after the bitmap e.X + Img. In these cases.ToString” in the above code. However.Y) If LB.DrawItem LB As ListBox = DirectCast(sender.Bounds.Bounds.Index).Black.strItem” instead of “lstItm. we can ignore Generic List Collections and header flags to mark our List Item entries.SelectedIndex = e.Images(CInt(S.lstFiles.DrawImage(Img.ToString 'get string in listbox Dim Img As Image = ilstFiles.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item End Sub Using a Custom Class as a List Item to Index Graphics The method I prefer to use comes in two flavors: a method providing an index into an ImageList. e. if you were to use this control in another application that does not use user-drawn code. It converts the generic type object to string text.DrawFocusRectangle() 'draw focus rectangle if this item is the selected item End Sub NOTE: You can also use “lstItm.DrawItem e. 1))) 'get the image stored in the ImageList e.DrawString(S.SubString(1). Brushes. which used only strings. For example. These custom classes do not have to be big or complicated.Items(e.Bounds. This is why we have to add “. to add a File Path and an ImageList index to a single ListBox entry. ByVal e As DrawItemEventArgs) Handles Me.Location) 'draw image at start of line 'draw the text after the bitmap e.Width + 4).Black.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben NOTE: If you are using a ComboBox then you should do this slightly differently.DrawBackground() 'redraw listbox line's background (blank it out) If e. A VB. we could render the owner-drawn data using the following DrawItem() event code: Private Dim Dim Dim Sub lstFiles_DrawItem(ByVal sender As Object. As such. that the ListBox or ComboBox will also use the ToString() method to acquire the text to display to the user within its lists. and then leave Dim LB As ListBox = DirectCast(sender. LB.Graphics.DrawImage(Img.Font. Brushes.NET ListBox and ComboBox store objects as members of their Items collection. and a method providing icon names for resource icons. CSng(e. e.Index Then e. CSng(e. LB.ToString.lstFiles. ListItem) 'get ListItem object from ListBox Img As Image = ilstFiles.Bounds.Index = -1 Then Return 'if no selection made.X. unlike VB6.Font.Index parameter may sometimes enter the event set to -1. just blank the line out. Page –194– .intItem) 'get the image stored in the ImageList e.Index).Items(e. ListBox) 'get the listbox pointed to by this event lstItm As ListItem = DirectCast(LB.Substring(0. because in some cases a ComboBox may have nothing selected or initially displayed. GetIconType(Path))) 'add a string for display and an index into the ImageList control With this approach. e. e.DrawBackground() 'redraw listbox line's background (blank it out) e. you should rewrite the above event code like the following: Private Sub lstFiles_DrawItem(ByVal sender As Object.SelectedIndex = e. ListBox) 'get the listbox pointed to by this event Dim S As String = LB.Enhancing Visual Basic . e. For example. ByVal e As DrawItemEventArgs) Handles Me. the e.Add(New ListItem(Path.Width + 4.ToString” to extract the text data from a ListBox item to determine the text that is displayed in the list.DrawString(lstItm.Graphics. I will show you just a couple examples.Resources.ilstFiles. ' which specifies the Name property of the image Page –195– .GetObject("myIcon"). Although I consider this a bit slower in execution.Enhancing Visual Basic . if you require it: Dim Icn As Icon = DirectCast(My.NET Beyond the Scope of Visual Basic 6.ico") 'You can use a string variable in place of the text.Images("myIcon. it does work and it may be useful.0 – David Ross Goben The final thing I wanted to cover is using Icon text names as references.ResourceManager. and you will be able to adapt it accordingly. Icon) 'You can use a string variable ' in place of the text You can also extract from a ImageList control by using the Name property of an Image: Dim Img As Image = Me. but I am certain that you will be able to see its full potential. this can sometimes look a bit goofy.NET A lot of people are wishing they could center a message box on a form. the object in question (once it is active). But along the way. NOTE: wParam and lParam are names carried over from 16-bit Windows. A Thread Processor has just 3 parameters: 1.0 – David Ross Goben Centering a Message Box on a Form under VB. the serial code stream that executes from one point to another. 2. If the window invoking it is off to the side somewhere. A ThreadProc is hooked into a particular Thread Message Stream. and any owner or parent window involved. from resize. The other queue type is a Thread Message Queue. can get quite verbose in notifying the system or its parent object or its child objects of what it is doing. It would certainly look better over the form. We then reposition the Message Box on the screen to center it over our form and release it. 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. There is the general one. It turns out that the explanation for how to do this is quite simple. typically the thread messages associated with the current application. or to being activated. to move. especially system objects. iMsg: An Integer message ID value targeted to the thread it is being pumped through. or checks for a known handle that the code is designed to monitor. and the messages passing through that queue are for that thread alone (and for anyone choosing to listen in by hooking into the stream). where wParam was a Word-sized integer –16 bits at the time – and lParam was a Long Integer-sized parameter – then 32-bits. hWnd: This is the Integer or IntPtr handle of the object concerned. A Thread is often thought of as a mysterious system creature whose identity and use are beyond the reach of mere mortals. 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. The harder part is getting there. Now they are just two 32-bit Integer values. lParam: An optional secondary 32-bit Integer parameters whose use is determined by how iMsg is defined. You are not going to find all those messages in the default System Message Queue. When a Message Box is displayed. 4.Handle. messages essential to maintaining order in a very complex system. though some prefer CBTProc. 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. whether a form. by default it is centered on the screen. But a Thread is technically nothing more than a flow process. 3. they are notified only if they are hooked into that queue and if they are programmed to monitor that action. we can intercept it. because it requires very little code to do it.NET Beyond the Scope of Visual Basic 6. The user’s WndProc method checks this value for being itself (Me. a text box. wParam: An optional 32-bit Integer parameters whose use is determined by how iMsg is defined. 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 system message is sent out into a Message Queue notifying all concerned parties connected into its pipeline of this action. iMsg: An Integer message ID representing an associated pre-defined message regarding the hWnd window. objects. 3. A WndProc method monitors the System Message Queue and has 4 parameters: 1. wParam: An optional 32-bit Integer parameter whose use is determined by how iMsg is defined. That is because there are actually many types of Message Queues.Enhancing Visual Basic . 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)). 2. We can therefore also see a Thread as the message chatter the application objects exchange as they wiggle their way through the decision gates of their code stream. Page –196– . for example). But again. which is handled by what I call a ThreadProc (Thread Processor. Some Background Information When a window does anything. lParam: An optional but secondary 32-bit Integer parameter whose use is determined by how iMsg is defined. often referred to as the System Message Queue that is monitored by a WndProc (Window Processor). The Simple Explanation of Centering a Message Box on a Form If we monitor the system. However. to being created or destroyed. when a message box is about to be activated. or a check box. In most applications. When we are done with that. designed around the MsgBox command. Each process is made up of one or more threads. So. and we launched a message box. which is a synchronization primitive that can also be used for inter-process synchronization. to avoid the simultaneous use of a common resource to ensure only one thread has access to the data at any time. and if none are available. As it happens. _ wStyles As MsgBoxStyle) As MsgBoxResult Page –197– . declare its prototype like this: Private Declare Auto Function MessageBox Lib "user32" (ByVal ByVal ByVal ByVal hwnd As IntPtr.0 – David Ross Goben NOTE: A computer runs many applications at once.NET Beyond the Scope of Visual Basic 6. _ Optional ByVal Buttons As MsgBoxStyle = MsgBoxStyle. _ lpTitle As String. Buttons. The Detailed Explanation of Centering a Message Box on a Form In order to center a message box over a form. is a sequence of code that can be responsible for one aspect of the program.Enhancing Visual Basic . we can unhook ourselves from our thread’s Message Queue. _ Optional ByVal Title As String = vbNullString) As MsgBoxResult 'Set up the CBT hook FrmhWnd = ParentForm.NET applications. or even a single task the program has assigned to it. it attaches to one. or at least the one that the current form is attached to. though all threads will check it or even create it as needed. and each instance of an application is known as a process. and then we can compute how to center the message box on the form (its size is already determined at this time). This is very easy to do. it can be programmed to check if a Mutex is linked to it. the Mutex is released until the next potential clash. one thread might focus on the user interface.Handle 'save the parent form handle for use by our ThreadProc '-------------------------------------' STUFF WE HAVE YET TO DEFINE GOES HERE '-------------------------------------'Display the message box Return MsgBox(TextMsg. it will be centered and the process unhooked End Function NOTE: If you really want to get down to the brass tacks. because we no longer need to monitor it. background spell checking. If you do want to implement that instead of MsgBox.OkOnly. Once all concerned threads have detached themselves. Once the new location is computed. Because the current MessageBox object or MsgBox command do not provide for one. because each runs at its own pace. When a thread reaches a potential clash point. looking at exactly how it is done will make more sense. For example. garbage collection. When the active thread completes its work. it is not known where each thread will be relative to each other. If one is not. _ lpMessage As String. and one or more others to performing behind-the-scenes tasks. In more complex environments it may be necessary to process more than one thread. a barrier flag is needed to signal all clashing threads to wait until the flag is cleared. The trick is when we are dealing with more than one thread. Problems can crop up if one thread tries to access shared data while another thread is presently using or updating that data. and sets a flag that other threads designed to monitor that thread will interpret as a wait signal. While it is guaranteed that each individual thread will progress through its code in sequence. the wParam parameter will contain the window handle of the window about the be activated (the message box. _ ByVal TextMsg As String. If we were to hook into our application’s Thread Message Queue. As such. Each thread. we can set the message box’s new position using the SetWindowsPos() P/Invoke. we will need to create a wrapper for it so that it will. Such a flag is called a Mutex (Mutual Exclusion). they run on a single thread and are therefore called single-thread applications.Zero '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. we would receive this message. the MessageBox() P/Invoke does in fact reference a parent window handle as its first parameter. and so we would know that a message box is about to be activated. Consider this wrapper function. it detaches from the Mutex and the next thread in the queue has its turn at the data. having a serial flow of control. A Mutex is not owned by any thread. in our case). when the HCBT_ACTIVATE message is received. then it will receive a HCBT_ACTIVATE message when a subordinate window is about to be activated. such as most VB. such as database maintenance. with that basic understanding of the process. it creates one. or even running Bot services to scour referenced data wells for relevant information. the first thing we need to do is provide the message box with the form that it shall be centered over. use our form’s handle to get its bounding reactangle. Title) 'by invoking the message box. which shall do nicely: '************************************************************************************* 'local storage '************************************************************************************* Private FrmhWnd As IntPtr = IntPtr. We can use it to get its bounding rectangle. A More Detailed Explanation of Centering a Message Box on a Form If a ThreadProc is set up to catch CBT (CallBack Thread) Messages. Do not get nervous. though that is just one aspect of the Thread Processor that it is most-often used for (many WndProcs also use callbacks)). We will want to store this handle away so that we can use it later when we invoke the UnhookWindowsHookEx() P/Invoke to unhook ourselves. with the above defined. Buttons) 'by invoking the message box.".0 – David Ross Goben And implement it in the above CntrMsgBox shell like this: Return MessageBox(FrmhWnd. TextMsg. 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 about to be actually activated (after the background system code constructs it for us). darn it. "NOTICE") Or: If CntrMsgBox(Me. courtesy of the MsgBox command. Because the message we are interested in is the HCBT_ACTIVATE message (the “H” indicates a handle will be invoved). "Verify deletion of '" & sFileName & "'. This is sort of like running two copies of Notepad at the same time.NET Beyond the Scope of Visual Basic 6. the heading of our module should now look something like this: Page –198– . or Callback Thread). To get the Instance handle of the invoking form (not of the application). Thus far. let alone run multiple instances of itself. Likewise. we need it now (this is why many simply refer to this ThreadProc as a CBTProc. Title. we can invoke the GetWindowLong() P/Invoke with the handle of the form and a GWL_HINSTANCE constant. we must tell this P/Invoke that we want to monitor “CBT” type messages by providing it with a WH_CBT constant (Word-Sized Handle related to CBT. _ ByVal lpfn As ThreadProcCenterDelegate.DefaultButton2. this wrapper simply saves off the handle of the “parent” form and then displays the message box. which tells the P/Invoke that we want the instance handle of the form whose window handle we just provided it with. We also have to provide SetWindowsHookEx() with the address of our ThreadProc (this means. The SetWindowsHookEx() P/Invoke also returns the Integer handle of the new hook (callback) we just added. once we have finished processing the window.YesNo Or MsgBoxStyle.Question Or MsgBoxStyle. _ ByVal hmod As Integer. it will be centered and the process unhooked So far. we can simply invoke the GetCurrentThreadId() P/Invoke. _ MsgBoxStyle. it still displays the message box screen-center. 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. "Selection Already Made!".dll" Alias "SetWindowsHookExA" (ByVal idHook As Integer. This stuff is actually pretty safe to work with as long as you are not prone to insane experiments.OkOnly. we will need the UnhookWindowsHookEx() P/Invoke to unhook from it. To get the current thread ID.No Then Exit Sub Ideally. however. Regardless. because many applications can run multiple instances of forms. But apart from that. For example: CntrMsgBox(Me. MsgBoxStyle. for example). To hook our callback method into the thread queue requires the SetWindowsHookEx() P/Invoke. we will treat it just like a regular MsgBox command. before we display the message box. _ "Verify Delete") = MsgBoxResult. _ 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 '------------------------------------------------------------------------------------- The SetWindowsHookEx() P/Invoke 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).Exclamation Or MsgBoxStyle. To invoke CntrMsgBox.Enhancing Visual Basic . but one where the very first parameter is now the form we eventually want to center the message box over. The SetWindowsHookEx() and the UnhookWindowsHookEx() P/Invokes are declared as follows: '------------------------------------------------------------------------------------'hook a WndProc into the Thread Message Queue Declare Function SetWindowsHookEx Lib "user32. plus the instance handle of our form (this simply identifies which instance of the form we are using. we actually do have to write this callback method). ByVal wParam As Integer.Enhancing Visual Basic . In order to center the message box. Further. _ Optional ByVal Title As String = vbNullString) As MsgBoxResult 'Set up the CBT hook FrmhWnd = ParentForm. hInst.Handle 'save the parent form handle for use by our TheadProc Dim hInst As Integer = GetWindowLong(FrmhWnd. Even with the code we have right now. _ Optional ByRef Buttons As MsgBoxStyle = MsgBoxStyle. ByVal lParam As Integer) As Integer '************************************************************************************* ' ThreadProcCenter: Thread Message handler hook '************************************************************************************* Private Function ThreadProcCenter(ByVal iMsg As Integer. ByVal lParam As Integer) As Integer 'On HCBT_ACTIVATE. _ ByVal lpfn As ThreadProcCenterDelegate. show the MsgBox centered over parent Form If iMsg = HCBT_ACTIVATE Then 'if our form receives this message. Buttons. Title) 'by invoking the message box. _ ByVal hmod As Integer.Information. and the “parent” form’s bounding rectangle. I have disabled the actual invocation of the SetWindowsHookEx() P/Invoke until we have our ThreadProc method. we will need to get its bounding rectangle. although this will not affect the size of the executed compiled code by one byte.dll" () As Integer '------------------------------------------------------------------------------------'************************************************************************************* 'local storage '************************************************************************************* Private hHook As Integer = 0 'store our new hook handle Private FrmhWnd As IntPtr 'store parent form handle '************************************************************************************* ' Method : CntrMsgBox ' Description: Center a message box on a specified form using MsgBox syntax '************************************************************************************* Public Function CntrMsgBox(ByRef ParentForm As Form. 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. ByVal nIndex As Integer) As Integer '------------------------------------------------------------------------------------'get current thread ID from a window handle Private Declare Function GetCurrentThreadId Lib "kernel32.OkOnly Or MsgBoxStyle. We can do that with another P/Invoke. ByVal TextMsg As String. GetWindowRect(). ByVal wParam As Integer. we can now enable the SetWindowsHookEx() line in the code above. and save its handle 'Display the message box Return MsgBox(TextMsg. we will have to declare two versions of this P/Invoke. which will be supplied by our wParam parameter.dll" Alias "SetWindowsHookExA" (ByVal idHook As Integer. and an Integer window handle.NET Beyond the Scope of Visual Basic 6. one to accommodate an Intptr window handle. ThreadProcCenter. So let us write it. which is supplied by our “parent” form. it will be centered and the process unhooked End Function As you can see. we will need Page –199– . Thread) 'insert a new hook. Our ThreadProcCenter method is defined by the following template shell: '------------------------------------------------------------------------------------'Declare Delegate for subclassed ThreadProc procedure: Private Delegate Function ThreadProcCenterDelegate(ByVal lMsg As Integer. However. GWL_HINSTANCE) 'get instance handle for our form Dim Thread As Integer = GetCurrentThreadId() 'get the current thread ID 'hHook = SetWindowsHookEx(WH_CBT. above). the message box will still display centered to the screen. It is probably the easiest part. AddressOf ThreadProcCenter.0 – David Ross Goben '************************************************************************************* 'P/Invoke stuff '************************************************************************************* Private Const GWL_HINSTANCE As Integer = (-6) 'obtain window instance handle (used by GetWindowLong P/Invoke) 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 Message Queue Declare Function SetWindowsHookEx Lib "user32. This is because we have yet to change its location (that code will go into the red zone. _ ByVal dwThreadId As Integer) As Integer '------------------------------------------------------------------------------------'unhook WndProc 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. actually written. as it did before. Width \ 2 'point to center of parent. provided by our ThreadProc wParam parameter. then backup 1/2 msgbox height All that we have left to do is to actually position the form. _ ByVal Y As Integer.iTop + 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. or as the lowest of the topmost windows. to ignore it. below. since I already know I will need to compute the width and the height of the received rectangles. this defines the top-left corner of the window.0 – David Ross Goben to provide it with rectangle structures that will receive 4 Integer values: the window’s Left. _ ByVal X As Integer. or as the highest of the nontopmost windows (-2). Top. _ ByVal cx As Integer. 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. this is accomplished with the SetWindowPos() P/Invoke. Right. but still above all non-topmost windows (-1). using Integer Private Declare Function GetWindowRect Lib "user32.dll" (ByVal hwnd As IntPtr. Leave it 0.NET Beyond the Scope of Visual Basic 6. We do not want to move it. Page –200– . and place the window after a specified window handle.iTop End Function End Structure '------------------------------------------------------------------------------------'method used to get the top-left and bottom-right pixel coordinates of a form. _ ByRef lpRect As RECT) As Integer 'method used to get the top-left and bottom-right pixel coordinates of a form. With Y. or as the topmost window in the ZOrder (0). then backup 1/2 msgbox width Dim Y As Integer = rectForm.Height \ 2 'point to center of parent. using IntPtr Private Declare Function GetWindowRect Lib "user32.iLeft + rectForm. as the last (1).Width \ 2 . _ ByVal cy As Integer.rectMsg. _ ByRef lpRect As RECT) As Integer '------------------------------------------------------------------------------------- With that. which is behind the lowest topmost window.Enhancing Visual Basic . I will add a couple of methods to compute them in the structure. inserting it below the test “If iMsg = HCBT_ACTIVATE Then” in the ThreadProcCenter method: Dim rectForm.rectMsg.Height \ 2 . The new structure and P/Invokes 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. As stated much earlier. _ ByVal hWndInsertAfter As Integer. which is declared below: '------------------------------------------------------------------------------------'set a window position on the screen Private Declare Function SetWindowPos Lib "user32. _ ByVal wFlags As Integer) As Integer '------------------------------------------------------------------------------------- The parameters for this P/Invoke are as follows: • • • The hWnd parameter is the handle of the window to position.iLeft End Function '---------------------------------'compute rectangle height Public Function Height() As Integer Return iBottom . That can make your brain hurt. The X parameter is the left coordinate for the window. The hWndInsertAfter parameter is useful if you want to change the ZOrder of windows.dll" (ByVal hwnd As Integer. and Bottom pixel positions on the screen. rectForm) 'get the parent form bounding rectagle GetWindowRect(wParam.dll" (ByVal hWnd As Integer. Also. We will use SWP_NOZORDER.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 . at the top of the non-topmost. we can now compute the new upper-left corner of the message box so that it will be centered on the form. and we do not want the form to be re-activated (which would force it to change position in the ZOrder stack. SWP_NOREPOSITION &H200 Same as the SWP_NOOWNERZORDER flag. Sends a WM_NCCALCSIZE message to the window. SWP_NOZORDER &H4 Retains the current Z order (ignores the hWndInsertAfter parameter). the nonclient area (including the title bar and scroll bars). WM_NCCALCSIZE is sent only when the window's size is being changed. 0. SWP_NOOWNERZORDER &H200 Does not change the owner window's position in the Z order. or change its size. If SWP_NOSIZE specified. If this flag is not specified. SWP_DRAWFRAME &H20 Draws a frame (defined in the window's class description) around the window. the window is activated and moved to the top of either the topmost or non-topmost group (depending on the setting of the hWndInsertAfter parameter). It is defined as follows: wFlags Value Meaning SWP_ASYNCWINDOWPOS If the calling thread and the thread that owns the window are attached to different input queues. X. SWP_FRAMECHANGED &H20 Applies new frame styles set using the SetWindowLong function. The wFlags parameter allows for customization of the window. If this flag is set. which we do not want). Because we do not want to change the current ZOrder. the valid contents of the client area are saved and copied back into the client area after the window is sized or repositioned. detailed below.Enhancing Visual Basic . This prevents the calling thread from blocking its execution while other threads process the request. The cx parameter defines the new width of the window. even if the window's size is not being changed. and SWP_NOACTIVATE constants. this defines the top-left corner of the window. The cy parameter defines the new height of the window. SWP_NOSIZE &H1 Retains the current size (ignores the cx and cy parameters). SWP_NOSENDCHANGING &H400 Prevents the window from receiving the WM_WINDOWPOSCHANGING message. If SWP_NOSIZE specified. we can add our last line of code. SWP_NOREDRAW &H8 Does not redraw changes. When this flag is set. SWP_DEFERERASE &H2000 Prevents generation of the WM_SYNCPAINT message. 0. the system 0x4000 posts the request to the thread that owns the window. SWP_SHOWWINDOW &H40 Displays the window. SWP_HIDEWINDOW &H80 Hides the window. detailed below. then this parameter is ignored. then this parameter is ignored. SWP_NOMOVE &H2 Retains the current position (ignores X and Y parameters). With X.NET Beyond the Scope of Visual Basic 6. the application must explicitly invalidate or redraw any parts of the window and parent window that need redrawing. This applies to the client area. and any part of the parent window uncovered as a result of the window being moved. If this flag is not specified. If this flag is not set. SWP_NOSIZE. SWP_NOACTIVATE &H10 Does not activate the window. inserted right after we calculated the X and Y coordinates of our message box’s new upper-left coordinate. SWP_NOSIZE Or SWP_NOZORDER Or SWP_NOACTIVATE) Page –201– . 0. and begin centering message boxes over forms at will: 'now Position the msgbox to the computed coordinates SetWindowPos(wParam. Y. we are thus interested in the SWP_NOZORDER. SWP_NOCOPYBITS &H100 Discards the entire contents of the client area. So we should declare the following constants at the top of our module with our other constants: Private Const SWP_NOSIZE As Integer = &H1 Private Const SWP_NOZORDER As Integer = &H4 Private Const SWP_NOACTIVATE As Integer = &H10 'Retains the current form size (used by SetWIndowPos P/Invoke) 'Retains the current Z order (used by SetWIndowPos P/Invoke) 'Does not activate the window (used by SetWIndowPos P/Invoke) With that. no repainting of any kind occurs.0 – David Ross Goben • • • • The Y parameter is the top coordinate for the window. _ ByVal Y As Integer. Private Declare Auto Function MessageBox Lib "user32" (ByVal hwnd As IntPtr.iLeft End Function '---------------------------------'compute rectangle height Public Function Height() As Integer Return iBottom . _ ByVal nIndex As Integer) As Integer '------------------------------------------------------------------------------------'get current thread ID from a window handle Private Declare Function GetCurrentThreadId Lib "kernel32. _ ByVal lpMessage As String.0 – David Ross Goben Putting It All Together Now that we have defined all parts of the solution. Just change the highlighted code. Simply ' invoke the CntrMsgBox() function as you would the ' MsgBox() form.dll" (ByVal hwnd As Integer.The CntrMsgBox() will center a message box on a ' specified form. a set of buttons. we should glue them all together so that you can copy them into your own module and begin using it to center your own message boxes. _ ByRef lpRect As RECT) As Integer 'method used to get the top-left and bottom-right pixel coordinates of a form. _ ByRef lpRect As RECT) As Integer Page –202– . below. _ ByVal X As Integer.dll" (ByVal hWnd As Integer.NET Beyond the Scope of Visual Basic 6.iTop End Function End Structure '------------------------------------------------------------------------------------'method used to get the top-left and bottom-right pixel coordinates of a form. 'such as status or error information. NOTE: You can quickly adapt this code to work with other system dialogs. _ ByVal lpfn As ThreadProcCenterDelegate. _ ByVal hWndInsertAfter As Integer. _ ByVal wStyles As MsgBoxStyle) As MsgBoxResult '------------------------------------------------------------------------------------' Structure RECT 'structure to declare the top-left and bottom-right pixel coordinates of a form '------------------------------------------------------------------------------------<StructLayout(LayoutKind.dll" Alias "SetWindowsHookExA" (ByVal idHook As Integer. The message box returns an integer value that indicates which button the user clicked.dll" () As Integer '------------------------------------------------------------------------------------'set a window position on the screen Private Declare Function SetWindowPos Lib "user32. _ 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.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 . except you additionally specify the form ' that the message should be centered over. What follows is the complete modCntrMsgBox module: Option Strict On Option Explicit On Imports System. and a brief application-specific message. '************************************************************************************* '************************************************************************************* 'P/Invoke stuff '************************************************************************************* Private Const GWL_HINSTANCE As Integer = (-6) 'instance handle flag (used by GetWindowLong P/Invoke) Private Const SWP_NOSIZE As Integer = &H1 'Retains the current form size (used by SetWIndowPos P/Invoke) Private Const SWP_NOZORDER As Integer = &H4 'Retains the current Z order (used by SetWIndowPos P/Invoke) Private Const SWP_NOACTIVATE As Integer = &H10 'Does not activate the window (used by SetWIndowPos P/Invoke) 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. _ ByVal cy As Integer. _ ByVal wFlags As Integer) As Integer '------------------------------------------------------------------------------------'Displays a modal dialog box that contains a system icon.dll" (ByVal hwnd As IntPtr.Runtime. _ ByVal lpTitle As String. using Integer Private Declare Function GetWindowRect Lib "user32. _ ByVal hmod As Integer. _ ByVal cx As Integer.Enhancing Visual Basic .InteropServices Module modCntrMsgBox '************************************************************************************* ' modCntrMsgBox . using IntPtr Private Declare Function GetWindowRect Lib "user32. instead of the center of the screen. Height \ 2 . 0. Title. rectForm) 'get the parent form bounding rectagle GetWindowRect(wParam. GWL_HINSTANCE) 'get the instance handle for our form Dim Thread As Integer = GetCurrentThreadId() 'get the current thread ID hHook = SetWindowsHookEx(WH_CBT. ByVal lParam As Integer) As Integer 'On HCBT_ACTIVATE. _ Optional ByRef Buttons As MsgBoxStyle = MsgBoxStyle. it will be centered and the process unhooked End Function '************************************************************************************* ' ThreadProcCenter: Thread Message handler hook '************************************************************************************* Private Function ThreadProcCenter(ByVal iMsg As Integer.Handle 'save the parent form handle for use by our TheadProc Dim hInst As Integer = GetWindowLong(FrmhWnd. Y.Width \ 2 'point to center of parent. TextMsg. hInst. then backup 1/2 msgbox width Dim Y As Integer = 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. _ ByVal lParam As Integer) As Integer '************************************************************************************* 'local storage '************************************************************************************* Private hHook As Integer = 0 'store our new hook handle Private FrmhWnd As IntPtr 'store parent form handle '************************************************************************************* ' Method : CntrMsgBox ' Description: Center a message box on a specified form using MsgBox syntax '************************************************************************************* Public Function CntrMsgBox(ByRef ParentForm As Form. 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. ByVal wParam As Integer. show the MsgBox centered over parent Form If iMsg = HCBT_ACTIVATE Then 'if our form receives this message. then backup 1/2 msgbox height 'now Position the msgbox to the computed coordinates SetWindowPos(wParam.Height \ 2 'point to center of parent.iTop + rectForm. Thread) 'insert a new hook.Enhancing Visual Basic .OkOnly Or MsgBoxStyle. _ ByVal wParam As Integer. a SUBORDINATE form is about to be activated Dim rectForm.rectMsg.Information.iLeft + rectForm. _ ByVal TextMsg As String.NET Beyond the Scope of Visual Basic 6.rectMsg. _ Optional ByVal Title As String = vbNullString) As MsgBoxResult 'Set up the CBT hook FrmhWnd = ParentForm. X. Buttons) 'by invoking the message box. AddressOf ThreadProcCenter.Width \ 2 .0 – David Ross Goben '------------------------------------------------------------------------------------'Declare Delegate for subclassed ThreadProc procedure: Delegate Function ThreadProcCenterDelegate(ByVal lMsg As Integer. SWP_NOSIZE Or SWP_NOZORDER Or SWP_NOACTIVATE) 'Release this WndProc hook because we now no longer need it UnhookWindowsHookEx(hHook) hHook = 0 End If Return 0 '0 allows an operation to continue to be processed (1 would prevent it) End Function End Module Page –203– . 0. and save the handle of this new hook 'Display the message box Return MessageBox(FrmhWnd. 0. Others are simple to adapt to. Under . In such a case. Indeed. Indeed.NET You should be quick to notice that VB. lines.GetPixel(intX. Just look at the damage hoards of die-hard fans had forced on bitwise and logical operations. etc.Width. these are now much easier to use and are also a lot faster. to eliminate flicker. is where such a command should be. back to just archaic and ambiguous (“Mongo fears change. These properties are easy enough to compute and update within local variables without eating control resources to store them within every single drawing surface. some changes are easy. when more advanced graphical capabilities are added to . intY) 'Get an ARGB color value at a specified X.NET Beyond the Scope of Visual Basic 6. or. are grouped under a separate namespace. logically. However. the only time we should really need to make a copy of it is when we will be making many changes to the image.PictureBox1.Height.0 – David Ross Goben Restoring Raster Graphics and Rubber Bands to .CreateGraphics) 'create a blank bitmap (notice the linking of the form's graphical interface) 'or. For instance. especially when only a few of them will actually use them. Granted. which a blank PictureBox or a form with no background image may be. And regarding Bitmap objects – most examples tell you to create a new Bitmap object and fill it will the image from the PictureBox.PictureBox1. arcs. we can use the methods in that class to draw on graphical objects. the VB6 Point command is now GetPixel().NET than under VB6.NET. Some others do not exist anymore because it is much easier and more efficient to provide and maintain these features within your own code. like shifting the VB6 Cls command to Clear under VB. we can then get the color at a specified point on it using: Dim Clr As Color = Bmp. we can alternatively declare it so: Dim Bmp As New Bitmap(Me. and must be accessed through a Bitmap object.PictureBox1. Me. as previously stated. to get a pixel point on an image.Drawing. Cases in point are the VB6 CurrentX and CurrentY last drawing location properties. Dim Bmp As New Bitmap(Me.” to quote Mongo from a cut scene from Mel Brook’s comedy. Me. For example. By keeping all these things in different namespaces. By placing them as members in a separate static class. and especially as a part of its core language. In the meantime we are not cluttering up the language as was sadly done to VB6. I had been very fearful that the broad and largely nonprofessional. and there is also less likelihood for method name collisions. albeit fiercely devoted user-base would force VB.Image) 'instantiate a new copy of the image as a bitmap (useful for many changes) We can instead simply do the following (note that a Bitmap and an Image are identical in structure): Dim Bmp As Bitmap = DirectCast(Me.NET. no longer eating up valuable resource space if you do not employ them.Width. Me. but this is a waste of time and resources if we just need to access the image or to make a quick alteration of it. If we forget to specify or import their namespace.Y point (duplicates the VB6 Point() method) Page –204– . Bitmap) 'define a reference to the image without instantiating a new image object Certainly..Height. but want to update the actual displayed image only once. circles.. drawing is largely different under VB.PictureBox1. changing them from innovative and more clearly defined. instead of chewing up resources and valuable time doing something like this: Dim Bmp As New Bitmap(Me. as had been the case under VB6. later. Blazing Saddles). any specialized features. such as using the System.Enhancing Visual Basic .NET to end up suffering such a fate during its development cycle. which.CreateGraphics) 'create a blank PictureBox image refrence Regardless of how we define the bitmap interface. the only times we would need to create a separate bitmap is when. suppose someone had developed a class supporting OpenGL or Direct3D drawing that had used intrinsic drawing commands. as easy and as better as all this is.PictureBox1. Others are not obvious. Me. we would find ourselves drawing using intrinsic features instead of enhanced features and we would then have to track down the bugs by finding where we forgot to reference our graphical class. such as drawing.Graphics object to draw shapes. to a PictureBox.Image.NET. I say thank goodness! I still cannot believe that the very primitive DOS-BASIC-style drawing commands had managed to make their way all the way into VB6. we have simplified the language and allowed for easy expansion of its capabilities. This is a very good thing. we need to make numerous changes or if the target image object might actually be set to Nothing. Otherwise. we can instead use the members of the namespace devoted to that paradigm.NET does not support general drawing commands as a part of its intrinsic language repertoire. 0) 'draw a white line from bottom-left to top-right End With Catch 'ignore errors (usually happens when no image data is loaded in a PictureBox.DrawEllipse(Pens.Graphics.Width. on page 172.EventArgs) Handles PictureBox1.NET Beyond the Scope of Visual Basic 6. It may be easy to draw within its Paint() event (as demonstrated earlier in this document – see the article. For example. or whatever raster drawing operation you would ever want to perform. 0.CreateGraphics You may also occasionally notice such objects defined like the following.0 – David Ross Goben To set a single point. R2_NotXOrPen. as will be demonstrated shortly. Even so. I find the above ellipse advice odd.SetPixel(100. .NET (regardless of the fact that these are in fact simple processes).FromImage(Me. To access a graphical interface to it using an object named eg (reminding us of e. we could use code something like this: 'generate a graphical interface to a picture box (same as e. it is not as wasteful as it might first appear because . The reason for this is that Raster operations are not supported by GDI+ under .CreateGraphics).Graphics provided by a PaintEventArgs object) Dim eg As Graphics = Me. as shown here: Bmp.PictureBox1. Easy Ways to Draw Lines and Shapes. Using egh is non-persistent and alters only the displayed image. shape.NET will see that the width and height are set to 1. they seem to get a bit stuck when they want to simply draw a unique line.Height. which is provided by a Paint event. Dim egi As Graphics = Graphics. However.CreateGraphics 'generate a non-persistent graphical interface to a picture box image With Me.Graphics object that is tied to the PictureBox.Object.Image) 'useful for drawing text or copying images DIRECTLY to the image. Color. complementing its GetPixel() method. It could be normal (R2_CopyPen). 0. or more often. In fact. Suppose we want to draw a non-persistent white “X” across an image if the user clicks on it. Though slothful.PictureBox1. 100. which is also valid: Dim egh As Graphics = Graphics.Click Try Dim egh As Graphics = Me.PictureBox1 egh. . and use it outside of Paint events.White.Handle) 'a graphical interface to the display (same as Me. Page –205– .PictureBox1. we can instead easily create our own graphical interface to the object. R2_XOrPen. and to Paint in VB. 0. 100y with width and height of 1 NOTE: Rather than using e. R2_MergePenNot.PictureBox1. such as a drawing program might use. the SetPixel() P/Invoke under gdiplus. and so it will just draw a dot.NET Other graphical features seem to be lost in limbo. or text to the PictureBox.Width. And because of this. where the PaintEventArgs “e” parameter provides us with access to a convenient Graphics object that is linked to the PictureBox.graphics). accessing this graphical interface is really not complicated. so the Image object is set to Nothing) End Try End Sub Restoring Raster Operations to . 100.Enhancing Visual Basic . implementing it in a customized method so that they could apply those non-typical drawing features. . we may often be advised to draw a circle with a radius of 1. What we need to understand is that the graphical interface provided by the Paint() event is just a simple System. 1. Try this: 'react to user clicking on picture Private Sub PictureBox1_Click(ByVal sender As System. out of frustration. NOTE: When drawing shapes.dll is much faster. like so: e. However.FromHwnd(Me.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. people have placed specialized hooks into a Paint() event that passes back the event argument object. .Black. R2_MergePen. I have seen code where.PictureBox1.Graphics.NET run into is when they try to draw shapes at will to something like a PictureBox.Drawing.DrawLine(Pens. as shown here.DrawLine(Pens. for instance). such as the VB6 DrawMode property that allowed you to specify how a pen object is to be drawn on the background. especially when VB. But this normally leads only to exception errors.Height) 'draw a white line from top-left to bottom-right egh.White.NET already supports a much faster SetPixel() method under the Bitmap object. Using egi is more persistent because it alters the stored image. suppose we want to draw to PictureBox1. be sure to refresh the image if you use egi so any changes using it will be displayed. and this is something that is quite easy to generate within our code. 100. saving it off somewhere. ByVal e As System. 1) 'using Elimpse to draw a point at 100x. providing an interface to the fore-mentioned SetPixel() P/Invoke.NET. under GDI+. and this is because GDI+ is linked directly to the operations available to graphics cards though their firmware (software burned into a board-based read-only memory chip (ROM)). such as the GDI+ SourceOver setting offers.NET. brush. which is the closest thing it has to the GDI Raster Operations property. graphical interface. Specifies a combination of the colors are common to the background color and the inverse of the pen. Specifies a combination of the pen color and the inverse of the display color. it is blended with the background color. This is just like the GDI Raster R2_CopyPen command. The pen over-writes anything it draws over (Default). create the foreground drawing pen with a selected color and width. creating pens and brushes to draw with. SourceCopy. but it was quite naturally incorporated into . What this would actually involve are two support methods that can be invoked automatically. select each of them into the object we want to draw on. Specifies a white pen color. select them into the DC. and finally release these created resources and the DC. Specifies a combination of the colors common to both the pen and the display. After we perform the drawing task. create the background brush (the fill) with its color and pattern. there is no alpha-channel-blending support provided by GDI.Enhancing Visual Basic . Indeed. perform the drawing operation. such as drawing a line. “behind the curtain”.Graphics. just like we would do under GDI+. but not in both. the System. Specifies a combination of the display color and the inverse of the pen color.NET and C#. Specifies a combination of the pen color and the display color. Page –206– . and so it was naturally exposed to VB. even with all this functionality. is faster and more powerful than standard GDI. Conversely. which we will name InitPenAndBrush(). the other method is invoked. Most-used next to R2_CopyPen. which will select back in the previous pen and brush to the target and finally release the resources of our new pen. What this means is that the raster-generated selection rectangle that is used so often in Windows is not supported by GDI+ under . is duplicated by the GDI R2_CopyPen setting. will get the Graphical interface and the Device context of the target object.NET. we perform the actual drawing task. the output remains unchanged. repeated steps that can be performed for you in an automated manner. and select the type of Raster Operation we need. Specifies the pen color. This involves obtaining the Device Context (DC) of the object we want to draw to. For example. has only 2 meager enumerated settings: Member name SourceOver SourceCopy Description Specifies that when a color is rendered. introduced in 2001 with Windows XP. The first method. Specifies the inverse of R2_CopyPen.Drawing. save the pen and brush that were previously assigned. The blend is determined by the alpha component of the color being rendered. Getting One’s Hands Dirty So. Next. Specifies the inverse of R2_MaskPen. and device context. Specifies a combination of the colors in the pen and in the display color. Specifies an inverse of R2_XOrPen. and the second will automatically close down from it after your drawing task. As you can see.0 – David Ross Goben GDI+. Specifies the inverse of the display color. the older GDI Raster operations implement 17 useful settings. GDI+ was originally meant to be used by C/C++ developers. The other GDI+ setting.NET Beyond the Scope of Visual Basic 6. Specifies that when a color is rendered. No other correlations between these two platforms are yet supported. if you want to implement GDI Raster operations under . the first will automatically set up for a drawing task you want to perform.CompositingMode property. Specifies a combination of the colors are common to both the pen and the inverse of the display. as defined below: Member name R2_Black R2_CopyPen R2_MakePenNot R2_MaskNotPen R2_MaskPen R2_MergeNotPen R2_MergePen R2_MergePenNot R2_NoOperation R2_Not R2_NotCopyPen R2_NotMaskPen R2_NotMergePen R2_NotXOrPen R2_White R2_XOrPen Description Specifies black pen color. so you can focus entirely on the actual drawing tasks. out of sight and mind. Specifies the inverse of R2_MergePen. it overwrites the background color (Default). Most Used. you need to get your hands dirty and perform GDI operations in much the same way as I do them under C/C++. GDI has no similar command because GDI does not support Alpha blending.NET. even though this appears to be an involved process. Of course. Specifies no operation. It thus naturally lacks support for Raster Operations (memory-mapped operations) simply because the card’s firmware lacks it. which we will name DisposeResources(). it all boils down to a uniform series of simple. listed on the next page. DrawCircle(). DrawEllipse().PenColor = m_GDI.0 – David Ross Goben Of course. be sure your Pen color (foreground).BrushColor m_GDI. naturally. Green.PS_DASH 3 = HatchStyle.DrawRectangle(Me.NET author who had designed an ordered structure to the class and defined its naming conventions – if anyone recognizes it. As such. and also from a lot of my own current and previous work (actually. You set the properties for the drawing operation by change the properties of the class. vARGB And &HFF) 'return RGB color End Function You then perform the desired drawing operation. is derived from various C++/C# sources. (vARGB >> 8) And &HFF. DrawRoundRect(). and the Raster operation is R2_CopyPen. when you are ready to draw.PenWidth = m_GDI.PS_SOLID) 'Set the pen width to 3 pixels (Default is 1) 'set the brush style to Crosses (default is HatchStyle. and Blue. For example: m_GDI. to invoke any required drawing operations.Transparent) 'merge the pen and background colors (default is RasterOps. DrawRectangle(). you can set the drawing pen’s color and width. please let me know so I can render credit). for Alpha.Enhancing Visual Basic . However. DrawEllipse(). DrawRectangle(). the Brush color is Transparent. Note that any additional features you would personally need can also be added to this class quite easily.NET Beyond the Scope of Visual Basic 6. You need to change colors and Raster operations only as needed. Somewhere within your code. defined below: '************************************************************************************************************* ' Function: ARGBtoRGB ' Helper function to covert Alpha Color ARGB value (AARRGGBB) to RGB (00BBGGRR) '************************************************************************************************************* Private Function ARGBtoRGB(ByVal clr As Color) As Integer Dim vARGB As Integer = clr. if you need to change them (they are saved within the class properties so you do not need to set them with every drawing command). NOTE: If the Pen color is Transparent. and DrawObround(). DrawRoundRect(). The class performs this for you automatically using the ARGBtoRGB() function. or by providing a bounding Rectangle object.25 (top-left) to 100. as needed. Also. GetPoint(). you can also specify them by providing Point objects to define their top-left and bottom-right bounds. and the Raster operation are as you need them to be. the Red and Blue color values must also be swapped. Once you instantiate an instance of this class. the Pen style is PS_SOLID. the Brush hatch pattern is HS_SOLID (no pattern).RasterOp = Color.ToArgb 'convert color value to AARRGGBB Return RGB((vARGB >> 16) And &HFF. but this is not enough.ToArgb And &HFFFFFF operation. the brush’s color and hatch style (hatch style HS_SOLID is the default).LightBlue PenStyles. 25). By default. New Point(25. ARGB is stored internally as AARRGGBB.HS_SOLID) 'set the brush color to Blue (default is Color. RGB is stored as 00BBGGRR. The GDI32 class. To implement this class into your own application is very simple. one of the anonymous C++/C# postings in fact credited a sadly unremembered VB. DrawLine().BrushHatch m_GDI. so all we have to do is concern ourselves with invoking its public methods and properties. For example: 'draw a rectangle from PictureBox coordinates 25.R2_CopyPen) NOTE: GDI requires RGB colors. such as DrawArc(). we can create a class that performs all this added work for each of the drawing functions for us. not every time.HS_CROSS = Color. you would declare an instance of the GDI32 class: Friend m_GDI As New GDI32 'instantiate an instace of the GDI32 class Then. then the drawing operation will use a stock Null Pen (invisible). perhaps as publicly as possible. This will draw using a solid white pen with an invisible brush (no background color for the pen will be painted over the target surface. New Point(100.PictureBox1. then the drawing operation will use a stock Null Brush.CreateGraphics. Page –207– .Blue RasterOps. As such. Most gurus will tell you to simply perform a Clr. Red. Because several of the methods. not the ARGB colors that are part of the System Color Palette and GDI+. the background brush. and invoke any of the required drawing operations: SetPoint().White) 'alternate dashes with dots (default is PenStyles. 100)) Now draw as you see fit.PenStyle = m_GDI.100 (bottom-right) m_GDI. the type of Raster Operation and. to make this even easier. the Pen color is White. typical for ellipses and rectangles). If the Brush color is Transparent. Brush color (background). as needed.R2_XOrPen 'set pen color to Light Blue (default is Color. We will simply define the pen we want to draw with. DrawArc(). where each letter represents a Hexadecimal digit. we must convert them from ARGB to RGB. and DrawObround() have overloaded methods. you must provide a graphical interface to the target object you want to draw upon. 'Windows variable-pitch (proportional space) system font. This stock object is provided only for compatibility with 16-bit Windows versions earlier than 3. left-to-right hatch. R2_NotMaskPen = 8 'Specifies the inverse of MaskPen. R2_MergePenNot = 14 'Specifies a combination of the pen color and the inverse of the display color. 'Hollow brush (equivalent to NULL_BRUSH) 'White pen. R2_MergeNotPen = 12 'Specifies a combination of the display color and the inverse of the pen color. The color can be changed by using the SetDCPenColor() function.NET platform.0. R2_White = 16 'Specifies a white pen color. End Enum 'Type of Raster operation (how drawing interacts with the background) Public Enum RasterOps As Integer R2_Black = 1 'Specifies black pen color. 45-degree crosshatch. '************************************************************************************** Page –208– . R2_XOrPen = 7 'Specifies a combination of the colors in the pen and in the display color.dll). 'Null Pen. R2_CopyPen = 13 'Specifies the pen color. ' 'the dimensions of the figure are shrunk so that it fits entirely in the bounding rectangle. The color can be changed by using the SetDCBrushColor() function. but not in both. a Graphical Design Interface used to take advantage of Graphic ' card hardware and software. the system uses the system font to draw menus. 'Default font for user interface objects such as menus and dialog boxes. such ' as displaying rubberband lines was lost. This is MS Sans Serif.NET Beyond the Scope of Visual Basic 6. ' GDI+ offers faster operations that those provided by GDI. dialog box controls.NET '************************************************************************************** #Region "GDI32 Enumerations" '***************************************************************************************************************************** ' Enumerations '***************************************************************************************************************************** ' Pen Styles (how lines are drawn) Public Enum PenStyles As Integer PS_SOLID = &H0 'A pen style that is a solid color. #End Region '************************************************************************************** ' GDI Class to support GDI operations not supported by GDI+ ' GDI+ (gdiplus. horizontal and vertical cross-hatch. The default color is white.Enhancing Visual Basic . vertical hatch. 'Win2K/XP: Solid pen color. left-to-right hatch. It was designed for ' use by C/C++ users. 'Light gray brush. It was introduced with Windows XP. ●●●●●● PS_DASHDOT = &H3 'A pen style that consists of alternating dashes and dots.GDI Support for . 'Windows fixed-pitch (monospace) system font. yet Raster operations. and text. This palette consists of the static colors in the system palette. but naturally was incorporated into the . 'Null Brush (equivalen to HOLLOW_BRUSH). The default color is white. R2_NoOperation = 11 'Specifies no operation. 'Default palette. -●●-●● PS_NULL = &H5 'A pen style that is invisible. R2_Not = 6 'Specifies the inverse of the display color. When this style is specified in a drawing record that takes a bounding rectangle.End Enum End Enum 'Hatch Styles Public Enum HatchStyle As Integer HS_HORIZONTAL = 0 '----HS_VERTICAL = 1 '||||| HS_FDIAGONAL = 2 '\\\\\ HS_BDIAGONAL = 3 '///// HS_CROSS = 4 '+++++ HS_DIAGCROSS = 5 'xxxxx HS_FDIAGONAL1 = 6 ' HS_BDIAGONAL1 = 7 ' HS_SOLID = 8 ' HS_DENSE1 = 9 ' HS_DENSE2 = 10 ' HS_DENSE3 = 11 ' HS_DENSE4 = 12 ' HS_DENSE5 = 13 ' HS_DENSE6 = 14 ' HS_DENSE7 = 15 ' HS_DENSE8 = 16 ' HS_NOSHADE = 17 ' HS_HALFTONE = 18 ' HS_SOLIDCLR = 19 ' HS_DITHEREDCLR = 20 ' HS_SOLIDTEXTCLR = 21 ' HS_DITHEREDTEXTCLR = 22 ' HS_SOLIDBKCLR = 23 ' HS_DITHEREDBKCLR = 24 ' HS_API_MAX = 25 ' End Enum 'Stock Objects Enum StockObjects As Integer WHITE_BRUSH = 0 LTGRAY_BRUSH = 1 GRAY_BRUSH = 2 DKGRAY_BRUSH = 3 BLACK_BRUSH = 4 NULL_BRUSH = 5 HOLLOW_BRUSH = NULL_BRUSH WHITE_PEN = 6 BLACK_PEN = 7 NULL_PEN = 8 OEM_FIXED_FONT = 10 ANSI_FIXED_FONT = 11 ANSI_VAR_FONT = 12 SYSTEM_FONT = 13 DEVICE_DEFAULT_FONT = 14 DEFAULT_PALETTE = 15 SYSTEM_FIXED_FONT = 16 DEFAULT_GUI_FONT = 17 DC_BRUSH = 18 DC_PEN = 19 End Enum A A A A A A horizontal hatch. 'Original Equiptment Manufacturer (OEM) dependent fixed-pitch (monospace) font. 'System font. 'Black pen. 'Black brush. ────── PS_DASH = &H1 'A pen style that is dashed.0 – David Ross Goben The GDI32 Class What follows is the GDI32 class: Option Strict On Option Explicit On '************************************************************************************** ' GDI32 . PS_INSIDEFRAME = &H6 'A pen style that is a solid color. 'Win2K/XP: Solid color brush. R2_MakePenNot = 5 'Specifies a combination of the colors are common to both the pen and the inverse of the display. R2_NotCopyPen = 4 'Specifies the inverse of CopyPen. R2_MaskNotPen = 3 'Specifies a combination of the colors are common to the background color and the inverse of the pen. 45-degree upward. R2_MergePen = 15 'Specifies a combination of the pen color and the display color. By default. R2_MaskPen = 9 'Specifies a combination of the colors common to both the pen and the display. 'Gray brush. 'White brush. -●-●-● PS_DASHDOTDOT = &H4 'A pen style that consists of dashes and double dots. 45-degree downward. Compare this with SYSTEM_FONT. ' 'taking into account the width of the pen. 'WinNT/Win2K/XP: Device-dependent font. -----PS_DOT = &H2 'A pen style that is dotted. R2_NotMergePen = 2 'Specifies the inverse of MergePen. R2_NotXOrPen = 10 'Specifies an inverse of XOrPen. 'Fixed-pitch (monospace) system font. the output remains unchanged. 'Dark gray brush. The null pen draws nothing. DLL" Alias "CreatePatternBrush" ( _ ByVal hBitmap As IntPtr) As IntPtr '----------------------------------------------------------------------------------------------------------------------------' Function: GetStockObject ' The GetStockObject function retrieves a handle to one of the stock pens.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. It will contain the previous current position Public Structure POINTAPI Dim x As Integer Dim y As Integer End Structure Page –209– . The bitmap can be a DIB section bitmap. the specified handle is no longer valid. or palettes.PS_SOLID m_penWidth As Integer = 1 Protected m_rasterOp As RasterOps = RasterOps. brush. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function CreatePen 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.DLL" Alias "GetROP2" ( _ ByVal hdc As IntPtr) As Integer 'structure used by MoveToEx. The pen can subsequently be selected into a device context ' and used to draw lines and curves. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function SelectObject Lib "gdi32. ' or palette. _ ByVal crColor As Integer) As IntPtr '----------------------------------------------------------------------------------------------------------------------------' Function: SelectObject ' The SelectObject function selects an object into the specified device context ' (DC). font. the return value specifies the previous mix mode. freeing all system resources associated with the object. the return value specifies the previous mix mode. which is created ' by the CreateDIBSection function.White m_penStyle As PenStyles = PenStyles. 'hatch style.DLL" Alias "DeleteObject" ( _ ByVal hObject As IntPtr) As IntPtr '----------------------------------------------------------------------------------------------------------------------------' Function: SetROP2 ' The SetROP2 function sets the current foreground mix mode. _ ByVal hObject As IntPtr) As IntPtr '----------------------------------------------------------------------------------------------------------------------------' Function: DeleteObject ' The DeleteObject function deletes a logical pen. After ' the object is deleted. ' If the function fails. brushes. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function CreateHatchBrush Lib "gdi32.DLL" Alias "CreateHatchBrush" ( _ ByVal Style As HatchStyle. The new object replaces the previous object of the same type.NET Beyond the Scope of Visual Basic 6. region.0 – David Ross Goben Friend Class GDI32 #Region "GDI32 Protected Fields" '***************************************************************************************************************************** ' Protected Fields '***************************************************************************************************************************** Protected m_hdc As IntPtr 'handle to drawing context Protected Protected Protected Protected Protected m_gdiPen As IntPtr m_oldPen As IntPtr m_penColor As Color = Color. and color. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function GetROP2 Lib "gdi32. the return value is zero. ' If the function succeeds. The foreground mix mode defines how colors ' from the brush or pen and the colors in the existing image are to be combined.R2_CopyPen Protected Protected Protected Protected #End Region m_gdiBrush As IntPtr m_oldBrush As IntPtr m_brushColor As Color = Color. GDI uses the ' foreground mix mode to combine pens and interiors of filled objects with ' the colors already on the screen. bitmap. ' If the function succeeds. None = use Solid Brush #Region "GDI32 P/Invoke Declarations" '***************************************************************************************************************************** ' INTEROP P/INVOKE DECLARATIONS '***************************************************************************************************************************** '----------------------------------------------------------------------------------------------------------------------------' Function: CreateSolidBrush ' The CreateSolidBrush function creates a logical brush that has the specified solid color. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function CreateSolidBrush Lib "gdi32. The mix mode specifies how the pen or interior color and ' the color already on the screen are combined to yield a new color. fonts. the return value is zero. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function CreatePatternBrush 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. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function DeleteObject Lib "gdi32. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function GetStockObject Lib "gdi32. ' width. ' If the function fails. _ ByVal crColor As Integer) As IntPtr '----------------------------------------------------------------------------------------------------------------------------' Function: CreatePatternBrush ' The CreatePatternBrush function creates a logical brush with the specified ' bitmap pattern.HS_SOLID 'handle to pen 'hold original pen 'Default drawing color 'init pen to solid 'pixel width of pen (line) 'Init raster operation to normal 'handle to new brush 'hold original brush 'default brush color (fill color). _ ByVal nWidth As Integer.DLL" Alias "SelectObject" ( _ ByVal hdc As IntPtr.Transparent m_brushHatch As HatchStyle = HatchStyle. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function SetROP2 Lib "gdi32. or it can be a device-dependent bitmap.Enhancing Visual Basic .DLL" Alias "CreatePen" ( _ ByVal nPenStyle As PenStyles. in logical coordinates. 'X: Specifies the x-coordinate. in logical coordinates. of the lower-right corner of the rectangle.DLL" Alias "Ellipse" ( _ ByVal hdc As IntPtr.0 – David Ross Goben '----------------------------------------------------------------------------------------------------------------------------' Function: MoveToEx ' The MoveToEx function updates the current position to the specified point ' and optionally returns the previous position. in logical coordinates. in logical units. The rectangle ' is outlined by using the current pen and filled by using the current brush. in logical units. the previous position is not returned. _ ByVal nRightRect As Integer. _ ByVal nBottomRect As Integer. 'nRightRect: The x-coordinate. 'lpPoint: Pointer to a POINT structure that receives the previous current position.DLL" Alias "MoveToEx" ( _ ByVal hdc As IntPtr. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function Ellipse Lib "gdi32. _ ByVal nWidth As Integer. of the lower-right corner of the rectangle. in logical coordinates.DLL" Alias "MoveToEx" ( _ ByVal hdc As IntPtr. in logical units. '----------------------------------------------------------------------------------------------------------------------------'hdc: A handle to the device context. _ ByVal x As Integer. of the new position. _ ByVal nLeftRect As Integer. in logical coordinates. of the new position. 'Y: Specifies the y-coordinate. of the upper-left corner of the rectangle. '----------------------------------------------------------------------------------------------------------------------------'hdc: Handle to a device context.DLL" Alias "Rectangle" ( _ ByVal hdc As IntPtr. _ ByVal nXEnd As Integer. _ ByVal nLeftRect As Integer. _ ByVal nTopRect As Integer. 'nTopRect: The y-coordinate. of the new position. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function RoundRect Lib "gdi32. but not ' including. in logical coordinates.DLL" Alias "RoundRect" ( _ ByVal hdc As IntPtr. _ ByVal nTopRect As Integer. in logical coordinates. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function LineTo Lib "gdi32. the specified point. The center of the ellipse is the ' center of the specified bounding rectangle. in logical units.Enhancing Visual Basic . of the upper-left corner of the rectangle. of the lower-right corner of the rectangle. in logical coordinates. _ ByVal nYEnd As Integer) As Boolean '----------------------------------------------------------------------------------------------------------------------------' Function: Ellipse ' The Ellipse function draws an ellipse. in logical coordinates. in logical units. 'nTopRect: The y-coordinate. 'nRightRect: The x-coordinate. of the upper-left corner of the bounding rectangle. 'nRightRect: The x-coordinate. in logical coordinates. 'nWidth: The width. of the ellipse used to draw the rounded corners. If this parameter is a NULL pointer. of the lower-right corner of the rectangle. 'nBottomRect: The y-coordinate. 'nBottomRect: The y-coordinate. in logical coordinates. _ ByVal nBottomRect As Integer) As Boolean '----------------------------------------------------------------------------------------------------------------------------' Function: Rectangle ' The Rectangle function draws a rectangle. 'X: Specifies the x-coordinate. of the lower-right corner of the bounding rectangle. the previous position is not returned. '----------------------------------------------------------------------------------------------------------------------------'hdc: A handle to the device context. of the line's ending point. If this parameter is a NULL pointer. of the ellipse used to draw the rounded corners. 'lpPoint: Pointer to a POINT structure that receives the previous current position. in logical coordinates. of the lower-right corner of the bounding rectangle. of the new position. in logical units. 'nXEnd: Specifies the x-coordinate. '----------------------------------------------------------------------------------------------------------------------------'hdc: Handle to a device context. in logical coordinates. '----------------------------------------------------------------------------------------------------------------------------'hdc: Handle to a device context. _ ByVal nTopRect As Integer. '----------------------------------------------------------------------------------------------------------------------------'hdc: A handle to the device context. _ ByVal lpPoint As IntPtr) As Boolean '----------------------------------------------------------------------------------------------------------------------------' Function: LineTo ' The LineTo function draws a line from the current position up to. in logical coordinates. of the line's ending point. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function MoveToEx Lib "gdi32. _ ByVal nBottomRect As Integer) As Boolean '----------------------------------------------------------------------------------------------------------------------------' Function: RoundRect ' The RoundRect function draws a rectangle with rounded corners. 'nLeftRect: The x-coordinate.NET Beyond the Scope of Visual Basic 6. The ellipse is outlined by using ' the current pen and is filled by using the current brush. 'nYEnd: Specifies the y-coordinate. _ ByVal nRightRect As Integer. in logical units. 'nLeftRect: The x-coordinate. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function MoveToEx Lib "gdi32. 'Y: Specifies the y-coordinate.DLL" Alias "LineTo" ( _ ByVal hdc As IntPtr. 'nBottomRect: The y-coordinate. _ 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. of the upper-left corner of the rectangle. _ ByVal nRightRect As Integer. in logical units. _ ByVal y As Integer. in logical units. 'nLeftRect: The x-coordinate. of the upper-left corner of the rectangle. of the upper-left corner of the bounding rectangle. _ ByVal nHeight As Integer) As Boolean Page –210– . in logical units. The rectangle is outlined by using ' the current pen and filled by using the current brush. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function Rectangle Lib "gdi32. _ ByVal x As Integer. _ ByVal nLeftRect As Integer. 'nHeight: The height. _ ByVal y As Integer. 'nTopRect: The y-coordinate. Enhancing Visual Basic . ' nYStartArc: The y-coordinate. _ ByVal nBottomRect As Integer. '----------------------------------------------------------------------------------------------------------------------------' hdc: A handle to the device context where drawing takes place. in logical units. of the ending point of the radial line defining the ending point of the arc. it is not filled. in logical units.m_penWidth End Get Set(ByVal value As Integer) Me.0 – David Ross Goben '----------------------------------------------------------------------------------------------------------------------------' Function: Arc ' The Arc function draws an elliptical arc. ' nBottomRect: The y-coordinate. _ ByVal nRightRect As Integer.m_penWidth = value End Set End Property 'Get/Set Raster Operation Public Property RasterOp() As RasterOps Get Return m_rasterOp End Get Set(ByVal value As RasterOps) m_rasterOp = value End Set End Property #End Region Page –211– . '----------------------------------------------------------------------------------------------------------------------------'The points (nLeftRect. _ ByVal nXStartArc As Integer. ' nRightRect: The x-coordinate. in logical units. nTopRect) and (nRightRect. _ ByVal nYStartArc As Integer. ' BrushHatch: Get/Set optional Brush Hatch Style. of the ending point of the radial line defining the starting point of the arc. of the lower-right corner of the bounding rectangle. in logical units. If the starting point and ' ending point are the same. of the upper-left corner of the bounding rectangle. of the ending point of the radial line defining the starting point of the arc. Default is HS_SOLID. a complete ellipse is drawn. Default is black. '----------------------------------------------------------------------------------------------------------------------------Private Declare Function Arc Lib "gdi32. ' RasterOp: Get/Set type of raster operation to perform.m_penColor End Get Set(ByVal value As Color) Me. ' nXEndArc: The x-coordinate. The arc ends where it ' intersects the radial from the center of the bounding rectangle to the (nXEndArc. An ellipse formed by the specified ' bounding rectangle defines the curve of the arc. of the upper-left corner of the bounding rectangle. _ ByVal nXEndArc As Integer.m_penStyle = value End Set End Property 'Get/Set pen width in pixels Public Property PenWidth() As Integer Get Return Me. Also be sure this is black for XOR operations and you want a "Rubberband" effect. in logical units.DLL" Alias "Arc" ( _ ByVal hdc As IntPtr. ' PenWidth: Get/Set pen pixel width. The arc extends in the current drawing direction from the point where it ' intersects the radial from the center of the bounding rectangle to the (nXStartArc. nYEndArc) point. nBottomRect) specify the bounding rectangle. _ ByVal nLeftRect As Integer. in logical units. Default is Black.m_brushColor End Get Set(ByVal value As Color) Me.m_brushHatch = value End Set End Property 'Get/Set pen color Public Property PenColor() As Color Get Return Me. ' nXStartArc: The x-coordinate. 'The arc is drawn using the current pen. in logical units.m_penColor = value End Set End Property 'Get/Set pen style (pattern) Public Property PenStyle() As PenStyles Get Return Me. ' nYEndArc: The y-coordinate. Default is R2_CopyPen (overwrite) '***************************************************************************************************************************** 'Get/Set brush color Public Property BrushColor() As Color Get Return Me. _ ByVal nYEndArc As Integer) As Boolean #End Region #Region "GDI32 Properties" '***************************************************************************************************************************** ' Properties ' BrushColor: Get/Set brush color.m_penStyle End Get Set(ByVal value As PenStyles) Me. ' PenStyle: Get/Set pen style. of the lower-right corner of the bounding rectangle. ' nLeftRect: The x-coordinate. ' PenColor: Get/Set pen color. in logical units. Default is PS_SOLID. of the ending point of the radial line defining the ending point of the arc. ' nTopRect: The y-coordinate. which specifies a solid brush.m_brushColor = value End Set End Property 'Get/Set Brush Hatch Style Public Property BrushHatch() As HatchStyle Get Return Me. nYStartArc) point. Default is 1. _ ByVal nTopRect As Integer.NET Beyond the Scope of Visual Basic 6.m_brushHatch End Get Set(ByVal value As HatchStyle) Me. Y.CreatePen(Me. ByVal p2 As Point) Me. IntPtr.DisposeResources(g) 'disposes of resources End Sub '----------------------------------------------------------------------------------------------------------------------------' Function: DrawArc ' Draw an arc within a rectangle specifyed by p1 (top-left) and p2(bottom-right).DisposeResources(g) 'disposes of resources End Sub '----------------------------------------------------------------------------------------------------------------------------' Function: DrawEllipse ' Draw ellipses defined within a bounding rectangle '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawEllipse(ByVal g As Graphics.X. ArcStart.Enhancing Visual Basic .X. ArcEnd. p1.m_brushHatch.m_penColor = Color.DisposeResources(g) 'disposes of resources End Sub '----------------------------------------------------------------------------------------------------------------------------' Function: DrawEllipse ' Draw circles and ellipses from a bounds defined by a top-left point and a bottom-right point '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawEllipse(ByVal g As Graphics.Right.ToArgb 'convert color value to AARRGGBB Return RGB((vARGB >> 16) And &HFF.m_oldPen = GDI32. .m_hdc.m_hdc. ByVal p1 As Point. delete replaced one g.SelectObject(Me. p2 is bottom-right Me.X.InitPenAndBrush(g) 'init pen and brush GDI32. ByVal p1 As Point.Ellipse(Me. ArcEnd.PenColor)) 'set pen pattern.SelectObject(Me. . Me. p1. ArcStart.SelectObject(Me.DeleteObject(GDI32. vARGB And &HFF) 'return RGB color End Function '----------------------------------------------------------------------------------------------------------------------------' Subrouine: InitPenAndBrush ' Initialize pen annd brush '----------------------------------------------------------------------------------------------------------------------------Protected Sub InitPenAndBrush(ByVal g As Graphics) Me.Left.m_brushColor)) 'set solid brush style and fill color) Else Me. style.X.m_hdc) 'release device context g.InitPenAndBrush(g) 'init pen and brush GDI32.m_oldPen)) 'reset old pen.m_hdc.m_hdc. p2.m_hdc. p1.Left. .Transparent Then Me.NET Beyond the Scope of Visual Basic 6. ByVal Bnds As Rectangle. ByVal p2 As Point.CreateHatchBrush(Me. Me.m_hdc = g.m_brushColor = Color.X. .X. ByVal Bnds As Rectangle) 'p1 is top-left of bounds.SelectObject(Me. ArcEnd is a point along end line '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawArc(ByVal g As Graphics. ArcEnd.Bottom. p2 is bottom-right Me. Point ArcStart defines a point from center along start line.m_gdiPen = GetStockObject(StockObjects.m_hdc.Y.InitPenAndBrush(g) 'init pen and brush GDI32.Zero) 'move to starting point Page –212– .Ellipse(Me. p1.X.m_hdc.MoveToEx(Me. p1. ArcEnd. ArcEnd is a point along end line '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawArc(ByVal g As Graphics.m_oldBrush = GDI32. ByVal ArcStart As Point.SetROP2(Me.PenStyle.X . Me. ByVal Radius As Integer) 'p1 is center of circle Me. and width End If GDI32. ByVal p1 As Point.InitPenAndBrush(g) 'init pen and brush GDI32.m_gdiPen) 'set new pen. ArcStart.DeleteObject(GDI32.Y) 'draw ellipse Me. .InitPenAndBrush(g) 'init pen and brush With Bnds GDI32.Bottom) 'draw ellipse within the rectangle End With Me. p1. .X. p1.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.ARGBtoRGB(Me.m_hdc. ByVal ArcEnd As Point) Me. delete replaced one GDI32. p1. ByVal p2 As Point) 'p1 is top-left of bounds.m_brushHatch = HatchStyle. ' Point ArcStart defines a point from center along start line.Radius.Y .m_gdiBrush = GetStockObject(StockObjects.m_gdiBrush = GDI32.HS_SOLID Then Me. m_rasterOp) 'set raster operation Me.m_gdiBrush) 'set new background brush.0 – David Ross Goben #Region "GDI32 Protected Support Methods" '***************************************************************************************************************************** ' Protected Support Methods '***************************************************************************************************************************** '************************************************************************************************************* ' Function: ARGBtoRGB ' Helper function to covert Alpha Color ARGB value (AARRGGBB) to RGB (00BBGGRR) '************************************************************************************************************* Private Function ARGBtoRGB(ByVal clr As Color) As Integer Dim vARGB As Integer = clr.Y + Radius) 'draw circle Me.CreateSolidBrush(Me.NULL_BRUSH) 'hide brush if brush color is transparent ElseIf Me.m_hdc. ByVal p1 As Point.Transparent Then Me.Top.Top.X + Radius.m_penWidth. Me.Dispose() End Sub #End Region #Region "GDI32 Public Methods" '***************************************************************************************************************************** ' Public Methods '***************************************************************************************************************************** '----------------------------------------------------------------------------------------------------------------------------' Function: DrawCircle ' draw a circle with a uniform radius from a center point '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawCircle(ByVal g As Graphics.Y) 'draw arc Me. Me.Y.Arc(Me. ArcStart.ARGBtoRGB(Me. p1. p2.X.Y.ReleaseHdc(Me. ByVal ArcEnd As Point) Me.m_gdiBrush = GDI32.Radius.Ellipse(Me. save old End Sub '----------------------------------------------------------------------------------------------------------------------------' Subrouine: DisposeResources ' Dispose of created data and reset old data '----------------------------------------------------------------------------------------------------------------------------Protected Sub DisposeResources(ByVal g As Graphics) GDI32. ByVal ArcStart As Point.Y) 'draw arc End With Me.Arc(Me. . (vARGB >> 8) And &HFF. save old Me.ARGBtoRGB(Me. Me.DisposeResources(g) 'disposes of resources End Sub '----------------------------------------------------------------------------------------------------------------------------' Function: DrawArc ' Draw an arc within a bounding rectangle.GetHdc 'save handle to device context 'process brush options If Me.m_hdc.NULL_PEN) 'hide pen if it is transparent Else Me.Y.Y. p2.Right.InitPenAndBrush(g) 'init pen and brush With Bnds GDI32.m_brushColor)) 'create hatch brush style End If 'process pen options If Me.m_gdiPen = GDI32.m_hdc. p2.m_oldBrush)) 'reset old brush. p1. Me. . PointsArray(0).m_hdc.Abs(. .DisposeResources(g) End Sub 'draw line from start point to end point 'disposes of resources '----------------------------------------------------------------------------------------------------------------------------' Function: DrawPolygon ' Draw a polygon from an array of points '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawPolygon(ByVal g As Graphics.. PointsArray(NumPoints).Top . CornerWidth.X.Abs(p1.0 – David Ross Goben GDI32.Y . PointsArray(Idx).. CornerRadius.Left.MoveToEx(Me.LineTo(Me.m_hdc.X .Rectangle(Me.LineTo(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. p1.Y .m_hdc..m_hdc.X.InitPenAndBrush(g) 'init pen and brush GDI32. p1.X. CornerRadius. P2 is the bottom Right corner '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawRectangle(ByVal g As Graphics. ByRef PointsArray() As Point) If PointsArray Is Nothing Then Return 'if nothing to process End If Dim NumPoints As Integer = UBound(PointsArray) 'get upper bounds of array Me.X . . .X.Y.m_hdc.p2.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 a chain GDI32.Y) 'draw rectangle from point1 to point2 Me. . CornerRadius) 'draw rounded rectangle from point1 to point2 Me.Abs(p1.RoundRect(Me. ByVal Bnds As Rectangle.X.DisposeResources(g) 'disposes of resources End If End Sub '----------------------------------------------------------------------------------------------------------------------------' Function: DrawRoundRect ' Draw a rounded rectangle (obround) from a bounding rectangle.Top. CornerRadius) 'draw rounded rectangle from point1 to point2 Me. .Bottom. p2. PointsArray(Idx).InitPenAndBrush(g) 'init pen and brush GDI32.Y) 'draw a line from previous point to current End If End If Me. CornerWidth.Top .Right) OrElse CornerRadius >= Math. p2. p1.Abs(. ByVal Bnds As Rectangle) Me.m_hdc.DisposeResources(g) 'disposes of resources End If End With End Sub '----------------------------------------------------------------------------------------------------------------------------' Function: DrawObRound ' Draw an ObRound. ByVal p2 As Point. . ByVal p1 As Point.Right. ByVal p1 As Point.Y.DisposeResources(g) 'disposes of resources End If End Sub '----------------------------------------------------------------------------------------------------------------------------' Function: DrawObRound ' Draw an ObRound from a bounding rectangle.Equals(PointsArray(NumPoints)) Then 'close polygon if start and last not the same GDI32.Rectangle(Me.m_hdc. ByVal Bnds As Rectangle. p2.NET Beyond the Scope of Visual Basic 6.Y. CornerHeight) 'draw rounded rectangle from point1 to point2 Me.InitPenAndBrush(g) 'init pen and brush GDI32.Y) 'draw a line from previous point to current Next If Not PointsArray(0). p2) 'just draw a rectangle Else Me. ByVal p2 As Point) Me. Make CornerWidth and CornerHeight equal for symetrical corners '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawRoundRect(ByVal g As Graphics.DisposeResources(g) 'disposes of resources End Sub '----------------------------------------------------------------------------------------------------------------------------' Function: DrawRoundRect ' Draw a rounded rectangle (obround).Y.Left .p2.X. p2. Make CornerWidth and CornerHeight equal for symetrical corners ' P1 is the top-left corner.Abs(.RoundRect(Me.InitPenAndBrush(g) 'init pen and brush GDI32.Right.LineTo(Me. .InitPenAndBrush(g) 'init pen and brush GDI32.Top.Bottom) 'draw rectangle within bounds End With Me. ByVal CornerRadius As Integer) 'if corner radius out of range If CornerRadius <= 0 OrElse CornerRadius >= Math.X.InitPenAndBrush(g) 'init pen and brush GDI32.RoundRect(Me.X) OrElse CornerRadius >= Math. ByVal CornerHeight As Integer) 'if corner radius out of range If CornerWidth <= 0 OrElse CornerHeight <= 0 OrElse CornerWidth >= Math. allowing one to specify a uniform corner radius by specifying just a single corner radius property ' P1 is the top-left corner.Bottom) Then DrawRectangle(g. P2 is the bottom Right corner '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawRoundRect(ByVal g As Graphics. p1.Y.Abs(p1.X. PointsArray(0). ByVal p2 As Point. p1.X. . p2) 'just draw a rectangle Else Me.Right) OrElse CornerHeight >= Math.Left.Right.Left. p2.m_hdc.p2. allowing one to specify a uniform corner radius by specifying just a single corner radius property '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawObRound(ByVal g As Graphics.Left .Y) Me. P2 is the bottom Right corner '----------------------------------------------------------------------------------------------------------------------------Public Sub DrawObRound(ByVal g As Graphics.p2. p1.Bottom) Then DrawRectangle(g.Y. p1.Bottom. p2. ByVal CornerHeight As Integer) 'if corner radius out of range With Bnds If CornerWidth <= 0 OrElse CornerHeight <= 0 OrElse CornerWidth >= Math.InitPenAndBrush(g) 'init pen and brush With Bnds GDI32.Top.RoundRect(Me. ByVal CornerRadius As Integer) 'if corner radius out of range With Bnds If CornerRadius <= 0 OrElse CornerRadius >= Math. p1. ByVal p1 As Point. p2..m_hdc. p2.Y) Then DrawRectangle(g. Bnds) 'just draw a rectangle Else Me. .X. . PointsArray(NumPoints).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.Abs(p1.Y) Then DrawRectangle(g. ByVal CornerWidth As Integer.Abs(. CornerHeight) 'draw rounded rectangle from point1 to point2 Me. . IntPtr. ByVal CornerWidth As Integer.DisposeResources(g) 'disposes of resources End If End With End Sub #End Region End Class Page –213– . Bnds) 'just draw a rectangle Else Me.m_hdc. .Enhancing Visual Basic .X) OrElse CornerHeight >= Math. Forms.Abs(m_LastPoint.R2_XOrPen 'use XOR to emulate rubberbanding m_StartPoint = e.Name & vbCrLf Next End If 'report results MsgBox(Msg. Then.Y)) End Function Page –214– . Math. where the rubber band will also cover any controls it passes over on the form).Y Then Y = m_LastPoint." & m_StartPoint. The code required to support this is very simple (I keep forcing myself to avoid the pun of saying it is very basic).OkOnly Or MsgBoxStyle. report selection data '************************************************************************************** Private Sub Form1_MouseUp(ByVal sender As Object.X 'NORMALIZE COORDINATES if inverted in any way If Y > m_LastPoint. update the location. add some controls to dress it up (unlike all other rubber band examples you may have seen. init the rectangle start and end points '************************************************************************************** Private Sub Form1_MouseDown(ByVal sender As Object. where the start point is ' always upper-left. erase the old rubberband.ToString & ". if you wish. ByVal e As MouseEventArgs) Handles Me. and the ending point is always lower-right '************************************************************************************** Private Function NormalizeSelectRectangle() As Rectangle 'define the final selection rectangle Dim X As Integer = m_StartPoint.ToString & ".ToString & ") to (" & _ m_LastPoint. Y. Also.m_StartPoint.m_StartPoint. add the following: Public Class Form1 Friend m_GDI As New GDI32 Friend m_StartPoint As Point Friend m_LastPoint As Point 'instantiate an instace of the GDI class 'keep track of starting point 'keep track of last mouse location '************************************************************************************** ' Subroutine: Form1_MouseDown ' When the mouse select button is down.X. ByVal e As MouseEventArgs) Handles Me. MsgBoxStyle. this one actually works exactly as you would expect of normal rubber band operations.MouseDown m_GDI.MouseMove If e.Generic. ByVal e As MouseEventArgs) Handles Me. if you choose to.List(Of Control) 'list to collect controls selected Dim Msg As String = "The selection rectangle was from (" & m_StartPoint.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.Information.Visible AndAlso Ctl. '************************************************************************************** Private Sub Form1_MouseMove(ByVal sender As Object.Button = Windows.0 – David Ross Goben Emulating a Selection Rubber Band under GDI Suppose you wanted to define a Selection Rubber Band on an image.Add(Ctl) End If Next If Ctls.X Then X = m_LastPoint.RasterOp = RasterOps.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.Y. in the Form1 code.X .BrushColor = Color.Abs(m_LastPoint.Y 'define a new rectangle to draw our current rectangle.Location 'set the start and end locations to the current mouse position m_LastPoint = e.X.Bounds.IntersectsWith(Rect) Then 'a control intersection was found Ctls.Y . "Selection Result") End Sub '************************************************************************************** ' Function: NormalizeSelectRectangle ' Defind the selection rectangle and normalize its definition.White 'set color to white (black will be invisible in an XOR operation) m_GDI." & m_LastPoint. Add a background image to it.X).PenColor = Color.Enhancing Visual Basic .MouseUp DrawRubberBand() 'erase the old rectangle Dim Ctls As New Collections.NET Beyond the Scope of Visual Basic 6.Y If X > m_LastPoint. such as a background image on a form. Create a new VB Windows project. Math.Left Then 'if the mouse select button is down DrawRubberBand() 'erase the old rectangle at the old location m_LastPoint = e.Location End Sub '************************************************************************************** ' Subroutine: Form1_MouseMove ' If the mouse button is down.Transparent 'hide the brush m_GDI.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. ' then draw the new rubberband. and compare against any controls on the form Return New Rectangle(X.MouseButtons. You can do this quite easily with the previousloy defined GDI32 class.Y.X 'get start point Dim Y As Integer = m_StartPoint. The NormalizeSelectRectangle() method normalizes coordinates so that no matter how you make your selection. my solution was unbelievably simple (actually.Bounds. Rect. any controls you place on the form will be in front of the rubber band. I think it can also look much less professional than the GDI method. Emulating a Selection Rubber Band under GDI+ Suppose you wanted to simply emulate a Selection Rubber Band under the graphics support that is already built into . this method draws to the screen over the top of everything. 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. this GDI+ example does not require our GDI32 class. Emulating VB6 Image Control Features in VB.Ctl. and people are constantly asking me about it.Width. which we can do using the PointToScreen() method as you will see in the new DrawRubberBand() method.NET. Unlike the previous example. New Rectangle(Rect.Height)) End If Next End Sub End Class NOTE: Unlike any other GDI example on the web.Left . the coordinate addressing will be standardized. Because it is drawing to the form’s surface. and compare against any controls on the form Dim Rect As Rectangle = NormalizeSelectRectangle() m_GDI. unless we reflect it to the surface of each control as well. below. The shaded code in the DrawRubberBand() method shows you how simple it is to actually reflect the rubber band to the surface of each control that it intersects with. Rect. it can also at times leave tiny graphical artifacts on certain controls on the form as well. using GDI+. once you have added the previously defined GDI32 class to it. taking advantage of XOR Raster method. though no real damage is caused and it cleans easily. Unlike the GDI method. Rect. As such.Left. NOTE: This method can look goofy and loses its luster if you move the selection outside the target form.NET on page 72). offsetting the rubberband as needed m_GDI.Ctl. I often see the selection rectangle disappear after about one second if you held it still.Enhancing Visual Basic . 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. I will check for this within the form code and automatically correct for that.0 – David Ross Goben '************************************************************************************** ' Subroutine: DrawRubberBand ' Draw or erase the rubberband.DrawRectangle(Me. which is useful for also conveniently covering form controls without the need for additional code. But. run the project. where it will leave graphical artifacts on the screen. But if it is rendered and handled properly. it will work great and it will look clean. Page –215– . For instance. '************************************************************************************** Private Sub DrawRubberBand() 'define a new rectangle to draw our current rectangle. though if you are not very careful in the way you write it. we must in turn translate our rectangle’s start coordinates to coordinates relative to the screen.CreateGraphics.Visible AndAlso Ctl. But regardless.DrawRectangle(Ctl. because it does draw to the whole screen. with some examples.Top .CreateGraphics. Also draw/erase ' over controls that may be placed on the form. 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. I do not like this method for this reason and consider it to be the least professional method to use. Using the GDI+ DrawReversibleFrame() Method The typical technique developers advise us to use for this task under GDI+ (and the technique I actually like the least) is to implement the ControlPaint.IntersectsWith(Rect) Then 'a control intersection was found 'erase the old rectangle or draw the new one over control. It emulates the rubber band function very smoothly. below.NET Beyond the Scope of Visual Basic 6. However. and I have seen some very bad examples on the web at some blog and support sites – but these folks are just trying to get people pointed in the right direction to solving their own problems. But we will cover it here because so many online examples feature it. Now. 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. as I will show you.DrawReversibleFrame() method. as you can see.Top. We can do that as well. ToString & ". Y.0 – David Ross Goben Like the GDI Rubber Band example.X.Height . taking advantage of XOR Raster method. and the ending point is always lower-right '************************************************************************************** Private Function NormalizeSelectRectangle() As Rectangle 'define the final selection rectangle Dim X As Integer = m_StartPoint. not Rect. '************************************************************************************** Private Sub DrawRubberBand() 'define a new rectangle to draw our current rectangle Dim Rect As Rectangle = NormalizeSelectRectangle() With Me. be sure to also set the form’s DoubleBluffered parameter to True. Also draw/erase ' over controls that may be placed on the form. ByVal e As MouseEventArgs) Handles Me.Update() 'update display of that region m_LastPoint = e.X Then X = m_LastPoint. Then.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.Y 'define a new rectangle to draw our current rectangle. '************************************************************************************** Private Sub Form1_MouseMove(ByVal sender As Object. and compare against any controls on the form Return New Rectangle(X. ByVal e As MouseEventArgs) Handles Me.Height = . And.Forms.Left + Rect.Invalidate(Rect) 'invalidate that region Me. Color.Bounds.Refresh() 'refresh form and controls. add the following: Public Class Form1 Friend m_StartPoint As Point Friend m_LastPoint As Point 'keep track of starting point 'keep track of last mouse location '************************************************************************************** ' Subroutine: Form1_MouseDown ' When the mouse select button is down.Size). otherwise the selection rectangle will be reflected to the If Rect.Y)) End Function '************************************************************************************** ' Subroutine: DrawRubberBand ' Draw or erase the rubberband. ByVal e As MouseEventArgs) Handles Me.Height Then 'screen area not covered by the form.MouseUp Me.ClientRectangle If Rect.Width += 1 'bump 1 for width/height because drawing edge is one higher than rectangle Rect. FrameStyle.Location).Y Then Y = m_LastPoint.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.Abs(m_LastPoint.Left Then 'if the mouse select button is down Dim Rect As Rectangle = NormalizeSelectRectangle() 'get current rectangle Rect.Location 'set the start and end locations to the current mouse position m_LastPoint = e.White. which is actually drown to the screen.MouseMove If e.MouseDown m_StartPoint = e.Y < 0 Then m_LastPoint. Math.m_StartPoint. Rect.Visible AndAlso Ctl. Also add some controls to dress it up if you choose. End If End With 'draw the selection rectangle to the actual screen. MsgBoxStyle.Generic. where the start point is ' always upper-left.Width > .IntersectsWith(Rect) Then 'a control intersection was found Ctls.Rect.Information.Dashed) End Sub End Class Page –216– .MouseButtons. in the Form1 code.Width Then 'make sure selection. not to the form and its controls.Abs(m_LastPoint.OkOnly Or MsgBoxStyle.ToString & ".Left + 1 'to the form.Y.X .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. ControlPaint. Add a background image to it if you wish. will not exceed the bounds of the form's client End If 'area. These two checks will eliminate that problem.m_StartPoint.Height += 1 Me.Y.Add(Ctl) End If Next If Ctls.Y If m_LastPoint.Button = Windows. and will look very Rect. erase the old rubberband.X = -1 'prevent selection from goung outside top-left of frame If m_LastPoint.Y = -1 If X > m_LastPoint.Width = .Width . "Selection Result") End Sub '************************************************************************************** ' Function: NormalizeSelectRectangle ' Defind the selection rectangle and normalize its definition. report selection data '************************************************************************************** Private Sub Form1_MouseUp(ByVal sender As Object.X.DrawReversibleFrame(New Rectangle(PointToScreen(Rect.Name & vbCrLf Next End If 'report results MsgBox(Msg. Math.X 'NORMALIZE COORDINATES if inverted in any way If Y > m_LastPoint.Height > .X).Location End Sub '************************************************************************************** ' Subroutine: Form1_MouseMove ' If the mouse button is down. ' then draw the new rubberband.Rect.Top + Rect.Y .ToString & ") to (" & _ m_LastPoint. update the location. to avoid window flashing.NET Beyond the Scope of Visual Basic 6. erasing rectangles Dim Ctls As New Collections." & m_LastPoint.X 'get start point Dim Y As Integer = m_StartPoint.Top + 1 'unprofessional." & m_StartPoint.Enhancing Visual Basic . init the rectangle start and end points '************************************************************************************** Private Sub Form1_MouseDown(ByVal sender As Object.X < 0 Then m_LastPoint. create a new VB Windows project.List(Of Control) 'list to collect controls selected Dim Msg As String = "The selection rectangle was from (" & m_StartPoint. most people seem to be at a loss as to how to do that with an alpha blended color value.CreateGraphics(). Creating a translucent brush simply involves assigning a color value to a brush. fully opaque].NET. 0. while drawing is going on. even if not affected (otherwise artifacts may be left behind) If Ctl.Navy. Rect.CreateGraphics.FromArgb(0. That is. the . Red=0.NET’s SolidBrush() method while studying brush creation on MSDN.Left. because it was now so stable. and Update() methods could be used to make the GDI+ DrawReversibleFrame() method appear more professional-looking.0 – David Ross Goben Using the GDI+ DrawRectangle() Method and a Translucent Brush If you want to emulate the selection rectangle that Windows Vista and later uses. In fact. and compare against any controls on the form Dim Rect As Rectangle = NormalizeSelectRectangle() DrawBand(Me. most examples of the SolidBrush() method specify just a single color parameter and leave it at that.NET Beyond the Scope of Visual Basic 6. Refresh(). 80) = Color.Navy = Color. Rect) 'draw selection rectangle for form For Each Ctl As Control In Controls 'also process any controls it intersects with If Ctl. I fully understood how one was defined within a 32-bit integer block as ARGB. Rect.Width. Color. 0. With this property set to True. I could have simply used the GDI+ DrawRectangle() method to emulate the previous examples. 80) = Color.Refresh() 'always refresh control surface. especially with the form’s DoubleBluffered property set to True. Also draw over controls that may be placed on the form. we can create a brush with a ½ transparent Light Blue color using the following: Dim Brsh As New SolidBrush(Color.R. Green. replace the DrawRubberBand() method in the previous example with the following two methods: '************************************************************************************** ' Subroutine: DrawRubberBand ' Draw the rubberband. 0. and no one seemed to have ready answers on the internet – only questions about how to create one. However. I felt that I had to figure it out. Rect.FromArgb(128. New Rectangle(Rect. NOTE: Setting the DoubleBuffered property for a form to True is important to cut down on screen flicker.LightBlue)) 'define translucent brush with 50% translucency To implement it. where the 0-255 alpha-channel member determined the color’s opacity or transparency.Ctl.FromArgb(64.Top . will be minimized. until I happened upon an obscure example for using . Blue=80) an alpha blend value of 64 (1/4 opaque. Green=0.B) the color Navy ([Alpha=255.FromName("Navy") = RGB(0.Enhancing Visual Basic .Left . Rect) 'do fill with translucient brush (we can also define the new brush here) Hence.FromArgb(Color. and flicker.NET support system will wait until it is between display refresh cycles to update the screen so that display updates. I was not able to invent this method until I figured out how to make the rectangle for the DrawReversibleFrame() method more stable. Blue) the integer value of Navy an alpha blend value of 128 (1/2 opaque) for the color Green (A. Worse. '************************************************************************************** Private Sub DrawRubberBand() 'define a new rectangle to draw our current rectangle.Navy) 'assign 'assign 'assign 'assign 'Assign 'Assign 'Assign Clr Clr Clr Clr Clr Clr Clr variable variable variable variable variable variable variable the color Navy the color Navy the color Navy (Red.Visible Then 'if control can be seen Ctl. The trick. 0. but being at the time new to VB. The only real problem I was running into was finding out how to create a translucent brush (actually a no-brainer). Color.Ctl. 80) = Color.ToArgb) = Color.G. though.FromArgb(128. offsetting the rubberband as needed DrawBand(Ctl. this is also very easy to do. Only then did I begin to wonder about how Vista and Windows 7/8 made their translucent selection rectangle. we need only fill a rectangle definition with the brush: Me.IntersectsWith(Rect) Then 'a control intersection was found 'erase the old rectangle or draw the new one over control. Granted.Top. The color value is one of the common ARGB colors that are typical to the WinXP+ operating systems. Unfortunately. instead of updating the screen as the user draws. 3/4 transparent) for the color Navy With this knowlege. Granted.FillRectangle(Brsh.Height)) End If End If Next End Sub Page –217– . once I discovered that the Invalidate(). I was not yet aware of the language features that supported its actual construction. is to realize that a color can be multiply defined. Consider the following examples: Dim Clr Clr Clr Clr Clr Clr Clr As Color = Color.CreateGraphics.Bounds. Windows 8. because I thought that this translucent selection rectangle was introduced with Windows 6.FromArgb(255. By the way. and Windows 8 is actually Windows 6.FillRectangle(New SolidBrush(Color. NOTE: Some people have reported that they have this type of selection rectangle under Windows XP. and Windows 10 is actually (finally) Windows 10 – there goes the Microsoft marketing department.Enhancing Visual Basic .1 is actually Windows 6.1.0 (Vista.0 – David Ross Goben '************************************************************************************** ' Subroutine: DrawBand ' Draw using a Vista/Win7-style selection rubberband '************************************************************************************** Private Sub DrawBand(ByVal eg As System. Rect) 'do fill with translucient brush (Alpha=64: 1/4 opaque.NET Beyond the Scope of Visual Basic 6. or have an option set that I have disabled.Graphics. Rect) 'draw outer edge of rectangle eg.2.3. VistaClr)). Page –218– .Drawing. 153. 3/4 transparent) End Sub You now have Vista-style rubber band selection. 51. 255) 'use Vista+ Selector color (Alpha=255: fully opaque) eg.DrawRectangle(New Pen(VistaClr). Perhaps they have an enhancement installed. again). Windows 7 is actually Windows 6.FromArgb(64. My XP platform uses just a rectangle outline. ByVal Rect As Rectangle) Dim VistaClr As Color = Color. Collection. Demonstrating Control Extension Using the VB. All these enhancements expand them into much more potent and much more useful tools. yielding wholly new and more powerful controls. if you wanted a ComboBox to display its drop down list through code alone. _ ByVal lParam As Any) As Long Private Const CB_SHOWDROPDOWN = &H14F 'Message code used to display or hide the dropdown list of a ComboBox 'Set Cmd to 1 to ensure the Dropdown is displayed. these newer objects also feature a generous reportoir of built-in functions – something that VB6 controls totally lacked. each message is encapsulated within a bundle that includes a unique identifier for the target object the message is specifically directed at. you had to use a P/Invoke to do so. 1).Enhancing Visual Basic . Messages. such as ShowCboDropDown(Me. not just simple strings. and that they now all sport a uniform Items collection. The unique identifier of the target object is often referred to as a Window Handle. 2) you can transmit a meaningful Win32 message to the control to extend it. with the exception being that you may have noticed that the ComboBox.0 – David Ross Goben Extending VB. loaded not only with the properties that you may have become accustomed to. we could issue a command.NET ComboBox (and ListBox) For example. Combo1. When you begin using these controls under VB. to ensure When we moved on to VB. NET. but also with a generous plethora of new properties. especially if this functionality was critical to the operation of our application. Under VB6. Each message is an integer flag value that is associated with a specific purpose. CB_SHOWDROPDOWN. NOTE: All form controls are actually individual windows that simply identify a form (yet another window) as their parent. so that we would not lose this feature’s functionality.NET they might first seem to simply exhibit properties that were part of their VB6 predecessors. was displayed.NET versions of these objects are even more profoundly different. that is understood by its target object. essentially. 0&) End Sub With the above code. _ ByVal wMsg As Long. you have to make one of three choices: 1) you can write your own code to extend it. reflecting how they were referred to in the C++ developer community.NET Control Functionality Although VB.NET simply refers them by the more meaningful term of Handle. that the dropdown list for the ComboBox control. and are in turn processed by a Window Processor function (a WndProc in programmer lingo). under VB6.NET Beyond the Scope of Visual Basic 6. the truth is that these seemingly familiar objects have been totally redesigned and their capabilities extended far beyond their provincial VB6 cousins. 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. Cmd. But under deeper scrutiny you are going to discover that the VB. are instructions that are added to an application’s message queue.hwnd. Optional ByVal Cmd As Long = 1) Call SendMessage(hWnd. or to 0 to ensure it is hidden Public Sub ShowCboDropDown(ByVal hWnd As Long.NET seems to incorporate most of the VB6 controls that you may have grown so accustomed to. Page –219– . or 3) you can mix your code and Win32 messages to perform a marriage of tasks to extend it. If you wanted to do anything beyond what these properties offered. plus two additional values that can contain or point to any ancillary data. _ ByVal wParam As Long. Consider the VB6 properties offered for the ComboBox control. which clearly describes exactly what they are. If you want to extend the functionality of a control. such as a task. Further. Further. you were forced to extend it using system Messages through the Win32 API. as shown to the right.Combo1. Consider the following VB6 method that can be used to do just that: Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hWnd As Long. This was pretty much all the power that the VB6 interface granted you. and ListBox now store objects as data. but . they were called hWnd. 85). NET ComboBox properties. Hence.com/en-us/library/system. used. but we must recognize that a Control under VB. each of which we can individually explore by clicking on them. under that extensive list of properties. And as you wade through it.combobox.NET (I constantly rediscover this).NET Beyond the Scope of Visual Basic 6. The SendMessage function invokes the window procedure ' for the specified window and does not return until the window procedure has processed the message. _ ByVal lParam As Integer) As Boolean ' The CB_SHOWDROPDOWN message ' The wParam parameter is set ' The lParam parameter is not Private Const CB_SHOWDROPDOWN will tell a ComboBox to either show or hide its dropdown list.microsoft. Being mindful of that. as an object.Handle. For example. If you set it to False. we will find a list of 39 entries. Were you to set this Boolean property to True. then the dropdown list for the ComboBox will immediately display. _ ByVal wMsg As Integer. Optional ByVal Cmd As Integer = 1) As Boolean If TypeOf sender Is ComboBox Then 'ensure the object is a ComboBox Return SendMessage(DirectCast(sender. and Cmd=0 will hide it. encompassing methods. CB_SHOWDROPDOWN. In apparent counterpoint. has an extensive battery of additional features that are only available for runtime operation that cannot be set through properties. such as Me. It lists the following Win32 messages (we will examine highlighted entries in detail): CB_ADDSTRING = &H143 CB_DELETESTRING = &H144 CB_DIR = &H145 CB_FINDSTRING = &H14C CB_FINDSTRINGEXACT = &H158 CB_GETCOMBOBOXINFO = &H164 CB_GETCOUNT = &H146 CB_GETCUEBANNER = &H1704 CB_GETCURSEL = &H147 CB_GETDROPPEDCONTROLRECT = &H152 CB_GETDROPPEDSTATE = &H157 CB_GETDROPPEDWIDTH = &H15F CB_GETEDITSEL = &H140 CB_GETEXTENDEDUI = &H156 CB_GETHORIZONTALEXTENT = &H15D CB_GETITEMDATA = &H150 CB_GETITEMHEIGHT = &H154 CB_GETLBTEXT = &H148 CB_GETLBTEXTLEN = &H149 CB_GETLOCALE = &H15A Page –220– CB_GETMINVISIBLE = &H1702 CB_GETTOPINDEX = &H15B CB_INITSTORAGE = &H161 CB_INSERTSTRING = &H14A CB_LIMITTEXT = &H141 CB_RESETCONTENT = &H14B CB_SELECTSTRING = &H14D CB_SETCUEBANNER = &H1703 CB_SETCURSEL = &H14E CB_SETDROPPEDWIDTH = &H160 CB_SETEDITSEL = &H142 CB_SETEXTENDEDUI = &H155 CB_SETHORIZONTALEXTENT = &H15E CB_SETITEMDATA = &H151 CB_SETITEMHEIGHT = &H153 CB_SETLOCALE = &H159 CB_SETMINVISIBLE = &H1701 CB_SETTOPINDEX = &H15C CB_SHOWDROPDOWN = &H14F . ' Cmd=1 to show the list.windows. were we to examine the reported VB. to 1 to ensure the listbox is shown. or still do perform P/Invokes for can now be processed through simple properties or methods under VB.ComboBox1. As Integer = &H14F '*************************************************************** ' ShowCboDropDown(): Force the listbox of a ComboBox to be displayed or hidden through software. You can also check this property.aspx). 0 will ensure that it is hidden. Win32 ComboBox Messages By now I may have perked your interest. as shown to the right. if you simply want to discover if its current state is opened or closed. you will find a runtime-only property named DroppedDown. were you to scan the list under ComboBox Class. _ ByVal wParam As Integer. 0) 'tell the list to be shown (Cmd=1) or hidden (Cmd=0) End If Return False 'failure End Function Yet. consider the following VB. NET fully-functional rendering of the above VB6 code: ' Sends the specified message to a window or windows. properties.NET. if not shown.forms.aspx). you would find that this huge ComboBox member catalog is almost overwhelming. the above ShowCboDropDown() method can now be removed and replaced by merely issuing a simple command. '*************************************************************** Public Function ShowCboDropDown(ByVal sender As Object.DroppedDown = True. that it has a superfluity of built-in methods that are designed to support the control.NET ComboBox. we will discover that most P/Invokes we had depended so heavily upon under VB6 is no longer required. you will discover that most-all things that you may have previously had to. such as you will find on MSDN (see http://msdn. instead. Cmd. the dropdown list will be removed. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr. if it is not already hidden.Enhancing Visual Basic . we might not see anything that might preclude the need for the above code. ComboBox).microsoft. and events.com/enus/library/ff485901(VS. You may be wondering: what is this mysterious list of control message incantations David has been babbling about? If we again visit MSDN and look at ComboBox Control Messages (see http://msdn. once we explore the rich list of features for the VB.0 – David Ross Goben For example. or files. _ ByVal wParam As Long. ' PS_DRIVES includes drives in the list.. The names must match a specified mask string and a set of file attribute flags. as specified in the SpecTypes Enumerated list.NET) The CB_DIR message adds names. _ ByVal wMsg As Long. ' where X is the drive letter. Obj. such as drives. almost all of these messages are supported either through properties or methods.hwnd.. ' PS_EXCLUSIVE Only files with the specified attributes are put in the list. even if PS_NORMAL is not ' specified.0 – David Ross Goben Of all these many messages. under VB. the CB_DIR message worked great. ' PS_ARCHIVE Files with the archive flag set are included (most useful if used ' with PS_EXCLUSIVE. 1) = "\" Then S = S & "*. Huh? It was time to put on my thinking cap… Page –221– . with optional ' attributes. Conversely. ' PS_DIRECTORY includes subdirectories in the list. included with PS_DRIVES. S) + 1 ElseIf TypeOf obj Is ListBox Then 'if ListBox.hwnd.bas: Option Explicit 'Fill ComboBox or ListBox with directory list of drives. dirs. This resulted from the fact that the Items collection within the ComboBox was still empty. ' PS_SYSTEM Include system files in the list.*" 'all if nothing If Right$(S. telling me that I had selected an invalid index. CB_SETEXTENDEDUI. only a few are not needed under VB6 due to built-in VB6 functionality. For example.The CBLBDirFill() function will fill a ComboBox or a ListBox ' control with a list of Drives. As expected. ' SpecTypes List: ' PS_NORMAL Default. Drives are listed in the form "-X-". By ' default. only subdirectories ' will be in the list. Subdirectories are enclosed ' in square brackets ([]).. directories. we will find CB_DIR. Included with PS_DIRECTORY. CB_GETEXTENDEDUI. Under VB6.NET Beyond the Scope of Visual Basic 6. But I quickly discovered that it did only half the job. The dropdown list even properly displayed when dropped down. Consider the following tried and true VB6 code that had served me well for many years.Enhancing Visual Basic . and files to the dropdown list of a ComboBox.Clear 'clear listBox CBLBDirFill = SendMessageStr(obj.Clear 'clear ComboBox CBLBDirFill = SendMessageStr(obj. an exception error would result. directories. Obj.Clear 'make fresh list ' I = CBLBDirFill(MyListBox. the supplied object was not a ' ComboBox or ListBox. I tested it. ByVal SpecType As SpecTypes) As Integer Dim S As String CBLBDirFill = -2 'init result to fail S = Trim$(PathSpec) 'grab pathspec If Len(S) = 0 Then S = "*. All files that are read/write are included in the list. ByVal PathSpec As String.NET. The CB_DIR Message (and how to make it work under VB. stored under the module filename modCBLBDirFill. files '****************************************************************************** ' modCBLBDirFill . ' PS_HIDDEN Hidden files are included in the list. BasePath & "\*. Most of them have no direct VB6 interface. then only drives ' will be in the list. CB_SETTOPINDEX. Were you to afterward click on any entry within that dropdown list.NET.*" 'allow basepathspec If TypeOf obj Is ComboBox Then 'if ComboBox. Of those that are not. otherwise not of much use). then there ' was not enough space to fill the list. CB_GETTOPINDEX. S) + 1 End If End Function After I upgraded it to VB. CB_DIR. If -1. If -2. normal files are also listed. ' otherwise not of much use). ' PS_READONLY Readonly files are included (most useful if used with PS_EXCLUSIVE. 'EXAMPLE: ' Dim I As Integer ' Dim BasePath As String ' BasePath = "C:\Program Files" 'base path to scan ' MyListBox.. At first it seemed to work. SpecType. ' ' NOTE: The function returns the number of items inserted in the list.*". it duly filled the dropdown list. _ ByVal lParam As String) As Long Private Const CB_DIR As Long = &H145 Private Const LB_DIR As Long = &H18D Public Enum SpecTypes PS_ARCHIVE = &H20 PS_DIRECTORY = &H10 PS_DRIVES = &H4000 PS_EXCLUSIVE = &H8000& PS_HIDDEN = &H2 PS_READONLY = &H1 PS_NORMAL = &H0 PS_SYSTEM = &H4 End Enum Public Function CBLBDirFill(ByVal obj As Object. SpecType. and CB_SETCURSEL. PS_NORMAL Or PS_DIRECTORY) ' 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 SendMessageStr Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As Long. LB_DIR. and FileListBox controls were not carried directly over into VB. LB_DIR. DirListBox. So much so. but it appears that Microsoft felt that. Under VB. even under VB. S) + 1 'NOTE: THIS FILLS THE DROPDOWN LIST. Under VB6. SpecType. Even though its dropdown list object will still store a list of strings. perhaps because they thought it so little-used.xxxItems. S) + 1 'NOTE: THIS FILLS THE DISPLAY LIST. it would require more than just a bit of code. which makes it tremendously more powerful and extensible. the CB_DIR message only fills the dropdown list with a list of strings. however. ByVal SpecType As DirSpecTypes) As Integer Dim pSpec As String = Trim(PathSpec) 'grab pathspec If Len(pSpec) = 0 Then pSpec = "*. but rather a boatload of those bits.Items.NET.NET Beyond the Scope of Visual Basic 6. Page –222– . being wholly ignorant of . consider the following code portion of my original VB. the Items list directly accessed the string entries stored within the dropdown list itself.NET translation of the working VB6 code: Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr. just like VB6. as was quite apparent to me (this dropdown text list.0 – David Ross Goben First. BUT NOT THE ITEMS COLLECTION Case "ListBox" Dim Lst As ListBox = DirectCast(obj. In the end.Handle.NET objects. and FileListBox back into the VB. The reason for this change is that under VB. with just a bit of code. In this case. add *.NET the Items list is now a collection of objects. they could argue that other methods are available that perform similar jobs.NET Toolbox). BUT NOT THE ITEMS COLLECTION Case Else Return -2 'failure.GetType.Clear lines. ByVal PathSpec As String. as specified in the DirSpecTypes Enumerated list. directories ' or files. 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.Enhancing Visual Basic . 1) = "\" Then pSpec &= "*. the Items list is now completely separate.* Select Case obj.NET.NET featuring very powerful file and folder browsers. '****************************************************************************** Public Function CBLBDirFill(ByVal obj As Object. mainly due to VB. The dropdown list for a ComboBox. ListBox) Lst. _ ByVal wMsg As Integer. DirListBox. no longer being a property mapped directly to the dropdown list. CB_DIR. ComboBox) Cbo. NOTE: This is why it is important for the objects stored within the a ComboBox or ListBox to have a meaningful ToString() method.*" 'all if nothing If Right(pSpec.Clear() 'clear the ComboBox Return SendMessage(Cbo. but it completely ignores the Items collection because it was never designed to do that.NET ComboBox are handled. even the VB6 DriveListBox. 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. so that the string versions of the objects can be represented as you would expect within their displayed lists. SpecType.Handle.Items. is still the same type of list used by the VB6 control). Change to an APPEND function by removing the .Clear() 'clear the ListBox Return SendMessage(Lst. Besides. _ ByVal wParam As Integer. but it is now a disconnected collection.*" 'if it ends in a blackslash. the developer could sufficiently emulate any needed functionality. ' Enumerated list. _ ByVal lParam As String) As Integer Private Const CB_DIR As Integer = &H145 'dir functions for a ComboBox Private Const LB_DIR As Integer = &H18D 'dir functions for a ListBox Public Enum DirSpecTypes As Integer PS_NORMAL = &H0 PS_READONLY = &H1 PS_HIDDEN = &H2 PS_SYSTEM = &H4 PS_DIRECTORY = &H10 PS_ARCHIVE = &H20 PS_DRIVES = &H4000 PS_EXCLUSIVE = &H8000& End Enum 'all normal files 'is locked against alteration 'hidden (file or directory) 'system type files or directories 'include directory type entries 'archive flag set (has been altered since last backup) 'include drive type entries 'display files that are exclusively of a selected non-PS_NORMAL types '****************************************************************************** ' CBLBDirFill(): Fills a ComboBox or a ListBox control a list of Drives. but it is now relegated the mundane task of storing the text representations of the Items collection objects.NET.Name 'check type of control Case "ComboBox" Dim Cbo As ComboBox = DirectCast(obj. even though there was still plenty of potential application for each of these controls (at the end of this article I will show you how to add the DriveListBox. rather than being the main storage medium as it was under VB6. still stores a list of strings. Microsoft chose not to provide support for CB_DIR-type functionality under . There is still practical use for the CB_DIR message when developers want system lists. Hence. with optional attributes.NET. this functionality has changed. not ComboBox or ListBox End Select End Function The reason for the seeming paradox of being able to fill the dropdown list with viewable strings and yet the Items collection still being empty is all due to the ways that a VB6 and a VB. the supplied object was not a ' ComboBox or ListBox. 'all normal files 'is locked against alteration 'hidden (file or directory) 'system type files or directories 'include directory type entries 'archive flag set (has been altered since last backup) 'include drive type entries 'display files that are exclusively of a selected non-PS_NORMAL types '****************************************************************************** ' CBLBDirFill(): Fills a ComboBox or a ListBox control a list of Drives. then there ' was not enough space to fill the list. and with that we can assign those entries to the Items collection. then only drives will be in the list. If -1. ' otherwise not of much use).NET result using the Win32 CB_GETLBTEXT message.Handle. otherwise not of much use).Handle. ' normal files are listed. ByVal SpecType As DirSpecTypes) As Integer Dim pSpec As String = Trim(PathSpec) 'grab pathspec If Len(pSpec) = 0 Then pSpec = "*. not from the Items collection. 'EXAMPLE: ' Dim BasePath As String = "C:\Program Files" 'base path to scan ' MyListBox.*" 'if it ends in a blackslash.Items.Clear() 'clear the ComboBox Dim Cnt As Integer = SendMessage(Cbo. dir's. ' PS_ARCHIVE Files with the archive flag set are included (most useful if used ' with PS_EXCLUSIVE. or files. vbNullString)) 'set aside space for an entry SendMessage(Cbo.* Select Case obj. If -2. in characters.The CBLBDirFill() function will fill a ComboBox or a ListBox ' control with a list of Drives. 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. CB_DIR.Enhancing Visual Basic . Because the dropdown list will contain a directory list after a CB_DIR message.Items. in characters. ' DirSpecTypes List: ' PS_NORMAL Default. because it will extract entries from the dropdown list. files '****************************************************************************** ' modCBLBDirFill . ByVal PathSpec As String. the partial result obtained after we pass the CB_DIR message to a ComboBox under VB. For example. By default. of a string from the index of a ListBox. ' PS_HIDDEN Hidden files are included in the list. ' PS_DRIVES includes drives in the list. ' PS_READONLY Readonly files are included (most useful if used with PS_EXCLUSIVE. pSpec) 'fill dropdown list with listing (return ubound index) Page –223– . 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.NET is fully salvageable if we add just 11 lines of code. LB_DIR. SendMessage(Cbo. '****************************************************************************** Public Function CBLBDirFill(ByVal obj As Object.Items. ' PS_SYSTEM Include system files in the list. Drives are listed in the form "-X-". this will still be significantly less code than that which would be required if we were to resort to emulating the full CB_DIR functionality. 'gets a string from the index of a ListBox. ' ' NOTE: The function returns the number of items inserted in the list.NET version. with optional attributes. CB_GETLBTEXTLEN. pSpec) 'fill dropdown list with listing (return ubound index) Dim Ary(Cnt) As String 'set aside storage for strings For Idx As Integer = 0 To Cnt 'gather each entry in the dropdown list Dim Tmp As New String(" "c. of a string in the list of a ComboBox. ' PS_DIRECTORY includes subdirectories in the list. as specified in the DirSpecTypes Enumerated list. 1) = "\" Then pSpec &= "*.Items. 'gets the length. Consider the following VB. Included ' with PS_DIRECTORY. BasePath & "\*. ComboBox) 'set aside a ComboBox reference pointer Cbo. _ ByVal wParam As Integer. only subdirectories will be in the list.Clear() 'clear the ListBox Dim Cnt As Integer = SendMessage(Lst.GetType. as specified in the DirSpecTypes Enumerated list. ' where X is the drive letter.Handle. ' Enumerated list.NET Beyond the Scope of Visual Basic 6. SpecType. _ ByVal lParam As String) As Integer Private Private Private Private Private Private Const Const Const Const Const Const CB_DIR As Integer = &H145 LB_DIR As Integer = &H18D CB_GETLBTEXT As Integer = &H148 CB_GETLBTEXTLEN As Integer = &H149 LB_GETTEXT As Integer = &H189 LB_GETTEXTLEN As Integer = &H18A Public Enum DirSpecTypes As Integer PS_NORMAL = &H0 PS_READONLY = &H1 PS_HIDDEN = &H2 PS_SYSTEM = &H4 PS_DIRECTORY = &H10 PS_ARCHIVE = &H20 PS_DRIVES = &H4000 PS_EXCLUSIVE = &H8000& End Enum 'dir functions for a ComboBox (must acceess its attached ListBox) 'dir functions for a ListBox 'gets a string from the list of a ComboBox. Idx. 'gets the length. directories ' or files.Name 'check type of control Case "ComboBox" Dim Cbo As ComboBox = DirectCast(obj. CB_GETLBTEXT. _ ByVal wMsg As Integer. ' PS_EXCLUSIVE Only files with the specified attributes are put in the list. ListBox) 'set aside a ListBox reference pointer Lst.0 – David Ross Goben However.Clear() 'clear the ComboBox dropdown list For idx As Integer = 0 To Cnt 'now fill both the items collection and dropdown list Cbo.Items. All files that are read/write are included in the list.Clear 'make fresh list ' Dim I As Integer = CBLBDirFill(MyListBox.*". add *. Tmp) 'gather the entry Ary(Idx) = Tmp 'add to the array Next Cbo.Add(Ary(idx)) 'stuff each entry Next Return Cnt + 1 'return the count of items added (add 1 for zero offset) Case "ListBox" Dim Lst As ListBox = DirectCast(obj. SpecType. Idx. even if PS_NORMAL is not specified. Subdirectories are enclosed ' in square brackets ([]). Even so. ' included with PS_DRIVES.*" 'all if nothing If Right(pSpec. with optional ' attributes. directories.Handle. we can augment this partial . 0) 'get the item that is the top-displayed item End If Return -1 'error End Function End Module NOTE: Because most examples also demonstrate their usage with a VB. which are used to set or get the index of the item that is displayed at the top of the dropdown list. '*************************************************************** 'The CB_SETTOPINDEX message is used to ensure that a particular item is visible in the list box of a ComboBox. If the message is ' successful. so the LB_SETTOPINDEX and LB_GETTOPINDEX messages are not needed. Index. The ComboBox messages used to establish these two features are CB_SETTOPINDEX and CB_GETTOPINDEX. Option Strict On Option Explicit On Module modCBLBTopIndex '*************************************************************** ' modCBLBTopIndex . CB_SETTOPINDEX.ComboBox1) '*************************************************************** Public Function CboGetTopIndex(ByVal sender As Object) As Integer If sender. _ ByVal wMsg As Integer.Handle. ' It will be displayed as the top item in the list.Handle. If the message is successful. SendMessage(Lst. the return value is CB_ERR (-1). ' another item may be at the top.. especially in a list that must implement a vertical scroll bar. The lParam parameter is not ' used. we would want to place that selected item as the topmost displayed item in the list or at least near the top if it is physically very near the top and where several above it are therefore also forced to be displayed. not ComboBox or ListBox End Select End Function End Module The CB_SETTOPINDEX and CB_GETTOPINDEX Messages Sometimes it is important to ensure that a certain entry in a dropdown ComboBox list is visible. If the message fails.Set or Get the index of the item that is ' visible at the top of a partial ComboBox or ListBox list. Page –224– . Idx. unless scrolling and list limits from ' being displayed at the top of the list.Name = "ComboBox" Then 'allow only ComboBox controls Return SendMessage(DirectCast(sender.NET does not include them as a GET/SET property item for the ComboBox class. _ ByVal lParam As Integer) As Integer '*************************************************************** ' CboSetTopIndex(): Ensure that a particular item is visible in the list box of a ComboBox. _ ByVal wParam As Integer. LB_GETTEXTLEN. vbNullString)) 'set aside space for an entry SendMessage(Lst. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr. 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. If the message fails. though I wish that it did.0 – David Ross Goben Dim Ary(Cnt) As String 'set aside storage for strings For Idx As Integer = 0 To Cnt 'gather each entry in the dropdown list Dim Tmp As New String(" "c.Add(Ary(idx)) 'stuff each entry Next Return Cnt + 1 'return the count of items added (add 1 for zero offset) Case Else Return -2 'failure.Handle. 0. ' 'EXAMPLE: ' CboSetTopIndex(Me. ComboBox) If Index > -1 AndAlso Index < cbo. 25) '*************************************************************** Public Function CboSetTopIndex(ByVal sender As Object. Parameters wParam and lParam are not used and must be set to zero.NET ListBox.Clear() 'clear the ListBox display list For idx As Integer = 0 To Cnt 'now fill both the items collection and dropdown list Lst.ComboBox1. LB_GETTEXT. 0) Return True End If End If Return False End Function As Boolean 'allow only ComboBox controls 'get the sender as a ComboBox control 'if the index is within the list bounds. They are so easy to use and implement that I find it odd that . Any other value is the index of the item displayed at the ' top of the list. ComboBox). The SendMessage function calls the window procedure for the specified ' window and does not return until the window procedure has processed the message.Name = "ComboBox" Then Dim cbo As ComboBox = DirectCast(sender. CB_GETTOPINDEX. The ComboBox DOES NOT have an exposed TopIndex property. but if the list box contents have been scrolled.GetType. the return value is zero. Private Const CB_GETTOPINDEX As Integer = &H15B ' Sends the specified message to a window or windows.Items. The wParam parameter specifies the zero-based index of the list item.NET Beyond the Scope of Visual Basic 6. ' A return value of -1 indicates failure. Idx. At other times..Count Then SendMessage(cbo.Items. though it will be visible. Tmp) 'gather the entry Ary(Idx) = Tmp 'add to the array Next Lst. we need to know what is currently displayed as the top item in the ComboBox dropdown. the item with index 0 is at the top of the list box.GetType. Initially. ' 'EXAMPLE: ' Dim Index as Integer = CboGetTopIndex(Me. ' 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.Enhancing Visual Basic . Typically. 'try to set the item as the top-displayed item 'error '*************************************************************** ' CboGetTopIndex(): Get the line index of the item that is at the top of the current displayed list. Optional ByVal Index As Integer = 0) If sender. I will point out that the ListBox control has a TopIndex property that already performs the above tasks.Handle. the return value is the index of the first visible item in the list box of the ComboBox.Items. By default. Private Const CB_SETEXTENDEDUI As Integer = &H155 ' Determines whether a ComboBox has the default user interface or the extended user interface. _ ByRef 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. Private Const CB_GETEXTENDEDUI As Integer = &H156 Public Sub SetExtendedComboDrop(ByVal sender As Object. By default. The wParam and the lParam ' are not used and must be zero.the ExtendedComboDrop() will tell a combobox to drop when the ' user hits the F4 key (default) or the Down-Arrow. as shown in this module: Option Strict On Option Explicit On Module modExtendedComboDrop 'Toggle Combobox to drop with F4 or DownArrow '*************************************************************************************** ' modExtendedComboDrop .GetType. The lParam parameter is not used.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. CB_SETEXTENDEDUI. However. the return value is CB_OKAY (0). go for the Down Arrow key. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr. ComboBox).Name = "ComboBox" Then Return SendMessage(DirectCast(sender. as if by instinct. it is CB_ERR (-1). then the extended UI is set for this ComboBox. If an error occurs. we can hit the F4 key to force its dropdown list to open without having to click the Dropdown button on the control. the F4 key is ' disabled and the DOWN ARROW key opens the drop-down list. which normally scrolls through the items in ' the list. has no effect when the extended UI is set. For example: If GetExtendedComboDrop(Me. 0) End If End Sub Public Function GetExtendedComboDrop(ByVal sender As Object) As Boolean If sender.GetType. ' 'If Result is FALSE. _ ByVal wMsg As Integer. assign the result of GetExtendedComboDrop(Me. Page –225– . If the ComboBox has the extended user interface. ComboBox).NET Beyond the Scope of Visual Basic 6. To reset it to where the F4 key (the default) is used to drop its list. this is easy to do using the CB_SETEXTENDEDUI message (ComboBox Set Extended User Interface).ComboBox1. should use the Down Arrow key to drop its list when the control has focus.ComboBox1) to a Boolean variable. the F4 key opens or closes ' the list and the DOWN ARROW changes the current selection. The SendMessage function calls the window procedure for the specified ' window and does not return until the window procedure has processed the message. False). _ ByVal wParam As Integer. issue the command SetExtendedComboDrop(Me.Handle. then the extended UI is NOT set for this ComboBox. If the operation ' succeeds. In a ' ComboBox with the extended user interface. the F4 key opens or closes the list and the DOWN ARROW changes the current selection.0 – David Ross Goben The CB_SETEXTENDEDUI and CB_GETEXTENDEDUI Messages Normally. DropWithDownArrow. such as ComboBox1. ' it is FALSE. If you would prefer that your users be able to open the dropdown list using the Down Arrow instead of the F4 key.Enhancing Visual Basic . 0. otherwise it is not. when a ComboBox has focus. Optional ByVal DropWithDownArrow As Boolean = False) If sender. _ ByRef lParam As Integer) As Integer Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr. _ ByVal wParam As Boolean. The mouse wheel. 'EXAMPLES: ' SetExtendedComboDrop(Me. issue the command SetExtendedComboDrop(Me. If you want to check the state of the Extended User Interface of a specific ComboBox. otherwise. issue a command like this: ' Dim Result As Boolean = GetExtendedComboDrop(Me. then the Extended User Interface is applied to this control. and they will typically.ComboBox1.Name = "ComboBox" Then SendMessage(DirectCast(sender. to tell the system that the specified ComboBox control. _ ByVal wMsg As Integer. The wParam parameter contains a boolean value that specifies whether ' the ComboBox uses the extended UI (TRUE) or the default UI (FALSE). the return value is TRUE.ComboBox1) ' 'If Result is TRUE. '*************************************************************************************** ' Sends the specified message to a window or windows. it is very easy to use and implement. In the extended UI. such as ComboBox1. If the result is True. False) 'force F4 for drop (default mode) ' 'To get the current extended user interface status of the ComboBox.ComboBox1. CB_GETEXTENDEDUI. Like the other messages. 0) End If End Function End Module As shown in the above examples. this is not always something easy for users to remember. the F4 key is disabled and pressing the DOWN ARROW key opens the drop-down list. True).ComboBox1.Handle. True) 'force dropping with DownArrow instead of F4 ' SetExtendedComboDrop(Me. Handle. it is LB_ERR (-1). if any. the return value is CB_ERR(-1) and the selection is cleared. which might be due to an out of range value. Sometimes. The lParam parameter is not ' used. NewIndex. Page –226– SelectedIndexChanged() . If no item is selected. in the list box ' of a ListBox. the return value is LB_ERR even ' though no error occurred. and FALSE if it ' does not. If an error occurs.The SetListIndex() function Set the ListIndex of a ListBox ' or ComboBox without triggering a click event. Under both VB6 and VB. use this command: SetListIndex(Me. The wParam parameter specifies the zero-based index of the string to select. The return value is the zero-based index ' of the currently selected item. The wParam and lParam parameters are not used and must be zero. To implement the CB_SETCURSEL (or LB_SETCURSEL) message is as easy as all the other messages that we have implemented so far. and any previous ' selection in the list is removed. If wParam is greater than the ' number of items in the list or if wParam is –1. _ ByVal wParam As Integer. The text in the edit control of the combo box changes to reflect the new selection. which is what ' will normally happen when the control's listindex is set to ' anything but -1. Enter the CB_SETCURSEL message (or the LB_SETCURSEL message for a ListBox). If the wParam parameter is –1. in the list box ' of a ComboBox.Name 'check which object to process Case "ComboBox" Dim Cbo As ComboBox = DirectCast(Obj. If necessary. the list scrolls ' the string into view.0 – David Ross Goben The CB_SETCURSEL Message If you set the Index property of a ComboBox or ListBox under VB6. any current selection in the list is removed and the edit control is cleared.NET control. CB_GETCURSEL (or LB_GETCURSEL for a ListBox). The wParam and lParam parameters are not used and must be zero. if any. Private Const CB_SETCURSEL As Integer = &H14E ' An application sends a CB_GETCURSEL message to retrieve the index of the currently selected item. and so is not normally required because the control already directly supports it. ListBox) 'set aside ListBox reference SendMessage(Lst. The SendMessage function calls the window procedure for the specified ' window and does not return until the window procedure has processed the message. The lParam parameter is not ' used. 0)) 'return true if it succeeded Case "ListBox" Dim Lst As ListBox = DirectCast(Obj.NET Beyond the Scope of Visual Basic 6. CB_GETCURSEL. it will trigger a Changed() event for that control. is the same as reading the Index property of a VB6 ComboBox or ListBox. CB_SETCURSEL. If this ' parameter is –1. _ ByVal wMsg As Integer. 0. the list scrolls ' the string into view.Enhancing Visual Basic .GetType. LB_SETCURSEL. Private Const LB_GETCURSEL As Integer = &H188 Public Function SetListIndex(ByVal Obj As Object. If this ' parameter is –1. 4). you might not want the reactive event to fire. which will in fact allow you to do just that – to change the selection index without triggering a SelectedIndexChanged() event.Handle. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr. you simply want to change the selection index and keep on processing. If no item is selected. it is CB_ERR (-1). however. Private Const CB_GETCURSEL As Integer = &H147 ' An application sends a LB_SETCURSEL message to select a string in the list of a ListBox. it will trigger a SelectedIndexChanged() event for that control.NET. The wParam parameter specifies the zero-based index of the string to select. Its companion message. If you set the SelectedIndex or SelectedItem property on a ComboBox or ListBox under VB. NOTE: Use this message only with single-selection ListBoxes. If necessary. to set ComboBox1 to a SelectionIndex of 4 without triggering a event. '******************************************************************************* ' Sends the specified message to a window or windows. 0) 'set it 'set the desired index Return (NewIndex = SendMessage(Lst. If the message is successful.NET. any current selection in the list is removed and the edit control is cleared. _ ByRef lParam As Integer) As Integer ' An application sends a CB_SETCURSEL message to select a string in the list of a ComboBox. You cannot use it to set or remove ' a selection in a multiple-selection list box. ComboBox) 'set aside ComboBox reference SendMessage(Cbo. 0)) 'return true if it succeeded End Select Return False 'fail if not ComboBox or ListBox End Function End Module For example. Consider this module: Option Strict On Option Explicit On Module modSetListIndex 'Set the ListIndex of a ListBox or ComboBox without triggering a click event '******************************************************************************* ' modSetListIndex . The return value is the zero-based index ' of the currently selected item. NewIndex. you cannot normally do this. which directly corresponds to the VB6 Changed() event. and any previous ' selection in the list is removed. This function returns TRUE if the control ' afterwards reflects the desired listindex.ComboBox1. The text in the edit control of the combo box changes to reflect the new selection. LB_GETCURSEL. the return value is the index of the item selected.Handle. ByRef NewIndex As Integer) As Boolean Select Case Obj. the return value is LB_ERR(-1). 0) 'set it 'set the desired index Return (NewIndex = SendMessage(Cbo. 0. Private Const LB_SETCURSEL As Integer = &H186 ' An application sends a LB_GETCURSEL message to retrieve the index of the currently selected item. or the SelectedIndex property of a VB.Handle. Handle. and if any text within that field would be displayed to the right of it. and to use that Handle to adjust the left margin of this edit field rightward so that any text within the ComboBox will not be hidden behind the CheckBox. we need to neatly place the CheckBox within the TextBox in such a way as to make it look like it is a part of the ComboBox control (2 pixels from the left and vertically centered within the ComboBox looks good).CheckBox1. we can quickly find it by specifying the parent handle. because there are Win32 P/Invokes that allow us to easily set the text margins within an edit control.BackColor = Me. we can optionally supply the window name as an additional parameter.CheckBox1. returning the handle of the found control.Text = Nothing). "EDIT". To place a CheckBox on the TextBox within the ComboBox is the really easy part.NET . the edit control embedded within the ComboBox has a class name of “Edit”.Enhancing Visual Basic .Zero.ComboBox1. Something like this would be extremely useful in an options list: when the user selects an option from the dropdown list. We supply it along with the handle of the control we want to modify. one must admit that a CheckBox actually embedded within the text field. and we need to ensure that the CheckBox is displayed in front of the ComboBox (Me.Height).NET Beyond the Scope of Visual Basic 6. we need to size the CheckBox to its minimum dimensions (Me.CheckBox1. The FindWindowEx() P/Invoke 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).BringToFront()). An example of this was in adding icons to the dropdown list entries. the CheckBox would be automatically checked. Using a handle of zero (null) will start from the beginning of this ‘list’. Although the typical solution to this problem is to just place a CheckBox with no Text property data to the immediate left of the ComboBox. especially on long lists where.CheckBox1. if the user decided against using one of the selected options. Another often-requested alteration is adding a CheckBox to the edit field of a ComboBox. P/Invokes are needed to obtain the Window Handle of the edit field control that is embedded within the ComboBox. If we are looking for a particular class item with a specific window name. If EC_LEFTMARGIN is selected. Further. However. The EM_SETMARGINS message (&H3) is a general message used with edit and rich text controls. plus another P/Invoke that will allow us to retrieve the handle of the embedded edit control within a ComboBox so that we can change its margins. NOTE: The CheckBox is still its own entity. This also looks much better than having an entry simply named “None”. that we should start at the start of the list. and it is also more readily accessible. they had to scroll it up or down to locate the “None” entry. Fortunately this is also a simple thing to do. We need to set the Text property of the CheckBox to blank (Me. is a very cool idea. and we will accept any window matching that class name by not supplying a window name. on page 191. we need to set its back color to the same as the ComboBox (Me. Adding Icons to ListBoxes and ComboBoxes with Ease under VB. For example. Child windows are ordered as in a list. The wParam parameter contains a constant value of EC_LEFTMARGIN (&H1) and/or EC_RIGHTMARGIN (&H2). but the user can uncheck it to clearly indicate that they have decided against that field of options. vbNullString).0 – David Ross Goben Adding a CheckBox to the Edit Field of a ComboBox Sometime we need to alter the way in which the ComboBox is displayed.CheckBox1. because a ComboBox has only one Edit control. using a command like this: Dim lhWnd As IntPtr = FindWindowEx(Me. the ability to add a CheckBox to the ComboBox turns out to be a quick and straightforward thing to do.Width = Me.BackColor). All classes have a name. IntPtr. We can scan for like-named objects using an IntPtr value to indicate which handle to scan after. as had been demonstrated in an earlier article. and you would write code for it just as you would for any other CheckBox. the lower 16-bits of the 32-bit integer lParam parameter will be set to the left margin to set to Page –227– .ComboBox1. that the class name is “EDIT”. 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. anyway). NOTE: Like VB6. Since we want to display the text to the right of the location where we are placing the CheckBox. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr.NET features a CheckListBox control.SizeDiff) 'set bounds of CheckBox within ComboBox chkThis. though it should never fail to get a handle chkThis.ComboBox1). ' You can send this message to either an edit control or a rich edit control.Height) \ 2 + 1 'compute top and buttom offsets for CheckBox within ComboBox chkThis. _ ByVal lpszWindow As String) As IntPtr '*************************************************************** ' AddCheckToCombo(): Adds a checkbox to the combobox text field.DropDownWidth = 100. Tests for the ' checkbox are performed as normal.Height . If EC_RIGHTMARGIN is selected.SetBounds(cboThis. The message redraws the control to reflect the new margins. 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.chkThis. ByRef cboThis As ComboBox) Dim lhWnd As IntPtr = FindWindowEx(cboThis. though clearly. if we want to set ComboBox1’s width to 100 pixels. Because we are only concerned with the left margin. cboThis. Private Const EM_SETMARGINS As Integer = &HD3 'Sets the widths of the left and right margins for an edit control. Hence.Handle. Fortunately. ' 'EXAMPLE: Add check1 checkbox to combobox Combo1 ' AddCheckToCombo(Me. ' at development time.BackColor = cboThis. _ ByVal lParam As Integer) As Integer ' Retrieves a handle to a window whose class name and window name match the specified strings. beginning with the one following the specified child window.Left + 3. The SendMessage function calls the window procedure for the specified ' window and does not return until the window procedure has processed the message.ComboBox1) '*************************************************************** Public Sub AddCheckToCombo(ByRef chkThis As CheckBox. but then find that its width does not match long entries in its contents? It would be nice to be able to adjust the width of the dropdown list based upon its contents. setting the left margin to CheckBox. Just place a CheckBox and a ComboBox control on a form.Text = Nothing 'remove any text from the CheckBox chkThis. VB. how do you determine the longest member.BackColor 'match background colors chkThis. ' You can send this message to either an edit control or a rich edit control. we would use a command like this: Me.Height . Me.ComboBox1.Top + SizeDiff. such as sizing its width to the width of its longest member. 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. However. similar to how I demonstrated owner-drawn ComboBox lines earlier in this document. _ ByVal lpszClass As String. _ ByVal wParam As Integer.NET Beyond the Scope of Visual Basic 6.CheckBox1. _ ByVal wMsg As Integer.0 – David Ross Goben the text control in pixels. and how do you determine the pixel width of that text? Page –228– . and the ComboBox was named ComboBox1.Zero. the checkbox can be placed anywhere. But. IntPtr. and its caption is erased. Provided a combobox and a checkbox. the VB. Hence. we can add CheckBox1 to ComboBox1 by issuing the command: AddCheckToCombo(Me. chkThis. Note that the width of the checkbox is set to its height.Width + 2 looks good. Some people prefer adding 3. cboThis. _ ByVal hwndChildAfter As IntPtr. and can be done using owner-drawn ComboBoxes. Private Const EC_LEFTMARGIN As Integer = &H1 '--------------------------------------------------------------' INTEROP P/Invokes '--------------------------------------------------------------' Sends the specified message to a window or windows.BringToFront() 'be sure CheckBox displays in front of ComboBox Dim lMargin As Integer = chkThis.CheckBox1. this is easy enough to do (lParam >> 16).Width = chkThis. Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" ( _ ByVal hWndParent As IntPtr.Width. "EDIT". EC_LEFTMARGIN. vbNullString) 'get the handle of the textbox within the ComboBox If CBool(lhWnd) Then 'safety net. so adding a CheckBox feature for a ListBox is not needed. This property replaces the CB_SETDROPPEDWIDTH and LB_SETDROPPEDWIDTH Win32 Messages. This function does not perform a case-sensitive search. but to me this adds too much of a gap (even though a single pixel normally appears to be next to nothing. lMargin) 'set left margin of ComboBox text to the right of the CheckBox End If End Sub To use the AddCheckToCombo() method is easy. we will not have to worry about shifting any values 16 bits. Me. adding checkboxes to each entry in a ComboBox would be a neat treat.NET ComboBox now sports a DropDownWidth property where we can check and set the width of the dropdown’s width in pixels.Height 'match width of CheckBox to height Dim SizeDiff As Integer = (cboThis. Sizing the ComboBox DropDown List Width How many times have you added entries to a ComboBox list. The function searches child ' windows. EM_SETMARGINS. The message redraws the control to reflect the new margins. If the CheckBox was named CheckBox1.Enhancing Visual Basic . on page 191.Width + 2 'compute space for CheckBox and 2-pixel buffer SendMessage(lhWnd. ToString. which would be important if you also used the ' AddCheckToCombo() function.Graphics = Me. and the font to be used to calculate the resulting dimensions. the MeasureString() method requires two parameters: the text string to measure.ToSize.DropDownWidth = lWidth End Sub 'Set the drop down width To use this method.Enhancing Visual Basic . which measures a specified string using a specified Font to determine the width and height of the text. which will adjust a specified ComboBox to the width of its widest data: '*************************************************************** ' CboDropDownWidthFromContents(): Compute maximum combobox dropdown field width based upon width of its list. which is an integer version of SizeF. simply supply it with a ComboBox to adjust the dropdown list width for. because it simply looks too goofy to me.Font).ComboBox1. _ Optional ByVal lMaxWidth As Integer = -1.CheckBox. I will not set a ComboBox dropdown list width to less than the width of the ComboBox itself. to allow for the added checkbox.Bounds.NET Beyond the Scope of Visual Basic 6. If not provided. ' 'EXAMPLE: Do not exceed form's width.Width ' ComboBox looks a bit strange if dropdown is narrower than the ComboBox Dim tmpRECT As New RECT 'structure used to check text dimensions For i As Integer = 0 To cboThis.Drawing. Add width of checkbox ' CboDropDownWidthFromContents(Me. For example: CboDropDownWidthFromContents(Me.Width + 8 'compute pixel width of text + buffer If txtWidth > lWidth Then lWidth = txtWidth 'update lWidth to wider value End If Next i lWidth += AddPixels 'add additional pixels for user-defined buffering if requested 'Don't allow width to exceed specified max width.Items(i). Me.ComboBox1.Font).Width) '*************************************************************** Public Sub CboDropDownWidthFromContents(ByRef cboThis As ComboBox. or. NOTE: Personally. As you can see. The task of determining the width of text has traditionally been determined in VB6 by filling a hidden auto-sizing Label’s text property to the text after setting the Label’s Font property to that of the target ComboBox.CreateGraphics 'cache graphics interface to ComboBox Dim lWidth As Integer = cboThis.0 – David Ross Goben That is also easy to do.Items.MeasureString(TextString. if we were to use a Graphics object that is associated to the ComboBox. Me. 'associate a Graphics object to ComboBox1 Next.Graphics = cboThis.Drawing. the ' width of the screen is the limit.Width.PrimaryScreen. ' An optional AddTwips parameter allows you to add an additional width value to the width being computed.1 'Loop through each ComboBox list item & get its width. cboThis. Consider the following method.Width. so I always establish the minimum width to be that of the ComboBox. we can create a Graphics object associated to ComboBox1 using a simple command like the following: Dim cboG As System. The Size object contains 2 properties: Width and Height. storing the largest: Dim txtWidth As Integer = cboG.Count . 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 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. Me. MeasureString() As shown in several examples earlier in this document. NOTE: A ListBox would be adjusted through its Width property.CreateGraphics. all without actually writing any text data to any control. However. if not specified.Width . and then examining the label’s updated width. and assign the with to the dropdown. _ Optional ByVal AddPixels As Integer = 0) Dim cboG As System.ComboBox1. We in turn use the SizeF’s ToSize() method to covert the SizeF result to a Size structure. Note that the width will not be assigned below the width of the combobox control's width.ToSize. An optional lMaxWidth value allows you to set a maximum limit. Page –229– .MeasureString(cboThis. We simply use the Width property to obtain the integer width of the text.ComboBox1). the width of the screen: If lMaxWidth <= 0 Then lMaxWidth = Screen. we can use its method.16 If lWidth > lMaxWidth Then lWidth = lMaxWidth cboThis. it would be nice to always display all items. plus top and bottom window border Dim ScHeight As Integer = Screen.X + 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 you add data to the ComboBox for pristine visual effect.1) \ cbo.Location) 'get screen coordinate for ComboBox location Dim CboBtm As Integer = CboTL. However.ItemHeight) * cbo. which established the minimum number of data items (lines) to display in the dropdown list. we must multiply the number of items we want displayed times the ItemHeight property of the ComboBox.nHeight < 0 Then 'list will go below screen or above? cbo.ComboBox1) 'set ComboBox dropdown list height to max contents NOTE: A ListBox would be adjusted through its Height property.ItemHeight) + 2 'compute full list height. and we had to resort to using the CB_SETMINVISIBLE message.ItemHeight . by default it length is computed to expose up to a maximum of 30 data items. but what if the list would scroll off the screen? And how would we calculate it to determine its dimensioning relationship to its position on the screen? We would also have to check to see if we can invert its location to display it above the ComboBox.DropDownHeight = ((ScHeight . if necessary.0 – David Ross Goben Sizing the ComboBox DropDown List Height When the dropdown list of a ComboBox is displayed.X . which will display the maximum amount of data possible: '*************************************************************** ' CboSetDropDownToMax(): Set a ComboBox dropdown list to display the maximum items possible. Fortunately.PointToScreen(cbo.Name = "ComboBox" Then 'allow only ComboBox controls Dim cbo As ComboBox = DirectCast(sender. Consider the following method. It also has a ListBox control with a class name of LISTBOX.Items.PrimaryScreen. Because the DropDownHeight property sets and returns its value in pixels. it can get just a little more complicated. so shorten the list to fit Else cbo. ' This must be done prior to displaying the dropdown list after adding items to it. Page –230– .CboBtm + cbo.GetType.ComboBox1) '*************************************************************** Public Sub CboSetDropDownToMax(ByVal sender As Object) If sender.Bounds.Height 'get screen height in pixels If CboBtm + nHeight > ScHeight AndAlso CboTL. You can change this using the ComboBox’s DropDownHeight property. to set the dropdown list height to the maximum possible for the current contents of ComboBox1. if we simply want to display the maximum number of entries possible in a dropdown list.Height 'get the screen location of the bottom of ComboBox Dim nHeight As Integer = (cbo. ' 'EXAMPLE: ' CboSetDropDownToMax(Me.FindForm.NET Beyond the Scope of Visual Basic 6.ItemHeight 'yes. ComboBox) 'get the sender as a ComboBox control Dim CboTL As Point = cbo. this is all easy to calculate. NOTE: I told you earlier that the edit control for a ComboBox had and edit comtrol named EDIT. which we picked up ing the FindWindowex() P/Invoke.Enhancing Visual Basic .Count * cbo. This method is easy to use. For example. For example. use the following command: CboSetDropDownToMax(Me. This property did not exist under VB6. ByVal e As System. Include Read-Only files in the list that match the specified pattern. then list only ReadOnly files. or System properties are also set to True.SelectedIndexChanged Dim Path As String = Me. or System properties are also set to True. if it exists.Path = Path 'reset directory. unless the Hidden. I typically also check its Click() event.Enhancing Visual Basic . shortly.NET For those familiar with VB6. you can set the Path property of a DirListBox control.*” to list all files. then list only Archive files. When you make a new selection from the list. Hidden. you can set the Path property of a FileListBox control.DirListBox1. assuming I have all three controls on a form. in which case it will display only files that match all specified properties. ByVal e As System. Property Path Pattern Normal Archive Hidden ReadOnly System Description Specifies the directory path to list files from. Include System files in the list that match the specified pattern. Normal. ":") 'scan for colon (we will ignore the volume name.DirListBox1.0 – David Ross Goben Adding the DriveListBox. which was reset when drive reset End Try Me. ReadOnly. for example. and System. a Pattern property.FileListBox1. I use code similar to the following. it also contains a Path property. or ReadOnly properties are also set to True. for example. so set path with trailing backslash Try Me.DirListBox1. then list only Hidden files. The DriveListBox is a modified version of a ComboBox. I). it triggers a SelectedIndexChanged() event for the control. ByVal e As System.DirListBox1. to update its display. Pattern mask for files. They are supported under VB. unless the Archive.Drive 'get selected drive path Dim I As Integer = InStr(Path. If the Normal property is not set to True.Drive = Path 'reset drive (extracted from path) Me. If the Normal property is not set to True.Path = Me. In addition to the typical ListBox properties.Path = Me. or System properties are also set to True.Object. Hidden. DirListBox.Path = Path 'set the file path End If End Sub The DirListBox is a modified version of a TreeView control that lists the current directory path in tree form. much as on the above displayed form: Private Sub DriveListBox1_SelectedIndexChanged(ByVal sender As System.DirListIndex) End Sub Private Sub DirListBox1_SelectedIndexChanged(ByVal sender As System. Include Archive files in the list that match the specified pattern. DirListBox. Here. in which case it will display only files that match all specified properties.Path 'update the file list to the new path selected in the directory list End Sub The FileListBox is a modified ListBox that has some properties hidden and other special properties added. in which case it will display only files that match all specified properties.Path 'save current directory path Me.txt” to list only text files.FileListBox1.EventArgs) Handles DirListBox1. Include Hidden files in the list that match the specified pattern. triggers a SelectedIndexChanged() event for the control. It also features a Path property that. Here. and the Boolean properties Archive. though you will not find them listed in your toolbox.DirListBox1. and FileListBox. It also features a Drive property that returns the string name of the currently selected drive. unless the Archive.Click Me. But. Page –231– . “*. after a brief necessary dissertation on these controls. that follows this) If I <> 0 Then 'found it? Path = Path. ReadOnly.EventArgs) Handles DriveListBox1.DirListBox1.Substring(0. I will show you how to quickly do that. and FileListBox Controls to VB. ReadOnly. which contained an embedded imagelist and performs owner-drawing so that drive icons will be displayed within its edit box and dropdown list. there were three useful controls named DriveListBox. If the Normal property is not set to True.DriveListBox1. and so in all I add the following.Object.NET Beyond the Scope of Visual Basic 6.EventArgs) Handles DirListBox1. when set.DriveListBox1. then list only System files.TrimEnd & "\" 'yes.SelectedIndexChanged Me.DirListBox1. unless the Archive. Include all (non-hidden) files in the list that match the specified pattern. in which case it will display only files that match all specified properties. For example.NET.Message) 'report error Path = Me.Path = Path 'set the directory path Catch ex As Exception MsgBox(ex. or “*. Typically. to update its display. Hidden. If the Normal property is not set to True.DirList(Me. 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. VisualBasic. but that does not mean in any way that we cannot use them.NET Framework Components tab. from the . select the OK button to lock in the changes. select the toolbox collection where you want to add them. and just like you did under VB6. Page –232– . NOTE: These three controls are part of the Microsoft. and add a check to their checkboxes as well. and FileListBox to your toolbox is easy. Add them to forms as you need them.Comptability namespace.Enhancing Visual Basic . Once you have all three items checked in the list. and then FileListBox. such as Common Controls. scroll down until you locate DirListBox under the Name heading and place a check in the checkbox next to it. Once the Choose ToolBox Items dialog window finally comes up. DirListBox. and use them just like you would any other form control. Right-click it and select the Choose Items… option. You will now see the three new controls listed at the bottom of your selected toolbox group.0 – David Ross Goben To add the DriveListBox. and they were primarily meant to be used to support upgraded VB6 applications. Then find DriveListBox.NET Beyond the Scope of Visual Basic 6. With any form up and selected in the IDE. who has developed any sort of VB. SMTP is the most common internet standard for sending email. many developers want to add them to other VB. as I will be showing you.NET technology.NET However.DLL. . Suddenly. is as of yet not a part of . you should not expect to see the VB. but none of them provide robust solutions).1 and Windows 10. before diving into these VB. albeit his solution was just a simple example with very limited capability. Microsoft’s MAPI (Messaging Application Program Interface). When a VB6 MAPI application is upgraded to VB. Simple Mail Transfer Protocol).NET projects so they can take advantage of them there. I had already put the finishing touches on my own full-featured POP3 Inbound Email solution. Their control sources had been copied locally and referenced. but they cannot seem to find a way to easily access the new DLLs from their newer projects.MSMAPI32. another project-local non-COM DLL named AxInterop.NET Toolbox sport controls such as VB6’s MAPI-based MAPISession or MAPMessage. Having found these controls on their upgraded applications. like Windows 8/8. The MAPIMessage control was used to process email messages.NET Brays whisper distantly beneath midnight mists as spent developers. such as Outlook or Mail.NET does not provide the MAPI controls they thought it did because Microsoft Windows defaulted to it.NET. Although it is easy to write VB.NET was over that antiquated VB6 the client revered. NOTE: MAPI is a Microsoft protocol allowing a MS email client to utilize all the features of an Exchange server.DLL is internally compiled by . Being COM-based. this does not remove the need for an inbound class in more controlled environments. even though these technologies are both simple TCP Clients (Transmission Control Protocol).NET code to demonstrate this ability. and this after having assured their skeptical client of how superior VB. because both VB6 controls actually accessed this DLL provider through the MSMAPI32. Another soul-torn howl trebles against the muffled parapets of the valley. let us first take a look at the kind support that is presently available to developers who had upgraded VB6 MAPI applications to VB.MSMAPI32. though a fundamental part of their email Input/Output in their Windows Operating Systems. Under . but it lacks POP3 support for inbound email.0 – David Ross Goben Send (SMTP) and Retrieve (POP3) Email with Ease under VB. and most other COM applications that accesses email.MSMAPI32. They realize too late that their strategy for the client’s email handler is unworkable: VB. they crash into a brick wall.NET. used by ASP (Active Server Page).OCX ActiveX interface.NET solutions. Yet. and he works at Microsoft. Additionally. but it requires numerous coding hacks. by the time I found his article within the catacombs of MSDN. in cringed horror. rasp desperate prayers against an ominous sense of impending doom. both incoming (POP3. is an absolute impossibility).NET is set up for SMTP outbound email. and most noticed in Windows Mail. IIS (Internet Information Server). a copy of the COM-based MSMAPI32. It is doable. Presently. Even so. Post Office Protocol – Version 3) and outgoing (SMTP. according to Murphy’s Law of Looming Deadlines. POP is the most common Internet standard for receiving email. Page –233– . pinned by looming deadlines and their brain’s threat of total collapse. it is still COM (Common Object Model).Enhancing Visual Basic . you may notice the application will still sport a VB6 MAPISession and MAPIMessage control on forms that had them before. Adding the VB6 MAPISession and MAPIMessage Controls to VB.NET.NET that will duplicate both the ActiveX visual interface construction services for the controls and the function mapping services to the new Interop.NET code to support POP3 Inbound Email services.NET Beyond the Scope of Visual Basic 6. Plain and simple.DLL is converted into a non-COM version (its DLLRegisterServer() entry is disabled) and saved to a project-local file named Interop. I have found only one other person (I have since found more. I think it may have something to do with many people wanting to read email using “eye-candy” apps. working at a fever’s pitch to hammer out viable code (which. it used the MAPISession control to (what else?) manage a MAPI session.DLL. Under the VB6 implementation of MAPI. Mail just to avoid typing “Mail. With any form up on the Visual Studio screen so that the IDE toolbox is active. then right-click that tab. 3. such as COM. and read email. But with this article. and without a need to add the more resource-hungry form controls as we had to do with VB6. NOTE: If you do not find these entries in the Choose Toolbox Items dialog box.” later in their code. then you may not or no longer have the VB6 redistributables on your system.0 – David Ross Goben But relax.Net. You should also install the Microsoft Visual Basic 6. and Microsoft MAPI Sessions Control. Were that it could be so easy.MSMAPI32. so you will have to minimally install the free Runtime Distribution Pack for Service Pack 6 for Visual Basic 6. we will first look at sending email out. you will be able to make your clients think you made it so. when we download email. This class library provides the . 5.microsoft. because I thought back in those younger and smarter years that it was all simple child’s play. such as to one named COM. In a pinch we launched a TelNet client and manually typed the various commands to log on to an email server. and finally disconnect. You are allowed to do this even if you no longer own VB6. receive. it was a work of art. and you can begin using these controls just exactly as you would had been using them under VB6.com/downloads/details. which then drills down to Interop.0. Version 6.DLL. Version 6. 2. NOTE: MIME (or Mime) is an anagram for Multipurpose Internet Mail Extensions. Back in the “old days” of software engineering.NET. but they will now both link to a new . Both of these are actually linked to MSMAPI32.Sockets namespace.NET using Native Methods VB. press ENTER. 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 you. but we will also need access to the System.MSMAPI32.Text 'Most the code in this article REQUIRES this Imports line! NOTE: Some people import System. This “socket” simply described the endpoint of a bidirectional inter-process communication flow across an Internet Protocol-based network.NET Beyond the Scope of Visual Basic 6. System. Those were cryptic and unforgiving days.NET has its own Outbound SMTP Email class that supports sending email. Click the OK button. Select the Choose Items… option. then type the name of your category.Net Namespace.Mime namespace. Page –234– . But looking back to those times.NET Toolbox and access them directly? 1.DLL.Enhancing Visual Basic .0 Service Pack 6 Cumulative Update to include the latest tweaks (www.Net. send. depending on the platform.aspx?FamilyId=7B9BA261-7A9C43E7-9117-F673077FFB3C&displaylang=en). I will ask you to add this line: Imports System.displaylang=en&id=7030). which in turn drilled down to MAPI32. right-click a toolbox category you want to add the MAPI controls to. Scroll down and put checkmarks in the boxes for Microsoft MAPI Messages Control.Net. we processed email through a thing called a Berkeley Socket (circa 1983). PART ONE Sending Email under VB. If you want to add them to their own category. 4. I have to wonder if I was either a brilliant genius or a major drool-monkey. the System. in the heading of your form or module. we have built-in tools to do all the hard stuff. such as the System. Once the Choose Toolbox Items dialog is finally displayed – select the COM Components tab.aspx?amp. and wait for the IDE to build a massive control reference list from the computer. It was sometimes a real trick to write code for. say the early 1990s.com/download/en/details. Why not just add these two VB6 controls to your VB.NET SMTP Outbound Mail class. but when it functioned correctly. available from Microsoft (www.0. Quick and Dirty Email Senders Nowadays.DLL. and even later.0. above the class declaration of the file you want to implement it in.OCX. To use it. Because this technique is more accessible than supporting inbound email under VB.NET-compiled axInterop.NET.microsoft. and you will find these two controls now in your selected Toolbox category list. right-click any category and select the Add Tab option. com.Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.Send(strFrom. "Bubba Dingus" <bob. Yahoo.dingus@cox. HotMail. The SMTP Host is the address of your email provider’s SMTP server.com".SmtpClient(smtpHost) 'create new SMTP client using TCP Port 25 smtpEmail. etc.com ' strSubject: Brief text regarding what the email concerns. David Dingus <daviddingus@att. If this field and SSLUsername ' : are blank.. then the mail object will surround the data with angle brackets. strBody) 'send email End Sub NOTE: The FROM and TO email addresses can be simple.net ' strTo : Full email address of who to send the email to. _ ByVal strSubject As String. The above method actually works for most SMTP servers. intranet servers).dingus@cox. ' smtpHost : This is the email host you are using for sending emails. are internet-based services providing both internet and SMTP/POP3 access. such as Windows Mail. or other third-party mail applications.dingus@cox. you 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. strTo. though they can also be major literary works. leave it blank. "authsmtp. as it should be. _ ByVal strTo As String. like most people post text messages on their cell phones. Outlook. I think it would be a bit difficult to drive down the road with a desktop PC and keyboard in hand. NOTE: Yahoo. ' SSLUsername: If usesSLL is True. Outlook Express. then use SSL/TLS Authentication protocol for secure communications. ' strBody : text that comprises the message body of the email. then default credentials will be used (only works on local. ' smtpPort : TCP Communications Port to use.comcast. 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.net". Gmail. Gmail. ie. _ ByVal smtpHost As String) Dim smtpEmail As New Mail.net". Leave blank if the same as strFrom. But that is a topic we will cover after we resolve the email sending issues that many thousands of developers are presently having. ' smtpHost : This is the email host you are using for sending emails. a free service. though 465 (SSL) or 587 (TLS) are becoming popular. Comcast and Juno both support this interface. "Bubba Dingus" <bob. this is the password to use for creating a credential.com". "authsmtp.com> or bob. if you want to send a fast note to someone.net> or daviddingus@att. trying to steer while I thumb a quick message.juno. otherwise. normally free. of course. Juno and HotMail provide this service freely to their subscribers. because SMTP is limited in its capability to queue messages at its receiving end. '******************************************************************************* Page –235– . and Juno. such as “Bernard Shaw Fullo” <FulloBS@highschnozez. including those that use security layers. among others. such ' : as "smtp. _ ByVal strBody As String. etc. ie. this is the username to use for creating a credential.net> or daviddingus@att. assuming that the entire text is an email address.net ' strTo : Full email address of who to send the email to. NOTE: Texting while driving is illegal here in Florida. For example. set this parameter. "smtp.0 – David Ross Goben First. like POP3 or IMAP (Internet Message Access Protocol). '******************************************************************************* Public Sub BrainDeadSimpleEmailSend(ByVal strFrom As String. SMTP is a TCP/IP (Transmission Control Protocol/Internet Protocol) process used for sending and receiving email. However.dingus@cox. such as bob.com>. ie. David Dingus <daviddingus@att. In 2010 I witnessed 6 accidents and 1 fatality due to driver Texting.com> or bob. strSubject. ie.comcast. Unlike the other three. ' strBody : text that comprises the message body of the email.com". it is typically used with one of two other protocols. the Mail object will use only the data contained within them. ' usesSLL : If this value is TRUE. ' SSLDomain : If creating a credential when a specific domain is required.com ' strSubject: Brief text regarding what the email concerns. I use this for quick messages. like Gmail. provides it if you set an option in the POP Download section of the Forwarding and POP/IMAP option within its internet account Settings. Most servers default to 25. requires an additional monthly fee for SMTP/POP3 access. For a much more robust method that supports most-all servers. If the data contains angle brackets. '=============================================================================== 'NOTES: strFrom : Full email address of who is sending the email. such ' : as "smtp. impairing their vision. primarily by young people. If there are no angle brackets.gmail.juno. have to set up SMTP/POP3 accounts and access within your favorite local email application for all four. most servers will allow you 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.com> or even Coat Mahatma <mahatmacoat@classydresser. You will.dingus@cox. However. or more “trendy” formats. ' SSLPassword: If usesSLL is True. TCP port 587 uses a newer breed of security called TLS (Transport Layer Security). _ ByVal strSubject As String. the kind the NSA likes to read.UseDefaultCredentials = True 'use default credentials Else 'otherwise. Just look below. you can send a full. some may differ.NET applications. MsgBoxStyle. _ Optional ByVal smtpPort As Integer = 25. SmtpEmail.SmtpClient("smtp. This is easy. and they use TCP Port 110 for their Inbound Server. and Creating Credentials By default. With this method. _ Optional ByVal SSLPassword As String = Nothing. strSubject. SSLDomain) End If End If End If smtpEmail. TLS is still an SSL. Also. _ ByVal strTo As String. _ Optional ByVal usesSSL As Boolean = False. SSL technology provides identical security. this will in turn mean that you will need to also supply credentials. I know.Credentials = New NetworkCredential(SSLUsername. for example). and the SMTP host (such as smtp. password. TCP Ports. smtpEmail. quick and dirty. use strFrom smtpEmail. and domain) of the user running the application (for ASP. And third. such as Gmail requires (Secure TCP Port 465). The above DefaultCredentials property represents the system credentials for the current security context in which the application is running. _ Optional ByVal SSLDomain As String = Nothing) As Boolean Try Dim smtpEmail As New Mail. if you will be using a secure TCP Port.0 – David Ross Goben Public Function QuickiEMail(ByVal strFrom As String. smtpPort) 'create new SMTP client smtpEmail.comcast.org/reading_room/whitepapers/protocols/ssl-tls-beginners-guide_1029. But first. as you are about to see.com. strTo. 587) 'Create new SMTP client using Secure TCP Port 465 or 587. SSLDomain) Else smtpEmail. requires an SSL (Secure Socket Layer) to process email. for example). a subject for the email (what it concerns).CredentialCache. SSLPassword. yes.com. negotiate. and some others. NOTE: To examine your default credentials. Refer to www. plain-text message to one recipient with no attachments.EnableSsl = True 'True if SSL authentication required (also for TLS authentication). For a client application.Message.. non-secure email transactions. Second. However.net.NET Beyond the Scope of Visual Basic 6.Net. This is no big whoop.gmail. then you will also require SSL Authentication. report it MsgBox(e. If you must use a TCP Port other than Port 25. By setting usesSSL to True. the body of the text message. then include the needed port number. authsmtp.com". and Kerberos-based authentication. providing unencrypted. hence. such as in the following hard-coded example: Dim smtpEmail As New Mail.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. SSLPassword.Exclamation. However. or the user being impersonated). which uses secure TCP Port 465 or 587 for their Outbound Server and TCP Port 995 for their Inbound Server. Page –236– . _ ByVal smtpHost As String.Credentials = New NetworkCredential(strFrom. so include True for the Boolean usesSSL flag. these are usually the Windows credentials (username. such as Gmail.SmtpClient(smtpHost.Enhancing Visual Basic .OkOnly Or MsgBoxStyle. as mentioned above. you also provide the full email address of the person you are sending it to. Do not panic.juno.UseDefaultCredentials = True 'Typical for NTLM. strBody) 'send email using text/plain content type and QuotedPrintable encoding Catch e As Exception 'if error. then return a success flag End Function With the QuickiEMail() method.Send(strFrom. smtpEmail. "Mail Send Error") Return False 'return a failure flag End Try Return True 'if no error. _ Optional ByVal SSLUsername As String = Nothing.EngulfNDevour.sans. In these cases you will need to set the optional parameter to that TCP Port. the default credentials are the user credentials of the logged-in user. NOTE: Yes.. All others will have to manually create a credential. access the System. you supply it with the full email address for whom the email is from (you. you are not going to find an EnableTls property in the Mail object. we must create a new credential If Not CBool(Len(SSLUsername)) Then 'if SSLUsername is blank. SSL Authentication.DefaultNetworkCredentials property. 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. most Outbound Servers use TCP Port 25. _ ByVal strBody As String. But this is primarily because Gmail. or smtp. you will not be accessing a local IIS (intranet) SMTP server. so Microsoft made its own version of OS2 that used it.Net. it should not concern you. ' : If multiple recipients. multiple attachments.RichText ' : AltView.juno. "smtp.Nachamichi. demonstrating default access through “plain” and SSL secure Comcast servers: QuickiEMail("Idjut@comcast. Negotiate authentication cannot be used if an intervening proxy does not support keep-alive connections. separate each full email address using a semicolon (. the user’s email address will be used as long as it is not decorated. An Email Sender with Some Muscle If you require multiple recipients. and is.net> or daviddingus@att. this is why NASA uses 20-year-old technology).net".comcast. set AltView. 465) 'create new Gmail SMTP client with SSL for outgoing eMail smtpEmail. For Negotiate authentication to function correctly. Hide this or these recipients from view by others.TransferEncoding. you will want a method with a whole lot more muscle.TransferEncoding = Mime. require the user’s FULL email address for their certificates (includes @gmail. Now. "smtp. ' AltView : A System. highly secure domains may require more than an SSL certificate to reach the outside world. multiple optional CC (Carbon Copy) recipients. think about how many times you have read or heard even Microsoft mentioning the term NT Technology? In most cases.. Based on RFC 1510 (www.Jones@gmail. ' : If need be. David Dingus <[email protected]). in fact. etc. or the type of data that would be contained within an HTML Body block.) ' strBcc : Blind Carbon Copy. just the above QuickiEMail() method supports most emails that people need to transmit.ContentType.com".AlternateView object containing a formatted message.com". or if you want to send the body text as HTML format. you need to create a NetworkCredential object if you access a server through an SSL/TLS layer.dingus@cox. May be raw text or HTML code ' IsHTML : True if the strBody data is HTML. "Bubba Dingus" <bob.Base64 ' StrCC : Send "carbon copies" of email to this or these recipients. multiple optional BCC (Blind Carbon Copy) recipients.net.) ' strSubject: Brief text regarding what the email concerns. ' smtpHost : This is the email host you are using for sending emails. like a few other SSL/TLS servers. "editor@nyt. ie. as shown in this hand-coded example: Dim smtpEmail As New Mail."MomBNipp0neze") 'new credential with Username.ContentType.MediaType = Mime.Enhancing Visual Basic . True. you may need to apply code that bypasses massive firewalls and multi-layer proxies. several exchanges must take place on the same connection. slow about everything (a self-study showed it took them 9 weeks to ship an empty box). Although the above methods work in most domains. IBM. "[email protected]".dingus@cox. You will not be able to use something like Tukool Firwurds <iBeAnicn@chic.) Page –237– .com". the Kerberos protocol provides enhanced authentication for the distributed computing environment and standardization to interact with other operating systems. ie. NOTE: The account management supported by NT Active Directory Services requires a corresponding authentication protocol for network log-on. "Letter to the Editor".NET Beyond the Scope of Visual Basic 6. because you will not be able to use default credentials.MediaType and AltView. Leaving the SSLUsername field blank. Compare these examples.net". separate each full email address using a semicolon (. In that case you will need to create an SSL SMTP Client credential through a new NetworkCredential Object.TransferEncoding to properly format the AlternateView.net") 'plain QuickiEMail("Bob <[email protected]. "authsmtp. ' : If multiple recipients. It was adopted when Microsoft and IBM parted on their joint OS2 venture. if you are a minion at the Engulf & Devour Credit Corp. they refused to adopt the revolutionary. Password NOTE: You cannot use decorated usernames for creating a network credential.SmtpClient("smtp. ' : For example: AltView.com). such as Gmail or HotMail. "Ltr 2 Ed".gmail. "Yr ppr lns m dg cgz 2. advanced technology Microsoft was quickly developing without it being time-tested (meaning proven. or send alternate views of the message body. Therefore.0 – David Ross Goben NOTE: To make sure that IIS supports both the Kerberos protocol and the NTLM protocol.net". But even so. 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.gmail. separate each full email address using a semicolon (.org/rfc/rfc1510.net ' strTo : Full email address of who to send the email to. "Idjut". "Your paper lines my dog cages. FUNNY DIGRESSION: the Term NT stands for New Technology. As indicated. such as Rich Text or HTML.com ' : If multiple recipients.com". all the outgoing email support than a great deal of people will ever require.EnableSsl = True 'True if SSL/TLS authentication is required smtpEmail. _ "smtp.comcast. If this is not understood. 465. naming it NT.Credentials = New NetworkCredential("Norio. You would have to provide just the actual email address: iBeAnicn@chic. such as home use.". ' strBody : text that comprises the message body of the email. providing it with your SSL Username and Password.Mail.ietf.comcast.". For example.com> or bob.net>. you must confirm that the Negotiate security header is set in the NTAuthenticationProviders metabase property.MediaTypeNames. such ' : as "smtp.com". which any high school youth worthy of their salt can usually break through before Second Period.net>". "pSSwd#6") 'secure NOTE: Gmail. separated each with ".") For Idx As Integer = 0 To UBound(Ary) If Len(Trim(Ary(Idx))) <> 0 Then .txt (text/plain.SevenBit Case Else Atch.TransferEncoding = Mime.") 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. the above content type.Subject = strSubject 'add SUBJECT text line to mail message '------------------------------------------. separated each with ".rtf) ' : The contents of the attachments will be encoded and sent. _ Optional ByVal SSLDomain As String = Nothing) As Boolean Dim Email As New Mail. If this field and SSLUsername ' : are blank. .MediaType = Fmt 'set media type to attachment Case 1 'index 1 specifes Encoding Select Case LCase(Fmt) 'check the encoding types and process accordingly Case "quotedprintable".Body = strBody 'add BODY text of email to mail message. _ Optional ByVal strAttachments As String = Nothing.From = New Mail.) (C:\my data\win32. SevenBit). Base64) by placing them within parentheses and separated by a comma.QuotedPrintable..TransferEncoding = Mime.ContentType. '------------------------------------------If AltView IsNot Nothing Then 'if an alternate view of plain text message is defined. For example: ' : C:\My Files\API32.Add(AltView) 'add the alternate view End If '------------------------------------------If CBool(Len(strCC)) Then 'add CC (Carbon Copy) email addresses to mail message Ary = Split(strCC.ToString) ' smtpPort : TCP Communications Port to use. otherwise.") '(possible list of file paths. _ Optional ByVal strCC As String = Nothing. Len(attach) . was defined by acquiring System. Encoding.TransferEncoding.Net. ". or Text lists. ' : The second parameter.I . then use SSL Authentication protocol for secure communications.Mime. Image.QuotedPrintable Case "sevenbit".Text. "quoted-printable" Atch.ToString) ' : SevenBit (acquired by System. ' SSLDomain : If creating a credential with a SPECIFIC domain is required.Net. ' : If multiple attachments. I + 1. ". . then default credentials will be used (but this only works on local.txt. I .MediaTypeNames class.Plain. _ ByVal IsHTML As Boolean.TransferEncoding.exe (application/octet-stream.TransferEncoding.Attachment(attach) 'create a new attachment Dim fmts() As String = Split(Fmt.TransferEncoding = Mime.Bcc.AlternateViews.Mime. ' SSLPassword: If usesSLL is True.MediaTypeNames.1)) 'strip format data from the attachment path Dim Atch As New Mail.") For Idx As Integer = 0 To UBound(Ary) If Len(Trim(Ary(Idx))) <> 0 Then .Add(Trim(Ary(Idx))) 'add each recipent Next End If '------------------------------------------If CBool(Len(strBcc)) Then 'add Bcc (Blind Carbon Copy) email addresses to mail message Ary = Split(strBcc. c:\jokes.Base64.Mime. ". ' SSLUsername: If usesSLL is True.CC. set this parameter.AlternateView = Nothing. '******************************************************************************* Public Function SendEMail(ByVal strFrom As String. this is the username to use for creating a credential.0 – David Ross Goben ' strAttachments: A single filepath. separated each with ".ToString) ' : Base64 (acquired by System.") '(possible list of email addresses. Dim I As Integer = InStr(attach. or a list of filepaths to send to the recipient. ". _ Optional ByVal SSLPassword As String = Nothing. separated each with ".Mime..Base64 End Select Page –238– .NET Beyond the Scope of Visual Basic 6. then follow the attachment name with the MediaType and optional encoding (default is ' : application/octet-stream. _ ByVal smtpHost As String. _ ByVal strTo As String. which ' : can specify Application.TransferEncoding.To.") For Idx As Integer = 0 To UBound(Ary) If Len(Trim(Ary(Idx))) <> 0 Then . separate each filepath using a semicolon (.Net. intranet servers).Left(attach.Mime.TransferEncoding.SevenBit.1) 'get format data attach = Trim(VB.Net.. "(") 'check for formatting instructions If CBool(I) Then 'formatting present? Dim Fmt As String 'yes. _ Optional ByVal AltView As Mail.Enhancing Visual Basic . "7bit" Atch. this is the password to use for creating a credential. For example. ' usesSLL : If this value is TRUE. ". _ ByVal strSubject As String. Base64) ' : Where: The MediaType is determined from the System. leave it blank.Net. is determined by the following the values specified with the ' : System.Mime..TransferEncoding.IsBodyHtml = IsHTML 'indicate if the message body is actually HTML text. so determine which type of instruction to process Case 0 'index 0 specified MediaType Atch. so set up format cache Fmt = Mid(attach.Add(Trim(Ary(Idx))) 'add each TO recipent (primary recipients) Next '------------------------------------------.Add(Trim(Ary(Idx))) 'add each recipent (hidden recipents) Next End If '------------------------------------------If CBool(Len(strAttachments)) Then 'add any attachments to mail message Ary = Split(strAttachments.MailMessage 'create a new mail message With Email . Most servers default to port 25 for unsecure transmission. C:\telnet.Net. _ ByVal strBody As String. _ Optional ByVal usesSSL As Boolean = False. _ Optional ByVal smtpPort As Integer = 25. _ Optional ByVal SSLUsername As String = Nothing. ' : If you wish to send the attachment by specifying content type (MediaType) and content transfer encoding ' : (Encoding). _ Optional ByVal strBcc As String = Nothing.") 'add TO to mail message (possible list of email addresses.") '(possible list of email addresses.TrasperEncoding enumeration: ' : QuotedPrintable (acquired by System.MailAddress(strFrom) 'add FROM to mail message (must be a Mail Address object) '------------------------------------------Dim Ary() As String = Split(strTo. ' : "text\plain". Leave blank if the same as strFrom.") '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. formatted as 7-bit ASCII text (ergo. but it does not seem to work (under Windows Vista. we handle just like TO. SSLPassword.OkOnly Or MsgBoxStyle. especially by older systems.Attachments collection object takes care of loading the actual file data. “0” through “9”. The file paths to the attachments are separated by semicolons. Catch e As Exception 'if error.0 – David Ross Goben Else End Select End If Next . or 4 bits (computer engineers must always be hungry). However.Credentials = New NetworkCredential(SSLUsername. Some claim there is no use for a BCC list. MsgBoxStyle... There is a TO list. so simplifying things by just using the default should be no issue of concern. which. as all.Send(Email) 'finally. allowing for a Base16 numbering system. report it MsgBox(e. Attachments are appended to the end of the email. where it is useful to inform the primary recipient that these other people also got a copy of it. The Attachments. and each character represents a nibble. and “A” through “F”.).UseDefaultCredentials = True 'use default credentials Else 'otherwise.Exclamation. and a Blind Carbon Copy list (BCC. smtpPort) 'create new SMTP client on the SMTP server SmtpEmail. so splitting them into an array will leave any last array element empty. converts them to encoded 7-bit text. strAttachments. when I actually discovered these solutions as I was learning. which.Add(New Mail. where I discuss formatting the attachment to different content types and encoding.SmtpClient(smtpHost. SSLDomain) Else SmtpEmail. 8-bits). As such. SSLPassword. The important thing to notice about these three recipient fields is that you separate each recipient with a semicolon (.Attachments. but I beg to differ. not viewable by TO or CC recipients). use strFrom SmtpEmail. send the email. An email is actually a series of bytes (in email lingo. encoded in effiecient Base64) End If End If Next End If End With '----------------------------------------------------------------------'now open the email server.txt file. anyway). The Mail. A lot more. CC and BCC. except for business transactions. by its extension. otherwise they will default to binary (“application/octet-stream”) and encoded for internet transport using the Base64 method.. where each byte is represented by two 7-bit characters. 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). Page –239– . SmtpEmail..Enhancing Visual Basic . like Base64.. SSLDomain) End If End If End If SmtpEmail. my API32. or the sender did not want the TO or CC viewers to know that these BCC recipients were being sent copies. even binary attachments are encoded into blocks of ASCII text. the 8th bit is never used). we must create a new credential If Not CBool(Len(SSLUsername)) Then 'if SSLUsername is blank.Add(Atch) 'add attachment to email . looks like gobbledygook at the bottom of my email when converted using the default Base64 encoding. Try Dim SmtpEmail As New Mail. but it was not essential. is hopefully a text file. these are called octets. which all emails must be formatted to for internet transfer..Attachment(attach)) 'add filepath (if no format specified. though this also doubles the data size). Here is a sampling of its beginning: NOTE: My comments in the SendEmail() method header regarding attachments.Attachments. you can also declare the encoding and display formats for an attachment. many servers now support various types of encoding. The CC list is an archaic vestige. Most email applications simply slap a semicolon on the tail of each email address. For example. The BCC was essential for sending copies to other concerned parties.Credentials = New NetworkCredential(strFrom. as ASCII Hexadecimal (hex.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. we will leave for later. NOTE: Microsoft Mail features BCC. "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.NET Beyond the Scope of Visual Basic 6. a Carbon Copy list (CC). As noted in the comments above the method. 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.Message. sometimes formatted. 80micro.com To: david. and constants continues from here. envelop the text within an HTML body. and append the text “</BODY></HTML>” behind it.Contains("</HTML>") Then Msg = "<HTML><BODY>" & Msg & "</BODY></HTML>" End If 'Msg contains an HTML wrapper? 'no so add one to it 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. name=API32. It is actually up to email reader software to use that information and determine how to present the data. But.Visible = True 'set the web browser to the same location and dimensions as my usual text display control 'hide my plain text/Richtext textbox 'Does my Msg contain an HTML wrapper? 'no so add one to it 'set web browser contents to that of my email message body and display it 'expose the web browser Page –240– . Sending Email Messages as HTML The IsHTML parameter in the SendEMail() method sets the state of the Mail.Bounds Me.Bounds = Me.ross. some plain text readers will simply show the raw data. Msg. If it is set to True. if it is not already.com Date: 21 Feb 2011 21:16:00 -0500 Subject: Test Content-Type: multipart/mixed. all you have to do is prepend the text “<HTML><BODY>” in front of the message.Contains("</HTML>") Then Msg = "<HTML><BODY>" & Msg & "</BODY></HTML>" End If Me. boundary=--boundary_0_5fbcc36e-0097-412e-bf2b-c4dc5bc543d0 ----boundary_0_5fbcc36e-0097-412e-bf2b-c4dc5bc543d0 Content-Type: text/plain. structures. then the SMTP interface will know to set the body text formatting flag to text/html instead of its usual text/plain. We will also learn how we can fully exploit it and very easily decode it (and also how to avoid it).com".Enhancing Visual Basic . where it will make more sense to us. True.Visible = False If Not Msg. "smtp. others will bring up a web interface.goben@gmail. Two other parameters you may have noticed in the SendEmail() method were IsHTML and AltView. "Test".NET Beyond the Scope of Visual Basic 6. with 7bit encoding) JyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t LS0tLS0tLS0tLS0tLS0tLS0tLS0NCicNCicgICAgIEFQSTMyLlRYVCAtLSBXaW4zMiBBUEkg VHlwZSBEZWNsYXJhdGlvbnMgZm9yIFZpc3VhbCBCYXNpYw0KJw0KJyAgICAgICAgICAgICAg ICAgICAgICAgQ29weXJpZ2h0IChDKSAxOTk2IERlc2F3YXJlDQonDQonICBZb3UgaGF2ZSBh IHJveWFsdHktZnJlZSByaWdodCB0byB1c2UsIG1vZGlmeSwgcmVwcm9kdWNlIGFuZCBkaXN0 .DocumentText = Msg Me.com") My Gmail account will receive an email with the following at the bottom of the data (less my notes): From: mercedes_silver@80micro. when I encountered this during my initial email tests. we will return to this when we examine alternate views and attachments. For example. 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>”.com Date: 21 Feb 2011 21:39:04 -0500 Subject: Test Content-Type: text/html. This is quite easy to do.ross.RichTextBox1.WebBrowser1. using code similar to the following: Me.goben@gmail. I would expose it within my Web Browser interface control.  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ The encoded definition of several thousand more API declarations.RichTextBox1. NOTE: In the back of my mind.WebBrowser1.. However. regardless. WebBrowser1.ross. 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).WebBrowser1.MailMessage object’s Boolean IsBodyHtml flag. where String variable Msg is assumed to contain the HTML-formatted text of the message body: If Not Msg. because some main body HTML is sent without HTML/BODY tags. The easiest test is to simply check to see if “</HTML>” is contained within the message. If not.goben@gmail. though most are. and present that to the user. such as a WebBrowser control.com"..com To: david. "david. I was really wondering about this Base64 Content Transfer Encoding. but there is a simple test you will need to perform.0 – David Ross Goben From: mercedes_silver@80micro. charset=us-ascii Content-Transfer-Encoding: quoted-printable This is just a test ----boundary_0_5fbcc36e-0097-412e-bf2b-c4dc5bc543d0 Content-Type: application/octet-stream.txt  ▬▬ The Name parameter identifies this as an attachment (and it was handled as a binary stream) Content-Transfer-Encoding: base64  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ How the file is encoded (I would have rather had this be text/plain. And that is all! Consider this test. leaving the Plain Text version as the “last ditch” option.TransferEncoding. then that space is converted to a special hexadecimal format. plain text is so monotonous and passé. most of us tend to send plain text emails. to create an alternate view from a RichTextBox control named myRTC.Rtf) AltView. I did not even blink an eye when I had to go though the time and effort of changing the TypeBall just to change fonts. such as Quoted-Printable. but are required to limit the line width of the email data. Data in an email is a series of lines. regardless of what you really want.CreateAlternateViewFromString(myRTC. and typically the first alternate view they are able to support.Text. Also. for example.txt). Sending Alternate Message Views Another thing I want to explore with you is one of the more interesting parameters I have listed for the SendEMail() method.AlternativeView object. Phooey! I still remember the thrill I got when I hit a button on my Selectric II Typewriter and it bolded what I typed something.Base64 'how it should be encode for transport across the internet Even though my SendEMail() method presently allows for only one alternative view. 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. 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 your web browser. or got my first TypeBall – we called them golf balls – that supported italics. which is usually all we ever really need. as well as decode Quoted-Printableencoded and Base64-encoded data back to its original form with ease. italics.MediaType = Mime.0 – David Ross Goben The two lines of text are dutifully displayed using the HTML formatting I assigned to them.NET SMTP processor will actually allow for as many as you want. such as by using a “soft return” flag. “=20”. Setting up an alternate view is easy. if there are any 8-bit characters embedded in the message. There is so much more to explore if you truly want to create a full-featured email processor. or simple text box. otherwise the system will automatically encode the data to Base64. The urgent code red message was processed and read in time! The world is once again safe. at least. 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. These considerations will also be covered in this article. which specifications require. RFC 2822 (www. which it may be forced to do simply to ensure 100% original data integrity.AlternateView = Mail. each terminated by a vbCrLf (Carriage Return and Linefeed – codes 13 and 10. 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. then you should convert them to Hex-Tags. Fortunately. Typically. 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.NET Beyond the Scope of Visual Basic 6. These must be decoded and removed before displaying the text. once it is decoded AltView. and that is AltView. In these cases they are tagged.ContentType. respectively). Also with Quoted-Printable-encoded text. if a space character precedes a vbCrLf. most email processors will. in compliance to RFC 2822. bolding. like the equals sign “=” used by Quoted-Printable encoding.Enhancing Visual Basic . rich text box. we can use: Dim AltView As Mail. Often these line terminators are not a part of the original text. outlines all the gory details of email formatting. the .TransferEncoding = Mime.AlternateView. or underlining.Net. and the second line was underlined. The AltView parameter is a System. The first line was Bold. display the alternate view. The Internet Message Format document.org/rfc/rfc2822.RichText 'how the alternate view should be handled. failing to add emphasis.MediaTypeNames. Page –241– . For example. even to just affect a single word. For me. Technically. which I habitually call Hex-Tags. by default. even if they are in fact formatted by a Rich Text or HTML editor.ietf.Mail. However. because they are formatted in Base16 (Hexadecimal). you have easy access to the Plain Text version and the Rich Text version of a document. but this does not provide for line formatting. some few really ' : primitive email readers.0 – David Ross Goben Most people prefer their emails to be formatted as HTML or as Rich Text. Even though I have seen a number of utilities. that offer this kind of service.”. ""). and I’m blind as a bat in the other. Most of you are aware that in a VB-written Rich Text editor using a RichTextBox control.Replace("&gt. NOTE: HTML Entities start with an ampersand (&) and end with a semicolon (. &lt. if you wish ' : to make this conversion the main body message of an email. there are two versions of tags. one that includes the decimal ASCII code.Replace("&lt. '******************************************************************************* Public Function QConvertHTML2Text(ByVal HTMLText As String) As String Return RegularExpressions. paste it to Notepad.” (non-breaking space) for a blank space “ ” where a space might normally be ignored. However. """"). as most of you are already aware. "&").".” for an intentional “>”.". even ' : though this is typically not an issue. Entity Name &quot. &#38. 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. "<[^>]*>". it will return a Plain Text ' : string with HTML code removed.". the following enhanced method does 99.Replace("&nbsp. both commercial and shareware. typically those that simply allow you ' : to preview email messages. and can sometimes look almost as disorganized as a college dorm room. without fully loading them.". " "). ">"). But a Plain Text rendering has always seemed to be an issue.” to represent an intentional “<”. NOTE: In a pinch.". ' : though most of these sysmbols will not be encountered in most ' : HTML documents we produce. ". “&nbsp. and its Rtf property provides the Rich Text version.“&gt.Replace(".” for an intentional “. Description quotation mark (?) apostrophe (’) ampersand (&) less-than (<) greater-than (>) As you can see. copy from the HTML display of a WebBrowser control. plus any special formatting that might be stored within or between them.Enhancing Visual Basic . &#60. The Entity Number also allows 8. or who are vision-impaired (“Can’t see out of one eye. 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. However.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. plus “.” as Grandpa often said). you 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..Regex.") End Function The above function will also remove all the extra data within the HTML tags.. "<").). or will not bother with it. RFC 2045 requires email handlers to support it. &apos. will not ' ' know how to support Base64. Its Text property provides the Plain Text version.". ' : ' NOTE : Numerous of these conversions will convert the text to 8-bit. it will return a Plain Text string ' : with all HTML codes and formatting removed from it. Following is a list of the HTML Reserved Entities: Character " ' & < > Entity Number &#34. such as “&lt. This adds up to a whole lot of work. such as “<HTML>” or “</BODY>”. 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. &gt. 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.NET Beyond the Scope of Visual Basic 6. NOTE: An HTML Text Tag is a thing starting with “<” and ending with “>”. '******************************************************************************* Page –242– .Replace(HTMLText. and one that includes the typical or “classic” representation. you ' : may have to further convert this using ForceQuotedPrintable() ' : to maintain Quoted-Printable encoding and avoid Base64.". but simply ' : display the raw data. &#39. Just invoke it like this: “Dim PlainText As String = QConvertHTML2Text(HtmlText)”. &#62.Replace("&apos. _ "'"). then copy it as plain text.or 16-bit extended characters to be displayed by a 7-bit source. But regardless of that.Replace("&quot.Replace("&amp. &amp. "Ó").".Replace("&#" & Idx.". "ª").Replace("&theta. _ "↵ ↵"). vbCrLf) 'replace ISO 8859-1 Symbols (160-255). "ç").Replace("&atilde.Replace("&hellip.Replace("&Zeta. "Τ"). _ "Ρ"). _ "Ð").Replace("&brvbar.". "˜").". vbCrLf). "–").Replace("&Kappa.Replace("&euro. _ "´").".Replace("&Chi.Replace("&epsilon.".".Replace("&empty. "Ö"). "Ζ"). and table entry terminators with vbCrLf Sb.Replace("&nu.Replace("&Ntilde. _ "Φ"). and &) Sb.Replace("&macr.Replace("&omicron.". "⊇ ⊇ ").Replace("&kappa.Replace("&sigmaf.".".NET Beyond the Scope of Visual Basic 6. _ "∧ ∧ "). _ "¼"). "″").".".Replace("&Yuml.".Replace("&beta.".".Replace("&eacute. "Õ").Replace("&oline.".Replace("&ouml.".".".Replace("&Iacute. _ "õ"). "∅ ∅").". "Ã").". _ " "). _ "‡").".". _ "Ô").Replace("&thetasym.Replace("&pound. "¦"). "’").". "æ").". _ "≥").Replace("&perp.Replace("&mdash.Replace("&nabla.".".".". " "c)) Next 'replace reserved entities (except <.Replace("&int.". Note that any matches will make the text 8-bit Sb.". "∴ ∴ ").Replace("&otilde. "™").".".Append(S.". "σ").Replace("&ugrave.Replace("&le.Replace("&ne.Replace("&radic.Replace("&lambda.".Replace("&oelig. "ü"). _ "Ε").".".". _ "Ý").".".Replace("&sdot.". "è").Replace("&circ.".Replace("&Alpha. """"). "Χ").".".".Replace("&Theta. "¡").Replace("&Otilde.".".".Replace("&laquo.Replace("&times.Replace("&thorn.Replace("&iacute.Replace("&permil.".".". "∨ ∨ ").Replace("&zeta.".Replace("&Aacute.Replace("&Euml.".".".".".".Replace("&phi.Replace("&Pi. "½").". "Î").". "Π"). _ "Ÿ").Replace("&curren.". "ÿ") 'replace Math Symbols Supported by HTML.". "≠").". _ "∇ ∇ ").Replace("&OElig.". "⋅ ⋅ ") 'NOTE: certain characters have tall characteristics 'replace Greek Letters Supported by HTML.".".Replace("&Iota.Replace("&sup3. "Ë").".Replace("&Iuml.Replace("&para. "š").".". _ "ω").".". "ø").Replace("&yen.Replace("&Ocirc.Replace(Sb.Replace("&oacute.0 – David Ross Goben Public Function ConvertHTML2Text(ByVal HTMLText As String) As String 'instantiate an initially blank StringBuilder object Dim Sb As New StringBuilder() 'first remove leading whitespace of each line and append the result to the StringBuilder Dim ary() As String = Split(HTMLText.". "º").".Replace("&supe. "¹"). "↓"). _ "ι"). "↔").Enhancing Visual Basic .Replace("&Upsilon.". _ "υ").Replace("&harr.Replace("&sim.".".Replace("&euml.".Replace("&there4.".Replace("&iexcl. "∂").Replace("&delta.".Replace("&cap.".Replace("&ntilde. "ý").Replace("&eth.".". "Þ"). "Œ").Replace("&ecirc.Replace("&aacute. _ "√").Replace("&uacute. "à").Replace("&divide.".Replace("&sect.Replace("&egrave.Replace("&ndash.Replace("&and.". _ " ").". "θ"). "'").Replace("&sup.Replace("&THORN.Replace("&Xi.".".Replace("&aring.".".". "⊄ ⊄ "). "∈ ∈ ").Replace("&pi.".".Replace("&ordf. _ "ñ"). "-"). "•").Replace("&ucirc.Replace("&diams.".". vbCrLf).". _ "þ").". "∝ ∝ ").Replace("&trade. "⌉⌉"). "∋ ∋ ").Replace("&exist. "⌊⌊"). " "). "ϒ"). "ö").Replace("&Tau. "ð"). "λ"). _ "ε").Replace("&ang. "</H[^>]*>".". "Μ"). Note that any matches will make the text 8-bit Sb.".Replace("</td>".".Replace("<BR>".Replace("&clubs. _ "ú"). _ "Ù").Replace("&Acirc.".".Replace("&cedil. "Â").Replace("&frac34.Replace("&loz.".Replace("&prop. Note that values > 127 will make the text 8-bit For Idx As Integer = 1 To 255 'See www.". line breaks.".Replace("&Uacute.Replace("&Nu.Replace("&forall.Replace("&lceil.Replace("&Ugrave. "®"). _ "¨"). "ξ"). "›").Replace("&icirc.Replace("&Eta.". _ "¤"). "â"). "¾"). "ô").Replace("&crarr.".Replace("&Dagger.Replace("&rho. _ "¬"). "²").Replace("&isin. _ "≈").". "ï").".". _ "⌋⌋ ").". "π").Replace("&omega. "Š").".Replace("&Delta.Replace("&Psi.Replace("&Atilde. "τ"). " "). _ "∏").Replace("&rfloor. "Û").".Replace("&rdquo.asp Sb.Replace("&Epsilon. "◊").".". "κ"). "©").Replace("</P>". "ϖ") 'replace Other Entities Supported by HTML.Replace("&cent.".". "⊕ ⊕ ").".".Replace("&prod.". "⊂ ⊂ ").Replace("&yacute. "∑").Replace("&AElig. "£").". vbCrLf). _ "ν"). >. "Α").Replace("&lsquo.Replace("&micro.Replace("&shy.Replace("&lsaquo. "†").".".". "û").". "←").".Replace("<br>".".Replace("&auml.". "∗ ∗"). "∠ ∠ ").Replace("&otimes. "À").".".". "η"). "Ê"). "∪ ∪").Replace("&part.ToString(). "³").Replace("&Omicron. "∩").Replace("&acirc.". _ "‾").Replace("&ni.Replace("&sube. "→"). "ä"). Chr(Idx)) 'replace most common numeric entities Next 'Ensure header definitions are followed by vbCrLf Dim NewText As String = RegularExpressions.Replace("&Ouml.".Replace("&frac14. "¿").Replace("&notin.".".Replace("&Scaron. "Σ").".".".Replace("&apos. "¥"). vbCrLf). vbCrLf).".".".Replace("&Ograve. "¢").".Replace("&sub.".Replace("&Igrave. "Ü"). _ "♥").Replace("&deg.Replace("&Phi. "«").Replace("&reg. "ς").Replace("&tau.".Replace("&or. "Á").Replace("&dagger.Replace("&Eacute.Replace("&uarr. _ "⊥ ⊥ ").Replace("&middot. _ "Ν"). "ϑ"). "Η").".Replace("&copy. "Ï").".Replace("&Gamma. _ "á"). "Í"). "¶"). "δ"). "ì").". "♦") 'NOTE: certain characters have tall characteristics 'replace special ASCII coding entities that were not captured by the above.".". _ "¸").".TrimStart(Chr(9).Replace("&ldquo.Replace("&scaron. "÷") 'replace ISO 8859-1 characters.Replace("&frac12. "∼ ∼ "). Note that any matches will make the text 8-bit Sb.".Replace("&spades.".Replace("&iota. "∞").Replace("&psi.Replace("&Uuml. "⊗ ⊗").". _ "↑").Replace("&sigma. "♠").Replace("&Egrave. "Ç").Replace("&Prime.Replace("&upsilon.Replace("<p>".Replace("&ETH.Replace("&ocirc. _ "×").".Replace("&thinsp.Replace("&cong. "œ"). _ "í"). "Å").".Replace("&not.".".".".Replace("&upsih.Replace("&Omega. _ "Ä").Replace("&equiv.".". "∃ ∃"). "≡").". _ "°").".".Replace("&Auml. "Ω"). _ "é").". "ζ").".Replace("&larr.".".Replace("&lfloor.Replace("&chi.".".".".". Note that any matches will make the text 8-bit Sb. "ψ").".". "ó").ToString & ".Replace("&xi.Replace("</TD>".". "‘").". "Β").".". "Ø").Replace("&prime.Replace("&sum.Replace("&szlig. _ "α").Replace("&Sigma.".". "É").". _ "Ì").Replace("&Rho.Replace("&lowast.".".Replace("&iquest. "Ò").". vbCrLf) 'Also seek out other Unicode encoded number entities not covered by the above and individually update them Page –243– . "φ").".Replace("&Yacute.Replace("&acute. _ "Ι").". Note that any matches will make the text 8-bit Sb.". "β"). "ã"). "χ").".".".Regex.".Replace("&Ucirc.Replace("&infin.".".Replace("&Agrave. vbCrLf) For Each S As String In ary Sb.Replace("&oslash. _ "å"). "∀ ∀ ").Replace("&emsp. _ "—").Replace("&Mu.".".Replace("&agrave.Replace("&darr.Replace("&Aring.".Replace("&yuml. "ƒ").Replace("&aelig.Replace("&fnof.".Replace("&Icirc.".".".Replace("&eta.". "§"). "±").Replace("&Oslash. "ê").".".Replace("&hearts.Replace("&alpha.". "»").Replace("<P>".Replace("&uml.Replace("&sup1.Replace("&raquo. "Ξ").Replace("&ge.". _ "′"). "∉ ∉ "). "Κ").".".Replace("&Beta.Replace("&tilde.Replace("&cup.Replace("&minus.".". "Ñ").".Replace("&igrave.".".". "‰"). "ë").Replace("&gamma. "ˆ").Replace("&piv. "Ο"). "ο"). "♣").Replace("&ordm. "−").". "‹").com/tags/ref_entities. "Δ"). " "). "⌈⌈"). _ "⊆ ⊆ "). "¯").".Replace("&rarr.Replace("&Lambda.Replace("&uuml.Replace("&mu. "€").Replace("&Oacute. _ "ρ"). "ß").Replace("&bull.".". "î"). "ò").Replace("&ensp. "Θ").".Replace("&ograve.Replace("&rsaquo.". vbCrLf).". "⊃ ⊃ "). _ "È").Replace("&sup2. "Λ").".Replace("</p>".".". _ "∫"). "µ").Replace("&asymp. "‚").".Replace("&bdquo.".w3schools.".Replace("&sbquo.Replace("&Ccedil.". "•"). _ vbCrLf).".".Replace("&nsub. "„"). "Ú"). "γ"). "Υ").". "≅ ≅ ").Replace("&rsquo.".".Replace("&oplus. "Æ").Replace("&quot. "…").Replace("&plusmn.Replace("&rceil.Replace("&nbsp.".".Replace("&Ecirc.Replace("&iuml. " ") 'replace HTML paragraph. "Ψ").Replace("&ccedil. "≤"). "ù").". "Γ"). "μ"). Email is progressing to the point to where most emails are in HTML format. Idz . though I would really hope that the main body would be forced to Plain Text. rtfView) As you can see from this example. Len(S) . "smtp. if they were viewing your email on their trendy Blackberry.CreateAlternateViewFromString(Me. Page –244– . iPhone. You typically pass the Plain Text version as the strBody parameter of the SendEMail() method. ". or other small Email-enabled portable device.NET Beyond the Scope of Visual Basic 6. "<[^>]*>".") 'find terminating semicolon Dim S As String = Mid(NewText. "david.Replace(NewText. they can read your email in plain text. ". "Test". Rich Text is becoming so common that I expect one day to see an industryrecognized IsBodyRtf parameter.". they could.Enhancing Visual Basic . where early on in my trials I sent an email with a RTF alternate view: 'create an alternate view of the RTF data from our RichTextBox Control Dim rtfView As Mail. replace . and IsHtmlBody be revoked. simply allow the user to optionally specify the Content-Type and Content-Transfer-Encoding of the message body they choose to send. S.") End Function Sending a Plain Text Message Body and a RichText AlternateView Suppose you wanted to send both a Plain Text and a Rich Text version of an email to someone. then return result Return RegularExpressions. Droid. allowing us to transmit the main message body as HTML. However.0 – David Ross Goben Dim Idy As Integer = InStr(NewText. NewText.com".RichTextBox1. "&#") Loop 'strip remaining HTML text tags. Idy.".AlternateView = Mail.Replace(". You can see it using the Rich Text data for the alternate view. which is why we have the IsBodyHtml parameter on a MailMessage object.com".". creating an AlternateView object is almost too easy. and its plain text for the message body parameter.3)))) 'replace expression InStr(Idy + 1.".AlternateView. ">"). Or. NewText. "&#") 'check for a numeric entity Do While Idy <> 0 'loop as long as we find one Dim Idz As Integer = InStr(Idy.Replace("&gt..Idy + 1) 'grab expression RegularExpressions. Or. so if they were trendy and wanted to view the chic version of your email as Rich Text.Replace("&amp. which reads much better on small screens. 3. Consider the following.Regex. to be diplomatic.Rtf) 'send our email as plain text and with a rich text alternater view SendEMail("[email protected]("&lt. with .80micro.Regex.Replace(NewText.. False. _ "<"). ""). Chr(CInt(Mid(S. Me. "&"). The Rich Text or HTML version would be sent as an alternate view.ross. convert ampersand.com". replace < and > placeholders.RichTextBox1.Text.. though in this case email readers would have to be able to convert the RichText or HTML main message to plain text if required.goben@gmail. where you could specify text indicating which decryption method for your custom email reader to use. as well as start boundary for next part This reports plain text using UTF-8 (8-bit extended ASCII) Encoded using Base64 method for safe internet transfer of data This blank line is NOT part of the attachment (MUST BE BLANK) Base64-encoded data (is this rich text?. replied with some resigned candor that the hapless inquisitors were stuck with Base64 encoding. looks fine.=0A  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ ----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Content-Type: text/plain. the plain text version. my RichText version of the message looks like the cat was dancing on the keyboard chasing the fish on the computer’s screen saver. Page –245– . I was disappointed to not to see actual RTF text: From: mercedes_silver@80micro. after all. Scanning the internet yielded no useful results. charset=us-ascii  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Content-Transfer-Encoding: quoted-printable ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This is a test of Rich Text data. that is what I first said. “text/richtext”. I should have found in the first place). Side note: note the extra "--" on both sides What the–? (Well. In my more rational mind. I had seen several other Rich Text emails. 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. After more time searching MSDN resources.ContentType object. but the knowledgeable respondents. unquenchable yearning for success. but with those I could also see the HTML-like Rich Text encoding of the message body perfectly fine. Main body of email follows (usually plain text) Used as clue toward how to process data after it is decoded Encoded as Quoted-Printable. UTF-8.Enhancing Visual Basic . that’s kinda true. Easy to address.000 anyway. we can specify the text that Content-Type reports. I did find myriad requests for help from people in the same boat as me (well – 438. When it is decoded..” Well.com Date: 22 Feb 2011 17:19:08 -0500 Subject: Test Content-Type: multipart/alternative.ross.Yes. I decided that it was time to hit the books and find out what I was not yet doing. And there is also that Base64 part.0 – David Ross Goben Although the results are valid and perfectly usable. also known as simply ASCII. a necessary result of email. which I was unfamiliar with at the time. always trying to be helpful (except for the non-social jerks who just want to make everyone else as miserable as they are). as plain text. This would be handy for encrypted emails. it will be able to be processed as rich text) End boundary for second part. specifying another parameter for the CreateAlternateViewFromString() method. So instead of shaking my fists at the heavens in forlorn. or whatever I choose. and we are presently incapable of changing that harsh truth any time soon under VB. but most of them typically showed the ContentType as “text/richtext”.. End boundary for main body. All controls codes rendered visible (=xx) This first blank line is NOT part of the message body (MUST BE BLANK) Note the decoration (=0A). is strictly 7-bit code). But for this implementation.. charset=utf-8 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Content-Transfer-Encoding: base64  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ e1xydGYxXGFuc2lcYW5zaWNwZzEyNTJcZGVmZjBcZGVmbGFuZzEwMzN7XGZvbnR0Ymx7XGYw  ▬▬ XGZuaWxcZmNoYXJzZXQwIFRpbWVzIE5ldyBSb21hbjt9e1xmMVxmbmlsXGZjaGFyc2V0MCBN aWNyb3NvZnQgU2FucyBTZXJpZjt9fQ0Ke1xjb2xvcnRibCA7XHJlZDBcZ3JlZW4wXGJsdWUw O30NClx2aWV3a2luZDRcdWMxXHBhcmRcY2YxXGYwXGZzMjQgVGhpcyBpcyBhIFxiIHRlc3Qg XGIwIG9mIFxpIFJpY2ggVGV4dCBkYXRhXGkwIC5ccGFyDQpccGFyZFxjZjBcZjFcZnMxN1xw YXINCn0NCg== ----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321-.NET Beyond the Scope of Visual Basic 6. but encoded.. Microsoft’s version of “ASCII” it is a transformation format for translating between 16-bit Unicode and 8-bit Extended-ASCII code (UTF-8). I acknowledged that the first part.. The notes tend to indicate garnered knowledge since then. I could add a second parameter that was a System. But the Rich Text version is tagged. ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ First REAL boundary marker.Net. Hmm.com To: david. Is it maybe bovine scat? And mine? If we look at this ContentType object. according to the Content-Type (how the reader should handle the decoded data).Mime. Yet the Content-Transfer-Encoding (how the data is encoded for internet transport) indicates Base64. With it. and it’s likely not the fish on the screen saver.) NOTE: Even though I did not realize it at the time I first saw this.. Maybe it was not fish I smelled.goben@gmail. Something smells fishy. dummy me. I found another overload (which. I can in fact fully use and properly translate the above alternate view data with unbelievable ease. the MSDN documentation states in its remarks: “The default media type is plain text and the default encoding is ASCII. such as “text/plain”.NET. Also. a crowded boat). it is.  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 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 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Content-Type: text/plain. except that UTF-8 (8-bit Unicode Transformation Format) is not actually ASCII (Standard ASCII. after the slash. But here. name="Baby Names.RichText)'create a ContentType object for the RTF data from our RichTextBox NOTE: All the code in this article will assume that you have at least imported System.Net.NET Beyond the Scope of Visual Basic 6. 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”.AlternateView = Mail.Text.MediaTypeNames. the Content-Transfer-Encoding mechanism (which I had been having issues with) will be internally set to one of 3 values: quotedprintable. if you specify “text/plain”. For example: Dim ContentType As New Mime.ContentType(Mime.Text. it can be expected to be processed as Base64. which took some real doing. if we are passing ASCII data. is the Subtype. "Test". which is un-encoded 7-bit data.Text. where binary or 8-bit code. By the way.Encoding class and then the System. But we must remember that this field is only text and informational to the email application. Based on the kind of conversion we specify from the Encoding class. And that is why we have been getting Base64 as the Content-Transfer-Encoding type all this time on attachments. Therefore.Mime. be discussed in the next section. I searched really hard to find MSDN examples that demonstrated them all. It does nothing to the data. and System. by specifying a method.Text. ContentType) 'send our email as plain text and with a rich text alternater view SendEMail("mercedes_silver@80micro. again. I have to also change the encoding). For example. but the Content-Transfer-Encoding was still set to Base64 (obviously. The first part is the Type.Enhancing Visual Basic . but this just gave me all the information I needed to easily use that data! But Wait! I did 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. “Content-Type: text/plain. such as “=0D” for a vbCr). MediaTypeNames specifies 3 subclass types of interest. base64. [email protected] string. as if VB code was totally incapable of doing this (untrue!). it will be encoded as Quoted-Printable as long as we pass a text-based method for Encoding. or text containing 8-bit code will be compressed into a string of 7-bit text tokens. all the examples I did find on MSDN were for C# and C++.ContentType(Mime. False. I can already sense a move into the right direction… Encoding specifies a series of conversion classes. or the data is not text. and the second part. All this will.ross. NOTE: An AlternateView object is actually embedded within an email as an Attachment.com". and Text. we are more interested in the Text type.RichTextBox1.NET. "david. "smtp. or Rich Text. even though I have plenty of time to sleep and still get up at 5 AM).RichText)'create an alternate view of the RTF data from our RichTextBox Dim rtfView As Mail. which is valid and legal because internet regulations require that your data cannot be altered in a way that it is not 100% reversible.com".80micro.com".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. Page –246– . In fact. we do not use the method. charset=us-ascii” versus “Content-Type: text/plain. we use these classes to convert one type of text to another. it simply specifies how the decoded data should be processed by the email application. Image.0 – David Ross Goben Constructing a ContentType object is easy. you will find the encoding method will be forced to Base64 to ensure data security. If we leave this field blank. or it contains 8-bit text. but the data contains 8-bit characters.Encoding. NOTE: Unbeknownst to me at the time. Each of these in turn has subtypes. Note further that the specified default filename in an Attachment will not be wrapped in quotes if there are no spaces embedded within it. which is why you see the Context-Type field indicate “text/plain” or “application/octet-stream”. where all control codes are marked with 7-bit Hex-Tags (“=” followed by a 2-character hexadecimal value. it states “text/richtext”. which will in turn use it to convert our data. such as Plain Text. The only difference between the two is that an AlternateView will not specify a filename parameter in the Content-Type field.CreateAlternateViewFromString(Me. Me. like System. but it expected a System. but rather we supply that method to SMTP. Usually.txt"”. or 7bit.AlternateView.RichTextBox1.Text. which are most notably useful for regular attachments: Application.Rtf. The third overload did not expect just a Mime parameter.ASCII. Presently. our email result is creeping much closer to what I had been expecting to see: From: [email protected]. so we can send them Quoted-Printable. NOTE: Though Quoted-Printable encoding will change conversion to Base64 if it finds bytes codes greater than 127 (8-bit data). This.}{\f1\fnil\fcharset0 Microsoft Sans Serif.CreateAlternateViewFromString(Me.MediaTypeNames class to determine this Content-Type data. Note also that an actual ‘=’ in the text will be. A-F) 'replace hex data with single character code 'stuff result to rich text box Page –247– . Nothing) For idx As Integer = &H1 To &HFF Dim Hx As String = Hex(idx) If idx < 16 Then Hx = "0" & Hx ' Msg = Msg.RichTextBox1.0 – David Ross Goben NOTE: Rather than depend on using the System. False. "smtp. or a Linefeed).Text. and any other hex digits that are formatted as a Hex-Tag. charset=us-ascii  ▬▬▬▬▬▬▬▬▬▬ This attachment is an alternate view. A rough example of an interim solution follows: Msg = Msg. which must be removed along with the vbCrLf following [email protected]("=" & Hx. you will see the (=) Hex-Tags decorating the following RTF message block. _ System. we will later provide a method to encode those 8-bit codes to Hex-Tags.RichText) 'send our email as plain text and with a rich text alternater view SendEMail("[email protected]. rtfView) And now.com". Chr(idx)) Next Me.com To: david.ross.Replace("=0A".Replace("=0D". 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. .com Date: 22 Feb 2011 20:56:39 -0500 Subject: Test Content-Type: multipart/alternative. as long as your recipient’s email software can use that information. _ System. "="c) Hence.Rtf = Msg. because a NAME parameter is not specified. vbCr).Replace("=" & vbCrLf.}}=0D=0A{\colortbl= to control codes to ensure total original media content recovery. as opposed to normal spaces between words. we could filter it to a more perfect form using the following general “quick-conversion” code: Me. which are inserted in places where it would otherwise be removed or ignored. With the above gained knowledge.} \viewkind4\uc1\pard\cf1\f0\fs24 This is a \b test \b0 of \i Rich Text data\i0 .goben@gmail. So we end up where we were. charset=us-ascii Content-Transfer-Encoding: quoted-printable This is a test of Rich Text data.ross.80micro. we will try transmitting our email again: 'create an alternate view of the RTF data from our RichTextBox Control Dim rtfView As Mail.Text. the above RTF data becomes perfectly formatted to its original form: {\rtf1\ansi\ansicpg1252\deff0\deflang1033{\fonttbl{\f0\fnil\fcharset0 Times New Roman.com".Enhancing Visual Basic .}} {\colortbl .}=0D=0A\viewkind4\uc1\pard\cf1\f0\fs24 This is a \b= test \b0 of \i Rich Text data\i0 .Net. encoded to “=3D”. Hex-Tags were added New Roman.Replace("=" & vbCrLf. " "c).RichTextBox1. which represents a non-breaking space.\red0\green0\blue0. or a Carriage Return.Net.=0A ----boundary_0_12390f25-c489-470a-a36a-c3eb5dc04ad9 ▬▬▬▬▬ End of message body. which we will inevitably encounter. you can simply supply your own string.NET Beyond the Scope of Visual Basic 6. will have to be translated back to their original codes (the fact that the encoding reported quoted-printable should clue you in to that). "Test". ironically.Mime.ASCII. Me.\red0\green0\blue0. {\rtf1\ansi\ansicpg1252\deff0\deflang1033{\fonttbl{\f0\fnil\fcharset0 Times=  ▬▬▬▬▬ Because the text is encoded Quoted-Printable.com".Rtf = Msg 'clean up line termination tags 'process the whole ASCII gambit (=00 never used) 'convert to hex value 'leading zero if less than 16 (1-9.AlternateView = Mail. unless we choose to implement code to slog through the data that could easily chew up a lot of resources while it processes.Replace("=20". boundary=--boundary_0_12390f25-c489-470a-a36a-c3eb5dc04ad9 ----boundary_0_12390f25-c489-470a-a36a-c3eb5dc04ad9 Content-Type: text/plain. "david.\par=0D=0A\pard\cf0\f1\fs17\par=0D=0A}= =0D=0A ----boundary_0_12390f25-c489-470a-a36a-c3eb5dc04ad9— In the above Rich Text data. Nothing). They are used to mark a ‘soft’ line return.RichTextBox1. or even as 7bit. For example. start of an attachment block. and “=0A” represents Hex(&HA). dictated by its requirements.Rtf. if a string variable named Msg contains the above lines of RTF data. vbLf). “=0D” represents Hex(&HD).\par \pard\cf0\f1\fs17\par } But one thing it does not do is take care of other possible hex codes.Encoding.Replace("=3D". an equals sign “=” precedes each physical end of line. Content-Transfer-Encoding: quoted-printable  ▬▬▬▬▬▬▬▬▬▬ Because data is encoded Quoted-Printable. the hex code for “=”.Mime. Content-Type: text/richtext. as you can see.}{\f1\fnil\fcharset0 Microsoft Sans Serif. not by the original text. You may also see a lot of “=20” tags. MediaTypeNames again to set it. You have to go somewhere else first. and objects seem to always have a lot of properties. where we will cover E-Z decoding of Quoted-Printable and Base64 data. and this specification always renders all of its non-displayable ASCII tokens as displayable Hex-Tags. considering everyone who has been complaining about this on the internet. I first thought about UTF-7..com". Unknown.Text. So what I really need is un-encoded 7-bit US-ASCII support. so not one character of that data is lost. However.com". That was when I considered that an AlternateView and an Attachment. If we look under . I did not expect that they would be so easy to find. and the other was TransferEncoding (the little hairs on the back of my neck are starting to rise).RichTextBox1. On the internet.com Date: 25 Feb 2011 14:22:46 -0500 Subject: Test Content-Type: multipart/alternative. boundary=--boundary_0_f3de9980-d92b-423b-84a7-cd77f3a35def Page –248– . Unix is King.Enhancing Visual Basic . Hence. I mentioned something about the StringBuilder object that was 200 times faster at string manipulation. is an object.NET.CreateAlternateViewFromString(Me.RichTextBox1.AlternateView. even though now we often think of it in terms of 8-bits (256 codes).com To: david. and many (ancient) versions of Unix use only standard ASCII. this time using the following code: 'create an alternate view of the RTF data from our RichTextBox Control Dim rtfView As Mail. where server standards must conform to the least-common-denominator. due to Quoted-Printable encoding. which is a true 7-bit ACII. "smtp. After a little exploring. but then I realized that the same result would follow.. offering me a choice of QuotedPrintable (0). ASCII refers to 7-bit code (128 character codes).Text. And even better.MediaTypeNames. according my references. though.ross.NET Beyond the Scope of Visual Basic 6.Rtf) rtfView. False. But the important thing was that I now had exactly what I need. spits only venomous messages of non-support into our eyes if we try to play with it.goben@gmail. Base64 (1).TransferEncoding. they are properties that we can both read from and write to). rtfView) The results were spectacular. TransferEncoding was an integer enumeration. “·”. 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.Net. We will explore such support in our next section.goben@gmail. the ASCII encoding class actually does support 7-bit ASCII. I could use System. You may also want to apply SevenBit encoding to HTML and Plain Text data. I figured out that all I had to do was set the MediaType string property of ContextType.ContentType. Hmm.80micro. Eureka! I instantly discovered two members that were perfect to my purpose: The first was ContextType. A fourth option.RichText 'format our new view as rich text. the un-encoded 7bit option is exactly what I need. So I took a look at Attachment and AlternateView properties. 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. as long as they do not contain any embedded 8-bit coding.SevenBit 'and send unencoded as 7-bit ASCII 'send our email as plain text and with a rich text alternater view SendEMail("mercedes_silver@80micro. they were both malleable properties (that is.NET. "david. like everything else in .Mime.com".TransferEncoding = Mime. But the good news is – I can use the SevenBit option! So I try again. rtfView. so that the data can be transmitted over the internet as 7-bit ASCII data and can afterward be fully restored. Me. But let us stop what we are doing and simply think about ASCII-type encoding for a moment. such as Chr(149). and what resources they do use will get flushed away in a flash when we leave a method.AlternateView = Mail. Consider my final result: From: [email protected] = Mime. the text will be forced to being encoded to Base64. so some of you may be wondering why your 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.” I would have to think outside the box I had worked myself into. This manipulation uses little resources. or SevenBit (2). But that is all because of the immense presence of the Microsoft Windows environment. It was like getting directions from someone in the beautiful State of Maine: “You can’t get there from here. even if you send a plain text message.ross. but the “plain text” contains some 8-bit codes.0 – David Ross Goben But wait a minute! If you recall from an earlier article. "Test". comcast.juno.}} {\colortbl .live.com (Port 995) POP SSL required? Yes User name: Your Hotmail name.mail. or 587 for TLS) Authentication required? Yes (this matches your POP username and password) SMTP TLS/SSL required? Yes (If No. SSL settings. or 587 for TLS) Authentication required? Yes (this matches your POP username and password) SMTP TLS/SSL required? Yes GMAIL SMTP/POP3 Info: NOTE: Some Cox servers use pop. because sometimes these things are set up only during the Logon process.net Password: The password you usually use to sign in to Yahoo SMTP server: smtp. etc. use SMTP Port 25) POP server: pop. NOTE: TLS is usually an option available for smart phones. Page –249– . Use a web search for something like “pop3 xxx settings”.com (Port 465) Authentication required? Yes (this matches your POP username and password) SMTP TLS/SSL required? Yes Most other servers SMTP/POP3 Info (Verizon. The above is the typical settings for “Plain” (unsecured) email. For example [email protected]/support/index. or 587 for TLS) Authentication required? Yes (this matches your POP username and password) SMTP TLS/SSL required? Yes Juno SMTP/POP3 Info: POP server: pop.gmail.net (Port 995) POP SSL/TLS required? Yes User name: Your email name.cox.com (Port 995) POP SSL required? Yes User name: Your Gmail address. Typical Email Server Specifications: Comcast SMTP/POP3 Info: COX SMTP/POP3 Info: POP server: mail.cox. it is probably best to let the system take care of the encoding details. For example yourname from yourname@yahoo.\red0\green0\blue0. etc.juno. you would have to do that on a computer that already has active internet access).comcast.xxx. POP server: pop. and many new servers will even insist on using TLS ports for even desktop computers subscribers. it was most fascinating learning a great deal about formatting email data in different ways. For example yourname from [email protected] Password: The password you usually use to sign in to Comcast SMTP server: smtp. etc. For example yourname from yourname@hotmail. Refer to the link in the note.com (Port 995) POP SSL required? Yes User name: Your email name.=0A  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Note the vbLf terminator here in the plain text ----boundary_0_f3de9980-d92b-423b-84a7-cd77f3a35def Content-Type: text/richtext.mail. after all this work. All things considered.cfm?docid=95 for others. it may be a good idea to also re-logon to your computer. or how to select a User Name. plus “east” and “central”.NET Mail interface.Enhancing Visual Basic . on the other hand is encrypted for security.gmail.com (Port 587) Authentication required? Yes (this matches your POP username and password) SMTP TLS/SSL required? Yes Yahoo! SMTP/POP3 Info (paid service): POP server: pop.} \viewkind4\uc1\pard\cf1\f0\fs24 This is a \b test \b0 of \i Rich Text data\i0 . 7bit indicating NO ENCODING) {\rtf1\ansi\ansicpg1252\deff0\deflang1033{\fonttbl{\f0\fnil\fcharset0 Times New Roman.net server) User name: Your email name.com (Port 25 (default)) Authentication required? Yes (this matches your POP username and password) SMTP TLS/SSL required? No NOTE: The above settings will also work with Comcast.com (Port 465 for SSL.west. making less work for us.com Password: The password you usually use to sign in to Hotmail SMTP server: smtp.net (Port 995) POP SSL required? Yes (If No. charset=us-ascii Content-Transfer-Encoding: quoted-printable This is a test of Rich Text data.com (Port 110 (default)) POP SSL required? No User name: Your email name. we would see the result shown on the right.xxx.com Password: The password you usually use to sign in to Juno SMTP server: authsmtp.yahoo.live. NOTE: If you change your email settings.0 – David Ross Goben ----boundary_0_f3de9980-d92b-423b-84a7-cd77f3a35def Content-Type: text/plain. EarthLink.com (Port 995) POP SSL/TLS required? Yes User name: Your email name.com Password: The password you usually use to sign in to Google and Gmail SMTP server: smtp. mobile PCs.): POP server: pop. and how to set up a Password (veteran email users can often do this on their own without pause).net (Port 465 for SSL. AOL.comcast.yahoo. Central. But even so.com (Port 465 for SSL.com Password: The password you usually use to sign in to your server SMTP server: smtp. East. and West COX servers.net (Port 465 for SSL.. and provider addresses with your provider.juno. charset=utf-8  ▬▬▬▬▬▬▬▬ Note text/richtext Content-Transfer-Encoding: 7bit  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Note 7bit encoding (or rather. NOTE: Always check and verify port numbers. though using your provider in place of xxx (obviously. to ensure the new ensure settings will work with the . Such as ATT. and to verify your selected.net Password: The password you usually use to sign in to Cox SMTP server: smtp. For example yourname from yourname@xxx. For example yourname from yourname@comcast. use POP3 Port 110 and pop3.\par \cf0\f1\fs17\par }  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Blank line is due to an additional embedded vbCrLf code in the Rich Text data ----boundary_0_f3de9980-d92b-423b-84a7-cd77f3a35def— Were we to plug the above raw Rich Text data without modification into the Rtf property of a RichTextBox. below. though it is very helpful to have alternate views that are readable. NOTE: Look to www. or 587 for TLS) Authentication required? Yes (this matches your POP username and password) SMTP TLS/SSL required? Yes HOTMAIL SMTP/POP3 Info: POP server: pop3. SSL/TLS email. NetZero.}{\f1\fnil\fcharset0 Microsoft Sans Serif. For example yourname from [email protected] Beyond the Scope of Visual Basic 6.net. though using TLS ports with SSL usually works.defcon-5. See RFC 2045 Section 6. When there is confusion. Specifies that the Image data is in Joint Photographic Experts Group (JPEG) format. All Text encoders default to this (US-ASCII. Based on the kind of conversion we specify for encoding. Also.NET MediaTypeNames class strictly follows the RFC 2045 specification. or the data is not 7-bit text. The goal is never to lose original content. but it is likely another email processor except your own will support it.NET how to transmit these instructions? What if I can barely understand “application/octet-stream. it will be encoded as QuotedPrintable as long as we specify a text-based method from Encoding.NET MIME processor has two built-in members that will help you easily resolve the Content-Type and Content-Transfer-Encoding issues. Specifies that the Application data is compressed.NET Beyond the Scope of Visual Basic 6. or any code you do not want to deal with control-code conversion Hex-Tags.txt. Maximum line length is 75 characters. If we leave this field blank. Specifies that the Image data is in Tagged Image File Format (TIFF).8. Best for Binary. Specifies that the Application data is in Portable Document Format (PDF).Mime. and that is why we tend to get Base64 as the Content-Transfer-Encoding type a lot on Attachments and Alternate Views. Therefore.org/rfc/rfc2045. A fourth. it is more efficient and has much less data loss than Jpeg. NOTE: RFC 2045 can be found at www. For example.Application type exposes the following default members: Member: Octet Pdf Rtf Soap Zip Context-Type Field Reports: application/octet-stream application/pdf application/rtf application/soap+xml application/zip Description: Specifies that the Application data is not interpreted (an Octet-stream (Byte-stream)). HTML. (2) Used for data that is not encoded. The . but should never be used: Member: QuotedPrintable Base64 SevenBit Unknown Description: (0) Encodes data that consists of printable characters in the US-ASCII character set.Net.Image type exposes the following default members: Member: Gif Jpeg Tiff Context-Type Field Reports: image/gif image/jpeg image/tiff Description: Specifies that the Image data is in Graphics Interchange Format (GIF).Mime.” let along spell it consistently? And what in blue blazes is Base64 encoding. ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬► Stored in email as “7bit”.TrasferEncoding enumeration. is presently defined.MediaTypeNames. But where does all this come from? Where and how can we tell VB. which in fact documents the IP Encapsulating Security Payload (ESP). UTF7. The MediaTypeNames. (MIME) Part One: Format of Internet Message Bodies.Mime. but the data is actually 8-bit. and the second helper is the System.Net.7. See RFC 2045 Section 6. RFC 2045 refers correctly to Multipurpose Internet Mail Extensions. ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬► Stored in email as “base64”. Page –250– . Specifies that the Application data is in Rich Text Format (RTF). etc. it can be expected to be processed as Base64. the parameters noted for the Content-Type and Content-TransferEncoding fields are of paramount importance! If the wrong information is provided. or Rich Text. ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬► Stored in email as “quoted-printable”. The .Enhancing Visual Basic . such as Plain Text.Net. NOTE: I hope they add Png to the specification. the user’s email application can get confused. we look to System. The first helper is the System. The data is in 7-bit US-ASCII characters with a total line length of no longer than 1000 characters. the MSDN documentation actually erroneously references RFC 2406. For specifying the data for the Content-Type field.).ietf. Unknown. (1) Encodes stream-based data. Specifies that the Application data is a Simple Object Access Protocol document (SOAP). the transmitter will supersede your option and specify Base64 encoding. MediaTypeNames specifies 3 types of interest.ContentType class. you can. See RFC 2045 Section 2.0 – David Ross Goben PART TWO Encoding and Decoding Email Data When we send and receive email. coming from the searing blue flame given off by petroleum gasses. which are most notably useful for regular attachments: Member: Application Image Text Description: Specifies the kind of application data in an email message attachment or AlternateView Specifies the type of image data in an email message attachment or AlternateView Specifies the type of text data in an email message attachment or AlternateView The MediaTypeNames. the Content-Transfer-Encoding mechanism will reflect one of three values. if you specify a Quoted-Printable-class encoder. Maximum line length is 75 characters. If you want to specify other sub-types. it is required to default the Content Type of “application/octet-stream” (a binary stream) and “Base64” for Content-Transfer-Encoding for safety. UTF8. if we are passing ASCII data.7. anyway? NOTE: Blue Blazes refers to very hot Hell fire. (-1) This is currently not supported and will never be selected by the SMTP server. or for allowing them to explicitly specify them. Base64)”. or allow them.Net.exe (application/octet-stream. consider having to hand-type “C:\My Files\API32.NET Mail object. like “C:\telnet. The easier you make it for them.ApplicationSoap Return "application/soap+xml" Page –251– . However. Specifies that the Text data is in Rich Text Format (RTF). to specify both parts themselves from dropdown ComboBox controls.NET Beyond the Scope of Visual Basic 6.Mime. Specifies that the Text data is in XML format.Text type exposes the following members: Member: Html Plain RichText Xml Context-Type Field Reports: text/html text/plain text/richtext text/xml Description: Specifies that the Text data is in HTML format. Specifies that the Text data is in Plain Text format. like the following: "David Ross Goben" <david.ApplicationRtf Return "application/rtf" Case MediaTypes. the options you offer your email application users can differ. 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. rather than just their full email addresses.ApplicationPdf Return "application/pdf" Case MediaTypes.com>. nor is adding parameterized declarations for an attachment. you will need to have a mechanism in place for either interpreting their Attachment/Alternate View choices.txt (text/plain. such as adding trendy features like specifying email recipients by name.Enhancing Visual Basic . and even more so for a binary file. the more trust and loyalty they will have for your products. though I would imagine that you would customize the code to your needs. Allowing Users to Specify Content-Type and Content-Transfer-Encoding Options Of course. a string representing ' : the selected type will be returned. Ack! It would of course be more convenient to allow them to select the files from a browser and for you to either interpret their attachment and determine how best to transmit them (this is not as difficult as you might think). or can even be similar to the previously mentioned tables.MediaTypes text ' : ' Returns : provided a MediaTypes enumeration value.ross. Sure. This makes selecting recipients more convenient. '******************************************************************************* Public Function GetMediaType(ByVal MediaType As MediaTypes) As String Select Case MediaType Case MediaTypes. but it is not very convenient for your users if they wish to specify these things.0 – David Ross Goben The MediaTypeNames. this all well and good when you are programming that code. such as using a current naming format that is popular and is already supported by the . If you refer to the SendEmail() method comments. through an optional advanced user option. and then you would simply wrap them up in a manner recognizable by the SendEmail() method. For example. Just do not make it so brain-dead-simple that you begin to restrict them in their options. you will notice that the specification for an Alternate View showed you how to stipulate a different Content-Type and Content-Transfer-Encoding type from the default by using VB programming methods. SevenBit)”.goben@gmail. Application. Since an Alternate View object’s MediaType parameter (MyAlternateView.0 – David Ross Goben Case MediaTypes.TrasferEncoding.Mime.ImageJpeg Return "image/jpeg" Case MediaTypes. that it will not be forced to Base64. Ultimately.TextXml Return "text/xml" Case Else Return "application/octet-stream" End Select End Function For options to the user.ContentType.ImageTiff Return "image/tiff" Case MediaTypes. email-friendly Content-Type text. presented to them in prettier or simpler text. Image. such as the text displayed within the enumeration in a ListBox or ComboBox control.TrasnferEncoding.ImageGif Return "image/gif" Case MediaTypes.TextHtml Return "text/html" Case MediaTypes. you can do the same thing for your users.Net. if it ends up containing any 8-bit data.MediaType) expects a string. which will quickly determine if the code contains any 8-bit data: Page –252– . a TransferEncoding value ' : is returned.Mime. and Text. Simply ensure that the final values match those listed in the comments for their integer values.Net. such as “Application/Octet” or even “Binary”. or the lack thereof for text. unless that is what you want to happen.Net.ApplicationZip Return "application/zip" Case MediaTypes. Base64. or by first separating the three types. or.TransferEncoding) End Function Using techniques similar to those already outlined for GetMediaType(). The methods for how to do this are many. System. MediaTypes)”) to the GetMediaType() function. '******************************************************************************* Public Function GetTransferEncoding(ByVal TransferEncoding As TransferEncodings) As System.TextRich Return "text/richtext" Case MediaTypes. just use the DirectCast() method to cast their ComboBox selection index (0-2) to System.Net.Net. you can first scan it with the following little function.TransferEncoding data ' : ' Returns : Provided a TransferEncodings value. Also. If not. you can present those options to them however you wish (or they wish).TransferEncoding Return DirectCast(TransferEncoding.TrasferEncoding) expects an integer cast to System. 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. you are the one who must specifically inform the SMTP server of the type and encoding. allowing them in optional advanced options to specify how they want their data to be encoded. or 7Bit When you are preparing to specify coding.NET Beyond the Scope of Visual Basic 6. you need to ensure that if you are going to submit it as using as Quoted-Printable or 7Bit. setting it is easy – you just supply it with the string you got from the GetMediaType() function. you can simply provide it with the value returned by the GetTransferEncoding() method.Mime.TextPlain Return "text/plain" Case MediaTypes. using something like “DirectCast(Value. because the Alternate View’s Transfer Encoding (MyAlternateView.Enhancing Visual Basic . You can then supply this integer value (or first cast it. which will return the appropriate.Mime. Determining if Text can be Sent Encoded As Quoted-Printable.Mime. and then present secondary subtype lists for each. For simplifying Content-Transfer-Encoding. that all code in the HTML text is 7-bit. the default . Is < 0 'if 8-bit or unicode code Sb. the source contains 8-bit data ' : and will be encoded by server. you can either encode it using Base64. even though it is permitted) using the ForceQuotedPrintable() function. but the HTML ' : souce code will no longer carry an actual 8-bit value. you can pass the HTML text through the Force7BitHtml() function. listed further below. and that any 8-bit or 16-bit code that was in it is dutifully converted to 7bit HTML Entity Numbers.Append("&#" & C. The ForceQuotedPrintable() ' : method performs essential conversions for non-HTML text. If you are passing the function Rich Text or Plaint Text. ' : even if only a single byte is 8-bit. If such ' : code had not been corrected. For example. Idx.UTF8. it returns a boolena flag.Enhancing Visual Basic . listed below.Append(ChrW(C)) 'else save text regardless End Select Next Return Sb. the encoding of the data would be ' : forced to change from quoted-printable to Base64.". then they are converted into a special ' : 7-bit HTML Entity Number. upon return. because that ' : would be the only way the email processor could guarantee that ' : the email text was fully intact.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 you are processing HTML text.ToString End Function Converting 8-Bit Text Data to 7-Bit for Sending without Data Loss If you test a text message with the TextNeedsEncoding() function and it comes back True. you can convert it to a HexTagged 7-bit text format by passing it through the ForceQuotedPrintable() function. listed here: Page –253– . but this ' : would be best served in Attachments and Alternate Views. ' : ' NOTES : If text data contains 8-bit values.NET ' : SMTP processor will force this code to be encoded to Base64. you can specifically Hex-Tag its 8-bit characters (something that the default Quoted-Printable encoders will not do. ' : ' Returns : Provided a source string. 1)) 'get a single character from the source Select Case C 'check each character Case Is > &H7F. if you want it to remain readable in raw format. which will ensure.ToString & ". ' : ' NOTES : If any characters in an HTML text string are 8-bit (values ' : greater than 127). or.NET Beyond the Scope of Visual Basic 6. ' : ' Returns : Provided a string containing HTML code. or Plain Text requires ' : 8-bit code translation to 7-bit Quoted-Printable tags. ' : If the returned value is true. 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. ' : ' : To avoid this. '******************************************************************************* 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. 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. it will return a string ' : containing HTML code that does not have any 8-bit data embedded.0 – David Ross Goben '******************************************************************************* ' Function Name : TextNeedsEncoding ' Purpose : Determine if HTML text. '******************************************************************************* Public Function TextNeedsEncoding(ByVal Message As String) As Boolean Dim Byt() As Byte = Encoding.") 'convert to 7-bit HTML ecoder Case Else Sb. which will ensure ' : that it will still be displayed on the HTML page. code 149 (•) is an 8-bit ' : value that can be changed to HTML "&#149. Rich Text. and the returned string is 7-bit. less initial null byte ' : Else ' : Return Result 'otherwise. You can simply check for this in your email processor. ' : you will have to use the DecodeQuotedPrintable() method to convert ' : it back to its original text form. and so their users will still see any Hex-Tag encoding that the ForceQuotedPrintable() method had placed in there.UTF8. Further.ToString End Function The ForceQuotedPrintable() method uses the same encoding tags for 8-bit code that are used by QuotedPrintable encoding for control codes. but the text data will no longer carry an actual ' : of the data would be forced to change from quoted-printable or 7bit ' : to Base64. but the internal SMTP processor will not know that these are Hex-Tags. you can also ‘encode’ this data in an attachment or alternate text as 7Bit. You may notice that the ForceQuotedPrintable() method leads the result text with “=00”. without data loss. so decode again and return. to see if you will still need to decode it back to 8-bit text. if you also pass it as Quoted-Printable. ' : ' : The Encoded text will begin with "=00". you would know that you would need to ' : double-decode the text. This will decode to “=95”.Enhancing Visual Basic .GetBytes(Message) 'convert message to byte array Dim Sb As New StringBuilder("=00") 'set up string builder for appending data. because it would not expect them. ' : ' Returns : Provided a source string that contains 8-bit data. because that would be the only way the email processor ' : could guarantee that the email text was fully intact. This is discussed in more detail in the next topic.SubString(3)) 'yes. which DecodeQuotedPrintable() would ' : convert back to "=xx". as shown in the above comments. you can decode it a second time. Of course. ' : ' NOTES : if any characters in a text string are 8-bit (values greater ' : than 127). and then the “=95” will in turn have to be decoded in a second pass (&H95 is 149 decimal.0 – David Ross Goben '******************************************************************************* ' Function Name : ForceQuotedPrintable ' Purpose : Force 8-bit code in a text message to 7-bit. the first time to decode the “=” tags added by the internal SMTP processor. and a second time to decode the tags that you had added using the above method prior to SMTP encoding the data as Quoted-Printable. However. so passing through a second time would ' : properly convert the additional encoding. but you should still check for the leading “=00” tag. this means that you will have to decode it twice.Append("=" & Hex(B)) 'convert to 7-bit tag Case Else Sb. so it would encode “=95” to “=3D95”. then they are converted into special 7-bit tags. Page –254– . 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. A second pass would be required. would be ' : reinterpreted as "=3Dxx". Init with "=00" For Each B As Byte In Byt Select Case B 'check each byte Case Is > &H7F 'if 8-bit code Sb. 3) "=00" Then 'tagged as pre-encoded? ' : Return DecodeQuotedPrintable(Result. 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). by checking the ' : text startiing with "=00". However. where the “=3D” is the equals sign encoded (this symbol has a hex ASCII value of &H3D). code 149 (•) is an 8-bit value that can be changed ' : to hex "=95".NET Beyond the Scope of Visual Basic 6. and represents the “•” symbol). return result of decoding ' : End If '******************************************************************************* Public Function ForceQuotedPrintable(ByVal Message As String) As String Dim Byt() As Byte = Encoding. ' : For example. and if found. the 8-bit ' : data is converted to Hex-Tags. and all the "=xx" byte-translations. ' : because if this translated code was afterward encoded as Quoted' : Printable.SubString(0. NOTE: Remember that other email processors will most-likely not have this kind of arcane knowledge for functionality. because that would be the only way the email processor ' : Base64. Because unencoded null codes ' : are not permitted in email data. which will ensure that it will still be displayed ' : in the text.Append(Chr(B)) 'else save text regardless End Select Next Return Sb. Also. This can be done because the above encoder may add Hex-Tags for 8-bit values. which. and can be decoded to Page –255– . "=0A" to vbLf. and "=3D" to "=". Chr(Idx)) 'replace hex data with character code (SHIFT is faster) Next For idx As Integer = &H10 To &HFF 'process the whole 8-bit extended ASCII gambit Msg.Enhancing Visual Basic . On top of that. Regardless. soft line-wraps were added by placing a ‘=’ at the end of a forced returned line. ' : This should be invoked for all data coded Quoted-Printable. and what resources that are used will be instantly ' : flushed when the method exits. but with some Umph! (And why does their breath smell like fish bait? Or was that restrained. except maybe extremely simple 1-line notes. or their original control codes (< &H20). providing you with pristine text data that was formatted exactly as it had been provided to the server. converting it to Text or a binary format is not difficult at all. which will very quickly ' : do a replacement of all control code translations using fewer ' : resources. I see it more as a “when you find out that you are at 72-75 characters. or anticipating breath?) Base64 represents binary (or 8-bit) data in an ASCII string by translating the data into a radix-64 format. vbNullString). vbCr). ' : ' NOTES : Typical cleaning involves changing "=0D" to vbCr. we often lost.Replace("=0D". because there was an imposed 75-character line limit. vbNullString)) If QuickClean Then 'perform a quick clean (clean up common basics) Return Msg. for those of you who have been waiting with bated breath for a definition.Replace("=" & Hex(idx). run all Quoted-Printable encoded data through the following DecodeQuotedPrintable() method to render them into their original format.Replace("=0A". '******************************************************************************* ' Function Name : DecodeQuotedPrintable ' Purpose : Method to clean typical control translations. plus a Find method to emulate Instr()-type functionality would also be nice. '******************************************************************************* Public Function DecodeQuotedPrintable(ByVal Message As String. or a detailed cleaning in just about an instant.Replace("=20". and is laid out much like a Lucky Charms™ secret decoder ring.ToString 'return result string End If End Function The above function will execute much faster than the conversion method demonstrated a much earlier. which require a leading 0 Msg. then do a forced soft line wrap”. _ vbLf). if at the end of a line. plus any line wrap ' : terminators at the end of lines to vbNullstring.NET Beyond the Scope of Visual Basic 6. but I wish a StringBuilder could do blind replacements (MID()-like operations) so I could make it even faster. Basically. A special case was spaces “=20”. ' : ' Returns : Provided a raw message string block. so they were encoded.Replace("=" & Mid(HxData. it returns a decoded string. " "). The DecodeQuotedPrintable() method reverses that. or all of them. Base64. Translating Base64 Data Back to Its Original Format For all the trouble I had earlier with Base64. All text encoded using the Quoted-Printable method will have Hex-Tag encoding for any control codes. ' : ' : A StringBuilder object will be used.ToString Else 'perform total cleaning 'store 2-character hex values that require a leading "0" Dim HxData As String = "X0102030405060708090A0B0C0D0E0F" For Idx As Integer = 1 To &HF 'initially process codes 1-15. "="). Idx << 1. This is to guarantee that the data remains fully intact without modification during transport. 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. is really simple enough. ' : "=20" to a space. including the line terminator. 2).0 – David Ross Goben Decoding Quoted-Printable Text Quoted-Printable text has a line length limit set at about 75 characters.Replace("=" & vbCrLf.Replace("=3D". Various forms of this are commonly used in applications when there is a need to encode binary data that must be stored and transferred over media that are designed to deal with strictly 7-bit text data.Replace("=" & vbCrLf. A-F) back to their original 8-bit character codes (> &H7F). plus it can perform an optional “quick clean” for minor jobs. Chr(idx)) 'replace hex data with single character code Next Return Msg. what this method does is convert the Hex-Tags with their 2-character hexadecimal digits (0-9. Were we to look at any bytes in radix-2 (Base2. The value 17 corresponds to the ASCII letter “R” in the previous Base64 table. 2. &H76. 6-bit blocks for Base64. 0101. it would be &H44.NET Beyond the Scope of Visual Basic 6. Second. it would look like a string of 8 ones and/or zeros. is represented by the decimal ASCII codes 68. &H61.Enhancing Visual Basic . Because of their compatability. That is. as used by MIME. where the position values from left to right are 8. So if we take the first 6 bits of the 8-bit value for the letter “D” (0100|0100). and 1. So examine this long stream of bits in that light: You can compare the bit patters to the upper. and should not be always divisible by 3. and 1111 in Binary. 97. where a plain ASCII character is used to represent a numeric index: Each byte of text is composed of 8 bits. each letter of my name. we padded the 8-bit data out 2 extra bits. 105. or 00|0000 to 11|1111 binary). and &H64. For example. which yields 4 Base64 values for each 3-byte group. and BinHex by the TRS-80 (this TRS-80 format was later adapted for the Macintosh). we get 0100010001100001011101100110100101100100. 1110. Together. Char Index Char Index Char Index Char Index A 00 Q 16 g 32 w 48 B 01 R 17 h 33 x 49 C 02 S 18 i 34 y 50 D 03 T 19 j 35 z 51 E 04 U 20 k 36 0 52 F 05 V 21 l 37 1 53 G 06 W 22 m 38 2 54 H 07 X 23 n 39 3 55 I 08 Y 24 o 40 4 56 J 09 Z 25 p 41 5 57 K 10 a 26 q 42 6 58 L 11 b 27 r 43 7 59 M 12 c 28 s 44 8 60 N 13 d 29 t 45 9 61 O 14 e 30 u 46 + 62 P 15 f 31 v 47 / 63 NOTE: Each letter of a hex value – 0 to F (15 decimal) – represents 0000. If we look at this stream as a table that is sectioned off at 8 bits. &H69. has a 64-character set defined in the table shown to the right. so that we could accommodate the 6-bits required for the last Base64 value. Base64-type ecoding for an uppercase-only. Explaining this makes it seem complicated. 01101001. so the conversion routine implements padding to fill it out. 01100001. the first 2 bits comprise the first hex value (0-3). symbol-rich character set was implemented as uuencode by UNIX. It pads the data by adding 1 to 3 ‘=’ symbols at the end of the encoding run (this is why you will frequently see them trailing Base64-encoded data). or hexadecimal. or Binary). Thus 1000 = 8. First. David. For example. it is less confusing. 0001. 1100. or 17. 1010. 01110110. or 00-3F hex. 0111. 0011. 0100. 1000. Page –256– . 0100 = 4. 0010. Because of this. but strangely. which uses only 6 bits (values 0-63. which can be computed using the Hex() function. but doing it is easy (even easier if we let the system do it!) You might notice other things. and 01100100. which is 01|0001. we get 11 Hex. 8-bit blocks for Base256. 0010 = 2.0 – David Ross Goben 100% of its orginal form. and 0001 = 1. what Base64 conversion does is process the original 8-bit values in groups of 3 bytes at a time. In hexadecimal. Rather confusing. These are represented in binary streams 01000100. However. and 100. 118. 1011. The Hex index takes 6 consecutive bits and converts them to Base16. you may notice that 3 8-bit values fit nicely into 4 6-bit values (24-bits total). it is actually quite easy to translate in your head… once you know hexadecimal. 4. and the remaining 4 bits comprise the second hex value (0-F). 1001. and less at 4 binary bits. 0110. it is clear that the original data cannot. 1101. or 1 x 16 + 1. But we are more interested in breaking this up into Base64. This type of encoding was needed to initially support dialup communication between systems running different operating systems. Base64. I know. and the lower. a messaging application could make better assumptions about what characters were safe to use. except that the stream remains entirely in-memory: b64Data = b64Data. charset=us-ascii Content-Transfer-Encoding: quoted-printable This is a test of Rich Text data.PictureBox1. and the accentuated and prolonged stinging on my hindquarters informed me that this was an inappropriate course of action. UBound(Byt) + 1) sw. 0. AlternateView attempts for my Rich Text data: From: [email protected]() 'remove artificial email line terminators from b64Data string 'convert b64Data string from Base64 to a byte array of ASCII values 'convert byte array to a memory stream 'import the memory stream to an image object 'close the memory stream (automatically invoke Dispose) Page –257– .com Date: 22 Feb 2011 17:19:08 -0500 Subject: Test Content-Type: multipart/alternative.JPG". book-less confines of Grandma’s sewing room.Image = Image. I did figure out how to sew. charset=utf-8 Content-Transfer-Encoding: base64  ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This blank line after the Content-Transfer-Encoding line is required. which means we are already halfway there! If we want to save this array of bytes to a file. boundary=--boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321 ----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321 Content-Type: text/plain.FromBase64String(b64Data) Me.RichTextBox1. will first check if it is the end of the data.Replace(vbCrLf.MemoryStream(Byt) Me.FromBase64String(b64Data) Dim sw As New System. unless I wanted to flip on the radio and catch up on pork-belly futures – but the good news was. and at the time presumed failed. and is not part of the message e1xydGYxXGFuc2lcYW5zaWNwZzEyNTJcZGVmZjBcZGVmbGFuZzEwMzN7XGZvbnR0Ymx7XGYw XGZuaWxcZmNoYXJzZXQwIFRpbWVzIE5ldyBSb21hbjt9e1xmMVxmbmlsXGZjaGFyc2V0MCBN aWNyb3NvZnQgU2FucyBTZXJpZjt9fQ0Ke1xjb2xvcnRibCA7XHJlZDBcZ3JlZW4wXGJsdWUw O30NClx2aWV3a2luZDRcdWMxXHBhcmRcY2YxXGYwXGZzMjQgVGhpcyBpcyBhIFxiIHRlc3Qg XGIwIG9mIFxpIFJpY2ggVGV4dCBkYXRhXGkwIC5ccGFyDQpccGFyZFxjZjBcZjFcZnMxN1xw YXINCn0NCg== ----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321-- If this data (highlighted in the above block) was stored in a string variable named b64Data. the first two lines of code will properly decrypt binary code from Base64 format to a binary array of bytes. warranting further personal review within the acetic seclusion of the TVless. which the Content-Type file may have informed us of by stating “Content-Type: image/jpeg. we could do this: b64Data = b64Data.FileMode. Stream all bytes '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. 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.NET Beyond the Scope of Visual Basic 6.Rtf = Encoding.Replace(vbCrLf.Close() 'remove artificial email line terminators from b64Data string 'convert b64Data string from Base64 to a binary byte array 'create destination file stream 'write file from Byte array. I actually did this on my grandparent’s farm.OpenOrCreate) sw.UTF8.ross.Replace(vbCrLf.=0A ----boundary_0_253a0455-dce6-4622-b262-ae5f1cfc0321 Content-Type: text/plain. but the decoder. and it was a JPEG file. vbNullString) Dim Byt() As Byte = System.FromBase64String(b64Data.Convert. which is just like a file stream.IO. we could then stuff it into our RichTextBox1 control using the following single line of code: [email protected](System.com To: david.GetChars(Byt) 'remove artificial email line terminators from b64Data string 'convert b64Data string from Base64 to a byte array of ASCII values 'convert 8-bit ASCII values in Byte array to a 16-bit Unicode String Array But what about translating binary data? Actually. toy-less.Write(Byt.FromBase64String(b64Data) Dim ImgStrm As New System. when grabbing values.UTF8. They are treated as 0 values and tossed away. name=NewImage.FromStream(ImgStrm) ImgStrm.JPG”. it will simply slog in the next set of 4 characters from the feeder stream.Replace(vbCrLf. vbNullString))) And that is it! But I think you might like to also look at what I did a little less cryptically: b64Data = b64Data.Enhancing Visual Basic . NOTE: Clodhoppers are sturdy ankle boots farmers use to protect their feet and ankles while walking through tilled fields.Rtf = Encoding. and if not.RichTextBox1. vbNullString) Dim Byt() As Byte = System. IO.Convert. vbNullString) Dim Byt() As Byte = System.Convert.Convert. Suppose we took the Base64-encoded data that was in one of my earlier.0 – David Ross Goben What these pad characters are used for is to pad out missing codes from the final 4-character Base64 grouping.FileStream("C:\MiscIO\NewImage. Considering the following method: '******************************************************************************* ' Function Name : DecodeBinHex ' Purpose : Decode a provided raw email message string that is encoded to BinHex.PictureBox1.Convert. using the Memory Stream technique shown above. finally that the DecodeBase64ToStr() method takes full advantage of the DecodeBase64ToBytes() method.Enhancing Visual Basic .Replace(vbCrLf. such as an image. PDAs. calling it by such derogatory terms as “Trash-80”. ' : ' Returns : Decoded String ' : ' NOTES : note that the lone vbCrLf at the end of lines is filtered out. such as saving it to a file. BinHex was a fix that just stuck.GetBytes(StrData. laptops.0 – David Ross Goben Or. vbNullString)) End Function The first function. DecodeBase64ToBytes(). you would be looking at a much different and more primitive world right now. such as the data provided in an email. '******************************************************************************* Public Function DecodeBase64ToStr(ByVal strData As String) As String Return Encoding. how about this one-liner that will do exactly the same thing: Me.NET Beyond the Scope of Visual Basic 6. It will automatically close when it falls out of scope.Replace(vbCrLf.ToUpper) Dim Result() As Byte 'init output buffer ReDim Result(UBound(Src) \ 2) 'set initial dimension to bytes defined in file Dim Index As Integer = 0 'init index for Result() array Page –258– . Notice. tablet PCs. DecodeBase64ToStr() is meant to process text data that has been encoded to Base64. Even the MacIntosh adopted it. Just supply the DecodeBinHex() function with a raw hex string. ' : ' Returns : Decoded String ' : ' NOTES : note that the lone vbCrLf at the end of lines is filtered out.Replace(vbCrLf. You can afterward process the byte array as you see fit.NET being a fully-functional object-oriented programming language. our smart phones. Translating BinHex Data Back to its Original Format BinHex was first written by Tim Mann for the TRS-80. which for some time did not support 8-bit data transfer. and most other gadgets. NOTE: Anyone who knocks the TRS-80 computer.UTF8. vbNullString)))) NOTE: Do not worry about closing the memory stream. vbNullString). and it will return a Byte array with the converted data. It is actually an inefficinet format. and thankfully strict scoping rules.GetChars(DecodeBase64ToBytes(strData)) End Function '******************************************************************************* ' Function Name : DecodeBase64ToBytes ' Purpose : Decode a provided raw email message string that is encoded to Base64. For those who discover that they have such data. because the data must always be first rendered to a Byte array. It was an essential technological platform that enabled you and me to be sitting here today with our PCs and our iMacs.FromStream(New System. returning a Byte array.FromBase64String(b64Data.IO. '******************************************************************************* 'this modification returns a Byte Array of the Base64 encoded source data Public Function DecodeBase64ToBytes(ByVal strData As String) As Byte() Return System. ' : ' Returns : Decoded binary Byte Array ' : ' NOTES : note that the lone vbCrLf at the end of lines is filtered out. for example). but is known to have a text result (perhaps by inspecting the parameter in the Content-Type field. which happens after the picture box received the image. but it was essential back in the mid-1980s to tranfer binary data over the internet by services like CompuServe.FromBase64String(strData. as we did in the previous example. '******************************************************************************* Public Function DecodeBinHex(ByVal StrData As String) As Byte() Dim Src() As Byte = Encoding. they can decode it quite easily.MemoryStream(System. is meant to process a binary data result. converting every byte into two. Consider the following two functions. and would have come along even if Tim had not written it.Convert.Image = Image. and finally having real. This is the real beauty with VB. or rendering it if it is a renderable object.UTF8. The second method. is speaking from a position of total ignorance. Had the relatively inexpensive TRS-80 failed and had not become a leader in the PC market and business. 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. it took me all afternoon to come up with my own VB.NET Beyond the Scope of Visual Basic 6. 2002 (www. and it used a lot of code where I needed only a little just to accomplish the same thing. mentioned earilier. Page –259– . last updated on 26 March. and we are going to do it with ease. submitted to The Code Project on 19 January. slow.NET using Native Methods The third part of the article will focus on receiving email into VB.48 If CL > 10 Then CL -= 7 Dim CR As Integer = Src(Idx + 1) . and he was just throwing out an example. 2 hex characters at a time 'Convert "0" . However.NET Framework is much easier now than it was back in the wild and wooly days of ‘ot 3 or even ‘ot 1. circa May 2002. Once you examine all the methods that we will use to define our email reading class.codeproject. it did not provide for SSL certificates. that if anyone can write an email processor that supports the RFC 2045 specification.NET solution. It implemented a very simple sample solution.codeproject. Randy sifted various C# solutions and tacked together his own.NET. you can use the returned array to save to a file.com/KB/IP/popapp. Plus. if that is its purpose.com/enus/library/ms973213.pdf. But because it was impossible.aspx).aspx). then they likely had sufficient knowledge to easily handle TCP Clients without an easier back end.NET. if you would like to convert to text. Bill Dean also made a 5 August 2003 C# submission to The Code Project (www.NET “gurus” will simply say that POP3 is impossible to do under VB. His example approach was very rudimentary (I almost said basic – no pun intended).com blog site (see “How to POP3 in C#” www.0 – David Ross Goben For Idx As Integer = 0 To UBound(Src) Step 2 Dim CL As Integer = Src(Idx) .kbcafe. But I am not knocking him.microsoft. Perhaps they figured. I must admit that after having fully developed this solution. and maybe rightly so. My solution was initially inspired by C# code pieced together by Randy Charles Morin. Most “solutions” simply direct you to a few samples of comment-challenged C# or C++ code.com/KB/IP/pop3client."F" to 0-F 'do the same for the right hex digit 'bump by 256 (allow for Index offset) 'stuff byte value 'bump index 'set array to final size 'return the final result Like with the DecodeBase64ToBytes() method. then you can convert the returned Byte array like this: Dim NewStr As String = Encoding. because none of the methods I present here are either long or complicated. which was inspired by Agus Kurniawan’s article on using C# to communicate with a POP3 server.UTF8. who was the Microsoft Visual Basic . 2003. or render it to an image.GetChars(DecodeBinHex(strData)) PART THREE Receiving Email Under VB.NET Content Strategist for MSDN.POP3. Most VB. not a full implementation. he was writing this for VB2002. if that was its origin.Enhancing Visual Basic . and it works better than any C# or C++ version I have yet seen. you may really start to wonder why Microsoft did not choose to make POP3 inbound mail processing as easily accessible as they did SMTP outbound mail.aspx). on MSDN by Duncan Mackenzie (see “Checking My E-mail” at http://msdn. But imagine the apps that would come out if they did? There are hundreds of thousands of programmer queries on the internet asking how to access POP3 from VB. I finally did find a solitary VB. it featured little functionality.48 If CR > 10 Then CR -= 7 If Index > UBound(Result) Then ReDim Preserve Result(Index + 255) End If Result(Index) = CByte(CL * 16 + CR) Index += 1 Next ReDim Preserve Result(Index .1) Return Result End Function 'scan the string.NET (and those are just the ones who took time out of their day to say they needed POP3 front end code).NET article. POP3.com/articles/HowTo. but my current link is to a recently updated version of the article). author of the KBCafe.CSharp. working with the . even HTML and Rich Text format.Enhancing Visual Basic . at the same level as its methods. text is usually formatted to certain line widths.NET Namespace is structured from this. and even rougher around the edges than that of Mackenzie’s work.NET Beyond the Scope of Visual Basic 6. Because email is transmitted as text. and to fully address all the limitations he pointed out (this in turn resulted in two complete rewrites of my code). I finally fully commented the code.vb. We will create a single file. Further. some of whom. It also has a field storing the messages size in bytes. So go ahead and create a class file named POP3. shall define a message storage class. but we will add 3 classes to it. This is the reference number that the email server will have assigned to each message. much like its abstract class cousin. keeping its data contained without the possibility of cloning itself whenever it is manipulated). which we will be able to import into an application. which is normally an incremental index. based on their writing skills. or just its references. 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. The above class object will store the message number as an integer. The trenches of World War I were a veritable breeding ground for every kind of parasitic pest imaginable.NET. After building a VB. I can easily imagine sitting there in 3-day-old boxers. It is declared below: 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' Class Name : POP3Message ' Purpose : POP3 message data 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Public Class POP3Message Public MailID As Integer = 0 'message number Public ByteCount As Integer = 0 'length of message in bytes Public Retrieved As Boolean = False 'flag indicating if the message has been retrieved Public Message As String = Nothing 'the text of the message Public Overrides Function ToString() As String Return Message End Function End Class NOTE: Do not be nervous about adding more than one class to a file.org/rfc/rfc1939. I then took great pains to significantly optimize the code for both size and speed. to include optional TCP Ports and SSL/TLS support. You will see this concept much more clearly once we put these classes together to create a class library. and then we store the message itself as a string. 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. My initial goal was simply to see why those internet gurus. In fact. a Structure. rich text box. The file will be a class module. which will store a single email message. meaning lice.NET version familiarly modeled after Randy’s approach. if these other classes were used only within the POP3 class. or web control. and binary attachments are tacked on as encoded 7-bit ASCII strings. we could have embedded them within its body. wolfing down a bag of M&Ms and flushing the sugar away with Diet Pepsi. By the way. not one of them could access SSL/TLS accounts. you might be able to see how a . 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. A Boolean flag acts to indicate if we have actually retrieved the message text. The second class. and each line is terminated by a Carriage Return and Linefeed (vbCrLf).txt). but we should not make any assumptions about it. For example.ietf.0 – David Ross Goben For all those listed. The email program usually translates this encoding and displays any formatted representation within a text box. were saying that this kind of solution was not possible under VB. named POP3Message. Page –260– . you are going to find a utility here to easily break emails up into its parts). 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. It will only hold fields. their code interfaces are very simplistic. and interpreting email was not addressed (yep. This is perfectly acceptable. and is likely to have come from the Malayan word kutu. 0 – David Ross Goben Our third class.SslStream Private SslStreamDisposed As Boolean = False Public LastLineRead As String = Nothing 'non-SSL stream object 'True if SLL authentication required 'set to SSL stream supporting SSL authentication if UsesSSL is True 'true if we disposed of the SSL Stream object 'copy of the last response line read from the TCP server Page –261– . but the Inherits instruction provides it with as much capability and functionality as any other Exception object. one for a standard non-SSL Network Stream. Therefore. POP3Exception. I already know that I will need to import 2 namespaces: 1. I want to store the last line read from the server in case we ever want to check it further. with all class bodies. I will also need to have local fields that will live as long as the class instance. 2. '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 'This VB. plus we can trap errors that we will know were issued by it.Text. ' cleaned up a lot of clutter. from which I can access the Sockets and Security namespaces.Net. Among those. Although it is possible to long-path these namespaces in the code without a compilation cost of even 1 byte. but we wrap it to give it an identy ' : that can be associated with our POP3 class 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Public Class POP3Exception Inherits ApplicationException Public Sub New(ByVal str As String) MyBase. the main body of my file. I have added MANY language and POP3 enhancements. Both of these streams shall be used for POP3 mail server communication. Now we are ready to construct our main and presently empty POP3 class. to inform reviewers of my use of resources.NetworkStream Private UsesSSL As Boolean = False Private SslStream As Security. '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Imports System. the Network Stream is essential for both types. Regardless.Copyright © 2011 . The Security namespace is important if we will require SSL/TLS authentication. ' forcing a complete rewrite. System. I will need a Boolean flag that will indicate if we will require SSL/TLS authentication or not. such as Gmail and many others servers require. The Sockets namespace is important because through it I will be able to access a POP3 server though a TC/IP Client class. I like to list the namespaces used at the top for self-documentation purposes.TcpClient 'this class shall inherit all the functionality of a TC/IP Client Private Stream As Sockets. Finally. That is the real beauty of inheritance. because I will need to translate between Unicode strings and Byte arrays.NET Code was inspired by C# code originally written by Randy Charles Morin. This way we can detect it in a Try…Catch Block. and added Port and SSL support. I will first import System. because an SSL piggybacks itself onto a Network Stream. such as by using “Catch e As POP3Exception”: 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' Class Name : POP3Exception ' Purpose : process exception ' NOTE : This is a normal exception. and a possible second SSL Stream. We will also need to store two stream objects. is an error class used to differentiate POP3 errors from normal coding errors.com.Text '------------------------------------------------------------------------------' Class Name : POP3 ' Purpose : POP3 Interface Class '------------------------------------------------------------------------------Public Class POP3 Inherits Sockets. becomes the following: Option Strict On Option Explicit On '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ' POP3 . 'author of KBCafe. ' Oh! And I include REAL comments.NET Beyond the Scope of Visual Basic 6. To do everything I want to do in this class.Enhancing Visual Basic .New(str) End Sub End Class It may not look like much.2015 by David Ross Goben. The second namespace to import is System.Net. ' ' I have optimized the heck out of the code to speed I/O and program execution. We will examine each of these parts in their turn.Enhancing Visual Basic . Once we have done that.NET Beyond the Scope of Visual Basic 6.New(str) End Sub End Class The first and last thing we need to do after defining this is to connect to the POP3 server. Plus. Page –262– . Connect() Connecting to our POP3 Host Server is always the first thing we do. typically through generic Port 110. in order to provide us with the ability to remove or not removed downloaded emails from the Host Server. and of course. we might as well find out if we have any email. but we wrap it to give it an identy ' : that can be associated with clsPOP3 '------------------------------------------------------------------------------Public Class POP3Exception Inherits ApplicationException Public Sub New(ByVal str As String) MyBase. but we must keep it open for other ports in case they are defined (note that email is now trending toward SSL/TLS-encrypted ports). because we will in fact need to invoke it. Communication with the server is just a long series of our text-based requests and its text-based responses. We must have this choice in terminating POP3 server communication. is a method that is built into the socket library. because we can still access the original Connect() method declared for the TCPClient base class that our POP3 class inherited from. which will also mean that we need a resource to check responses from the server (its replies to our queries). or we can explicitly disconnect from the server. Connecting to a POP3 Server Connecting to a server involves making a connection using a port. as you will learn. We then have to submit our email Username and Password to it. we will be able to issue all other POP3 commands until we either remove our connection to the POP3 class. to disconnect from it when we are finished. We will also require a method to submit queries to the server. which breaks our connection to the server. This is not anywhere near as scary as it might sound. We will also want to save the server name off in case we will require later SSL authentication. so we will have to overload it in order to apply our own customizations on top of it. which is a good thing.0 – David Ross Goben '>>>>>>>>>ANY MORE CODE WILL BE INSERTED HERE HERE<<<<<<<<< End Class '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '------------------------------------------------------------------------------' 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. such as “guest” or “user” and “password”.CheckResponse() Then Exit Sub ' 'exit if an error was encountered If CBool(Len(Username)) Then Me. but this is usually because they also use SSL Authentication.net 110 (submit) '+OK POP3 mail. We do this by providing the Connect() method with the host server address. so build an SSL stream object on top of the non-SSL Network Stream SslStream. ' 'and if so. and True if our POP3 server will require a Secure Socket Layer. we submit Port 110 for communication. some few servers sometimes do not even accept usernames and/or passwords and will reject them.AuthenticateAsClient(Server) 'add authentication as a client to the server End If If Not Me. if your particular server. User Name. The first thing that Connect() does is check to see if we have a valid POP3 connection. such as our SSL choice.net” or “pop. our email username (or our whole email address. NOTE: Some servers let anyone and everyone on. or even to accept any and all responses. we stow away important references. and a if SSL authentication is required ' : ' Returns : Nothing ' : ' Typical TelNet I/O: 'telnet mail.domain. Gmail does this. which is becoming common.CheckResponse() Then Exit Sub ' End If End Sub 'if the password is defined (some servers will reject submissions) 'submit password 'exit if an error was encountered As you can see. password please 'PASS mysecretpassword (submit) '+OK Mailbox open. _ Optional ByVal InPort As Integer = 110. For example. In such cases. though encrypted SSL/TLS connections are growing in popularity.CheckResponse() Then Exit Sub ' End If 'if the username is defined (some servers will reject submissions) 'submit user name 'exit if an error was encountered If CBool(Len(Password)) Then Me. such as “mail.0 – David Ross Goben Following is our new Connect() method. it uses a secure TCP Port 995 for POP3. InPort) 'connect to the server via our base class (signature differs from our method) Catch Throw New POP3Exception("Cannot connect. After that. 3 messages (the server locks and opens the appropriate maildrop) '******************************************************************************* Public Overloads Sub Connect(ByVal Server As String.Submit("PASS " & Password & vbCrLf) If Not Me. such as Gmail.gmail. In those cases. or they might allow you to use a generic sign-on. disconnect that session UsesSSL = UseSSL 'set flag True or False for SSL authentication Try Connect(Server. Password. If we will use SSL. our email password. we first have to set up a non-SSL stream If UsesSSL Then 'do we also need to use SSL authentication? SslStream = New Security. Page –263– .domain. requires it).NET Beyond the Scope of Visual Basic 6. Port. a port number if it differs from TCP Port 110. Check Server.com”.83 server ready 'USER myusername (submit) '+OK User name accepted. Note that it references other methods that we will address as we continue: '******************************************************************************* ' Sub Name : Connect (This is the first the we do with a POP3 object) ' Purpose : Connect to the server using the Server. Next.SslStream(Stream) 'yes.Submit("USER " & Username & vbCrLf) If Not Me. _ ByVal Username As String. _ Optional ByVal UseSSL As Boolean = False) If Connected Then Disconnect() ' 'check BaseClass Boolean flag to see if we are presently connected. we will set our UsesSSL flag to True. we access the base class’s Connect() method and submit the server name we wish to access. Username or Password for errors") Return End Try Stream = GetStream() 'before we can check for a response. By default. This is the port most often used by mail servers.comcast. we begin our adventure into the world of POP3 by initiating communication with our POP3 server. we have to allow for those situations.net v2011.Enhancing Visual Basic . _ ByVal Password As String. Some servers use a different port. True if connected to server '******************************************************************************* Public Function IsConnected() As Boolean If Not Connected Then 'if not connected. a returned string begins with “+OK”. I broken out into a separate function that returns True if it was successful (its full response. begins with “+OK”). Return True for Success. such as the first 3 letters of the response line not being “+OK”. Checking for Being Connected to a POP3 Server The IsConnected() method consolidates a lot of checks to the underlying base class’s Boolean Connected property.NET Beyond the Scope of Visual Basic 6. indicating that it is not in a Transaction state: '******************************************************************************* ' Function Name : IsConnected ' Purpose : Return True if we are connected to Server in TRANSACTION state.89 fahrvergnugen8. stored in our public string. Throw an error and return False if not.567.0”.Substring(0. and False if it was not (an error begins with “-ERR”). which all start with “-ERR”). or request granted) ' or : -ERR (NAGATIVE. so throw an exception Return False 'return failure flag End If Return True 'else return success flag End Function This method in turn invokes our Response() method. ' : ' Returns : Boolean Flag. the rest of the “+OK” message can be informative (as are error messages. LastLineRead.IsConnected() Then Return False ' 'exit if not in TRANSACTION mode LastLineRead = Me. But what is important here is that we get our first look at returned data from the POP3 server. Here is the CheckResponse() method: '******************************************************************************* ' Function Name : CheckResponse ' Purpose : Check the response to a POP3 command ' : ' Returns : Boolean flag. transmitting the returned text line to the message pump hidden with the exception object. Success.") Return False 'return failure flag End If Return True 'Indicate that we are in the TRANSACTION state) End Function Page –264– . ' : ' NOTE : All status responses from the server begin with: ' : +OK (OK. then we throw our POP3Exception error. throw an exception Throw New POP3Exception("Not Connected to an POP3 Server. Because this test runs rampant throughout a TCP client session.0 – David Ross Goben Checking for a POP3 Server Response We next invoke CheckResponse() to see if a request was accepted.34. This test follows most-all submissions. and if we encounter an error.Enhancing Visual Basic . You may even want to preview what the full responses are by examining the LastLineRead text. 3) <> "+OK") Then 'OK? Throw New POP3Exception(LastLineRead) 'no. Throw an error and return False if not. If so. such as “+OK Gpop ready for requests from 12. It throws a POP3ExceptionError if the server is not connected.Response() 'check response (and save response line) If (LastLineRead. error) '******************************************************************************* Public Function CheckResponse() As Boolean If Not Me. otherwise. but I think reading one byte at a time is still efficient enough.Read(ServerBufr. 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.Read() method. If we do not specify a buffer size. and still quite fast). depending on whether we require SSL authentication to communicate with the TCP Server or not. 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 . then those number of bytes will be streamed in.Enhancing Visual Basic . so the user should find a more efficient means of reading more than one byte. then the index/counter is incremented.GetString(ServerBufr. If the Stream. 10). dtsz) 'read a server-defined block of data from SSLstream Else 'else process through general TCP Stream sz = Stream. which is a more concerning issue when reading one byte at a time.ReadByte() methods do not report back that they failed to read from their stream. However. which can read a Byte value (the docs do state that a temp 1-byte internal array is created to read the byte. 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. so we end up trying to read beyond the end of the provided stream..NET Beyond the Scope of Visual Basic 6.ReadByte() (function returns Int32) Do If UsesSSL Then 'process through SSL Stream if secure stream byteRead = SslStream. Every C#/C++ example I have ever seen first reads a stream byte into a single-element intermediate buffer array using the stream. we read the port one byte at a time until we encounter an end of line code (vbLf . Here we will finally implement our communication streams to receive “real” information from the POP3 server. Index) 'decode from the byte array to a string and return the string End Function Here we read from either the Network Stream (Stream) or the SSL Stream (SslStream). More importantly. dtsz) 'read a server-defined block of data from Network Stream End If If sz = 0 Then Return Nothing ' 'we lost data. If UsesSSL Then 'process through SSL Stream if secure stream sz = SslStream. Unlike everyone else.0 – David Ross Goben Getting a Response from the POP3 Server The Response() method is where all the really interesting things happen. the data will be ' : read in a line at a time. I chose the ReadByte() method instead. I have noticed that this can hang until an eventual timeout (usually 30-60 seconds) if the signal drops off. Index. Page –265– . Index. '******************************************************************************* ' Function Name : Response ' Purpose : get a response from the server (read from the mail stream into a buffer) ' : ' Returns : string of data from the server ' : ' NOTE : If a dataSize value > 1 is supplied. 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.Read(ServerBufr... ReadByte() will return a -1 immediately if we moved beyond the end of the stream or if the connection dropped off. which requires a destination byte array.ReadByte 'read a byte from SSLstream Else 'else process through general TCP Stream byteRead = Stream. 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.ReadByte() method or the SslStream..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.1) 'size to dataSize to read as single block (allow for 0 index) Dim dtsz As Integer = dataSize 'set aside updating countdown Dim sz As Integer 'variable to store actual number of bytes read from the stream Do While Index < dataSize 'while we have not read the entire message. 0. the ServerBufr is immediately dimensioned to that size (-1 is applied for the 0 offset). the ' maildrop may result in having some or none of the messages marked as deleted be removed.0 – David Ross Goben We loop until we get an end of a line code (a value of 10.GetBytes(message) 'converts the submitted string into to a sequence of bytes If UsesSSL Then 'using SSL authentication? SslStream. Just be sure that the command word is uppercase. and the body of an email is considered another block. the POP3 session terminates ' but does NOT enter the UPDATE state. In no case may the ' server remove any messages not marked as deleted.Length) 'yes. 0.Write(WriteBuffer. This size is implementation-defined. we should NEVER assume it. vbCr). '******************************************************************************* Public Sub Submit(ByVal message As String) Dim enc As New ASCIIEncoding 'medium for ASCII representation of Unicode characters Dim WriteBuffer() As Byte = enc. WriteBuffer. typically following a Carriage Return. Documentation says that the default maximum stream buffer size is 998 bytes. For example. the POP3 session does NOT ' enter the UPDATE state and MUST NOT remove any messages from the maildrop. If there is an error. "pass pw1Smorf" would not be acceptable.Write(WriteBuffer. vbLf. is the Submit() method: '******************************************************************************* ' Sub Name : Submit ' Purpose : Submit a request to the server ' : ' Returns : Nothing ' : ' NOTE : Command name must be in UPPERCASE. ' : Though some servers do allow for this. depending on whether we use SSL authentication or not. ' Page –266– . Even though our Stream and SslStream objects do have a SetLength property that is supposed to allow you to set the stream buffer size.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. which is so far unresolved. Once it is done reading bytes. the byte array is then translated to a Unicode string (the default string format for VB. We then continuously try to fill the buffer full. such as "PASS pw1Smorf". WriteBuffer. the server actually returns data to us in blocks. encountered while removing messages.NET Beyond the Scope of Visual Basic 6. representing a Linefeed character. That is accomplished through the Disconnect() method: '******************************************************************************* ' 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. so write SSL buffer using the SslStream object Else Stream. Disconnecting from the POP3 Server A final primary thing that we may need to do. However. if the data block is longer than the server’s stream buffer. or if no data was received (it could mean that the signal dropped off). not in one big blobby mess of data. 0. such as 1400 bytes. it will break it up into a sequence of sub-blocks.NET) and it is returned to the invoker. the “header” of an email actually consists of a number of headers. as are attachments and alternate views. Submitting a Request to the POP3 Server The one thing left that we must look at. if we want the server to enter its usual UPDATE state.Enhancing Visual Basic . the POP3 session enters the UPDATE state. One thing you may not realize is that we can directly access this method and submit a command directly. this is not yet supported: a sad fact that MSDN’s documentation also states. ' (Note that if the client issues the QUIT command from the AUTHORIZATION state. If a buffer size was supplied.) ' ' If a session terminates for some reason other than a client-issued QUIT command. And even then. is to disconnect from the POP3 Server. ' ' The POP3 server removes all messages marked as deleted from the maildrop and replies as to the status of this ' operation. though it is not uncommon to find buffers that are larger. such as a resource shortage. Submit("QUIT" & vbCrLf) 'submit quit request CheckResponse() 'check response If UsesSSL Then 'SSL authentication used? SslStream. constructing a ListArray of POP3Message objects from the server. and disconnect from the POP3 server.Parse(msgInfo(1)) 'get the number of emails Result(1) = Integer. The actual Stream object is taken care of by the underlying TCPClient base class. before we disconnect. we dispose of this resource. because we only maintained a pointer to it. now that we can connect.Submit("STAT" & vbCrLf) 'submit Statistics request LastLineRead = Me. 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. which will dispose of that object for us (we did not instantiate the Network Stream object with “New”. because we had to instantiate our SSL Stream (but only if we require SSL authentication). Of course. Getting an Email Reference List from the POP3 Server The next thing we might want to do after connecting and finding out that we have email in our drop box. so you should expect a slight discrepancy when you load files. the server then releases any exclusive-access lock on the maildrop ' and closes the TCP connection. but we throw it away.Dispose() 'dispose of created SSL stream object if so SslStreamDisposed = True End If End Sub We check for a response. Our List() method takes care of this.0 – David Ross Goben ' Whether the removal was successful or not. because we are leaving. 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 interger array. at least at this level. We do not have to concern ourselves with the Network Stream object. log on. rather than vbCrLf. NOTE: Most online servers are Unix. and Result(1) contains the total number of bytes the messages occupy on the server. Result(0) will contain the number of email messages residing in your mail drop. 3) <> "+OK") Then 'OK? Throw New POP3Exception(LastLineRead) 'no. because it was already instantiated by the underlying code of the base class). and store line terminators as vbLf. is to get a list of those emails from the server. ' : 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. such as how many emails we have in our mail drop on the server and how many bytes this data occupies on that server. regardless.Enhancing Visual Basic .Parse(msgInfo(2)) 'get the size of the email messages Return Result End Function This returns an integer array. Also. it would also be a really good idea to be able to read some data from it.Substring(0. such as an email maybe. '******************************************************************************* Public Sub Disconnect() Me. because your TCP connection will read and convert a lone vbLf to vbCrLf.NET Beyond the Scope of Visual Basic 6. " "c) 'separate by spaces. which divide its fields Dim Result(1) As Integer Result(0) = Integer. so throw an exception Return Nothing 'return failure flag End If Dim msgInfo() As String = Split(LastLineRead. and then returning them to the invoker: Page –267– .Response 'check response If (LastLineRead. that number of body lines will ' : be returned. such as retrieving the actual email message text (gee. Optional ByVal BodyLines As Integer = 0) As POP3Message If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode Me.Submit("LIST" & vbCrLf) 'submit List request If Not CheckResponse() Then Return Nothing ' 'check for a response. separated by a space. Get an Email Header from the POP3 Server '******************************************************************************* ' 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. The first is the reference index for each item in its mail drop for the Username. if any Page –268– .ToString & vbCrLf) If Not CheckResponse() Then Return Nothing ''check for a response. and the other is the number of bytes the message occupies on the server.0 – David Ross Goben '******************************************************************************* ' 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 '.Add(msg) 'add a new entry into the retrieval list Loop Return retval 'return the list End Function The List() method constructs a list of POP3Message objects. but you may notice that we do not fill their Message field with anything. (end of records terminator) '******************************************************************************* Public Function List() As ArrayList If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode Me. but if an error. " "c) 'separate by spaces.Message = Nothing 'erase current contents of the message. (end of record terminator) '******************************************************************************* Public Function GetHeader(ByRef msg As POP3Message.ByteCount = Integer. These peripheral methods will be listed one at a time.MailID. return nothing ' 'get a list of emails waiting on the server for the authenticated user ' Dim retval As New ArrayList 'set aside message list storage Do Dim response As String = Me. the server returns a simple string for each email that it has pending for the Username. what a unique idea). just their server reference number and the number of bytes that the message occupies.Parse(msgInfo(1)) 'get the size of the email message msg. There are also a number of peripheral POP3 commands that we can exploit to round out our email retriever.Response 'check response If (response = ". which divide its fields msg.Retrieved = False 'indicate its message body is not yet retreived retval. The returned object is the submitted POP3Message.Parse(msgInfo(0)) 'get the list item number msg. below. but if an error. 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).Enhancing Visual Basic . ' : ' Typical telNet I/O: 'TOP 1 0 (submit request for record 1's message header only. That is because when we submit a “List” request." & vbCrLf) Then 'done with list? Exit Do 'yes End If Dim msg As New POP3Message 'establish a new message Dim msgInfo() As String = Split(response. 0=no lines of body) '+OK Top of message follows ' xxxxx (header for current record is transmitted) '. This simple string consists of two numeric text values. deleting an email from the server.NET Beyond the Scope of Visual Basic 6. (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) '.Submit("TOP " & msg. If an integer value is provided. return nothing msg.MailID = Integer. resetting any deletions (great for OOPS! situations).ToString & " " & BodyLines. such as “1 2532”. also have a Content-Type field. most thinking it contains nothing but routing data. Outlook.Response 'grab message line If response = ". But regardless. The “Date:” field displays the Date and Time as local. Consider the following trailing end of an email header: From: mercedes_silver@80micro. after decoding.NET Beyond the Scope of Visual Basic 6. So we must retreive more data to account for this. but the reported email size does not ' account for them. If it returns Nothing. if the user wants to save the attachment to disc. a name parameter that will tell you the default filename (name) to store the data under. By submitting a POP3Message to the GetHeader() function. and maybe a parameter containing character set used for the message. “To:”. You can also grab the Content-Type and. England.Enhancing Visual Basic . except to mere mortals. which it will always report if there are alternative views or attachments included. So. However. if multipart/alternative.goben@gmail. ' : ' NOTE: Some email servers are set up to automatically delete an email once it is retrieved from the server. after a boundary marker. Greenwich in England. the above time means that now was this time 5 hours ago (-0500) in Greenwich.com Date: 25 Feb 2011 17:59:01 -0500 Subject: Test Content-Type: multipart/alternative. ' Outlook Express. and if an attachment. which will informs you of the format the data is formatted as. or the header plus a specified number of body text lines." & vbCrLf Then 'end of data? Exit Do 'yes. if we do not submit a POP3 ' QUIT (the Disconnect() method). the last few lines of the header actually do have some interest to them. ' ' Typical telNet I/O: 'RETR 1 (submit request to retrive 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 retreived as one long string) '. It is an option under Juno and Gmail. and its ByteCount property properly ' : fitted to the message size. and Windows Mail do this. though they may not realize it. Alternative views and attachments. It is now often used to refer to Coordinated Universal Time (UTC)).Message &= response 'else build message by appending the new line Loop Return msg 'return new filled Message object End Function The GetHeader()method allows you to grab only the header of the message. then there was an error in trying to retrieve the header. with an offset value to GMT (Greenwich Mean Time. The Header of email is interesting. the message(s) will not be deleted. originally referring to mean solar time at the Royal Observatory. and “Subject:” are easily extractable. such as text/plain. it will also include a “boundary=” parameter. then the Content-Type will contain the type of data.ross.0 – David Ross Goben ' 'now process message data by binding the lines into a single string ' Do Dim response As String = Me. containing the header of the message. If no alternative views or attachments. ' most Windows-based server-processors will add an additional CR for each LF. Retrieve an Email from the POP3 Server '******************************************************************************* ' Function Name : Retrieve ' Purpose : Retrieve email from POP3 server for the provided POP3Message object ' : ' Returns : The submitted POP3 Message object with its Message property filled. (end of record terminator) '******************************************************************************* Page –269– . done with the loop if so End If msg. boundary=--boundary_0_570b636e-3a77-40a7-bf02-5b178a2cff5b The lines beginning with the fields “From:”.com To: david. who have no use for it. but just close out the POP3 object. which informs you of what the boundary between the various parts will consist of. Even so. this method will return a pointer to the POP3Message object that you had submitted. ' Outlook Express.. 'But even if this was not the case.Submit("RSET" & vbCrLf) 'submit Reset request CheckResponse() 'check response End Sub The Reset() method will undelete emails that you selected with Delete() during the current session. and Windows Mail do this.Submit("RETR " & msg. Reset (Undo) All Deletes from the POP3 Server '******************************************************************************* ' Sub Name : Reset ' Purpose : Reset any deletion (automatic or manual) of all email from the current session.ByteCount) 'grab message line 'the stream reader automatically convers the NewLine code. but if an error.Message) 'ensure full size updated to actual size (server size may differ) Return msg 'return new message object End Function By submitting a POP3Message to the Retrieve() function. then exit loop End If msg. For example.MailID. presumably its present message string being empty or ignorable.ByteCount = Len(msg. Page –270– . a files that was 233 lines will therefore have 233 more characters not 'yet read from the files when it has reached its reported data size. including both the full header and body of the message. It will also return a pointer to the message.ToString & vbCrLf) 'submit Delete request CheckResponse() 'check response End Sub The Delete() method will delete a specified message. Do Dim strData As String = Response() 'grab more data If strData = ".ToString & vbCrLf) 'issue request for indicated message number If Not CheckResponse() Then Return Nothing ' 'check for a response.Message &= strData 'else tack data to end of message Loop 'keep trying msg.NET Beyond the Scope of Visual Basic 6. Deleting an Email from the POP3 Server '******************************************************************************* ' Sub Name : Delete ' Purpose : Delete an email ' : ' Returns : Nothing ' : ' NOTE: Some email servers are set up to automatically delete an email once it is retrieved from the server.Message = Me.Submit("DELE " & msgHdr." & vbCrLf is still pending. it will fill its Message property with the entire email. It is an option under Juno and Gmail.Enhancing Visual Basic . Just provide the selected POP3Message object.0 – David Ross Goben Public Function Retrieve(ByRef msg As POP3Message) As POP3Message If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode Me. ' : ' 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." & vbCrLf Then 'end of data? Exit Do 'If so.Response(msg. except if there was a failure. so the files is not yet 'fully read.. So we will scan these in. vbLf. ' ' 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.MailID. to vbCrLf. the trailing ". Outlook. return nothing msg. This command can be issued by a timer that also monitors users inactivity. the resources are released. where servers often delete retrieved messages. if the server does not receive a QUIT command (issued by Disconnect()).Submit("NOOP") Return CheckResponse() End Function The NOOP() method will do nothing but obtain a response from the server. above. ' ' 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. Juts gets a position response from the server ' : ' Returns : Boolean flag. thus keeping the connection alive. during testing anyway. This is not really a big issue. NOTE: You should never invoke the Finalize method yourself.Enabled=True) each time you issue a command to the server. grab all the emails. ' : ' NOTE: This NO OPERATION command is useful when you have a server that automatically disconnects after a certain idle ' period of activity. Some servers will disconnect from a user after a certain period of time. the created resources will not be removed. Let the system automatically take care of that for you. and issues a ' NOOP to reset the server timer.myTimer. Of course. we added a Finalize() method that will be issued by the Garbage Collector before destroying the POP3 object. and disconnect. because the . However. But obtaining a response from the server. However. when you issue a Disconnect() command. Using the Completed POP3 Class Now that we have defined all the parts of our POP3 class (the complete class listing is at the end of this article). or they make it an option. so do it End If MyBase. and automatically submits a NOOP request to the server. then it will not enter into its UPDATE state. False if disconnected. we need to be able to use it. As noted. which will connect to the POP3 host server. Hence. Many email providers will delete the message from the server once you have retrieved it. you could maintain your own timer that times out before the server time.myTimer.Dispose() 'no.NET Garbage Collector will detect that the resources are no longer referenced and remove them.Enabled=False: Me. but it is always a good idea to take care of that. if you do not issue a Disconnect() command.NET Beyond the Scope of Visual Basic 6. this method can be used just to tickle the server. Page –271– . this timer is reset to the beginning of its counter. 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. to reset your own timer.Enhancing Visual Basic . Even though the following is a short-cut. else True if connected. But you may not want to always do that.Finalize() 'then do normal finalization End Sub Normally. consider invoking the following sample intermediary method. you will want to reset your own timer (Me. If you have such a server. then the emails will remain on the server.0 – David Ross Goben Send a ‘Keep-Alive’ NOOP Command to the POP3 Server '******************************************************************************* ' Function Name : NOOP (No Operation) ' Purpose : Does nothing. If it does not enter the UPDATE state. though in the rare cases of an all-are-welcome server. InPort.OkOnly Or MsgBoxStyle. Also.0 – David Ross Goben However. which needs to have the entire email address. Username. UseSSL) 'Connect to user account '----------------------------------------------------------Dim Stats() As Integer = InMail. to use for later testing. typically. but rather values retrived from previously saved user preferences.Connect(Server. such as “pop. specifying Server. or Nothing if no emails ' : ' NOTE : This method should be modified to suit your application. you should skip invoking the Disconnect() method. The last two parameters are required only if your POP3 TCP Port is different or your server needs SSL authentication. Page –272– . MsgBoxStyle.Message. it does!). MsgBoxStyle. It runs as a separate thread in the background. "Error Encountered") Catch e As Exception MsgBox(e. such as ' : "Me. each member of the array stored as individual POP3Message objects: '******************************************************************************* ' Function Name : SampleReadPOP3 ' Purpose : Samples method to Read a POP3 account maildrop ' : ' Returns : ArrayList containing a list of POP3Message objects.com”. ByVal Password As String. But on Comcast.Message.com”. most notably Gmail. The raw text ' : should be plugged into the appropriate medium.net". "Im1Idjut".TextBox1. during testing.Statistics() 'get maildrop statistics (# of message. Optional ByVal UseSSL As Boolean = False) As ArrayList Try Dim InMail As New POP3 'create a new POP3 connection InMail. In that case.Message" for proper viewing. "CantRecall1"). total byte size) If Stats(0) = 0 Then 'check number of messages for being 0 (none) Return Nothing 'no email found in the maildrop. No worries. and Password. The Server is always mandatory. such as “[email protected]. the Username is the part of the user’s email address that comes before the “@” (At) sign. this list should not be hard-coded as shown. the text may be HTML or Rich Text format. though on most log-ins. and it runs much more often than most people would imagine. The following sample method. the parameter list would be ("mail.NET Beyond the Scope of Visual Basic 6. the connection will be lost.verizon. returns all the available online emails from the server to the invoker as an ArrayList filled with all your Email Messages. once our objects go out of scope. Were if up to me. If you are concerned about waiting for the Garbage Collector – do not waste even a bead of sweat.Enhancing Visual Basic .gmail. Username. ByVal Username As String. it might be a good idea to leave your email on the server.Retrieve(msg)) 'add a message object with message text Next 'process all messages '----------------------------------------------------------InMail. "Im1Idjut". For example. as of VB2010. Optional ByVal InPort As Integer = 110.Add(InMail.comcast. For ' : example. you may need to provide a blank Username and/or Password. I would have it running constantly in the background (well. such as “Im1Idjut”. as you prod and modify it to implement your own test designs. the parameter list for a Verison account would be ("incoming.Exclamation.OkOnly Or MsgBoxStyle.com” or “authpop. or are set to Nothing. Password.List 'parse each header object (contains only index and size) localList. "CantRecall1". some require the entire email address. However.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 MsgBox(e. "Error Encountered") End Try Return Nothing End Function 'POP3-side error 'general programming error The first three parameters are mandatory. 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.Text = msg2. 995. True).net". '******************************************************************************* Public Function SampleReadPOP3(ByVal Server As String.juno. SampleReadPOP3(). “Multipurpose Internet Mail Extensions. the type that my QuickiEmail() or BrainDeadSimpleEmailSend() would send out. following Content-Transfer-Encoding. It is all easy. (MIME) Part One: Format of Internet Message Bodies”. To. making some parts read as though it was a foreign language. Let’s first us look at a simple email and extract its important parts. Page –273– .org/rfc/rfc2045. you could let me read them. and Subject are easy enough. There are of course other routine fields located above it.com". or how to decode them. MsgBoxStyle. MsgBoxStyle. reply to them.". '******************************************************************************* Friend Sub SampleReadEmail() Dim EmailBag As ArrayList = SampleReadPOP3("pop. It is NOT part of the message. though you would want to plagiarize the really good ones.Enhancing Visual Basic . or until a boundary marker is encountered. "No Email") End If End Sub This is all. yyyy HH:mm:ss zzz”. From our example. Quoted-Printable means data is Hex-Tag encoded. It then builds an ArrayList named localList that will contain all retrieved emails as POP3Message objects. Or. is ALWAYS blank. The Message body data is Text and is formatted as HTML. “ContentType” and “Content-Transfer-Encoding”.goben@gmail. England time). seconds. Message Body. yyyy HH:mm:ss zzz). Data is here. Subject. CONTENT-TYPE Field. plus any possible attachments and/or alternate views. displaying each raw message in a MsgBox (rather than using absolute user account parameters.ietf. Content-Transfer-Encoding Field.the. and then simply let me show you how to use them.0 – David Ross Goben It first instantiates a POP3 instance and connects it to the [email protected] Beyond the Scope of Visual Basic 6. month name.gmail.Count <> 0 Then 'if data was found For Each msg As POP3Message In EmailBag 'display each email MsgBox(msg. DATE Field (always d mmm. TO. Using the methods provided in the POP3 class. until the end of the data. we are going to add a single method to our library that will break all this information out for you so you can afterwards access each of its component parts. "bob. “Date”.Message. you will find all the answers. and what we can do with them: From: mercedes_silver@80micro. True) If EmailBag IsNot Nothing AndAlso EmailBag. or how to separate Alternate Views from Attachments. What follows is SampleReadEmail(). you can do quite a bit with it. This line. 995. then hours. TO Field.Count. separating each from its data. To. and then it returns this list of raw emails. or what the hell did you do to make the boss ‘volunteer’ you to develop an email interface? Relax. day. Subject”. a sample method. SUBJECT Field. "YudLk2Knw". That is. "Message # " & msg. “To”. 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.OkOnly Or MsgBoxStyle.ToString) 'display each raw email Next 'process all messages Else 'transfer here if no one loves you MsgBox("No email found on server. followed by the Zulu-time offset (number of hours against Greenwich. If you refer to RFC 2045 (www.txt). Date. and DATE Fields can be placed in any order within their 4-field zone).ross. forward the same jokes for the twenty-third time. But you may be wondering how the data is formatted. minutes. that invokes the above SampleReadPOP3() method and loop through its returned array list. From. Each is followed by a colon and a space “: ” to delimit the end of the fields. You may want to design an interactive email program.com". To solve all these issues. each of which we must afterward break up into their individual component parts. we can see 6 fields defined: “From”. such as From. In such cases you would want to keep a connection open and allow the user to fiddle with their email to their heart’s content.MailID.ToString & " of " & EmailBag.com ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Date: 25 Feb 2011 19:07:02 -0500 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Subject: Test ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Content-Type: text/html.Information. The Date field stores the date and time as “d mmm. It then disconnects from the server (only do this when you want the server to enter the UPDATE mode and delete its list of email files). such as giving them time to read them. year.OkOnly Or MsgBoxStyle. The above is just a simple email. though you will have to wade through a lot of cryptic descriptions and lots of jargon. charset=us-ascii ◄▬▬▬▬▬▬ Content-Transfer-Encoding: quoted-printable◄▬▬▬▬▬ ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This is a test=0D=0AThis is a second test=0D=0A◄▬ FROM Field (FROM. your application should process them as variable components): '******************************************************************************* ' 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. above. SUBJECT.com ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬ To: david. com ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ To: <david. indicates text could have control conversions (though HTML usually covers most). there will not be any actual control code translation. start of alternate view/attachment. tagged by “=”.ross. ------=_NextPart_000_0000_01CBD67A. dashed line that at 73 characters there was an email-impose end of line. Picture image is attached. If there is more than one. Content-Type: text/plain. (these 4 To Field. the next line will be a continuation. Build 1234. ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Main body is plain text charset="us-ascii" Content-Transfer-Encoding: 7bit ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Encoding. If a semicolon ends a line. as well. (these 4 Field (d mm. are not counted as data. ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Notice that a Content-Transfer-Encoding line is followed by a REQUIRED blank line. and such. This can all be easily cleaned up by the DecodeQuotedPrintable() method. etc. Carriage Returns. Notice on the long. the lack thereof in the case of 7bit. If the line ends in a semicolon “. The email is so short.365E8770 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Beginning of main body message area. you may often see this: Content-Type: text/plan.365E8770" ◄▬▬▬▬▬ Notice we have ANOTHER boundary declaration.”. both the main (plain). It is clear that the next line is a continuation because the previous line ended with a semicolon. so it is encoded ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line consisted only of vbCrLf David Ross Goben ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line consisted only of vbCrLf \|/ ~ ~ (@ @)=20 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ a space preceded end of line. consider the following abbreviated email (it was actually quite long. The final 3 dashes are lost in the translation. no filename. boundary="----=_NextPart_001_0001_01CBD67A. then the next line is considered a continuation of the previous line. Between parameters. 27 Feb 2011 12:31:07 -0500 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Date Message-ID: <8883802E7DC3402AA08372C008416D95@David> ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ MIME-Version: 1.Enhancing Visual Basic . (these 4 Obvious email extension as allowed by RFC fields can be in any order) fields can be in any order) fields can be in any order) fields can be in any order) 2045.0 Content-Type: multipart/mixed. so this is not the end of the field parameters Name=API32. Can be ignored. Obvious email extension as allowed by RFC 2045. and alternate (HTML). and followed by a vbCrLf. the line will actually continue on the next line.) The above is considered one line of data. Notice that there was a space down by “@)”.365E6060" ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ X-Priority: 3 (Normal) ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ X-MSMail-Priority: Normal X-Mailer: Microsoft Outlook. Mercedes ------=_NextPart_001_0001_01CBD67A. yyyy HH:MM:SS zzz). being that I have set the line limit of plain text data at 80 characters in Outlook. Content-Type: multipart/alternative.goben@gmail. Notice we have a boundary declaration set within quotation marks. or rather. ------=_NextPart_001_0001_01CBD67A.0 – David Ross Goben The data for a field can consist of one or more parameters. (these 4 Subject Field. ◄▬▬▬▬▬▬ ". tabs. indicating at lease one more parameter is pending. even with a small image (302x244). What this means is that when this message is processed. so it is encoded --oOO-{_}-OOo------------------------------------------------------------= ◄▬▬▬▬▬▬▬▬▬▬▬▬ non-breaking line continuation tag (=) ------.TXT ◄▬▬▬▬▬▬ continuations of previous lines are led by white space (non-printable characters: spaces. You may also notice in message bodies and the like. This equals an 80-character line once we strip off the “=” and vbCrLf.”.◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 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.NET Beyond the Scope of Visual Basic 6. so we have to live with this overflow. This means that between the text and my signature are 4 lines. ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ charset="us-ascii" ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Content-Transfer-Encoding: quoted-printable ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ End of plain text message. but it contains some interesting differences from previous raw emails we have examined): From: "Mercedes Silver" [email protected] Importance: Normal From Field. alternate view/attachment data is HTML." indicates more parameters defined. two of which have a space in them. These represent spaces prior to an end of line. 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. Linefeeds. such as spaces. Can be ignored.365E8770 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Content-Type: text/html. Now. A parameter ending in vbCrLf marks the end of the parameter(s). intervening white space (non-displayable characters). which is used here to wrap message types.◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ continuation of previous line --. ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ boundary="----=_NextPart_000_0000_01CBD67A.365E6060 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Begin next block (note the 2 extra dashes at the beginning). Page –274– . they are separated from each other by a semicolon “.5. where we find 7 more dashes.com> ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Subject: Check out this scary thing! ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Date: Sun. tabs. ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Beginning of plain text message. so what follows is an alternate view. so it is encoded ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line consisted only of vbCrLf =20 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ line ended with blank space. For example. +4v9XWWZt7COWIKD7Dy6p/8ADPPhL/oI63/3/i/+N0UV3x2RxS3D/hnnwkP+Yjrf/f6L/wCN0f8A DPPhL/oI63/3/i/+N0UVRI5P2e/CKNk32ssPQzx4P5R11s/w+0mbw8dESe8htTH5W6JkD4xjqVI/ Siis6m6Kich/wzz4S/6COt/9/ov/AI3R/wAM8+Ev+gjrf/f+L/43RRWgjbtPhDoFnpI01LzU2gD7 8tLHuz/3xVm/+F2iaikazXWoAR9Nkif1SiivMlGPtL26np0JyUVZn//Z ------=_NextPart_000_0000_01CBD67A. being unlike any other line in all of its other text..NET Beyond the Scope of Visual Basic 6.365E6060 ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ Beginning of second boundary data. such as Outlook. say. which were last updated in 1996. If “multipart/” leads.365E8770-. MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAD0AS4DASIA ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ This blank zone is ACTUALLY filed with about a page and a half of encoding. such as Outlook. Further. The first part is simple enough. ------=_NextPart_000_0000_01CBD67A. and the fact that they pack them full of self-defined extensions that are not specified by the RFC 2045 document. we simply need to be sure to absorb each one. but for some reason Microsoft Outlook chose to separate the main body and the alternate view from the attachment(s) with different boundaries. However. This simply identifies the data block as an attachment. We just need to check its left side for “multipart/”. That is. </body> </html> ------=_NextPart_001_0001_01CBD67A. if you refer back to our tables. 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. though they may actually be useful and help define a future specification. filename="David Goben. which indicates that we have attachments and/or alternate views.JPG" ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ filename for data (the filename indicates it is an attachment). This is an allowed user-defined extension.◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ End of HTML alternate view.365E6060-.Enhancing Visual Basic . and that a boundary will be unique to each email. ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ email extension as allowed by RFC 2045. which are grouped by the outer blocking. 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. some email processors.. when any of the defined boundaries are encountered. When the email has attachments of any sort. so depend on name.</p> <p> <p> &nbsp. use multiple boundaries.0 – David Ross Goben <html> <body> Picture image is attached. Notice that an email can have multiple boundaries defined. This is why some simple email readers have trouble parsing Outlook email. because one would have served this email. embraces within quotation marks). then we can be sure that the second parameter will specify a boundary definition string (which Microsoft Outlook. plus a specific filename (some emails do not have a disposition. So we will ignore it. Issues that you may encounter regarding the Content-Type field will be in interpreting its fields. Content-Type: image/jpeg. alternate views and attachments. but we can treat any as though they were the same boundary. Mercedes<p> ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ HTML forcing a leading space by inserting a non-breaking space. which specifically indicates an attachment. simplifying our processing. Page –275– . Can be ignored. Another thing to notice is the Content-Disposition line. but notice that the first one is different if we have attachments (attachments and/or alternate views). as we check for boundaries. then we will simply assume we have hit a boundary and think nothing of it beyond that. also can specify “multipart/mixed” if they feature. Content-Transfer-Encoding: base64 Content-Disposition: attachment. I do not understand why. Because some email applications. and to always test for each one. Though allowed.JPG" ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ optional disposition field. notice that boundary definitions can be optionally enclosed within quotation marks.◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ End of second boundary data (note the 2 extra dashes at the beginning and end). we simply need to check each line for containing the boundary using an Instr() function. name="David Goben. However. but few others. above) /9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0a HBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIy ◄▬▬▬▬ Base64 Binary image data. But we can still use it. the first parameter for Content-Type is supposed to be “multipart/alternative”. ◄▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ data is a binary image formatted as Jpeg. Remember. and the data can be saved to a file. or several EmailItem class object members. depending on the number of Alternate Views and/or Attachments included in the message. Issues that you may encounter with regard to reading the Content-Transfer-Encoding field will be how their parameter tags are actually embedded in the emails. text/html. you could specify that encryption specification here and say “so. You will then have to decode each EmailItem ContentBody member based upon what is set to its ContentEncoding member. or something similar. the main message body. you will need to decode the ContentBody member to a Byte array or to a string through System. to us at least. named AlternateViews and Attachments within the EmailInfo object. named MessageBody. and if their Count member is non-zero. you may have a proprietary encryption format that you do not want competitors to read.NET Beyond the Scope of Visual Basic 6. We will only concern ourselves when this second parameter begins with “name=”. are the To. what?” if someone else’s email reader cannot make heads or tails of it). a header contains many more fields than those shown in the above email sample. Page –276– . the second parameter will either be “name=”. such as “charset=us-ascii”. et al. The Remainder of the EmailInfo object stores the e-mail’s From. This is because they cannot be declared in an enumeration the same way the RFC 2045 document dictates that they must be provided. as opposed to how you specified them. It is the complete message as it is stored on the server (though. 7bit. To extract this important information. Date. you will need do nothing extra. as previously noted. If it is base64. you will have to run it through twice. that is reserved for the main body of the email. Provided this POP3Message. If it is quoted-printable. In that case.FromBase64String() as needed. such as “application/rtf” or “text/plain” or “image/jpeg”. in your company. we are instructed to assume the encoding is “base64”. depending on what the ContentType member specifies. any lone vbLf codes contained within it will have been replaced by vbCrLf). Of course. and the ByteCount member of POP3Message is set to the length of the desired email. Otherwise. or base64. plus all attachments and alternate views. Retreive() will return this POP3Message object with its Message member filled with an entire raw email. quoted-printable. using the specified filename as a default.0 – David Ross Goben If the first part of the Content-Type specifies a data type. look to the data in the right column for what you will find. text/rtf. Both the Alternate Views and the Attachments are maintained within ArrayLists of type EmailItem that allow the arrays to store multiples of each. defined below. which will only happen when we are in an attachment. You can check these ArrayList objects. If it is 7bit. There is one EmailItem object. and Subject members. such as image/jpeg.Convert. if the text data begins with =00. To. the GetEmailInfo() method will break that message data up and distribute it an EmailInfo class object.Message member. which in turn will contain one. simply provide the Message parameter of a POP3Message object to the GetEmailInfo() method. attachments. 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 you want retrieved. you can extract each EmailItem object from their Items collection and inspect them as you see fit. or whatever. depending on the global region. Consider the following table: How WE Declare Content-Transfer-Encoding Value QuotedPrintable SevenBit Base64 How RFC 2045 specifies they are Shipped/Received with Email quoted-printable 7bit base64 When you read email. but only if the user chooses to save it.Enhancing Visual Basic . which had previously been initiated by the POP3 class List() method. regardless of what the Content-TransferEncoding field specifies (of course. header. you will need to run the ContentBody member through the DecodeQuotedPrintable() method. the data is either the main message. and Subject fields. or it is an alternate view. From. or it will be a display format. Date. If it is not of these values. but the important header fields. as outlined earlier. " "c) 'append next line but remove tabs and spaces End If Select Case LCase(Left(S. I + 1)) 'stuff to structure Inheader -= 1 'drop 1 from flag Case "to: " 'Found TO field Info. the data should be decoded using ' DecodeQuotedPrintable(). whether it ' is a message or binary information. check for one of 4 fields Case "from: " 'Found FROM field Info. The Message Body. so bump index S &= Ary(Idx).ToData = Trim(Mid(S. '******************************************************************************* 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. ": ") 'find field delimiter If CBool(I) Then 'found one? If Right(S. SUBJECT. 1) = ". and each AlternnateView or Attachment ' are contained within EmailItem objects within the EmailIngo object.ContentTypeData = ContentTypeData 'save filename or character set Itm.Count) Then 'check any defined boundaries For Idy As Integer = 0 To Boundaries. vbCrLf) 'break full email into lines Dim Idx As Integer = 0 'index into Ary() Dim MX As Integer = UBound(Ary) + 1 'find end if list+1 Dim Boundaries As New List(Of String) 'boundary definitions Dim Dim Dim Dim IsMultiPart As Boolean = False SeekingEncoding As Boolean = False BuildingDataBlock As Boolean = False HaveMessageBody As Boolean = False 'true 'true 'true 'true if if if if we we we we have multiple parts are looking for encoding are building a data block have the message body defined Dim ContentType As String = Nothing 'hold last-defined Content Type Dim ContentTypeIsName As Boolean = False 'true of Content Type specified a file Dim ContentTypeData As String = Nothing 'if block is an attachment Dim ContentEncoding As String = Nothing 'hold last-defined Content Transfer Encoding Dim ContentBody As String = Nothing 'block data accumulator '----------------------------------------------------------Dim Inheader As Integer = 4 'flag for gathering To.. SeekingEncoding = True 'start looking for a Content-Transfer-Encoding field S = Nothing 'purge current data End If End If '------------------------------------------------------' check for boundaries '------------------------------------------------------If CBool(Len(S)) AndAlso CBool(Boundaries. I + 1)) 'stuff to structure Inheader -= 1 'drop 1 from flag Case "subject: " 'Found SUBJECT field Info. I + 1)) 'yes. CompareMethod.. ' : ' Returns : EmailInfo object with component parts of email broken down. a flag indicating if the ContentTypeData is ' a filename or if it is text formatting. SubjectData.Enhancing Visual Basic .Trim(Chr(9). The listing also include its two required support classes. Dim I As Integer = InStr(S.. and the raw encoded.DateData = Trim(Mid(S.. TO. Subject Do Dim S As String = Ary(Idx) 'grab a line of data from the email ' ' check for important header items ' If CBool(Len(S)) AndAlso CBool(Inheader) Then 'if we are currently in the header. content-transfer-encoding data. it is 7-bit data and does not need to be decoded.FromData = Trim(Mid(S. I + 1)) 'stuff to structure Inheader -= 1 'drop 1 from flag End Select End If If Not CBool(Inheader) Then 'if InHeader flag is now zero. ' : ' NOTES: This method uses classes EmailItems and EmailInfo.Item(Idy)." Then 'line continues? Idx += 1 'yes. From. DateData.Count .0 – David Ross Goben What follows is the GetEmailInfo() method. Content-Type.ContentTypeDataIsFilename = ContentTypeIsName 'save flag indicating if Attachment Page –277– .SubjectData = Trim(Mid(S. AlternateView and Attachment members.1 If CBool(InStr(S. If it is "7bit".NET Beyond the Scope of Visual Basic 6. the data should be ' decoded using the DecodeBase64() method. ToData.ContentType = ContentType 'store content type Itm. Date. If it is "quoted-printable". If the content-transfer encoding is set to "base64". which returns an EmailInfo class instance containing FromData. EmailItem and EmailInfo: '******************************************************************************* ' Function Name : GetEmailInfo ' Purpose : Break email down into its component parts. Boundaries. MessageBody. I + 1)) 'stuff to structure Inheader -= 1 'drop 1 from flag Case "date: " 'Found DATE field Info.Text)) Then If BuildingDataBlock Then Dim Itm As New EmailItem 'create a new item Itm. data. ' ' An EmailItem contains fields for FROM. Attachments. Nothing) 'strip any quotes Else ContentTypeData = sbAry(1) 'AlternateView.AlternateViews. Page –278– . so create a new EmailItem..Text) = 0 Then 'multipart.Text) = 0 Then ContentTypeIsName = True 'attachment if a filename specified (otherwise a view) sbAry = Split(sbAry(1).Trim().. Dim Itm As New EmailItem 'we will create it now. I + 1).MessageBody Is Nothing Then 'if the email lacks a border for message data.. 1) = ".MessageBody = Itm HaveMessageBody = True End If ContentTypeData = Nothing BuildingDataBlock = False End If SeekingEncoding = True S = Nothing Exit For End If 'store encoding 'store data 'reset accumulator 'already have a message body? 'if an attachment 'add an attachment 'otherwise an alternate view 'else stuff new item to message body 'indicate we now have a message body 'reset filename/charset 'turn off building flag 'turn block seeing on again 'purge current data Next End If '------------------------------------------------------' build data block '------------------------------------------------------If BuildingDataBlock Then ContentBody &= S & vbCrLf 'add a line to content data End If '------------------------------------------------------' if seeking encoding '------------------------------------------------------If CBool(Len(S)) AndAlso SeekingEncoding Then 'are we seeking TCE? Dim I As Integer = InStr(S. 10).") 'now check the content type data ContentType = sbAry(0) 'keep first part for ContentType If StrComp(Left(sbAry(0). assume ContentEncoding=base64 ContentEncoding = "base64" 'is blank. " "c) 'grab next line and trim tabs and spaces End If ContentTypeIsName = False 'init flag specifying a file as false Dim sbAry() As String = Split(ContentType. " "c) 'yes.Trim(Chr(9). Nothing) Boundaries.... check for field delimiter If CBool(I) Then 'did we find one? Select Case LCase(Left(S. and strip any quotes Dim Bnd As String = Trim(Mid(sbAry(1).Add(Bnd) 'and add a boundary ElseIf StrComp(Left(sbAry(1).ContentEncoding = ContentEncoding Itm. I + 1)) 'yes.Add(Itm) Else Info.Trim(Chr(9). so force it! '----------------------------------------------------------If Info. Itm. so stuff display character set If Len(Trim(Ary(Idx + 1))) = 0 Then 'if next line blank.Add(Itm) End If Else Info.. so assume base64 SeekingEncoding = False 'turn off seeking flag BuildingDataBlock = True 'turn on building data block flag Idx += 1 'bump to skip the blank line End If End If '=================================================== Case "content-transfer-encoding: " ContentEncoding = Mid(S." Then 'more to add? Idx += 1 'yes. CompareMethod. Itm. so grab second parameter 'get second part of second parameter (filename definition) ContentTypeData = sbAry(1). so grab data and trim tabs and spaces If Right(S.. CompareMethod... "multipart/". so a message body is not created. ". InStr(sbAry(1). check for types '======================================================= Case "content-type: " 'Content type? ContentType = Mid(S. I + 1).ContentEncoding = ContentEncoding 'store encoding.. 5).ContentTypeDataIsFilename = ContentTypeIsName 'save flag indicating if Attachment. so grab data SeekingEncoding = False 'turn off seeking flag BuildingDataBlock = True 'turn on building data block flag Idx += 1 'bump to skip required following blank line End Select End If End If Idx += 1 'bump array index Loop While Idx < MX '----------------------------------------------------------'some emails do not define borders. "name=".ContentBody = ContentBody ContentBody = Nothing If HaveMessageBody Then If ContentTypeIsName Then Info.Replace("""". Itm.. ": ") 'yes.ContentType = ContentType 'store content type. "=") 'multipart. " "c) 'yes.Enhancing Visual Basic . so grab second parameter (boundary definition).NET Beyond the Scope of Visual Basic 6. Itm.Trim(Chr(9). so bump index ContentType &= Ary(Idx).ContentTypeData = ContentTypeData 'save filename or character set.Replace("""".0 – David Ross Goben Itm. "=") + 1)). MessageBody = Itm End If Return Info End Function 'store data. To do this is really easy (see the full listing later. nice ’n’ easy! Page –279– . “Public Shared Sub BraindeadSimpleEmailSend(” and “Public Shared Function SendEmail(”. This creates the class shell: Public Class SMTP End Class I then decorate its heading with essential references and identification.ContentBody = ContentBody Info.. And that is all there is to creating the class. like so: Option Strict On Option Explicit On '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ' SMTP .NET Beyond the Scope of Visual Basic 6. It will be re-added if you do not type it in. Next. we modify the three method’s declarations by adding Shared after Public.Enhancing Visual Basic .vb” file extension. BrainDeadSimpleEmailSend().. within the SMTP class body. I embed in a class wrapper named SMTP. '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Imports System. being sure to include the imports I had needed for the three methods. if you want to just copy it). and SendEMail().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. For example. and type SMTP into the name field. I just select the menu options Project / Add Class….0 – David Ross Goben Itm. Do not worry about it maintaining the default “. Easy. 'and finally set the new item to the message body 'return with filled data block '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '******************************************************************************* ' Class Name : EmailItem (used by EmailInfo class) ' Purpose : Stores structure of an email block '******************************************************************************* Public Class EmailItem Public ContentType As String = Nothing 'CONTENT-TYPE data Public ContentTypeData As String = Nothing 'filename or text encoding Public ContentTypeDataIsFilename As Boolean = False 'True if ContentTypeData specifies a filename Public ContentEncoding As String = Nothing 'CONTENT-TRANSFER-ENCODING data Public ContentBody As String = Nothing 'raw data of block End Class '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '******************************************************************************* ' Class Name : EmailInfo (used by GetEmailInfo method) ' Purpose : Store component parts of an Email '******************************************************************************* Public Class EmailInfo Public FromData As String = Nothing 'FROM: Public ToData As String = Nothing 'TO: Public DateData As String = Nothing 'DATE: Public SubjectData As String = Nothing 'SUBJECT: Public MessageBody As EmailItem 'contents of message body Public AlternateViews As New List(Of EmailItem) 'list of alternate views Public Attachments As New List(Of EmailItem) 'list of attachments End Class Compiling Everything into a Class Library My email sending methods.Copyright © 2011 .2015 by David Ross Goben. QuickiEMail(). QuickiEMail(). breezy. BrainDeadSimpleEmailSend(). and SendEMail(). We would create the Utilities class exactly the same way as we did the SMTP class. (used by GetEmailInfo) (used by GetEmailInfo) Page –280– . Method to clean typical control translations. shortly).com". or Plain Text requires 8-bit code translation to 7-bit Quoted-Printable tags. "david. add a dot. 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). The only difference is.Copyright © 2011 . But that is no big deal. Structure used by GetTransferEncoding Provide easy access to [email protected] without having to instantiate either class. What this means is that you can just select the class name. As previously stated. we can still access a method within the SMTP class by simply specifying SMTP. And like with the SMTP class utilities. where each two characters prepresents a binary Byte. that we would no longer be referencing a locally compiled class file.Net.MediaTypes text.NET Beyond the Scope of Visual Basic 6. Break email down into its component parts. that is exactly what we have been doing. It is as easy as stepping on an upturned rake and rearranging your face with its handle.vb.TcpClient. because so far. Method to convert 8-bit code in an HTML message to 7-bit. we have already been talking about the poor thing as though it already exists.Net. Short-Form Convert HTML formatted text to plain text. When we build our class library. msg.2015 by David Ross Goben. Decode Base16-encoded files to their original format. "smtp. we would add the following items to this Class file. Determine if HTML text. But how would we reference this class library? No problem. or Utilities. or all of them. you will not have to instantiate a Utilities class (this class will be covered. It is not quite as easy for our POP3 class.com". This is because our POP3 class inherits from System.SendEMail("[email protected] Visual Basic .Mime. First of all. For example. But before we get to those simple steps. Method to force 8-bit code in a text message to 7-bit. and then import the class library into our project file. so we will need to in turn instantiate an instance of our POP3 class. to send an email.Sockets. because it is a Non-inheritable Class (note the NotInheritable verb in the class declaration). during the course of this article/novella (it is also fully listed at the end of this article): Method/Enum/Class Item DecodeBase64ToStr() DecodeBase64ToBytes() DecodeQuotedPrintable() DecodeHexDec() TextNeedsEncoding() Force7BitHtml() ForceQuotedPrintable() QConvertHTML2Text() ConvertHTML2Text() Enum MediaTypes GetMediaType() Enum TransferEncodings GetTransferEncoding() GetEmailInfo() Class EmailItem Class EmailInfo Description Decode Base64-encoded files to their original format and return a string. But we also want to create a Utilities class. After all. Enumeration used by GetMediaType. without losing any data. reference it from our application.methodname. We would add a new class and name it Utilities. how about we actually build this class library. "Test". False. We would then initially tattoo its new class shell like this: Option Strict On Option Explicit On '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ' Utilities .Text Public NotInheritable Class Utilities ░░░░░░░░░░░░░░░░░ ◄▬▬▬▬▬▬▬▬▬▬ Class Body is here End Class Next.TransferEncoding data. and to reiterate. Provide easy access to System. This should be invoked for all data coded Quoted-Printable. 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. Rich Text.ross. with the class library we are about to build. '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Imports System.0 – David Ross Goben Notice that you do not need to. and then specify the method you want to invoke. Decode Base64-encoded files to their original format and return a Byte Array. and cannot instantiate an instance of the SMTP class. you will also have to change all the Public methods defined within the SMTP and Utilities classes to Public Shared methods. Convert HTML formatted text to plain text.80micro.com") I will also take all my utilities and add them to a new class file named Utilities. all of which we have explored earlier. the methods for the SMTP and Utilities classes can be accessed by simply specifying SMTP or Utilities. select them. ● Now. Projects\VBNetEmail\VBNetEmail\obj\Release.vb. and Utilities. and open it. To use the Inbound POP3 email class. and Utilities. adding them to a Utilities class. make sure the References tab is selected. Remember. We will end up with a DLL file named VBNetEmail. in Project Explorer. create a new Class Library project named VBNetEmail. if you need to.dll (look within your Projects folder.) ● Select the menu options Build / Build VBNetEmail (be sure that you are building a Release version). actually. though later. we already have the class files we require: SMTP. and Utilities class files. select the menu options Project / Add Existing Item… Browse to the location where you saved your POP3. ● Once created. Choose the Up One Level button to back out of any current project. I even do this for my related utilities. I tend to test the class files within test projects. place the line “Imports VBNetEmail” above the first class declaration at the top of the file. such as those used within this article. SMTP. For subsequent projects. they are).Enhancing Visual Basic . you can simply choose the Recent Tab and very quickly select the VBNetEmail. Select the Browse Tab.dll file. I copy them to a class library. Within the Obj folder. we should further wrap these three classes within a Class Library project named VBNetEmail.vb. Finally.NET Beyond the Scope of Visual Basic 6. for it). in the project file that you will need to access the classes in. The first thing to do is to save off and close your project that contains the classes SMTP. then click the Add button. once proven. you can add it to other projects in just a few easy steps. We do not need it. and remember where you saved them! Copy them from the listings at the end of this article. it is the folder icon with a curved green up arrow: Drill down into the VBNetEmail project to find the Obj folder. This is incredibly easy to do. Class1.dll file and then select the OK button (if you have one-click enabled. • • • • • • • In the Project Properties for your new application that will use the VBNetEmail library. Building the VBNetMail Class Library To incorporate our three classes into a Class Library project named VBNetEmail is really easy. The three class files should then appear in your Project Explorer. hitting the dot. select and open the Release folder.vb. and build them to a DLL for easy access by our application. and then access its methods through that instance. Page –281– . ● Next. Accessing your New VBNetEmail Class Library DLL from another Project Once you have built the VBNetEmail. OK may be auto-selected). and choose the ADD button. and then browse to your VBNetEmail Class Library project (you should find yourself initially within your current project). (We now have all we need to compile our class library. Within the Release folder. you will see a Class File. POP3. you must instantiate an instance of the class.0 – David Ross Goben Once we have all three classes fully defined (and there are no errors laughing at us).vb. and selecting the method you want to use. click the VBNetEmail. You will now find a reference to VBNetEmail listed in your References list. Simply right-click it and choose Delete. You can now access your classes as though they were included in your project (well.dll file. POP3. Leave blank if the same as strFrom. SSLPassword. strBody) 'send email using text/plain content type and QuotedPrintable encoding Catch e As Exception 'if error. "authsmtp.juno.Exclamation. If this field and SSLUsername ' : are blank.com ' strSubject: Brief text regarding what the email concerns.Message. smtpPort) 'create new SMTP client smtpEmail. Optional ByVal SSLDomain As String = Nothing) As Boolean Try Dim smtpEmail As New Mail. SSLPassword. ByVal smtpHost As String) Dim smtpEmail As New Mail. "smtp.Copyright © 2011 .0 – David Ross Goben You are now ready for some serious email processing.Enhancing Visual Basic .gmail.com". we must create a new credential If Not CBool(Len(SSLUsername)) Then 'if SSLUsername is blank.com". though 465 (SSL) or 587 (TLS) are becoming popular.comcast. David Dingus <daviddingus@att. "Bubba Dingus" <bob. "authsmtp. this is the username to use for creating a credential.Credentials = New NetworkCredential(strFrom. ' SSLUsername: If usesSLL is True. such ' : as "smtp.com ' strSubject: Brief text regarding what the email concerns. ByVal smtpHost As String. strBody) 'send email End Sub '******************************************************************************* ' Function Name : QuickiEMail ' Purpose : Send a simple email message (but packed with a lot of muscle) '=============================================================================== 'NOTES: strFrom : Full email address of who is sending the email.. David Dingus <daviddingus@att. set this parameter.dingus@cox. Optional ByVal SSLUsername As String = Nothing. SSLDomain) Else smtpEmail. strTo.net ' strTo : Full email address of who to send the email to. Optional ByVal SSLPassword As String = Nothing.OkOnly Or MsgBoxStyle. report it MsgBox(e. otherwise.SmtpClient(smtpHost. '******************************************************************************* Public Shared Function QuickiEMail(ByVal strFrom As String.juno. such ' : as "smtp.Net 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' Class Name : SMTP ' Purpose : SMTP Static Interface Class 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Public NotInheritable Class SMTP '******************************************************************************* ' Function Name : BrainDeadSimpleEmailSend ' Purpose : Send super simple email message (works with most SMTP servers) '=============================================================================== 'NOTES: strFrom : Full email address of who is sending the email. ' smtpHost : This is the email host you are using for sending emails. ByVal strSubject As String. [email protected](strFrom. ByVal strSubject As String.NET Beyond the Scope of Visual Basic 6. smtpEmail. The Complete SMTP Class File Option Strict On Option Explicit On '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ' SMTP .com". ' smtpHost : This is the email host you are using for sending [email protected]> or daviddingus@att. "Bubba Dingus" <bob. then return a success flag End Function Page –282– . Most servers default to 25.UseDefaultCredentials = True 'use default credentials Else 'otherwise. strSubject.SmtpClient(smtpHost) 'create new SMTP client using TCP Port 25 smtpEmail. "Mail Send Error") Return False 'return a failure flag End Try Return True 'if no error. then default credentials will be used (but this only works on local. etc. ' usesSLL : If this value is TRUE. ie. ByVal strTo As String. Optional ByVal usesSSL As Boolean = False. then use SSL/TLS Authentication protocol for secure communications. ByVal strBody As String. intranet servers)..net> or daviddingus@att. strSubject. ' SSLDomain : If creating a credential when a specific domain is required. ie. ' strBody : text that comprises the message body of the email. MsgBoxStyle. ByVal strBody As String.Credentials = New NetworkCredential(SSLUsername. Optional ByVal smtpPort As Integer = 25.comcast. '******************************************************************************* Public Shared Sub BrainDeadSimpleEmailSend(ByVal strFrom As String.net ' strTo : Full email address of who to send the email to. ByVal strTo As String.Send(strFrom. ' strBody : text that comprises the message body of the email. ' smtpPort : TCP Communications Port to use. ' SSLPassword: If usesSLL is True. use strFrom smtpEmail. leave it blank.dingus@cox. strTo. this is the password to use for creating a credential. etc.net". '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Imports System. ie.2015 by David Ross Goben. SSLDomain) End If End If End If smtpEmail.com> or bob.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.net".com> or bob. AlternateView object. ' SSLUsername: If usesSLL is True. separate each filepath using a semicolon (.From = New Mail. . ' SSLDomain : If creating a credential when a specific domain is required. ByVal strSubject As String.Base64) by placing them within parentheses. "authsmtp. Optional ByVal AltView As Mail.Base64 ' StrCC : Send "carbon copies" of email to this or these recipients.") '(possible list of email addresses. then default credentials will be used (but this only works on local.. ' : For example: AltView. otherwise. For example.CC.com> ' : If multiple recipients.Mail. set AltView.To. ByVal IsHTML As Boolean. "smtp.Mime. c:\jokes.Mime.Net.AlternateView = Nothing. May be raw text or HTML code.) ' strBcc : Blind Carbon Copy.gmail.ToString) ' : Base64 (acquired by System. Optional ByVal strAttachments As String = Nothing.dingus@cox. David Dingus <[email protected] (application/octet-stream.Base64.Add(Trim(Ary(Idx))) 'add each recipent (hidden recipents) Next End If Page –283– . "Bubba Dingus" <bob.") 'add TO to mail message (possible list of email addresses. ". Optional ByVal smtpPort As Integer = 25. Optional ByVal strCC As String = Nothing.SevenBit. intranet servers). Image.IsBodyHtml = True '------------------------------------------If AltView IsNot Nothing Then 'if an alternate view of plaint text message is defined. Base64) ' : Where: The MediaType is determined from the System. this is the username to use for creating a credential.Text.Net. ie.Add(AltView) 'add the alternate view End If '------------------------------------------If CBool(Len(strCC)) Then 'add CC (Carbon Copy) email addresses to mail message Ary = Split(strCC.Net. For example: ' : C:\My Files\API32. ByVal strBody As String.TransferEncoding = Mime. Optional ByVal strBcc As String = Nothing.") For Idx As Integer = 0 To UBound(Ary) If Len(Trim(Ary(Idx))) <> 0 Then .MediaTypeNames. ' IsHTML : True if the strBody data is HTML.Net.Net.ContentType. such ' : as "smtp. C:\telnet. is determined by the following the values specified with the ' : System.MediaTypeNames class. separated each with ". ' SSLPassword: If usesSLL is True. If this field and SSLUsername ' : are blank. ' : The second parameter. separate each full email address using a semicolon (. Leave blank if the same as strFrom.Add(Trim(Ary(Idx))) 'add each TO recipent (primary recipients) Next '------------------------------------------. ' : If need be. or the type of data that would be contained within an HTML Body block.ToString) ' smtpPort : TCP Communications Port to use.ContentType.Mime. separate each full email address using a semicolon (.TransferEncoding to properly format the AlternateView.Mime.Net.Mime. or a list of filepaths to send to the recipient.NET Beyond the Scope of Visual Basic 6. ".) ' strSubject: Brief text regarding what the email concerns. was defined by acquiring System.ToString) ' : SevenBit (acquired by System.) ' strAttachments: A single filepath.MailAddress(strFrom) 'add FROM to mail message (must be a Mail Address object) '------------------------------------------Dim Ary() As String = Split(strTo.Enhancing Visual Basic .net".Add(Trim(Ary(Idx))) 'add each recipent Next End If '------------------------------------------If CBool(Len(strBcc)) Then 'add Bcc (Blind Carbon Copy) email addresses to mail message Ary = Split(strBcc.com".") For Idx As Integer = 0 To UBound(Ary) If Len(Trim(Ary(Idx))) <> 0 Then . ' : If multiple recipients.) (C:\my data\win32.txt (text/plain.MediaTypeNames. then follow the attachment name with the MediaType and optional encoding (default is ' : application/octet-stream. '. '******************************************************************************* Public Shared Function SendEMail(ByVal strFrom As String. ie..Net.Subject = strSubject 'add SUBJECT text line to mail message '------------------------------------------. ' strBody : text that comprises the message body of the email. Optional ByVal SSLUsername As String = Nothing. leave it blank.QuotedPrintable. ' : If multiple attachments.com". ByVal smtpHost As String. such as Rich Text or HTML.") For Idx As Integer = 0 To UBound(Ary) If Len(Trim(Ary(Idx))) <> 0 Then .TrasperEncoding enumeration: ' : QuotedPrintable (acquired by System. ByVal strTo As String. this is the password to use for creating a credential. the above content type. SevenBit). ' smtpHost : This is the email host you are using for sending emails. Optional ByVal SSLPassword As String = Nothing.TransferEncoding. separate each full email address using a semicolon (.AlternateViews.Body = strBody 'add BODY text of email to mail message. .comcast.Mime. which ' : can specify Application.RichText ' : AltView.txt.Text.Plain.MediaType = Mime. set this parameter.juno. separated each with ".net> ' strTo : Full email address of who to send the email to. ".") '(possible list of email addresses.TransferEncoding.rtf) ' : The contents of the attachments will be encoded and sent. separated each with ". Hide this or these recipients from view by others. Optional ByVal usesSSL As Boolean = False. Optional ByVal SSLDomain As String = Nothing) As Boolean Dim Email As New Mail.Bcc.MailMessage 'create a new mail message With Email .IsBodyHtml = IsHTML 'indicate if the message body is actually HTML text. then use SSL/TLS Authentication protocol for secure communications.TransferEncoding. Most servers default to 25. etc.TransferEncoding. or Text lists. ' usesSLL : If this value is TRUE. ' AltView : A System. ' : If you wish to send the attachment by specifying content type (MediaType) and content transfer encoding ' : (Encoding). ' : If multiple recipients.0 – David Ross Goben '******************************************************************************* ' Function Name : SendEMail ' Purpose : Send a more complex email message '=============================================================================== 'NOTES: strFrom : Full email address of who is sending the email. ' : "text\plain".MediaType and AltView. and separated by a comma. Encoding. ' Oh! And I include REAL comments. smtpPort) 'create new SMTP client on the SMTP server SmtpEmail. "quoted-printable" Atch.I .Credentials = New NetworkCredential(SSLUsername.Exclamation. System.Add(Atch) 'add attachment to email End If End If Next End If End With '----------------------------------------------------------------------'now open the email server.Attachment(attach) 'create a new attachment Dim fmts() As String = Split(Fmt.") 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.ContentType. SmtpEmail. send the email.MediaType = Mime.Net.TransferEncoding = Mime.Base64 'set encoding to base64 . "(") 'check for formatting instructions If CBool(I) Then 'formatting present? Dim Fmt As String 'yes.0 – David Ross Goben '------------------------------------------If CBool(Len(strAttachments)) Then 'add any attachments to mail message Ary = Split(strAttachments. so determine which type of instruction to process Case 0 'index 0 specified MediaType Atch.NET Code was inspired by C# code originally written Randy Charles Morin. 'author of KBCafe. Dim I As Integer = InStr(attach.Copyright © 2011 – 2015 by David Ross Goben.") '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.OkOnly Or MsgBoxStyle. encoded in effiecient Base64) Dim Atch As New Mail.") '(possible list of file paths.Attachment(attach)) 'add filepath (if no format specified. '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' Class POP3 ' POP3 Inbound Email Support ' 'This VB.Add(Atch) 'add attachment to email Else '.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. ' cleaned up a lot of clutter.Add(New Mail. I + 1.Attachments.MediaType = Fmt 'set media type to attachment Case 1 'index 1 specifes Encoding Select Case LCase(Fmt) 'check the encoding types and process accordingly Case "quotedprintable". ".TransferEncoding. "Mail Error") Return False 'return failure flag End Try Return True 'return success flag End Function End Class The Complete POP3 Class File Option Strict On Option Explicit On '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ' POP3 . ".SmtpClient(smtpHost. Catch e As Exception 'if error.Substring(0. Len(attach) .1)) 'strip format data from the attachment path Dim Atch As New Mail.TransferEncoding = Mime. Try Dim SmtpEmail As New Mail.MediaTypeNames..TransferEncoding = Mime.Application..Attachments. 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Imports System. ' forcing a complete rewrite.com.Base64 End Select End Select End If Next .TransferEncoding = Mime.TransferEncoding. SSLDomain) Else SmtpEmail.QuotedPrintable Case "sevenbit".NET Beyond the Scope of Visual Basic 6. report it MsgBox(e.Credentials = New NetworkCredential(strFrom. so set up format cache Fmt = Mid(attach..Attachment(attach) 'add attachment to email Atch. SSLDomain) End If End If End If SmtpEmail..Text Page –284– . separated each with ".ContentType. SSLPassword. I .SevenBit Case Else Atch..Attachments.UseDefaultCredentials = True 'use default credentials Else 'otherwise. ' ' I have optimized the heck out of the code to speed I/O and program execution.Message.TransferEncoding.. I have added MANY language and POP3 enhancements. and added Port and SSL support.. use strFrom SmtpEmail. MsgBoxStyle. we must create a new credential If Not CBool(Len(SSLUsername)) Then 'if SSLUsername is blank. "7bit" Atch.Send(Email) 'finally.TransferEncoding.Enhancing Visual Basic .Octet 'returns string constant "application/octet-stream" Atch.1) 'get format data attach = Trim(attach. SSLPassword.. 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.Substring(0. Throw an error and return False if not. and a if SSL authentication is required ' : ' Returns : Nothing ' : ' Typical TelNet I/O: 'telnet mail.SslStream(Stream) 'yes.CheckResponse() Then Exit Sub ' End If 'if the username is defined (some servers will reject submissions) 'submit user name 'exit if an error was encountered If CBool(Len(Password)) Then Me.Response() 'check response (and save response line) If (LastLineRead.TcpClient 'this class shall inherit all the functionality of a TC/IP Client '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private Stream As Sockets. InPort) 'now connect to the server via our base class (signature differs from our method) Catch Throw New POP3Exception("Cannot connect. Port. 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.AuthenticateAsClient(Server) 'add authentication as a client to the server End If If Not Me. ' 'and if so.83 server ready 'USER myusername (submit) '+OK User name accepted.NET Beyond the Scope of Visual Basic 6. User Name.Enhancing Visual Basic . _ ByVal Password As String.0 – David Ross Goben '------------------------------------------------------------------------------' Class Name : POP3 ' Purpose : POP3 TCP Client Interface Class '------------------------------------------------------------------------------Public Class POP3 Inherits Sockets.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 the we do with a POP3 object) ' Purpose : Connect to the server using the Server.net 110 (submit) '+OK POP3 mail.") Return False 'return failure flag End If Return True 'Indicate that we are in the TRANSACTION state) End Function '******************************************************************************* Page –285– . error) '******************************************************************************* Public Function CheckResponse() As Boolean If Not Me. disconnect that session UsesSSL = UseSSL 'set flag True or False for SSL authentication Try Connect(Server.domain. so build an SSL stream object on top of the non-SSL Network Stream SslStream. Check Server.IsConnected() Then Return False ' 'exit if not in TRANSACTION mode LastLineRead = Me. ' : ' NOTE : All status responses from the server begin with: ' : +OK (OK.Submit("PASS " & Password & vbCrLf) If Not Me. Username or Password for errors") Return End Try Stream = GetStream() 'before we can check for a response. ' : ' Returns : Boolean Flag. password please 'PASS mysecretpassword (submit) '+OK Mailbox open. Password. throw an exception Throw New POP3Exception("Not Connected to an POP3 Server. 3 messages (the server locks and opens the appropriate maildrop) '******************************************************************************* Public Overloads Sub Connect(ByVal Server As String. _ Optional ByVal UseSSL As Boolean = False) If Connected Then Disconnect() ' 'check underlying Boolean flag to see if we are presently connected. Success.Submit("USER " & Username & vbCrLf) If Not Me. or request granted) ' or : -ERR (NAGATIVE.CheckResponse() Then Exit Sub ' End If End Sub 'if the password is defined (some servers will reject submissions) 'submit password 'exit if an error was encountered '******************************************************************************* ' Function Name : CheckResponse ' Purpose : Check the response to a POP3 command ' : ' Returns : Boolean flag. Throw an error and return False if not.net v2011. 3) <> "+OK") Then 'OK? Throw New POP3Exception(LastLineRead) 'no.domain. _ ByVal Username As String.CheckResponse() Then Exit Sub ' 'exit if an error was encountered If CBool(Len(Username)) Then Me. we first have to set up a non-SSL stream If UsesSSL Then 'do we also need to use SSL authentication? SslStream = New Security. True if connected to server '******************************************************************************* Public Function IsConnected() As Boolean If Not Connected Then 'if not connected. Return True for Success. _ Optional ByVal InPort As Integer = 110. 0.0 – David Ross Goben ' Function Name : Response ' Purpose : get a response from the server (read from the mail stream into a buffer) ' : ' Returns : string of data from the server ' : ' NOTE : If a dataSize value > 1 is supplied. so write SSL buffer using the SslStream object Else Stream.ReadByte 'read a byte from SSLstream Else 'else process through general TCP Stream byteRead = Stream. 0. the POP3 session enters the UPDATE state. then those number of bytes will be streamed in.Write(WriteBuffer. ' ' Whether the removal was successful or not.ReadByte() (function returns Int32) Do If UsesSSL Then 'process through SSL Stream if secure stream byteRead = SslStream.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.GetString(ServerBufr. 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. the server then releases any exclusive-access lock on the maildrop ' and closes the TCP connection.NET Beyond the Scope of Visual Basic 6. Linefeed code) read in If Index > UBound(ServerBufr) Then 'if the index points past the end of the buffer. such as a resource shortage.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.Length) 'yes. the ' maildrop may result in having some or none of the messages marked as deleted be removed. the POP3 session does NOT ' enter the UPDATE state and MUST NOT remove any messages from the maildrop. Index. If UsesSSL Then 'process through SSL Stream if secure stream sz = SslStream...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. ' : Though some servers do allow for this. '******************************************************************************* Public Sub Submit(ByVal message As String) Dim enc As New ASCIIEncoding 'medium for ASCII representation of Unicode characters Dim WriteBuffer() As Byte = enc. we should NEVER assume it. WriteBuffer. 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. dtsz) 'read a server-defined block of data from Network Stream End If If sz = 0 Then Return Nothing ' 'we lost data.Read(ServerBufr. 0. ReDim Preserve ServerBufr(Index + 255) 'then bump buffer for another 256 bytes. In no case may the ' server remove any messages not marked as deleted.Write(WriteBuffer.Enhancing Visual Basic . encountered while removing messages. WriteBuffer. the data will be ' : read in a line at a time.. 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. 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. otherwise. '******************************************************************************* Page –286– .. dtsz) 'read a server-defined block of data from SSLstream Else 'else process through general TCP Stream sz = Stream. the POP3 session terminates ' but does NOT enter the UPDATE state. 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 . "pass pw1Smorf" would not be acceptable. ' ' The POP3 server removes all messages marked as deleted from the maildrop and replies as to the status of this ' operation. Index. such as "PASS pw1Smorf".Read(ServerBufr.) ' ' If a session terminates for some reason other than a client-issued QUIT command. If there is an error. ' (Note that if the client issues the QUIT command from the AUTHORIZATION state.GetBytes(message) 'converts the submitted string into to a sequence of bytes If UsesSSL Then 'using SSL authentication? SslStream. Dispose() 'dispose of created SSL stream object if so SslStreamDisposed = True End If End Sub '******************************************************************************* ' Function Name : Statistics ' Purpose : Get the number of email messages and the total size as any integer array ' : ' Returns : 2-element interger array. " "c) 'separate by spaces.Add(msg) 'add a new entry into the retrieval list Loop Return retval 'return the list End Function '******************************************************************************* ' Function Name : GetHeader ' Purpose : Grab the email header and optionally a number of lines from the body ' : ' Returns : Gets the Email header of the selected email.ByteCount = Integer.Parse(msgInfo(1)) 'get the size of the email message msg. ' : ' Typical telNet I/O: 'TOP 1 0 (submit request for record 1's message header only.Parse(msgInfo(1)) 'get the number of emails Result(1) = Integer.Submit("STAT" & vbCrLf) 'submit Statistics request LastLineRead = Me. return nothing ' 'get a list of emails waiting on the server for the authenticated user ' Dim retval As New ArrayList 'set aside message list storage Do Dim response As String = Me. " "c) 'separate by spaces.Response 'check response If (response = ". which divide its fields msg.0 – David Ross Goben Public Sub Disconnect() Me.Retrieved = False 'indicate its message body is not yet retreived retval. 3) <> "+OK") Then 'OK? Throw New POP3Exception(LastLineRead) 'no.Response 'check response If (LastLineRead.Submit("LIST" & vbCrLf) 'submit List request If Not CheckResponse() Then Return Nothing ' 'check for a response. so throw an exception Return Nothing 'return failure flag End If Dim msgInfo() As String = Split(LastLineRead.MailID = Integer.Parse(msgInfo(0)) 'get the list item number msg. (end of records terminator) '******************************************************************************* Public Function List() As ArrayList If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode Me. (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) '.NET Beyond the Scope of Visual Basic 6. that number of body lines will ' : be returned. The returned object is the submitted POP3Message." & vbCrLf) Then 'done with list? Exit Do 'yes End If Dim msg As New POP3Message 'establish a new message Dim msgInfo() As String = Split(response. but if an error.Enhancing Visual Basic . If an integer value is provided. ' : 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("QUIT" & vbCrLf) 'submit quit request CheckResponse() 'check response If UsesSSL Then 'SSL authentication used? SslStream. (end of record terminator) '******************************************************************************* Page –287– . which divide its fields Dim Result(1) As Integer Result(0) = Integer. 0=no lines of body) '+OK Top of message follows ' xxxxx (header for current record is transmitted) '.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 '.Substring(0. MailID.ByteCount) 'grab message line 'the stream reader automatically convers the NewLine code. but just close out the POP3 object.Submit("DELE " & msgHdr.Message = Me." & vbCrLf Then 'end of data? Exit Do 'If so. if any ' 'now process message data by binding the lines into a single string ' Do Dim response As String = Me.ByteCount = Len(msg. and Windows Mail do this..Response 'grab message line If response = ". ' ' 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." & vbCrLf is still pending. then exit loop End If msg. ' : ' 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.Enhancing Visual Basic . and Windows Mail do this. return nothing msg.MailID. For example.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.MailID. It is an option under Juno and Gmail. So. but if an error. if we do not submit a POP3 ' QUIT (the Disconnect() method).Submit("RSET" & vbCrLf) 'submit Reset request CheckResponse() 'check response End Sub Page –288– . vbLf. the trailing ". Do Dim strData As String = Response() 'grab more data If strData = ". ' : ' NOTE: Some email servers are set up to automatically delete an email once it is retrieved from the server. done with the loop if so End If msg. so the files is not yet 'fully read.NET Beyond the Scope of Visual Basic 6. So we must retreive more data to account for this.ToString & vbCrLf) If Not CheckResponse() Then Return Nothing ''check for a response. It is an option under Juno and Gmail." & vbCrLf Then 'end of data? Exit Do 'yes.. a files that was 233 lines will therefore have 233 more characters not 'yet read from the files when it has reached its reported data size.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.Message = Nothing 'erase current contents of the message. ' Outlook Express.Submit("RETR " & msg. Outlook. Outlook.0 – David Ross Goben Public Function GetHeader(ByRef msg As POP3Message. ' Outlook Express.ToString & " " & BodyLines.ToString & vbCrLf) 'issue request for indicated message number If Not CheckResponse() Then Return Nothing ' 'check for a response. Even so. and its ByteCount property properly ' : fitted to the message size. the message(s) will not be deleted. Optional ByVal BodyLines As Integer = 0) As POP3Message If Not IsConnected() Then Return Nothing ' 'exit if not in TRANSACTION mode Me. 'But even if this was not the case. to vbCrLf. but if an error.Submit("TOP " & 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. So we will scan these in. return nothing msg.Message &= strData 'else tack data to end of message Loop 'keep trying msg. but the reported email size does not ' account for them. ' most Windows-based server-processors will add an additional CR for each LF. (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. ' ' Typical telNet I/O: 'RETR 1 (submit request to retrive 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 retreived as one long string) '.Response(msg. 0 – David Ross Goben '******************************************************************************* ' Function Name : NOOP (No Operation) ' Purpose : Does nothing. False if disconnected.Finalize() 'then do normal finalization End Sub End Class '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' Class Name : POP3Message ' Purpose : POP3 message data 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Public Class POP3Message Public MailID As Integer = 0 'message number Public ByteCount As Integer = 0 'length of message in bytes Public Retrieved As Boolean = False 'flag indicating if the message has been retrieved Public Message As String = Nothing 'the text of the message Public Overrides Function ToString() As String Return Message End Function End Class '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' Class Name : POP3Exception ' Purpose : process exception ' NOTE : This is a normal exception. and issues a ' NOOP to reset the server timer. else True if connected.Enhancing Visual Basic .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.New(str) End Sub End Class Page –289– . but we wrap it to give it an identy ' : that can be associated with our POP3 class 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Public Class POP3Exception Inherits ApplicationException Public Sub New(ByVal str As String) MyBase. so do it End If MyBase. ' ' 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. ' : ' NOTE: This NO OPERATION command is useful when you have a server that automatically disconnects after a certain idle ' period of activity.Dispose() 'no. This command can be issued by a timer that also monitors users inactivity.NET Beyond the Scope of Visual Basic 6. Juts gets a position response from the server ' : ' Returns : Boolean flag. vbLf). Nothing). it returns a decoded string.Replace("=" & vbCrLf. Idx << 1. ' : ' Returns : Provided a raw message string block. Chr(idx)) 'replace hex data with single character code Next Return Msg. which require a leading zero Msg. ' : ' Returns : Decoded String ' : ' NOTES : note that the lone vbCrLf at the end of lines is filtered out. '******************************************************************************* Public Shared Function DecodeQuotedPrintable(ByVal Message As String. ' : This should be invoked for all data coded Quoted-Printable. vbCr). plus any line wrap ' : terminators at the end of lines to Nothing.ToString 'return result string End If End Function '========================================================================== '========================================================================== Page –290– .Replace("=" & Mid(HxData. and "=3D" to "=". 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("=20". '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Imports System.2015 by David Ross Goben.ToString Else 'perform total cleaning 'store 2-character hex values that require a leading "0" Dim HxData As String = "X0102030405060708090A0B0C0D0E0F" For Idx As Integer = 1 To &HF 'initially process codes 1-15. ' : ' : A StringBuilder object will be used.GetChars(DecodeBase64ToBytes(strData)) End Function '******************************************************************************* ' Function Name : DecodeBase64ToBytes ' Purpose : Decode a provided raw email message string that is encoded to Base64. Nothing)) End Function '========================================================================== '========================================================================== '******************************************************************************* ' Function Name : DecodeQuotedPrintable ' Purpose : Method to clean typical control translations. "=").Replace("=0A". " ").Replace("=" & Hex(idx). or all of them.Replace("=" & vbCrLf. which will very quickly ' : do a replacement of all control code translations using fewer ' : resources. ' : ' NOTES : Typical cleaning involves changing "=0D" to vbCr.NET Beyond the Scope of Visual Basic 6.Replace(vbCrLf.Replace("=0D".FromBase64String(strData. 2).Copyright © 2011 . and what resources that are used will be instantly ' : flushed when the method exits.0 – David Ross Goben The Complete Utilities Class File Option Strict On Option Explicit On '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ' Utilities . '******************************************************************************* 'this modification returns a Byte Array of the Base64 encoded source data Public Shared Function DecodeBase64ToBytes(ByVal strData As String) As Byte() Return Convert. ' : ' Returns : Decoded binary Byte Array ' : ' NOTES : note that the lone vbCrLf at the end of lines is filtered out. "=0A" to vbLf.UTF8. Nothing)) If QuickClean Then 'perform a quick clean (clean up common basics) Return Msg.Text Public NotInheritable Class Utilities '******************************************************************************* ' Function Name : DecodeBase64ToStr ' Purpose : Decode a provided raw email message string that is encoded to Base64. ' : "=20" to a space. '******************************************************************************* Public Shared Function DecodeBase64ToStr(ByVal strData As String) As String Return Encoding.Replace("=3D".Enhancing Visual Basic . Chr(Idx)) 'replace hex data with single character code (SHIFT is faster) Next For idx As Integer = &H10 To &HFF 'process the whole 8-bit extended ASCII gambit Msg. ToString & ". 1)) 'get a single character from the source Select Case C 'check each character Case Is > &H7F. For Example.Append(ChrW(C)) 'else save text regardless End Select Next Return Sb. the source contains 8-bit data ' : and will be encoded by server.0 – David Ross Goben '******************************************************************************* ' Function Name : DecodeBinHex ' Purpose : Decode a provided raw email message string that is encoded to BinHex. the encoding of the data would be ' : forced to change from quoted-printable to Base64.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. which will ensure ' : that it will still be displayed on the HTML page.48 If CR > 10 Then CR -= 7 If Index > UBound(Result) Then ReDim Preserve Result(Index + 255) End If Result(Index) = CByte(CL * 16 + CR) Index += 1 Next ReDim Preserve Result(Index ."F" to 0-F 'do the same for the right hex digit 'bump by 256 (allow for Index offset) 'stuff byte value 'bump index 'set array to final size 'return the final result '========================================================================== '========================================================================== '******************************************************************************* ' Function Name : TextNeedsEncoding ' Purpose : Determine if HTML text.NET ' : SMTP processor will force this code to be encoded to Base64. it will return a string ' : containing HTML code that does not have any 8-bit data embedded. ' : even if only a single byte is 8-bit. Is < 0 'if 8-bit or unicode code Sb. it returns a boolena flag.UTF8. ' : ' Returns : Decoded String ' : ' NOTES : note that the lone vbCrLf at the end of lines is filtered out. but the HTML ' : souce code will no longer carry an actual 8-bit value. 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 default .Append("&#" & C.NET Beyond the Scope of Visual Basic 6.ToUpper) Dim Result() As Byte 'init output buffer ReDim Result(UBound(Src) \ 2) 'set initial dimension to 1024 bytes (includes offset 0) Dim Index As Integer = 0 'init index for Result() array For Idx As Integer = 0 To UBound(Src) Step 2 Dim CL As Integer = Src(Idx) . ' : ' Returns : Provided a string containing HTML code. but this ' : would be best served in Attachments and Alternate Views.".ToString End Function '========================================================================== '========================================================================== Page –291– . Idx.Replace(vbCrLf. code 149 (•) is an 8-bit ' : value that can be changed to HTML "&#149. '******************************************************************************* 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.1) Return Result End Function 'scan the string. '******************************************************************************* Public Shared Function DecodeBinHex(ByVal StrData As String) As Byte() Dim Src() As Byte = Encoding. The ForceQuotedPrintable() ' : method performs essential conversions for non-HTML text.GetBytes(StrData. Rich Text. '******************************************************************************* Public Shared Function TextNeedsEncoding(ByVal Message As String) As Boolean Dim Byt() As Byte = Encoding. ' : If the returned value is true. ' : ' Returns : Provided a source string. ' : ' : To avoid this. 2 hex characters at a time 'Convert "0" . then they are converted into a special ' : 7-bit HTML Entity Number.48 If CL > 10 Then CL -= 7 Dim CR As Integer = Src(Idx + 1) . Nothing). If such ' : code had not been corrected. ' : ' NOTES : If text data contains 8-bit values.") 'convert to 7-bit HTML ecoder Case Else Sb.UTF8. ' : ' NOTES : If any characters in an HTML text string are 8-bit (values ' : greater than 127). because that ' : would be the only way the email processor could guarantee that ' : the email text was fully intact.Enhancing Visual Basic . or Plain Text requires ' : 8-bit code translation to 7-bit Quoted-Printable tags. because that would be the only way the email processor ' : Base64.Replace("&amp. ' : For Example. so decode again and return. ' : though most of these sysmbols will not be encountered in most ' : HTML documents we produce.Enhancing Visual Basic . """").Replace("&apos. "<[^>]*>". but the text data will no longer carry an actual ' : of the data would be forced to change from quoted-printable or 7bit ' : to Base64. you would know that you would need to ' : double-decode the text.". However.Append("=" & Hex(B)) 'convert to 7-bit tag Case Else Sb. Because unencoded null codes ' : are not permitted in email data. typically those that simply allow you ' : to preview email messages.Append(Chr(B)) 'else save text regardless End Select Next Return Sb. _ "'"). even ' : though this is typically not an issue. ' : ' NOTES : if any characters in a text string are 8-bit (values greater ' : than 127).SubString(0. Further.Replace("&quot. or will not bother with it.UTF8. RFC 2045 requires email handlers to support it. without fully loading them."..0 – David Ross Goben '******************************************************************************* ' Function Name : ForceQuotedPrintable ' Purpose : Force 8-bit code in a text message to 7-bit. ". "<"). ">").". it will return a Plain Text string ' : with all HTML codes and formatting removed from it.Replace(". return result of decoding ' : End If '******************************************************************************* Public Shared Function ForceQuotedPrintable(ByVal Message As String) As String Dim Byt() As Byte = Encoding.Replace("&gt.". '******************************************************************************* Public Shared Function QConvertHTML2Text(ByVal HTMLText As String) As String Return RegularExpressions. " "). will not ' ' know how to support Base64.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. 3) "=00" Then 'tagged as pre-encoded? ' : Return DecodeQuotedPrintable(Result.". it will return a Plain Text ' : string with HTML code removed. and all the "=xx" byte-translations. would be ' : reinterpreted as "=3Dxx". so passing through a second time would ' : properly convert the additional encoding. But regardless of that.Replace("&lt. but simply ' : display the raw data. and the returned string is 7-bit. without data loss. However. ' : ' NOTE : Numerous of these conversions will convert the text to 8-bit. ' : because if this translated code was afterward encoded as Quoted' : Printable. '******************************************************************************* Page –292– . you ' : may have to further convert this using ForceQuotedPrintable() ' : to maintain Quoted-Printable encoding and avoid Base64. because that would be the only way the email processor ' : could guarantee that the email text was fully intact. ' : ' Returns : Provided a source string that contains 8-bit data.NET Beyond the Scope of Visual Basic 6. which will ensure that it will still be displayed ' : in the text. ' : you will have to use the DecodeQuotedPrintable() method to convert ' : it back to its original text form. code 149 (•) is an 8-bit value that can be changed ' : to hex "=95".Replace("&nbsp. 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. by checking the ' : text startiing with "=00". some few really ' : primitive email readers.".ToString End Function '========================================================================== '========================================================================== '******************************************************************************* ' Function Name : QConvertHTML2Text ' Purpose : Short-Form Convert HTML formatted text to plain text ' : ' Returns : Provided a simple HTML source string. "&"). the 8-bit ' : data is converted to Hex-Tags. Also. A second pass would be required.".SubString(3)) 'yes. "").") End Function '========================================================================== '========================================================================== '******************************************************************************* ' Function Name : ConvertHTML2Text ' Purpose : Convert HTML formatted text to plain text ' : ' Returns : Provided a complex HTML string. 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). if you wish ' : to make this conversion the main body message of an email. ' : ' : The Encoded text will begin with "=00". then they are converted into special 7-bit tags.Replace(HTMLText.Regex. which DecodeQuotedPrintable() would ' : convert back to "=xx". less initial null byte ' : Else ' : Return Result 'otherwise. ".Replace("&Zeta.".Replace("&Yuml. "Õ").".Replace("&bdquo.Replace("&spades.". _ "ñ"). _ "⌋⌋ ").".Replace("&acirc.". "’"). _ "¨"). _ "Ô").".".".0 – David Ross Goben Public Shared Function ConvertHTML2Text(ByVal HTMLText As String) As String 'instantiate an initially blank StringBuilder object Dim Sb As New StringBuilder() 'first remove leading whitespace of each line and append the result to the StringBuilder Dim ary() As String = Split(HTMLText.Replace("&divide. vbCrLf) For Each S As String In ary Sb.". "¾"). "ê"). "Ë").Replace("&raquo.Replace("&cup.Replace("&frac14.Replace("&Agrave. _ "¸").".Replace("&szlig. "ß").Replace("&uml. "η"). "ì").". _ "á").Replace("&euml.".Replace("</p>". " "). "÷") 'replace ISO 8859-1 characters.". vbCrLf).Replace("&rho.".Replace("&crarr.".".Replace("&Dagger. "λ"). "Ñ").". "ƒ"). "«"). _ "´").". _ "Ù").Replace("&sup2.". "ù").".".Replace("&Nu.Replace("&exist.".Replace("&Oacute. "ð").". "²").".".Replace("&sigma.Replace("&sdot. "κ").Replace("&pi.Replace("&apos.". "−").Replace("&thorn. "∑").Replace("&supe.Replace("&ograve.". "Ã"). "§").Replace("&uuml. "≤").". "Υ"). "š"). "Á").".".".".Replace("&Iuml.Replace("&perp.Replace("&rceil.Replace("&cap.".Replace("&zeta.Replace("<br>".".Regex.Replace("&laquo.Replace("&sub. _ "Ε").Replace("&oslash. _ "Ν").".".".Replace("&eacute. Note that any matches will make the text 8-bit Sb.".".Replace("&iota.". _ "ú").". "Θ"). "˜").Replace("&nu. "θ").".".".". _ "α").". vbCrLf).". "←"). _ vbCrLf).Replace("&Kappa.Replace("&epsilon.".". "Æ").".". _ "‾").Replace("&quot.Replace("&rfloor. "£").Replace("&ensp. "Ω"). _ "þ").". "'").". _ "¼").Replace("&iquest. "Ζ").Replace("&rarr.Replace("&Xi.Replace("&rdquo. "Î").Replace("&Ucirc. "‘").".Replace("&euro.".".".".Replace("&sube. "Ê"). and table entry terminators with vbCrLf Sb. _ "ρ").".Replace("&emsp.Replace("&thinsp.Replace("&phi.". "Å").". "‚").Replace("&THORN.Replace("&cong. "⌊⌊"). "Κ"). _ "υ").".".". "Π"). "Η").Replace("&alpha.Replace("&Otilde. "â").Replace("&int.Replace("&aring. "Β"). "¢").Replace("&Upsilon. "É"). "↔").Replace(Sb.".Replace("&ocirc.Replace("&psi.".Replace("&Omega. _ "Ι").Replace("&ouml.".Replace("&para.". _ "å").com/tags/ref_entities.".".". _ " ").". "ξ").". _ "♥").Replace("&Iacute. "∴ ∴ ").Replace("&tilde.".Replace("&cent. "•"). "Ç"). "±"). _ "Ì").Replace("&ntilde. "″").".".".".Replace("&Atilde.Replace("<p>".". _ "Ý").".". "∀ ∀ "). "€"). "ς").". "Ó").Replace("&ugrave. "ý"). "τ").".Replace("&Beta.Replace("&agrave.Replace("&ETH. "Χ").Replace("&macr. _ "í"). _ "√").Replace("&prop. >.Replace("&iacute. _ "¬"). "‰").". "Û"). "†").Replace("&asymp.". "∃ ∃").Replace("&micro. vbCrLf) 'Also seek out other Unicode encoded number entities not covered by the above and individually update them Page –293– .Replace("&larr.Replace("&thetasym.".Replace("&mu.Replace("&Euml.Replace("&harr.Replace("&Ccedil.Replace("&prime.". _ "≈").Replace("&Phi. "¡"). "Τ"). "Γ"). "Λ"). _ "ι"). "⌈⌈").".ToString().Replace("&Mu. "•"). "è"). _ "¤").Replace("&Iota.".Replace("&cedil.".Replace("&lowast. "æ"). _ "↵ ↵").NET Beyond the Scope of Visual Basic 6. "♠"). _ "õ"). "¥"). "ο").Replace("&nsub. "Ï").".Replace("&beta.Replace("&uacute.Replace("&Rho.".". "›"). "ä").Replace("&oline. "↓"). _ "∇ ∇ "). Note that any matches will make the text 8-bit Sb. "Ο").Replace("&Uacute. "ï"). "∼ ∼ ").Replace("&deg.Replace("&Ouml.Replace("&forall.Replace("&ne.".Replace("&minus. "À"). "¿"). _ "Ÿ"). "σ"). _ "⊥ ⊥ "). "Þ"). "⌉⌉").Replace("&nbsp. "◊").".".Replace("&Ugrave.Replace("&sim. "…").".".Replace("&igrave.Replace("&tau.Replace("</TD>". _ "∫"). "ÿ") 'replace Math Symbols Supported by HTML.Replace("&lsquo.".Replace("&lsaquo.".Replace("&hearts. " ").". "β"). "⊗ ⊗").Replace("&infin. "Š").".". _ "Ρ").". "♣").Replace("&nabla. "ø").". "©").Replace("&omicron.".Replace("&Ograve.". _ "↑"). "º").".Replace("&times.Replace("&notin.".".".".Replace("&circ.". "∪ ∪").". "¶").Replace("&part. "Σ").Replace("&omega.Replace("&icirc.".Replace("&sup3.Replace("&egrave. _ "∏").".Replace("&Alpha. "∝ ∝ "). "ˆ").".". "Μ"). "φ").". _ "≥").Replace("&Scaron.Replace("&Egrave. "»").Replace("&yen.".Replace("&there4.".".Replace("&lambda.".Replace("&loz.Replace("&Eta.Replace("&lfloor.Replace("&hellip.".Replace("&Auml. "¯").".".".Replace("&aacute.Replace("&acute. "⊕ ⊕ ").Replace("&empty.".". "∞"). """"). "ô").".". "î").". "</H[^>]*>".". "∨ ∨ "). "Α"). "ç").". _ "‡").Replace("&lceil.".Replace("&aelig. "à").".Replace("&yacute.Replace("&Ecirc.". "μ").Replace("&middot.Replace("&delta. "∅ ∅"). _ "ε").Replace("&isin.Replace("&iexcl. vbCrLf). "≡").".".".".". "Ψ").Replace("&ang. _ "Φ").".Replace("&oacute.".Replace("&chi.Replace("&Omicron. "ò").Replace("&Lambda.". "≠"). "⊄ ⊄").Replace("&le.Replace("&shy.Replace("&OElig. "ó").". "⊇ ⊇ "). "®"). "œ").Replace("&AElig.". "ã").".Replace("&equiv.".Replace("&reg.".Replace("&scaron. "ë"). vbCrLf). vbCrLf).Replace("&trade.".Enhancing Visual Basic . "û").Replace("&or.".".Replace("&piv.". "≅ ≅ ").Replace("&ndash.Replace("&frac34.".ToString & ".Replace("<P>".".". "Œ").Replace("&sect.Replace("&ecirc. "ϖ") 'replace Other Entities Supported by HTML. " ").".Replace("&prod. "™").Replace("&sbquo.". "⋅ ⋅ ")'NOTE: certain characters have tall characteristics 'replace Greek Letters Supported by HTML.".Replace("&otimes. "∉ ∉ ").Replace("&Yacute.". "∠ ∠ ").Replace("&iuml.". _ "È").Replace("&#" & Idx.Replace("&upsih.".".Replace("&Prime. "ü").Replace("&bull. "Ò").Replace("&Tau.Replace("&not.".Replace("&copy. _ " "). "γ").Replace("&Delta.".".Replace("&radic.Replace("&upsilon.Replace("&curren. "ö").". Note that any matches will make the text 8-bit Sb.". "–").Replace("&Ntilde.". "ª"). "π"). "⊃ ⊃ "). "Ö").Replace("&yuml.Replace("&Epsilon.Replace("&ordm.".".". "¦").Replace("</td>". "→").".".Replace("&Pi.Replace("&Psi.".".".Replace("&eta.Replace("&frac12.".TrimStart(Chr(9).".".".Replace("&Uuml.Replace("&theta.". vbCrLf). Note that values > 127 will make the text 8-bit For Idx As Integer = 1 To 255 'See www.Replace("&Chi. "∈ ∈ "). "¹").asp Sb.Replace("&Acirc.Replace("&permil.". "„").".Replace("&Igrave.Replace("&rsquo.Replace("&fnof.".".Replace("&ucirc.". Note that any matches will make the text 8-bit Sb.". "½"). "δ"). "-").".".Replace("&eth.".Replace("</P>".".".Replace("&and. "³"). "Ø"). "∋ ∋").".Replace("&oplus.". "ϑ").Replace("&ldquo.Replace("&diams. "‹"). " "). _ "é").Replace("&sup.".Replace("&Oslash. "ζ").".Replace("&xi. "Ξ"). _ "ω"). "ψ").".Replace("&dagger.".".".".Replace("&plusmn.".".Replace("&uarr.".".Replace("&darr.".Replace("<BR>". "Â").Replace("&pound. " "c)) Next 'replace reserved entities (except <.".".Replace("&gamma. _ "Ä"). line breaks.".". " ") 'replace HTML paragraph.".Replace("&sup1.". "∂").Replace("&Aring.Replace("&Aacute. "µ"). "χ"). _ "°").Append(S.".".".Replace("&brvbar.Replace("&Eacute.Replace("&Ocirc.". "Í").Replace("&kappa.".Replace("&ordf.". "∩").Replace("&oelig.Replace("&mdash. "♦") 'NOTE: certain characters have tall characteristics 'replace special ASCII coding entities that were not captured by the above.Replace("&ni.".". _ "Ð").".Replace("&Theta.".Replace("&sum.".Replace("&otilde.Replace("&clubs.Replace("&ge.Replace("&Sigma.Replace("&sigmaf.Replace("&atilde. "Ú").Replace("&rsaquo.". _ "ν"). _ "×"). "⊂ ⊂ ").Replace("&Gamma. Chr(Idx)) 'replace most common numeric entities Next 'Ensure header definitions are followed by vbCrLf Dim NewText As String = RegularExpressions.". "Ü").Replace("&auml.Replace("&ccedil.".w3schools. "ϒ"). _ "—"). _ "′"). _ "⊆ ⊆ ").".".". and &) Sb.Replace("&Icirc.". "Δ"). Note that any matches will make the text 8-bit Sb. vbCrLf) 'replace ISO 8859-1 Symbols (160-255).".". _ "∧ ∧ "). Replace(".". ">"). ""). "<[^>]*>".TextRich Return "text/richtext" Case MediaTypes. "&#") Do While Idy <> 0 Dim Idz As Integer = InStr(Idy. replace .ApplicationRtf Return "application/rtf" Case MediaTypes.Replace("&gt.Regex. replace < and > placeholders.ImageGif Return "image/gif" Case MediaTypes.TextPlain Return "text/plain" Case MediaTypes. Chr(CInt(Mid(S.. 3.Replace("&amp. ". Idz . _ "<").Idy + 1) RegularExpressions.Replace(NewText.3)))) InStr(Idy + 1.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 –294– ..Replace(NewText.Net.0 – David Ross Goben Dim Idy As Integer = InStr(NewText.TextHtml Return "text/html" Case MediaTypes.") 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. ". convert ampersand. then return result Return RegularExpressions. '******************************************************************************* Public Shared Function GetMediaType(ByVal MediaType As MediaTypes) As String Select Case MediaType Case MediaTypes.Regex.ApplicationSoap Return "application/soap+xml" Case MediaTypes. "&#") Loop 'check for a numeric entity 'loop as long as we find one 'find terminating semicolon 'grab expression 'replace expression 'strip remaining HTML text tags. a string representing ' : the selected type will be returned.ApplicationPdf Return "application/pdf" Case MediaTypes. Len(S) .ImageTiff Return "image/tiff" Case MediaTypes.MediaTypes text ' : ' Returns : provided a MediaTypes enumeration value. "&"). NewText. NewText.".Enhancing Visual Basic . S.NET Beyond the Scope of Visual Basic 6. with .".ImageJpeg Return "image/jpeg" Case MediaTypes.".Mime.") Dim S As String = Mid(NewText.. Idy.Replace("&lt.ApplicationZip Return "application/zip" Case MediaTypes. SeekingEncoding = True 'start looking for a Content-Transfer-Encoding field S = Nothing 'purge current data End If End If '------------------------------------------------------' check for boundaries '------------------------------------------------------If CBool(Len(S)) AndAlso CBool(Boundaries.Item(Idy). SUBJECT. " "c) 'append next line but remove tabs and spaces End If Select Case LCase(Left(S.ContentTypeDataIsFilename = ContentTypeIsName 'save flag indicating if Attachment Itm.NET Beyond the Scope of Visual Basic 6." Then 'line continues? Idx += 1 'yes. Boundaries.Mime. whether it ' is a message or binary information.. a TransferEncoding value ' : is returned. '******************************************************************************* Public Shared Function GetTransferEncoding(ByVal TransferEncoding As TransferEncodings) As System.TransferEncoding data ' : ' Returns : Provided a TransferEncodings value.Net. If it is "quoted-printable". System.1 If CBool(InStr(S. ' : ' NOTES: This method uses classes EmailItems and EmailInfo.ToData = Trim(Mid(S. Date. I + 1)) 'stuff to structure Inheader -= 1 'drop 1 from flag Case "date: " 'Found DATE field Info. content-transfer-encoding data.ContentBody = ContentBody 'store data ContentBody = Nothing 'reset accumulator Page –295– . '******************************************************************************* 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. I + 1)) 'stuff to structure Inheader -= 1 'drop 1 from flag Case "subject: " 'Found SUBJECT field Info. vbCrLf) 'break full email into lines Dim Idx As Integer = 0 'index into Ary() Dim MX As Integer = UBound(Ary) + 1 'find end if list+1 Dim Boundaries As New List(Of String) 'boundary definitions Dim Dim Dim Dim IsMultiPart As Boolean = False SeekingEncoding As Boolean = False BuildingDataBlock As Boolean = False HaveMessageBody As Boolean = False 'true 'true 'true 'true if if if if we we we we have multiple parts are looking for encoding are building a data block have the message body defined Dim ContentType As String = Nothing 'hold last-defined Content Type Dim ContentTypeIsName As Boolean = False 'true of Content Type specified a file Dim ContentTypeData As String = Nothing 'if block is an attachment Dim ContentEncoding As String = Nothing 'hold last-defined Content Transfer Encoding Dim ContentBody As String = Nothing 'block data accumulator '----------------------------------------------------------Dim Inheader As Integer = 4 'flag for gathering To. and the raw encoded. Dim I As Integer = InStr(S.TransferEncoding Return DirectCast(TransferEncoding. The Message Body. CompareMethod.Mime. ": ") 'find field delimiter If CBool(I) Then 'found one? If Right(S.TransferEncoding) End Function '========================================================================== '========================================================================== '******************************************************************************* ' Function Name : GetEmailInfo ' Purpose : Break email down into its component parts.ContentType = ContentType 'store content type Itm. data.Net.Trim(Chr(9).ContentEncoding = ContentEncoding 'store encoding Itm. the data should be decoded using ' DecodeQuotedPrintable()..ContentTypeData = ContentTypeData 'save filename or character set Itm.Net. the data should be ' decoded using the DecodeBase64() method.Count) Then 'check any defined boundaries For Idy As Integer = 0 To Boundaries.0 – David Ross Goben '******************************************************************************* ' Function Name : GetTransferEncoding ' Purpose : Provide easy access to System.. I + 1)) 'stuff to structure Inheader -= 1 'drop 1 from flag End Select End If If Not CBool(Inheader) Then 'if InHeader flag is now zero. it is 7-bit data and does not need to be decoded. and each AlternnateView or Attachment ' are contained within EmailItem objects within the EmailIngo object. TO. From. a flag indicating if the ContentTypeData is ' a filename or if it is text formatting. I + 1)) 'yes. If it is "7bit". so bump index S &= Ary(Idx).Mime..Text)) Then If BuildingDataBlock Then Dim Itm As New EmailItem 'create a new item Itm. 1) = ". ' ' An EmailItem contains fields for FROM. I + 1)) 'stuff to structure Inheader -= 1 'drop 1 from flag Case "to: " 'Found TO field Info. Subject Do Dim S As String = Ary(Idx) 'grab a line of data from the email ' ' check for important header items ' If CBool(Len(S)) AndAlso CBool(Inheader) Then 'if we are currently in the header. check for one of 4 fields Case "from: " 'Found FROM field Info. Content-Type.FromData = Trim(Mid(S. ' : ' Returns : EmailInfo object with component parts of email broken down.Count . If the content-transfer encoding is set to "base64".SubjectData = Trim(Mid(S.DateData = Trim(Mid(S.Enhancing Visual Basic . 5).NET Beyond the Scope of Visual Basic 6." Then 'more to add? Idx += 1 'yes.. Itm. I + 1).Add(Itm) Else Info. " "c) 'yes. so a message body is not created.Trim(Chr(9). Itm.ContentTypeData = ContentTypeData 'save filename or character set..MessageBody = Itm 'and finally set the new item to the message body End If Return Info 'return with filled data block End Function '========================================================================== ' Sample Code '========================================================================== Page –296– . check for field delimiter If CBool(I) Then 'did we find one? Select Case LCase(Left(S. InStr(sbAry(1). Nothing) Boundaries. assume ContentEncoding=base64 ContentEncoding = "base64" 'is blank. " "c) 'yes...AlternateViews. "name="..Replace("""".Trim(Chr(9).Text) = 0 Then ContentTypeIsName = True 'attachment if a filename specified (otherwise a view) sbAry = Split(sbAry(1).MessageBody = Itm HaveMessageBody = True End If ContentTypeData = Nothing BuildingDataBlock = False End If SeekingEncoding = True S = Nothing Exit For End If 'already have a message body? 'if an attachment 'add an attachment 'otherwise an alternate view 'else stuff new item to message body 'indicate we now have a message body 'reset filename/charset 'turn off building flag 'turn block seeing on again 'purge current data Next End If '------------------------------------------------------' build data block '------------------------------------------------------If BuildingDataBlock Then ContentBody &= S & vbCrLf 'add a line to content data End If '------------------------------------------------------' if seeking encoding '------------------------------------------------------If CBool(Len(S)) AndAlso SeekingEncoding Then 'are we seeking TCE? Dim I As Integer = InStr(S.Text) = 0 Then 'multipart.ContentEncoding = ContentEncoding 'store encoding. so grab data and trim tabs and spaces If Right(S. "=") + 1)). "=") 'multipart.MessageBody Is Nothing Then 'if the email lacks a border for message data. Info. ".ContentTypeDataIsFilename = ContentTypeIsName 'save flag indicating if Attachment. so grab data SeekingEncoding = False 'turn off seeking flag BuildingDataBlock = True 'turn on building data block flag Idx += 1 'bump to skip required following blank line End Select End If End If Idx += 1 'bump array index Loop While Idx < MX '----------------------------------------------------------'some emails do not define borders... Itm..ContentBody = ContentBody 'store data.Trim(). so force it! '----------------------------------------------------------If Info. ": ") 'yes. so grab second parameter (boundary definition).Trim(Chr(9).. I + 1)) 'yes. Dim Itm As New EmailItem 'we will create it now.Attachments.. and strip any quotes Dim Bnd As String = Trim(Mid(sbAry(1).Add(Bnd) 'and add a boundary ElseIf StrComp(Left(sbAry(1). 10). so assume base64 SeekingEncoding = False 'turn off seeking flag BuildingDataBlock = True 'turn on building data block flag Idx += 1 'bump to skip the blank line End If End If '=================================================== Case "content-transfer-encoding: " ContentEncoding = Mid(S. CompareMethod..Add(Itm) End If Else Info.ContentType = ContentType 'store content type. CompareMethod. Itm. 1) = ". so grab second parameter 'get second part of second parameter (filename definition) ContentTypeData = sbAry(1). Nothing) 'strip any quotes Else ContentTypeData = sbAry(1) 'AlternateView. I + 1). so bump index ContentType &= Ary(Idx).Enhancing Visual Basic . "multipart/". Itm. check for types '======================================================= Case "content-type: " 'Content type? ContentType = Mid(S..") 'now check the content type data ContentType = sbAry(0) 'keep first part for ContentType If StrComp(Left(sbAry(0).. so create a new EmailItem.. so stuff display character set If Len(Trim(Ary(Idx + 1))) = 0 Then 'if next line blank. " "c) 'grab next line and trim tabs and spaces End If ContentTypeIsName = False 'init flag specifying a file as false Dim sbAry() As String = Split(ContentType.0 – David Ross Goben If HaveMessageBody Then If ContentTypeIsName Then Info.Replace("""". '******************************************************************************* Public Function SampleReadPOP3(ByVal Server As String. The raw text ' : should be plugged into the appropriate medium. total byte size) If Stats(0) = 0 Then 'check number of messages for being 0 (none) Return Nothing 'no email found in the maildrop.OkOnly Or MsgBoxStyle. ByVal Password As String.the.Information. "Error Encountered") Catch e As Exception MsgBox(e. '******************************************************************************* Friend Sub SampleReadEmail() Dim EmailBag As ArrayList = SampleReadPOP3("pop.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 MsgBox(e.Exclamation.Add(InMail. MsgBoxStyle.OkOnly Or MsgBoxStyle.OkOnly Or MsgBoxStyle. InPort. "Message # " & msg.Enhancing Visual Basic .ToString & " of " & EmailBag. ByVal Username As String. "bob.Message. Password.ToString) Next 'process all messages Else 'transfer here if no one loves you MsgBox("No email found on server. UseSSL) 'Connect to user account '----------------------------------------------------------Dim Stats() As Integer = InMail. "YudLk2Knw".Count <> 0 Then 'if data was found For Each msg As POP3Message In EmailBag 'display each email MsgBox(msg. such as ' : "Me.Message. MsgBoxStyle.Text = msg2.".OkOnly Or MsgBoxStyle.Statistics() 'get maildrop statistics (number of message.Connect(Server.Message" for proper viewing.Retrieve(msg)) 'add a message object with message text Next 'process all messages '----------------------------------------------------------InMail.com".Information.MailID. For ' : example.Count. "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 –297– .gmail.Message. MsgBoxStyle.com".TextBox1.0 – David Ross Goben '******************************************************************************* ' Function Name : SampleReadPOP3 ' Purpose : Samples method to Read a POP3 account maildrop ' : ' Returns : ArrayList containing a list of POP3Message objects. "Error Encountered") End Try Return Nothing End Function 'POP3-side error 'general programming error '========================================================================== ' 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.builder@gmail. the text may be HTML or Rich Text format.Exclamation.List 'parse each header object (contains only index and size) localList. MsgBoxStyle. 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. Optional ByVal InPort As Integer = 110.NET Beyond the Scope of Visual Basic 6. True) If EmailBag IsNot Nothing AndAlso EmailBag. Username. 995. Optional ByVal UseSSL As Boolean = False) As ArrayList Try Dim InMail As New POP3 'create a new POP3 connection InMail. or Nothing if no emails ' : ' NOTE : This method should be modified to suit your application. just as you can in Outlook or Windows Mail.NET Mail class you cannot send the main body of an email as Rich Text (you remember that IsBodyRtf option I mused about? You can add it in your own version of the class). After investigating. With your own Mail class.TcpClient. and then assigned to the MessageBody member. For example. a new EmailItem object is instantiated.0 – David Ross Goben Conclusion This concludes this article on Internet SMTP and POP3 Email processing.Net. SMTP and POP3 both inherit from System. you could. 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. and because of this the gathered data is never collected into the EmailInfo object’s MessageBody member.Socket. and send email the way YOU want to send it. If not. Page –298– . For instance. to include encryption and other protections to keep your our your company’s information safe. the underpinning of such a class is already wrapped up within our POP3 class. with the . I discovered that some emails do not specify Boundary strings.NET Beyond the Scope of Visual Basic 6. The listed GetEmailInfo() method in this article includes this fix. but it is certainly not the end of what can be done. you may ask? It means that you can create your own SMTP Mail object. or you cannot send a message body coded 7bit. 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. Actually. You can even create a proprietary email format that can only be properly read by your email application.Enhancing Visual Basic . filled. What does this mean to you. NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Useful References Page –299– .Enhancing Visual Basic . NET contains a vast repertoire of functionality that you will discover and rediscover as you continue to explore deeper and ever deeper within the trees of namespaces under such headings as My. Microsoft.IO namespace of the . vbDirectory) 'check for subfolders Page –300– . or. alternatively.0 – David Ross Goben Comparing Visual Basic System I/O Commands VB. or that they now use FSO (a File System Object) to do “super-fast” file/directory I/O. it took a very impressive 1 minute and 0. and so on. As an experiment.WriteLine("Scanning using Old Dir I/O") Dim startTime As Date = Now ODoDir("C:\") Console. Microsoft is often very good in their language development and at making their functionality work faster and faster (I wish they could have said the same about their Office 2007 product).ToString()) Console.NET Framework.WriteLine("Old Dir process time: {0}". The next step was file streams using the Windows Scripting Host Object Model (WSHOM) available to VB6 through its WSHOM.DLL.113 seconds.IO.IO approach.Generic. The results were very surprising. System. Using DIR()took 5 minutes and 18.227 seconds to scan through 124.IO. Environment. But now we have what I like to call Turbo Streaming.352 folders (about 6217 folders per second). where each array element represents the full drive path to a sub-folder.WriteLine("Scanning using FSO Dir I/O") startTime = Now fsoDoDir("C:\") Console. Now.352 folders (about 391 folders per second). And that is for 124. where the code seems to be written much closer to the machine.ToString()) Console. which I think justified my own transition to that method while developing code under VB6 (about 2072 folders per second). Using FSO. I wrote three subroutines in a Console application that used each method to scan through every folder on my C:\ drive. using the System. But like any developer. All I was doing was scanning each and every folder for subfolders.Subtract(startTime). What follows is the console application I wrote to test it. One glorious gem is the file/folder Input/Output (I/O) functionality found in System.Enhancing Visual Basic .DLL (used by WSHOM. This returned an array of strings.Subtract(startTime). I have been told over and over by users that scanning directory folders using the VB6-style Dir() command was good enough for everyone. 1) <> "\" Then path &= "\" End If Dim col As New System. instead add a COM reference to “Microsoft Scripting Runtime”: Option Explicit On Option Strict On Imports IWshRuntimeLibrary ' This requires a COM reference to 'Windows Scripting Host Object Model' 'Imports Scripting 'Alternatively. using the Microsoft Scripting Runtime through Scripting.GetDirectories command Sub Main() Console.WriteLine("FSO Dir process time: {0}". Now. Now. VB started with the Dir() function to scan the directory structure of a system. from which I just used the GetDirectories() method.848 seconds.OCX under VB6) ' The third uses the SYSTEM.DLL). or. It took all of 20.OCX (this actually redirects control to IWshRuntimeLibrary. 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.Collections.WriteLine("Scanning using New Dir I/O") startTime = Now NDoDir("C:\") Console.NET Beyond the Scope of Visual Basic 6.Subtract(startTime).WriteLine("New Dir process time: {0}". recursing my code to access every single folder on the drive. if you prefer.*".ToString()) End Sub '******************************************************* ' process directory scan using VB6-style DIR() command '******************************************************* Private Sub ODoDir(ByVal dPath As String) Dim path As String = dPath ' add trailing slash to dir path if not there If Right(dPath. But I was really impressed with the System. You will also have to add a COM reference to “Windows Scripting Host Object Model” in the project properties so that the FSO method will work.List(Of String) 'get all sub-folders Dim S As String = Dir(path & "*. 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. Page –301– . 1) <> ". so add to temp list End If End If S = Dir() 'check for another Loop ' now scan subfolders (using a collection prevents recursing from walking over 'the Dir() process this current levels is using. With col Do While CBool(.0 – David Ross Goben Do While CBool(Len(S)) 'found one? If Left(S.RemoveAt(0) 'remove entry from list Loop End With col = Nothing 'break connection to collection object End Sub '******************************************************* ' process directory scan using VB6-style FSO object '******************************************************* Private Sub fsoDoDir(ByVal dPath As String) Dim path As String = dPath ' add trailing slash to dir path if not there If Right(dPath.Count) ODoDir(.. the Dir() command has been redesigned to run almost as fast as File System Objects. 1) <> "\" Then path &= "\" End If 'get all sub-folders Try Dim dcol() As String = System." or ".GetFolder(path).Enhancing Visual Basic ." Then 'if not ".IO.IO.Directory) Then col. we get this (times may vary with each run.Item(0)) 'recurse through subfolders .SubFolders fsoDoDir(fld.Add(path & S) 'found one.NET Beyond the Scope of Visual Basic 6. Skipping normal processing is enough End Try End Sub End Module Running this using Debug \ Start without debugging (Ctrl + F5) (this allows a pause for any key at the end of the run).Path) Next Catch ' ignore errors.GetDirectories(path) If dcol Is Nothing Then Exit Sub For Each S As String In dcol NDoDir(S) Next Erase dcol Catch ' ignore errors." If CBool(GetAttr(path & S) And FileAttribute. Skipping normal processing is enough End Try FSO = Nothing End Sub '******************************************************* ' process directory scan using SYSTEM. depending on what is going on in the background): NOTE UPDATE: As of VB2010.Directory. 1) <> "\" Then path &= "\" End If Dim FSO As New FileSystemObject 'get all sub-folders Try For Each fld As Folder In FSO. Perhaps the only real advice I would like to pass on to you for reference.1 folders now have so little content). Page –302– . V3.VisualBasic. you will find it within one of its “V”-version subfolders. They are at C:\Windows\Microsoft. you may start getting curious about what it is doing behind the green curtain. other than my strong recommendation to get . Google Chrome.NET Reflector (http://reflector. you can explore what system DLL’s are used by them – .1.Compatibility.NET Once you start feeling comfortable with VB.NET Framework (V1.Compatibility for Microsoft. C#.0 folder (mine is currently named V2. and using the syntax for your choice of Visual Basic. which includes the build). then select Disassemble. you can disassemble the code of the Framework and see what makes it “tick”. no longer available). V2. so I explore the V2.NET Framework. But the best part is. but more importantly – those of the .VisualBasic.red-gate. Free updates are also available.NET Framework from 1. then you 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.5. You can do so for a modest fee ($95 USD. I was able to use it with ease. simply by watching the demo video. Although there was no real documentation for using the beta version (the free version. the VB6 Compatibility Library is found in version . is that the Reflector’s file list is dependant upon DLL. It works with all versions of the .Enhancing Visual Basic . finding Microsoft. or IL. you can perform a File / Open on your own DLL or EXE files generated by the VB.0 upward.NET compiler.VisualBasic. MCL. V3.0 and V1. If features are enhanced or superseded by a subsequent version of the .0.0. V1. As such. and EXE files.dll.VB6. I would browse through the sub-folders of Microsoft. Dephi. VC++.com).NET Framework. Finding these namespace files is not as difficult as you might think. each representing a version of . then explore its sub-folders for FixedLengthString.0.0.Compatibility. right-click it.NET Framework 2.NET Reflector.NET. and so on).NET code is packed with P/invokes to the Win32 API.NET Beyond the Scope of Visual Basic 6.NET\Framework. If you know the namespace you want to find – simply browse through these few version folders. Depending on the Version of the Framework that the feature you want to explore exists within. Also be sure to check out the Support subfolder for many more commands.0. Suppose we wanted to see how the FixedLengthString class of the VB6 namespace was constructed. By selecting Analyze instead. You can then explore this namespace to your heart’s content.0 – David Ross Goben REALLY Looking under the Hood of Visual Basic . a version that can decode 3rd part code is $199 USD) by using Redgate Software’s .50727. For example. What is really impressive (but perhaps this is because I started my career out as an Assembly Language Developer) is to view it in IL.NET Beyond the Scope of Visual Basic 6. I see a list of methods. if you are trying to pick up C# as another language.m_strValue = New String(ChrW(0). (Me. By selecting it. "")) End If Me.m_nMaxChars) Then Me.m_strValue = InitialValue Else Me. This exercise also points out that in release code you should employ . Me.Left(Value.Len(Value) >= Me.NET code listing that is shown below: Public Class FixedLengthString ' Methods Public Sub New(ByVal MaxChars As Integer) If (MaxChars <= 0) Then VB6Errors. or a 3rd-party Code Obfuscator.num))) ElseIf (num = MaxChars) Then Me. "MaxChars". Also.Enhancing Visual Basic .NET program code you wrote it in. "MaxChars". such as the VB. "". ByVal InitialValue As String) Dim num As Integer = 0 If (MaxChars <= 0) Then VB6Errors. Resources.Value End Function ' Properties Public Property Value() As String Get Return Me.RaiseError(5. Me.m_nMaxChars = MaxChars Me. giving you a leg up on learning how to transition some of your VB.m_strValue = Strings. Me.Strings.m_strValue = (InitialValue & New String(" "c.m_nMaxChars) Else Me. properties and fields that are encapsulated by the FixedLengthString class.m_nMaxChars) End Sub Public Sub New(ByVal MaxChars As Integer.NET’s built in Dotfuscator. but as a C# program. at the bottom you will see a click-on option entitled “Expand Methods”. compact code.m_nMaxChars . suddenly you see a workable approximation of the source code (in whichever language you selected from the dropdown in the toolbar) that will define that class.Left(InitialValue.RaiseError(5. Resources.m_nMaxChars .GetResourceString("Argument_InvalidValue1". Page –303– .GetResourceString("Argument_InvalidValue1". "".Len(InitialValue) If (num < MaxChars) Then Me. (Me.m_nMaxChars = MaxChars num = Strings.m_strValue = (Value & New String(" "c.NET skills to C#. That is some seriously tight.m_nMaxChars) End If End Sub Public Overrides Function ToString() As String Return Me. you can load your EXE application and view it not as the VB.m_strValue End Get Set(ByVal Value As String) If (Strings. on the right side of my application.m_strValue = 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 you can quickly switch between languages and see how it would have been coded in another language. "")) End If Me. Almost too tempting to resist.0 – David Ross Goben Suddenly. Directory.DrawEllipse function Clipboard.NET replacements (most changes are actually unnoticed). Asc. CultureInfo Class Visual Basic Run-Time Library Members.StartupPath or System. and the MSDN. Convert. SetDataObject class .ChrB. but it would be much faster to use AndAlso.Forms namespace. You are strongly encouraged to explore these functions yourself using the Object Browser (F2). Many VB6 elements have been renamed.GetExecutingAssembly. CultureInfo.Clear or Clipboard. refer to the . Marshalling does allow an AsAny expression.Graphics namespace System.TaskVisible Array App. Strings module System Namespace Visual Basic Run-Time Library Members. but this is subject to the same errors the VB6 form was subject to. such as System. then the trailing expression will not need to be evaluated.NET on-line help regarding the Common Language Runtime. ChrW. or provide a much more efficient means to accomplish them.Dictionary ChDir function.ToChar. Strings module System. Math not needed if imported.NET equivalent Asc Math.DrawEllipse function or System. Strings module System. VB6 element Abs function And (logical function) VB.Windows. Math not needed if imported. FileSystem module Visual Basic Run-Time Library Members. If the leading expression is false. or run-time library location System Namespace.SetCurrentDirectory() ChDrive function Chr$. Form class System namespace.Clipboard namespace. All can be emulated. Several elements are no longer supported. or the faster System.Drawing. See the App Class definition.Runtime. Location() Math. reclassified.Abs function.NET supports overloading declares. Math Class Visual Basic Run-Time Library Members Not Applicable System. This might accidentally lead to superior knowledge. support for various programming elements has changed since VB6. such as passing wrong-type parameters. Math Class System.ToInt32(char) AscB function Asc function As Any keyword phrase Atn function Calendar property VB.Enhancing Visual Basic .Clear Page –304– Namespace. because the common language runtime includes functionality that makes them unnecessary.ShowInTaskBar or Me.Reflection. System.Windows. for more convenient VB6 App member declarations. Collections.Forms within Form code. or Convert.IO.Path ChDir statement Clipboard. class. Note that some long paths are not required. “Using the AsAny Marshaling Parameter” System. described earlier in this document.CurrentCulture Property Chr Chr. For more information.Forms. so explicit data types should be used.Globalization Namespace. And (logical function).Windows.Atan function.NET Beyond the Scope of Visual Basic 6.HashTable. or CChar Collection ChDrive statement Collection. Strings module Visual Basic Run-Time Library Members.NET (Based on various Microsoft sources) For VB. mostly for reasons of interoperability with the Common Language Runtime (CLR). functions Chr function Circle function e. FileSystem module Visual Basic Run-Time Library Members.Assembly Namespace System Namespace.Reflection. The following is the most complete table that you will find to date that lists changed VB6 programming commands and their VB.ShowInTaskBar The { } braces can be used in the declaration of a standard array to pre-assign values to it.InteropServices.Assembly. the . MSDN Help. Application.Form. or combined with other programming elements to assume a more logical structure in VB. See the earlier article. Collections.NET Reflector.0 – David Ross Goben VB6 Changed Commands In VB.Forms.Drawing. However.com website.Windows. Array class Visual Basic Run-Time Library Members.SetDataObject(New DataObject("")) App statement App.NET.NET.Graphics. SetDataObject(New DataObject(DataFormats. DateAndTime module Visual Basic Run-Time Library Members. and Debug.Forms. but it is high recommended that you invoke Application.SetText or Clipboard.Subtract Debug. or run-time library location System. DateString Property DateAdd DateAdd. All variables must be explicitly declared. VariantType Enumeration Visual Basic Run-Time Library Members.Windows. EOF.WriteLine. FileSystem module System. Debug. or System.Write.Text.NET.Add DateDiff DateDiff. GetDataObject class System.GetData (DataFormat.IO.GetCurrentDirectory function Replaced by the more accurate Decimal data type. class. Still supported as a data type but no longer a function returning a 4-byte double precision value.Windows.GetDataObject. SetDataObject class System.WriteLineIf methods Not supported in VB.SetData or Clipboard. Debug. Debug Class System. System.Clipboard namespace. Application Class Visual Basic Run-Time Library Members.0 – David Ross Goben VB6 element Clipboard.Clipboard namespace.Peek = -1 Page –305– Namespace.GetText or Clipboard. GetDataObject class System.GetDirectories. FileSystem module Visual Basic Run-Time Library Members.NET equivalent Clipboard.GetText Clipboard. DeleteSetting function Def<Type> statements DeleteSetting statement Dir DoEvents function Empty keyword End keyword EOF Dir.GetFormat or Clipboard.Forms.Cos function CurDir. Application Class Does not apply System.Forms Namespace.Exists Appication.Windows.DoEvents function Handled by Nothing keyword.Assert.Fail functions Debug.File.IO.Directory.IO.IO.Print method Debug. DateAndTime module Visual Basic Run-Time Library Members. See Data Type Changes for VB6 Users in Help.GetFiles.NET Beyond the Scope of Visual Basic 6. DateAndTime module System.Clipboard namespace. "Hello")) FileClose Function Math. DateAndTime module Visual Basic Run-Time Library Members. according to the current culture settings.IO. DateValue function Date$ function Error Statement Now. Use the Today property to return the day in the 8-byte CLR format. Interaction module Visual Basic Run-Time Library Members.Windows.Forms. FileSystem module System Namespace.Assert method Debug. or DateObject. or DateObject.SetData Clipboard. or prior to End. The Decimal type’s ToString(“c”) method can be used to display Decimal values as Currency.Forms. True) Clipboard. or the much faster System.Exists.GetDataPresent (DataFormat) Clipboard. Debug.WriteIf.Clipboard namespace. Debug Class Does not apply Visual Basic Run-Time Library Members. System. System.Diagnostics Namespace.GetFormat Clipboard.SetText Close statement Cos function CurDir function Currency data type CVDate function CVErr function Date function. DateAndTime module Does not apply Visual Basic Run-Time Library Members. End.IO. or Today Property.Exit instead of.GetDataObject.Forms Namespace.Directory.Diagnostics Namespace. Math Class Visual Basic Run-Time Library Members.Enhancing Visual Basic . nor should it be. Date statement VB. DataObject class Visual Basic Run-Time Library Members.Windows.StreamReader. FileSystem module .SetDataObject(New DataObject(textobject)) Clipboard.Windows. or the faster test: System. ToLongDateString.File. which erases all references. Strings module Visual Basic Run-Time Library Members. Use System. etc. Math Class Visual Basic Run-Time Library Members.Application.IO. FileSystem module Visual Basic Run-Time Library Members. or System.Length Array. Pack:=1)> Page –306– Does not apply Visual Basic Run-Time Library Members Visual Basic Run-Time Library Members System Namespace. &.IO.Enhancing Visual Basic .GetLastAccessTime.NET version differs from VB6 in several ways.Exp function FileCopy function. class.ToString(“Format String”).ToString(“Format String”) FormatPercent NumberExpr. Err.File. Math Class Does not apply Visual Basic Run-Time Library Members. When FileGet gets a dynamic array in Random Access files. For structures.Array.Raise. To force the same results as VB6. VB.NET does not support the Error statement.Application namespace. or System. and > placeholders. a two-byte length value is expected prior to each array.Celing (for positive number) Format or AnyObject.ToString(“c”) FormatDateTime FormatNumber DateObject.NET uses only Dynamic Arrays.Clear() if you want to erase only the array cell contents but not the array cells themselves. FileSystem module Visual Basic Run-Time Library Members. or StreamObject. NumberExpr. which had been used to raise an error. FileSystem module System Namespace. The biggest difference is that VB6 also supported static arrays.Copy FileDateTime. FormatCurrency ScalarObject. Array Class System Namespace. DateObject. or run-time library location Boolean Operator for VB6 Users in Help. Strings module Visual Basic Run-Time Library Members. also lead them with: <StructLayout(LayoutKind. though they can be easily emulated with custom definitions. so you cannot swap data with VB6 apps IF the file has Variant values. FileGet does not work the same with nonscalar values. Math.NET version does not support: 1. The VB.OpenForms Get # statement FileGet function. in a form such as result = (CBool(op1) = Cbool(op2)). 2. VB. If a file has dynamic arrays or structures. or Math.InteropServices . System.BinarySearch Fix. FirstDayOfWeek or FirstWeekOfYear optional arguments. Strings module My.ToString(“p”) Forms collection My.Floor (for negative numbers). Erase.IO. or Math. 3. Strings module Visual Basic Run-Time Library Members.NET Beyond the Scope of Visual Basic 6.NET equivalent Use the equals (=) operator. It uses newer methods to implement these features. The VB. OpenForms collection Visual Basic Run-Time Library Members.Sequential. use the ArrayIsDynamic argument with FileGet to prevent length descriptors from being expected or checked.ToString(“Format String”). FileSystem module. making a slightly longer file.Runtime. A few rare date/time formats. <. Formatting of string values using the old @. and erased their data but not their memory allocation.GetCreationTime FileLen.0 – David Ross Goben VB6 element Eqv operator VB. you must marshal them. System. See Erase keyword Error keyword Exp function FileCopy statement FileDateTime FileLen Filter Fix Format function Namespace. Use the equals (=) operator in conjunction with Not and Or. InStrRev IsNull function Does not apply Visual Basic Run-Time Library Members. See Class_Initialize Changes for VB6 Users and Using Constructors and Destructors Input. and Boolean Operator for VB6 Users. Math module Visual Basic Run-Time Library Members.Enhancing Visual Basic . Remove all in original code before upgrading and replace with method invocations. see Shared (Visual Basic). Strings module Does not apply Does not apply Does not apply Visual Basic Run-Time Library Members. InStr function Int InstrRev function. Input$ statements. Strings module Visual Basic Run-Time Library Members.NET equivalent GetSetting function No longer supported. Information module . See Control Statement for VB6 Users in Help.NET.Value property. and then an Activated event when the input box is closed and the form regains the input focus. IntegerObject. InputB$ functions InputBox function VB. FileSystem module The VB. For procedure-level declarations. but it would be much faster to use the LastIndexOf() method of the string to check. i. class.NET fires a Deactivated event for the current form. For class-level declarations.NET. Convert..0 – David Ross Goben VB6 element GetSetting GoSub statement Hex Imp operator Implements Initialize event Input #. Instancing property Namespace.NET Beyond the Scope of Visual Basic 6. but VB. InputB. replace “A Imp B” with “Not A Or B”.NET version of the InputBox function works just like the VB6 version. Handled by Nothing keyword. Input$. or. Database nulls can be tested for using the IsDBNull function or against the System.VisualBasic Namespace Page –307– Visual Basic Run-Time Library Members Does not apply Visual Basic Run-Time Library Members. see Private (Visual Basic) and Public (Visual Basic).NET. Further. Information module Does not apply Visual Basic Run-Time Library Members. Handled by Nothing keyword.e. but its argument must be an Interface type. but it would be much faster to use the IndexOf() method of the string to check. the Implements keyword is also used to qualify method and property declaration that implement a member of the interface. Instr function InStrB function VB6 accepts vbCR characters as line separators in the message. Not supported in VB. Not supported in VB. VB. but used class skeletons in their place). Strings module Visual Basic Run-Time Library Members. See Procedure Declaration for VB6 Users.NET requires vbCRLF. if A and B are more complex. Instr function. whereas VB. See Not and Or operators. VB6 takes an optional pair of coordinates defined in twips.ToString(“x”). InputString function 2. use Sub New.NET. 16) Not supported in VB. except for: 1.Tostring(Integer.DBNull.NET supports the Implements keyword. Use the Return Statement.NET uses pixels. Int (for 16-bit Short Integers) IsEmpty function IsNothing function IsMissing function Not supported in VB. VB. Strings module Visual Basic Run-Time Library Members. “((Not A) Or B)”. or run-time library location Microsoft. not a class as in VB6 (VB6 did not support interface types. 1)”.IO.ToLower Left$. it returns the number of bytes taken when the block is written to disk or passed to a Windows API method. FileSystem module Visual Basic Run-Time Library Members. and for Objects and Structures it returns their full byte sizes (in earlier versions.GetLowerBound Lcase function.File.NET interprets it as a reference to the Left property of the Form and UserControl object.GetByteCount() to get the full byte sizes of strings. 1)” The VB6 method works with both strings and User-Defined Types. Structures (returns full byte size).Left(arg. returning the number of bytes. Information module Visual Basic Run-Time Library Members. Strings module Visual Basic Run-Time Library Members. Strings module Visual Basic Run-Time Library Members. VB. if you simply want the character count of a string. VB. LeftB$ functions VB.Unicode. Strings module Visual Basic Run-Time Library Members. or System.VisualBasic”. The VB. because LenB() is unsupported. Set assignment statements Line function Line Input # statement Load statement LOC # statement Use the New keyword to create a new instance of a form. or ArrayObject. Not supported in VB.VisualBasic namespace using “Imports VB = Microsoft.NET equivalent IsObject function IsReference function Join Kill statement Join. Also use System.SubString(0. if the code runs in a form or user control.Windows.SubString class. The VB6 method works with strings and UDTs. FileSystem module Visual Basic Run-Time Library Members. Strings module. or String. FileSystem module Visual Basic Run-Time Library Members.Text. Math Class .IO. FileSystem module System Namespace. the new Set statement is unrelated to the older one. and Objects (returns full byte size). To avoid this. or use the Substring method exposed by the System. and specify it as in “Return VB. Structures were not supported).Position Lock # statement Lock. Unicode class Does not apply System. It returns a character count (½ the actual byte size) of Strings. System.0 – David Ross Goben VB6 element VB. or System. System. consider the faster Length method of the target string.NET method works with Strings (returns character count). FileSystem module Visual Basic Run-Time Library Members. But.NET Beyond the Scope of Visual Basic 6.Enhancing Visual Basic .Join() Kill function LBound Lcase$ function Lbound. DrawLine function LineInput function Len function LenB function Let. reference the Microsoft. LOC statement.NET’s Len() function now works with the functionality of both VB6’s Len() and LenB() functions.Log method Page –308– Namespace.NET. Unlock functions LOF # statement LOF statement.Text namespace.NET supports the Left string function. and the Show function to display it (Load was a shorthand substitute for New).Length Log function Math. Also. With UDTs. LeftB.File. such as “Return arg.Graphics Visual Basic Run-Time Library Members. or StringObject. or run-time library location Visual Basic Run-Time Library Members.NET recommends Len() instead.Forms namespace Visual Basic Run-Time Library Members. class.Drawing. See Default Property Changes for VB6 Users. VB. forcing a compile error. Strings module Visual Basic Run-Time Library Members. Namespace. “Marshaling Memory and Passing Strings to P/Invokes in VB. Strings module Visual Basic Run-Time Library Members. The VB.NET. Rset statements VB. these cannot be copied directly. and then an Activated event when the input box is closed and the form regains the input focus. i Now function VB. except for: Ltrim$ function Mid function MOD function MsgBox function 1. the result is the remainder of the floating-point division. but VB. VB. but it would be much faster to use the SubString() method of the string to check.TrimStart Mid function. or the much faster StringObject. See Data Type Changes for VB6 Users. The VB6 Mod operator converts its operands to integers and then returns the remainder of the division. Next j. Note that LSet did not copy user-defined types. Now Property Null keyword Handled by Nothing keyword. The VB6 version accepts individual CR characters as line separators in the message.0 – David Ross Goben VB6 element LSet. for structural integrity.. or System. String Class Visual Basic Run-Time Library Members.NET does not support this syntax and requires. If your code requires the same VB6 functionality under VB.NET version fires a Deactivated event in the current form. that each For loop be explicitly terminated by a distinct Next keyword. and System Namespace. However.NET Beyond the Scope of Visual Basic 6.Directory.CreateDirectory MOD function. Also see an earlier article. but the members can be copied one by one to achieve the same result. Ltrim function.. FileSystem module Does not apply Visual Basic Run-Time Library Members. whereas the VB.IO. as in “result = Cint(op1) Mod Cint(op2)”.String class. 2. Strings module. or run-time library location Visual Basic Run-Time Library Members. Name statement Rename function Next statement Next statement. DateAndTime module Does not apply . FileSystem module Visual Basic Run-Time Library Members Does not apply The VB. Strings module Visual Basic Run-Time Library Members. If the operands are floatingpoint numbers.NET version uses pixels. For example: For I = 1 To 10 For j = 1 To 10 . The VB6 version takes an optional pair of coordinates defined in twips.NET” to see how to move a structure as a block of memory.NET requires a CR-LF pair (vbCRLF). Mid function MidB statement Mid Statement MkDir statement MkDir function. Strings module Visual Basic Run-Time Library Members.NET uses structures. under VB6 a single Next keyword could terminate two or more For loops.Enhancing Visual Basic .NET Mod operator does not perform integer conversion. Page –309– Visual Basic Run-Time Library Members. but retains the operands in their original type. The VB. class.NET equivalent MidB function Replaced by PadRight and PadLeft methods of the System. the operands of the Mod operator should be explicitly converted to 32-bit integers before using the Mod operator.NET version of the MsgBox function works just like the VB6 version. For these. FileSystem module System. use the ArrayIsDynamic argument with FileGet to prevent length descriptors from being expected or checked.Drawing namespace. However. yet equivalent functionality exists in the System. PrintLine function Not supported in VB. you must marshal them. Any module can be marked as private using the Private keyword.NET.Color. Or (logical function). FileGet function. FileOpen function Not supported in VB. or Convert. Property Set statements Pset. FileSystem module. FileSystem module Does not apply N/A Visual Basic Run-Time Library Members. To force the same results as VB6. also lead them with: <StructLayout(LayoutKind. then the trailing expression will not need to be evaluated.Drawing.NET uses ARGB color values. the VarPtr() function demonstrated on page 132 of this document can be used to return a valid ObjPtr address.ToString(Integer.NET. useful for API. Scale Put # statement VB. Use the Module Statement in VB. Oct function. See Property Procedure Changes for VB6 Users. class. The Option Private directive is used only in Access VBA and had no effect in VB6.NET. If the leading expression is true. Not supported in VB.NET does not support this directive. but . but it would be much faster to use OrElse.Drawing Namespace. Print.NET. System. Zero-based arrays only. 8) Not supported in VB. FileSystem module Does not apply Does not apply Does not apply Visual Basic Run-Time Library Members Visual Basic Run-Time Library Members. Namespace. You must add an apostrophe at the beginning of each line. Random Class VB. See Array Bounds for VB6 Users. No longer supported. or run-time library location Does not apply Visual Basic Run-Time Library Members. When FileGet gets a dynamic array in Random Access files. Does not apply Visual Basic Run-Time Library Members. If a file has dynamic arrays or structures. See Control Statement for VB6 Users. Property Let. See Control Statement for VB6 Users. For example: ‘ first line _ second line _ third line Replace Reset statement StringObject. VB.NET Beyond the Scope of Visual Basic 6. See point 7 on page 318 for a simple solution to this issue.Enhancing Visual Basic .0 – David Ross Goben VB6 element ObjPtr function Oct$ function On … GoSub construction On … GoTo construction Open # statement Option Base statement Option Explicit Option PrivateModule statement Or (logical function) Print # statement Property Get.Replace() method Reset function RGB RGB() function. use the System.NET equivalent Not supported.NET doesn’t support this.Sequential. Option Explicit On Private Module declaration. For structures.Random object is already randomized declared VB6 supported multi-line remarks. use Select…Case Statement. Color Class . use Select…Case Statement. Page –310– System Namespace.NET.Runtime. a two-byte length value is expected prior to each array. Conversion module Does not apply Does not apply Visual Basic Run-Time Library Members.FromArgb() function. so you cannot swap data with VB6 apps IF the file has Variant values.InteropServices Pack:=1)> Randomize Rem System. making a slightly longer file. FileGet does not work the same with nonscalar values. Fonts collection System.Windows.PrimaryScreen.Height Me. or use the Substring method exposed by the System.SetAttributes Process.Drawing namespace. Forms class.IO.Windows. 1)”. LSet statements Rtrim$ function Screen. FontFamily class System.NET.0 – David Ross Goben VB6 element Right$.Width SendKeys. Screen class Visual Basic Run-Time Library Members Does not apply Visual Basic Run-Time Library Members.ActiveControl Screen. Forcing a compile error.SubString class. System.PadRight Page –311– Not Applicable Not Applicable System.Round function Rset Function. FileSystem module System Namespace.Save method SaveSetting statement SaveSetting function Scale method Screen.Forms. Math Class Visual Basic Run-Time Library Members. such as “Return arg. or System.Windows. Cursor.Current RmDir statement Rnd Round function Rset.TwipsPerPixelY Screen. class.Drawing.NET.IO.PrimaryScreen.Forms namespace.Forms.FontFamily.NET Beyond the Scope of Visual Basic 6.IO.SubString(arg.TrimEnd Image.ActiveForm Not supported in VB.ActiveForm. or System. FileSystem module System Namespace.Directory.FontFamily.Enhancing Visual Basic . VB. if the code runs in a form or user control. or the much faster StringObject. Interaction module Does not apply System. System.Windows.Drawing.Families.Forms.Windows. Random Class System Namespace. Strings module Visual Basic Run-Time Library Members Visual Basic Run-Time Library Members. Math Class System Namespace.RemoveDirectory Rnd.Sin Method StringObject.Drawing namespace. SetAttr function.Windows.Length Screen. see Data Type Changes for VB6 Users Rtrim function.Forms.Forms namespace.Form. Let assignment statements SetAttr statement Shell Sgn function Sin function Space 15 15 System.ActiveForm.Forms namespace.RandomObject.Sign function Math.TwipsPerPixelX Screen.B ounds. Screen.Form. NOTE: This no longer works. But.NET equivalent SavePicture method VB. 1)” RmDir function. See Default Property Changes for VB6 Users.SetAttributes.Cursor = New Cursor(“C:\WINDOWS\Cursors\newcur. ActiveControl or Me.cur”) System. or run-time library location Visual Basic Run-Time Library Members.Families Screen. and specify it as in “Return VB.Forms namespace.Directory.Height System. or System. Form class System.MouseIcon Screen.Windows.Forms namespace. Math Class Does not apply .FontCount System.PadLeft.Send method Not supported in VB.VisualBasic namespace using “Imports VB = Microsoft. Form class System.Forms.Windows.Start function Math.VisualBasic”. or StringObject.Width SendKeys Set. Screen class Not applicable System. FontFamily class System.Length-1.MousePointer Namespace.Screen.NET supports the Right string function.Windows.ActiveControl System. Strings module Visual Basic Run-Time Library Members.Screen.B ounds. the new Set statement is unrelated to the older one.NET interprets it as a reference to the Right property of the Form and UserControl object. reference the Microsoft. RightB functions VB. See Black Book Tip # 32 on Page 455 for a working solution.ActiveForm Screen. Strings module Visual Basic Run-Time Library Members.Next Math.File.Windows. To avoid this. Use Anchor property.Right(arg.Form. the VB.Split function.UTF8. StrConv function. and Debug. WriteLine. VB.0 – David Ross Goben VB6 element Spc function Split function Sqr function Static keyword Stop keyword StrConv function VB. but the Stop keyword will cause the application to crash. or StringObject. Extremely few applications or methods would ever benefit from such VB6 functionality. you should replace this Debug keyword with Debugger. the VB6 version would take both strings and Byte array as its first argument.0 Users.Encoding. meaning that all pertinent variables within a method should be declared Static if you wish to emulate the VB6 functionality.Break keyword will be ignored when the project is compiled. The Spc keyword was used in VB6 to insert spaces in a Print. Page –312– System Namespace. Split. Not supported in VB.Enhancing Visual Basic .NET SPC function is used only within Write. the VB6 version returns a non-initialized dynamic string array (the array has Lbound=0 and Ubound=-1. preserving the value of the variable during the lifetime of the running application.Break.Text.Text. The VB.NET version works only with strings. nor can it convert to/from Unicode.GetBytes(MyString) ‘To convert the Byte Array back to a String: Dim strText As String = _ System. because the Debugger.GetChars(Bytes) String function String[$] functions VB functions that returned Strings by appending a dollar sign ($) are replaced by overloaded methods. it cannot take Byte arrays.NET Beyond the Scope of Visual Basic 6. and Debug.Print statements.NET supports the Static keyword only at the variable-declaration level. It could also convert from/to ANSI and Unicode.Encoding namespace ‘To convert a String to a Byte Array: Dim MyString As String = “Hello” Dim Bytes() As Byte = _ System. class. Math Class Does not apply System. See String[$] Function for Visual Basic 6. Debug Class System. The VB. however.Text. Stop keyword.Encoding. nor is it a very practical solution. Their counterparts that return Variants have been replaced with overloaded methods that return Objects.) However.NET.Encoding namespace. Print#.UTF8.Sqrt Function Static keyword. or run-time library location Does not apply Does not apply System Namespace.NET equivalent SPC function. VB6 supported the Static keyword at both the variable-declaration level. String Class Does not apply . resulting in all variables within the method being treated as static variables. with only one minor difference from the VB6 function: when its first argument is an empty string. However.NET version more safely returns an array with one element at zero index set equal to vbNullString (“”). However. For example: Namespace. Math. such conversion functionality has been marshaled to the System. and though more powerful. though this is not memory-efficient. and at the method-declaration level.Text.Diagnostics Namespace.Print method. Tab function. See Structure Declaration for VB6 Users.NET.NET as 16-bit Short.NET. use Sub Dispose and/or Sub Finalize. however.Tan Not supported in VB. For example. or run-time library location Does not apply Does not apply System Namespace. this keyword can be removed with no difference in program operation. and Debug. Note that the Date data type is no longer represented as a Double. the VB.DateTime structure. Date Data Type.0 – David Ross Goben VB6 element StrPtr function Tab function Tan Terminate event Time function. The To keyword is supported inside Dim and ReDim statements. See DateTime Structure.NET the lower indices of the array can only be zero. Math. the VarPtr() function demonstrated on page 132 of this document can be used to return a valid StrPtr address. and Debug. Strings module Does not apply Does not apply . and a VB6 Long is a 32-bit Integer in VB.TopString.NET Tab function is used only within Write. Replaced by the TimeOfDay property of the System. The VB6 function returned a Single value.GetType.NET: If TypeName(MyValue) = “Integer” Then The text in the expression cannot be upgraded. DateAndTime module Visual Basic Run-Time Library Members. The text must be manually changed to “Short”. instead. Typename function or Object. class. In VB. there is one important detail that you should verify when migrating VB6 code over to VB.Enhancing Visual Basic . A VB6 Integer is in VB.NET an Integer is 32-bits. and you were upgrading the following statement to VB. in practice. Time statement Time$ function Timer function To keyword Trim$ function Type statement Typename function VB. However.NET Beyond the Scope of Visual Basic 6.NET and that is that in checking for upgraded variable type naming.NET. use the Structure Statement. under VB. See Using Constructors and Destructors. Page –313– Namespace. Not supported in VB.Trim function. and a Long is 64-bit.NET Timer function returns a Double value. DateAndTime module Does not apply Visual Basic Run-Time Library Members. Print#. Math Class Does not apply Visual Basic Run-Time Library Members. it is represented as a DateTime structure. Trim or the much faster StringObject. However. The VB. Normally. if a variable named “MyValue” was a VB6 Integer.NET equivalent Not supported.Print method. this is confined to VB6 Integer (16-bit) and Long integer (32-bit) variables. TimeString Property Timer Property. Therefore.Print statements. The Tab keyword was used in VB6 to insert spaces in a Print. DateAndTime module Visual Basic Run-Time Library Members. WriteLine. Unlock functions In VB6 you could use the UserControl keyword within a user control class to reference the current user control. the following test always succeeds in VB.ToUpper method.Object: If TypeOf value Is Object Then Does not apply If the test for Object is meant to check that a value isn’t scalar. or run-time library location TypeOf function. To avoid this.NET. and you will close the form while it is executing the form’s Load event. because all data types inherit from System. in the form “Me. but with a system error. However. or look to page 132. Note: On forms that you have displayed as a dialog (non-modal). However. except for passing the address of a Structure to a P/Invoke. For example. Page –314– Visual Basic Run-Time Library Members. if the application is to close itself when the primary form is closed. The VB6 VarPtr function returned the address of a variable’s data as an integer that could in turn be passed to P/Invokes that take addresses.Close” has the same effect. if you must re-close a form during its Load event. However. You cannot unload a form in VB. As such. in many cases the VB6 version does not exactly corresponds to the VB. Me.Close() is not enough.NET version.RunTime. even under VB6. Strings module Does not apply Visual Basic Run-Time Library Members. Object Data Type.NET as you did in VB6. because VB.GetUpperBound UCase function. to see how to easily implement this lost functionality back into VB.GetType().NET is uniformly configured. so does sending a variable ByRef. you would instead use the keyword Me. FileSystem module Does not apply Does not apply System.NET Beyond the Scope of Visual Basic 6. or ArrayObject. class. The in-load closing will cause an error because there are other processes that must execute during the form’s instantiation. Refer to the very first point of the next article.Enhancing Visual Basic .NET. there is little practical use to this functionality. Lock. activate a timer at the end of the Load event for a short period (10 milliseconds is more than enough time in most cases). in VB6.0 – David Ross Goben VB6 element TypeOf function VB. the form and application will close. but you must also invoke the form’s Dispose method if its resources are a great concern. before it can properly close. and let it close the form outside of the Load event. Invoking the form’s Close method.InteropServices namespace . or the much faster StringObject. see Universal Data Type Changes for VB6 Users. you will instead need to use code much like the following: If TypeOf value IsNot String AndAlso _ Not value.NET equivalent Namespace. “VB6 AFICIONADO COMPLAINT DEPARTMENT”. However.IsValueType Then Ubound UCase$ function Unload statement Unlock statement Usercontrol keyword Variant data type VarPtr function Ubound. class. DataFormats enumeration System.Windows.Rtf vbCFText enumeration DataFormats. it should be replaced by the GetTypeCode() method of the specific data types.Forms namespace.NET. DataFormats enumeration System. or run-time library location N/A System.DIB vbCFEMetafile enumeration DataFormats. DataFormats enumeration System.Windows. DataFormats enumeration System.Forms namespace. FileSystem module . FileWidth function Write # statement Write. which returns an object of class Type that has properties to get information.MetafilePict vbCFPalette enumeration DataFormats.FileDrop vbCFMetafile enumeration DataFormats.Bitmap vbCFDIB enumeration DataFormats. DataFormats enumeration Does not apply Visual Basic Run-Time Library Members.EnhancedMetafile vbCFFiles enumeration DataFormats. or use GetType().Forms namespace. DataFormats enumeration System. see Control Statement for VB6 Users.Windows.NET equivalent Although it still exists.0 – David Ross Goben VB6 element VarType VB. DataFormats enumeration System.Windows.Enhancing Visual Basic .Forms namespace.Forms namespace.NET Beyond the Scope of Visual Basic 6.Forms namespace. vbCFBitmap enumeration Vartype as of VB2005 allows you to inspect the actual type of an Object.Forms namespace.Windows. WriteLine functions Page –315– Namespace. which complies with uniform blocking declaration in VB.Windows.Palette vbCFRTF enumeration DataFormats.Windows.Windows. FileSystem module Visual Basic Run-Time Library Members. DataFormats.Text Wend keyword Width # statement While loops under VB.NET end with the End While statement instead of Wend.Forms namespace. DataFormats enumeration System. they can be automatically converted to and from 8-bit ANSI. it is just that a fully object-oriented interoperable development language like VB. even under VB6 the VarPtr function was famously ‘undocumented’. The returned address could in turn be passed to P/Invokes that expect addresses. Because the CLR must be free to move data around in memory so it can support the Garbage Collector. etc.NOT or Visual Fred (implying. when they are passed to a P/Invoke. of course. except for the rare instances where one needed to pass a fixed address to a P/Invoke.mvps. it has to know when it can and cannot move data. to in fact add this functionality to VB. you can supply strings directly to functions as ByVal strings and this will provide the beginning address of the string text.NET string are composed of a series of 16-bit (wide) Unicode characters. such as when that data is in use or not. which P/Invokes use in many cases. you had to use the StrPtr function. as far back as QuickBasic and even DOS Basic. VarPtrAry.NET VarPtr in #1 will do the job for you. such as the RtlCopyMemory() P/Invoke. which they may refer to as VB. If you used the VB6 VarPtr function on a variable of type String. there was little practical use for this functionality. The StrPtr function returned the address of the first character of the string (you must also take into account. all you need to do is place the line “Imports System. VarPtr is not available. that it is no longer Visual Basic. Even though VB. are stored as 16-bit-wide Unicode characters). once prominently featured at Karl E. even under VB6.Enhancing Visual Basic . under the title of “Visual Fred”. Page –316– . However. ObjPtr. explaining that it was retained for backward compatibility to older code. and ObjPtr are not and will not be supported by contacting Microsoft Technical Support (see the complete Microsoft Knowledge Base article Q199824 at http://support. StrPtr is not available. you would get the address of the BSTR (see page 127 for an explanation of BSTR). and so it must apply different solutions to many issues that diverge from how VB6 had implemented them. many things do have to be worked around in order to deliver identical functionality as that of VB6. in those few situations where you do need the string’s address. StrPtr. and each will feature my explanation of the issue and also my own means for implementation/duplication. StrPtr.NET strings. Allowing data to be moved by the CLR dramatically improves performance.NET Answered) Here we will examine 116 internet-famous complaints. Rhe VarPtr() method in #1 will get a string address. just by changing the “Declare” to “Declare Auto” in the P/Invoke Signature. it also included complete documentation on how to use it. but this does not mean that Visual Basic is now broken (the opposite can as easily be said).NET Beyond the Scope of Visual Basic 6. you accomplish much the same thing.com/kb/199824 for full VB6 implementation details). just as the VB6 StrPtr function had done.NET version of VB6 VarPtr. which is a pointer that in turn points to a Unicode character array that is next to useless.NET is restricted to following stringent hierarchal rules that cannot be as bent or twisted or hacked as severely as VB6 had famously bent and twisted and hacked them.Free() 'free the allocated space used End Function 2. However. which is utterly silly). Complaints with lines through them have been resolved by Microsoft. However. The StrPtr function is seldom required under VB. like the RtlCopyMemory() P/Invoke.AddrOfPinnedObject. StrPtr was unsupported for VB6 by Microsoft. The VB6 VarPtr function returned the address of a variable’s data as an integer that could in turn be passed to functions that takes pointer addresses. Even so. the VB. To get the actual address of the string data itself.ToInt32 'get the address of the pinned object (the variable's data address) GC.0 – David Ross Goben VB6 Aficionado Complaint Department (116 VB6 User Complaints against VB. they were very clear regarding the fact that commands such as VarPtr.Alloc(o. Under VB.NET string. by sending a variable ByRef to a P/Invoke (String as ByVal). GCHandleType.org/). that VB6 die-hard fanatics railed against VB. though not through VarPtr. In fact.NET. There.Pinned) 'get a trackable handle and pin the obj address VarPtr = GC.NET. The original text (complaints) will be underlined at the beginning of each numbered point. Granted.NET. As such. though through the fact that Microsoft had explained as much. or returned from a P/Invoke to a VB. the address of such a data item can still be retrieved. that VB6 Strings. Petersen’s Classic VB website (http://vb. (ALL of them are now supported by this one function) Public Function VarPtr(ByVal o As Object) As Integer 'use Object as a 'catch all' universal data type (this can also be 'As IntPtr') Dim GC As GCHandle = GCHandle. 1.InteropServices” at the top of the file before the declaration of your class or module and then add the following method to that class or module: 'VB.NET.Runtime.microsoft. The reason why this protected and normally inaccessible feature is not directly supported by VB. just like VB.NET is because it accesses unhandled (unmanaged) memory space. but instead through invocations into the managed space of the CLR (Common Language Runtime) using GCHandles (Garbage Collector Handles). if you try to manipulate the string at that point. VarPtrStringArray. there is in fact support for passing parameters As Any by using the System. which defeats the object’s ability to protect its own data from intrusion. though this is not a recommended practice.NET eliminated the need for As Any. of course it can access them! And thank all that is good for it! This is standard Object-Oriented fare that any professional who has experience in object-based programming knows about and does not even think twice about. Private. <MarshalAs(UnmanagedType. what follows is code that will provide “As Any” support for the CopyMemory() (actually RtlMoveMemory()) P/Invoke (be sure to also place the instruction “Imports System. except in the very specific case where an instance object of a class is accessing another instance of that exact same class! Because each member of the same class shares the exact same code. However. To put it succinctly. we would have to expose the NextDataCell and the PrevDataCell members as Friend or Public. but a much-demanded feature.Enhancing Visual Basic . or Dim. ' NOTE : Take advantage of the unique fact that a class object can access private ' : members of other class objects that are declared to be instances of this ' : class (because this class has specific knowledge of instances of itself. Fields or methods declared anywhere within a class are normally private to the class instance if they are declared Protected. Dim. whose codespace must be able to access all objects instantiated of it class. Overloading under VB.AsAny)> ByVal Source As Object.NextDataCell = Tmp 'Complete swap of NextDataCell member fields End Sub In the above example.NextDataCell 'access private member of Sibling Object because it is a member of this class Sibling. For example. 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 P/Invokes (APIs).AsAny)> ByVal Destination As Object. 4.NextDataCell = Sibling. This was addressed in VB2003. Page –317– . Granted. even under VB6. ObjPtr is not available. you would think that being that two cells of the same class are uniquely able to access Private. where members of the chain need to swap their protected sequential locations in the chain: '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Protected NextDataCell As DataCell 'next data cell is this data cell's chain Protected PrevDataCell As DataCell 'previous data cell is this data cell's chain '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '********************************************************************************* ' Method : SwapWithSibling ' Purpose : Swap doubly-linked list references between sibling cells. which is why the As Any feature was added to VB6 in the first place.NET Beyond the Scope of Visual Basic 6. but because.Runtime.PrevDataCell = Sibling. and this single body of code must be able to access every member that is instantiated by this class. and in order to perform the above. a class that inherits from this class does in fact lack the ability to access these private members of its base class so those members are in fact hidden. What they mean by this is that an object instance of a class is actually able to access private or protected members of another object instance of the same class.InteropServices” at the top of the file body): Public Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( <MarshalAs(UnmanagedType. when you need to swap private references between two class instances in a doubly-linked list. as of VB2005. This is not a bug. Private class variables are not private to the class instance. ByVal Length As Integer) 5. where one Cell of type DataCell is accessing a sibling DataCell object. their codespace is actually the very same shared codespace. VB6 was unable to support overloading. As Any is not supported for API Declares. Even so.PrevDataCell 'access private member of Sibling Object because it is a member of this class Sibling.NextDataCell 'swap private references to Next Data Cells Me. If the class were ignorant of itself. and should only be used by experts.InteropServices namespace.PrevDataCell = Tmp 'Complete swap of PrevDataCell member fields Tmp = Me. otherwise this capability would be impossible) '********************************************************************************* Friend Sub SwapWithSibling(ByRef Sibling As DataCell) Dim Tmp As DataCell = Me. Use the VarPtr() function provided in #1 to obtain the address of an object.0 – David Ross Goben 3. to reiterate. we want to enable the ability to swap DataCell Class members in a linked chain. Consider the following method declared in a doubly-linked list class. shared codespace must be able to access all members that encapsulate that code. it would not be able to do the above.PrevDataCell 'swap private references to Previous Data Cells Me. This is also a very powerful and very useful feature. which is access the NextDataCell and PrevDataCell members of the provided Sibling. or even Protected members defeats this.Runtime. for instance. Use of ByVal/ByRef directly within API calls is not supported. 6. ' : it is able to do this. because dynamic arrays are the only type of array that is allowed. UBound = upper bounds. <VBFixedArray(40)> Dim myInts() As Long 'simplified and much better declaration format for the above line However. because classes (structures are actually abstract classes) store its code separately from its data fields).NET Beyond the Scope of Visual Basic 6. a Structure. value type._lbound) = Value End Set End Property End Class This type of class allows us to define not only the lower and upper bounds of an array. it can be easily emulated through a very small helper class (this is very much how VB6 did it). The easiest solution is to simply embed a method named Initialize at the bottom of the structure (this will not affect any structure size value. 10) 'define string array a() to have bounds 1 through 10 8. Technically. ByVal UBound As Integer) Me. Set as Default so you do not need to specify . you will need to also dimension the array after you instantiate the structure because the provided dimensioning value (40) only tells the marshalling support how much reference storage space (40+1 references) to allow for (is it just me. because it already knows the target dimension value? Hmmm._lbound) End Get Set(ByVal Value As itemType) _vbArray(Index . it is the other way around: fix-sized arrays are not allowed within Structures. LBound = Lower bounds. if such a thing is really needed. above End Sub End Structure Page –318– . Dynamic arrays are not allowed within Structures (UDTs). to be fully compliant with the OOPL paradigm. such as the result of Len(myStruct).ReDim(LBound.ByValArray. but the type of array. or could VB. Consider the following example: Structure BOXSTRUCT Dim boxHeight As Integer Dim boxWidth As Integer <VBFixedArray(32)> Dim boxName() As Byte 'set aside space for 32 bytes 'method that can be used to resize the VBFixedArray member of the structure once it has been instantiated Public Sub Initialize() ReDim boxName(32) 'note that this parameter value MUST match the VBFixedArray parameter..NET not have easily taken the next logical step and simply automatically redimension this for us. ByVal UBound As Integer) _lbound = LBound ReDim Preserve _vbArray(UBound . you can declare a Long array named myInts with 41 elements (0-40) within your structure in one of the two following ways: <MarshalAs(UnmanagedType. whether it be String. but retain previous data End Sub 'Default Property. Arrays may not have a lower bound other than zero. That would have been my complaint).InteropServices namespace.NET definition with the VB6Array class: Dim a As New VB6Array(Of String)(1. Obtain and assign indexed item. The array lower bound is forced to zero.Runtime.. you can work around the problem of not being able to declare sized arrays by using the System.LBound + 1) 'redimension. Public Sub New(ByVal LBound As Integer. However.Enhancing Visual Basic . Integer. SizeConst:=40)> Dim myInts() As Long 'or more easily as. If you want to emplement it so you can upgrade the following VB6 definition: Dim a(1 To 10) As String 'define string array a() to have bounds 1 through 10 Use this VB. However. With it imported. another Class.Item() Default Public Property Item(ByVal Index As Integer) As itemType Get Return _vbArray(Index . 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. or any other type. For example: ' simple class (no error-trapping) to define a 1-dimensional array with a non-zero lower bounds Public Class VB6Array(Of itemType) 'itemType is a placeholder for the actual instantiation array type Private _lbound As Integer 'save lower bounds Private _vbArray() As itemType 'array definition 'define lower and upper bounds of array.0 – David Ross Goben 7. 477. Everything in the . 12. value types or abstract classes). With 28 decimal places. 10. Variants are not supported. allocating 41 elements.162. Arrays are not declared using the upper bound.337.. “CDec(123). scaled by 10. where the zeroeth element is considered part of the count. which it was loosely based upon. to simplify dimension allocation by making the upper bounds of arrays less confusing to users.NET supports Decimal but not Currency.. For example. who might not understand zero-based definitions.337.477. ReDim boxName(32. Decimal variables are stored as 96-bit signed integers.00” in Region US-En). VB.NET CLR (Common Language Runtime) is zero- based. but you could not declare a variable to be of type Decimal.) As Byte 'set aside space for data (note the Rank in the parentheses. you can specify the Decimal’s ToString(“C”) method. the value used to dimension an array specified the number of items to allocate. The powerof-10 scaling factor specifies the number of digits to the right of the decimal point.335. a serious issue with the Currency data type was that it did not provide sufficient accuracy to avoid frequent rounding errors. VB.950. Currency variables were stored as 64-bit numbers in integer format.Initialize() 'initialize any dynamic members of the structure using this developer-built method NOTE: If you require multiple dimensions. adjust the pertinent lines in the above structure to: <VBFixedArray(32.NET Beyond the Scope of Visual Basic 6. 11. it diverged from FORTRAN. Currency is not supported.32)> Dim boxName(.685.5807. Program that use variants are slow because they require a lot of interpretation at runtime. such as 32x32. Granted.514. especially among those developing financial applications.337. although variants could have a subtype of Decimal. see the response for # 7. scaled by a variable power of 10.264. defining a general type as Object is just like using Variant. as is evident by examining most professional high-level languages. VB. VB. Conversely.NET uses the Object data type as its universal data type because all class objects and value types inherit from it.9228162514264337593543950335. the largest possible value is +/-79. Professional developers actually prefer the former method.32) 'note that this parameter MUST match the VBFixedArray parameter.Enhancing Visual Basic . Previously.NET defines Decimal as its own data type and used it to completely replace the error-prone Currency data type. However. such as hobbyists. Hence.NET’s type system is also simplified by having only one universal data type. Page –319– .0 – David Ross Goben We can now instantiate and initialize an instance of the above structure in the following way: Dim myBox As New BOXSTRUCT 'the optional NEW initializes all member fields of a Structure to their default values myBox. This was addressed in Beta2.NET could have continued to use Variant for its universal data type. VB. 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. above 9. and the smallest nonzero value is +/-0.5808 to 922. Even so. the largest value is +/-7. because zero-based arrays are fully understood. NOTE: To display the Currency format of a Decimal value. Option Base is not supported.NET now also does this. With a scale of 0. indicated by a comma) .000 to give a fixedpoint number with 15 digits to the left of the decimal point and 4 digits to the right. like C and C++. if you really do need to use an array with a non-zero base. VB. VB6 supported a Currency data type.203.685. according to current culture settings. but with the added benefit of them also being processed much faster. “Dim Test(40) As Integer” had originally created an array with elements indexed 0 to 39.543.203. VB6 would create an array with elements indexed 0 to 40. ranging from 0 to 28. Because all variables are in fact objects (technically.0000000000000000000000000001. which was the biggest complaint for this type.228.593. This representation provided a range of -922. When BASIC was developed.ToString("C")” yields “$123. NET format 'display result of it 14. Any recognizable literal date values can be assigned to Date variables. Negative whole numbers represent dates before 30 December 1899.0000000001 fraction of a second. Unsigned versions of these types are available by pre-pending “U” to the type. Dates are not stored internally as Double values.0 – David Ross Goben 13. To non-US standards.5. which conveniently used the Double format. they are 32-bits. because 64-bit integer manipulation is many times faster than manipulating Doubles. they can no longer be manipulated directly as a 64-bit Double. Longs are not 32-bits. Under VB. Regardless. and also a MegaMillion. By representing dates as integers. True. This results in much faster program execution. this simplified and significantly sped up the manipulation of dates.ToOADate() Debug.NET Framework technologies are designed around modern 32-bit and 64-bit technologies. Integers are not 16-bits. NOTE: A nanosecond is one billionth of a second.NET Beyond the Scope of Visual Basic 6.NET Framework (the OA in the functions represents OLE Automation). values to the left of the decimal represented date information. representing part of a 24-hour day in seconds (24*60*60. 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. while values to the right of the decimal represent time as a fraction of 1. When other numeric types are converted to Date. a Date variable was stored internally in a Double format and could be manipulated as a Double.400 seconds). 15. a US Billion is a thousand million. DtNET2 As Date Dim DtVB6 As Double DtNET = Now DtVB6 = DtNET.NET date/time format to VB6 format 'display result of it 'convert VB6 format date/time to VB. It therefore makes a lot of sense to update the data sizes to the new technology.9999999 PM. and times from 12:00:00 AM (midnight) through 11:59:59. Therefore. Using OOPL standards. Further. the size of an Integer should be the word-size of the platform. Regardless. that means that Integers should be 32-bit wide.Enhancing Visual Basic .FromOADate(DtVB6) Debug. Integers should always be the word size of their platform. However. See note for # 14. and 100 nanoseconds is a 0. which is also called a Milliard. especially for professional developers who require cross-language compatibility and need to develop code for modern PC platforms. But this is really a good thing. User Short or Int16 if you want 16-bit integers. Due to this finer tuning. Use Integer or Int32 if you want 32-bit integers. Date variables hold IEEE 64-bit integer values that represent dates ranging from January 1 of the year 0001 through December 31 of the year 9999.NET-compliant Date (Date and Time) using a variable’s ToOADate() and FromOADate() methods provided by the . such as they are implemented in C++ and C# and VB.NET. 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).Print("VB6 Format Date: {0}".NET. midnight was 0 and midday was 0.00000001 fraction of a second. For example: Dim DtNET. By strict software engineering standards. they are 64-bits. 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. a nanosecond is a 0. The maximum value represents 100 nanoseconds before the beginning of January 1 of the year 10000.NET Format Date: {0}". 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 word-size). or 86. Under VB6. Unsigned versions of these types are available by pre-pending “U” to the type. and add to that the fact that some of the . DtVB6) DtNET2 = Date. 1752 in America. 1923 in Greece. DtNET2) 'get present date and time 'Convert VB. Date variables were stored as IEEE 64-bit floating-point numbers that represent dates ranging from 1 January 100 to 31 December 9999.Print("VB. Page –320– . On a 32-bit platform. times were from 0:00:00 to 23:59:59. and finally 1999 in Russia). you can still use it if you need to by converting between the VB6-compliant Double and the VB. even for multi-dimensional arrays 'VB6 works fine here. are not supported. This feature was a carryover from concepts for FORTRAN 57. D’OH! I think that it should have remained 1 for C/C++ compatibility. DefLong. I guess too many people checked for < 0 (less than zero) rather than correctly for <> 0 (not zero). 10)”. such as “Dim myStr As New String(ChrW(0). ReDim will not create arrays not already declared. unlike VB6. by adding a reference to and importing Microsoft. Fixed-length strings were not supported in the first version of the CLR. the dynamic array then Page –321– . In an OOPL world.NET cannot declare a simple un-initialized dynamic array. You can prepend “<VBFixString(bytecount)>” to the string declaration if the string is declared in a structure. such as “Dim tmpArray()”.and variable-size arrays. if A and B are more complex. but is 1 instead. 17. but VB. Use Private for fields and methods at this level and they will be private. For example: Dim tmpArray() As String ReDim tmpArray(10. or visa versa. yes they are.. et al. From this.NET documentation. Eqv is simply replaced by the “=” operator in the form “result = (CBool(op1) = CBool(op2))”. Imp can be implemented using “result = Not A Or B”.NET form requires the dimension rank. StringBuilders are definitely worth exploring in the . or. that old approach makes very little sense. These old-school 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. Readability and robustness of code is greatly improved by avoiding the use of implicit type declarations. Personally.or 2-characters. VB6 had a distinction between fixed. For example: Dim tmpArray(. Though very little used.NET Beyond the Scope of Visual Basic 6. such as “Dim myStr As New StringBuilder(64)”. use “result = Not (A) Or (B)”. 19. By this I assume you mean a Field that is a sibling of procedures (methods). Addressed in Beta2. 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. 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. What part of “re” is not understood? This hack functionality was originally meant to work with Variants. or. DefInt.VisualBasic. is not -1. True. VB. which features string manipulation that is 200 times faster than with standard strings. the world’s first high-level language. Enabling ReDim to also declare standard variables in VB6 was a hack. However.) As String ReDim tmpArray(10. I suggest you only use Dim for declaring local variables within methods. such as ReDim tmpArry(10.NET.FixedLengthString(charcount)”). Fixed-size arrays were declared with the Dim statement. Support was later added through the VB6 Compatability Library once the VB Upgrade facility was implemented. True. Standard variable type arrays in VB6 should have been explicitly declared before being RE-dimensioned. Dim may not always create procedure-level variables. So. where they cannot accept the Private keyword. coerced to an Integer. they are almost too E-Z to implement. I would rather simply dynamically declare a string of a specific length.0 – David Ross Goben 16. which includes the bounds of the array within this declaration. you might instead opt for a StringBuilder (declared in the System.NET will issue an error notice due to rank variations Under VB. 18. you can then declare a variable or field “As New VB6. The Imp and Eqv operators are not supported. to be specified 21. Fixed-length Strings are not supported.Text namespace).Compatability. Another difference with Dim that many developers may not at first be aware of is that. on which BASIC was loosely based. 10) 'the VB.Enhancing Visual Basic . 20. and then redimension it multi-dimensionally. 64)”. 10) 'VB6 allows a simple declaration form. and using it this way is also very bad programming practice. Dynamic arrays were declared in Dim statements by not specifying bounds information. indicated by commas. you can easily define custom functions to support them. which confines scope only to the zone of the block range they are declared within. The ReDim statement is. Specifically. This is all in accordance to strict OOP rules.. But they are in block- local scope within the block level they were declared within.NET all arrays in VB. under VB6 they were recognized from their declaration forward within the body of a method or class.NET both to declare and to initialize a dynamic array. from their declaration forward to the block’s end. who depended upon strict scoping rules to govern the visibility of local variables. because this practice was not in compliance to general scoping rules. therefore. 22. Local variables are not necessarily visible (in scope) throughout a procedure. If the variable needs to be available to the entire procedure. and a Dim statement can be used in VB. this also generated more code Page –322– . the variable "y" in the example above is available only within the block in which it is declared.Enhancing Visual Basic . the VB6 ReDim statement also provided a shorthand way to both declare and allocate space for a dynamic array within a single statement.NET because local variable “y” would not be accessible outside the scope of its declaration block.NET are dynamic. To require only the Dim statement to be used to declare variables keeps the language simpler and more consistent.. Just as procedure locals support structured programming by allowing variable definitions that are private to a procedure. Under VB. Consider the following poorly-designed. and by sub-blocks. if those sub-blocks did not over-ride them with like-named local variables declared within those deeper.. This VB6 variable leakage problem was actually a pain in the butt for OOP developers. block-level variables support structured decomposition by allowing variable definitions that are private to its block. Only undisciplined VB6 programmers took advantage of this anyway. Sadly. such as a variable in an inner block that can be used by an outer block. as software engineering standards declare that a well-designed programming language should behave. which is actually very poor design and Microsoft should have known much better than to allow that. Perhaps unbeknownst to them. Unfortunately.. it is available only from its declaration. though working VB6 code: If x=1 Then Dim y As Integer End If If x=1 Then y=2 End If The above would make no sense under VB.NET. Strict block scoping rules are common to structured languages. as it should. End If End Sub Under VB. The VB6 ReDim statement happens to also be the only statement that can be used both to declare and to initialize a variable.0 – David Ross Goben needed to be redimensioned using the ReDim statement before it could be used. Else '. Because all variable declarations can both declare and specify an initial value for variables. Some VB6 programmers used this technique to declare variable only if they are needed. regardless of their block-local scope level. For example: Sub Test(ByVal x As Integer) If x < 0 Then Dim y As Integer = -x '. used only to allocate or reallocate (redimension) the space for an existing dynamic array. It also caused a significant number of bugs for those who made the silly assumption of believing that VB6 might actually respected block-local scoping rules. inner blocks. then it must be declared outside of the If/Else/End If control structure. VB6 allowed “y” to be used anywhere after its declaration.NET Beyond the Scope of Visual Basic 6. down to the Else statement. the use of ReDim to declare and initialize variables becomes redundant and unnecessary. Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 If x=1 Then End If If x=1 Then 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. You 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. 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 you 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. 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. 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 objects) and class-instantiated objects (reference types; concrete objects). Page –323– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 your code file, and suddenly VB6 “vb” color codes are restored. However, even 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 the above additional code overhead, you can instead just use the following constants, declared in a module somewhere: Public Public Public Public Public Public Public Public Const Const Const Const Const Const Const Const vbBlack As Integer = 0 vbBlue As Integer = &HFF0000 vbCyan As Integer = &HFFFF00 vbGreen As Integer = &HFF00 vbMagenta As Integer = &HFF00FF vbRed As Integer = &HFF vbWhite As Integer = &HFFFFFF 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. However, it could be forced with the Garbage Collector’s GC invocation, “GC.Collect()” (if you did not want the application to continue until the garbage collection process completed, follow that with the “GC.WaitForPendingFinalizers” command). Nevertheless, when the last reference to the object is set to Nothing, you can no longer touch the actual object the reference pointed to. Also, because you cannot force reconstitution (resurrection) by rereferencing 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. 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. The reason that VB.NET objects are not immediately destroyed when the user sets them to Nothing is because the space used by the reference variables that we typically set to Nothing are often reused when we instantiate another instance of a new, identical object. Totally obliterating an object, such as VB6 did, involves the time-consuming process of releasing resources, object method interfaces, and such. By delaying their destruction until the Garbage Collector runs as a separate thread at indeterminate intervals and invokes an object’s Finalize method, that object space is most-likely still around when we instantiate the new object, and so that unused but still allocated space can be immediately released, re-used, and re-initialized extremely quickly, because object space and interface tables already exist. I never understood why Microsoft never got up in front of this uniformed but strongly-worded user-complaint and simply clearly explained the reason to them. 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 Page –324– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 you have code that holds a resource handle open (for example, Microsoft SQL Server™ connections or file handles), you 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 you implement IDisposable, you should write the Dispose method code 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 you may not be able to determine which will invoke Dispose first. Also, you can guard against instances where a consumer dos 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 your problem is? See note for # 32. When you 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. Object instantiation should be, as it is under VB.NET, performed immediately upon declaration. This complaint is actually an issue that has to do with COM (Component Object Model). This VB6 derivative “feature” was also the source of a lot of confusion regarding memory leaks. This quirky VB6 behavior should never have been allowed and should have instead been considered and addressed as a critical software bug. Because of its imperfect implementation in VB6, the following code: Dim x As MyClass '... Call x.MyMethod() 'Declare empty reference with no instantiation '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 '... If x Is Nothing Then Set x = New MyClass End If Call x.MyMethod() 'Delclare empty reference with no instantiation 'If x is an empty reference (no instance yet instantiated)... 'Declare an instance of the class for x to point to 'invoke a method of the object x is pointing to Actually, it should be rare that this problem will ever be an issue under VB.NET. Even so, if code tries to use a class after it has been set to Nothing, it will cause a run-time exception. 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 ... If x IsNot Nothing Then x = Nothing x = New MyClass 'Delclare a new reference 'If x is not an empty reference, then make it so 'Declare an instance of the class for x to point to Or, more simply: Dim x As New MyClass ... x = Nothing x = New MyClass 'Delclare new reference 'If x is not an empty reference, then make it so 'Declare an instance of the class for x to point to Well, technically, when you assign a new object to a reference variable, the old object will be automatically thrown away, anyway. Thus using only “x = New MyClass” works just fine. Page –325– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 35. Public object variables are not safe from alteration when passed as parameters. DUH! Of course not! This is poor programming practice. Normally with variables, if there is any doubt, then explicitly pass parameters as ByVal, which will prevent it from being altered because a clone of the value will be sent to the method instead of the address of the public variable. However, variables referencing instances of class objects are simply references, so sending a reference variable ByVal will still send a clone of the variable that is still a pointer to the same actual object instance. This is poor program design and is a technique that should be avoided at all costs. Also remember that strings, and arrays of any type, are actually objects and not value types. 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 you 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 so rare, it is really no big deal. Simply name the exposed Public property as your 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 that did not specify either ByVal or ByRef defaulted to ByRef. For VB.NET, this change to a default of ByVal was used 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 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 ability may have been handy for those who, for whatever reason, wanted to manipulate the optional parameters passed to it and have those changes made permanent, but such functionality is much better served by absolute method invocations. A much more common scenario for ParamArray arguments is for them not to modify variables that are passed in to them. Not supporting ByRef ParamArray arguments also simplifies the ParamArray calling convention by making ParamArray arguments normal arrays. 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 an argument list. Indeed, because arrays are objects, passing a dynamic reference array to a method as a normal parameter allows such variable-count references to be processed by reference. 39. Property parameters may not be passed ByRef anymore. Yes they can… but only when they are used with objects. However, an important question to those who are complaining is – what is a Set property that passes back to the invoker modified values for parameters? It is one with bad design. Properties should either accept or return data and should most certainly not modify the containers for the passed parameters – though they can modify their locally-stored copies of that data all they want. The functionality they are describing is a job best suited for a method, not a property. Page –326– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 40. Implements is not implemented the same, so must be rewritten. Actually, .NET makes polymorph implementation of class interfaces easier. Implementation is now supported the way OOPL Implementation is supposed to be supported, which is to say that it is no longer limited to supplying polymorphism only through class shell interfaces (actually, this class shell interface was just one more of many VB6 cheats). Most object-oriented programming systems provide polymorphism through inheritance. VB6 did not use or support inheritance to provide polymorphism, and so provided polymorphism only through multiple ActiveX interfaces. These minor changes complained about by VB6 users were in fact necessary in order to implement inheritance, which was a feature too often begged for or loudly demanded by the very same VB6 users who were complaining. Implements under VB.NET differed from VB6 in two essential ways: 1. 2. Class skeletons cannot be specified in Implements statements (VB6 used class skeletons as a cheat because VB6 actually did not support genuine interface objects because it did not support inheritance). Every method that implements an interface method requires an Implements clause at the end of the method declaration statement. This helps document when and from where an interface method is being implemented. VB.NET maintains a strict distinction between interfaces and classes. Readability is also improved by requiring an Implements clause on each method that actually implements a method from an interface. It makes it obvious when reading code that the method is being used to implement an interface method. For example, if class "a" implements interface "b", the interface is declared for "b" and class "a" is changed to implement the interface of "b": Interface b Function MyFunction() As String End Interface Class a Implements b Function b_MyFunction() As String Implements b.MyFunction 'this entry is generated when you add the above line End Function End Class 41. Static is not supported as a procedure level modifier. Under VB6, Procedures could be declared with the Static verb, which indicated that all of the method’s (procedure's) local variables were preserved between invocations. Under VB.NET, in keeping with OOPL specifications, the Static verb is not supported on a method, and all static procedure-level variables need to 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 you want to re-instantiate an object that you have already gone though all the trouble of intentionally releasing it? Such a strategy is very poor program design and should never have been allowed in the first place under VB6 (one of many bug-as-feature issues that I have had with VB6 over the years). On top of that, does not “As New” actually imply that we want a new object, not the old one? 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 methods invocations, especially functions, looking like variables when they were missing their parentheses (invoked without using the “Call” keyword). When examining C/C++ code, it is quickly apparent if something was a variable or a method simply by the presence of the parentheses and the context of their use. Page –327– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 actual use of the Set keyword would be a totally useless, redundant requirement. Even so, if you do type it, it will be accepted, though automatically deleted. 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 be passing the text contents of the text box. Face it – the VB6 implementation was total crap for selfdocumentation 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 also 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. 46. Default values for Optional parameters are not optional. One should always declare a default value for every optional parameter for reasons of self-documentation. For each programmer who makes this complaint, many more will wail if a parameter was optional and did not have a default value declared. The default of zero for numeric parameters or empty for strings was a cheat, but it also did not make procedural self-documentation clear, especially when Variants were passed as parameters under VB6. The main reason for this requirement is due to the design of Delegates, or prototype templates, which are used as structure guides used by the compiler to ensure that methods and its parameters are passed correctly. At the compiler level, the compiler needs to know what to pass if the invoker did not explicitly specify an optional parameter. Even though you might think that numerical values should default to zero, and strings to Nothing, this is not good programming practice, and should be avoided. Under VB6, I always specified what an optional parameter should render by default, just so it would be clear to my code reviewers. 47. Code is not compiled to native, thus making recompilation much easier. Think that one through, please. ALL code, even native, can be easily de-compiled and the resulting source code can then be altered and/or recompiled (I have done that for learning purposes since I had a TRS-80 Model I). It is true that due to all the embedded meta-data, VB.NET code could be generally reconstructed from the Intermediate Language tokens. As such, Visual Studio does provide access to a free code obfuscator, which impedes this de-compilation, and other more robust obfuscators are available for sale by third parties. Even so, this is not some shiny new issue that only cropped up because Microsoft chose to employ an extremely powerful Machine Independent Language. Decades ago I used to constantly de-compile assembler and pcode to learn how a compiled language did what it did. By the way, p-code (pseudo-code) was an intermediate code resting between a high-level language, such as Pascal, and machine-native code, and it was also easy to decompile them into compilable source code. This thorny issue has existed since the heyday of FORTRAN, the first high-level language compiler, first became viable 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). Page –328– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 in a .resX file. To ease transition from VB6, the Microsoft.VisualBasic.Compatibility namespace contains the VB6 member, which exposes 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 resource data. With the introduction of namespaces such as My.Resources, My.Computer.Audio, and My.Resources.ResourceManager, you have a tremendous amount of raw power suddenly at your fingertips, totally eliminating the need to implement P/Invokes just to access all of that data, not to mention that there no longer exists a need to separately compile resources because the IDE can automatically compile and recompile them for you as you update them. 49. LSet is not supported. Addressed in VB2003. Regardless, this was not a safe method for copying structures or UDTs. LSet was often used to copy one User-Defined Type variable to another. This can now be done more easily by assigning one structure to another using the equal sign (Struct1 = Struct2), because structures are handled as value types (abstract classes), not reference variables (concrete classes), and so each variable contains 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 emplemented 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 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/fields are also technically structures, which is a form of a class known as an abstract 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 VC++ and C#. Page –329– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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. 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 prepending “<StructLayout(LayoutKind.Sequential)>” to the beginning of the structure to ensure that it in fact passes it members contiguously. You must also Import System.Runtime.InteropServices into your 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 additional code. 54. While/Wend loops are not supported. Yes they are, but as While..End While loops instead. Big 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. 55. GoSub/Return is not supported. We are now doing structured programming, baby. Primitive referencing has absolutely no place in Object-Oriented Programming, but only in procedural languages that did not recognize method bodies, but which VB.NET most definitely does. Well, maybe I should take some of that back. After all, GOTO is fully functional in VB.NET. 56. On/GoTo is not supported. See note for # 55. However, “On Error Goto 0” is still supported, to kill error detection code initiated by “On Error Resume Next”, though one should most definitely instead use the Try…Catch… End Try structure, which is much friendlier to structured programming, they can be grouped where the error might be encountered, and in my mind are much easier to use. 57. On/GoSub is not supported. See note for # 55. 58. Line numbers are not supported. Labels may be numeric. You still want to use line numbers? See the note for # 55. 59. Erl is not supported. No line numbers, remember? PUH-LEASE, if you want to do DOS BASIC, then go to a garage sale and pick up an ancient computer. 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 –330– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 you have any window forms loaded, which is usually the case. You 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, you can employ the ToOADate and FromOADate functions to convert it (both date and time) to and from the VB6 format. Following is an example of usage: Dim DtNET, DtNET2 As Date Dim DtVB6 As Double DtNET = Now DtVB6 = DtNET.ToOADate() Debug.Print("VB6 Format Date: {0}", DtVB6) DtNET2 = Date.FromOADate(DtVB6) Debug.Print("VB.NET Format Date: {0}", DtNET2) 'get present date and time 'Convert VB.NET date/time format to VB6 format 'display result of it 'convert VB6 format date/time to VB.NET format 'display result of it 63. The Time statement is not supported. It most certainly is, and is much more powerful, though it no longer returns the more primitive OLE Automation-format double-precision value as it did under VB6. However, you 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 create. 65. Comparison operators are not evaluated before logical operators. Addressed in Beta2. 66. Sqr is not supported. Use Math.Sqrt. Big woop. 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 you 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 your system resources and memory permit. Elements of the same control array have their own property settings. Under VB.NET, The Windows Form Page –331– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben architecture natively handles many of the scenarios for which control arrays were used. For instance, in Windows Forms you can handle more than one event on more than one control with a single event 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 prefer not to use it, but prefer to easily build my own control arrays and add event support through the AddHandler method. The free companion to this document, “Navigating Your Way Through Visual Basic 6.0 To VB.NET Upgrades” (www.slideshare.net/davidrossgoben), explains this process quite well. Also, as of VB2008, forms once again have a Controls and a Forms collection class. 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. The VB.NET OpenForms (My.Application.OpenForms) property returns a FormCollection object (System.Windows.Form.FormCollection; as opposed the less obvious VB6 Forms collection) that contains all of the application's open forms. 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. You 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 you 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 your file to fill the list). If you upgraded a project from VB6 to VB.NET, you 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, you use VB6.GetItemString(), as for example, “Dim Result As String = VB6.GetItemString(List1, List1.SelectedIndex)”, or you can instead use “Dim Result As String = List1.Items(List1.SelectedIndex).ToString()”. In most cases from here on out, it is assumed that you will be developing code in VB.NET from scratch. As such, you can greatly simplify all this by instead employing a very small custom class and gain complete control over your list and reporting methods. When you realize that you 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 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 you 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 you might need): Page –332– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Option Strict On Option Explicit On Public Class ListItem '------------------------------' protected Field Data - You can store more than just these fields here '------------------------------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 you need to do is add items to your ListBox or ComboBox using this class. For example, suppose you had a ListBox named ListBox1 on a form and you 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 .Items.Add(New .Items.Add(New .Items.Add(New .SelectedIndex End With ListItem("Carol Philips", 101)) ListItem("Jim Kim", 102)) ListItem("Jossef Goldberg", 103)) ListItem("Patricia Doyle", 104)) = 0 ' Note that the VB6 AddItem became Add, which brings it in ' line with other objects, and now a Listbox and Combobox ' exhibit the same functionality, whereas previously this ' was often confusing between the two. 'Optionally set then first item as the selected item. 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 'Alternately, 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 you 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 –333– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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, you could declare a private field with a name based upon NewIndex in your form and reserve it for use with a particular ListBox or ComboBox. Then, each time you added an item to the ListBox or ComboBox, you would save the return value to the associated variable (which is typically tossed away by not being collected). Your 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 you will not be using the NewIndex value except to set the selection to the new item (especially useful when the list is sorted), you 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 window handle (hWnd property). Because of this, they used fewer system resources. You created a lightweight user control in VB6 by setting the 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 (window handles) when you have a very large number of controls on a form. However, this applied to Windows 9x ONLY. Windows NT, Windows 2000, Windows XP, Windows Vista, and Windows 7/8 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 VB2010 does not yet support them). Instead, see my earlier article, Emulating VB6 Images Control Features in VB.NET, on page 72 to see how to implement controls that will emulate the lost windowless VB6 Image control. 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 Languages). During an upgrade, lightweight controls become Window Forms. See # 75. Also see my earlier article, Emulating VB6 Images Control Features in VB.NET, on page 72 to see how to implement controls that will emulate the lost windowless VB6 Image control. Page –334– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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+16 classes in System.Drawing replaced this control. If you needed to draw shapes on the form, you would override the OnPaint or Paint event to paint circles, squares, and so forth by using the GDI+ Draw methods. It is much faster, more powerful, and has more options than VB6 drawing methods. Consider the following example: Private Sub Form1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint Dim pen1 As New System.Drawing.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 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 online for free 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 you add a .NET referene to Microsoft.VisualBasic.PowerPacks.Vs, you can replace the VB6 command “Dim Printer As New Printer” with “Dim Printer As New PowerPacks.Printing.Compatibility.VB6.Printer” and you 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 you 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 you to add OLE objects to your 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 you need the equivalent of the OLE container control, you 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, your expertise with the environment becomes greater faster. 81. The Tag property is not supported. Addressed in Beta2; a Windows Forms extender 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 you can also use inheritance to extend the builtin controls and add your own properties. Having inheritance available as a tool makes the built-in controls significantly more flexible. Not only can you add as many properties as you like, but you can also make those properties strongly typed. 16 GDI+: Graphical Display Interface Plus. Page –335– Implementing it as a control helps speed the execution of code.0 – David Ross Goben 82.TextBox1.GetHeight() property of the control for which you wish to obtain the height. "Continue processing") 'associate a tooltip to btnCont button Another advantage to implementing tooltips this way is you can define multiple ToolTip controls so you can format each tooltip control to display their data differently. as in “Me. Yet. what we call a Font should actually be referred to as a TypeFace. that Twips should be dropped in . The TextHeight property is not supported. If you do not have much experience in pixels. 1440x900. as my 1920x1080 setting is (1920/96=20 inches. making my screen diagonal size actually 22. 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. • There are 72 points (typographical resolution) to a logical inch (or Imperial or Imperial Inch). or even 1920x1080. Thus.25 inches. as it was early on.ToolTip1. in particular on platforms requiring lightening-fast integer math. It is an intermediate resolution developed to easily translate between pixels and points using integer math.Enhancing Visual Basic . but are rather more familiar with Twips or Font Points (and perhaps more comfortable. • There are 1440 Twips to a logical inch. is usually referred to as a screen’s optimal or nominal resolution. I suppose 1440 TPI was chosen to allow for fractional points? The broader point is that this multiple-resolution fracas. 1280x960. It also translates very well during a VB6 upgrade to VB. 83. here is an easy breakdown between the various standards: • There are 96 pixels (display resolution) to a logical inch. the physical size of your monitor does not change. and therefore an additional translating unit of measurement. or by adding code for each control with something like this: Me. 288.9469 inches).btnCont. Page –336– . 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). adding a ToolTip control to the form allows you to add text to the ToolTip property on each required control. as my 23” wide-screen monitor is set to.NET Beyond the Scope of Visual Basic 6. In VB.NET. Use instead the more logical Font. if you application requires such things. On current computer displays. Apply control tool tips to only the tooltip control that suits how you want it displayed.NET. the line spacing includes the blank space between the lines along with the height of the character itself.SetToolTip(Me. as the term Font precisely refers to a specific size and style of type within a Type family). Under VB/NET. The line spacing is the vertical distance between the base lines of two consecutive lines of text. because up through VB6 you were almost forced to live with Twips). There are 15 Twips for each Pixel. and 20 Twips for each Point (and 567 twips to a centimenter). yet this is entirely impractical in many cases. and are now handled through a ToolTip control. this returns the line spacing in pixels (picture elements) of the font assigned to it (technically. which even then would not normally be precisely translatable between these two popular formats without resorting to fractions. but the number of displayed logical inches does. 288 times 5 equal 1440.NET platform to offer pixels as its primary support base. Twips (abbreviated from “twentieth of an inch point”) were adopted very early on in the life of Visual Basic. 1024x768. and not arbitrarily. was confusing graphical interpretation to no end. When you change these resolutions. The monitor setting that actually reflects 96 dots per physical inch. Having it built into each control ate time and resources.Font. which you might select as 800x600. 96 times 3 and 72 times 4 both equal their least common multiple. between them.GetHeight()”. rather than a hodgepodge of various resolutions that too frequently required translation. For this reason it was decided. meaning that there are 4 pixels for each 3 points (96/72 = 4/3). Tooltips are. A logical inch can be a physical inch. The ToolTipText property is not supported. the number of logical inches displayed is based upon the monitor resolution setting. which would be impossible if you did any kind of display resolution work in C++ and/or with the Windows Application Programming Interface. not to mention applying physical measurements to logical measurements. and 1080/96=11. NET. and cannot be set to 0. and ContextMenu objects.NET. which provided greater control over shared data. If it was set to 0. Old forms using vbPixels for Scalemode will not upgrade correctly.NET. MenuItem. DDE was later replaced (and absorbed) by COM.NET Beyond the Scope of Visual Basic 6. a MenuItem control. the chart in the document changed. Under VB. use a ContextMenu. which used a DOS Editor/IDE). The same Menu control instance could also be used simultaneously as a main menu or as a context menu. But even OLE is now clunky compared to current technology. under . represents each item in a menu tree. The use of top- level menus for context menus in VB6 was a by-product of the design implementation used to allow support of context menus in VB6.GetWidth() property of the control for which you wish to obtain the width for. because these objects can transfer and receive arrays of Menuitems. a Menu control was used to represent each item in a menu tree. As such. On top of that. the Interval property on a Timer control returned or set the number of milliseconds between calls to the Timer event. Adding Custom Menus to VB. The Enabled property indicates whether the timer is running.NET. you are not able to do so quite so easily with the tool-strip variations that you can create at designtime: MenuStrip and ContextMenuStrip. DDE was introduced in VB3 to enable running applications to share data.NET. 86. which.NET by using the CloneMenu() method of the needed MenuItem object to create an exact copy of it if you'd like to share that menu between a MainMenu object and a ContextMenu object. which could be a branch or a leaf object. See these solutions in the article. COM and its reference counting is not part of the new . and is working its way to the dustbin. which is not possible in OOP. 87. DDE is not supported. 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++. However. on page 136 in this document. a single menu unfortunately cannot be encapsulated by both at once. I agree that if you construct your menus using Menu. and as such. older than OLE. being much like a node of a Treeview control. The TextWidth property is not supported. you have to go through the task of reconstructing the menus.Enhancing Visual Basic . DDE made it possible to insert a spreadsheet chart into a document.Font. 85. older than COM. but was still supported for backward compatibility through VB6. Page –337– . DDE was an early attempt to enable the exchange of data between applications. Dynamic Data Exchange is supported through COM references. However. Otherwise. it was supplanted by COM’s OLE back in VB4. The Enabled property also determined whether the timer was running. This provides a more intuitive behavior to simplify coding with Timer objects.TextBox1. This was confusing for testing because even when Enabled was True. the timer would not be “enabled” if the interval was 0. because in OOPL terms. Setting a Timer control's Interval to 0 does not disable it. which must be done at run-time. 88. and. In VB. Under VB6. In VB. older than digital dirt. you can in fact take advantage of the Clone() method. Top-level menus may not be used as context menus. in terms of current technology. Addressed in VB2003. the Interval property indicates the time in milliseconds between timer ticks. not a Menu control. are two entirely different form controls. See the notes for #83. it disabled the Timer control. That may be. Although DDE was still used by some VB6 applications.NET. data safety would have been compromised.NET paradigm.GetWidth()”. This makes it archaic technology. as in “Me. most VB programmers did not ever know that DDE even existed before reading this. Under VB. why should they maintain DDE? Still. with the more convenient Strip versions. Under VB6. I have found easy solutions to these problems.0 – David Ross Goben 84. It has been reported that you can easily get around this little snag under VB. if Microsoft is now trying to dump OLE and is inching away from COM. For example. Use instead the more logical Font. The MenuItem control can be added to either a MainMenu object or to a ContextMenu object. When the spreadsheet changed. this returns the text width in pixels (picture elements). DrawEllipse(pen1.NET. New Rectangle(50.space pen1.Refresh() 'create a Graphics Object and attach it to the form (Me) 'Clear the form (Me) background using the specified color 'refresh the form (and propogate refresh to child controls) NOTE: Be sure that you do not do a “Me.Drawing. Line. 75. For Forms.FromLTRB(75. you 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. The VB6 Line control was a “lightweight control”. In VB6.space. see notes for #90 for creating a Graphics object) and calling its methods.5!. Circle is not supported.DrawLine(pen1.Dispose() 'dispose of allocated resources 90. as shown below: Dim Grf As System.0!.Color = Color.LimeGreen 'change pen color pen1. 0. and the Width or Height property to 1.Color = Color.Drawing namespace). 2) Grf.DrawEllipse(pen1.LightSkyBlue 'change pen color. but keep pixel width of 2 Grf. For example: Dim Grf As System.DrawEllipse(pen1.CreateGraphics Dim pen1 As New System.Width.DashStyle. 200)) 'draw ellipse using a rectangle declaration (Left/Top/Right/Bottom) pen1.0!.Refresh()” or “Me.Drawing.DashPattern = CustomDats 'assign the above custom pattern to the pen pen1. Rec) 'draw ellipse ' 'circle object example 4 Dim CustomDats() As Single = {1.Drawing. 1.Height) Pen1.BackColor) Me.Graphics = Me. However. You access the DrawEllipse method using Graphics and Pen objects.Drawing. See points #77 and #78 for new line controls. In VB. 0. 0.Graphics = Me.space. you can achieve the same effect by invoking the DrawLine() method from a Graphics object (available from the System. 50. 150. 1. 0.5!. In VB6. 0. For Console applications.DrawEllipse(pen1. you can use “Me. 95) 'declare rectangle at (Left.0!. the flexibility and power of GDI+ will allow developers to quickly design applications that that would have taken significantly more work in previous versions of Visual Basic. 200) 'draw a circle at x100. and 100-wide by 25-high (x.Dispose 'create a Graphics Object 'creat a pen drawing object 2 pixels wide 'draw a line top-left to bottom right 'dispose of allocated resources Page –338– . and had a confused parameter structure.5!} 'dash.NET. The Windows Forms package is built on top of GDI+ (Graphical Display Interface Plus). Rectangle. Me.CreateGraphics 'create a Graphics Object for the form ' 'circle object example 1 Dim pen1 As New System. Using “Grf. y100 that is 200-high/200-wide ' 'circle object example 2 pen1. Line is not supported. the Background color as needed.CreateGraphics Grf. you can draw vertical. 0. While the learning curve is a little steeper.Right. at best.FromLTRB(25. PSet.Custom 'tell the pen to use the custom pattern pen1. the Line control provided easier ways to draw lines on a form.0 – David Ross Goben 89. VB programmers have not been able to access these types of features in previous versions without having to resort to Declare statements and GDI P/Invokes. or diagonal lines in a form's Paint event handler by creating a new Graphics object (available in the System. 75.height) ' 'circle object example 3 pen1.dash.width.Width = 5 'change pen width in pixels Grf. and Point.Color = Color. 91. you can use “Console. 100.Long dash.Graphics = Me. For example: Dim Grf As System. more powerful form through the DrawEllipse() method from the System. 100.Enhancing Visual Basic .Clear” there is OK.5!. a feature-rich 2-D text and imaging graphics library that is now directly accessible from VB. 85.Pen(Color.NET Beyond the Scope of Visual Basic 6. Me. It is implemented in a different. You can also re-set the BackColor property to clear the palette by using the Clear method from a Graphics object (available from the System. The Line command carried over from DOS BASIC and QuickBasic. 2. Cls is not supported.space.Invalidate()” (though you should never do this within a Paint event). 25)) 'draw ellipse at x50/y50. In VB. there is no equivalent for the Line control because lightweight controls are no longer supported.Drawing. At design time.Blue.Clear”.Clear(Me.NET.DashStyle = Drawing2D. there are ways to draw lines on a form both at design time and at run time. Windows Forms has a new set of graphics commands that replace the old methods Circle.Drawing namespace (just set the width and height to the same value). the BorderStyle property to None. the Text property to blank.Bottom) specs Grf.y.Drawing namespace) in the Paint event for the container control. At run time. 200.Refresh” within a Paint event. you can draw a vertical or horizontal line on a form by adding a Label control and setting AutoSize to False.0!. in a pinch.Top. Cls.Red 'change pen color Dim Rec As Rectangle = Rectangle. 2) 'creat drawing pen with a width of 2 pixels (pen width defaults to 1 pixel) Grf. horizontal.Black.Pen(Color.dash. 100.Drawing namespace. 0 – David Ross Goben 92. 99.White) .txt" For Input As #1 This would be expressed in VB. However. For example. The AutoScaleBaseSize property automatically adjusts the scale according to the resolution (dpi) of the screen and font size you use. also features a form printer. PSet is not supported. Color.IO imported) as: FileOpen(1. Under VB. anyway. To upgrade VB6’s use of PrintForm.DrawEllipse(pen1.NET Windows Forms simplifies form layout by always making measurements in pixels. No longer needed. Print.Image. VB. the PrintForm method sent a bit-by-bit image of a Form object to the printer. The Printer is no longer a general object. Bitmap). 100. 96. because identifiers such as Open. Windows Forms has a better way to handle resizing. it can be emulated. AutoRedraw is not supported.Drawing namespace) to draw a circle with a height and width set to exactly 1 pixel.Refresh() 'cause the image display to update to show the change End With 93.Image. Print will not include a linefeed at the end of a line. 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. It is implemented in a different form. VB.Drawing namespace). or an object that is formatted as a Bitmap. even this printing feature did not work correctly on some forms. "MyFile. Their functionality is available through class libraries. using a platform-compatible form. 95.SetPixel(.NET has a printing framework that allows you to build complex print documents quickly. However. Bitmap).100) 'dispose of allocated resources Alternatively. The VB6 form was an old DOS BASIC cheat. Addressed in Beta2. Removing the file I/O statements from the language allows different I/O libraries to be used from VB.GetPixel(X1. In Windows Forms.Drawing. in the end.Width \ 2. you can use the SetPixel() method from the Bitmap object. This would be even more awkward if the file I/O statements were in the language. Also. 1) Pen1. Y1) 'Get the color at point X1. scaling really slows a program down a lot.NET. File I/O statements under VB6 were included in the language.NET. referenced in the notes for #77.Black. See the notes for #89 for additional details. such as the Image from a PictureBox.CreateGraphics Dim pen1 As New System. It can be duplicated by using the DrawEllipse() method from a Graphics object (available from the System. File I/O will not be compatible. OpenMode.PictureBox1.NET Beyond the Scope of Visual Basic 6.PictureBox1 'display a white point in the center of PictureBox1 DirectCast(. 98. 2) Grf. 97.Graphics = Me. It also includes a built-in Print Preview dialog box. Under VB6. at all. if you are using a Bitmap object (declared in the System. Point is not supported. Close. 1.Dispose 'create a Graphics Object 'creat drawing pen (default pen width is 1 pixel) 'drap a dot 1-high/1-wide at (100. PrintForm is not supported. Still.NET (with System. consider the following VB6 format command: Open "MyFile. File I/O operations are available through class libraries. See the next example for a more efficient example. Dim Grf As System. the free Visual Basic Power Packs. Use PrintLine. Scale is not supported. and Write would consequently be reserved words.Enhancing Visual Basic .Input) Page –339– .Drawing. The Name property for forms and controls is not exposed at runtime. Consider the following example: Dim Clr As Color = DirectCast(Me. Use the GetPixel() method from a Bitmap object (available from the System. In addition.Height \ 2. and must be rewritten.Y1 94. Old VB6 syntax was archaic. you can use this printing framework to quickly build a print document. . Consider the following example: With Me. a result of its non-structured origins. 100. the graphics in a PictureBox now automatically persist.txt".Pen(Color.Drawing namespace). NET reference to “Microsoft. Rich applications can be supported in a much more secure way by using VB. Web Forms support broad-reach applications through standard HTML. Clipboard I/O is also built right into many . Page –340– . However.VB6” at the top of your code file.NET System. 106.GetFormat(DataFormats. No more. It is a different object.MousePointer does not have a direct replacement.NET as Clipboard.SetText "hello". add “Imports Microsoft. plus a hefty host of additional powerful features. Screen.Clear Clipboard. add “Dim Printer As New PowerPacks. vbCFText If Clipboard.PowerPacks.Vs”. use Web Forms instead.NET Beyond the Scope of Visual Basic 6. The App object is not shared between managed (. vbCFText If Clipboard.Windows.NET 'supported in VB. This had been addressed.NET) and unmanaged (ActiveX) code.Text) 'Previous (old) VB. any code that referenced the Clipboard object was not upgraded and had to be rewritten. 102.VB6. Web Forms have no such restrictions. with the introduction of the “My” object.NET.NET as Clipboard.Clipboard object.Text) 'supported in VB. Use Web Forms.NET code (VB2002 through vb2003 allowed only these formats) 'VB6 code comparison 'Clipboard.Clear Clipboard.Windows. 104.GetText(vbCFText) End If 'VB2005+ notes 'supported in VB. An IIS application also requires the presence on the server of the Microsoft runtime DLL. where the only upgrading simply involves constant values.GetDataPresent(DataFormats. A Webclass Designer application. DHTML applications contained DHTML pages and client-side ActiveX DLLs. 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).NET’s Windows Forms controls hosted in a browser. Webclasses are not supported. As of VB2005. have returned through the new My. Under VB. also known as a VB IIS (Internet Information Server) application. DHTML applications.Text = CStr(Clipboard. and other files that were used to create a project and any supporting files that Web pages require.Text = Clipboard. DataFormats.GetText(vbCFText) End If 'End If The Err object is not shared between managed (. altogether.NET. This code runs inside of a secure sandbox. such as text boxes. and run from the server. the CLR does expose support for unmanaged code.VisualBasic. The following example shows how to modify code that used the VB6 Clipboard object to the alternate VB. DHTML projects are not supported.SetDataObject(New DataObject(DataFormats.Clipboard format: 101.Compatibility.0 – David Ross Goben Printer object methods are not automatically upgraded and must be rewritten. However.Forms. Under VB6. Webclasses were dropped because they would not always work. and Web Forms. For VB.GetData(DataFormats. and during a VB6 to VB.Text)) ' TextBox1.Text = Clipboard.DLL. and finally. It is a different object. all the old VB6 clipboard functions. you can still navigate between ActiveX documents. or with a downloaded “safe Windows Form” EXE. Previously.Printer”. "hello")) 'Clipboard.Printing.Text. add a .NET as Clipboard.NET controls.GetDataObject. 100.Enhancing Visual Basic . The Printer is no longer a general object and can be easily emulated.PowerPacks. Clipboard object methods are not automatically upgraded and must be rewritten.Clipboard) had no direct equivalents to the VB6 clipboard. Also. but that solution no longer works with later version of VB. It now upgrades with perfect ease.Text) 'supported in VB.Forms. so that it cannot do harm to a client computer. VB6 DHTML projects are a different paradigm. 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.SetText "hello".Text) Then 'If Clipboard. like HTML. While ActiveX documents and DHTML applications cannot be directly upgraded.GetFormat(vbCFText) Then TextBox1.NET) and unmanaged (ActiveX) code. 103. With it installed.GetFormat(vbCFText) Then TextBox1. 105.GetText(DataFormats.Printing.NET upgrade.Computer.GetDataObject.VisualBasic.Compatibility. the VB. altogether. is an in-process COM component (DLL) that runs with IIS as its client. where you need to access the VB6-style printer object. Also.NET Clipboard object (the one supported by System. see “Back Book Tip # 32: Dealing with the Form Cursor not displaying over Form Controls” on page 455 for a working solution. MSWCRUN.SetText("hello". the CLR does not support unmanaged code. ' VB6 code with vb2005+ notes Clipboard. Debug. or a Word document by launching the appropriate server application. you could use the Print command. Word allows us to work with several documents at once. through VB2005. While ActiveX Documents and DHTML applications cannot be directly upgraded. Run->Break->Edit->Continue development is not supported. DHTML applications. 115. ADO. it is a brand new IDE. and menu negotiation. VB2008 reintroduced the Debug. SDI applications are the applications which allow us to work with a single document at once. which is sent to the new Immediate Window. Debug. DAO is not upgraded because its technology. whose output went to the Output page. just as it had been done under VB6. Also. Under VB6. and Web Forms. Visual Web Developer. or with a web browser on a form. 109. Thus. yes. The . Page –341– . and is still fully functional in VB. C#. hyperlinks.0. Under VB. ADO. MDI applications are applications in which we can view and work with several documents at once. That is. plus any CLS-compliant language you wish to add into Visual Studio.NET data binding.NET. ActiveX Document projects are not supported. through VB2005.Asset method. 110. IE is able to view a Web page.NET user controls are also MUCH easier to develop and use. A cost to VB6. an SDI (Single Document Interface) applicant in VB.NET UserControl classes are similar to the differences between the VB6 and VB. Addressed in Vb2008. DAO data binding cannot be upgraded and will add errors to the upgrade report. and F#. except for the free language-locked versions of Visual Studio Express. As of Version 3. Previous to that. The Immediate window will not work in Design mode. The VB6 IDE was designed to operate specifically with VB. 113. Previous to that. ActiveX Documents were extensions to Microsoft's OLE compound document architecture that allowed a container application to use the full capabilities of server applications.Assert is not supported. VB6 ADO data binding is upgraded to the new ADO. just as it had been done under VB6. you could use the Asset command. As such.NET. Under VB6. VB. you can still navigate between ActiveX Documents. 108.Enhancing Visual Basic . Microsoft Office applications and the HTML viewer used to render Web pages are ActiveX Document "servers". Of course not.NET can be made into an MDI (Multiple Document Interface) easily. The IDE Extensibility Model is not backwardly compatible.0 – David Ross Goben UserControl projects are not supported.NET Form classes. 114. whose output went to the Output page. An example of an MDI application is Microsoft WordPad or Notepad. ActiveX Documents could appear within Internet browser windows and offered built-in viewport scrolling. VB2008 reintroduced the Debug. 112. SDI will not be an option in the IDE -. Addressed in VB 2005. Formerly known as "Document Objects" (DocObjects).NET brought it into compatibility with other platforms.NET Beyond the Scope of Visual Basic 6. An example of an MDI application is Microsoft Word or Excel. 111.NET. DAO is a COM (Common Object Model) technology. Many of the differences among the VB6 and VB.NET IDE is language-neutral and will operate with all Visual Studio languages: C++. Internet Explorer (IE) was made an ActiveX Documents "container". which is sent to the new Immediate Window. Data binding with DAO is not supported. However.Print is not supported. but the changes to .NET as code through a COM interface.NET offers read/write data binding to controls for Windows Forms and read-only data binding for Web Forms. 107. an Excel spreadsheet. you can replace them with user controls containing web browsers and scrolllers. Controls on Visual Basic forms can be bound to DAO data sources. This was true only during the initial trials.MDI or nothing. but not after that. In contrast. .Print method. ADO. ADO.NET data binding. As such. was being passed up in favor of the more powerful and capable ADO technology. Indeed. Page –342– . Under VB6. VB6 ADO data binding is upgraded to the new ADO. when VB6 was still actively supported. and even later the ADO technologies. even under VB6.Enhancing Visual Basic . As such. Data binding with RDO is not supported.NET as code through a COM interface.NET provides additional classes for disconnected data access.NET Beyond the Scope of Visual Basic 6. which was also dropped. Controls on Visual Basic forms can be bound to RDO data sources. Indeed. RDO data binding cannot be upgraded and will add errors to the upgrade report. Under VB. technical support was dropped for RDO. See notes in #115. However. They also allow simpler integration of XML data with your database data. 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. when VB6 was still actively supported. which is even older and more primitive technology than DAO. These classes provide performance and scalability improvements over previous versions of ActiveX® Data Objects (ADO) when used in distributed applications. and is still fully functional in VB.0 – David Ross Goben even under VB6. RDO is a COM (Common Object Model) technology. 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? 116.NET offers read/write data binding to controls for Windows Forms and read-only data binding for Web Forms. They are not upgraded because its technology. was being passed up in favor of the more powerful and capable DAO.NET. yet it was maintained as legacy code because the initial release of VB6 supported it. yet it was maintained as legacy code because the initial release of VB6 supported it. technical support was dropped for DAO. Such tips include adding a file to the Recent Folder. ascending or descending sorts on any column in a ListView control. but for some reason it had been locked away (See Black Book Tip # 30). easily cleaning up image backgrounds on menus. In Black Book Tip # 9 on page 365. A tip recently added shows you how enable Full-Text-Justification in a RichTextBox control. I was originally going to release this entire document as “My VB6 to VB. and this term. Since I first put this treatise to e-book form in September of 2009 as a 23-page personal collection of notes. For all the fuss people have made about it over the years on so-called expert forums. or they just do not have enough experience in a particular arena of knowledge (I have been there plenty of times).NET Beyond the Scope of Visual Basic 6. how to associate unique file extensions to your application. to invent sometimes very unique. However. how to get instances of your app.NET Beyond the Scope of Visual Basic 6. it had gone through a steady evolution all the way through early 2012. locking out or hiding tab control pages. every one of them had got it wrong! They kept forgetting to perform the one simple step that would have made their solutions work: the one puny line of code required to enable the Advanced Typography Options in a Rich Text control. have tended to over-use it to the point that it does not have the same powerful meaning that it once had. Indeed. because for all those online gurus and forum monitors expounding about it and boasting “this is how you do it”. and context menus on the fly. After that.0”. dithering a form’s background. toolbars. I show you how to display justified text in a TextBox. it has sat relatively idle. though it seems that there are still a great many users out there who are in desperate need of finding a solution to such problems. getting a minor tweak now and then as I found better solutions than the ones I previously shared. TreeViews. and some tell me quite patentable solutions to problems. how to display TextBox data as justified text (great for About Boxes). sizing a TextBox to fully contain and expose provided text. and sometimes even brain-dead simple.Enhancing Visual Basic .NET Compared to Visual Basic 6. So. how to get the file path from a link file. or how to create shortcut files. but because numerous other black books have already been released. easily recovering crashed menus. I chose to give the document an alternative. and finally publishing it online in July of 2010 with 140 pages. reading through Microsoft’s online documentation on this control just once clearly tells anyone bothering to read it exactly how to enable it. many other tinker toys I keep within my personal bag of tricks. I decided that instead of concealing many of those solutions. but. unless perhaps forced to at gun-point. and in Black Book Tip # 46 on page 500. plus many. just before we need to use them). a features that has actually been built right into these controls ever since Windows XP was released. It seems few of these gurus actually read documentation. first as “Visual Basic . making printing brain-dead simple. I would share them in an additional section. but even in your MsgBox dialogs. But justification does not end there. but there were no major updates.NET Black Book”. easily redimensioning leftward dimensions in multi-dimensional arrays. bypassing the default TextBox context menus.0”. Interestingly. easily tracking ComboBox items under the mouse pointer. once marketing departments got their fists wrapped around it. Some of what I share here are sometimes quite mundane. whether it is because they cannot see the forest due to the trees because they are so close to the problem. being that I am knee-deep in a mega project that has required me. But this really confuses me. and finally as “Enhancing Visual Basic . preferred solutions to typically complicated or hard to remember things. even from a RichTextBox and with full support. I show you a very simple method of displaying justified text not just in a TextBox or Label. or placing a link to the application in their SendTo folder. how to do very easy owner drawing on TabPages. what follows is a sampling of the additional tricks beyond what I have already drawn up for you… Page –343– . and other controls. it is actually almost brain-dead simple to enable. almost on a daily basis. here. though meaningful title. A Black Book is typically one’s Go-To Guide that stores our personal. being a ready reference so that we do not have to cram so much into our brains (or things we inevitably forget.0 – David Ross Goben Additional "Black Book" Tips This entire document was originally my personal “Black Book”. usually. ...................... and Dialog Boxes................................................................................................................. 471 Black Book Tip # 37: Comparing Color Values ...... KB.................................................................................................................. MB.... 381 Black Book Tip # 17: Passing a Parameter ByVal when the Invoking Function Specifies a ByRef Parameter ................................................................412 Black Book Tip # 25: Setting Context Menu Icon Image Transparency at Runtime ................... 355 Black Book Tip # 6: Sorting Any Column in a ListView Control in Ascending or Descending Order ............. 390 Black Book Tip # 20: Tracking ComboBox Items under the Mouse ............... 446 Black Book Tip # 32: Dealing with the Form Cursor not displaying over Form Controls.......... 356 Black Book Tip # 7: Sizing a Label or TextBox to Fully Contain a String for Display.. 385 Black Book Tip # 19: Easily Recovering Crashed Menus and Toolbars under VB...................................................................................... 455 Black Book Tip # 33: Passing Scalar Values and Strings to and from a Byte Array ............................... 550 Black Book Tip # 53: Determining If a Computer Has an Internet Connection................ 351 Black Book Tip # 4: Create a Shortcut File within Your Code... 500 Black Book Tip # 47: Owner-Drawn Controls Made Easy .....................................NET ...................................................................... 509 Black Book Tip # 49: Embedding Images within Your Source Code...........NET . Creating.................. 469 Black Book Tip # 36: Opening an Associated Application without a File........................... Labels.................................................................... 360 Black Book Tip # 8: Set a New SelectedIndex for a ListBox or a ComboBox without Firing its Click Event .................................... 599 Page –344– ............ 589 Black Book Tip # 56: Getting................. Enumerating.....Enhancing Visual Basic ................... or TB Strings ............ 365 Black Book Tip # 10: Get The ListIndex of a ListBox Row that is Under the Mouse Cursor ............................................................................................................................... 477 Black Book Tip # 39: Detecting Transparency Colors ........ 591 Black Book Tip # 57: Printing Plain Text and Formatted Rich Text with Ease........................... 581 Black Book Tip # 54: Manipulating Color Value Members with Ease...................................................... and Changing Screen Settings ................................................. 483 Black Book Tip # 42: Taking Advantage of VB........................................................................................ 442 Black Book Tip # 31: Easily Replace Power Pack Shape Controls with Faster Paint() Event code ..............................................GetValues() to Parse Enumeration Definitions....................................424 Black Book Tip # 28: Implementing a C-Style Bit Field Class in VB........................... 372 Black Book Tip # 12: Determining if an array is Dimensioned ............439 Black Book Tip # 30: Enable Built-In Justify-Alignment in a RichTextBox from VB.. 373 Black Book Tip # 13: Customizing the Display of TabControl Tabs.......................... 457 Black Book Tip # 34: Display Buttons Labels wider than the button normally allows.. 371 Black Book Tip # 11: Open File Explorer with a Target File Selected......................................... 476 Black Book Tip # 38: Use [Enum]......... and Embedding ImageStrips with Ease ..... 506 Black Book Tip # 48: Owner-Drawn TreeViews Made Easy......... 478 Black Book Tip # 40: Greatly Accelerate TreeView Reflection of Directory Trees.......................................................... 353 Black Book Tip # 5: Adding a File to the Recent Documents Folder........................................................................................................ 488 Black Book Tip # 43: Taking Advantage of the DateTimePicker Control ................... 348 Black Book Tip # 3: Get the Linked File Path from a Shortcut File ................ 467 Black Book Tip # 35: Performing Selections or inserting RichText Data Off-screen without Scrolling ............. and Extending Application Events ...... 499 Black Book Tip # 46: Quick and Easy Text-Justification for Text Boxes... GB...................................... 493 Black Book Tip # 44: Padding Strings and Filling Separator Lines in Proportionally-spaced Text .........................NET . 417 Black Book Tip # 26: Adding Background Transparency to Images on Toolstrips and Menus...............NET Drag and Drop Features ......... 513 Black Book Tip # 50: Replace the BrowseFolderDialog Control with a Custom BrowserDialog Form ......................................................... 544 Black Book Tip # 52: Extracting Icon Images from Files and Displaying Them in a Directory TreeView....... 496 Black Book Tip # 45: Making Sense of Form Default Buttons and Their DialogResult Properties ...................................................... 399 Black Book Tip # 22: Easily Sorting Strongly-Typed Generic Collections Lists........................... 527 Black Book Tip # 51: Using........................ 380 Black Book Tip # 16: Hiding Tab Pages without Destroying the Tab Pages or their Resources...................................................................................................................................................................................................................... 379 Black Book Tip # 15: Prevent the User from Selecting a TabControl Tab. 479 Black Book Tip # 41: Sorting TreeView Directory Trees in an Orderly Fashion .......... 405 Black Book Tip # 23: Dithering a Form's Background ........................................................................................ 586 Black Book Tip # 55: Converting Byte Sizes to Formatted Byte..............................409 Black Book Tip # 24: Designing Intelligent Context Menus for TextBox and RichTextBox Controls ........................................ 384 Black Book Tip # 18: Show and Hide Additional Information at the Bottom of the Form .................................428 Black Book Tip # 29: Taking Advantage of...................... 374 Black Book Tip # 14: Detecting a TabControl Tab on a Right MouseDown Event ...........421 Black Book Tip # 27: Easily Do a Redim Preserve with Leftward Bounds of Multi-dimensioned Arrays...... 396 Black Book Tip # 21: Greatly Simplifying P/Invoke Definitions of POINTAPI and RECT Structures ........................................................................................... 346 Black Book Tip # 2: Creating an Association between a File Extension and Your Application .........................0 – David Ross Goben Black Book Table of Contents Black Book Tip # 1: Bypassing Click Events when a Context Menus is Not Displayed on a Right-Click......NET Beyond the Scope of Visual Basic 6............................ 363 Black Book Tip # 9: Display TextBox Text Format-Justified at Runtime ... .........NET Beyond the Scope of Visual Basic 6...0 – David Ross Goben Black Book Tip # 58: Creating Fancier ProgressBar Marquees and Enhanced ProgressBars ........... 643 Black Book Tip # 61: Allowing a Form’s BackColor Property to Accept Transparency Colors............. 624 Black Book Tip # 59: Adding a Horizontal Scrollbar to a ComboBox DropDown List ..........Enhancing Visual Basic ................ 640 Black Book Tip # 60: Restoring 3D Curved-Surface Appearances for Buttons and Tabs............................................................ 645 Page –345– ........ . For instance. or MouseUp events in order to perform other tasks when the user is not trying to bring up the Context Menu. MouseClick. but this is useless because the Click event only fires after a mouse button is released. and all they end up doing is getting frustrated and bald. this works fine in that you simply return from the MouseDown event without throwing up the Context Menu. then the pending mouse button-state messages will not be consumed and handled by the Context Menu mechanism as it normally would be. Some have resorted to the GetKeyState() P/Invoke to check the state of Keys.Enhancing Visual Basic . but if it is set. because.RButton. if the Context Menu is not displayed. Thus. And. suppose you also need to monitor the control’s Click. MouseClick. which are both handled before the MouseUp event. The MouseDown. As a case in point. 'and then leave Page –346– . MouseClick. Until such a time. but unlike Button controls. so leave (let MouseUp finish up and actually reset the pSuppressMousePress flag) Finally. then simply return to the invoker: If pSupressMousePress Then Return End If 'skip this click event because we right-clicked but no context menu was displayed? 'yes. like Forms. The MouseClick is normally a good alternative to using a Click event when we need to determine which button was used. This way we could stop further processing of the mouse button from the MouseDown event so that the Click. if my application monitors Click. in a required MouseUp event. MouseClick. MouseClick. left or right. will fire a Click event on both left. such as Private pSupressMousePress As Boolean = False. these messages will find their way to them. Return Some developers have done lots of hair-pulling in order to surmount this issue.. it funnels on through any existing Click. all these ideas are useless because our goal here is to ignore these messages until after MouseUp is triggered. and set it to True when my application determines that it will not be displaying its usual Context Menu during the MouseDown event. this can happen in a spreadsheet application where you may need to click on a mapped grid cell to deselect a previously selected dynamic range of cells if the clicked location was not also within the previously selected dynamic range. we are still clicking on the PictureBox. even if this comprises its only code. but they differ enough that this is not always applicable. or MouseUp events. pressing the ENTER key will also fire a Click event but it will not also fire a MouseClick event. NOTE: PictureBox controls. Some developers try to check for the right mouse button being used during these events.and right-clicks. However. MouseClick and MouseUp events would never be issued a message in the system message queue (this was also the suggestion I submitted to Microsoft). leaving via a instruction if it is detected.NET Beyond the Scope of Visual Basic 6. or MouseUp events. though it does not do so for a general Click event. 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. Truth is. then at the start of any Click or MouseClick events. check the pSupressMousePress flag.0 – David Ross Goben Black Book Tip # 1 Bypassing Click Events when a Context Menus is Not Displayed on a Right-Click Suppose you want to display a Context Menu when you right-click the Mouse Cursor on a PictureBox control. such as executing a process when the user simply does a normal click on the control. as long as we support events such as Click. likewise check the flag. and MouseUp events do in fact expose the needed MouseEventArgs parameter to test for this. and if it is set. If a right-click on this PictureBox is determined to not need a Context Menu. except when certain conditions exist that will not require that Context Menu. then reset the flag and return without doing anything else: If pSupressMousePress Then pSupressMousePress = False Return End If 'skip this event because we came from a right-click with no context menu? 'yes. and MouseUp in our code for that control. my current solution is to declare a local flag. perhaps to hide exposed auxiliary features or pop up a properties dialog. Normally. Instead. so first reset the flag. because the program is simply doing exactly what it was designed to do. Plus many more features.NET and C# is similar to the VB6 API Viewer. The built-in Declaration Editor makes such changes a breeze with just a few clicks of the mouse.DLL" (ByVal nVirtKey As Int32) As Int16): this P/Invoke is also really useful when you are monitoring a Scroll control’s Scroll event. Page –347– . It simply assumes that you know what you are doing.asp?txtCodeId=8293&lngWId=10&txtForceRefresh=213201411542115354). though Dot NET developers using P/Invokes often employ them as well. This allows the user to keep program speed optimal by not slowing down for object (variant) conversions. but numeric values can be declared as hexadecimal. NOTE: I wrote DotNET API Viewer.planet-sourcein source code form only. Constants are created as allcapitals.NET Beyond the Scope of Visual Basic 6. The stand-alone DotNET API Viewer application. you can completely eliminate the click-dribbling headaches that typically come with such feature-rich applications that support conditional context menus.NET. all of which do not fire until the mouse button is actually released. available for free from Planet Source code (http://www. press the left mouse button down on it. You can also apply + or . in case you were not aware of it. No complex checks are performed on the value. The New API Viewer makes farming these additional types a breeze with a few quick clicks of the mouse.offsets. It automatic checks for new parameter dependencies. which are Int32 constants mapped to the keyboard and mouse. BONUS TIP: Regarding the mentioned GetKeyState() P/Invoke (Private Declare Function GetKeyState Lib "user32. we can actually ignore further processing of our Scroll event code and just return to our invoker. which provides P/Invoke support for both VB. Using the GetKeyState() P/Invoke. written in VB. You can Delete entries from the API library list.KeyCode property of the KeyEventArgs employs the Keys enumerated list of Virtual Keys. If an added declaration or structure requires another structure or constant not included in the selection list. If so.NET applications a breeze. code. and finally MouseUp. You can edit Declared Subroutine and Function parameter lists.com/vb/scripts/ShowCode. Additions created within the New API Viewer can be optionally saved for later re-used in the API library file. because such checks can involve complex offsets and naming of other constants. You can create new Constants right within the viewer. Assigned values are expected to be numeric or numeric constants. the .and time-stamp marker. but on steroids. Also. New entries are appended to the API file with a date. These additional structures in turn require the ACL and LUID_AND_ATTRIBUTES structures and the ANYSIZE_ARRAY constant. as was required by the VB6 API interface.Enhancing Visual Basic . You can create new API method Declarations right within the viewer and add them to your API library list. exactly mirroring the Virtual Key Codes defined for use by Visual Studio C++ and VB6 development (usually via the VB6 API Viewer Add-In). PRIVILAGE_SET. 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 you have to do in that code.NET and C#. and SECURITY_DESCRIPTOR structures. you can view the requested types in a dialog and select them or reject them for inclusion in your selection list. which requires the additional inclusion of the GENERIC_MAPPING. Note that event processing goes though MouseDown. Click. makes pasting new API declarations into your VB and C# . and Click events. MouseClick. Although constants can be of types other than Integer. and apply these changes to new subroutine or function names. and when you release the left mouse button over it. These newer structures in turn also require the LUID structure. The DotNET API Viewer for VB.0 – David Ross Goben Using this simple technique. The Scroll event fires whenever you click on the Scroll control. You can create Overloaded Declarations of methods right within the Viewer and add them to your API library list. The viewer will also check to ensure that the newly entered constant does not already exist.NET interop API interface uses only Integer values for its constants. All you really have to do is type “keys. we can quickly check the key state of the left mouse button (Keys.           You can dynamically declare constants and Enumerators as Integer in the selected local copy of a constant or enumerator.” to see a list of all these enumerated Virtual Keys (see MSDN for additional details on virtual keys). or binary. This positive state will be the case during a MouseUp. octal. or set and option to automatically include them. You can create new Structures and Enumerations right within the viewer and add them to your API list. not realizing they can use the Keys enumeration instead of a declared constant.LButton) at the top of our event code and see if it is greater than or equal to zero. the e. MouseClick. This can make resolving declaration headaches such as with the complicated AccessCheck Function. thus totally eliminating the jerkiness and redundant invocations. in that order. which indicates that it is not pressed. These are file extensions known to the operating system. It has a sub-folder named open. the user is instructed to right-click the uniquely-extended file if the Open With dialog does not come up. when I define such entries. If we examine this referenced entry. Their default entry contains the registry entry that files of that extension are to be associated with. suppose we need to associate the file extension “xyz” to our application. or at its reflection in HKEY_CURRENT_USER\Software\Classes.Enhancing Visual Basic . The name for a registry entry that will link the extension to the executable file. such as Notepad. This is the only path we need to be interested in right now. text files with the extension “txt” are usually associated with a text editor. We need not concern ourselves with any other sub-keys or sub-folders defined within any given extension. The full path to our executable that the files with the target extension are to be associated with. you will see that the default entry for the shell\open\command path is the Notepad executable with at least one parameter list placeholder. We have sole interest in its Default registry entry. to the right. and then select or browse to the appropriate application so that afterward that application will be associated with that type of file. unless you have changed that association. but it is also entirely unnecessary within your own applications. To do this. Here. in this case txtfile. The executable file. if we are running this code from the application startup (it is OK to run it each time you run your application. in case the executable location is changed by you or your user). For example. We can grab the full path to our application’s executable with the simple command. such as within the main form’s Load() event. Page –348– . such as “xyzfile” for the “. the executable program that they are associated with (one that utilizes the file’s data format) is launched and it will load or run that file. If you are one to explore the Registry using RegEdit or some other Registry viewer. and it in turn has a sub-folder named command. then you are prompted to select or browse for a compatible application that you want to process that file with.ExecutablePath”.NET the process of creating such registry entries is almost brain-dead simple. we need only three pieces of information: 1. and is also a bit amateurish if you expect the user to perform this task in order to use the files your application creates. select the Open With… context menu option. and your end-user needs to do nothing except take advantage of the fact that you have created this association for them.NET Beyond the Scope of Visual Basic 6. The lower-case file extension that we want to have associated with our executable. then if you look in the HKEY_CLASSES_ROOT hive. you will notice a subfolder named shell. “Application. as you might see here. Unlike VB6. By default. This is all well and good. you will see a lot of entries that are typically 3 text characters preceded by a period (a dot). 3. I wrap the executable and each individual optional parameter extension within quotation marks because any one of them could potentially contain spaces that can sometimes cause confusion about the delineation of a file if they were not present). Applications that can use multiple files usually have a list of numbered parameters (for safety. This is because you can easily force this association between your file extension and the desired executable with just a few lines of simple code. In many cases I have seen on the web. For example. you can define the linking entry by simply using the text of the extension and tacking “file” onto its end. all neatly wrapped inside a function. under VB.xyz” extension. 2. If the file is not associated with any particular executable.0 – David Ross Goben Black Book Tip # 2 Creating an Association between a File Extension and Your Application When you select most files in File Explorer. Win32.CreateSubKey( _ "Software\Classes\xyzfile\shell\open\command"). 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).xyz” entry that will in turn reference our yet-to-be-created “xyzfile” entry.ExecutablePath.Computer. it is the text of the ' Extension plus "file". For example.CreateSubKey( _ "Software\Classes\.ExecutablePath & """ ""%l""" Of course. below. if Extension is set to "txt". 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. You can supply the file extension with or without a leading Dot.RegistryValueKind. 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. Further. The first half creates the Sub-Key “.Enhancing Visual Basic . if we are using a file extension of “.Registry.ExecutablePath & """ ""%l"" ""%2"" 'create the application reference and also the registry shell command to open My.CurrentUser.String) And that is all there is to creating a file extension association with your application.xyz").String) Notice that we are not providing a registry key name as our first parameter. and xyzfile will in its turn reference our target executable. to define the linking entry within the registry. of the executable that is to be invoked when a file with the ' provided extension is selected. then AppReference would become "txtfile". By default. For it.xyz” and linking it to “xyzfile”. The operating system simply ensures that what is being set is safe). we would construct the following command line: 'Add a registry entry that will notify the operating system of the file extenion we are going to associate My. ' AppReference: Optional parameter to specify the File Association within the registry.xyz” (notice also that we cheated by defining it within the HKEY_CURRENT_USER hive.xyz” (note the lowercase) that will reference “xyzfile” (also note the lowercase). The following will define a function named SetFileAssociation() that expects two parameters with an optional third. _ ByVal Extension As String. For example. 2 double-quotes defines a single double quote) ""%3"" ""%4"" ""%5"" ""%6"" ""%7"" ""%8"" ""%9""" Extension-type files using the Exe Microsoft.Computer. Microsoft.Registry.SetValue”) is setting the actual default value for the now-created “.SetValue(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 Page –349– .0 – David Ross Goben We need execute only a couple lines of code to create all of the required registry entries.CurrentUser. Notice. 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 Dim CmdPath As String = """" & Application. "XYZ") will create a registry entry named “. Hence. The second line of code does basically the same thing. SetFileAssociation(Application. "xyzfile". including the full name. but rather we give it a value of Nothing. CmdPath. ' Extension: The file extensions. notice that we actually perform two separate tasks at the very same time. but it creates the full “xyzfile\shell\open\command” registry path and also sets the default value within the command folder to our executable. we may want to use this in other applications that we may have. such as "txt" or "myExt".NET Beyond the Scope of Visual Basic 6. ' ' Return Values: 0 = Success ' -1 = No AppExePath priovided ' -2 = AppExePath is invalid ' -3 = No extension provided '******************************************************************************* Friend Function SetFileAssociation(ByVal AppExePath As String. _ Optional ByVal AppReference As String = Nothing. that should be associated with AppExePath. ' NumParams: Number of file parameters to add (1-9). NOTE: You will only want to add the number of parameters that your executable can handle.RegistryValueKind. use: """" & Application. if it can process only one submitted file. so we would probably want to define a method within a reusable Module file. which actually sets the Classes data within HKEY_CLASSES_ROOT and simply reflects the data back to this location. and the intermediate registry entry defaults to the text of the provided file extension plus “file”. complete with 9 optional parameters: '******************************************************************************* ' Method Name : SetFileAssociation ' Purpose : Associate QWS file extension with Qabala Qube ' Parameters : ' :AppExePath: The full filepath. all without having to set a ton of security features if we had wanted to access HKEY_CLASSES_ROOT directly.SetValue(Nothing. The second part (starting with “.Win32. The first line will create the file extension entry. Computer. because that file extension might also be associated with another application. " ") <> 0 Then 'does the executable path contain any spaces? AppExePath = """" & AppExePath & """" 'embrace application path with qutation marks if it contains spaces End If Try 'Add a registry entry that will notify the operating system of the file extenion we are going to associate My.CurrentUser. AppReference. Microsoft.File. In that case. if they say NO.ToString & """" Next 'Now create the application reference and also create the registry shell command to open Extension-type files using the AppExePath My.0 – David Ross Goben If Len(AppExePath) = 0 Then Return -1 End If If Not IO.SetValue(Nothing. to let them decide if they want to associate the files with your application....CurrentUser. 'indicate that this was so and do no more 'if the desired file extension was not provided. you should leave the door open to them under your File or Option menu for them later to bind the association if they change their minds.Substring(0.CreateSubKey( "Software\Classes\" & Extension)." Then Extension = ".Registry. Page –350– ... 1) <> ".RegistryValueKind.Computer. CmdPath.String) Return 0 'return success Catch Return -4 'return failure to create the registry entries End Try End Function NOTE: You should actually make associating your file types an option to your user. 'indicate that this was so and do no more If AppReference Is Nothing Then AppReference = Extension.NET Beyond the Scope of Visual Basic 6.SetValue(Nothing. If InStr(AppExePath. 'create our own (also lower-case) 'if the selected executable cannot be found.CreateSubKey( "Software\Classes\" & AppReference & "\shell\open\command").String) 'Nothing references the Default '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. Microsoft.Exists(AppExePath) Then Return -2 End If If Len(Extension) = 0 Then Return -3 End If Extension = LCase(Extension) If Extension." & Extension End If 'if the Executable was not provided. 'indicate that this was so and do no more 'ensure we are lower case! 'ensure it has a leading Dot...Registry..Win32.RegistryValueKind.Win32.Enhancing Visual Basic .Substring(1) & "file" End If 'if the registry application reference was not provided. we knew we had found the drive path to our link-to file. click on the entry to select it and place a check in its checkbox if it has one. But to reference Shell32. For example. Technically. and Path. With the above knowledge. the registry.lnk” is through a DOS shell or through directory parsing). exposing read/write properties for its icon information. one common cheat (which. Go to your Project Properties. Of all these properties. and then parsed forward to find a Null at the end of the path.dll. and finally click the OK button to add it. We first need to set a COM reference to Shell32.DLL. what does it mean: use the applicable interface instead? However. and is the operating system’s primary workhorse. select the COM tab on the Add Reference dialog. Back in the days of VB6. more direct. after adding a Shell32 reference to your application. You also have to set a special parameter to prevent the Interop Types defined for Shell32 in our application’s assembly file from being embedded within our application. Once we encountered this. what this does mean is that you should only use these objects as delivered by the operating system instead of trying to embed these core operating system Interop Types. and maybe the kitchen sink. Use the applicable interface instead. select the References tab. we can still use. is simply another ShellLinkObject reference used for re-direction). Specifically. we are interested only in Path. even though it is so brilliantly crude) was to parse backward through the Link file and searching for the character sequence “:\”. not automatic GUIDs generated for embedded objects. Once we had that. as otherwise you will not be able to assign its all-important ShellClass object to anything. This last property is the path that the shortcut links to (another property. Hotkey. Page –351– . and much more robust method is to implement the COM-based Shell32. maybe back then it was just called Shell. as it is in your references.Shell = New Shell32. memory. However.DLL. it will have an issue with Shell32. you are not alone. though it may well state the problem (that ShellClass cannot be embedded within the application). if you were to enter the following line into your code (as we will need to. A ShellLinkObject is a class that simply describes a shortcut file. It is a really crappy error message that. This step is important.DLL in our application. A Shortcut file.shell32. in fact. which is a DLL that is as old as Microsoft Windows itself (well….ShellClass 'define our shell class You will discover that the Visual Studio cross-language compiler will flag this line to be in error.lnk” file extension (so hidden. that the only way we are usually able to actually see that its extension is in fact “. It will also recreate it during any build if you ever delete it. technically. just as now Shell64. Description.DLL is not enough. We then parsed backward until we encounter a Null to pick up either the drive letter or the network root drive name. which covers just about anything you can imagine regarding files.” If you are left scratching your head.DLL is the new normal on 64-bit systems). a much cleaner. the process of extracting the Path property from the shortcut is relatively simple.ShellClass by underscoring it with a red tag and tell you in the Error List panel that “Interop type 'ShellClass' cannot be embedded. hit the M key to skip down to the M’s in the list. shortly): Dim Shell As Shell32. WorkingDirectory. But what is important to us at present is its ShellLinkObject class. and scroll to the “Microsoft Shell Control and Automation”. we had the full target path. select the “Add…” button. Specifically. Target. it needs to use the GUIDs (Global Unique Identifiers) as delivered by the provider. it is likewise too pithy to really clue us into what we need to do to fix it.Enhancing Visual Basic . plus the properties Arguments. is simply a file that you might find on your desktop or elsewhere that has a hidden “.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 3 Get the Linked File Path from a Shortcut File Sometimes within certain applications we need to know the actual file that a Shortcut file (a Link file) is linking to. performing most any task required of the Windows interface shell. of course. The IDE will create a non-COM version of the DLL named interop. folders. from the operating system itself.ShellLinkObject). this is an instance where we do not want to do that (you might notice other autoadded references in your project also do this). 0. Shell32. plus an optional index.lnk separate to highlight the fact that we need to add it) Dim ShortcutFilepath As String = _ Environment.dll). NOTE: It is unfortunate that.GetFileName(ShortcutFilePath)) 'define our shell class (ensure its Embed Interop Types parmameter = False) 'define a folder object to the link file's directory folder 'define a link to the shortcut file object from the folder object All that is left to do is to pick up the actual path from the FolderItem object. which are all text properties. We actually want it to derive this class from outside our code. description. simply remember to invoke the object’s Save() method before leaving. Shell32.SpecialFolder. or at least I have not figured it out.Path. the name of the shortcut file to access.GetFolderPath(Environment. To update the shortcut in case you made changes that you want to keep.ShellLinkObject).exe. although we can access and modify shortcuts through Shell32’s ShellLinkObject class.ParseName(System. and HotKey (like “Alt-F11”).ShellClass Dim Folder As Shell32.ParseName(System. However. which is the point of this whole exercise.NameSpace(System. we may want to reuse this code. See the next tip (Black Book Tip # 4 on page 353) to see how to do that.DLL (this Dynamic Linking Library actually wraps the IShellLink interface of Shell32.Enhancing Visual Basic . such as its arguments.Folder = _ Shell.Trim 'then return the link's command path End If Catch End Try Return Nothing 'otherwise failure. Page –352– .NameSpace(System.GetLink.ShellClass 'define our shell class object as a link to our operating system shell Dim Folder As Shell32.GetLink.NET Beyond the Scope of Visual Basic 6.Save()”.IO. in the meantime.Path. We can do all these seemingly complex tasks by using the following three lines of code: Dim Shell As Shell32.IO. so indicate so End Function BONUS TIP: After creating a shell FolderItem. Return DirectCast(FolderItem.GetFileName(ShortcutFilePath)) 'define a link to the shortcut file object from the folder object If FolderItem IsNot Nothing Then 'if it exists. we next define a string variable to hold the path to the link file we want to read.Path. path. if we want to access a Desktop shortcut named BetterSlicedBread. working directory.Shell = New Shell32.ShellLinkObject = DirectCast(FolderItem.Path. icon (the path to the icon.IO.GetDirectoryName(ShortcutFilePath)) 'define a folder object to the link file's directory folder Dim FolderItem As Shell32. derive from that a shell Folder object that is associated with the folder path to the shortcut. We can pick up this path property using the following single line of code: Dim LinkToPath As String = DirectCast(FolderItem.lnk" What we need to do next is to define a ShellClass object. When interfacing with the core operating system. such as the Desktop (Environment.Path Like with anything useful.Folder = _ Shell. plus “. such as “ShortCutObj.0 – David Ross Goben When you add a COM reference to your application.Path. and derive from that a shell FolderItem object that refers to the shortcut file.FolderItem = _ Folder. to include its “.ShellLinkObject)”.GetFolderPath(Environment.lnk” at the end.DesktopDirectory) & "\BetterSlicedBread" & ". For example. This simply consists of the folder path where we are accessing the link file. and ensure its Embed Interop Types parmameter = False '******************************************************************************* Private Function GetShortcutLinkToPath(ByVal ShortcutFilePath As String) As String Try Dim Shell As Shell32.DesktopDirectory)).Shell = New Shell32.FolderItem = _ Folder. which is the default embedded icon index).SpecialFolder. 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.GetLink. you can persist access to the shortcut’s properties by using something like “Dim ShortCutObj As Shell32. we cannot directly create them through it. such as myApp. and so we must not embed the interface within our application code.GetDirectoryName(ShortcutFilePath)) Dim FolderItem As Shell32. we can instead use IWShRuntimeLibrary. With this object you can examine or alter the other properties exposed by it. as shown to the right (you can view this property from the Reference Tab on the Application Properties page when you click on the added Shell32 reference entry): In our code.IO.. we could set up a string variable to acquire the full path using the following: 'build the full path to the Link file (note that I keep the addition of . it embeds its Interop Types in your application by default. Shell32.DLL)..lnk” extension. 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. ... If the user is curious about what that icon is that they just found on their desktop.. or DLL. filename. 0”).. and the other named ShortcutFolder. If not supplied. Shortcut Arguments . If none is provided. such as application-meaningful instructions to do something.(Optional) A text string holding the destination folder path where the shortcut will be created.. including the folder. For example.Enhancing Visual Basic .DLL)... we need to pick up the path to the Desktop. Minimized (1).... the Desktop is used. If your application has parameters.. Typically.(Optional) A text string holding the folder path you want the linked application or file to treat as its local folder. like setting up a required special screen resolution. you must ensure that its Embed Interop Types parameter is set to False from its default True state... Shortcut Folder. 1” to select the second icon in the icon list (default is “. By default. With that.NET Beyond the Scope of Visual Basic 6.. we are ready to begin.. With a reference made to Windows Scripting Host Object Model. plus a hidden “...DLL. IWShRuntimeLibrary.. just like with Shell32. and its Embed Interop Types parameter set to False... you will need the Working Directory parameter. Working Directory. Window Style . if no icon is provided. The path to the shortcut that is to be created (or over-written) is constructed by combining the Shortcut Folder with the Shortcut Name. Shortcut Description . such as Normal (0.0 – David Ross Goben Black Book Tip # 4 Create a Shortcut File within Your Code If you have ever needed to create a shortcut that the user can select to launch your application or an auxiliary file.(Optional) A text string holding an optional argument list you want to supply the shortcut with..(Optional) An integer value indicating how the target should be displayed (if it is displayable)... but if you think about it.. A shortcut file actually consists of a number of properties.(Required) A text string holding the full path. for the file or executable you want the shortcut to re-direct to. You can accomplish this using a simple COM Reference to the Windows Scripting Host Object Model (WSHOM. and extension... one named ShortcutName that hold the name for the shortcut to create. Shortcut Icon ... we can prepare for creating the shortcut with the following lines of code: Page –353– .. it will extract the main icon from an executable or from the executable associated with the file if it is a non-executing file... in the properties for the Windows Scripting Host Object Model reference. which hold the path to the directory where we want to create the shortcut... This will tell us which icon to display for this shortcut.. such as files to process.... icon library. default). which might contain additional support files. executable.... this is the folder holding the linked file or executable...exe. Refer to the last tip to see how to add a COM reference to your application..(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.....(Optional) A text string holding the name we want to be displayed on the shortcut icon. If the file has multiple icons... ShortCut Name..... this is not necessary because parameters.. the process to do so is actually quite simple. then you will want to include those with the Shortcut Arguments.. The space after the comma is optional. The above seems like a lot to consider.. but such parameters would typically be special manual instructions.lnk” extension. but it should treat another as its local working directory... all but one of which is optional: Link-To Path.. We want to also check to see if the Shortcut Folder is supplied. If your executable operates from one directory...lnk” to the end.. If not.. examining its properties it will give them a Description. 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). and adding “. or Maximized (2).. such as “MyApp”. This comprises the actual name of the shortcut.(Optional) A text string holding the full path to an icon file. Suppose we had two string fields. If you want the application to start up minimized or maximized. it all makes perfect sense. NOTE: Also.. we can extract a default name from the Link-To Path’s filename and extension. “C:\MyFolder\MyApp. choose the one you want to use by appending a comma and then the icon index to use. actually operates entirely from within the operating system and so it’s Interop Types cannot be embedded within our application.. then you will need the Window Style parameter.. such as the path to your Desktop... By default.. will append just fine. DesktopDirectory) End If Dim ShortcutFilepath As String = ShortcutFolder & "\" & ShortcutName & ".WorkingDirectory = Environment..IWshShortcut = DirectCast(wshShell. IWshRuntimeLibrary. Optional ByVal ShortcutArgs As String = Nothing.TargetPath = LinkToPath MyShortcut. Consider the following method that can be placed in a module: '******************************************************************************* ' Method Name : CreateShortcut ' Purpose : Create Windows Shortcut 'NOTES: ' LinkToPath = Full path. and with as many parameters as we need. no arguments. Optional ByVal ShortcutFolder As String = Nothing.0 – David Ross Goben If ShortcutFolder = vbNullString Then ShortcutFolder = Environment. Optional ByVal WorkingDir As String = Nothing. there is nothing at all complicated about this process.IO. 0" 'specify its first icon (offset 0) End If MyShortcut. save the shortcut file and close the object As you can see. For re-use.SpecialFolder.WshShellClass 'define our shell class and our Shortcut object Dim MyShortcut As IWshRuntimeLibrary. save the shortcut file and close the object End Sub BONUS TIP: If your application has files associated with it (see Black Book Tip # 2 on page 348). we are ready to create the shortcut object. which we have stored in a string variable named LinkToPath.WshShellClass 'define our shell class and our Shortcut object Dim MyShortcut As IWshRuntimeLibrary.Application. and ensure its Embed Interop Types parmameter = False '******************************************************************************* Friend Sub CreateShortcut(ByVal LinkToPath As String.GetFolderPath(Environment. let us create a method that can be invoked to create as many shortcuts as we require. With the above method present. the icon is the main icon for the file specified in LinkToPath.lnk" 'build the full path to the Link file '------------------------------------------------------------------------------------------Dim wshShell As New IWshRuntimeLibrary. IWshRuntimeLibrary.Save() 'finally. "Open the File using SuperWizBang") Page –354– . we are ready to define its properties.WindowStyle = WinStyle 'apply a windows style MyShortcut. Consider the following two lines of code: Dim SendTo As String = Environment.Path. For this example.CreateShortcut(ShortcutFilePath). we issue the following two lines of code: MyShortcut.NET Beyond the Scope of Visual Basic 6.SendTo) 'get the path to the system’s SENDTO folder CreateShortcut(Application.DesktopDirectory) 'no. including executable. Optional ByRef WinStyle As Windows.TargetPath = LinkToPath 'then set the target path MyShortcut.IconLocation = Environment. Optional ByVal ShortcutName As String = Nothing.lnk" 'is ShortCutFolder not defined by the user? 'no.. We will let the system assume the working directory is the execution folder. Optional ByVal ShortcutDescr As String = Nothing. you can right-click them in File Explorer and select the Send To menu to the select your application by adding a shortcut to your application in your system’s SendTo folder. we will supply only the Link-To path.Forms. such as applying the Link-To Path. ShortcutName = System. This only takes two lines of code: Dim wshShell As New IWshRuntimeLibrary.IWshShortcut = DirectCast(wshShell.SpecialFolder. to the target of the shortcut ' ShortcutName = Optional Shortcut Name to display for link file ' ShortcutFolder = Optional Destination folder for shortcut (default=Desktop) ' ShortcutDescr = Optional description for the shortcut ' WorkingDir = Optional working directory path for the target executable ' ShortcutIcon = Optional path to an Icon file (this can also be the executable.Info. so default to the desktop End If If ShortcutName = vbNullString Then 'If the shortcut name is not defined.IWshShortcut) MyShortcut. My.SpecialFolder. MyShortcut.IWshShortcut) Now.. so assign it to the shortcut object End If If Len(ShortcutIcon) <> 0 Then 'if an icon path exists.CreateShortcut(ShortcutFilepath). then default to the Desktop '------------------------------------------------------------------------------------------If ShortcutFolder = vbNullString Then 'is ShortCutPath not defined by the user? ShortcutFolder = Environment.ExpandEnvironmentVariables(WorkingDir) 'yes. To do all this and save the shortcut file besides. SendTo.Save() 'then set the linking path 'finally.Normal) '------------------------------------------------------------------------------------------'get path where to place the shortcut.ExpandEnvironmentVariables(ShortcutIcon) & ". doing this is almost too easy.ProductName. and the program will start up displayed normally. itself) ' ShortcutArgs = optional Argument list for the target executable ' WinStyle = App display style (default is Normal Focus) ' 'This method requires COM references to: ' System Windows Object Model (IWShRuntineLibrary.GetFolderPath(Environment.dll).Description = ShortcutDescr 'apply a file description if required MyShortcut. Optional ByVal ShortcutIcon As String = Nothing. it will have no description.ExecutablePath. so default to the desktop 'build the full path to the Link file With that.Enhancing Visual Basic ..FormWindowState = FormWindowState.GetFileNameWithoutExtension(LinkToPath) 'grab the filename of the file from the full path End If Dim ShortcutFilePath As String = ShortcutFolder & "\" & ShortcutName & ".GetFolderPath(Environment.Arguments = ShortcutArgs 'then apply them to the target path If Len(WorkingDir) <> 0 Then 'did we define a working directory? MyShortcut. If it is Nothing. Path. Microsoft has complicated the determination of Windows 8.Recent)).2 (Windows 8). ByVal FilePath As String) Up through Windows Vista (Windows 6. which indicates the FilePath points to a null-terminated Unicode string. I am opting for a value of 3.3). suppose that a string named ProcessedFilePath contains the full path to the file that our application is processing. Basically. Other. and everything else. System. and selecting the file. To accomplish this task is so simple it is almost stupid.0). which is fine and I am perfectly comfortable with it. we simply execute the following line of code: SHAddToRecentDocs(3. Hidden deep within the catacombs of the system workhorse. However. To save a shortcut to it in the Recent Documents folder. because Dot NET strings are Unicode. but prepping it by extracting the name of the file.1 (Windows 6. ProcessedFilePath) 'add the file to the Recent Documents Folder And that is all there is to it. Because we are using Dot NET. etc. it would look something like this (here. for the purposes of tracking those items used ' most recently and most frequently. I typically had saved a shortcut to the file I am opening by creating it in the system’s Recent folder (Environment. This little method requires only two parameters. and Windows 10 (Windows 10.2).Recent)) This works well enough and has never failed me. the last two of which hold interest to us. apply the icon that is associated with it via its associated executable (which it will find in order to extract it). an Integer and a text string. we are using the CreateShortcut() method used in the previous tip): CreateShortcut(LinkToPath. Windows 8 (Windows 6.aspx) The great thing about this method is that I do not need to set anything up. the user can bring up the Recent File list by selecting Start / Recent Files. higher values can be found on MSDN’s website. being that I used it constantly while developing C++ code.1).IO. 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. I will declare the Unicode version so that the processor will not have to first convert this string to ANSI.microsoft. A value of 2 indicated that FilePath points to a nullterminated ANSI string (we would need to change the “Unicode” in the above definition to “Auto” in order to use that value). and go through the usual shortcut creation process as outlined in the last tip. unless the application is specifically manifested for those operating systems (see https://msdn. is an innocent-looking P/Invoke named SHAddToRecentDocs(). Friend Declare Unicode Sub SHAddToRecentDocs Lib "shell32.NET Beyond the Scope of Visual Basic 6. With file associations in place as shown in Bonus Tip # 2 on page 348. as we often choose to do under VB6: ' Notifies the system that an item has been accessed.SpecialFolder. For example. This function can also be used to clear all usage data.GetFolderPath(Environment. so the usual version information is returned as 6.DLL" (ByVal uFlags As Int32.Enhancing Visual Basic . I missed it plenty of times.GetFileName(LinkToPath).GetFolderPath(Environment. and they are defined for Windows 7 (actual Windows 6. Page –355– . oddly enough). NOTE: For some bizarre reason.DLL.85). were it not for the fact that this information seems to be difficult to locate. this method had just 3 integer values for its uFlags parameter.com/en-us/library/windows/desktop/ms724832(v=vs. For some time.SpecialFolder. Shell32. Environment. but even this is more complicated than it needs to be. which will be opened by the application it is associated with. It will automatically extract the filename to name the shortcut.0 – David Ross Goben Black Book Tip # 5 Adding a File to the Recent Documents Folder Some applications save a shortcut to the files they open in the system’s Recent Documents folder. Windows 8.1 and Windows 10. or trying to sort off of multi-columns.SubItems(col).ColumnClick With DirectCast(sender. . CType(y. Consider the following simple event code that is supposed to toggle sorting on the first column of the ListView when the user clicks on any of its column headers (and it will on simple string data): Private Sub lvFileList_ColumnClick(sender As Object. where it toggles between ascending and descending sorted order each time you click it.NET Beyond the Scope of Visual Basic 6. e As ColumnClickEventArgs) Handles lvFileList. It is instead simply the comparator used by the QuickSort method that the ListView and other Dot NET controls use to sort their lists (the Shell-Metzner algorithm is many times faster. For example. if you set the Sorting property to SortOrder. I thought perhaps the ListView control also required the invocation of the Sort() method to resort it. they tell you that you can sort on a particular column using the follow small class.Descending Then 'if we are presently sorting in Descending order.NET ListView control to sort. and we end up scrambling madly about like stray cats in a dog pound as we try to figure out how to make multicolumn sorts work properly.Compare Return [String]. 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. however. If you look to MSDN’s documentation on the ListViewItemSorter property. Using custom objects or even structures featuring a ToString function to deliver display content. on the other hand. did nothing. ListViewItem).. or more often to get the ListView’s Sorting property to work with the objects they assigned to the ListView. When I first ran into this issue. We do not need to do that. 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.Enhancing Visual Basic .0 – David Ross Goben Black Book Tip # 6 Sorting Any Column in a ListView Control in Ascending or Descending Order You have probably played around with the Details-View headers in File Explorer to sort the items based on the column selected.Descending.Text) End Function End Class The problem with this simple class. but that.Sorting = SortOrder. most especially in long lists. ListView) If . as logically it should (it likely had to do with actually reading some documentation – in truth.. Class ListViewItemComparer Implements IComparer Private col As Integer Public Sub New() col = 0 End Sub Public Sub New(ByVal column As Integer) col = column End Sub Public Function Compare(ByVal x As Object. sweat spraying off our heads like nervous cartoon characters. A number of users on the web have expressed seething exasperation in trying to either get any selected column under a VB. but QuickSort has the advantage of supposedly being small in size (I would argue SM is shorter) while still offering relatively quick speed). or even not at all if you fed objects to the ListView that did not define their own ToString function to provide displayable content. is that toggling the Sorting property between Ascending and Descending no longer does anything but sort in Ascending Order! Grrrr… Page –356– . ListViewItem). Afterward.Sorting = SortOrder. ByVal y As Object) As Integer Implements IComparer. sometimes throws gasoline on a fire. reading documentation is a pass-time that I do even when I am not up against a deadline or am in desperate need of a solution).Sorting = SortOrder.Compare(CType(x. I verified that it does automatically invoke its Sort() method whenever you change the Sorting property’s state.Ascending 'change it to Ascending order Else . This method is not the actual function used to sort the list.Descending 'otherwise.SubItems(col). it might still sort on the first column in ascending order. as I fully expected.Text. select Descending Order End If End With End Sub The frustrating thing here is that ascending and descending sorts work fine for simple string-only data. if we want our class to also support descending sorts on any column. we will also tell the sorting class that we want to sort in Ascending or Descending order.ListViewItemSorter = New ListViewItemComparer(0. SortingOrder). We would normally initially plug our class object into the ListView within the form’s Load event.Column. consider the following example ColumnClick() event (this event will only fire when we are in the Details View mode. or before we load the ListView to avoid any delays (in the Bonus Tip at the end of this article I will show you how easy it is to in fact key its sorting order directly from the ListView’s Sorting property).Sorting = SortOrder. as in Microsoft’s example above. euqal to.ListViewItemSorter = New ListViewItemComparer(e.lvFileList. When we use objects that do not feature built-in awareness of the ListView’s Sorting property.Column 'keep track of the last column selected . It exposes a ListViewItemSorter property that allows developers to assign to it a comparison ' object that implements iComparer criteria.Column. but not in any of the other modes). For example. then we will have to also provide descending comparison program code (do not worry. SortOrder.ListViewItemSorter = New ListViewItemComparer(e. and only toggle between Ascending and Descending Order when we click on the same column as the last time. we will be invoking it like this: New ListViewItemComparer(iColumn. Clicking the same column again toggles the sort order ' between Ascending and Descending. Ascending ' 'To sort on a particular Column. or greater than the second string.Ascending) 'sort on the new column. we can tell if we should return True if the left string is greater than the right string.Sorting = SortOrder. as demonstrated in the previous Microsoft example.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 is still useful as a reporting mechanism. because changing the Sorting property does not work with custom objects that are unaware of the associated ListView’s Sorting property.Ascending 'force Ascending if Descending or different col End If End With End Sub What is left is to simply define a comparator class that will work for both Ascending and Descending comparisons. ' I have enhanced the comparer to support both ascending or descending comparisons. But first. it basically ends up being just a flag exposed for the user code to inspect. '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.Enhancing Visual Basic .Column AndAlso . During ColumnClick() events.Ascending)”. Ascending . The way I like to do it is to always sort in Ascending Order if we clicked on a different column.Descending Else _LastColumn = e. we want to determine which column is being clicked and apply the sorting class to it. Descending . Because of this. do something like the following variation of the above: ' ' NOTE: The following example assumes the user is clicking on a Column header in a Multi-column ListView.listView1.Sorting = SortOrder. ListViewItemComparer. we would intially assign our class to it within the Form’s Load event by entering “Me. This way. if we have a ListView control named lvFileList.ColumnClick With DirectCast(sender. which sadly performs only ascending comparisons. and this ' method will sort the lists based upon the column clicked. whenever you set this property. but. ' Page –357– . how can we use it? Suppose we defined a class named.NET Beyond the Scope of Visual Basic 6. it is amazingly easy to do!). SortOrder. and featuring similar operation. where 0 indicates the first column and we are starting with an Ascending Sort. e As ColumnClickEventArgs) Handles lvFileList. to also set the Sorting property appropriately to reflect our chosen sorting order.0 – David Ross Goben 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. SortOrder. This comparer is provided two strings and is expected to determine ' if the first string is less than. or False if it is not: Option Explicit On Option Strict On 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' ListViewItemComparer Class Module (VB. and also keeping track of Ascending or Descending sort order. assuming we already have a working comparison class for the ListView. ListView) If _LastColumn = e. It would also be a good idea. Therefore. '------------------------------------------------------------------------------------'Assign this Class to the ListView object using something like the following: ' Me.Ascending Then 'same column and ascending? . However. '********************************************************************************* ' 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 column 0) Private Sub lvFileList_ColumnClick(sender As Object. With that in mind.ListViewItemSorter = New ListViewItemComparer() 'Sort Coumn 0 (first or only column). Thus. We can do that by keeping track of the column.Descending) 'sort on the new column. ListViewItemSorter interface '******************************************************************************* 'comparison method used by the Listview (can sort Ascending or Descending) Friend Function Compare(ByVal x As Object.Compare Dim Obj1 As ListViewItem = DirectCast(x. which means that this method can also sort columns containing date fields that are formatted for any global locale.Column. Page –358– .Ascending) 'sort on the new column.Length) * AscDecFlag End Select End Function End Class NOTE: The above class also takes into consideration Date entries. which had sorted strictly based upon text. I found Shannon’s example long after I had developed my first example class.SubItems(_Column). e As ColumnClickEventArgs) Handles lvFileList.CompareTo(Obj2. See http://msdn. use column 0 (the first column) _SortingOrder = SortOrder. defaulting to False because checking dates is a bit slower than simply comparing strings.SubItems(_Column).Parse(Obj1. 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. invert the result for Descending End If '--------------------------------------------------------------If _CheckDates Then 'if we will be checking dates (slower) Try 'first.Ascending 'default to Ascending sort _CheckDates = False 'default to not checking date formats (checking dates is much 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.com/enus/library/ms996467. but if a different Column is clicked. Optional ByVal SortingOrder As SortOrder = SortOrder.Column 'keep track of the last column selected ' Me. Notice also that I have made it an optional parameter. ListViewItem) 'get local objects Dim AscDecFlag As Int32 'define Ascending/Decending flag If Me._SortingOrder = SortOrder.Text) Dim secondDate As Date = DateTime. compare their lengths if the result is zero Select Case String.lvFileList.SubItems(_Column).Sorting = SortOrder.Compare(firstDate.lvFileList. ListViewItem) 'get local objects Dim Obj2 As ListViewItem = DirectCast(y.SubItems(_Column).Sorting = SortOrder.NET Beyond the Scope of Visual Basic 6.Descending) 'sort on the new column.Descending ' Else ' _LastColumn = e.Column AndAlso Me.Ascending.Text.Ascending Then 'same column and ascending? ' Me. This will be used to enhance operation so that if the same column ' 'is clicked. the the sort ' 'order begins as Ascending for that new column.Text.0 – David Ross Goben ' Private _LastColumn As Int32 = -1 'init last column clicked.Ascending 'force Ascending if Descending or different col ' End If 'End Sub 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 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.Text) Return DateTime. ByVal y As Object) As Integer Implements IComparer.lvFileList. ' ' Private Sub lvFileList_ColumnClick(sender As Object.Text. SortOrder. Descending ' Me. then treat as strings End Try End If '--------------------------------------------------------------'because equal compares check only for the length of the shortest member.Compare(Obj1.ListViewItemSorter = New ListViewItemComparer(e. try to treat the items as dates Dim firstDate As Date = DateTime._Column = Column 'creating class with a specified column number to sort on Me. the sort order is toggled.aspx.SubItems(_Column).lvFileList. Optional ByVal CheckDates As Boolean = False) Me.ListViewItemSorter = New ListViewItemComparer(e. secondDate) * AscDecFlag 'if we get this far.SubItems(_Column).Ascending Then 'ascending? AscDecFlag = 1 'treat result normally if Ascending Else AscDecFlag = -1 'otherwise. 2002 on how to sort either text or dates accurately. but can have a noticeable lag on really long lists. they ARE dates.Length.Text) * AscDecFlag Case 1 Return 1 Case -1 Return -1 Case Else Return Obj1.Enhancing Visual Basic ._CheckDates = CheckDates 'if we will check dates or not End Sub '******************************************************************************* ' Method Name : Compare ' Purpose : String comparison using by the ListView.Sorting = SortOrder.microsoft. Ascending ' Me.Parse(Obj2. SortOrder._SortingOrder = SortingOrder 'store Ascending/Descending sort flag Me.ColumnClick ' If _LastColumn = e.Column. Obj2.lvFileList. so return the result of the comparison Catch 'if either are NOT dates. ListView)) 'sort on the new column . 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.Sorting 'set our sort order from it End If If Me._lvParent = lvParent 'set listview parent so we can access its Sorting property Me.Column.Ascending Then 'ascending? AscDecFlag = 1 'treat result normally if Ascending Else AscDecFlag = -1 'otherwise. ByVal y As Object) As Integer Implements IComparer. e As ColumnClickEventArgs) Handles lvFileList.ColumnClick With DirectCast(sender.lvFileList)”. Making these modifications requires very little code. DirectCast(sender. Optional ByVal CheckDates As Boolean = False) Me. Me. which is flipping sort order when the same column is clicked. If .Ascending 'else flip back to Ascending End If Else 'we clicked a different column. for faster sorts) End Sub Here.Column Then 'if we are sorting on the same column._lvParent._CheckDates = CheckDates 'set to True if we will check dates (default is False. in the Compare function. ByRef lvParent As ListView.._lvParent IsNot Nothing Then 'if we have a ListView control defined. ListView) If _LastColumn = e.Sorting = SortOrder._SortingOrder = Me.lvFileList. _LastColumn = e.. Page –359– . To make the above small changes actually work.Descending 'flip to Descending (no need to assign a new Comparer instance) Else . 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. to start.ListViewItemSorter = New ListViewItemComparer(0.Compare Dim Obj1 As ListViewItem = DirectCast(x.. consider this much simpler update to our example ColumnClick event: '********************************************************************************* ' 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 column 0) Private Sub lvFileList_ColumnClick(sender As Object.NET Beyond the Scope of Visual Basic 6. ListViewItem) 'get local objects Dim Obj2 As ListViewItem = DirectCast(y..0 – David Ross Goben 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... we instead deliver it with the ListView control that we are in fact assigning this comparer class to.ListViewItemSorter = New ListViewItemComparer(e.Enhancing Visual Basic .. immediately after we declare our Integer AscDecFlag variable.Ascending Then 'and we are presently Ascending._Column = Column 'creating class with a specified column number to sort on Me.Sorting = SortOrder._SortingOrder = SortOrder. For example.Ascending 'force Ascending to start End If End With End Sub Although we are doing much the same as before. simply add a new private field in the heading of our above class. 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. ListViewItem) 'get local objects Dim AscDecFlag As Int32 'define Ascending/Decending flag If Me. we would add 3 simple lines of code: Friend Function Compare(ByVal x As Object. ' : This will allow us to access the state of the ListView's Sorting property '******************************************************************************* '******************************************************************************* Public Sub New(ByVal Column As Integer. ..Column 'so keep track of the last column selected .Ascending 'default to Ascending in case user provided lvParent as Nothing Me. 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.Sorting = SortOrder. instead of supplying the class constructor with a sorting order as we did with our other parameterized constructor.Sorting = SortOrder. And that is all there is to it! Now._SortingOrder = SortOrder. Me. namely vbCrLf codes to terminate each line.Height = TextSize.MeasureText(strMessage.Font) Me. Although setting the WordWrap parameter of a TextBox to True. If your text has built-in line breaks. or the text of the message is simply one long line of text without any line breaks.txtMessage.Height .Height Me.Width + 4 'set the label width to match it (+4 pixels for appearance) Me. its only overload allowing size restrictions features a ProposedSize parameter of type Size.Height = Me.Width = TextSize.txtMessage. being almost embarrassed that I have it in my bag of tricks.Height + TextSize. of course). which will not work for us. But were it that our needs could be so simple. adding line breaks as needed to property wrap the text within a specified width. but we do not want to rescale the form’s width. it is incredibly easy to calculate the display size for the text data. but you cannot determine beforehand how large the Label or TextBox should be sized. If the MeasureText() method did allow for auto-wrapping. the results of a diagnostic report. Me. control-wrapped text to properly resize the form because it assumes that each physical line of text is not wrapped.txtMessage. 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 initial dimensions.MeasureText(strMessage. we would not require the two lines that specifically resize the control. we must adjust the text by manually “wrapping” it so the MeasureText() method will work as needed. usually.Width = TextSize.Width = Me.txtMessage. For example. we can assign a Size-type structure with the required width and height of the text data using the built-in TextRenderer class’s MeasureText() method. like so: Dim TextSize As Size = TextRenderer. of course).Width + 4 Me.NET Beyond the Scope of Visual Basic 6. leaving such code intact will not hurt it.Me. Too bad MeasureText() does not have a MaxWidth parameter. or turning the AutoSize parameter of a Label to False will provide auto-word-wrap if you stuff long text into them.Height Me.Me.Height 'set the label height to match it Me. such as a data file.Height = TextSize.0 – David Ross Goben Black Book Tip # 7 Sizing a Label or TextBox to Fully Contain a String for Display Often you have sometimes quite lengthy text that you want to display within a Label or a TextBox control. this is not feasible with a Label (with its AutoSize parameter set to False. though we would actually want to do is resize the form immediately after we have acquired the new size. Me. I realized that this trick I constantly used was not as widely known as I had at first thought it was.Text = strMessage 'get the calculated text size of the data 'resize the form width to Match (+4 pixels for appearance) 'resize the form height to match 'set the label width to match it (+4 pixels for appearance) 'set the label height to match it '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. because resizing the form would. after a number of conversations with developers who were having trouble trying to figure out how to properly resize their dialogs to fit such data. either – it would simply show no effect on the control.MeasureText() is not able to provide us with the actual dimensions we require of such long. 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. NOTE: When using a Label. Using just the Font information defined in the label.txtMessage. However. which is still easy enough to do. in that case. like so: Dim TextSize As Size = TextRenderer.txtMessage. Even so. or some text of a length that cannot be predetermined. suppose you have a message string in a variable named strMessage and you want to display it in a TextBox named txtMessage. 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.lblMessage.Width + 4 Me. docked. the solution is so easy that I almost did not include it in these tips. for appearances sake. we also have to resize our form to accommodate this re-dimensioned control. TextRenderer. such as keeping our message dialog down to a manageable size. Page –360– . the following exercise would be pointless. Unfortunately. but that includes height restrictions. itself. However. which is the most common scenario. and a single line simply would not accommodate it unless we were using the Times Square Jumbotron as a computer monitor.Width + TextSize. txtMessage. but before we actually resize the Label or TextBox.txtMessage.Width . automatically properly resize it without the need for additional assistance from us. Thus.Text = strMessage 'stuff the message However.txtMessage. We run into trouble when we want to limit the width of the string.Font) 'get the calculated text size of the data Me.Enhancing Visual Basic .txtMessage. Although with a TextBox you can apply a scroll bar (with its Multiline parameter set to True. It would also be a good idea to set an initial height that is compatible with the control layout on your form. Let us call our user interface function SizeMessage(). the only resizing you should then have to worry about is the sizing of the form. the following describes our SizeMessage() function: '********************************************************************************* ' Method : SizeMessage ' Purpose : Compute the display size of the text '********************************************************************************* Friend Function SizeMessage(ByRef Message As String.Width + TextSize. Dim Ary() As String = Split(Message. 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..Enhancing Visual Basic . so calculated text size of the data new form width size (+4 pixels for appearance) than the minimum (current) size? adjust the form width 'resize the form height to match 'set the label width to match it (+4 pixels for appearance) 'set the label height to match it 'stuff the message Now. we will need to supply it with a maximum width. when the method returns.Font. Me. We will also require the same parameters we supplied the MeasureText() method to process each individual line. we will need to write these support methods (there is always a catch…). The Font that we will use to measure the text (this should always be the target control’s font. The SizeMessage() method is the simplest. suppose that our lblMessage control can be of variable size. 3. I have found that 200 pixels is optimal for a minimum label width size.. we can replace the first two lines of the above example code.Height Me. SizeMsgLine(). ByRef Fnt As Font. the return value will be a Size structure that will provide the suggested width and height for the Label or TextBox.Width Then Me. say. but also consider that the controls should be set up and anchored in such a way that they will accommodate a variable-size label. ByVal MaxWidth As Int32) As Size If InStr(Message. If the control is anchored or docked and the width will not change.Width = TextSize..Width + 4 Me. The maximum allowed width for the target control. Ary(Idy) = SizeMsgLine(Ary(Idy). SizeMsgLine(). to say the least.txtMessage. vbCrLf) 'reconstruct the array.Me... this maximum width value is simply the width of the control. For Idy As Int32 = 0 To UBound(Ary) 'then check the width of each one.Height + TextSize. and 550 pixels is optimal for a maximum size in a message area.txtMessage. but we want it to have only a maximum width of 550 pixels (a MsgBox control has a maximum text width of 375 pixels).0 – David Ross Goben One way I like doing this is to break the string up on its current line breaks. This way.lblPrompt.txtMessage. The string of text to check. please make sure its AutoSize property is set to False so that you are in fact able to use its initial width. and make sure that each of those lines is still within the width limit established for the text display. In such cases. of course. and our private support function (which only services SizeMessage()).Width .lblMessage. 2. 550) Dim ShowWidth As Int32 = Me. in case anything changed in the above loop End If Return TextRenderer. MaxWidth) 'process the single line Else 'otherwise.Text = strMessage 'get the 'compute 'greater 'yes. If the form will resize with the resizing of the control. yielding: Dim TextSize As Size = SizeMessage(strMessage. With that.Width = ShowWidth End If Me. if any. And because it will no doubt alter the message string sent to it. On custom message boxes. Fnt) 'return the computed size of the final message End Function Page –361– . if required. though it is initially set to its minimum size of. for proper display.MeasureText(Message.Height = Me. It will also recombine all the resulting lines to be the message passed back to the invoker. Fnt. Message = SizeMsgLine(Message.. we will have to pass it to SizeMessage() By Reference. Also. and the message text passed to it is also properly delineated.Height = TextSize.Me.Width + 4 If ShowWidth > Me. For example. Hence.Height Me.Height . because dialogs containing message areas beyond these bounds tend to make a dialog form look a bit disturbing.NET Beyond the Scope of Visual Basic 6. vbCrLf) 'split out each line of the prompt. MaxWidth) 'to make sure the prompt will wrap properly Next Message = Join(Ary. vbCrLf) = 0 Then 'if there are no CR/LF’s embedded. To properly adjust each line of the text. of course). lblMessage. Fnt. Proper anchoring to a form corner or top or bottom usually works best. we will need three pieces of information: 1. 200 pixels. 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. if you are indeed using a Label control. Idx .NET Beyond the Scope of Visual Basic 6. and Dialog Boxes” on page 500. Label.MeasureText(Left(TextLine.1) & vbCrLf & Trim(Mid(TextLine. which accumulates the updates to each line. Idx . or even a MsgBox with a cleaner. in Black Book Tip # 46. Afterward. the updated message string is returned to the invoker. In this case you will not need its return Size value and you can instead invoke it as if it were a subroutine. you should set its MaxWidth parameter to no higher than 375 pixels. NOTE: Later in this document. and then recombines them as shown above in the SizeMessage() method.MeasureText(TextLine. any recursion is wound back down and the final string is constructed and returned to the ultimate invoker. " ".1). ByVal MaxWidth As Dim TextSize As Size = TextRenderer. However. Idx . Page –362– . 'break the text up with vbCrLf and break that down 'now break the text up into an array 'and then test each higher line (index 0 is already correct) 'process and break up each part as needed (recursion) 'convert the array into a single string with linebreaks 'return the text to the invoker NOTE: You can also use the SizeMessage() function to prepare text for display in a MsgBox.. because a MsgBox will itself wrap text that is longer than this. you should use the SystemFonts. vbCrLf) For Idx = 1 To UBound(Ary) Ary(Idx) = SizeMsgLine(Ary(Idx). Once that point is reached. Fnt) Loop If Idx > 0 AndAlso Idx < Len(RTrim(TextLine)) Then TextLine = TextLine. and it is used again to find an acceptable length for the first part of the message if it does not.0 – David Ross Goben 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. 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. when using a MsgBox. at least for the start of the line. Idx + 1)) Dim Ary() As String = Split(TextLine. Fnt) Dim Idx As Int32 = Len(RTrim(TextLine)) Do While Idx <> 0 AndAlso TextSize. 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. better-formatted appearance of its text.1) TextSize = TextRenderer. Labels. MaxWidth) Next TextLine = Join(Ary..Width > MaxWidth Idx = InStrRev(TextLine. The interesting thing about this method is that it will take advantage of the MeasureText() function to determine if the line is too long. below: '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : SizeMsgLine (support method) ' Purpose : Ensure the string does not exceed the maximum Width value '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function SizeMsgLine(ByVal TextLine As String. “Quick and Easy Text-Justification for Text Boxes. 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. 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.MessageBoxFont font to measure the text. vbCrLf) End If Return TextLine End Function Int32) As String 'get the dynamics of the individual line of text 'grab the length of the entire text line in characters 'is its present width greater than the maximum allowed? 'track backward for a space 'recalculate that size based on the space found 'if the length of the text is to be adjusted.Enhancing Visual Basic .Substring(0. ByRef Fnt As Font. Also. Fnt. Consider the recursive SizeMsgLine() function. so that you can invoke this functionality for any and all ListBoxes and ComboBoxes you have can be accomplished by placing the following code within a module file. For example. the ListBox or ComboBox will still scroll the selected item into view within its displayed list and mark the item as being selected. and for a ComboBox control it is CB_SETCURSEL (&H14E). We send messages through the SendMessage() P/Invoke. If wParam is greater than the ' number of items in the list or if wParam is –1. We accompany this message with the handle of the selected control and the index to set.0 – David Ross Goben Black Book Tip # 8 Set a New SelectedIndex for a ListBox or a ComboBox without Firing its Click Event Typically. _ ByRef lParam As Integer) As Integer ' An application sends a CB_SETCURSEL message to select a string in the list of a ComboBox.The SetListIndex() function Set the ListIndex of a ListBox ' or ComboBox without triggering a click event. This cannot be used to support for selecting multiple items in the list. This function returns TRUE if the control ' afterwards reflects the desired listindex.Enhancing Visual Basic . However. any current selection in the list is removed and the edit control is cleared. _ ByRef lParam As Integer) As Integer To send a message is easy. If the message is successful. 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 list scrolls ' the string into view. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr. A wrapper for this. this message is LB_SETCURSEL (&H186). the return value is CB_ERR(-1) and the selection is cleared. We would clearly expect this of the SelectedIndexChanged() event because we are in fact changing that index. As would be expected. we would issue the command: SendMessage(lstContacts. and also fire a SelectedIndexChanged() event. Private Const CB_SETCURSEL As Integer = &H14E Page –363– . following is the definition of SendMessage() that we want to use: ' Sends the specified message to a window or windows. to set the SelectedIndex of a ListBox control named lstContacts to 17. _ ByVal wMsg As Integer. which might be due to an out of range value. and any previous ' selection in the list is removed. 17. the return value is the index of the item selected.NET Beyond the Scope of Visual Basic 6. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _ ByVal hwnd As IntPtr. _ ByVal wParam As Integer. 0) NOTE: Be aware that this message only works with Single-Selection lists. the respective control’s Click() event and its SelectedIndexChanged() events. If this ' parameter is –1. After all. when you set the ListIndex property of either a ListBox control or a ComboBox control. If necessary. _ ByVal wParam As Integer. The wParam parameter specifies the zero-based index of the string to select. but the Click() event is fired because it is considered to be a selection that simply makes the assumption that it was clicked on. For example. _ ByVal wMsg As Integer. often we just want to set this index without causing any special processing we might have developed for the control’s Click() event to be passed through. this method was originally designed to change the highlighted selection during mouse auto-tracking in the ListBox portion of a ComboBox dropdown list. For a ListBox control. The SendMessage function calls the window procedure for the specified ' window and does not return until the window procedure has processed the message. also fires. A way to do this is to set the index for the control by sending it a message.Handle. LB_SETCURSEL. which is what ' will normally happen when the control's listindex is set to ' anything but -1. However. The text in the edit control of the combo box changes to reflect the new selection. The SendMessage function calls the window procedure for the specified ' window and does not return until the window procedure has processed the message. just as though you had set its SelectedIndex property. The lParam parameter is not ' used. it will NOT broadcast a Click() event. if defined. and FALSE if it ' does not. '******************************************************************************* ' Sends the specified message to a window or windows. CB_SETCURSEL.SelectedIndex) 'return true if it succeeded Case "ListBox" Dim Lst As ListBox = DirectCast(LstOrCboBox. LB_SETCURSEL. If the wParam parameter is –1.Handle. 0) 'set it 'set the desired index Return (NewIndex = Lst.Handle. any current selection in the list is removed and the edit control is cleared. The lParam parameter is not ' used. BVal NewIndex As Integer) As Boolean Select Case LstOrCboBox. to be fair. You cannot use it to set or remove ' a selection in a multiple-selection list box.Enhancing Visual Basic . If an error occurs. NOTE: Use this message only with single-selection ListBoxes.SelectedIndex) 'return true if it succeeded End Select Return False 'fail if not ComboBox or ListBox End Function End Module NOTE: Another way around this issue is to simply abandon using the Click() event altogether and use instead the MouseClick() event. as the Click() event will. However. as its name implies. the return value is LB_ERR(-1).0 – David Ross Goben ' An application sends a LB_SETCURSEL message to select a string in the list of a ListBox. but fires only when the mouse performs a click. Page –364– . The MouseClick() event does not fire when the index is changed. ListBox) 'set aside ListBox reference SendMessage(Lst. The wParam parameter specifies the zero-based index of the string to select.Name 'check which object to process Case "ComboBox" Dim Cbo As ComboBox = DirectCast(LstOrCboBox. If this ' parameter is –1. and any previous ' selection in the list is removed. If necessary. ComboBox) 'set aside ComboBox reference SendMessage(Cbo. the list scrolls ' the string into view. NewIndex.NET Beyond the Scope of Visual Basic 6. NewIndex. The text in the edit control of the combo box changes to reflect the new selection. the return value is LB_ERR even ' though no error occurred. 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. 0) 'set it 'set the desired index Return (NewIndex = Cbo. Private Const LB_SETCURSEL As Integer = &H186 Friend Function SetListIndex(ByRef LstOrCboBox As Object.GetType. Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 9 Display TextBox Text Format-Justified at Runtime When I am displaying certain information on the monitor, such as in an About Box, or I am displaying a dialog to explain some technical information, sometimes I think that the text just looks better when its display format is Justified, not simply Ragged-Right or Centered. Consider a comparison of the same text, first formatted Left-Aligned on the left, and then Justified-Aligned on the right, below: The method behind accomplishing text justification is actually rather simple. The technique I used was the exact same method that I had employed while developing quite a number of printer drivers for commercial release that, among their many features, also supported 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 width 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 uses 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 increment 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 in 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 you 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 you use the “traditional” DrawText() method from the Graphics Page –365– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben object, 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 simply does not expose all the possible parameters that the TextRederer class exposes). When I had initially tried 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 you 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 you may have need of in your application. You simply invoke it in the Form’s Load() event with your 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. You 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 you have your 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 you enable your user to change Fonts or Point Sizes in order to allow them to read your 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. Page –366– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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: 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. '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modJustifyTextBoxDisplay '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private Const Flags As TextFormatFlags = TextFormatFlags.NoClipping Or TextFormatFlags.NoPadding Or TextFormatFlags.TextBoxControl Or TextFormatFlags.SingleLine 'flags used to properly render the TextBox text to a PictureBox '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Friend Class TextDataReference 'this class is used so that we can store more than one item within the TextBox's Tag property. Friend PictBox As PictureBox 'reference to the PictureBox. It will be updated by the JustifyTextBoxDisplay() method and stored here. Friend FmtText As String 'formatted Text Data that will be used by the Paint Event Friend Sub New(Optional ByRef PicBox As PictureBox = Nothing) Me.PictBox = PicBox Me.FmtText = Nothing End Sub Public Overrides Function ToString() As String 'simplify access to the formatted text data of this little class Return FmtText End Function End Class 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC '******************************************************************************* ' 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 ' 'This will also link the created PictureBox's 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 'render the textbox invisible (at runtime, we will see only the PictureBox) Dim picImage As New PictureBox 'create a PictureBox object (The text will be painted to this object) With picImage .Location = TxtBox.Location 'locate the PictureBox to the provided TextBox control .Size = TxtBox.Size 'size it to the textbox boundaries .Parent = TxtBox.Parent 'set parent so it will display when the parent is displayed .BackColor = TxtBox.BackColor 'duplicate the TextBox's background color to the PictureBox .BorderStyle = TxtBox.BorderStyle 'duplicate the TextBox border style .Anchor = TxtBox.Anchor 'reflect the TextBox anchoring to the PictureBox .Tag = TxtBox 'save a reference to the textbox for use during painting AddHandler picImage.Paint, AddressOf JustifyText_Paint 'attach an event handler to the picture box's paint event AddHandler picImage.Resize, AddressOf PictureBox_Resize 'attach an event handler to the picture box's resize event End With TxtBox.Tag = New TextDataReference(picImage) 'object to allow a Tag to contain multiple items AddHandler TxtBox.FontChanged, AddressOf TextBox_FontChanged 'attach an event handler to the text box's font changed event RefreshPicTextData(TxtBox) 'then refresh the TextBox data for display data End Sub Page –367– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '******************************************************************************* ' Method Name : RefreshPicTextData 'Purpose : Support JustifyTextBoxDisplay(), PictureBox_Resize, and TextBox_FontChanged events ' 'Consider the following complete sample form class code that features a single TextBox. 'This code assumes a TextBox filled with text data named TextBox1, a button control named 'BtnOK, another button control named btnSelectFont, and a FontDialog attached to the form. '<============================= BEGIN COPY ====================================> 'Public Class Form1 ' '******************************************************************************* ' ' Method Name : Form1_Load ' 'Purpose : Set up justified TextBox data, then set the FormLoaded flag ' '******************************************************************************* ' Private Sub Form1_Load(sender As System.Object, e As System.EventArgs) Handles MyBase.Load ' JustifyTextBoxDisplay(Me.TextBox1) 'display this textbox with justified text ' End Sub ' ' '******************************************************************************* ' ' Method Name : btnOK_Click ' 'Purpose : Close the sample form ' '******************************************************************************* ' Private Sub btnOK_Click(sender As System.Object, e As System.EventArgs) Handles btnOK.Click ' Me.Close() 'OK button ' End Sub ' ' '******************************************************************************* ' ' Method Name : btnSelectFont_Click ' 'Purpose : Change the Font and/or Point Size used to render the textbox text ' '******************************************************************************* ' Private Sub btnSelectFont_Click(sender As Object, e As EventArgs) Handles btnSelectFont.Click ' With New FontDialog 'with a new instance of a Font Dialog form… ' .FontMustExist = True 'make sure the fonts specified exists ' .ShowEffects = False 'do not show effects. We are not Stan Winston, here ' .Font = Me.TextBox1.Font 'start with the current font and point size being used ' If .ShowDialog(Me) = DialogResult.OK Then 'user selected OK? ' Me.TextBox1.Font = .Font 'yes, so change the font (an event handler that was automatically ' End If 'applied to the TextBox will take care of any other housekeeping ' .Dispose() 'release the created resource ' End With ' End Sub ' End Class '<============================== END COPY =====================================> '******************************************************************************* Private Sub RefreshPicTextData(ByRef TxtBox As TextBox) With TxtBox Dim TextSize As Size = TextRenderer.MeasureText("W y", .Font, .Size, Flags) 'get pixel width of "W y" Dim SpcSize As Int32 = TextSize.Width 'save measurement (we will adjust the Space Size variable soon) TextSize = TextRenderer.MeasureText("Wy", .Font, .Size, Flags) 'get pixel width (plus height) of "Wy", without the space SpcSize -= (TextSize.Width + 1) 'compute space width (drop 1 more pixel for perfect rendering) Dim SpcInit As Char = Chr(128 + SpcSize) 'initial size of space and encoding (space size in pixels + 128) Dim MeasureAdjust As Int32 = CInt(.Font.Size - 0.5!) - SpcSize 'spacing adjustment that we must subtract from MeasureText results Dim Result As String = Nothing 'init encoded result Dim LastLine As Int32 = .GetLineFromCharIndex(Len(.Text)) 'get the last line index of the TextBox, offset from zero Dim SpcAry(15) As Int32 'storage array for space locations found in a line. Bump as needed '----------------------------------------------------------------------For Idx As Int32 = 0 To LastLine 'process each individual line of the TextBox text Dim Tmptext As String 'The copy of a line's Text that will be updated Dim SkipJustify As Boolean = False 'init the flag that can be used to skip justification Dim CurlineIdx As Int32 = .GetFirstCharIndexFromLine(Idx) 'get the index to the start of the indicated line of TextBox text If Idx = LastLine Then 'are we at the last line of text? Tmptext = RTrim(.Text.Substring(CurlineIdx)) 'yes, so grab the text from the line's start index to the end SkipJustify = True 'indicate there is not a reason to justify it Else Dim NxtLineIdx As Int32 = .GetFirstCharIndexFromLine(Idx + 1) 'not the last line, so get the index to the start of the next line Tmptext = RTrim(.Text.Substring(CurlineIdx, NxtLineIdx - CurlineIdx)) 'then grab the text of the line. Also remove trailing spaces End If If Len(Tmptext) <> 0 AndAlso Right(Tmptext, 2) = vbCrLf Then 'does the line terminate with a linebreak? Tmptext = RTrim(Left(Tmptext, Len(Tmptext) - 2)) 'yes, so strip CR/LF and any spaces leading them SkipJustify = True 'indicate there is not a reason to justify it End If '------------------------------------------------------------------If Len(Tmptext) <> 0 AndAlso Not SkipJustify Then 'If are not skipping justification and the line DOES require 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 to spaces in the line to justify it Tmptext = Tmptext.Replace(" "c, SpcInit) 'then replace spaces with the special code for point-size spacing Dim Idy As Int32 = 1 'init the column start index (used to skip over LEADING spaces) Do While Asc(Mid(Tmptext, Idy, 1)) > 127 Idy += 1 'skip past leading spaces (indent) Loop '--------------------------------------------------------------If SpcNeeded > 0 Then 'if space insertions are required for this 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 'needing to scan every character. This greatly speeds up processing. '----------------------------------------------------------Dim SpcIdx As Int32 = -1 'init our index into the space location storage array For Idz As Int32 = Idy To Len(Tmptext) 'scan through line data to find spaces Dim cAsc As Int32 = Asc(Mid(Tmptext, Idz, 1)) 'get the currently indexed character code If cAsc > 127 Then 'special spacing character found? Mid(Tmptext, Idz, 1) = Chr(cAsc + 1) 'yes, so re-stuff it with a single incremented result SpcNeeded -= 1 'drop 1 from the number of padding pixels needed If SpcNeeded = 0 Then 'have we added all the spaces we require for this line? Exit For 'yes, so do not add any more End If Page –368– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben SpcIdx += 1 If SpcIdx > UBound(SpcAry) Then ReDim Preserve SpcAry(SpcIdx + 16) End If SpcAry(SpcIdx) = Idz End If 'otherwise, bump the space location storage array index 'if we need more array space... 'bump it up... 'then stuff a space location into it Next '----------------------------------------------------------'If more space insertion is required, speed things up by processing only 'spacing positions within the line using the space location array. '----------------------------------------------------------Dim AddedSpacing As Boolean = False 'set up a flag to detect if we added any spacing to the line Do While SpcNeeded > 0 'while we still need space For Idz As Int32 = 0 To SpcIdx 'loop through the space index Mid(Tmptext, SpcAry(Idz), 1) = Chr(Asc(Mid(Tmptext, SpcAry(Idz), 1)) + 1) 'increment space code AddedSpacing = True 'indicate at least one pixel-sized space was added to this line SpcNeeded -= 1 'drop 1 from the number of padding pixels needed If SpcNeeded = 0 Then 'have we added all the spaces we require for this line? Exit For 'yes, so do not add any more End If Next '----------------------If Not AddedSpacing Then 'if we could not add any space... Exit Do 'then we should exit the loop to avoid looping infinitely Else AddedSpacing = False 'otherwise, reset the flag for another pass through the full line End If Loop End If End If '------------------------------------------------------------------Result &= vbCrLf & Tmptext 'add TmpText data to the Result accumulator string Next 'then process the next line of text '----------------------------------------------------------------------DirectCast(TxtBox.Tag, TextDataReference).FmtText = Mid(Result, 3) 'finally, store the result to the TextBox, less the leading vbCrLf End With '(NOTE: The Paint() event will finish the job) End Sub '******************************************************************************* ' Method Name : JustifyText_Paint ' Purpose : Draw text from a TextBox onto a PictureBox control (created in the ' : JustifyTextBoxDisplay() method), using specially inserted spacing codes. ' : ' NOTE : A reference to the associated Textbox is stored in the PictureBox's Tag property '******************************************************************************* Private Sub JustifyText_Paint(sender As Object, e As PaintEventArgs) With DirectCast(DirectCast(sender, PictureBox).Tag, TextBox) 'using the TextBox this PictureBox is associated with... Dim TxtData As String = .Tag.ToString 'grab formatted text Dim TextSize As Size = TextRenderer.MeasureText("X y", .Font, .Size, Flags) 'get the pixel width and height of this sample text Dim Yinc As Int32 = TextSize.Height 'save the height for incrementing lines in the PictureBox Dim SpcSize As Int32 = TextSize.Width 'save the length result TextSize = TextRenderer.MeasureText("Xy", .Font, .Size, Flags) 'grab the same text, but without the space SpcSize -= (TextSize.Width + 1) 'compute space width (drop 1 more pixel for perfect rendering) Dim MeasureAdjust As Int32 = CInt(.Font.Size - 0.5!) - SpcSize 'spacing adjustment that we must subtract from MeasureText Dim PosnX As Int32 = e.ClipRectangle.Left 'start postion for drawing text in the PictureBox Dim PosnY As Int32 = e.ClipRectangle.Top Dim TmpTxt As New System.Text.StringBuilder(256) 'init temporary text holder '--------------------------------------------------------------------------For Each Ch As Char In TxtData 'scan each character in the master text Dim iChar As Int32 = Asc(Ch) 'grab the ASCII code for the currently indexed character Select Case iChar Case Asc(vbCr), Asc(" "c), Is > 127 'Line-break, Space, or special spacing character? If TmpTxt.Length <> 0 Then 'yes, so dump the temp text if it has accumulated data TextRenderer.DrawText(e.Graphics, TmpTxt.ToString, .Font, New Point(PosnX, PosnY), .ForeColor, .BackColor, Flags) TextSize = TextRenderer.MeasureText(TmpTxt.ToString, .Font, .Size, Flags) ' grab the size of the text PosnX += (TextSize.Width - MeasureAdjust) 'then bump the x offset, less the hard-coded margins TmpTxt.Clear() 'clear the temp text buffer End If Select Case iChar Case Asc(vbCr) 'line-break? PosnX = e.ClipRectangle.Left 'yes, so reset the X offset to the left side... PosnY += Yinc 'and bump the line index to the next line position Case Asc(" "c) 'a space character? PosnX += SpcSize 'yes, so bump the X offset by a single space size Case Else 'otherwise, it is a special spacing character... PosnX += (iChar And 127) 'so bump the X offset by the spacing code End Select Case Asc(vbLf) 'Linefeed? If so, ignore it Case Else 'normal character? TmpTxt.Append(Ch) 'yes, so add the character to the temp text buffer End Select Next '--------------------------------------------------------------------------If TmpTxt.Length <> 0 Then 'done, so dump any remaining temp text data TextRenderer.DrawText(e.Graphics, TmpTxt.ToString, .Font, New Point(PosnX, PosnY), .ForeColor, .BackColor, Flags) End If End With End Sub Page –369– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '******************************************************************************* ' 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 Reize() 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 the textbox the same size as the picture box (textbox might 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 need 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 you take advantage of the SizeMessage() method demonstrated in Black Book Tip # 7 (page 360, “Sizing a Label or TextBox to Fully Contain a String For Display”), but use it with the TextBox you want to display justified, you 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 you can be assured that the TextBox will fully contain your message, especially if you do not know ahead of time the exact size of your 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 the proposed size for the TextBox to contain the message .Parent.Width = .Parent.Width - TxtBox.Width + TextSize.Width 'next adjust the parent form to properly contain the resized TextBox .Parent.Height = .Parent.Height - TxtBox.Height + TextSize.Height .Width = TextSize.Width 'now resize the TextBox. If the TextBox is anchored top the Form sides... .Height = TextSize.Height 'then these 2 lines will do nothing new, but it also will not hurt it. JustifyTextBoxDisplay(TxtBox) 'finally, justify the display End With End Sub BONUS TIP 2: If you want or need to perform full text justification on a RichTextBox, you 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. Dot Net uses at least RichEdit 3.0. But these newer versions actually support Full Text Justification. See Black Book Tip # 30 on page 442 to see how you can very easily do just that! BONUS TIP 3: If you would like to use a much simpler function that will very decently justify text on-the-fly and offthe-cuff, which you 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 500. Page –370– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 10 Get The ListIndex of a ListBox Row that is Under the Mouse Cursor Often we need to know the item we are moving the Mouse Cursor over or right-clicking on in a ListBox or a CheckedListBox. This is useful for updating the control’s tooltip or allowing us to construct a Context Menu that is tailored for the data the cursor is currently pointing at. As you may already be painfully aware, the selection index for a ListBox does not update when we move over or right-click one of its items, nor should it. Even so, there is no GetItemAt() method for a ListBox to tell us which item line the Mouse Cursor is located at as there is with a ListView control. Even so, this index is very easy to compute. As it happens, both the MouseMove() and MouseDown() event handlers for a ListBox also provides us with a X and Y pixel coordinate within the ListBox, telling us where the Mouse Cursor is located relative to the ListBox client area. The Y (vertical) offset is the only part of this information that we will need in order to compute the relative line the cursor is over. If we divide this Y value by the Item Height of a line (LstBox.ItemHeight) and then add the index of the line that is currently displayed at the top of the ListBox (LstBox.TopIndex), we have in fact computed the relative line within the ListBox where the Mouse Cursor is located. All that we have left to do is to ensure that this computed line does not exceed the number of actual lines in the ListBox, in case the mouse might be over the blank field below a list of items, which can happen on short lists. We can do this by comparing the computed index against the number of items in the ListBox (LstBox.Items.Count). Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modListItemByCoordinate Static Class Module '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modListItemByCoordinate '******************************************************************************* 'Get a ListIndex in a ListBox by coodinates. Useful on Right-Clicks '******************************************************************************* ' modListItemByCoordinate - The ListItemByCoordinate() function returns the ListIndex of an item in a ListBox. ' Although this is normally possible with a left-click, where you can obtain it by ' the obvious ListBox.ListIndex property, the item is not selected on a rightpick or ' a mousemove, where you might want to display information on the item in a ToolTip. ' If the returned index is -1, then the mouse is not over a valid item in the list. 'EXAMPLE: ' Private Sub MyListBox_MouseDown(sender As Object, e As MouseEventArgs) Handles MyListBox.MouseDown, ItsLstBox.MouseDown ' If e.Button = MouseButtons.Right Then 'Right pick? ' Dim LstBx As ListBox = DirectCast(sender, ListBox) 'yes, so Get the listbox we are to work with ' Dim Itm As Int32 = ListItemByCoordinate(LstBx, e.Y) 'get list item the mouse is over ' If Itm <> -1 Then 'if it is a valid index... ' Me.ToolTip1.SetToolTip(LstBx, "Right-Clicked on " & LstBx.Items(Itm).ToString) ' End If ' End If ' End Sub '******************************************************************************* '******************************************************************************* ' Method Name : ListItemByCoordinate ' Purpose : Get a ListIndex in a ListBox by coodinates '******************************************************************************* 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 coodinates '******************************************************************************* 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 –371– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 11 Open File Explorer with a Target File Selected Have you ever wondered how some applications will open File Explorer and already have the target file selected in the window? This is actually so easy to do that this tip was almost not included, yet I am constantly asked how to do it. But, once I show it to you, you will see exactly how simple it really is. First, most seasoned developers take advantage of File Explorer through the ShellExecuteEx() P/Invoke. Through this, we can issue specific commands like “open”, “explore”, “Print”, “properties”, “edit”, and “find”. This is all well and good, and I use it all the time to take advantage of its many features, though it does implement a complicated-looking, though actually quite easy-to-use Structure named SHELLEXECUTEINFO. But we do not actually need any of that. All we need to do is directly access Explorer.exe, the executable for File Explorer, and provide it with some special parameters. For this, we will need only 4 pieces of information: 1. 2. 3. 4. The path to our Target File (too easy… NEXT!). Where to find Explorer.exe (so easy it is almost stupid). What the special parameters are (easy to find). How to launch all this (almost too easy to do). We should already have the path to our Target File. We can directly access Explorer.exe without locating it through our launcher, Shell(). Its command line options can be quickly found by searching http://support.microsoft.com for “Windows Explorer Command-Line Options” or “KB152457”. The Shell() command is often derided by developers as amateur access to the operating system. Forgive them, O Lord, for they know not what they do. Granted, it requires little effort to use, which most developers see as simplistic, but its complexity is just hidden by the primary workhorse of the Windows Operating System, Shell32.DLL. One of its best features is, if your executable is in the system path, you do not need to provide the folder path to the Shell() command. Hence, “Shell("explorer.exe")” is all you need to launch File Explorer, and you can open it to folders on your drive, such as your MyDocuments folder, with “Shell("explorer.exe " & Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments))”. Actually, we do not even need the “.exe” extension, but I am fickle about that because I want my code reviewers to know my exact intent, and introducing ambiguity into code is an invitation for bugs that tend to easily get overlooked. If we look to Microsoft’s Knowledge Base at http://support.microsoft.com/kb/152457, we find a table describing and exemplifying the four optional Explorer.exe parameters, as shown to the right: As you can see, we certainly need the “/select,” parameter to select our target file or folder, but we should also not forget about the “/e,” parameter as well, which will open File Explorer using its default view; this being the view you may have specified at some point when you opened Explorer (you can also easily adjust this by using Explorer’s View menu). Explorer [/n] [/e] [(,)/root,<object>] [/select,<object>] /n Opens a new single-pane window for the default selection. This is usually the root of the drive Windows is installed on. If the window is already open, a duplicate opens. /e Opens Windows Explorer in its default view. /root,<object> Opens a window view of the specified object. /select,<object> Opens a window view with the specified folder, file or application selected. Examples: So, for our use, we will combine these two commands into the primary string, “explorer.exe /e,/select,”, and then append the path to our target file or folder. For example: “Shell("explorer.exe /e,/select," & MyFilePath)”. Page –372– Example 1: Explorer /select,C:\TestDir\TestApp.exe Opens a window view with TestApp selected. Example 2: Explorer /e,/root,C:\TestDir\TestApp.exe This opens Explorer with C: expanded and TestApp selected. Example 3: Explorer /root,\\TestSvr\TestShare Opens a window view of the specified share. Example 4: Explorer /root,\\TestSvr\TestShare,select,TestApp.exe Opens a window view of the specified share with TestApp selected. And that is all there is to it! Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 12 Determining if an array is Dimensioned I have seen a barrage of code on the web explaining to .NET users how to detect if an array is dimensioned, some getting quite elaborate, thanks to the more complex formatting of arrays. For example, apart from Arrays now being objects, which are immensely useful, you must also take into consideration ranking, which is the number of dimensions. Ranking, in simple terms, is the number of commas you have in your array declarations. For example, my1DArray(10) is a 1-dimensional array, my2DArray(10, 20) is 2-dimensional, and my3DArray(10, 20, 30) is obviously a 3-dimensional array. If you have an array and you know what its ranking is, you can quickly pick up how deeply each range of the array is by using the optional Ranking parameter of a UBound() function. For example, Dim aDim As Int32 = UBound(my3DArray, 1) to get the upper bounds of the first dimension of any array (the 1parameter is the default), or use Dim aDim As Int32 = UBound(my3DArray, 2) to get the UBound of the second dimension, or use Dim aDim As Int32 = UBound(my3DArray, 3) to get the UBound of the third dimension. But what if you want to know if the array, regardless of dimensioning, is dimensioned to begin with? This would be important when you might declare an empty array to start, and later redimension it to fit the data that your program will need to load into it. Under VB6, this was amazingly simple to do. You might define a simple function that returned TRUE if the array was dimensioned, or FALSE if it was not. For example, consider this simple VB6 code I had written to determine if any type of array was dimensioned: 'Determine if an array is dimensioned '******************************************************************************** ' modIsDimmed - The IsDimmed() function returns True if the specified array is ' dimensioned. 'EXAMPLE: ' Dim Test() As String ' ' Print IsDimmed(Test) 'prints False ' ReDim Test(5 To 6) ' Print IsDimmed(Test) 'prints True ' ReDim Test(0 To 5) ' Print IsDimmed(Test) 'prints True ' Erase Test ' Print IsDimmed(Test) 'Prints False '******************************************************************************** Public Function IsDimmed(vArray As Variant) As Boolean On Error Resume Next IsDimmed = IsNumeric(UBound(vArray)) On Error GoTo 0 End Function That was all there was to it, regardless of how many dimensions there were defined. Unfortunately, under VB.NET this does not play well with scalar arrays, because they are abstract 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 you must include the type of the array, you must therefore also include its ranking, and so you would see 3 versions of each test, each addressing 1, 2, or 3 dimensions. However, one thing I think we are all missing in the heat to solve, or over-engineer the solution to this dilemma is to determine the root of what we are actually testing. For example, if I declare the following: Dim my3DArray(,,) As Double It is actually the very same as declaring it as the following: Dim my3DArray(,,) As Double = Nothing So, from this, how do we determine if an array of any ranking is dimensioned? Simply like this: If my3DArray IsNot Nothing Then 'True if the array is dimensioned Page –373– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 13 Customizing the Display of TabControl Tabs. As wonderful and as easy as the TabControl is to use, one of its less glamorous aspects is in its seeming inability to distinctly highlight which tab is selected. Granted, it is slightly obvious in that the selected tab is shown as white and the other tabs are a light-light-gray, unless you are visually impaired like me. However, apart from selecting its appearance to alternatively be normal or as flat buttons (refer to the control’s Appearance property), I like to make the normal appearance of these tabs much more distinct, which I usually do by displaying the selected tab’s text in bold and sometimes also as a different color, and making the other tabs a bit darker, but always a shade of gray, although a dark version of a brighter color assigned to the selected tab also works quite well. Fortunately, the control’s default display can be changed because it does afford us the ability to customize each tab’s appearance, even to the point that we can make them another shape, such as a manila folder tab or like the tab formats shown in the various evolving versions of the Visual Studio Integrated Development Environment (IDE). I will show you where you can do it, but it will be left entirely up to you to actually implement such dramatic, creative enhancements. The Tab Control offers a hook into its tab display process through its DrawItem() event handler, allowing us to customize each tab after it has already drawn helpful tab shadows on the TabControl’s background, which we can afterward erase or over-draw within the DrawItem() event, if we are so bold. The DrawItem event handler is activated by setting the Tab Control’s DrawMode property. This property is set to Normal, meaning that the Dot NET Framework’s Common Language Runtime (CLR) will take care of drawing our tabs. But if we want to take care of that instead, we would set this property to OwnerDrawFixed. Setting this will also initially render the object a bit differently, because it is now expecting us to perform all the detail work, as shown to the right. This is because the drawing responsibility for each tab, and even within the IDE, is now squarely in our hands. This includes rendering the tabs to look distinctly different, which, as you can see from the example, the CLR no longer handles, making the distinction between tabs, save for a shadowed, prior-drawn edge, difficult to discern. Some developers are not aware that the Tab Control even fires events because if you double-click on the control during development, a default event handler is not generated. However, you can quickly generate this event body by going to the form’s code page (click the ViewCode icon in the Solution Explorer’s menu tab), and from the left dropdown at the top of the code page (the Controls dropdown), select your TabControl object, and then from the right dropdown, select DrawItem from the Declarations dropdown; this will construct for us an event body like the one shown below: Private Sub TabControl1_DrawItem(sender As Object, e As DrawItemEventArgs) Handles TabControl1.DrawItem End Sub The “sender” object provides you with a connection to the TabControl object you are working with. This is immensely handy if you are wanting to handle more than one TabControl object with this event, where you 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”. 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 you 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. Page –374– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 your 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, you accidentally reference TabControl instead of TabControl1, and then sit there wondering why a bug is being reported. Besides, if you actually do want to clearly differentiate the divide between your reference names and their object types, you 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 you are over-riding a base object command, such as ToString, but you might still need to use the base command version of ToString within your 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) e.Graphics.DrawRectangle(Pens.Silver, tabRect) e.Graphics.FillRectangle(Brushes.White, tabRect) 'get the tab rectangle for the current tab page 'draw the tab rectangle (I personally do not bother with this step) '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 the tab contents using our selected font and font color... 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 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 e.Graphics.FillRectangle(Brushes.White, tabRect) 'now, fill the tab with our selected background color... e.Graphics.DrawString(TabPage.Text, TabPage.Font, Brushes.Black, tabRect) 'draw the tab contents using our selected font and color... 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 you 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 originating 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 you have multiple rows of tabs, easy to discern by the user after they have becomes “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. 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 Page –375– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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) If TabControl.SelectedIndex = e.Index Then e.Graphics.FillRectangle(Brushes.White, tabRect) Else e.Graphics.FillRectangle(Brushes.LightGray, tabRect) End If e.Graphics.DrawString(TabPage.Text, TabPage.Font, Brushes.Black, tabRect) 'draw the tab rectangle 'is the current tab to paint also the active tab? 'yes; fill the tab with our active color... 'else fill the tab with our inactive color... 'draw the tab text This renders the Tab Control more distinctly, as show 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) If TabControl.SelectedIndex = e.Index Then e.Graphics.FillRectangle(Brushes.White, tabRect) Dim txtFont As Font = New Font(TabPage.Font, FontStyle.Bold) e.Graphics.DrawString(TabPage.Text, txtFont, Brushes.Black, tabRect) txtFont.Dispose() Else e.Graphics.FillRectangle(Brushes.LightGray, tabRect) e.Graphics.DrawString(TabPage.Text, TabPage.Font, Brushes.Black, tabRect) End If 'draw the tab rectangle 'is the current tab to paint also the active tab? 'yes; fill the tab with our active color... 'draw selected tab's font as bold 'draw the tab text 'and finally dispose of the created resource 'else fill the tab with our inactive color... 'draw the tab text This change has the effect shown to the right: One thing that I dislike about this so far, and this is just of the Tab Control in general, is the presence of the selection rectangle on the selected tab. There is really not much you can do about it, unless you set the focus to a control on the tab when the tab is entered, which is where focus is normally, if we actually had controls on each tab page. For example, suppose I added a button to each tab (for those new to Tab Controls, simply click the Tab Control, then select the displayed tab to select in the actual tab – sometimes we must do this twice in order to roll object focus inward to what we actually want to select – and then select the body of the tab page to select the actual TabPage control for the desired tab). Then, I would add an Enter() event for each tab page, and set the focus for this button. For example: Private Sub TabPage1_Enter(sender As Object, e As System.EventArgs) Handles TabPage1.Enter Me.Button1.Focus() 'emulate focus going to the controls on the tab page, where we normally want it End Sub Private Sub TabPage2_Enter(sender As Object, e As System.EventArgs) Handles TabPage3.Enter Me.Button2.Focus() End Sub Private Sub TabPage3_Enter(sender As Object, e As System.EventArgs) Handles TabPage3.Enter Me.Button3.Focus() End Sub This provides a cleaner appearance to the tab, as shown on the right: We can of course do other things here, like set the selected color not just to White, but to any color we choose. We can even use the DrawImage() Graphics method to actually draw small images to the tab, which might be more useful in an interface employed in perhaps a child’s learning game. However, this will be left for you to fully explore on your own. What I want to conclude this tip with is displaying the text centered on the tab, not just drawing to its top-left corner. To do this is quite easy, though it will require a different target rectangle. This rectangle, like the previous rectangle that we had gathered from the GetTabRect() method of the Tab Control, will tell the DrawText() method where to draw the text on the Tab. All we need to do is compute a new topleft corner for the text that will center it, both horizontally and vertically, within the tab. 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 Dim Dim Dim txtSize As Size = TextRenderer.MeasureText(TabPage.Text, txtFont) Y As Int32 = tabRect.Y + (tabRect.Height - txtSize.Height) \ 2 X As Int32 = tabRect.X + (tabRect.Width - txtSize.Width) \ 2 cntrRect As New Rectangle(New Point(X, Y), txtSize) Page –376– 'get the width and height of the text to render in pixels 'computer vertical centering start location 'compute horizontal centering start location 'computer new centering rectangle with text width/height limits Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 rather 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 '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 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 '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 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 Dim txtBrush As Brush Dim txtFont As Font 'store to desired tab background color 'store to desired font color 'store the draw font as bold or normal If TabControl.SelectedIndex = e.Index Then bgBrush = Brushes.White txtBrush = Brushes.Navy txtFont = New Font(TabPage.Font, FontStyle.Bold) Else bgBrush = Brushes.LightGray txtBrush = Brushes.Black txtFont = New Font(TabPage.Font, FontStyle.Regular) End If e.Graphics.FillRectangle(bgBrush, tabRect) Dim txtSize As Size = TextRenderer.MeasureText(TabPage.Text, txtFont) Dim Y As Int32 = tabRect.Y + (tabRect.Height - txtSize.Height) \ 2 Dim X As Int32 = tabRect.X + (tabRect.Width - txtSize.Width) \ 2 Dim cntrRect As New Rectangle(New Point(X, Y), txtSize) e.Graphics.DrawString(TabPage.Text, txtFont, txtBrush, CntrRect) txtFont.Dispose() End Sub Page –377– 'is the current tab to paint also the active tab? 'yes, so use White for the tab background... 'and use Navy for its text... 'then define the Bold font... 'otherwise, use a grayed background color... 'use our default text color... 'and ensure that the tab font is normal (in case it is not) 'now, fill the tab with our selected background color... 'get the width and height of the text to render in pixels 'computer vertical centering start location 'compute horizontal centering start location 'computer new centering rectangle with text width/height limits 'draw the tab text 'and finally dispose of the created resource NET Beyond the Scope of Visual Basic 6.DrawLine(tPen. generally.0!) e.Height + 1.DrawLine(tPen. or it will not seem to work 'create a 2-pixel pen to cover the bottom border 'draw a thick. we can simply draw the rectangle. you can actually draw your tab shapes to whatever you want. 1 for the tab border that we had just drawn. .Dispose() 'dispose of the created pen resources End With NOTE: If you are doing the math. . and another for the line drawn by the system for the Tab Page border. thus defeating our intent.Enhancing Visual Basic . . the darker background will simply wash through it. which is performed before the DrawItem() Event fires.Backcolor is not set to Transparent.Graphics.Width. covering white line beneath the tab 'dispose of the created pen resources Of course. and 2) as indicated above. 2. we could add the following lines immediately below the test line “If TabControl.0!) 'create a 2-pixel pen to cover the bottom border e.Backcolor is not set to Dim tPen As New Pen(TabPage. or it will not seem to work Dim tPen As New Pen(TabPage.Width.BackColor.Y .Index Then” in the above example: With tabRect 'for the line below to work. We should keep in mind two things: 1) if the Tab Page background color is set to Transparent. though. by 1 pixel. you will notice that we are actually drawing outside the tab’s bounding rectangle. though you may have to do part of this within the Paint() event of the tab control itself to get around its drawing of the shadows for the tabs.X + . Page –378– . the tabs are actually button controls that have been rendered to just look like tabs).Y + . . not a limit. That being said. . I prefer the latter method because even if we did just draw the 3 other borders. 2. or.X. anyway. make sure TabPage. We can do that in one shot by simply drawing a line that is 2 pixels wide.BackColor.Y . make sure TabPage.X + . and right border lines around the tab and not bother with the bottom border. the line we draw along the bottom will need to be 2 pixels wide.1. and then draw a bottom border that is the same color as the Tab Page background. but the rectangle provided to us by the GetTabRect() method is just a guidline. which had been drawn before any of our tabs are rendered. so we will still have to hide that one as well. if we had set all our tab pages to have their BackColor properties set to White.X.SelectedIndex = e. we should still not stray too far from it if we are not looking to totally redesign the tab page (technically. but 1 pixel outside the range of the tab’s bounding rectangle. top. and we can draw anywhere on the tab page.Dispose() End With Transparent.Graphics.1) 'draw a thick covering line above the tab tPen. . we will still see a border along the bottom because the system will have already drawn a line below it. . For example.0 – David Ross Goben BONUS TIP: If you want to eliminate the line drawn beneath the Selected Tab so that this tab and its Tab Page blend more evenly. the above additional code should instead be rendered: With tabRect 'for the line below to work. For example. you can actually draw the left.Height + 1) tPen. you will have to adjust the X and Y offsets if you have the Tabs displayed on a different side of the page (see the Tab Control’s Alignment property).Y + . if you had the Tab Control’s Alignment property set to Bottom instead of the default Top. . Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 14 Detecting a TabControl Tab on a Right MouseDown Event. Have you ever wanted to display a context menu for a Tab Control tab, but you are foiled in that you cannot seem to find out how to detect which tab the mouse cursor is over? This is actually easy to do, but it requires that we use a P/Invoke that really should have been made accessible from the Dot NET Framework’s Tab Control, much as you are able to detect which List View Item you are over in a List View control by invoking its GetItemAt() method. Granted, it is possible to roll through each tab’s bounding rectangle and find out if the mouse cursor is located within any of them. For example, consider the following code I had written to do just that: Private Sub TabControl1_MouseDown(sender As Object, e As MouseEventArgs) Handles TabControl1.MouseDown If e.Button = Windows.Forms.MouseButtons.Right Then 'right mouse button pressed down? Dim SelectedIndex As Int32 = -1 'init to no tab selected With DirectCast(sender, TabControl) For Idx As Int32 = 0 To .TabPages.Count - 1 'process each tab page If .GetTabRect(Idx).Contains(e.Location) Then 'mouse within the tab of this tab page? SelectedIndex = Idx 'yes, so save that index... Exit For 'and we are done parsing End If Next 'parse all tabs on the control If SelectedIndex <> -1 Then MsgBox("User Right-Clicked on tab " & .TabPages(SelectedIndex).Text) End If End With End If End Sub 'a tab was found? 'yes, so report which Tab the user right-clicked on 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 P/Invokes, 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 value). Because this application of the SendMessage() P/Invoke 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 P/Invoke, 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. 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 = Windows.Forms.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 you have the index of the Tab you right-clicked on, you 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 –379– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 15 Prevent the User from Selecting a TabControl Tab. Now that we have covered displaying and clicking on a Tab of a Tab Control, I have seen a number of developers wondering how they can PREVENT a user from clicking on a tab. Ideally, they want to disable a tab because its functionality is not available for the particular application that the dialog is set up for. For example, some people want to disable a tab if the user is not authorized to access its functionality. The general solution that I use for my own projects is to simply delete the tab entirely, but unlike most developers, I never use the pre-defined instance of my forms in my code. I always create a new instance, such as Dim Frm As New frmTicTacToe, and use that instance, and also remember to dispose of its resources after I have closed it if I am using it as a Dialog. As such, I seldom have instancing issues. Even so, for those people who want to prevent a tab from being selected, there is an Tab Page event handler named Selecting() that is just what the doctor ordered. This event will fire before a Tab is actually selected (at that time the system already knows that the user has clicked on it, but the system has not yet initiated the actual tab selection process). Within this event, if you determine that the user should not be able to access the selected tab, set the Cancel parameter of the TabControlEventArgs parameter e to True (e.Cancel = True). For example, suppose you have a Boolean flag named bUserAuthorized that is initialized to False, and is set to True somewhere in the code if the user is in fact authorized to access restricted tabs on the Tab Control by way of authentication through passwords or whatnot. You could use the following simple code to restrict access to the second tab (Index =1, from zero): Private bUserAuthorized As Boolean = False 'our code must set this to TRUE if the user has authorization Private Sub TabControl1_Selecting(sender As Object, e As TabControlCancelEventArgs) Handles TabControl1.Selecting If e.TabPageIndex = 1 Then 'do not show the 2nd tab page if the user is not authorized e.Cancel = Not bUserAuthorized 'set the Cancel to True if the user is not authorized End If End Sub If this flag is set to False each time the user click 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 374. NOTE: Also refer to Black Book Tip # 16 on page 381 (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 –380– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 16 Hiding Tab Pages without Destroying the Tab Pages or their Resources. Previously in Black Book Tip # 15 on page 380, you were shown how to prevent a user from selecting a tab page by using the Tab Control’s Selecting() event handler and setting e.Cancel = True if you do not want the user to actually select in that tab. The reason we might have to do this instead of removing the tab would be if objects on that tab are accessed throughout the run of the code of its containing form. For example, text fields are updated or images are changed, and the code will broadcast an exception error as soon as we remove the tab. Ideally, if it were possible that this would not affect the operation of anything else, the absolute best solution would have been to simply delete the tab page. For example, to remove the second tab from our Tab Control, we could simply use a command like this: Me.TabControl1.TabPages.RemoveAt(1). Unfortunately, in the real world, this seldom seems to be feasible without errors being reported. Yet, having said that, as long as we maintain a reference to the Tab Page object we want to hide from the user, we can in fact remove the Tab Page from the Tab Control and not lose any of its resources. All objects are data that occupy storage space, and so any variable defined as that object is actually just a reference to that object, and it does not actually constitute the object itself as a scalar (numeric) variable might. If we were to copy this reference variable to another reference variable, we will end up with two reference variables that actually reference, hence, point to the very same object. For example: Dim Pen1 As New Pen(Color.FromArgb(64, Color.Blue)) Dim Pen2 As Pen = Pen1 'define a Blue pen that is 3/4 transparent 'point Pen2 to the very same pen object as Pen1 Thus, if we maintain a reference variable of type TabPage assigned to the Tab Page we want to remove, then if we remove it from the Tab Control, it will still exist and any code that might access it within the form will not crash because the Garbage Collector will detect the object is still referenced. For example: Private pThumbPageHold As TabPage = Nothing 'declare this in the heading of our form ... '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 your 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, though nothing we cannot easily program ourselves out of. 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 you are using SelectedIndex to check for which tab is active. If you have code that must check for specific indexes, you could run into trouble. For example: Page –381– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Select Case Case 0 'do Case 1 'do Case 2 'do Case 3 'do End Select Me.tcImageOptions.SelectedIndex setup stuff specific to the "General" tab setup stuff specific to your "Thumbnail" tab setup stuff specific to the "Cropping" tab setup stuff specific to the "Watermark" tab 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} ... 'check for displaying the Thumb Tab Page (index If bHideThumbnailTab = True Then pThumbPageHold = tpThumbResizing Me.tcImageOptions.TabPages.RemoveAt(1) OrgIndexes(1) = -1 OrgIndexes(2) = 1 OrgIndexes(3) = 2 End If 'set up the original tab indexes in the header of our form (we will improve on this later) 1) within the Form's Load() event handler 'if we will need to hide the Thumbnail tab... 'reference the tab page to remove... 'and remove the tab from display without destroying it 'flag Thumbnail for non-display (we will automate all of this later) 'adjust Cropping ref indexes down 'adjust Watermark ref indexes down 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, we find that we are going to hide our Thumbnail page, so 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, you 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 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)”. Page –382– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Of course, in the removal process we should want to remove tabs by starting from the last tab in the index chain and move toward the front, lower-index tabs, 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, later, if we add another tab, things will not come crashing down about our ears. Next, in the form Load() event, we get the maximum index of the tabs initially defined for the tab control, re-dimension the index array, and then fill it with the original index sequence. Now, we can actually hide our tab pages in any order as long as we do so by invoking a method such as the following, named HideTab(), where the index we provide is the original index value (this way we can hide tab index 1 and later hide tab index 3 without worrying about tab index 3 being at actual tab index 2): '********************************************************************************* ' Method : HideTab ' Purpose : Hide a tab, based on its original, startup index offset '********************************************************************************* Private Sub HideTab(ByVal OriginalIndex As Int32) Dim NewIndex As Int32 'storage for our adjusted index Try NewIndex = OrgIndexes(OriginalIndex) 'pick up adjusted Index Catch Return 'if we suffered a brain fart and went out of original bounds End Try If NewIndex = -1 Then Return End If 'if the item has already been hidden... 'then there is nothing to do HiddenTabs.Add(Me.tcImageOptions.TabPages(NewIndex)) Me.tcImageOptions.TabPages.RemoveAt(NewIndex) OrgIndexes(OriginalIndex) = -1 For Idx As Int32 = OriginalIndex + 1 To UBound(OrgIndexes) Dim Updt As Int32 = OrgIndexes(Idx) If Updt <> -1 Then OrgIndexes(Idx) = Updt - 1 End If Next End Sub 'save a reference to the tab page to remove 'remove tab page from the tab control 'mark the item removed 'now update the upper tabs, if any 'pick up an item to update 'if the item is not already hidden... 'drop the actual index for this item down 'process the next 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. The only thing left to do is release our resources within our FormClosing() event code: Do While HiddenTabs.Count <> 0 HiddenTabs(0).Dispose() HiddenTabs.RemoveAt(0) Loop 'while the list contains tab page references... 'dispose of a tab page's resources 'then remove it from the list And there you 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 you would reinsert them at the appropriate offsets on the tab control using its TabPages collection’s InsertAt() method. But this is only required if you do not use new instances of your forms. As such, this is a very easy exercise that I will leave up to your resourcefulness. Page –383– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 17 Passing a Parameter ByVal when the Invoking Function Specifies a ByRef Parameter. This subject was actually covered in passing very early in this manual (see “A Note Regarding Passing Parameters ByVal” on page 29), though that particular point was just made in passing at its tail end and was very likely overlooked by most readers. Sometimes we need to pass an object or structure to a pre-defined method that requires that the parameter be passed By Reference, but 1) we do not want a particular object to actually be altered, and 2) we will not need the modification to the object when the invoked method returns. This is very much like invoking a function as a subroutine and ignoring the returned value. For example, recently I had a function that took a structure as a parameter and did things with the data supplied by the structure, but also altered the data in the structure to format its data for display. But all I wanted was for the data to be gathered and stored for later reference within the class the method was encapsulated within, and I did not want this structure to be altered in that particular instance. The typical solution to this problem is to copy the structure, or whatever the object is you want to pass to the ByRef parameter in the invoked method, and pass the copy or clone to the method instead, and simply toss the temporary copy away afterward. For example: Dim Tmp As MyStruct = OriginalStruct FormatSetup(Tmp) 'make a copy of the original structure (structures are abstract, so a clone is auto-copied to Tmp) 'format the Tmp structure and store data in the structure Here, we made a copy of the original structure and saved it to the Tmp structure (you will have to clone it yourself if the object is an instantiated (concrete) class). However, you can accomplish the exact same thing with less code and less code overhead, and even 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 the object is actually being 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, such as an autoinstantiated clone, of the object itself, but with the advantage of the object type being returned without needing to cast it, not as a generic Object as the Clone method will typically return. Therefore, the above expression, but with a concrete object, would be the same as performing the following: FormatSetup(DirectCast(OriginalObject.Clone, OriginalObjectType)) 'format a COPY of OriginalObject -- do not alter the original object itself Although, as already shown, you can invoke the following and get the same results as the above line: FormatSetup((OriginalObject)) 'format a COPY of OriginalObject -- do not alter the original object itself E-Z as Drinking Beer Cloning of Concrete Classes By the way, this also makes for very easy cloning of concrete objects. For example, to clone a concrete class object, such as an array, we can easily demonstrate it using something like the following: Dim Ary1() As String = {"Ah", "Beh", "Veh", "Geh", "Deh", "Zshe"} Dim Ary2() As String = DirectCast(Ary1.Clone, String()) 'standard strictly-explicitly-by-the-book cloning Dim Ary3() As String = (Ary1) 'E-Z grab-another-beer-while-the-computer-is-thinking cloning For Each Itm As String In Ary3 Debug.Print(Itm) Next 'display the new array's contents Ah Beh Veh Geh Deh Zshe Page –384– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 18 Show and Hide Additional Information at the Bottom of the Form. Have you seen forms that provide a “Details” or “Additional Information” or “Show More Details” button on its bottom, and when you click it, the form expands to show additional, previously hidden details? Although this concept is as simple as resizing the form, it has been known to give some developers severe headaches if they do not allow for situations where the form is already full screen, or worse, simply close to it. In addition, if you want to expand and contract the form through animation techniques, or to add slick button features, it might turn from a simple idea into a complicated quagmire. As a veteran of numerous such forms, I have honed this sticky process down to a few simple steps. First, place all main body controls on a single Panel, which I always name pnlMainBody. That is the selected panel, shown below. Likewise, place the data we want displayed in the “expanded” view in another Panel immediately below it, always named pnlMoreDetails. This makes management easy. Note that the OK and Cancel buttons, and the Show More Details label are all located on the pnlMainBody surface. Next, set aside a 24x24-pixel flat PictureBox named picShowMore, to act as a button, used to show when to display more information or less. Accompany it with a label, lblShowMore, which the user can also click on. Place them on the left side of the form, across from the usual btnOK and btnCancel buttons. NOTE: Using a flat PictureBox for a Show More ‘button’ requires 6 support images, which are embedded in our demo program (so you can copy them to your own program), that are used to vitalize the image when the mouse interacts with it. So far, such a form might look something like this: Anchoring can be the tricky part, because we want to be able to expand the form and the controls to adjust to it, but we will want to expand and contract the form without moving all the other controls, and hence, the reason for panels. To start, I will have pnlMainBody anchored to all four sides. I would also anchor the OK and Cancel Buttons to the bottom-right corner of pnlMainBody, the more details PictureBox and Label to its bottom-left corner, and the additional details panel to the form’s bottom, left, and right. When we are resizing the form, we will temporarily have to alter the anchoring of PnlMainBody and pnlMoreDetails to only Top, Left, and Right sides. Once the resizing has completed, we would reset pnlMainBody back to all four sides and pnlMoreDetails back to the Bottom, Left, and Right sides. In order to animate the form’s expansion and contraction, we will also require a timer control, which I always name tmrMoreDetails, and set it to a interval of a paltry 25 (milliseconds). The last thing we need to do, for setup that is, is to set aside some private variables that will determine the pixel increment during each timer tick, the target form size, whether we are resizing the form or resizing the panels in case the form is full or near-full screen. Plus, for brevity’s sake, I will add some common constants that I will be using. I define them in the header of my form, like this: Const Const Const Const Const IncDecAmount As Int32 = 16 AnchorLR As AnchorStyles = AnchorStyles.Left Or AnchorStyles.Right AnchorLRT As AnchorStyles = AnchorLR Or AnchorStyles.Top AnchorLRB As AnchorStyles = AnchorLR Or AnchorStyles.Bottom AnchorLRTB As AnchorStyles = AnchorLRT Or AnchorStyles.Bottom Private Private Private Private pIncrement As Int32 pNewTarget As Int32 pSizeByForm As Boolean = False pCanShowMore As Boolean = False 'increment 'Anchoring 'Anchorint 'Anchorint 'Anchoring resizing the form by 16-pixels each timer tick left and right (used by following constants) Left, Right, and Top (used during resizing) Left, Right, and Bottom (default for pnlMoreDetails) in all directions (default for pnlMainBody) 'when resizing the form, this holds the IncDecAmount value as + or 'the target height when the resizing is finished 'True if we resize the form, not move items 'True when we can show more details. False for Less Page –385– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Within my Form’s Load() event, I initially conceal pnlMoreDetails like this: Me.pnlMainBody.Height += Me.pnlMoreDetails.Height Me.pnlMoreDetails.Top += Me.pnlMoreDetails.Height Me.btnMoreDetails.Image = My.Resources.ShowMore pCanShowMore = True 'fill the form with the Main panel 'hide the more details panel by placing it below the form's client edge 'initially start with being able to show more details 'indicate we can show more details As you can see, I have actually expanded the size of pnlMainBody to fill the form and I also moved pnlMoreDetails downward in the form, out of sight and below the bottom edge of the form’s client area. We need to write a method to handle expansion and contraction, which I call FlipMoreLess(), and write the Timer’s Tick() event method. In all, the code for the form, which just supports the expansion and contraction of the form, to include moving the details panel in and out if the form fills the screen, is as follows: Public Class frmDemoMoreLess '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Const incdecAmount As Int32 = 16 'increment Const anchorLR As AnchorStyles = AnchorStyles.Left Or AnchorStyles.Right 'Anchoring Const anchorLRT As AnchorStyles = anchorLR Or AnchorStyles.Top 'Anchorint Const anchorLRB As AnchorStyles = anchorLR Or AnchorStyles.Bottom 'Anchorint Const anchorLRTB As AnchorStyles = anchorLRT Or AnchorStyles.Bottom 'Anchoring Private Private Private Private Private pIncrement As Int32 pNewTarget As Int32 pSizeByForm As Boolean = False pCanShowMore As Boolean = False imgList As New ImageList resizing the form by 16-pixels each timer tick left and right (used by following constants) Left, Right, and Top (used during resizing) Left, Right, and Bottom (default for pnlMoreDetails) in all directions (default for pnlMainBody) 'when resizing the form, this holds the IncDecAmount value as + or 'the target height when the resizing is finished 'True if we resize the form, not move items 'True when we can show more details. False for Less 'image list to hold More-Less images (no need to drop it on the 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 System.Object, e As System.EventArgs) Handles MyBase.Load Me.pnlMainBody.Height += Me.pnlMoreDetails.Height 'fill the form with the Main panel Me.pnlMoreDetails.Top += Me.pnlMoreDetails.Height 'hide the more details panel by placing below the form's client edge 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 System.Object, e As System.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) End If End Sub '********************************************************************************* ' Method Name : picMoreLess_MouseDown ' Purpose : Mouse poped down over fake button '********************************************************************************* Private Sub picMoreLess_MouseDown(sender As Object, e As System.Windows.Forms.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 Page –386– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '********************************************************************************* ' Method Name : picMoreLess_MouseUp ' Purpose : Mouse poped up over fake button '********************************************************************************* Private Sub picMoreLess_MouseUp(sender As Object, e As System.Windows.Forms.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 the panel Me.pnlMoreDetails.Anchor = anchorLRT 'remove Anchor Style Bottom from the panel and set Top If Me.pCanShowMore Then 'if it presently hides the search options... Me.pCanShowMore = False 'then set up for when it is already showing the additional details Me.picMoreDetails.Image = Me.imgList.Images(Images.LessUP) 'image to indicate we can select showing 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 the controls instead of the form pSizeByForm = False 'we will not resize a maximized form; we will move the controls pIncrement = -incdecAmount 'we will decrease the additional panel visibility pNewTarget = Me.pnlMainBody.Height - Me.pnlMoreDetails.Height 'compute final target value 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 'we will not resize a maximized form; we will move the controls Me.pSizeByForm = False 'indicate sizing by controls Me.pIncrement = incdecAmount 'we will increase the 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 the resizing timer and let it move everything End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method Name : tmrMoreDetails_Tick ' Purpose : Handle slide-resizing the form '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub tmrMoreDetails_Tick(sender As System.Object, e As System.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 ahcnor values Me.pnlMoreDetails.Anchor = anchorLRB ElseIf Me.pIncrement < 0 AndAlso Me.Height = Me.MinimumSize.Height Then 'are we trying to reduce the form to less than the minimum size? Me.pSizeByForm = False 'yes, so switch to moving the 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 now now maxed in size, so switch to moving the controls Me.pIncrement = -incdecAmount Me.pNewTarget = Me.pnlMainBody.Height - diff 'only move by what is left Else Me.Height += Me.pIncrement 'adjust form height End If Else Dim diff As Int32 = Math.Abs(Me.pnlMainBody.Height - Me.pNewTarget) 'compute how much is left to adjust If diff <= incdecAmount Then 'within limits? Me.tmrMoreDetails.Enabled = False 'yes, so disable the timer... Me.pnlMainBody.Height = Me.pNewTarget 'set to target height... Me.pnlMainBody.Anchor = anchorLRTB 'and reset ahcnor values Me.pnlMoreDetails.Anchor = anchorLRB '--------------------------------------------------------------Else Me.pnlMainBody.Height += Me.pIncrement 'adjust main panel size Me.pnlMoreDetails.Top += Me.pIncrement 'adjust additional details panel top End If End If End Sub Page –387– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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) '-------'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 Page –388– & & & & & & & Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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 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 you see in the samples, above. 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 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. You can also take advantage of this slick feature in your own applications by including the BuildImageListCode() method found in Black Book Tip # 49, Embedding Images Within Your Source Code, on page 513, which will build a full InitializeImageList() method customized to your own images. Just supply it with an ImageList control pre-filled with your selected images. When using the resulting InitializeImageList() method in your target applications, supply the method with the ImageList control to fill. Also, set the optional Replace parameter to False if you want to append, not replace any images in your ImageList: Page –389– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 19 Easily Recovering Crashed Menus and Toolbars under VB.NET Have you ever worked on a VB.NET project and “suddenly” a form’s Designer loads and our menu entries and/or toolbar buttons are dead in the water (well… this is usually after selecting the Ignore and Continue option when a form will not display, even though a data not found warning was reported, telling us the full path(s) to the data it cannot find, but we tend to forget about lamenting these annoying facts to friends)? Most who work on only simple, straightforward projects may be lucky enough to have never experienced it, but most of us who have found ourselves needing to reorganize a project as it grows have had it happen at least once. It typically occurs as we are experimenting with container folder assignments and fine-tuning the naming of resource objects that were already assigned to our application resources. I first noticed this issue being reported in VB2005, and it has happened to me personally under VB2008 and VB2010, and I expect it to face it again in VB2015. When reported on blogs, most moderators typically castigate the user for not backing up their work, or it being their own tough break, so deal with it. A few of them offer sympathy, but no one has offered any sort of solution. Granted, all seems, or is assumed lost. All those hours of work appear to have gone down the tubes. But even after your finally sigh, suck it up, and accept your fate, as you try to rebuild the menus, it will refuse to accept new objects with property names that you had previously given them, such as mnuFile, mnuEdit, and so on. Suddenly, we all start talking like pirates on September 19th (International TalkLike-a-Pirate Day); Aaarrrrr!!! DON’T PANIC As the late great Douglas Adams reminded us with the big bold-lettered “DON’T PANIC” message on his book, The Hitchhikers Guide to the Galaxy, there is absolutely no reason to, well… panic. And that is because all of your menu objects are still intact. What is missing, however, are the instructions that add those menu and toolbar items to your 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 is said, had always been why it happens in the first place. That part is easy because it is so bleeding obvious: Because WE shifted resources around or renamed them AFTER we had added references to these file items in our project resource file. This will consequentially confuse Dot NET, which will still expect these resources to have the same names and/or exist at their previous locations. Your 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-based reference filepaths to these resource files. The IDE recompiles this data into an internal binary any time it discovers resources have changed. This makes it more like an auto-compiled VB6 Resource Sournce File (.rc). Instead of sending programmers into a black spiraling abyss of despair, my thinking is that the IDE should have replaced the unfound image resources with their default image, and then warned them about that, or offered a browser to find the new location and/or name. Instead of just commenting them out, they prevent the existing code from working at all, totally eliminating the possibility of it crashing by deleting the menu constructor instructions that had caused the error to fire. It is nice that the IDE will toss up a rather benign warning about not being able to find a particular resource (refer to the top of this page), but after we, pressed by looming deadlines, stupidly (er… 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. Page –390– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben The reason Microsoft did this was because it seemed the best way to ensure all your form data was not lost, though, as I have argued, there are much less confusing or more recoverable ways of disabling the problem. Anyone who works on a complex project, especially one that will be commercially available, tends to change their icons and other resources a lot as they experiment with finding a better look and feel to the presentation of their masterpiece. This is exactly why you can edit a source object that is 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 your project resources in order to update them. You usually see a form go blank and then refresh as it updates to the altered resource data (the IDE keeps an eye on your resource files, and it will update the affected forms when it detects that a resource has a newer or different timestamp on it). DUPLICATING THIS ISSUE The easiest way to duplicate this issue, to get some experience in dealing with it without panic, is to create a form and place a simple menu on it, such as a File and an Edit entry with some sub-menu items, such as Exit for the File main menu entry, and perhaps Cut, Copy, and Paste for the Edit menu. Though not necessary, I tend to name such entries mnuFile, mnuFileExit, mnuEdit, mnuEditCut, mnuEditCopy, and mnuEditPaste (I do not like their default naming, such as FileToolStripMenuItem for File, but I must admit that it is accurate and helpful for most simple projects). Look at the Designer form (enable access to it in Solution Explorer by clicking the Show All Files icon) and select the form name with the Designer.vb extension, such as Form1.Designer.vb. Please do not bother wasting any of your precious time with an attack of nerves about peering into the dark catacombs of this file. Previous to VB2005, prior to the introduction of the Partial Class modifier that enabled us to have multiple classes declared under the same name, thus allowing multiple files to support the same class, all this data was actually stored in the main code body of the form’s class structure. Its only ‘protection’ back in those days was simply wrapping this designer-modified code within a simple, non-protected Region declaration that could be collapsed. Whoopie-do. If we examine this simple code (well, simple once we understand that what it actually does is simple), we see within the InitializeComponent() method, where each of our objects are first declared, such as MenuStrip1, which is our main menu object, and all of its items, such as mnuFile, mnuEdit, and so on: Me.MenuStrip1 = New System.Windows.Forms.MenuStrip() Me.mnuFile = New System.Windows.Forms.ToolStripMenuItem() Me.mnuFileExit = New System.Windows.Forms.ToolStripMenuItem() Me.mnuEdit = New System.Windows.Forms.ToolStripMenuItem() Me.mnuEditCut = New System.Windows.Forms.ToolStripMenuItem() Me.mnuEditCopy = New System.Windows.Forms.ToolStripMenuItem() Me.mnuEditPaste = New System.Windows.Forms.ToolStripMenuItem() Below that, we see where each of these objects are customized and bound together: ' 'MenuStrip1 ' Me.MenuStrip1.Items.AddRange(New System.Windows.Forms.ToolStripItem() {Me.mnuFile, Me.mnuEdit}) Me.MenuStrip1.Location = New System.Drawing.Point(0, 0) Me.MenuStrip1.Name = "MenuStrip1" Me.MenuStrip1.Size = New System.Drawing.Size(284, 24) Me.MenuStrip1.TabIndex = 0 Me.MenuStrip1.Text = "MenuStrip1" ' 'mnuFile ' Me.mnuFile.DropDownItems.AddRange(New System.Windows.Forms.ToolStripItem() {Me.mnuFileExit}) Me.mnuFile.Name = "mnuFile" Me.mnuFile.Size = New System.Drawing.Size(37, 20) Me.mnuFile.Text = "File" ' 'mnuFileExit ' Me.ExitToolStripMenuItem.Image = Global.WindowsApplication1.My.Resources.Resources.iexplore_14_5 Me.mnuFileExit.Name = "mnuFileExit" Me.mnuFileExit.Size = New System.Drawing.Size(152, 22) Me.mnuFileExit.Text = "Exit" ' Page –391– This is the same propeller photographed in 1986). "Two"}. 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.AddRange(New System.mnuEdit. which is not necessary in such cases). and moved them to the appropriate folders. Me.Text = "Cut" ' 'mnuEditCopy ' Me. it reported it could not find the location for the main form’s icon. It throws up a warning like the one shown on the first page of this article. 22) Me. but this is simply creating a new array.mnuEdit.Forms. Me. The first binds the mnuFile and mnuEdit objects to the MenuStrip1 main menu strip.Text = "Edit" ' 'mnuEditCut ' Me. Next. so I exited the IDE and tried to copy the offending icons back to their original locations. this is more difficult to recover from). Icons.Size(152.mnuEdit. hoping to fix the problem. and ran my code. I already had abundant folders. but if you choose Ignore and Continue… BOOM! Your menu that contained the altered data is missing! Once such damage is done. 22) Me. though my menus and toolbars seemed to come up OK.mnuEdit}.mnuFile. Page –392– . which bind submenu items to their parent menu. so before starting work on the code one day. any theory and speculation often mean nothing. fixed it in resources by deleting the missing icon place-holder and reloading it from its new location.mnuEditCopy.Enhancing Visual Basic .mnuEditCut. so it was used instead. When I went back in my app and brought up the main form.Size = New System.mnuEditCopy. 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. saved the changes. try to re-open your form.Drawing. More resource errors were reported. Now close the form design view and we will try to make this thing crash.Text = "Paste" Notice the three highlighted lines. where the removed sequential letters “AN” (TITANIC) actually revealed the large engraved letters “MP” (OLYMPIC) underneath.Size = New System.Drawing.Drawing. the damage had already been done (the application’s text-based Resources.Size = New System.Resx file was already pointing to the wrong source locations for the moved images).mnuEditCut. under which I had subfolders for Cursors.mnuEditCut.Name = "mnuEditCopy" Me.Name = "mnuEditCut" Me. These will become important later because it is these that tend to get killed off by the IDE (NOTE: I have heard from people questioning how this weird AddRange() parameter formatting works. 22) Me. I closed the warning. the menu bar and the toolbars were completely blank.Size(39. Oops! When I re-entered VB2010 and loaded my project.Size(152.Drawing. "One". However.ToolStripItem() {Me. a Modules folder. such as New ToolStripItem() {Me.mnuEditPaste. but not specifying a target variable.mnuEdit.mnuEditCopy. and a Resources folder. Also.mnuEditCut. but Titanic’s was. 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. such as Dim Ary() As String = {"Zero". like many of us do every day. several class group folders further stored under a general Classes folder. the Olympic had acquired starboard propeller # 401 from the Titanic when it damaged its own and its replacement was not ready. the Olympic.0 – David Ross Goben 'mnuEdit ' Me.Size = New System.mnuEditPaste}) Me. 20) Me. and then add those images as images to one or more of your menu entries (be sure to specify Project Resource File when you are assigning an image to a menu from the Add Image dialog). Me.NET Beyond the Scope of Visual Basic 6. Finally. and Images.mnuEditPaste.Name = "mnuEditPaste" Me. Go into the Solution Explorer and then rename one of the image objects you had loaded into your project resources (although you can simply delete it for this example. add at least one image or icon to your resources (not directly to the menu from local resources).Name = "mnuEdit" Me. The Icons subfolder was getting a bit stuffed (I wish we could sub-group our project resources). I used File Explorer to build several subfolders for the images to accommodate each toolbar and menu dropdown. such as Forms to hold my form files.mnuEditPaste.Text = "Copy" ' 'mnuEditPaste ' Me.DropDownItems.Size(152.mnuEditCopy.Windows. and hence.System. 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. 0)  Notice that there is no longer an Me.Bitmap.Text = "File" ' 'mnuFileExit ' Me.Size(284. the XML Value entry specified the resource data name (“data name="iexplore_14_5"”).mnuFileExit.Name = "mnuFile" Me.Drawing. The easiest solution. such as renaming it to “iexplorer. This is because the Resource.ResXFileRef.0.Image = Global. the warning of potential data corruption.Resources. This fixes the issue.Resources. System. For example. then we have more work ahead of us. and/or if the folder path is not where the file is now located.0.Enhancing Visual Basic . This value does not change when you use Explorer or Solution Explorer and move or rename the file.Text = "MenuStrip1" ' 'mnuFile ' Me. Edit this text entry as needed.0 – David Ross Goben Having had this happen to me previously. Please avoid exiting the . 24) Me.Location = New System.Drawing.AddRange() line preceding this one Me.png”).Size = New System. EASY.Drawing. but this does not update if you physically move the image file to another folder location using File Explorer.iexplore_14_5  The Source of the error in the first place Me.Drawing.MenuStrip1.png. and also my personal favorite because it keeps files named and located as I want them named and located. 22) Me.Forms. QUICK. It simply all came down to misplaced resource sources.MenuStrip1.Forms"> <value>.mnuFile.\Resources\iexplore_14_5. and edit it by hitting the View Code button on the Solution Explorer tool bar.mnuFileExit.Drawing”).Drawing. and it loses track of a referenced resource. however. So. the actual resource file is automatically updated when you exit the IDE.Name = "MenuStrip1" Me. Version=4.DropDownItems.Size(152.Size = New System.mnuFileExit}) Me. 20) Me.ResX file.My.NET IDE! Even if you choose not to save the changes you may have made to your files. and so on. just edit the folder path to reflect its new path. PublicKeyToken=b03f5f7f11d50a3a</value> </data> As you can see.Name = "mnuFileExit" Me.MenuStrip1.Drawing. or simply use File Explorer to restore the missing (deleted) object.MenuStrip1.Windows.AddRange(New System. which is from the same code we had looked at previously: ' 'MenuStrip1 ' Me. AND PAINLESS SOLUTIONS Problems arise when the editor tried to recompile the resource data (which it does more often than we might suspect).TabIndex = 0 Me. consider the previously-shown XML snippet. relative file location from the application folder (“. For example.Bitmap”). I knew I could fix this in a matter of minutes. if you had renamed the file. REVIEWING THE DAMAGE IN CASE WE IGNORED THE WARNING If we made the mistake of choosing Ignore and Continue.mnuFile. though I spent an afternoon researching how this happened in the first place.mnuFile. and so exiting can mean much more work for you to do later..Point(0.png”).0. By staying within the IDE with the application loaded. we might see something like the following. You can find this by selecting the Resources. thus inviting a crash when the resources get rebuilt.mnuFile.ResX file (located in the My Project Solution Explorer Folder).Resources.NET Beyond the Scope of Visual Basic 6. is to simply edit the offending line in the Resources. or simply doing a FIND search for the offending image path (such as “\iexplorer_14_5.\Resources\iexplorer_15_4.Drawing. simply replace the old filename shown there with its new name.mnuFileExit.mnuFileExit. though not any that is very difficult.WindowsApplication1.. Were we to look at our designer code for the damaged form. System.ToolStripItem() {Me.ResX file was assigned source locations for images when you placed them in the resources.MenuStrip1.png”.MenuStrip1. the type of item (“System. even if it is with temporary same-named objects of identical type.Size(37.Windows. Culture=neutral.Size = New System. the problem is as easy to fix as using File Explorer to correct the file path or file name to what was expected.Text = "Exit" Page –393– . the Namespace that supports it (“System. even after a problem was reported. In any case.mnuEditCut. we will select all of the Notepad data and replace all the designer code we had previously copied out.mnuEditPaste}) Me.mnuEditPaste. which would greatly simplify recovery. and but more often also the main menu items have lost their mojo. along with a comment warning.ToolStripItem() {Me. EASILY REPAIRING THE “DOOMSDAY SCENARIO” DAMAGE NOTICE: For this solution to work. which is the one that should add submenu items to it (I wish they would have simply commented the line out. we would insert a blank line under its heading. especially if we did not back our source code up as often as we should (…guilty!).MenuStrip1. and past it into it (Ctrl-V). that in comparing this code to the previous. No other code is ever altered.Size = New System. but this is likely not practical. because the Designer code often recreates itself.Name = "mnuEditCopy" Me. copy it (Ctrl-C).Size(152. You can “cheat” by renaming the offending files back to the expected name.Name = "mnuEditCut" Me.AddRange(New ToolStripItem() { }) Page –394– .AddRange(New System.Items. were we to then simply reinsert the missing “AddRange” lines (this applies equally to toolbars).Name = "mnuEdit" Me.Text = "Edit" ' 'mnuEditCut ' Me. however.DropDownItems.mnuEditCopy. 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. however. This will happen with every menu item that has at least one submenu item that has an unfound resource. Me. Once our edits are done. then start out by specify the main menu object. as demonstrated below.Size(152.Size = New System. just the first line of their definitions.Name = "mnuEditPaste" Me.mnuEdit}) More often. which was the first line under the MenuStrip1 comment header: Me. which the text below outlines how to recover.0 – David Ross Goben ' 'mnuEdit ' Me. which is why I wish they would just replace unfound images with their default image. Me.MenuStrip1. after correcting our missing resource errors. for our main menu.mnuEditCopy. Me. we see that there is sometimes only a single line that is missing.Size = New System. in typically more disastrous situations.mnuEditCut. Me.Drawing. or moving the files to their expected locations. 20) Me.Enhancing Visual Basic .Drawing.MenuStrip1.mnuEdit. because further submenus are in most-all cases still fully intact. even the “trashed” app will suddenly return to full working condition.mnuEdit. For example. you must still repair the unfound resources in the Resources.Size(39.mnuFile.mnuEditCut. I typically repair them in Notepad – it is just less tinkering with the interactive nature of the IDE.mnuEdit.mnuEditPaste.DropDownItems. even if that strategy was a user-selectable option in the IDE Settings).mnuEditCut.mnuEditPaste. What we want to do is take a look at the very first line of all menu items that in turn should have submenus.ResX first.Windows. resulting in the following line: Me. they are actually quite easy to reconstruct. I find errors only the main menu strip. But regardless of even that. Do a Select All (Ctrl-A) of the designer code. 22) Me.Items.NET Beyond the Scope of Visual Basic 6.Text = "Paste" Notice.Text = "Cut" ' 'mnuEditCopy ' Me.Size = New System.Forms.ToolStripItem() {Me. or simply remove the image assignment.Text = "Copy" ' 'mnuEditPaste ' Me.Size(152. It all depends on where the moved/renamed/deleted resources were assigned. but this will not restore the lines that will be missing from our designer code after we hit Ignore and Continue.Windows. However. bring up Notepad.mnuEdit.AddRange(New ToolStripItem() { }). MenuStrip1. we might not have the missing source lines.mnuEditCopy.Drawing. 22) Me. Sometimes. 22) Me.mnuEditCopy.Forms. and after that add .AddRange(New System.Drawing. AddRange(New ToolStripItem() {Me. we should always FIRST repair the location and naming of the presumed missing resources. better. not deleting them.mnuFile. After you have done it a time or two. to have named them with non-spaced versions of their Text entries. 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.mnuEdit. Me. add them to an error log file. rendering: Me. though such also includes a trailing “ToolStripMenuItem”. We would then add the required items following them.mnuEditCopy.DropDownItems. hopefully we are usually aware of what we named our main menu items. But we must also remember that if we had hit Ignore and Continue and lost some dropdown definitions. the artificial sweetener Aspartame is an accumulative toxin that can cause you to gain water weight)… Page –395– . and add mnuEditCut.AddRange(New ToolStripItem() {Me.DropDownItems. as shown above. In the example’s case.AddRange(New ToolStripItem() { })” below the mnuFile heading.mnuEdit. as stated earlier. It also reminded me that I should back my work up much more often than I do.mnuEditCut.AddRange(New ToolStripItem() {Me. Notice. though mind you that any submenu items that also have submenus have their items listed sequentially after them. though. In those cases. otherwise any repairs to dropdown items will again be lost. all we need to do is populate this array definition with the appropriate submenu objects. it should become a simple “Oh. you just as easily reconstruct them exactly as we had done to reconstruct our main menu entry.mnuEdit}) And suddenly all is well.mnuEditPaste}) CONCLUSION Although this was a simple example. in case you are not sure of exactly what you named them. Now.Enhancing Visual Basic . so we could add “Me. or. they are listed in an automatic dropdown option box.0 – David Ross Goben Now.mnuFile. so there is some intuitive skipping you might need to do. we would add mnuFileExit within the curly-braces for the mnuFile entry. Me. ironically. often the other submenus with dropdowns are also missing their initial “AddRange” entries. well…” moment.MenuStrip1.DropDownItems. but that would be because one of their submenu items also had a missing resource assigned to it.mnuEdit” within the curly braces (“{ }”) above: Me. However.DropDownItems.NET Beyond the Scope of Visual Basic 6. and the IDE was usually kind enough.mnuFile. this solution is just as simple and applicable for even the most complex menus and toolbars. It also reminds me that I should NEVER exit the IDE after such a disaster. to keep repairs fast and easy.mnuFileExit}) And… Me. followed by a quick repair. Me.mnuFile. all because we forgot to first repair the original problem.Items. and “Me. I simply add “Me. But. and mnuEditPaste to the mnuEdit entries. in cases we did not rename our menu items. In our example’s case.AddRange(New ToolStripItem() { })” under the mnuEdit heading. which are all listed following their parent menu item. 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. which are mnuFile and mnuEdit in our example’s case. which is why you often see developers doing such seemingly oxymoronic things. Me. mnuEditCopy. Nothing is worse than taking the time rebuild the usersource 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. if we could just convince Microsoft to simply comment the offending lines out. This is truly a source of frustration for other developers who are depending on that SelectionIndex property reflecting the displayed selection. However. but that does not address hooking and unhooking the system message queue and monitoring window messages required to monitor mouse movment and automatic item selection. Being aware that the selection index changes with mouse movement offers us the possibility for another solution… if only we can trap it. I have found that if I do an absolutely out-of-the-hat cheap parlor trick using a short-interval timer. which seems like a heck of a lot of work to make yourself go through just to detect which entry the mouse is currently hovering over. even though the item displayed within the TextBox of the ComboBox does not change. and it has been reported repeatedly to Microsoft as a bug (even though it actually is not). which it might not be. I can track and update data with the mouse movement with absolute ease. However. The first uses the DrawItem method. Besides.Enhancing Visual Basic . a keyboard selection. as I just said. Then. That is to say it does nothing (a stub is a method that simply returns upon invocation. once you see my simple little solution. having said that. you will have to write the DrawItem() event handler method so that you will be able to still view the contents of the ComboBox list. the SelectedIndex actually updates as the mouse moves over the dropdown list.NET Beyond the Scope of Visual Basic 6. and the second uses a simple timer. We can do this with the following methods: Page –396– . Unlike with a ListBox control. especially because the SelectedIndexChanged event does not fire when mouse move changes SelectedIndex. we should also initialize an external variable to the current selection when the ComboBox is opened. Naturally. Adding a Horizontal Scrollbar to a ComboBox DropDown List on page 640. that event only fires as a result of a click event on the ComboBox list. using it to detect when a selection has changed. Indeed. because. or if you change either the SelectionIndex property or the SelectedItem property in-code. which has an exposed TopItem property so that we know which index item is being displayed at the top of the list. you will find most people responding to someone wondering about how to do this with advice to obtain this index from the DrawItem method. Although it is relatively easy to use. I will provide two separate methods for picking up this hover selection index value. so if we want to track mouse movement for it. which is described in Black Book Tip # 59. even though we could send a message to the ComboBox to get its TopItem value. both of which are relatively easy to implement. it would be incumbent upon us to over-ride it with our own method that will do something useful. we cannot compute the index from the mouse position (see Black Book Tip # 10 on page 371). of course. you have to set the ComboBox’s DrawMode property to OwnerDrawFixed. which is why the selection rectangle displayed within the ListBox changes automatically with the mouse movement. mouse movement changing the SelectedIndex and SelectedItem does not cause the SelectedIndexChanged event to fire. which is just a stub. actually). you have to locate the Handle for that Listbox and monitor its mouse movement. To keep our code robust. It is doable using the GetComboBoxInfo() P/Invoke and a COMBOBOXINFO structure. the dropdown list of a ComboBox control is in fact a separate window (a ListBox. you will probably slap your forehead because it will suddenly appear to be so obvious. Acquiring the Hover Index using the DrawItem Technique To implement this method. In fact.0 – David Ross Goben Black Book Tip # 20 Tracking ComboBox Items under the Mouse Some get frustrated trying to determine the real-time index of the item the mouse is hovering over in the dropdown list of a ComboBox control. which is in fact the only method that does provide mapped coordinate changes. as otherwise it will be rendered blank because the DrawItem() event handler invoked by default will simply be the MyBase object’s version of that method. useful for disabling methods that you do not wish to be invoked). being a solution I had discovered on my own. it sure seems like an over-engineered solution just to get an index for the item within the ComboBox the mouse is hovering over. If you spelunk through the internet for alternative solutions. e.. which is simply to draw the contents of the index line provided.Graphics.Enhancing Visual Basic . brsh.NET Beyond the Scope of Visual Basic 6. and detect which item is selected '********************************************************************************* Private Sub ComboBox1_DrawItem(sender As Object. but this is usually almost useless for tracking indexes in a ComboBox because you cannot monitor MouseMove events when the mouse is over anything except the text field of the ComboBox. although you can in fact access this object with a little bit of code gymnastics to acquire its handle and then monitor its messages.Dispose() 'dispose of created resource If e.Index). once all this is in place. we will set our comboboxIndex value to the current selection index of the ComboBox. 2) Draw the line’s text. e As DrawItemEventArgs) Handles ComboBox1.ToString.Location) 'draw the current item's text to the ComboBox list brsh. because we usually want to do this in real time? Page –397– .DrawString(. Were you aware that you can still monitor the hover selection of the mouse? As indicated above. If you spelunk around MSDN. If comboboxIndex <> e. Acquiring the Hover Index using a Timer Another thing – suppose you do not want to bother with the trouble of implementing a DrawItem() event handler for the ComboBox. e As System. This was covered earlier on page 224..ForeColor) 'brush used for painting text e.DropDown comboboxIndex = Me. and the one below. Within our DrawItem() event handler. you can also invoke a method that should react when this index changes. .Index 'then update the tracking index '---------------------------------------------------------------------------'Here. although I had strengthened it by initializing the stored variable from within the DropDown() event handler. The trick is – how do we track it.SelectedIndex 'set to current selection when the combo-dropdown opens End Sub '********************************************************************************* ' Method : ComboBox1_ DrawItem ' Purpose : Draw each individual item in the ComboBox.DrawItem e. that although you can use this stored index value in your method. which is 1) Draw the field background. And that is the typical solution. we would invoke any immediate reaction to changes in the SelectedIndex value right from within the DrawItem method. comboboxIndex = e.Selected Then 'if the current item is the mouse hover selection. 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.. But however we do it. also an effective SeletedIndex-tracking monitor. if we are really sneaky. we are doing the minimal work necessary. for all intensive purposes. 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 you can see. and 3) keep track of the item that is currently selected by the mouse hovering over it.. or. Finally.State = DrawItemState. ComboBox1.0 – David Ross Goben Private comboboxIndex As Int32 'keep track of the item index '********************************************************************************* ' Method : ComboBox1_DropDown ' Purpose : Init selection index '********************************************************************************* Private Sub ComboBox1_DropDown(sender As Object. work quite well.EventArgs) Handles ComboBox1. which is technically.Index Then 'if the stored index does not match. 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. The dropdown list for a ComboBox is actually a separate ListBox object and.Index <> -1 Then 'if something is selected (-1 = none) With DirectCast(sender. however.Font. we can declare a method WithEvents and fire off that event. you will find a CB_GETTOPINDEX message that will pick up the TopIndex property of a ComboBox. 'Note. the SelectionIndex property actually changes as you move the mouse over an item. ComboBox) Dim brsh As New SolidBrush(. which will always highlight first.Bounds.DrawBackground() 'redraw combobox line's background (blank it out) If e. because the DrawItem event handler will be invoked as the mouse tracks over each item). when the ComboBox dropdown list opens.Items(e. ComboBox1.SelectedItem.ToString 'record the selection to the label End Sub '********************************************************************************* ' Method : cboUserDefined_DropDown ' Purpose : Init mouse index and reveal demo image when the dropdown opens '********************************************************************************* Private Sub Timer1_Tick(sender As System.Text = .Object. Finally.SelectedIndex 'save the current selection Me.ComboBox1. being 1/10th of a second.ComboBox1. e As System.Add("Sample Line " & Index.ComboBox1. Below that. even though.ToString) Next End With Me. if you wanted to or needed to.Label1.Items For Index As Int32 = 0 To 49 'fill the combobox with some simple data .Load With Me.EventArgs) Handles ComboBox1.EventArgs) Handles Timer1.ComboIndex = Me.Enabled = True 'turn the timer on End Sub '********************************************************************************* ' Method : ComboBox1_DropDownClosed ' Purpose : Stop the timer '********************************************************************************* Private Sub ComboBox1_DropDownClosed(sender As Object.ComboIndex = Me.Object.EventArgs) Handles ComboBox1. place a ComboBox control named ComboBox1. Next. you could also reference your saved variable value. and we will disable the timer when the dropdown closes through its DropDownClosed() event handler.Timer1.SelectedIndexChanged Me.Timer1.Timer1.EventArgs) Handles MyBase.Enhancing Visual Basic .SelectedIndex Then 'if we detected a change Me. e As System.SelectedIndex 'update the test index Me.NET Beyond the Scope of Visual Basic 6. although some people prefer to use a longer interval. which is perfect. add a Timer control named Timer1 on the form.ToString 'report the current indexed item End If End With Me.Object.Enabled = False 'disable the timer for now With Me.Text = Me. To demonstrate this simple technique.Enabled = True 'turn the timer back on End Sub End Class Run this code and you will notice that when you drop the ComboBox list down. e As System.SelectedItem.ComboIndex <>. but a delay much longer than 100 milliseconds can sometimes get to be a bit distracting.EventArgs) Handles ComboBox1.ComboBox1 If .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.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 System.DropDown Me.ComboIndex = .ComboBox1.Tick Me. start a new Windows Form project and place a Label Control named Label1 on the form.Timer1.Label1. The timer will default to an interval of 100 milliseconds. which means we will enable the timer when the dropdown opens through its DropDown() event handler.DropDownClosed Me.SelectedIndex 'save the selection index Me. add the following complete form code to your 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 System.0 – David Ross Goben My solution is to use a timer that will only be active when the ComboBox’s dropdown is open. Page –398– .SelectedIndex <> -1 AndAlso _ Me. the Label1 control will faithfully reflect the item under the mouse cursor as you move it. e As System. e As System. but if we do it a lot. which we may have over time grown numbly used to.Enhancing Visual Basic . contains two Int32 fields of data. just like POINTAPI. is that the Dot NET versions of these constructs are unrestricted abstract classes. like so: Dim Pnt As POINTAPI 'declare storage space for a POINTAPI structure Pnt. and a bonus property that we can use to set and retrieve the data using a Dot NET Point: '********************************************************************* '********************************************************************* ' Structure : POINTAPI ' Purpose : Provide simple Dot 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 Dot NET Point to it. but many think they are forced to assign its data to the POINTAPI structure before being able to use it in a P/Invoke.Location) Me. but must instead resort to declaring separate and cruder Win32 structures. a Point. as are the data footprints of a Dot NET Rectangle and a Win32 RECT.NET Beyond the Scope of Visual Basic 6. declared below End Sub Page –399– . methods and properties. The data footprint of a Dot NET Point and a Win32 POINTAPI are exactly the same. but limited in that they did not support methods and properties. What is more frustrating is when we have a Dot NET Point structure already defined. it can become a major pain in the tin can. you can still use them exactly as you did before with a P/Invoke.Location Friend Sub New(ByVal Point As Point) ' or: Dim myPnt As New POINTAPI(Me. What sets structures apart from the User-Defined Types of Visual Studio 6 and its predecessors. providing significant code reuse. which we can use to both create a new copy of the structure and initialize it with a Point structure or two integer values. In order to understand the general misconceptions that a great many developers have about these structures. and a Rectangle. but also appearing to confuse a lot of their users. just like RECT. so you can in fact use them! What we must embrace is that a Point and Rectangle structure under Dot NET is just that – a structure. largely because Dot NET Points and Rectangles do contain constructors. Getting the Point Consider the traditional POINTAPI structure that we may have used to death for countless P/Invokes: Friend Structure POINTAPI Friend X As Int32 Friend Y As Int32 End Structure 'X field 'Y Field This is typical of how we declare this structure. but with the bonus of various methods and properties to enhance their use. But that is a myth. Consider the following POINTAPI structure declaration that features two constructors. UserDefined Types were abstract classes. Internally.X = 10 'fill the POINTAPI structure with data Pnt. filling them from the Dot NET structures and pass the Win32 structures on to the P/Invoke. let us likewise enhance POINTAPI and RECT in a similar fashion. much as we may have done with mechanical regularity under Visual Studio 6 and its ancestors. and property. and so they are fully able to support constructors. Actually.0 – David Ross Goben Black Book Tip # 21 Greatly Simplifying P/Invoke Definitions of POINTAPI and RECT Structures Many developers have expressed frustration that when they need to resort to a P/Invoke to summon some obscure capability deep within the bowels of the operating system that requires either a Point or a Rectangle structure as a parameter. But even with all these new enhancements added to them. We then assign it to a field name and initialize it.Y = 25 This is OK if we do it only a time or two. contains four Int32 fields. methods. ie: Dim myPnt As POINTAPI = Me. but they think they cannot specify a Dot NET Point or Rectangle structure as a P/Invoke parameter.Point = Point 'take advantage of our own Point property. which defines the actual object. Top. To recap. which is how we can reference and use a new Structure that is only now being declared. and to reiterate. when we begin executing these methods or properties. and even right within the parameter list of the P/Invoke that we might be executing. iY) Friend Sub New(ByVal X As Int32. intrinsic declaration. X2.Point = Me. Left. if we wanted to. or Me. The methods. 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 compiler would assume that we are actually referencing our property. we can also introduce Width. 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.Y = value. we can commence declaring out POINTAPI structures on the fly. 17) Dim Pnt1 As New POINTAPI(10. will still have exactly the same data footprint size as the data from a structure declared without methods and properties.X = value. not its program code.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 Dot NET Point.Y) 'return data as a Dot NET Point End Get Set(value As [Point]) 'assign data from a Dot NET Point Me. and not confuse it with our property of the same name. For example: Dim NetPnt1 As New Point(40. because it is declared locally in scope and would otherwise take precedence over the outer. 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. Y1.X Me. and Y2 properties so that we can communicate with these different objects exactly the same. 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. prior to the code of a New method even being executed. in the program space set aside for regular program code.0 – David Ross Goben 'constructor to create a POINTAPI structure using two integer values. we can also assign two Dot NET Points to them.X = X Me. Height. however. which specifies Left. Bottom.Location. the data. an advantage of Dot 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 being used.NET Point type Get Return New [Point](Me.Point Friend Property Point As [Point] 'use [] to bypass declaration ambiguity and reference the base . are always stored separately. This also helps explains one of the biggest points of confusion to new programmers.Location = myPnt. Following is a typical RECT declaration: Friend Structure RECT Friend iLeft As Int32 Friend iTop As Int32 Friend iRight As Int32 Friend iBottom As Int32 End Structure As previously stated.Point Pnt2. ByVal Y As Int32) Me. Simplifying P/Invoke Rectangle Structures A traditional Win32 RECT structure declared for P/Invoke use contains 4 integer data elements. ie: Dim myPnt As New POINTAPI(iX. and Bottom pixel coordinates. Me. X1. and assign and retrieve Point values after it has been created.Point 'declare a new Dot NET Point structure 'declare POINTAPI structure with two integer values 'declare POINTAPI structure using a Dot NET Point 'assign a Dot NET Point from an existing POINTAPI structure 'assign POINTAPI data from an existing Dot NET Point 'create a new Dot 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. Right. Right. because data and code are stored separately. Page –400– . Y.Point = NetPnt1 Dim NetPnt2 As Point = Pnt2.Y = Y End Sub 'property to get/set the POINTAPI data via a VB.NET Beyond the Scope of Visual Basic 6. Apart from declaring such structures and assigning four integer values to them. Top. P/Invokes typically expects of rectangles the client coordinates of a region. consecutively.Enhancing Visual Basic .NET Point ie: myPnt. Also. On top of that.X. X. With this declared. 25) Dim Pnt2 As New POINTAPI(NetPnt1) NetPnt1 = Pnt1. If we did not do this. but they will still store their respective data in the specific way that is expected of them. 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._Bottom 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. and plenty of other properties to access and set the rectangle’s data._Top = value End Set End Property '-----------------------------------------------------------------------'return our right-most location '-----------------------------------------------------------------------Friend Property Right As Int32 Get Return Me._Left End Get Set(value As Int32) Me.Left 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. either as individual values.Enhancing Visual Basic .Left = value End Set End Property '-----------------------------------------------------------------------'get/set our top location as Y '------------------------------------------------------------------------ Page –401– . though feature-rich enhancement of the RECT structure._Top End Get Set(value As Int32) Me. by Point._Left = value End Set End Property '-----------------------------------------------------------------------'return our top-most location '-----------------------------------------------------------------------Friend Property Top As Int32 Get Return Me.0 – David Ross Goben Consider my rather long.NET Beyond the Scope of Visual Basic 6. providing various constructors to initialization them during declaration._Right 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.Top + value End Set End Property Page –402– .Left + value End Set End Property '-----------------------------------------------------------------------'return the height of our rectangle '-----------------------------------------------------------------------Friend Property Height As Int32 Get Return Me.Right .Right End Get Set(value As Int32) 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.0 – David Ross Goben Friend Property Y As Int32 Get Return Me.Right = Me.Top 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.Left End Get Set(value As Int32) Me.Enhancing Visual Basic .Me.Me.Bottom = value End Set End Property '-----------------------------------------------------------------------'return the width of our rectangle '-----------------------------------------------------------------------Friend Property Width As Int32 Get Return Me.Bottom End Get Set(value As Int32) Me.Bottom = Me.Top = value End Set End Property '-----------------------------------------------------------------------'get/set our left location as X1 '-----------------------------------------------------------------------Friend Property X1 As Int32 Get Return Me.Top End Get Set(value As Int32) Me.Top End Get Set(value As Int32) Me.NET Beyond the Scope of Visual Basic 6.Bottom . Location = tlLocation Me.Y End Set End Property '-----------------------------------------------------------------------'return the size of our rectangle '-----------------------------------------------------------------------Friend Property Size As [Size] Get Return New [Size](Me.Right.X Me. ByVal Y1 As Int32.NET Beyond the Scope of Visual Basic 6.Location = Location Me.Bottom = Y2 End Sub End Structure Notice in this example structure that it features many of the same properties as its Rectangle cousin.brLocation = brLocation End Sub '-----------------------------------------------------------------------'declare a RECT using Dot Net Point and Size items '-----------------------------------------------------------------------Friend Sub New(ByVal Location As Point.Size = Size End Sub '-----------------------------------------------------------------------'declare a RECT using 4 integer location values '-----------------------------------------------------------------------Friend Sub New(ByVal X1 As Int32.Width = value. ByVal Size As [Size]) Me.Top = value. Page –403– . Indeed.Top) End Get Set(value As Point) Me. ByVal Y2 As Int32) Me.Location Me. like the one I provided for POINTAPI.Bottom) End Get Set(value As Point) Me.Left = X1 Me. completely encapsulates its data and the user communicates with that data only through its methods and properties.Right = X2 Me. Me.X Me. Me. ByVal brLocation As Point) Me.Y End Set End Property '-----------------------------------------------------------------------'return the bottom-right location of our rectangle '-----------------------------------------------------------------------Friend Property brLocation As Point Get Return New Point(Me. this structure.Enhancing Visual Basic .Height = value.Width Me.Height End Set End Property '-----------------------------------------------------------------------'declare our rectangle using a Dot NET Rectangle '-----------------------------------------------------------------------Friend Sub New(ByVal NetRect As Rectangle) Me.Height) End Get Set(value As [Size]) Me.Left = value.Width.Left.Location = NetRect. ByVal X2 As Int32.Right = value.Bottom = value. Me.Top = Y1 Me.Size End Sub '-----------------------------------------------------------------------'declare a RECT using 2 Dot NET Points '-----------------------------------------------------------------------Friend Sub New(ByVal tlLocation As Point.Size = NetRect.0 – David Ross Goben '-----------------------------------------------------------------------'return the Top-left location of our rectangle '-----------------------------------------------------------------------Friend Property Location As Point Get Return New Point(Me. But simply remember this: you in fact do not need to use the POINTAPI or RECT structure anymore. methods.NET Beyond the Scope of Visual Basic 6. even with P/Invokes. By understanding the enhancements we have made to the POINTAPI and RECT structure. bringing them closer to their Dot NET Point and Rectangle cousins. and also provide some of the other features available to the Dot NET Point and Structure objects.Enhancing Visual Basic . and properties. but you can in fact use the Dot NET Point and Rectangle objects in their place. You could get your feet wet by expanding these structure’s capabilities and provide them with much more robust methods. one can begin to comprehend and embrace the real power of Dot NET software development with a sense of greater understanding. Page –404– .0 – David Ross Goben Hopefully you can use these structures as a launching point for your own adventures into declaring structures that feature constructors. we actually have 4 different options to choose from: 1. This is necessary because the sorter has no knowledge of your object’s structure and what part of it you want to base the sort on. and we want the sort to be based on that selected field. we do not really need to create a whole new class if we do not want to. The only thing we have to decide on is how we want to implement the sorting service. The final option simply allows us to perform either limited-range (partial) sorts and binary searches utilizing the class already suggested by option 2. but nothing nearly as complex. We need to provide something similar. then we do not need to do anything special. We can simply do nothing and use the List object’s default Sort() method right out of the gate if we are storing string data or scalar (numeric) values. and we want to perform the sort based upon just one of those fields (we can also perform more complex sorts involving multiple items within the object.Generic List types expose a Sort() method. but only if we really wanted to go through all that trouble). We can just invoke the Sort() method on our list. 2. you will see that they are referring to a Comparison function that the internal QuickSort sorting algorithm requires you to provide it in order for it to perform comparisons on your objects. For example: Dim MyList As New Collections. However. first consider the simple Contact class we use in our examples: Page –405– .NET Beyond the Scope of Visual Basic 6. we could just define a comparison function.Generic.Sort() 'sort the list using the default comparison methods For Each Item As String In MyList Debug. 3. For example. Before we can start with the solution. Option 1: Using the Default Sort() If we are storing strings or any scalar types that VB. If you refer back to Black Book Tip # 6 on page 356.Add("Judy") MyList. In fact. If you examine the error message.List(Of String) MyList.NET already recognizes.Print(Item) Next 'display the sorted contents of the list The above code would yield the following result in the Debug Output panel of the IDE: Bob Judy Ralph Timmy Option 2: Using Sort(IComparer(Of T)) Suppose we were storing within the list a class object that encapsulate numerous fields as its data.Enhancing Visual Basic .0 – David Ross Goben Black Book Tip # 22 Easily Sorting Strongly-Typed Generic Collections Lists Many developers get excited when then see that the Collections. We can alternatively declare a comparison function to provide this equity service. However. Grrrr… The solution to this issue is really rather simple. that excitement can often flip to frustration if it throws up error messages because they are trying to assign complex objects to them. to Generic Collections Lists.Add("Timmy") MyList. as opposed to simple strings or scalar values. We can declare a simple separate comparison class to provide the required equity services that is designed to access the appropriate fields in each object to check during the sorting process. Let us assume for now that we have an object class of type Contact that has a property called Name that is a string field. 4. you were shown a small class object that provided the comparison method required to properly sort any column on a ListView control.Add("Bob") MyList. All these options make doing sorts with Generic Collections Lists almost too easy.Add("Ralph") 'fill the list MyList. then 1 is returned. For example: Dim MyList As New Collections. then Y is assumed greater and -1 is returned.com> Judy <[email protected](Y.List(Of Contact) MyList.Length. or simply flip the result by multiplying it times a value of -1. Try writing a class to sort in reverse.Compare If X Is Nothing Then 'if X is Nothing. "Bob@Home. because X is Nothing and Y is not End If ElseIf Y Is Nothing Then 'Else X is not Nothing but if Y is Nothing.ToString) Next 'display the sorted contents of the list This provides a result of: Bob <[email protected]")) MyList. we will load our Contact list with some data. 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) ' <-. ByVal Y As Contact) _ As Integer Implements System. a string comparison is performed on their Name properties. Page –406– .eMail & ">" End Function End Class 'we will assume that text data is provided in both fields 'assign contact name 'assign contact email 'return the name and email Now. VB will automatically produce the Compare function heading below Friend Function Compare(ByVal X As Contact. 0 for equity is returned.eMail = eMail. If both exist. because one may in fact be longer than the other.Generic.Name. ByVal eMail As String) Me.Trim Me. Y > X. it is safe to declare the sorting object in case we will be using it again.Sort(Comp) 'define an instance of our comparison class 'sort the list using that instance For Each Item As Contact In MyList Debug. If X exists and Y does not. "[email protected]> Ralph <Ralph@Home. If X does not exist but Y does. We only check further if equity is detected.Trim End Sub Public Overrides Function ToString() As String Return Me.Sort(New ContactComparer)”.Name.0 – David Ross Goben Friend Class Contact Friend Name As String Friend eMail As String 'contact name 'contact email Friend Sub New(ByVal Name As String.NET Beyond the Scope of Visual Basic 6.com> Note also that if this is in fact a one-shot deal.. Return 1 'X > Y because Y is Nothing and X is not Else Select Case X. indicating that X is greater.IComparer(Of Contact). we will declare a small comparison class to perform the Sort() method’s comparisons on our object. However. that we did not really have to also declare the Comp object in order to sort the list.Add(New Contact("Bob".CompareTo(Y.Name & " <" & Me.. "[email protected]> Timmy <[email protected]) 'compare the strings if X and Y are not Nothing… Case 1 Return 1 'X>Y (reflect string's CompareTo result) Case -1 Return -1 'X<Y (reflect string's CompareTo result) Case Else 'X=Y Return X. but the check is only performed until the length of one or the other gives out.. but instead we could have simply sorted this list using “MyList. we compare the lengths of the strings.typing this.com")) MyList. To use it.Add(New Contact("Judy".. If both the X and the Y objects do not exist.Name = Name..Print(Item.Name.Length) 'return the comparison of their lengths if strings seem to match End Select End If End Function End Class That is all there is to this class. "[email protected](New Contact("Ralph". It will check to see if either of the objects exits. to get started. This is very easy to do. If Y Is Nothing Then 'and Y is ALSO Nothing. Return 0 'then they are equal Else Return -1 'otherwise.com")) MyList.com")) 'new list of type Contact 'fill the list Dim Comp As New ContactComparer MyList. so if a 0 result is detected.Enhancing Visual Basic .Add(New Contact("Timmy".Generic. com")) MyList.Print(Item. Return 1 'X > Y because Y is Nothing and X is not Else Select Case X.Length) 'return the comparison of their lengths if strings seem to match End Select End If End Function Implementation of this method is almost like using the class except that first.Name.Name.. "Judy@Home. Here we rename the method ContactCompare() and declared it right within the body of our code: Friend Function ContactCompare(ByVal X As Contact.0 – David Ross Goben Option 3: Using Sort(Comparison(Of T)) An alternative to defining a comparer class is to instead simply declare a comparison function and reference it when invoking the Sort() method.ToString) Next 'display the sorted contents Option 4: Using Sort(Int32.Add(New Contact("Timmy". The use of this sort method is to simply sort a portion of the list.Generic. leaving everything else alone..CompareTo(Y. ByVal Y As Contact) As Integer If X Is Nothing Then 'if X is Nothing. Comp) For Each Item As Contact In MyList Debug.Add(New Contact("Timmy".Add(New Contact("Judy". "[email protected]. Notice that the comparison method below simply pulls the Compare() function out of the previously mentioned class.NET Beyond the Scope of Visual Basic 6.com")) MyList. Int32. "Timmy@Home. because X is Nothing and Y is not End If ElseIf Y Is Nothing Then 'Else X is not Nothing but if Y is Nothing. Y > X.Name. Instead of doing a full sort. This comparison class is also useful if you want to perform a binary search (an example of a binary search will be demonstrated in a moment) within a limited window in a list collection and you will be expected to supply a comparison class to it if you are using non-default types. Return 0 'then they are equal Else Return -1 'otherwise. ContactComparer.Enhancing Visual Basic . consider this example: Dim MyList As New Collections. we obviously do not need to instantiation a comparison class.com")) Dim Index As Int32 = 2 Dim Count As Int32 = 2 Dim Comp As New ContactComparer MyList.Add(New Contact("Ralph".com")) MyList.List(Of Contact) MyList. To sort just the last two entries on our almost standard list of items.com")) MyList..Sort(Index.Sort(AddressOf ContactCompare) 'new list of type WinzBang 'fill the list 'sort the list through the referenced comparer function For Each Item As Contact In MyList Debug. and a count of the consecutive number of items to sort. Count.Add(New Contact("Judy".List(Of Contact) MyList.com")) MyList.ToString) Next 'new list of type WinzBang 'fill the list (entry 0) '(entry 1) '(entry 2) '(entry 3) 'start sort on Timmy 'include only Timmy and Bob in the sort 'create a new comparison class 'sort the list from Index for Count Items 'display the sorted contents Page –407– ..Add(New Contact("Bob". "[email protected]. and second. which produces the exact same results as the last example: Dim MyList As New Collections.. we simply reference our comparison method through the AddressOf operator.Add(New Contact("Bob". "Ralph@Home. "Timmy@Home. "Bob@Home. "[email protected](Y. it will start sorting on the 4th item in the list (the index is offset from zero).com")) MyList.Name) 'compare the strings if X and Y are not Nothing… Case 1 Return 1 'X>Y (reflect string's CompareTo result) Case -1 Return -1 'X<Y (reflect string's CompareTo result) Case Else 'X=Y Return X.Print(Item. and sort that and the following 4 items.com")) MyList.Add(New Contact("Ralph". Consider this example. Hence. if we specify an index of 3 and a count of 5. even though it uses our ContactComparer class outlined in Option 2. IComparer(Of T)) I save this method for last. you supply it with an index into the list where to start the sort. If Y Is Nothing Then 'and Y is ALSO Nothing.. placing the new object in the proper sorted location: Dim MyList As New Collections. and then insert it into the list if it is not found. "david.0 – David Ross Goben This example yields the following result.com> Page –408– [email protected]") Dim Index As Int32 = MyList. then the returned index will be the bitwise complement of the index of the next element that is larger than the search string. 'then insert it into the list: -Index-1 = insert point 'display the sorted contents Notice that when a negative value is returned. "Judy@Home. then in the upper half.Insert(-Index . Note.com> Timmy <[email protected](Of Contact) MyList. eliminating the search range by half each time until it or the place where it should be inserted is found. "Timmy@Home. If the value is less than it. Performing a Sort and a Binary Search If you want to insert data sequentially into a list. except that you would also include a search object and it returns the index of the match found.1.com> Bob <[email protected](NewItem. then the search continues by doing the same in the lower half of the array.. and all without the fuss and muss of doing it yourself. but you 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. that if an exact match is not found. A binary search finds the position of a target value within a sorted array incredibly fast. I invoke the sort. "Bob@Home. which is invoked much as we will below for Sort().Add(New Contact("Judy". NewItem) End If For Each Item As Contact In MyList Debug.NET Beyond the Scope of Visual Basic 6. sorting only the Timmy and Bob entries: Ralph <[email protected]> Timmy <Timmy@Home. Consider the following.Sort(Comp) Dim NewItem As New Contact("David".com")) 'new list of type Contact 'fill the list (entry 0) '(entry 1) '(entry 2) '(entry 3) Dim Comp As New ContactComparer MyList.com")) MyList.com")) MyList. this means that a match was not found. or if it is greater.com> Judy <Judy@Home. and then invoke BinarySearch(). we simply have to flip the negative result’s sign and then subtract 1 from it (-Index – 1).goben@gmail. however.com> Judy <Judy@Home. For me. you first have to sort it to get it in order.com")) MyList.Add(New Contact("Timmy".com> David <david. Comp) If Index < 0 Then MyList. the negative value is. search for a new object using a binary search. This yields the following result: Bob <[email protected]) Next 'define an instance of our comparison class 'sort the list using that instance 'create an object to search for 'find where to insert it. if it does not already exist 'if it was not found in the list. What this really means in plain English is that if you receive a negative result.Print(Item. and then invoke a built-in binary search to find where to insert it. "[email protected]> I will leave you to explore the finer points of using selective BinarySearch() method on your own. where we will sort the list.Add(New Contact("Ralph".ross.Enhancing Visual Basic . as stated earlier the bit-wise complement (AKA 2’s Complement) of the index of the item that is larger than the tested item. It begins by comparing your value to that at the middle of the array.Generic. to get the actual index to insert the new item at from this result.. Again.com> Ralph <[email protected](New Contact("Bob". What is even more apparent is if you place labels on the form that are supposed to have a transparent background. Normally we can create our own Graphics object as easily as “Dim eg As Graphics = Me. archaic graphics capabilities. is really quite simple. just like some impressive parlor trick. e. 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.Paint Static Processing As Boolean = False 'flag to eliminate recursive invocations If Not Processing Then 'if following code is not already running. down to black. but that looked a bit grainy on the larger and more modern screen displays. like my 23-inch digital monitor). which starts at 255. which will happen if you perform other paint operations that will draw on top of the form background that was just dithered. If you have no other painting tasks..CreateGraphics”. consider the following Paint() event that I cooked up. They tend to lose that transparent effect using a different DC during a paint event. However.NET Beyond the Scope of Visual Basic 6. For example. We just draw a series of horizontal colors from one color down to black across the screen. or near black at the bottom? It is a really cool effect. it actually looks amazingly cool.Graphics. By setting the Red and Green color Page –409– . I spiffed it up using enumerators and computed a more controlled transition on any screen size (the VB6 scaled the screen as though it was consistently 1600 twips high. Just recently I upgraded it to VB.0 – David Ross Goben Black Book Tip # 23 Dithering a Form’s Background Have you ever seen forms where their background smoothly transition from a color. and each drops 1 down to 0 from the previous. and even better. We must keep in mind that for a 32-bit color. we will also need to pass the paint method’s Graphics object (the form’s Device Context) to our dithering method. such as Blue.Blue) 'dither the form background from Blue down to black '------------------------------------------'Other form-painting methods go here. which is 800 pixels. Also. 'then the Processing flag and this If-block wrapper are really not needed. you will find your Paint() method recursively re-invoking itself because you actually used a different device context to over-write various portions of the display. Even with just that. and it worked great. we can nip recursive invocations right in the bud. but if you use a different DC to perform various paint operations. The process of dithering. In 1998 I wrote a VB6 method to perform this task. if we will be performing other painting chores aside from dithering. e As PaintEventArgs) Handles Me.Enhancing Visual Basic . although it sounds mysterious and complicated. all we need to do is to pass on the Graphics object. Processing = True 'set the processing flag Dither(Me. which you should use as a model for your own dithering operations: '********************************************************************* ' Method : Form1_Paint ' Purpose : Draw background '********************************************************************* Private Sub Form1_Paint(sender As Object. where we will draw the form’s background. even with its clunky. Because we invoke this method from the Form’s Paint() event handler. to fix this. and in most cases this works fine.. '------------------------------------------Processing = False 'reset the processing flag before leaving End If End Sub By using the Processing Boolean flag.NET. DitherColors. it is really easy to do. each of the 3 emittive color values (not counting the Alpha component) has only 256 (0-255) values available. it is also a very good idea to fully obstruct any possible recursive invocations to our Paint() event. Blue = 255 cBlue = 255 Case DitherColors. Blue = 0 Case DitherColors.Yellow cRed = 255 'full Yellow: Red = 255. Green = 0.Magenta cRed = 255 'full Magenta: Red = 255. Optional ByVal Intensity As Int32 = 1) Dim tHeight As Int32 = Frm.Green cGreen = 255 'full Green: Red = 0. if you 'are using more than one form graphics object during the same process.NET Beyond the Scope of Visual Basic 6. because Red=0. they can conflict and 'cause the application to hang.White cRed = 255 'full white: Red = 255. Green = 0.Height Dim tWidth As Int32 = Frm.Enhancing Visual Basic . Blue = 255 cBlue = 255 Case DitherColors. e. Green = 255. Blue = 255 Case DitherColors. 'because you may be painting other things to the form within the form's Paint(). '********************************************************************* Friend Enum DitherColors As Int32 Red Green Blue Yellow Magenta Cyan White End Enum Friend Sub Dither(ByVal Frm As Form. Green = 0.Cyan cGreen = 255 'full Cyan: Red = 9. and by the time we get to the bottom. Green = 255.Paint ' Dither(Me. Optional ByVal ClrValue As DitherColors = DitherColors.ClientSize. the display will start at full blue at the top. Green = 255. Blue = 0 cGreen = 255 Case DitherColors.Blue. Blue = 255 cGreen = 255 cBlue = 255 Case Else 'default to Blue cBlue = 255 End Select '--------------------------------------------------- Page –410– .ClientSize. ByRef eGraphics As Graphics. Following is my Background Dithering Module: Option Explicit On Option Strict On Module modDitherBackground '********************************************************************* ' Dither: The Dither() method dithers a form background.Abs(Intensity) + 2 'compute the intensity value (set actual minimum to at least 2) If Intensity > 10 Then Intensity = 10 'but do not let it go above 10 End If '--------------------------------------------------Dim cRed As Int32 = 0 'init RGB base color flags Dim cGreen As Int32 = 0 Dim cBlue As Int32 = 0 Select Case ClrValue 'set used color values to 255 as needed Case DitherColors.Width 'height of the form's client area 'width of the form's client area Intensity = Math.0 – David Ross Goben values to zero and setting the Blue color value to 255.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. Blue = 0 Case DitherColors.CreateGrraphics(). Green=0. and Blue =0 is black. event. blue ' down to black by default. e As PaintEventArgs) Handles Me. For example: ' 'Private Sub Form_Paint(sender As Object. Green = 255.Red cRed = 255 'full Red: Red = 255.Blue cBlue = 255 'full Blue: Red = 0. ' 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. it will become black. From your form's Form_Paint ' event. you can do: Dither(Me) ' ' Optional Intensity 0-8. For example: Dither(Me. tWidth.Graphics. Further. intLoop + Lp) Next Pn..Drawing.. 0..Cyan..Dispose() 'release pen resource Next End Sub End Module To use it.intLoop \ Intensity 'compute Red intensity If tRed < 0 Then tRed = 0 'if too low.Drawing.Color. tGreen = cGreen . you provide it with the form that you want to dither the background for. tRed = cRed . the default to 0 End If End If If cGreen <> 0 Then 'if Green is defined.intLoop \ Intensity 'compute blue intensity If tBlue < 0 Then tBlue = 0 'if too low. intLoop + Lp. you can specify a primary color to dither from..1 'process vertical list of line rows to define eGraphics. as already demonstrated.NET Beyond the Scope of Visual Basic 6. and you can also adjust the intensity.. and the e. tGreen.FromArgb(tRed. tBlue)) For Lp As Int32 = 0 To htOffset . e.Graphics object that is supporting the background paint. the default to 0 End If End If 'create pen to draw with Dim Pn As New System. tBlue = cBlue .intLoop \ Intensity 'compute gree intensity If tGreen < 0 Then tGreen = 0 'if too low.0 – David Ross Goben Dim tRed As Int32 = 0 Dim tGreen As Int32 = 0 Dim tBlue As Int32 = 0 'init RGB pen creator color indexes Dim htOffset As Int32 = tHeight \ 256 'determine vertical pixels to assign to each step 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. the default to 0 End If End If If cBlue <> 0 Then 'if Blue is defined.Pen(System.Enhancing Visual Basic . such as Red or Green. 0) 'dither the form background from Cyan down to black Page –411– .DrawLine(Pn. within your paint event. DitherColors. Length) HasSelect As Boolean = CBool(TB. Copy. because were I to throw up a menu by intercepting the MouseDown event and display a context menu if the user selected the right-mouse-button. copy. Copy.NET Beyond the Scope of Visual Basic 6.txtMessage. I have seen lots of grumbling on web blogs and forums. such as cut. you are still able to toss up your own context menu by intercepting right-mouse-button-down. and implement UNDO using its own Undo() method. The problem. I determined that the culprit was its ContextMenuStrip property (both a TextBox and a RichTextBox sport a ContextMenu property to support old-style menus. Paste. so I would not normally offer that as a solution. my menu would pop up. you are able to bring up a default pop-up menu by right-clicking the TextBox. if you wish to continue defining on-the-fly context menus that way. This default pop-up context menu was added to the . one thing you should also consider is the benefit of enabling and disabling context menu items as needs require. The additional Undo option can be checked by the CanUndo property of the TextBox itself. paste. Typically. these shortcuts are handy enough to keep active. I had once also been foiled.ReadOnly AndAlso HasSelect CanPaste As Boolean = Not TB. delete. What happens if the TextBox is empty? What happens if it is presently tagged ReadOnly until some Edit mode is set? What happens of there is no text selected? You can test for all these things by defining just a few simple fields: Dim Dim Dim Dim Dim TB As TextBox = Me. however. and then re-enable the TextBox (fortunately. and Paste. You are able to disable the default context menu by assigning a new ContextMenuStrip object there. you might as well go all-out and throw in a little extra functionality to add some pizzazz to your context menu. and select all.0 – David Ross Goben Black Book Tip # 24 Designing Intelligent Context Menus for TextBox and RichTextBox Controls With a TextBox on your form. And yes. one moderator suggested that on a right-click that you disable the TextBox.NET repertoire for all languages because most developers wanted a simple auto-provided context-menu interface so they would not have to construct a simple context menu for every TextBox they added to their forms to provide the user-expected editing services. If it is not Nothing. And therein lays our solution. However.SelectionLength) CanRemove As Boolean = Not TB. After some quick research. but then that darned default menu would pop up right over the top of it.ReadOnly AndAlso Clipboard.txtMessage HasText As Boolean = CBool(TB. the interface will assume that a custom menu is in place and so it will not display its default menu.Text. because you are creating a new context menu. Even on Microsoft’s own VB Forum. people just want to throw up a context menu that minimally features Cut. NOTE: You can also disable the default context menu by setting the ShortCutsEnabled TextBox property to False.Enhancing Visual Basic . Or. I feel their pain.ContextMenuStrip = New ContextMenuStrip 'disable the default context menu with an empty one With just that. after all. however.ContainsText(TextDataFormat. whether a ContextMenuStrip of your own design. display your custom menu. a RichTextBox does not have this issue). but that is simply because this property was already set to Nothing by default. and Select Line. seems to be when you want to display your own context menu. Setting the ContextMenuStrip property to Nothing does exactly that – nothing. Page –412– . empty one. or even if it is just a blank.Text) 'get a simpler reference to the textbox 'True if text is present 'set TRUE if selection exists 'true if we can remove existing data 'set true if the clipboard contains data With these few Boolean fields we can enable or disable the above menu options Cut. all frustration is alleviated. However. Select All. For example: Me. and a ContextMenuStrip property to support more advanced menu interfaces). Delete. New EventHandler(AddressOf Undo_Click)). which we will list afterward).Enabled = CanPaste cMenu.. it is much easier to use what I have locally defined previously that do not need P/Invokes.Copy).Items.Length) 'True if text is present Dim HasSelect As Boolean = CBool(TB.MouseDown Me. Me. We will save this position in a Point structure named ContextLocation.Images(Images.SelectionLength) 'set TRUE if selection exists Dim CanRemove As Boolean = Not TB.NET Beyond the Scope of Visual Basic 6.Delete). and right within our context menu code.imgList. Me.Images(Images.imgList. we will actually disable the default context menu.imgList = New ImageList 'instantiate an ImageList InitializeImageList(Me. just to be fancy.Images(Images.Add("Cut (Cntrl-X)". Me. Me..Enabled = CanRemove cMenu. These messages are: Private Private Private Private Private Private Const Const Const Const Const Const WM_CUT As Int32 = &H300 WM_COPY As Int32 = &H301 WM_PASTE As Int32 = &H302 EM_UNDO As Int32 = &HC7 EM_CANUNDO As Int32 = &HC6 EM_REPLACESEL As Int32 = &HC2 'Copy selected data to the clipboard and remove from the textbox 'Copy selected data to the clipboard 'Paste data from the clipboard 'UNDO an edit 'returns non-zero if data can be undone 'replace selected data with new text But even so. And.Add() method returns a reference to the added MenuItem that you could capture and use to 'enhance the item even more.Paste). New EventHandler(AddressOf Paste_Click)).Text) 'set true if the clipboard contains data 'The ContextMenuStrip. New EventHandler(AddressOf Copy_Click)).Add("Paste (Cntrl-V)".Items.Forms.ContextMenuStrip = New ContextMenuStrip 'disable the default context menu End If Dim TB As TextBox = DirectCast(sender.Enabled = TB.Cut). TextBox) 'get a simpler reference to the textbox Dim HasText As Boolean = CBool(TB. or if they double-click the text. If Me. there are P/Invoke messages already defined that you can invoke in order to perform these checks by sending a message to the TextBox control and collecting the result.ReadOnly AndAlso HasSelect 'true if we can remove existing data Dim CanPaste As Boolean = Not TB.Enabled = HasSelect cMenu. I like to also feature a Select Line option.. Dim cMenu As New ContextMenuStrip 'declare a new context men cMenu. With just that.txtMessage. and how to select the entire line when the user either chooses the Select Line context menu option.ReadOnly AndAlso Clipboard.Add("Copy (Cntrl-C)".Enhancing Visual Basic .Items. Note: the weird spacing is to adjust text spacing between the command and properly 'aligning the text control’s Cntrl-key shortcut..MouseButtons.Items.Undo).Enabled = CanRemove cMenu.Images(Images. if needed.Text.imgList. New EventHandler(AddressOf Delete_Click)).0 – David Ross Goben Granted.CanUndo Page –413– .Right Then 'if the mouse-down was with the right button. e As System.Items.Button = Windows.Images(Images.txtMessage.imgList) 'load ImageList with images End Sub '********************************************************************************* ' Method : txtMessage_MouseDown ' Purpose : Pop up edit menu for textbox '********************************************************************************* Private Sub txtMessage_MouseDown(sender As Object. e As MouseEventArgs) Handles txtMessage. Here is the complete code to support a TextBox named txtMessage: Public Class Form1 '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private ContextLocation As Point 'context menu top-left corner location Private imgList As ImageList 'image list to store context menu images '--------------------------------------------------------------------------------'image imdex within ImageList '--------------------------------------------------------------------------------Private Enum Images As Int32 Cut Copy Paste Delete SelectAll SelectLine Undo Redo End Enum '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '********************************************************************************* '********************************************************************************* ' Method : Form_Load ' Purpose : Initialize ImageList for use im form '********************************************************************************* '********************************************************************************* Private Sub Form_Load(sender As Object. Me. but this requires that we save the cursor position when the mouse is pressed down. and enable or disable our several options as needed (we will still need to define support methods.Add("-") cMenu. New EventHandler(AddressOf Cut_Click)). Me.ContainsText(TextDataFormat. This way our Select Line support method can easily compute the line clicked on.Location 'save selection location if this is not a right-click If e.imgList. we have everything we need to construct and display a context menu when we right-click a TextBox.Add("Undo (Cntrl-Z)".Items.ContextMenuStrip Is Nothing Then 'if the default context menu can be triggered.EventArgs) Handles Me.Load Me.ContextLocation = e.Items.imgList.Add("Delete (Del)". Text.Cut() 'cut the selected data to the clipboard End If End With End Sub 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : Copy_Click ' Purpose : Support context menu COPY in a RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub Copy_Click(ByVal sender As Object..SelectionLength = .txtMessage If CBool(.Position) 'show the menu with top-left at the mouse pointer location End If End Sub 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : Paste_Click ' Purpose : Support context menu PASTE in a RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub Paste_Click(ByVal sender As Object.SelectAll). .Length) Then 'set select location from mouse-down location ..Length + 1 'then this was the last line..Add("Select All (Cntrl-A)".Items.NET Beyond the Scope of Visual Basic 6. New EventHandler(AddressOf SelectAll_Click)). New EventHandler(AddressOf SelectLine_Click)).Enabled = HasText AdjustContextMenuItemTransparency(cMenu) 'adjust context menu image transparency (See BlackBook Tip # 25) cMenu.Add("-") cMenu.1 .txtMessage If CBool(..Focus() 'make sure focus stays here End With End Sub 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : SelectLine_Click ' Purpose : Select whole line in a RichTextBox ' NOTE : We do double-duty here by also supporting a Double-Click on the TextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub SelectLine_Click(sender As Object.Enhancing Visual Basic . .Show(Cursor.Length 'and select everything .SelectionStart = 0 'set the selection to the start.SelectionLength) Then 'if there is something to Cut. ByVal e As EventArgs) Me.Images(Images.SelectionStart = lineStartingCharIndex 'set select to the start of the clicked line . e As EventArgs) Handles txtMessage.SelectionLength = lineLength 'select entire line . ByVal e As EventArgs) With Me.Items..Images(Images.txtMessage.0 – David Ross Goben cMenu.GetCharIndexFromPosition(ContextLocation) 'get the line the cursor location is on Dim lineIndex As Int32 = .SelectedText = vbNullString 'blank out selection End Sub 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : Undo_Click ' Purpose : Undo the last edit made in the RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub Undo_Click(ByVal sender As Object.Items..Text. Me.Add("Select Line (Dbl-Click)".Text.imgList.SelectLine).GetFirstCharIndexFromLine(lineIndex) 'get the start of next line Dim lineNextStartingCharIndex As Integer = .imgList.GetLineFromCharIndex(.Copy() 'copy the selected data to the clipboard End If End With End Sub 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : Delete_Click ' Purpose : Support context menu DELETE in the RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub Delete_Click(ByVal sender As Object. ByVal e As EventArgs) With Me. ByVal e As EventArgs) Me.Undo() 'UNDO an edit End Sub 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : SelectAll_Click ' Purpose : Support context menu SEDLECT ALL in a RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub SelectAll_Click(ByVal sender As Object.lineStartingCharIndex .txtMessage.txtMessage. ByVal e As EventArgs) Me.SelectionStart) 'get the start of this line Dim lineStartingCharIndex As Integer = .MouseDoubleClick With Me.GetFirstCharIndexFromLine(lineIndex + 1) 'if this value is negative..txtMessage . so fake it End If 'compute line length Dim lineLength As Integer = lineNextStartingCharIndex ..Focus() 'make sure focus stays here End If End With End Sub Page –414– . .Enabled = HasText cMenu. Me. If lineNextStartingCharIndex < 0 Then lineNextStartingCharIndex = .SelectionStart = .txtMessage If CBool(.SelectionLength) Then 'if there is something to Copy. ByVal e As EventArgs) With Me.Paste() 'paste text or over-write selection End Sub 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : Cut_Click ' Purpose : Support context menu CUT in a RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub Cut_Click(ByVal sender As Object. Were we to go to the trouble to cast it to a ToolStripMenuItem. A RichTextBox is just as easy to work with. such as “mItem.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. Copy. Because it is so similar to a TextBox.MemoryStream = New IO. and then returns a reference to a ToolStripMenuItem.NET Beyond the Scope of Visual Basic 6. UnDo. the ReDo_Click method. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub InitializeImageList(ByRef imgList As ImageList.FromStream(memStream) 'construct image from stream data memStream. The context menu’s Items collection’s Add() method constructs a ContextMenuItem object from the information we provide it. as opposed to adding individual images.Images. SelectLine. or even an image. Delete.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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.AddStrip(Img) 'add this as Images 0 . because all the other methods are exactly the same: Page –415– . 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. SelectAll.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. Note also that we are building an ImageStrip and storing it in the ImageList. we can set it up to paste plain text if that was saved in the clipboard.X”. and the updated Paste_Click method. not appending images imgList. and also as detailed in Black Book Tip # 49.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image.ImageSize = New Size(16. Notice that we are enabling/disabling menu options as we add them to the context menu. Using. plus it features a CanRedo property and a Redo() method. An ImageStrip is just a series of uniformly-sized images arranged shoulder-toshoulder. Creating. Optional ByVal Replace As Boolean = True) If Replace Then 'if we are filling. as shown above.Control Or Keys.Enhancing Visual Basic . Paste.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO. set the Replace ' : parameter to FALSE. Even more. I will list only the MouseDown event. as we demonstrated previously. but we also do not want to redefine the editing shortcuts because they are already defined with viable support methods. if that is what was saved in the clipboard. This enhancement to Black Book Tip # 49 is detailed in Black Book Tip # 51.ShortcutKeys = Keys. Embedding Images Within Your Source Code on page 513.Clear() 'initialize image list imgList. we could then add a hot key to it instead by assigning values to its ShortCutKeys property. and avoid pad spacing. 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. RichText Format text if that us what was saved.Images. We can immediately use that returned reference by applying our enablement options using its properties. and Embedding ImageStrips with Ease on page 544. Enabled = HasSelect cMenu.Images(Images.Images(Images.Enabled = TB.imgList.Enabled = HasText cMenu.Add("Copy (Cntrl-C)".Enabled = CanPaste cMenu. sysFont) & "(Cntrl-A)". maxWidth.Length) 'True if text is present Dim HasSelect As Boolean = CBool(TB.Add(PadTextString("Cut"..MenuFont 'simplify common reference Dim maxWidth As Int32 = TextRenderer.NET Beyond the Scope of Visual Basic 6.Items.Enhancing Visual Basic . maxWidth. Me.. New EventHandler(AddressOf Copy_Click)).ContainsData(DataFormats. Me. New EventHandler(AddressOf SelectAll_Click)).ContainsData(DataFormats.Undo). New EventHandler(AddressOf Cut_Click)). sysFont) & "(Cntrl-Z)". Me.Forms.SelectLine). Dim TB As RichTextBox = DirectCast(sender.Items. which was why I was padding the text in the first place).imgList.Images(Images. sysFont) & "(Cntrl-Y)".Rtf) OrElse Clipboard.Images(Images.Add("Redo (Cntrl-Y)".SelectAll).Rtf)) ElseIf Clipboard.Images(Images.Cut).Enabled = CanRemove cMenu.Enabled = HasText cMenu.Text. New EventHandler(AddressOf Redo_Click)).Items. New EventHandler(AddressOf Undo_Click)).ContainsText(TextDataFormat.Text) OrElse Clipboard. New EventHandler(AddressOf Delete_Click)).Images(Images.Resources.Items. sysFont) & "(Cntrl-V)".Items.Enabled = TB.imgList.Paste).Enabled = CanPaste cMenu. Me.Resources. Me.Copy).Bitmap)) End If End With End Sub 'if we can paste RTF data.Add("-") cMenu.SelectAll.imgList. New EventHandler(AddressOf Redo_Click)).imgList.Items.Enabled = CanRemove cMenu. Me.Enabled = CanRemove cMenu. like so: Dim cMenu As New ContextMenuStrip 'declare a new context men Dim sysFont As Font = SystemFonts. sysFont) & "(Dbl-Click)".delete.Images(Images.Paste(DataFormats..Add("-") cMenu. maxWidth. Me..Right Then 'if the mouse-down was with the right button.Delete). maxWidth. For example.Text)) ElseIf Clipboard. New EventHandler(AddressOf SelectAll_Click)).Rtf) Then .SelectionLength) 'set TRUE if selection exists Dim CanRemove As Boolean = Not TB. ByVal e As EventArgs) Me. My. New EventHandler(AddressOf Undo_Click)). adding an actual ShortCutKey of Del is a can of worms I try to avoid. New EventHandler(AddressOf Paste_Click)).imgList. maxWidth.GetFormat(DataFormats.Add("Delete (Del)".Bitmap) Then .ContainsText(TextDataFormat. Me.Images(Images.imgList. sysFont) & "(Cntrl-X)".Paste(DataFormats. sysFont) & "(Del)".Items.Images(Images.Images(Images. check for an image 'and paste it if we have it 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : Redo_Click ' Purpose : Redo the last edit made in the RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub Redo_Click(ByVal sender As Object. so paste it 'or finally. sysFont).Add("Select Line (Dbl-Click)".Position) 'show the menu with top-left at the mouse pointer location End If End Sub 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : Paste_Click ' Purpose : Support context menu PASTE in a RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub Paste_Click(ByVal sender As Object.Add(PadTextString("Copy".Enabled = HasSelect cMenu.CanUndo cMenu.imgList..Items. ByVal e As EventArgs) With Me.Show(Cursor. My.Images(Images. New EventHandler(AddressOf Paste_Click)). sysFont) & "(Cntrl-C)". Me. 'we can.Enabled = CanRemove cMenu.Enabled = TB.imgList. its PadTextString() function would eliminate the padding we inserted (BTW. maxWidth. we could change the above lines to the following using that function.Redo).Paste).Add("Paste (Cntrl-V)".Images(Images.Button = Windows.Items.Items.CanRedo cMenu.Add("Undo (Cntrl-Z)".imgList.CanUndo cMenu.Add(PadTextString("Redo".MeasureText("Select Line". New EventHandler(AddressOf Delete_Click)).Items. maxWidth.ReadOnly AndAlso HasSelect 'true if we can remove existing data Dim CanPaste As Boolean = Not TB.Add(PadTextString("Select Line".Items.Redo() End Sub NOTE: If you look to Black Book Tip # 44 on page 496.Text) Then .Enabled = HasText AdjustContextMenuItemTransparency(cMenu) 'adjust context menu image transparency (See BlackBook Tip # 25) cMenu. Dim cMenu As New ContextMenuStrip 'declare a new context men cMenu.GetFormat(DataFormats.Cut). Me.Add("-") cMenu.Add("-") cMenu.SelectLine.Items.Resources.CanRedo cMenu.MouseDown Me.Add("Cut (Cntrl-X)".txtMessage.imgList.Add(PadTextString("Paste".Items.Items. maxWidth.Add(PadTextString("Delete".ContainsText(TextDataFormat. Me.Location 'save selection location if this is not a right-click If e.txtMessage If Clipboard.Redo). New EventHandler(AddressOf Copy_Click)). New EventHandler(AddressOf SelectLine_Click)).Width + 32 'define master length + 32-pixel padding cMenu.Items. check for text data. 'then paste it 'otherwise. e As MouseEventArgs) Handles txtMessage.Enabled = TB.Add("Select All (Cntrl-A)".Items.MouseButtons. My. New EventHandler(AddressOf Cut_Click)).ReadOnly AndAlso (Clipboard.ContainsText(TextDataFormat.Items.Bitmap)) 'set true if the clipboard contains data 'The ContextMenuStrip.0 – David Ross Goben 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm ' Method : txtMessage_MouseDown ' Purpose : Bring up a context menu for a RichTextBox 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm Private Sub txtMessage_MouseDown(sender As Object.imgList.Items. RichTextBox) 'get a simpler reference to the textbox Dim HasText As Boolean = CBool(TB.Images(Images. Note: the weird spacing is to adjust text spacing between the command and properly 'aligning the text control’s Cntrl-key shortcut.. Me.Enabled = HasText Page –416– .Paste(DataFormats.GetFormat(DataFormats.Add(PadTextString("Select All".ContextLocation = e.imgList.Copy). New EventHandler(AddressOf SelectLine_Click)).Items.Undo).Add(PadTextString("Undo". Me.Add() method returns a reference to the added MenuItem that you could capture and use to 'enhance the item even more.Items. enablement might first seem useless and counterintuitive for a context menu.Enhancing Visual Basic . and at the same time assign an handler and set its transparency color to Magenta cMenu.ImageTransparentColor = Color.PointToScreen(e.Add("-") 'add a line Separator (ToolStripSeperator object) ' ' Add other context menu entries at this location ' cMenu. Page –417– . or Fuchsia. This would be typical for those of us who go on the cheap and might use MS Paint to build our menu images.Resources. if any If Nd IsNot Nothing Then 'is there in fact a node there? Me. but we may want the user to become familiar with all options available through this context menu. In this case we will need to store it in an object reference and manage it that way. 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.NET Beyond the Scope of Visual Basic 6. more ancestral definition of our ToolStripMenuItem objects. by itself it is not much of an issue.GetNodeAt(e. For example.Forms. such as menu item enablement. Granted. But.MouseDown If e.Location)) 'display the context menu. some express exasperation because there is more than just ToolStripItems in a menu.Enabled = False 'disable the menu entry Some developers have the bright idea to loop through a Context Menu and set each member’s image transparency color before displaying it. New EventHandler(AddressOf btnUndefine_Click)) Item.Clear. a typical 256-color variation of Magenta that is interchangeable with it.ImageTransparentColor = Color.Transparent. to do so for the images assigned to a ToolStripMenuItem.Show(Me. Consider the following typical approach to solving this issue: '********************************************************************************* ' Method : tvSelectRange_MouseDown ' Purpose : Context menu support for our TreeView control tvSelectRange '********************************************************************************* Private Sub tvSelectRange_MouseDown(sender As Object. My. get the TreeView node the mouse is on. at the mouse cursor 'Consider replacing the above line with: cMenu. and so throw errors at us.Items. this can also turn out to be a heck of a lot of work.Items. 16-bit by 16-bit image file with a Magenta background color named Clear. such as Magenta. we can access and manipulate the Menu item using something like this: Dim Item As ToolStripItem = cMenu. with that in mind. so ensure that it is selected End If End If Dim cMenu As New ContextMenuStrip 'we have a valid node. This is not a big issue. which uses screen coordinates. such as combo boxes. though that particular option might not be available under the current circumstances. e As MouseEventArgs) Handles tvSelectRange. we will have to tinker with its ImageTrasnparentColor property.tvSelectRange. and something that has driven some new developers to head-bangingthe-wall distraction is that even though we store ToolStripMenuItem objects in the Items collection of a Context Menu object. it becomes trickier if we may also have to set other properties.png in my Image Resources.tvSelectRange.Items. which lack this property.Resources.Show(Me. typical for most 16-color images.Button = Windows.0 – David Ross Goben Black Book Tip # 25 Setting Context Menu Icon Image Transparency at Runtime Sometimes we add images to ContextMenuStrip objects that we cannot assign a border transparency color to. suppose I had stored a 16-color. so construct a Context menu 'add a menu item.Add("Undefine the selected Named Range".tvSelectRange. if our Context Menu has a lot of such images. The thing that sometimes bothers me about this. My. and separators. which in most cases is a non-used color. its Add() method will instead return to us a reference to it as a ToolStripItem object.SelectedNode = Nd 'yes. text boxes.MouseButtons. which I want to use in one of my popup Context Menus.Clear. such as Color.Magenta 'set this entry's transparency color Item.Magenta cMenu. New EventHandler(AddressOf btnUndefine_Click)). However.Show(Cursor.Location) 'if so. Even so.Right Then 'right-mouse? Dim Nd As TreeNode = Me. 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. However. However. because when we set them to the Image property of a Main Menu object we can always set its ImageTrasnparentColor property to the image’s background color. e.tvSelectRange.Location) 'Easier – let it compute screen coordinates for us 'Consider replacing the above line with: cMenu.Add("Undefine the selected Named Range".Position) 'Easiest – simply supply the screen coordinate of the mouse End Sub As you can see. which is a narrower. in which case this will do nothing at all to the display of the object).MakeTransparent(TransparentColor) 'finally. but that is not a guarantee. shock of shocks. so set its ImageTransparentColor property to Magenta 'process the next 'return the context menu strip item count To use it. so simply dispose of its resources 'otherwise. For example: If AdjustContextMenuItemTransparency(cMenu) = 0 Then cMenu. pass it to the above function as a parameter. because in those cases its actual transparency color is used.Count If Count <> 0 Then For Each item As Object In cMenu. With DirectCast(Img. the above code will work even if the image has a built-in transparency color.Show(Me. which will do three fundamental things: 1) it will ensure that we are manipulating only ToolStripMenuItem objects. so treat as Object) 'is this one a ToolStripMenuItem? 'yes. ToolStripMenuItem).1. display the context menu (object disposal is handled by the menu processor) Now consider the very real possibility that these images might have different color backgrounds that we will want to render as its transparency color.Transparent Then 'if the background color was not already transparent.. tested for by something like “If (Clr.Items.tvSelectRange. As such.Transparent End If '--------------------------------------------------------------If Clr <> Color. TransparentColor = Clr 'if so.. Even so. 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. at least to start. Consider the following function.1) 'check the bottom-right pixel – the most likely candidate for a background '--------------------------------------------------------------If (Clr. If TransparentColor = Nothing Then 'then check to see if the user-defined transparency color is Nothing. In most cases the border of the image is black. This is useful when we build menus based on the situation. .Transparent 'its Alpha-Blend value was zero.ToArgb And &HFF000000) = 0 Then”).. we can pass the Image property of the menu item image to another method.ImageTransparentColor = Color. For example. such as to the following. instead of directly assigning a transparency color. then set that value as its ImageTransparentColor property. 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. Bitmap) 'treat the image as a bitmap (identical format) Dim Clr As Color = . you will have to take this into consideration as you encounter these instances. '********************************************************************************* ' 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.0 – David Ross Goben Hence. after you construct your context menu. not the color assigned to its ImageTransparentColor property.Name = "ToolStripMenuItem" Then DirectCast(item.. set the desired transparency color End If End With End If End Sub Page –418– . some images might use the entire image surface. so use Color. then use the color grabbed from the B-R corner of the image End If .Location)) End If 'anything defined within the context menu? 'no. the transparency color might already be set within and image (its Alpha-Channel value is set to Zero.Magenta End If Next End If Return Count End Function 'get the number of context menu strip items 'anything to do? 'yes. For example.Dispose() Else cMenu. leaving no outer border for a transparency field.. or.Enhancing Visual Basic . Process each (different types.GetType.Items If item.Height .ToArgb And &HFF000000) = 0 Then 'determine if we are dealing with a transparent color Clr = Color.GetPixel(. the above assignment to a ToolStripItem object reference fails if the object is actually a ToolStripSeparator. to figure out transparency: '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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.PointToScreen(e. this simple code can start getting rather complicated. However. even then. 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. Assume.NET Beyond the Scope of Visual Basic 6..Width . 2) we will set the ToolStripMenuItem object’s ImageTransparentColor property to Magenta. which is typical of PNG image files that sport a built-in transparency color. and 3) we will return the number of items in the context menu. As such we can invoke the above method from our AdjustContextMenuItemTransparency() method using the line: SetImageTransparency(DirectCast(item. the method will pick it up from the image. ToolStripMenuItem) SetImageTransparency(. I use it so much that I seldom even think about it.Name = "ToolStripMenuItem" Then 'if we can use the object With DirectCast(mnuItem. you will notice no difference in operation. so process each item pAdjustMenuItemTransparency(item.DropDownItems 'yes.GetType.Count 'get the number of main menu items If Count <> 0 Then 'anything to do? For Each item As Object In mMenu.Magenta) Also in this example. if we require it (if you do not supply a color. so process each item (there are different types. where the second method is used for recursion. assuming its selected corner contains a background color (our example is testing its bottom-right corner). ToolStripMenuItem). as you might note. We can also tell it to use the color defined in its bottom-right pixel.Items. we can redesign our AdjustContextMenuItemTransparency() method. Although some developers immediately fall into a sweaty panic and break out in hives whenever they encounter this term. Color. it will allow us to specify a transparency color. if they exist. Optional ByVal TransparentColor As Color = Nothing) As Int32 Dim Count As Int32 = cMenu. but still use it as before. It is one of the absolute greatest tools for walking any branching linked structure.Items 'yes. but it will also take into consideration the possibility of dropdown menus.NET Beyond the Scope of Visual Basic 6. we are still assigning Magenta as a transparency color. TransparentColor) 'set its transparency.Image. Optional ByVal TransparentColor As Color = Nothing) If mnuItem. TransparentColor) 'recurse through it for however many levels required Next End If End With End If End Sub With the above.Count <> 0 Then 'does it contain dropdown items? For Each mnu As Object In .Items. In this case. so use Object) 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 Page –419– .Enhancing Visual Basic . 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.Image) Another thing to consider is a more complex context menu that itself contains additional or even cascading dropdown menus to several levels. if required If . This will totally eliminate the usual task of manually going through your menus and setting the ImageTransparentColor properties on any entries that have different background colors that are also to be used as transparency keys. we would still invoke AdjustContextMenuItemTransparency() just as we did before. dividing it into two separate methods.DropDownItems. if no transparency color is provided.Image. but we are also telling it that if the image contains a transparency color (it will report an Alpha-Blend value of zero if so). BONUS TIP # 1: You can take advantage of the above pAdjustMenuItemTransparency() method for your main menus as well. Optional ByVal TransparentColor As Color = Nothing) As Int32 Dim Count As Int32 = mMenu. ToolStripMenuItem). just pass your MenuStrip object to the following method: '********************************************************************************* ' Method : AdjustMainMenuItemTransparency ' Purpose : Set the ImageTransparentColor property of main menu items '********************************************************************************* Friend Function AdjustMainMenuItemTransparency(ByRef mMenu As MenuStrip. it will try to determine one by sampling the image). which is code recursion. In this case we can take advantage of one of my favorite tools in my bag of tricks. such as the following: '********************************************************************************* ' Method : AdjustContextMenuItemTransparency ' Purpose : Set the ImageTransparentColor property of Centext menu items '********************************************************************************* Friend Function AdjustContextMenuItemTransparency(ByRef cMenu As ContextMenuStrip. Otherwise. so process each item in the dropdown list pAdjustMenuItemTransparency(mnu. and also.Items 'yes.Count 'get the number of context menu strip items If Count <> 0 Then 'anything to do? For Each item As Object In cMenu. to ignore setting it. though still respecting any image with a built-in transparency color by issuing the command: SetImageTransparency(DirectCast(item. Within your Form’s Load() event.0 – David Ross Goben In this example. So in our case. named ScreenToClient()).pnlComment. if they feature one. such as drawn objects. cMenu. The PointToScreen() method takes the provided Point structure.Show(Cursor. 'then check the current viewport. Screen areaconsuming controls feature a PointToScreen() and PointToClient() method. Awareness of screen coordinates in our code can be very important.. One of those gems was a tiny method that converted client coordinates to screen coordinates.PointToClient(Cursor. I use it so much that I seldom ever bother using anything else. HOT TIP: But we are still not through. named ClientToScreen() (there is also another one that converts screen coordinates to client coordinates. Dot NET users have it easy because they do not need to resort to calling up a P/Invoke. It took a simple Point structure as a parameter. we do not bother removing the comment panel.. which are relative to the object the mouse event is associated with. PictureBox) If . and return a Point structure containing client coordinates. As such. storing local-relative X and Y pixel offsets. it typically appears off to the upper-left of the mouse pointer. their MouseEventArgs parameter. Hence. 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.Visible Then With DirectCast(sender.Show(Me. you implement the PictureBox’s MouseLeave() event to detect when the cursor leaves the PictureBox so you can remove the comment panel. especially if we display lots of context menus.NET software design). 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.Position)) Then Return End If End With CloseplnComment() End If End Sub 'Handles are added via AddHandler as Viewports are added 'if the comment panel is presently visible.Enhancing Visual Basic . you saw a point that was assigned a translation from client coordinates (mouse coordinates relative to a ListView control) to screen coordinates. we often figure that we must compute several offsets. Oops. However. you might notice that if you toss up a context menu based upon those mouse coordinates.. context menus. and thus use P/Invokes at a torrential pace. such as “cMenu.Location))”. How do we avoid that? Easy. e. Indeed. What this means is that we can forgo any translation at all and simply use “cMenu.tvSelectRange.Bounds. do indeed use screen coordinates. HOT TIP: Did you know that we can in fact provide context menus with local. However. Because you want to remove this comment panel when the cursor leaves the drawing surface.Contains(.. 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. 'then DO NOT remove the comment panel 'otherwise. In our PictureBox’s MouseLeave() event.0 – David Ross Goben BONUS TIP # 2: In the previous example code. Grrr… Being a professional C/C++ developer (who find himself spending more any more time in the blazing Rapid Application Development (RAD) speed of VB. 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. Both of these methods return a Point object that is a translation of the Point object provided to it. and on return it held the screen-relative offsets. Typically. and the Cursor object. there is a situation where you might need to translate coordinates. it will work exactly as before. client coordinates if we also provide it with the control so that it can compute the screen coordinates for us. The PointToClient() method. if you replace our computed position in the above examples with this.Location)”. HOT TIP: All that being said. for a TreeView control.NET Beyond the Scope of Visual Basic 6. close the comment panel Page –420– . For controls themselves. So. I have developed mountains of C/C++ code under the Microsoft Foundation Class (MFC). Consider that you have a program that uses a large PictureBox surface to draw a user’s doodles or to format a matrix. the location of the control the TreeView is parented by. such as the form’s X and Y locations.. 'if the cursor is still within the bounds of the viewport. using the tvSelectRange TreeView control. If so. always provide Client Coordinates.Show(Me. the width and height of the form’s border and banner. Y=0. we tend to use the PointToScreen() method the most. we would display its context menu. and it returns a Point structure containing Screen coordinates. which we assume contains Client coordinates. and so on. Some objects. e As EventArgs) If Me. at the cursor position using the command “cMenu. what happens is the comment panel will be dutifully removed. provided a Point structure that we assume contains screen coordinates.. Suppose you have a custom panel that displays comments over certain known locations. which the MouseEventArgs parameter would report as being coordinate X=0. such as forms.PointToScreen(e. on the other hand. and also cuts considerable internal code out of our application. the cursor position is relative to its top-left corner.tvSelectRange.Position)”. like so: Friend Sub picViewport_MouseLeave(sender As Object. not relative to the object the mouse cursor position is located over (Client Coordinates). or a MainMenuStrip.ToBitmap” tag appended to the resource (e. with this solution you can also create images that use a nonused color as a background color. where you may need to hijack images from actual icons that you force to image format by assigning them with the “. dropdown buttons. For example. This is useful when you have an image in a ToolStrip. so you never see it. which is fully recursive and can walk as many branches as the menu tree holds. or dropdown menu option. to include any and all dropdown members. The five methods that you can use to roll through these controls are: AdjustToolStripTransparency(ToolStripReference) This method will set the ImageTransparentColor property of all members of a Toolstrip. But one thing that detracts from this is if the embedded image has obvious box-framing around it. or a ToolStripSplitButton. I designed helper methods to parse a MenuStrip. as previously demonstrated in Black Book Tip # 25. and a ToolStripMenuItem are capable of having a DropDown collection. This way you can readjust its background transparency after you have assigned a new image to it.g. a menu. AdjustContextMenuItemTransparency(). such as ToolStripLabels and ToolStripSeparators.Enhancing Visual Basic . a ToolStripButton. Grrr… If its background was a transparency color. “My.Resources. MenuStrips. a ToolStrip. But if you are programming on a shoestring. you would pass cMenu using AdjustContextMenuItemTransparency(cMenu) just prior to displaying that pop-up context menu. a ContextMenuStrip. and AdjustMainMenuItemTransparency() methods. The pAdjustMenuItemTransparency() method is invoked by each primary method. This is normally invoked by the above pAdjustMenuItemTransparency() method. or ToolStripDropDownButton that may have dropdown lists attached to them. MenuStrip or ContextMenuStrip when you have changed its image and want to adjusts the new image’s background transparency. a split dropdown button.NET Beyond the Scope of Visual Basic 6. You can also provide it with the MenustripItem from a ToolStrip. a ToolStripSplitButton. MenuStrip. To address this issue. but this solution is more universal. a ToolStripDropDownButton. and invoke the pAdjustMenuItemTransparency() method on each member in its primary Items collection. The first three methods roll through their primary objects. or in a hurry.Open. SetImageTransparency(ImageReference) (this method was also shown in Black Book Tip # 25) This method directly sets an image transparency color. but you can invoke it directly by providing it with a ToolStripMenuItem. Like with the previous solution. and ContextMenuStrip really help sell the functionality of a button. or DropDown menu. and these methods will render those backgrounds transparent even if you may have selected a different color for various images.0 – David Ross Goben Black Book Tip # 26 Adding Background Transparency to Images on Toolstrips and Menus Images embedded into ToolStrips. though I tend to speed things up by selecting the method below. but it can also be invoked directly on an image. such as I get when I take the time to edit my images as PNG images with Transparency. The pAdjustMenuItemTransparency() method might do Page –421– . It just looks less blended into your toolbar or menu when you can see a little square box behind it.ToBitmap”). When an Image property is found. and even individual images stored anywhere. such as Magenta or Fuchsia. resources can get pretty tight. along with adding tool tips to your menu items to better explain their functionality.. AdjustContextMenuItemTransparency(ContextMenuReference) (this method was also shown in Black Book Tip # 25) This method will set the ImageTransparentColor property of CentextMenuStrip tree. This method is normally invoked by the above AdjustToolStripTransparency(). menu dropdown items. are ignored. Others. ToolStripButton. a ToolStripDropDownButton. to include any and all dropdown members. such as on a form or control. a dropdown button. pAdjustMenuItemTransparency(MenuStripItemReference) This method is used for Recursion menu support to set image transparency. AdjustMainMenuItemTransparency(MernuStripReference) (this method was also shown in Black Book Tip # 25) This method will set the ImageTransparentColor property of MenuStrip tree and any and all of its dropdown members. a reference to it is passed to the SetImageTransparency() method. I had already touched on some of these in Black Book Tip # 25 (see page 373). especially when it is highlighted. after you have constructed a ContextMenuStrip object named cMenu. a ToolStrip. then this is not an issue and its background color adjusts with the highlighting seamlessly. such as a ToolStripMenuItem. Of these three main types. the Alpha 2-digit hexadecimal value (&H00 . This method then grabs the pixel from its bottom-right corner.Items 'yes. 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 Centext menu items ' : ' Parameters : cMenu = Context Menu to parse ' : TransparentColor = optional user-provided color value to assign ' : as transparency color for the image. but the SetImageTransparency() method does all the fine detail work. Blue. though this is very rare. If they did not. Indeed. The first thing it does is cast the Image object to a Bitmap (Bitmaps and Images have Identical structure. for Alpha component.99% of the time) to have a background pixel. but are named differently due to their names making better sense in their use context). Optional ByVal TransparentColor As Color = Nothing) As Int32 Dim Count As Int32 = mToolstrip. and even idividual images.Items 'yes. as may be needed if their image draws to the bottom-right corner in a color other than black. which is the location most likely (like. '********************************************************************************* Friend Function AdjustContextMenuItemTransparency(ByRef cMenu As ContextMenuStrip. '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------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. If it is not. and are interchangable. Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modAdjustTransparency Static Class Module ' This module is used to clean up image background transparency on ToolStrips. even if it is just that single pixel in the very corner. AA RR GG BB. below) ' : TransparentColor = optional user-provided color value to assign ' : as transparency color for the image. it checks if the color is already a transparency color (if its ARGB value. I have enhanced this version to also check for Black (the least likely background color).&HFF) is zero. '********************************************************************************* Friend Function AdjustToolStripTransparency(ByRef mToolstrip As ToolStrip. This is ' : useful if the user used a primitive image editor ' : to create image icons with solid-color backgrounds. and I have only had to do this twice in a 3-year period. so process each item (there are different types. Next. I specifically design my images to ensure the bottom-right corner only has the background color.Enhancing Visual Basic . 99. then the color picked up from the image background is used as the transparency color.Items.0 – David Ross Goben all the heavy lifting. or fully transparent). so use Object) pAdjustMenuItemTransparency(item.NET Beyond the Scope of Visual Basic 6. What follows is my modAdjustTransparency module. so a transparency key can accurately be assigned. Red.Count 'get the number of context menu strip items If Count <> 0 Then 'anything to do? For Each item As Object In cMenu. Next. Green. it checks to see if the user supplied a default transparency color. '********************************************************************************* Page –422– . it then checks to see if the invoker supplied a Transparency color. Optional ByVal TransparentColor As Color = Nothing) As Int32 Dim Count As Int32 = cMenu. ' ContextMenuStips. It then checks to see if the color value it picked up is not already defined as being transparent or black. This is ' : useful if the user used a primitive image editor ' : to create image icons with solid-color backgrounds. MenuStrips.Count 'get the number of main menu items If Count <> 0 Then 'anything to do? For Each item As Object In mToolstrip. This is ' : useful if the user used a primitive image editor ' : to create image icons with solid-color backgrounds. which will provide all these services.Items. so process each item pAdjustMenuItemTransparency(item. TransparentColor) 'process and adjust each menu item Next 'process the next End If Return Count 'return the context menu strip item count End Function '********************************************************************************* ' Method Name : AdjustMainMenuItemTransparency ' Purpose : Set the ImageTransparentColor property of main menu items ' : ' Parameters : mMenu = Main Menu to parse (this will in turn invoke pAdjustMenuItemTransparency(). 1. This method is normally invoked ' : by the above AdjustToolStripTransparency(). or ToolStripDropDownButton ' : that may have dropdown lists attached to them. TransparentColor) 'set its transparency.Image. TransparentColor) 'set its transparency. so process each item in the dropdown list pAdjustMenuItemTransparency(mnu. TransparentColor) 'recurse through it for however many levels required Next End If End With End Select End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method Name : SetImageTransparency ' Purpose : Directly sets an image transparency color. but you can invoke it directly by ' : providing it with a ToolStripMenuItem.Count <> 0 Then 'does it contain dropdown items? For Each mnu As Object In .DropDownItems.Items 'yes.Enhancing Visual Basic . ' : ' Parameters : Img = Image object to adjust transparency on ' : TransparentColor = optional user-provided color value to assign ' : as transparency color for the image. but you can also invoke it ' : directly if you have.0 – David Ross Goben Friend Function AdjustMainMenuItemTransparency(ByRef mMenu As MenuStrip.Count <> 0 Then 'does it contain dropdown items? For Each mnu As Object In .NET Beyond the Scope of Visual Basic 6. ToolStripSplitButton) SetImageTransparency(.. 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. ' : ' Parameters : mnuItem = Menu Item to start scanning from ' : TransparentColor = optional user-provided color value to assign ' : as transparency color for the image. Optional ByVal TransparentColor As Color = Nothing) If Img IsNot Nothing Then 'if a bitmap object was picked up. so process each item pAdjustMenuItemTransparency(item. so process each item in the dropdown list pAdjustMenuItemTransparency(mnu. With DirectCast(Img.. so see if the user-defined transparency color is Nothing. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub pAdjustMenuItemTransparency(ByRef mnuItem As Object. or ToolStripDropDownButton. This way you can readjust ' : its background transparency after you have assigned the new image. ToolStripDropDownButton) SetImageTransparency(.Count <> 0 Then 'does it contain dropdown items? For Each mnu As Object In .MakeTransparent(TransparentColor) 'finally. ToolStripMenuItem) SetImageTransparency(. AdjustContextMenuItemTransparency(). ' NOTE : This method is used by all of the above. set the desired transparency color End If End With End If End Sub End Module Page –423– .Image. if required If . if required If . menustrip. TransparentColor) 'set its transparency.DropDownItems. This is ' : useful if the user used has icons that draw to ' : the bottom-right corner of an image.GetType.DropDownItems 'yes. so process each item in the dropdown list pAdjustMenuItemTransparency(mnu. .DropDownItems 'yes. but it can also ' : be incoked directly on an image. changed an image assigned to a ' : toolstrip. ' : and: AdjustMainMenuItemTransparency() methods. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Sub SetImageTransparency(ByRef Img As Image.1) 'check the bottom-right pixel – the most likely background candidate '--------------------------------------------------------------Dim iClr As Int32 = Clr. for example.Height .. 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. if required End With Case "ToolStripDropDownButton" 'dropdlown button on a toolstrip With DirectCast(mnuItem. This is normally invoked ' : by the above pAdjustMenuItemTransparency() method. ToolStripButton) SetImageTransparency(.Items. Optional ByVal TransparentColor As Color = Nothing) Select Case mnuItem.DropDownItems. ToolStripButton.Count 'get the number of main menu items If Count <> 0 Then 'anything to do? For Each item As Object In mMenu. TransparentColor) 'recurse through it for however many levels required Next End If End With Case "ToolStripButton" 'regular button on a toolstrip With DirectCast(mnuItem. This is useful when you can an image ' : in a ToolStrip or MenuStrip or DropDown menu. Optional ByVal TransparentColor As Color = Nothing) As Int32 Dim Count As Int32 = mMenu. TransparentColor) 'set its transparency. TransparentColor) 'recurse through it for however many levels required Next End If End With Case "ToolStripSplitButton" 'a split dropdown button on a toolstrip With DirectCast(mnuItem.Image.GetPixel(.ToArgb 'grab integer version of color If (iClr And &HFF000000) <> 0 AndAlso (iClr And &HFFFFFF) <> 0 Then 'determine if we are dealing with Transparent or Black If TransparentColor = Nothing Then 'not. Bitmap) 'treat the image as a bitmap (identical format) Dim Clr As Color = .DropDownItems 'yes.Name Case "ToolStripMenuItem" With DirectCast(mnuItem.Width .Image. then use the color grabbed from the B-R corner of the image End If . This is ' : useful if the user used a primitive image editor ' : to create image icons with solid-color backgrounds. if required If . TransparentColor = Clr 'if so.. when I wrote the ReDim function in my Tokenized VB interpreter in 1984. Why? Because first. 10. 7. or 8 bytes of memory. and my C compiler in 1995. “I can do anything. such as using “Int Tmp = myArray[13]”. and with ease. used by the compiler (set during the array declaration). 2}. simply told it how wide and long the data was in the 2D array. I automatically assumed the existing data should be preserved. like everyone else. However. or 4 bytes of memory. it might take me a couple more days.NET Cross-Language compiler is going to smack us in the face with a warning if we redimension a leftward dimension and also try to preserve its data. this took me completely by surprise. but because it was laid out uniformly and sequentially in memory.NET myData(10. you could also access it as one long single-dimensioned array if you needed to. 2] (VB. we get this: We are informed that Redim can only change the rightmost dimension. To make this short story even longer. B. The fact that they allow data preservation during the redimensioning of the rightmost dimension has me really confused.1)). never realizing that the vast hoard of (self-proclaimed) software development gurus were declaring that I could not do what I was doing. Now that I have done my Obama impersonation by blaming anything but me for my oversight. such as an Int32 (Integer) always using exactly 32-bits. such as Int myData[11. Grrr… Interestingly.NET Beyond the Scope of Visual Basic 6.Enhancing Visual Basic . 8}. it is a fact that 99% of the time developers dimension their arrays just once (I will not count declaring an empty array. In fact. {12. 2)”. Internal bookkeeping indexes that were associated with the myData variable. I will make the excuse that I have designed a number of languages and have always incorporated full and unrestricted multidimensional redimensioning in them. 13. 4. I normally redimension arrays without needing to preserve its old data. 16. 5}. for scalar arrays (numeric data). In my 1995 C Compiler. but I did it because it never occurred to me that this was declared impossible. let us take a look at how they process array information. {15. but I do not understand how the redimensioning of preserved data in multidimensional arrays is supposed to be such a difficult task. An Int64 (Long) always uses 64 bits. This is also the technique that the procedural language C used. because that simple task is actually the lion’s share of the work when one allows FULL redimensioning of an Array. For example. {9. assume we have made the following declaration: Dim MyData(. {3. Microsoft. stores the array table in the same memory space as it does its array data. Funny thing. 1. this data is the array table. This is thanks to the fact that these items are of identical scaling. one after the other. As an example.0 – David Ross Goben Black Book Tip # 27 Easily Do a Redim Preserve with Leftward Bounds of Multi-dimensioned Arrays We often redimension arrays to add additional data to them dynamically as we gather it. such as “Dim myData(. such as my VB Interpreter in 1984. Thus.) As Int32 = Nothing”). when I added a Redim method to it. the . {6. 17}} Page –424– . 2) As Int32” and we try to do this: “ReDim Preserve MyData(20. But my philosophy for the past 40-odd years of my working career has always been. and I went about redimensioning multi-dimensional arrays without even pausing. And if it is impossible. 14}. 11}. I assumed this functionality was expected by default. if we have a 2D integer array declared “Dim MyData(10. because I almost obsessively use linked lists that store the objects I need as data within them.) As Int32 = {{0.” So why does Microsoft think this is so impossible? Well. and second. For example. The neat thing about this layout in C was that you could declare it as a 2D array. you can easily stack scalar values neatly and uniformly on top of each other. I very seldom did it. which was in turn borrowed from its predecessor language. All I would have to do is the following: 'Redimension myData(. 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. similarly computing the rightward size of the new dimension. The old space is then released. I knew how many bytes each rightward dimension occupied (rightward_dim_size * scalar_byte_size). There is nothing tricky being done here. and a secondary index informs it that the right parameter is 2 (internally. 5 * 3 + 2 (or (4+1) * (2+1) + (1+1) in VB. I was actually surprised that Microsoft had not adopted this technique. we can construct a broader array. But all this blather is academic. for example. I likewise computed the needed space and allocated it in new memory space. But. thus very quickly satisfying an expanded rightward dimension. Idy) = myData(Idx. it also doers a lot of things. Even from the VB-side. 1) 'grab rank 1 dimension (default). I declared it this way so that you can see how the data is stored in physical memory. it multiplies the left dimension times the width of the right dimension plus the desired right-dimension offset. Therefore. as opposed to thinking more logically in term of setting aside X number of objects and then applying them in zero-based terms. really simple stuff. this is 3). Remember. and copied the old data for the old size into the new space in these groupings. In my C compiler. a compiler can compute an array index in a snap.Enhancing Visual Basic .) to have a left dimension 10 higher than present Dim LSize As Int32 = UBound(myData. like automatically dimensioning declared dimensioned arrays in structures (another thing that is supposed to be impossible). If the user accessed myData(4. and repeat until I was done. This results in the 18th element. or offset 17 in zero-based terms. remembering to add 1 to each dimension due to VB6 hobbyists during VB. I was slightly divergent from this extremely fast and dirty solution (this tried and true technique is almost as old as computer programming itself). thus resizing this Integer 2D array of myData(10. You are probably more interested in how to get around this problem within your own code.2) up to myData(20.2). copy the original array’s data to it.NET language I am currently developing does this (but then.NET are objects.1). Even the new .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. Using the second index. which requires much less memory and computer cycles to process). arrays in VB. using the old pointer into the old space to copy the rightward dimensioned data. We simply reconstruct the multi-dimensioned array and then assign the new array over to the original array pointer.0 – David Ross Goben This declares myData to be an array dimensioned to 5.NET Beyond the Scope of Visual Basic 6. Suppose that we want to bump the base array size of a 2D array named myData by 10. and so on to 17. for some reason they cannot seem to be able to redimension any of the leftward bounds. All of this is a very easy thing to do. then 1.2. I then very quickly rebuilt the entire array. which consists of 6 groups of 3 element each. As such. the right dimension Dim Tmp(LSize + 10. An internal index associated with the myData variable informs the compiler that the left dimension is 5 (internally. a variable that is declared as an array is really just a pointer to the actual array data. and then assign that new array directly to the original array pointer. Microsoft copies the entire array to another allocated location in storage space that is initially zeroed and scaled to the required expanded size. interestingly enough. 2) 'grab rank 2 dimension. 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. is our stored integer value of 17. the left dimension Dim RSize As Int32 = UBound(myData. and it also auto-invokes a non-parameterized New() constructor if it is declared within them when the structure is assigned AS NEW to a variable). it is still very easy to rebuild these arrays. bump the new space pointer to the base of its next grouping. which. this is actually 6). which starts with the 0. It is just really. 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 itself is not affected) Page –425– . . so assume success End If Dim Size1 As Int32 = UBound(userArray. 17}} Bol As Boolean = RedimInt32Array(MyData. 16. you may want to construct helper methods to resize the array as needed. to include shrinking them.New size for second dim. muss.New size for third dim (see next function).. New2Dim = Size2 'then absorb the current second dimension End If 'one or the other of these might be < 0. plus an optional New3Dim function parameter.. Set to -1 if no change (used in 3D version) ' ' Returns : True if successful. ' ' Parameters : userArray(. 8}. -1. 2) 'shrink both diemensions Adapting this method for other types. and then assign Tmp to the original array. Just add a Size3 integer to store the extra dimension. then copied the data from the original 2D array to the Tmp array. False usually indicates negative parameters when an ' : array is not dimensioned. 2) 'get the current second dimension If New1Dim < 0 Then 'if the user did not provide a new first dimension. 5}. New1Dim = Size1 'then absorb the current first dimension End If If New2Dim < 0 Then 'if the user did not provide a new second dimension. 10..) As Int32. -1) 'no actual change = RedimInt32Array(MyData. a method that will support any 2D Integer array. Return True 'then Nothing to do. and an Idz loop control to loop through a third dimension. dimension it to the new requirement. If you do a lot of this. and thus the original 2D array is redimensioned without any fuss. Page –426– .Enhancing Visual Basic . and so the original variable now also points to Tmp's data. {15. such as String or Doubles is easy and straightforward. 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.2D or 3D Integer Array to resize (see the 3D overloaded version in the next function) ' : New1Dim ------.New size for first dim. Just change the storage types of the Int32Array and Tmp variables (a name change from Int32Array to reflect your data type might also be nice). 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.. For example. 1) 'get the current upper bounds of the user array Dim Size2 As Int32 = UBound(userArray. and redimensioning of Left. {3. 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 To use the above. Idy) = userArray(Idx.. {12.. 20. 20. {9. Right. Enhancing it to process 3D arrays is also very easy. keep right the same = RedimInt32Array(MyData. could be declared much like the following function: '********************************************************************************* ' Method : RedimInt32Array ' Purpose : Redimension a 2D or 3D Integer Array to a new size while preserving current data in it. 13. so nothing to do. 11}. but no harm. Optional ByVal New1Dim As Int32 = -1. I then erase the original array to release its resources. {6. 3) 'expand right dim by 1 = RedimInt32Array(MyData. 2}. -1) 'expand left dim.NET simply copies the pointer to the Array.) As Int32 = {{0.. 4. Copying arrays between variable in VB. '********************************************************************************* Friend Function RedimInt32Array(ByRef userArray(.0 – David Ross Goben As you can see. or Aces up our sleeves.) -. 1. Return True 'then nothing changed. Set to -1 if no change ' : New2Dim ------. 14}.. consider these rough examples: Dim Dim Bol Bol Bol MyData(. or both dimensions.NET Beyond the Scope of Visual Basic 6. the way I solved it was to create a new 2D array Named Tmp. 7. Optional ByVal New2Dim As Int32 = -1) As Boolean If userArray Is Nothing Then 'if the array does not exist. but not both '------------------------------------------------------If New1Dim = Size1 AndAlso New2Dim = Size2 Then 'if both dimensions already match. 10.. Return False 'do nothing and report failure ElseIf New1Dim < 0 AndAlso New2Dim < 0 Then 'if both user-defined sizes are not defined. Set to -1 if no change ' : New3Dim ------. so assume success Dim Size1 As Int32 = UBound(userArray... 1) 'get the current upper bounds of the user array Dim Size2 As Int32 = UBound(userArray... Idy.) Optional ByVal New1Dim As Int32 Optional ByVal New2Dim As Int32 Optional ByVal New3Dim As Int32 If userArray Is Nothing Then Return False ElseIf New1Dim < 0 AndAlso New2Dim < 0 AndAlso New3Dim < 0 Then Return True End If As Int32.. = -1... as required. 3) 'get the current third dimension If New1Dim < 0 Then 'if the user did not provide a new first dimension.. New1Dim = Size1 'then absorb the current first dimension End If If New2Dim < 0 Then 'if the user did not provide a new second dimension. Page –427– . but not both '------------------------------------------------------If New1Dim = Size1 AndAlso New2Dim = Size2 AndAlso New3Dim = Size3 Then 'if all dimensions already match. simply rename the twin methods appropriately..Enhancing Visual Basic . and change the storage types defined in the above shaded areas from Int32 to either Double or String. = -1. Idy. 'do nothing and report failure 'if all 3 user-defined sizes are not defined. = -1) As Boolean 'if the array does not exist. New2Dim. 'then Nothing to do.. such as RedimDblArray or RedimStrArray. Idz) = userArray(Idx... so nothing to do. but no harm. New3Dim = Size3 'then absorb the current third dimension End If 'one of these might be < 0.NET Beyond the Scope of Visual Basic 6. 2) 'get the current second dimension Dim Size3 As Int32 = UBound(userArray. New3Dim) As Int32 'dimension temporary 3D array to new size '------------------------------------------------------For Idx As Int32 = 0 To Size1 'process first dimension For Idy As Int32 = 0 To Size2 'process second dimension For Idz As Int32 = 0 To Size3 'process third dimension Tmp(Idx. Return True 'then nothing changed. Return success End If '------------------------------------------------------If New1Dim < Size1 Then Size1 = New1Dim 'adjust in case user is shrinking the first dimension End If If New2Dim < Size2 Then Size2 = New2Dim 'adjust in case user is shrinking the second dimension End If If New3Dim < Size3 Then Size3 = New3Dim 'adjust in case user is shrinking the third dimension End If '------------------------------------------------------Dim Tmp(New1Dim. Idz) 'copy data Next 'process next third dimension element Next 'process next second dimension element Next 'process next first dimension element Erase userArray 'release the original data to the garbage collector userArray = Tmp 'point the original 3D array pointer to the new data Return True 'return success End Function To adapt the above methods for Doubles or Strings..0 – David Ross Goben An overload to the above method that supports full 3D array redimensioning is defined below: 'version of RedimInt32Array that supports 3D arrays Friend Function RedimInt32Array(ByRef userArray(. New2Dim = Size2 'then absorb the current second dimension End If If New3Dim < 0 Then 'if the user did not provide a new third dimension. 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. but with its value rotated upward in the field so that numeric data is defined from the upper side. apart from the fact that you now know that I am an old guy (and the older you get. The Fraction part (also referred to as the Mantissa) consists of 52 consecutive bits (0-51). But Bit Fields were also extremely useful for storing all sorts of integer data.0 – David Ross Goben Black Book Tip # 28 Implementing a C-Style Bit Field Class in VB. The final Sign bit indicates the positive or negative value of the fractional part.NET in the Collections. Bit Fields are already used in a simplistic manner under VB. though in its world. diapers. I remember upgrading a computer from 4K (that’s Kilobyes. which we will discuss later in this Tip) on the VAX computers used a strange. But the point is. But. Further. 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. which superseded the error-prone VMS-D format. The 11 bits set aside for the Exponent is also like a 11-bit integer value.NET Beyond the Scope of Visual Basic 6. For example.Enhancing Visual Basic . The old VMS-G (Grand Double-Precision. which represents each sequential bit in the list as a Boolean value of True (1) or False (0). used to set how far the decimal place is moved left or right of the left-most “fraction” digit. which is sadly lacking in C# and VB.BitArray list. Even in the hobbyist world. memory had an extremely steep price. we can declare a thing called a Bit Field. such as the following: typedef struct { unsigned int flag:1. Using it. /* 8-bit space to store a border definition pattern */ } borders. probably just as logical format. giving me tons of breathing space (well. you could use these Bit Fields to translate data between data formats. or 64 bits. And back in their youth. the less bashful you are about the fact – though my niece informs me that this phase is followed shortly by bed pans.NET Under C and C++. until I went on a memory gluttony binge by expanding to a whopping 48K). 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. This allowed me to easily store 3 items of data within the confines of a single word of data (16 bits. /* 3-bit space to store an 8-color value 0-7 */ unsigned int pattern: 8. where 128K would cost in the thousands of dollars. A Bit Field was often used to pack data very tightly. as shown here: VMS-G (Grand) 64-bit Double precision floating point: Sign/Exp/Fraction: FFFFFFFF FFFFFFFF 1’s bits: FEDCBA98 76543210 16’s bits: 33333333 33333333 bytes: byte7 byte6 16-bit words: <-------3-------> Exponent Bias: Excess-1024 FFFFFFFF FFFFFFFF FEDCBA98 76543210 22222222 22222222 byte5 byte4 <-------2-------> Page –428– FFFFFFFF FFFFFFFF FEDCBA98 76543210 11111111 11111111 byte3 byte2 <-------1-------> SEEEEEEE EEEEFFFF FEDCBA98 76543210 00090000 00000000 byte1 byte0 <-------0-------> . /* 1-bit space used to enable/disable flag */ unsigned int color:3. Under C. especially on old Unix systems where memory was more scarce than target-practice volunteers in medieval times. 32 bits. or 4 x 1024 bytes. we could declare a special type of structure that included a bit reservation count after a colon. which can basically be viewed as a 52-bit integer value. you also know of another powerful way to massage data. kids. depending on your system). and walkers… and that left turn signal that never switches off). 4096) of memory and paying Radio Shack $127 so I could rip out those 8 wimpy chips and plug in a massive 16K of memory. you could store more than just Boolean data in the fields. This layout is actually a hang-over from the days of the PDP-11 mini-computers (well. started humbly in Ed Robert’s MITS Altair 8800. manually computing every bit. Did you do a double-take? As you can see. they started storing bits in logical progressive order from left to right. when families got together for the holidays. they had gone insane. No wonder Unix programmers had such odd behavior. but not to those wacky old programmer hacks. nothing was ever the same. The lowest value is stored at… try and guess… no. especially to mainframe programmers. sacrilegious practice of retrieving memory words in actual sequential order. 8-bit. the individual 16-bit words were internally stored in proper order. and the Mantissa was from bit 12 thru bit 63. was that the 16-bit words of the fractional part were likewise stacked in reverse order. would be considered the lowest value. as I did above. byte. Normally the low bits. broken up in 4-bit. What is even more confusing. Without it. 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. suspenders. 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). the exponent was bits 1 thru 11. people transcribing their bit positions from their notes to switch settings on the programming panel (no keyboards back then). sporting beards. but this was the source of great confusion (and a cacophony of clenched-teeth screaming) when Intel introduced the revolutionary. but rather bit 48. and 16-bit segments. 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 adding them together. Once memory became cheaper. released in 1976. grabbing the bytes in lo-hi order from the left. they would actually talk to each other. ill-fitting rumpled smelly clothes (and wearing those same cloths for days or weeks on end). Well. VSM-G seems to split up the Fraction portion into 2 parts.Enhancing Visual Basic .0 – David Ross Goben NOTE: VMS stands for Virtual Memory System. separating the first 4 bits (0-3) from the rest by an 11-bit Exponent and a 1-bit Sign. placing bit 0 on the right end. after retrieval. Page –429– . and to many programmers. for it the sign would then be at bit 0. which could be individually “edited” (replaced. but the actual words (the 16-bit frames) of memory were in reversed order. and to cut corners and simplify its printed circuit designs. which stored up to 32 words of code. heads bowed in hushed. like keeping an army cot next to their consoles. Once Intel’s 16-bit 8086 CPU came along. as they are actually stored in memory. The 8-bit Intel 8008 featured 16-bit instruction pointers and frames. pipes. then tape and teletype keyboards. a part of the OpenVMS Operating System. introducing the 4-bit CPU in their Intel 4004. Paper tape came along. and always keeping a punch bowl filled to the brim with M&Ms within easy reach. such as bit 0. and replaced later by the Intel 8080.NET Beyond the Scope of Visual Basic 6. using the old ones for bookmarks) to patch transcription errors (or so the programmers claimed). because they were programming computers entirely by hand. they were still the size of a full-sized chest freezer). where. and staring at their phones. Intel entered the fray. not just sit around the dinner table. This was profoundly confusing. Nope. the PDP11 actually stored its words of data in memory in the same order as on paper! This seems really weird now. If the computers stored the data in physical memory in exactly the same way they represented it on paper. and word value. that was the highest value. reverent silence. because we represent binary values on paper from right to left. and bit 15 on the left. Perhaps odd now. still an 8-bit CPU but with more powerful 16-bit framing. or dividing by simple subtraction – I actually did this in the US Army when I was an Artillery Surveyor). Card readers came later. and thus sparking the computer revolution. and then that is followed by 48 bits for the remainder of the Fraction. because the PDP-11 had read values from memory in 16-bit words in OPPOSITE order. In fact. before magnetic tape became more reliable. not bit 63. and then looking up their antilog. but it made total sense to computer scientists back when they were trying to establish what would become computer science. For a first easy example. when it is probably all just an inside joke). another for IEEE. 16) End With 'create with a 64-bit VMS-G Value '16 bits lowest fraction (bits 0 to 15) '16 bitsnext higher fraction (bit 16 to 31) '16 bitsnext higher fraction (bits 32 to 47) '16 bitsnext highest fraction (bits 48 to 51) '11 bits. or using a Double Precision value.AddField("Exponent".AddField("Sign". to grab however many bits of data you needed. because the VAX system stores words of the fraction in quasi-reverse order. a Signed or Unsigned Long. and you can use it to translate between formats. 16) . field bit 63 '4 bits highest Fraction (bits 0 to 3) '11 bits exponent (bits 4 to 14) '1 bit sign (bit 15) '16 bits next lower fraction (bits 12 to 27) '16 bits next lower fraction (bits 28 to 47) '16 bits lowerest fraction ( bits 48 to 63) There is still one more thing to consider. you could define a Bit Field to address a standard IEEE 64-bit Double-Precision value: Dim IEEE_Value As New BitField64 With IEEE_Value .AddField("Fraction3".AddField("Fraction". because the binary exponent values 11111111111 and 00000000000 have special meaning. 4) .AddField("Fraction4". 1) End With 'Flat IEEE Double value (no init value. Once the fields are defined for a Bit Field. I created a class named BitField64 that in fact allows you to define named fields along with how many bits to set aside for each field named within the confines of a 64-bit space. and to accommodate this for translation to and from IEEE format. you could set up one 64-bit Bit Field for VMS-G. you can create as many 64-bit Bit Fields as you require. not 1023 (why those idiots could not just call it what it is. and then. I can appreciate the irony in the terminology used here).AddField("Sign". or even a Double. 1) . 16) . 16) . these values represent binary decimal placement. or even use it for data encryption purposes. so it is set to zero) '52 bit fraction (bits 0 to 52) '11 bit exponent (bits 52 to 62) '1 bit sign (bit 63) Or Even a 64-bit VMS-G Double-Precision Value: Dim VMS_G_Value As New BitField64() With VMS_G_Value . 11) .NET Beyond the Scope of Visual Basic 6. For example.Enhancing Visual Basic .AddField("Sign". and if you actually wanted to translate between them. 1) End With 'Flat IEEE Double value (no init value. so it is set to zero) Dim VMS_G_Value As New BitField64 With VMS_G_Value .AddField("Fraction3". 48) End With 'create with a 64-bit VMS-G Value '4 bit fraction (bits 0 to 3) '11 bit exponent (bits 4 to 14) '1 bit sign (bit 15) '48 bit fraction (bits 16 to 63) However. had more sober senses Page –430– . the value is less than the bias.AddField("Fraction1". and when the decimal place is left. VAX programmers.AddField("Fraction2".AddField("Fraction2". a Signed or Unsigned Integer. and that is a thing called Exponent Bias or Excess. including overlapping fields if you need them. field bits 52 to 62 '1 bit. is probably a conspiracy to make us mere mortals envy the programming demigods who claim to actually understand these things. you can fill and grab the whole data using a Signed or Unsigned Long. when developing VMS-G. not base10 decimal placement (yes. But this is all academic if we had no way of implementing it under VB.AddField("Exponent". 52) . Further. you can set and grab data using a Byte. 1) .AddField("Fraction2". 11) . When the decimal value is rightward. 4) . 16) . 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. the “zero” exponent value stored here is actually 1022. 11 . by defining your fields to line up correctly.AddField("Exponent".AddField("Fraction4". By default. 16) . With it.AddField("Fraction1". instead of Excess-1023. Using this class. Further.NET. the value stored in the exponent is higher than the bias. 4) .AddField("Sign". you 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 With IEEE_Value . Because a friend of mine was struggling with how to work around the very problem described above. Excess-1022.AddField("Exponent".0 – David Ross Goben But resolving such number translation issues are the types of problems where Bit Fields really shine.AddField("Fraction1". 11) . you could very quickly swap values between the two and accurately translate Double-Precision values back and forth between them. AddField("Fraction1a".AddField("Fraction4". Notice also that VMS-D has an Exponent Bias of a paltry 64. 16) .AddField("Fraction4". 50.SetBitsFromUInt32("Fraction3". 49. For example.AddField("Fraction1b".NET Beyond the Scope of Visual Basic 6.GetBitsToUInt32("Fraction3")) IEEE_Value.GetBitsToByte("Fraction1")) IEEE_Value. 7) . and finally subtract 1. 2 pages ago).AddField("Fraction3". this thing only has an 8-bit Exponent. but to get there.SetBitsFromUInt32("Exponent".AddField("Fraction2". 8) .AddField("Sign". but it has a 55-bit fraction.SetBitsFromUInt32("Fraction4".GetBitsToUInt32("Exponent") . Because we must break the highest 7 bits up into 3 and 4 bits. VMS_G_Value. we can use the upper 4 bits of Fraction1 to assign to the IEEE’s 4-bit Fraction1 field.AddField("Exponent".AddField("Sign". 3) . The trick here is to translate the 8-bit exponent in Excess-64 format to an 11-bit exponent in Excess-1023 (1022) bias. because the Excess-1024 Exponent bias they used in their double-precision values actually use 1024 to represent an Exponent of zero. We can actually throw the lowest 3 bits away. No wonder that it was dropped. VMS_G_Value. though not before mountains of data that depended on it leached it way across the world. 16) . just add 1022 and subtract 128. VMS_G_Value. the lowest 3 bits (refer to the discussion on this for VMS-G earlier.2UI) IEEE_Value. The tricky business is managing the 55-bit fraction. where the final 3 bits 48. then the fun really begins. 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 you to first subtract 1024. VMS_G_Value. add 1023. but they are clearly playing you for a fool). 16) End With 'create with a 64-bit VMS-D Value 'first 3 bits of 7 bit highest Fraction (bits 0 to 3) 'last 4 bits of 7 bit highest Fraction (bits 4 to 7) '8 bits exponent (bits 8 to 14) '1 bit sign (bit 15) '16 bits next lower fraction (bits 12 to 27) '16 bits next lower fraction (bits 28 to 47) '16 bits lowerest fraction ( bits 48 to 63) Page –431– . 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 With VMS_D_Value . 1) . which rendered its version of the IEEE format as best it could. because we have to squeeze the bottom 3 bits to the top of the next 16-bit fraction word. 8) . VMS_G_Value. we have to shift all sorts of things around. next.GetBitsToUInt32("Fraction4")) '1 bit from VMS Sign to 1 bit IEEE '11 bits from VMS Exponent IEEE (adjust down 2 for IEEE) '4 bits from VMS Fraction1 to 4 bits IEEE '16 bits from VMS Fraction1 to 16 bits IEEE '16 bits from VMS Fraction1 to 16 bits IEEE '16 bits from VMS Fraction1 to 16 bits IEEE A much trickier prospect is to process the old VMS-D format.SetBitsFromByte("Sign". However. 16) End With 'create with a 64-bit VMS-D Value '7 bits of highest Fraction (bits 0 to 6) '8 bits exponent (bits 7 to 14) '1 bit sign (bit 15) '16 bits next lower fraction (bits 12 to 27) '16 bits next lower fraction (bits 28 to 47) '16 bits lowerest fraction (bits 48 to 63) To translate this to IEEE format requires some gymnastic ability. This is actually rather simple.SetBitsFromByte("Fraction1".AddField("Fraction1". to “properly” convert a VMSG exponent to an IEEE exponent. as opposed to the IEEE and VMS-G having 11-bit Exponents and 52-bit fractions. 1) . The 64-bit VMS-D (Double Precision) format is a format that was supposed to be superseded by VMS-G.GetBitsToUInt32("Fraction2")) IEEE_Value. Using my BitField64 class you 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.Enhancing Visual Basic . We can do this by shift the first 3 bits to the last 3 bits of a word (shift left 13). and put these two results together.AddField("Fraction2". 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. and then shift right 3 bits the Fraction2 word. before those old clunkers finally flip their last bit). Consider VMS-D’s even odder layout: VMS-D (Double) 64-bit Double precision floating point: Sign/Exp/Fraction: FFFFFFFF FFFFFFFF 1’s bits: FEDCBA98 76543210 16’s bits: 33333333 33333333 bytes: byte7 byte6 16-bit words: <-------3-------> Exponent Bias: Excess-64 FFFFFFFF FFFFFFFF FEDCBA98 76543210 22222222 22222222 byte5 byte4 <-------2-------> FFFFFFFF FFFFFFFF FEDCBA98 76543210 11111111 11111111 byte3 byte2 <-------1-------> SEEEEEEE EFFFFFFF FEDCBA98 76543210 00000000 00000000 byte1 byte0 <-------0-------> As you can see.SetBitsFromUInt32("Fraction2".GetBitsToByte("Sign")) IEEE_Value. We trickle this all the way down through Fraction4. 16) . 4) .0 – David Ross Goben of humor.AddField("Exponent". A BitField64 layout of the above data would be like this: Dim VMS_D_Value As New BitField64 With VMS_D_Value . VMS_G_Value.AddField("Fraction3". 16) . IEEE_Value..128UI) 'convert EX-64 value to EX-1022 (stupid 1023.AddField("Fraction2". because with the BitField64 class.Runtime.SetBitsFromUInt32("Exponent".NET (and a C# translation) as well. VMS_D_Value.AddField("Sign".GetBitsToUInt32("Fraction2") << 13 Or VMS_D_Value.AddField("Fraction1".AddField("Fraction3".GetBitsToUInt32("Fraction2") >> 3) IEEE_Value. 0UI) '16 bits next lower fraction (bits 48-63) ''convert VMS-G to IEEE format 'IEEE_Value. 16) '16 bits next lower fraction (bits 12 to 27) ' . so it is zero) '16 bits lowest fraction (bits 0 to 15) '16 bits next higher fraction (bit 16 to 31) '16 bits next higher fraction (bits 32 to 47) '16 bits next highest fraction (bits 48 to 51) '11 bits exponent (52 to 62) '1 bit. IEEE_Value. actually occupying bits 48.AddField("Fraction3". &H1800UI) '16 bits next lower fraction (bits 16-31) 'VMS_G_Value.Uint64Value = 1234UL 'IEEE_Value.SetBitsFromUInt32("Fraction1". 16) ' .DblValue = 1234.DblValue. 16) ' . and 15 of the 16-bit word. 49.AddField("Fraction4".Int64Value 'Dim UL As ULong = IEEE_Value. but we have no choice –.AddField("Fraction3". 16) . VMS_D_Value.75): 'VMS_G_Value.DblValue. Well.SetBitsFromUInt32("Exponent". I was also informed by those supposedly “in-the-know” that this can only be done from C and C++. are shift out and never used.GetBitsToUInt32("Exponent") . Consider the following translation code: IEEE_Value.SetBitsFromByte("Sign".2UI) IEEE) 'IEEE_Value. 0UI) '16 bits next lower fraction (bits 41-47) 'VMS_G_Value. VMS_G_Value. 0UI) '4 bit upper fraction (bits 0-4) 'VMS_G_Value. so it is zero) '16 bits lowest fraction (bits 0 to 15) '16 bitsnext higher fraction (bit 16 to 31) '16 bitsnext higher fraction (bits 32 to 47) '16 bitsnext highest fraction (bits 48 to 51) '11 bits. VMS_D_Value.ToString) 'display converted result What follows is my BitField64 class: Option Explicit On Option Strict On Imports System.GetBitsToUInt32("Fraction1a") << 13 Or VMS_D_Value.AddField("Fraction1".SetBitsFromUInt64("Fraction2". 4) . IEEE_Value.0 – David Ross Goben Using the very same IEEE_Value BitField64 object we used before: Dim IEEE_Value As New BitField64 With IEEE_Value . These bits are minor and do not affect 'accuracy to any noticeable degree (except by bean-counters).GetBitsToByte("Sign")) 'Transfer sign bit (bit 15) IEEE_Value.5678R 'Flat IEEE Double value (no init value.SetBitsFromUInt64("Fraction1".AddField("Fraction2". VMS_D_Value.GetBitsToUInt64("Fraction2")) 'IEEE_Value.GetBitsToUInt64("Fraction3")) 'IEEE_Value. 16) ' . 1) End With 'Flat IEEE Double value (no init value.ToString) 'display cohnverted result Page –432– '1 bit from VMS Sign to 1 bit IEEE '8 bits from VMS Exposent to 11 bits IEEE (adust down 2 for '4 bits from VMS Fraction1 to 4 bits IEEE '16 bits from VMS Fraction1 to 16 bits IEEE '16 bits from VMS Fraction1 to 16 bits IEEE '16 bits from VMS Fraction1 to 16 bits IEEE . 1032UI) '8 bit Exponent (bits 4-14) (setting 1024+8 = 1032) 'VMS_G_Value. 16) .AddField("Fraction4".AddField("Fraction1".SetBitsFromByte("Fraction1".AddField("Exponent". VMS_D_Value. field bit 63 'int64 (Long) value 'UInt64 (Unsigned Long) 'set a double value ''grab full 64-bit values like this: 'Dim D As Double = IEEE_Value.GetBitsToUInt64("Fraction1")) 'IEEE_Value..SetBitsFromUInt64("Fraction3".) IEEE_Value.AddField("Fraction2". 'and the matching 16-bit word is shifted down to accommodate it. that the final 3 bits. 11) .we are moving 55 bits into a smaller 52-bit field. 11) '11 bits exponent (bits 4 to 14) ' . 16) . field bit 63 We can perform an otherwise extremely difficult translation with ease.GetBitsToUInt32("Fraction3") << 13 Or VMS_D_Value. field bits 52 to 62 '1 bit.GetBitsToUInt32("Fraction4") >> 3) Debug.GetBitsToByte("Fraction1b")) 'easily transfer 4 highest bits 'Note that we must add 3 preceding bits to each 16-bit grouping.SetBitsFromUInt32("Exponent". VMS_G_Value.Enhancing Visual Basic .SetBitsFromUInt64("Fraction4".GetBitsToUInt32("Fraction3") >> 3) IEEE_Value. Note also that in doing this all 'the way down.GetBitsToUInt64("Fraction4")) 'Debug. and 50.SetBitsFromUInt32("Fraction2".SetBitsFromUInt64("Fraction2". VMS_G_Value.Print("Result = {0}". The previous 3 bits are shifted left to occupy bits 13. 1) 'End With ''Assign Values like this: 'IEEE_Value. you can do it from VB.SetBitsFromUInt64("Fraction3".InteropServices '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------'BitField64 Class '64-bit bitfield operations ' ' Field names are case-specific.AddField("Exponent".Uint64Value ''assign defined fields like this (Fill VMS-G fields with VAX version of 128. "Sign" is different from "sign" ' ''Create Instances like this: 'Dim VMS_G_Value As New BitField64 'create with a 64-bit VMS-G Value 'With VMS_G_Value ' . CByte(0)) '1 bit sign (bit 15) 'VMS_G_Value.AddField("Sign".AddField("Exponent". VMS_G_Value. 16) '16 bits lowest fraction ( bits 48 to 63) 'End With 'Dim IEEE_Value As New BitField64 'With IEEE_Value ' . VMS_G_Value.SetBitsFromByte("Sign". 4) ' .GetBitsToByte("Sign")) 'IEEE_Value.AddField("Sign". 14.SetBitsFromUInt32("Fraction4".Print("Result = {0}".NET Beyond the Scope of Visual Basic 6.SetBitsFromUInt32("Fraction3".AddField("Fraction4". VMS_G_Value. I am one C++ developer who is not convinced.DblValue 'Dim L As Long = IEEE_Value. 11) ' .SetBitsFromUInt64("Fraction4". 4) '4 bits upper Fraction (bits 0 to 3) ' .Int64Value = 1234L 'IEEE_Value. 1) '1 bit sign (bit 15) ' .GetBitsToUInt32("Exponent") + 1022UI . VMS_D_Value.SetBitsFromByte("Sign". 16) '16 bits next lower fraction (bits 28 to 47) ' . temporarily tossing its lower 3 bits into the aether. _BaseData.VarDouble = value End Set End Property '******************************************************************************* '******************************************************************************* ' Method Name : New ' Purpose : Define a New BitField64 object._BaseData._FieldNames) + 1 'get the number of fields defined Catch Return 0 'array not defined End Try End Get End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property Name : Int64Value ' Purpose : Get/Set the Value as a signed Long Integer 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property Int64Value As Int64 Get Return Me.VarDouble End Get Set(value As Double) Me. and the number of bits assigned to it '------------------------------------------Private _HoldOffset As Int32 'local storage for a local grab if the specified field data Private _HoldBitCount As Int32 'local storage for a local grab if the specified number of bits assigned to the field '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss ' Structure Name : structBitField ' Purpose : Local storage for 64-bit field.) As Int32 'array to store the field's offset in the 64-bit bit field.NET Beyond the Scope of Visual Basic 6.Enhancing Visual Basic . UInt64._BaseData._BaseData._BaseData.VarInt64 End Get Set(value As Int64) Me.VarInt64 = 0L End Sub '******************************************************************************* '******************************************************************************* ' Method Name : New ' Purpose : Define a New BitField64 object. initialized with a null value '******************************************************************************* '******************************************************************************* Friend Sub New() Me._BaseData. or a Double Value 'sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss <StructLayout(LayoutKind. or 0 if none 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend ReadOnly Property FieldCount As Int32 Get Try Return UBound(Me.VarUInt64 = value End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property Name : DblValue ' Purpose : Get/Set the Value as a Double Precision (Real) value 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property DblValue As Double Get Return Me.VarInt64 = Value End Sub '******************************************************************************* Page –433– ._BaseData.0 – David Ross Goben '------------------------------------------------------------------------------------'-----------------------------------------------------------------------------------Public Class BitField64 '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private _BaseData As New structBitField Private _FieldNames() As String 'store the name defined for each bit field member Private _FieldOffsets(._BaseData.VarInt64 = value End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property Name : Int64UValue ' Purpose : Get/Set the Value as an Unsigned Long Integer 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property Uint64Value As UInt64 Get Return Me.VarUInt64 End Get Set(value As UInt64) Me. Overlap fields (Union) so they occupy the same space ' It can be loaded initially with an In64. Bit 0-63 represented as Signed Long End Structure 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property Name : FieldCount ' Purpose : Return the number of fields the user has defined.Explicit)> Friend Structure structBitField <FieldOffset(0)> Friend VarDouble As Double 'Primary storage location. initialized with a Long Value '******************************************************************************* '******************************************************************************* Friend Sub New(ByVal Value As Int64) Me. 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. _FieldNames(Idx)) = 0 Then 'a match found? Throw New Exception("FieldName not defined for this BitField64 object") 'yes.CompareTo(Me. Throw New Exception("FieldOffset+NumBits is out of range for this BitField64 object") 'Danger._FieldOffsets(Count . '******************************************************************************* Friend Function AddField(ByVal FieldName As String._FieldOffsets(0.._FieldOffsets(Count ...._FieldOffsets(0. so use this manual offset End If End If Me. First._FieldOffsets(0. but you should do ' : this only after you have added any stacking values. 0) = Me.1._BaseData. so see if this is a redef. Offset = Me.NET Beyond the Scope of Visual Basic 6._FieldOffsets 'erase offsets End Sub '******************************************************************************* ' Method Name : ClearValue64 ' Purpose : Clear the value space but do not alter any field definitions '******************************************************************************* Friend Sub ClearValue64() Me. we have fields defined.. initialized with an Unsigned Long Value '******************************************************************************* '******************************************************************************* Friend Sub New(ByVal Value As UInt64) Me.. if needed._FieldOffsets(0._BaseData. it is not likely we will need this.VarDouble = Value End Sub '******************************************************************************* ' Method Name : InitNewFormat ' Purpose : Erase any current definition and start a new field definition. copy the original 2D data to it. Will Robinson! Return -4 Else Me. 0) = 0 'init to a base starting offset (new definitions) If FieldOffset >= 0 Then 'did the user specify a hard-coded offset? If FieldOffset + NumBits > 64 Then 'if the forced definition takes us over the top. 1) = Me.. set aside storage for the new definition 'save the field name 'dim to new size (we can only redim right side on multi-dims).Enhancing Visual Basic . For Idx As Int32 = 0 To Count .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. 0) + Me._FieldNames(Count) Me. ' so use a new array. initialized with an Unsigned Long Value '******************************************************************************* '******************************************************************************* Friend Sub New(ByVal Value As Double) Me. Will Robinson! 'otherwise.1._FieldNames(0) = FieldName 'save field name Me.._FieldNames(Count) = FieldName Dim Tmp(Count. 1) Me. 1) Next Page –434– 'if the new definition takes us over the top. ByVal NumBits As Int32.._FieldNames 'erase field name list Erase Me._FieldOffsets(Idx. 0) Tmp(Idx. 0) = FieldOffset 'offset OK.._FieldOffsets(Idx. ' : But it is useful if the same class will later be used differently '******************************************************************************* Friend Sub InitNewFormat() ClearValue64() 'nullify value Erase Me._FieldNames(0) 'create initial data storage ReDim Me. 1) = NumBits 'save number of bits to assign to the field Return 0 'return success End If 'here. 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. Optional ByVal FieldOffset As Int32 = -1) As Int32 Dim Count As Int32 = Me. you can declare any number of additional fields with hard-coded ' : offsets within the BitField and a specified number of bits (NumBits). 1) As Int32 For Idx As Int32 = 0 To Count . 1) 'then compute starting offset into the bit field from previous End If If Offset + NumBits > 64 Then Throw New Exception("NumBits is out of range for this BitField64 object") Return -4 Else ReDim Preserve Me.IsNullOrWhiteSpace(FieldName) Then 'if the field name is not declared.1 Tmp(Idx. ' : Because this is a class. Once such fields are ' : defined.. so throw an error Return -3 End If Next Dim Offset As Int32 = FieldOffset 'init to manual offset If FieldOffset < 0 Then 'if FieldOffset is not defined... ReDim Me.1 'scan through list of defined fields If FieldName.FieldCount 'get number of fields defined If String..0 – David Ross Goben '******************************************************************************* ' Method Name : New ' Purpose : Define a New BitField64 object. 'Danger. 'dim 0 'dim 1 ._BaseData.VarUInt64 = Value End Sub '******************************************************************************* '******************************************************************************* ' Method Name : New ' Purpose : Define a New BitField64 object. Return CLng(GetBitsFromBuffer(Me. CULng(Value)) 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. Me..._HoldOffset._HoldBitCount. 8) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from. 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. ByVal Value As UInt32) As Int32 Dim Result As Int32 = ComputeIndexing(FieldName._HoldBitCount. Return CInt(GetBitsFromBuffer(Me._HoldOffset.._HoldOffset. Me. 64)) 'get range of bits End If Return CLng(Result) 'else return error code End Function Page –435– .._HoldOffset.. 8) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from. Me. Me._FieldOffsets = Tmp Return 0 End If End Function 'save the offset into the bit field for the new data elements 'save the number of bits defined for this field 'erase the original 2D data to release this space to the CLR 'assign Tmp array data pointer to Me. 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. 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..._HoldOffset. CULng(Value)) 'set range of bits End If Return Result 'else return error code End Function '******************************************************************************* ' Method Name : GetBitsToUInt32 ' Purpose : Get the bits assign to a Field Name to a 32-bit Unsigned Integer '******************************************************************************* Friend Function GetBitsToUInt32(ByVal FieldName As String) As UInt32 Dim Result As Int32 = ComputeIndexing(FieldName._HoldOffset._HoldBitCount..0 – David Ross Goben Tmp(Count.. 1) = NumBits Erase Me._HoldBitCount... Me. 32) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from. 0) = Offset Tmp(Count. Return SetBitToBuffer(Me.. ByVal Value As Byte) As Int32 Dim Result As Int32 = ComputeIndexing(FieldName. Return SetBitToBuffer(Me._FieldOffsets Me. 64) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from. ByVal Value As Int32) As Int32 Dim Result As Int32 = ComputeIndexing(FieldName... 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._HoldOffset._FieldOffsets 'return success '******************************************************************************* ' 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. Me._HoldBitCount. 32) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from.NET Beyond the Scope of Visual Basic 6. Return CUInt(GetBitsFromBuffer(Me. Me. Return SetBitToBuffer(Me._HoldBitCount. Return CByte(GetBitsFromBuffer(Me._HoldBitCount. 32) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from.Enhancing Visual Basic . 32) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from. VarUInt64 = Local 'plug value back into class storage Return True 'return success End If Return False 'failure End Function Page –436– . Huston..NET Beyond the Scope of Visual Basic 6. Return SetBitToBuffer(Me.Pow(2. then.FieldCount 'get the number of fields defined If Count = 0 Then 'if none._HoldBitCount & "-bits) is larger than the receiving " & ReturnSize & "-bit buffer") Return -5 End If Return 0 'indicate success End If Next Throw New Exception("FieldName not defined for this BitField64 object") Return -3 End Function '******************************************************************************* ' Method Name : TestBitAt ' Purpose : Test a single indexed bit (0-63) for being set (True) or reset (False) '******************************************************************************* Friend Function TestBitAt(ByVal Index As Int32) As Boolean Dim Result As Int32 = CByte(CheckOffsets(Index.. Dim Mask As UInt64 = CULng(Math. Throw New Exception("Missing FieldName for this BitField64 object") Return -2 End If For Idx As Int32 = 0 To Count ..0R. we have a problem. CDbl(Index))) 'init bit mask to the starting position within the bit field Return CBool(Mask And Me.. 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. 0) 'yes.._BaseData.CompareTo(Me._FieldOffsets(Idx.0 – David Ross Goben '******************************************************************************* ' 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... 64) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from.VarUInt64) 'return True if the indexed bit is set. Return CULng(GetBitsFromBuffer(Me...IsNullOrWhiteSpace(FieldName) Then 'if the supplied Field name is not valid.._BaseData._HoldBitCount > ReturnSize Then Throw New Exception("The desired bit field (" & Me.1 'scan through defined fields If FieldName.. Return SetBitToBuffer(Me._HoldBitCount.0R._HoldOffset = Me.. 64) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from. 1)) 'check indexing If Result = 0 Then 'if all is well._HoldOffset. ByVal Value As UInt64) As Int32 Dim Result As Int32 = ComputeIndexing(FieldName. so grab the bit field offset and store locally Me.. Me.Pow(2.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. 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. Throw New Exception("No Fields Defined for this BitField64 object") Return -1 End If If String._HoldBitCount._BaseData. 64) 'init the result value If Result = 0 Then 'if we found the user-supplied fieldname to grab from. Me. 1) 'and also the number of bits assigned to this field If ReturnSize > 0 AndAlso Me. so make sure the bit is off End If Me. 1)) 'check indexing If Result = 0 Then 'if all is well.._HoldBitCount = Me. otherwise return 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._FieldNames(Idx)) = 0 Then 'found the field definition? Me._FieldOffsets(Idx._HoldOffset.Enhancing Visual Basic . ByVal Value As UInt64) As Int32 Dim Result As Int32 = ComputeIndexing(FieldName._HoldOffset. CDbl(Index))) 'init bit mask to the starting position within the bit field Dim Local As UInt64 = Me. Optional ByVal ReturnSize As Int32 = 0) As Int32 Dim Count As Int32 = Me. 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. ByVal SetState As Boolean) As Boolean Dim Result As Int32 = CByte(CheckOffsets(Index. Me. Dim Mask As UInt64 = CULng(Math._HoldBitCount. ByVal NumBits As Int32. 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. 8)) 'check indexing If Result = 0 Then 'if all is well. 1. ByVal NumBits As Int32. ByVal NumBits As Int32.. ByVal NumBits As Int32) As UInt32 Return CUInt(GetBitsFromBuffer(Index. CULng(Value). NumBits. ByVal Value As Int32) As Int32 Return SetBitToBuffer(Index. NumBits. CULng(Value). 1.0 – David Ross Goben '******************************************************************************* ' 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. 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. 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. 64) End Function Page –437– . 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. 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 Int32 Return CInt(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.Enhancing Visual Basic . 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.. Return GetBitsToByte(Index * 8. ByVal Value As Byte) As Int32 Return SetBitToBuffer(Index. NumBits. ByVal NumBits As Int32) As Byte Return CByte(GetBitsFromBuffer(Index. NumBits. ByVal Value As Int64) As Int32 Return SetBitToBuffer(Index. ByVal NumBits As Int32) As UInt64 Return GetBitsFromBuffer(Index. ByVal Value As UInt32) As Int32 Return SetBitToBuffer(Index. ByVal NumBits As Int32. 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. 8. CULng(Value). 8)) End Function '******************************************************************************* ' 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. NumBits. NumBits. Return SetBitsToByte(Index * 8. ByVal ByteValue As Byte) As Boolean Dim Result As Int32 = CByte(CheckOffsets(Index. 8)) 'check indexing If Result = 0 Then 'if all is well. 8) 'compute the index offset.. 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.NET Beyond the Scope of Visual Basic 6. 0R. 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._BaseData._BaseData.Pow(2. CDbl(Index))) 'init bit mask to the starting position within the bit field Dim lMsk As UInt64 = 1 'mask to check provided value Dim Local As UInt64 = Me._BaseData. ByVal Value As UInt32) As Int32 Return SetBitToBuffer(Index. CDbl(Index))) 'init bit mask to the starting position within the bit field Dim lMsk As UInt64 = 1 'mask to check provided value Dim Local As UInt64 = Me. Numbits. ByVal NumBits As Int32.Enhancing Visual Basic . 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 –438– .. ByVal NumBits As Int32. 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. Numbits As Int32. ByVal Numbits As Int32. 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.Pow(2.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '******************************************************************************* ' 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.0R. ReturnSize) 'see if parameters are ok If Result = 0 Then Dim Mask As UInt64 = CULng(Math. Value As UInt64. NumBits. Optional ByVal ReturnSize As Int32 = 0) As Int32 Dim Result As Int32 = CheckOffsets(Index..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.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 the bit field (this ensures the bit is always updated) If Not CBool(Value And lMsk) Then 'if a bit is not set in the source value. NumBits.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 If CBool(Local And Mask) Then 'is this bit set in the bit field? Result = Result Or lMsk 'yes. Optional ByVal ReturnSize As Int32 = 0) As UInt64 Dim Result As UInt64 = CULng(CheckOffsets(Index. 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. ReturnSize)) If Result = 0 Then Dim Mask As UInt64 = CULng(Math. ' Partial Friend Class MyApplication <Global. “Oh. the Application.Diagnostics. and make changes on the Application tab. the IDE will create a program file named ApplicationEvents. This event is not raised if the application terminates abnormally.NET Beyond the Scope of Visual Basic 6. before the startup form is created. crap! What did I just do?!”. ' NetworkAvailabilityChanged: Raised when the network connection is connected or disconnected. we will also lose the auto-generated information it pre-loaded it with. which you may notice is declared as Partial.VisualBasic.System. ' Shutdown: Raised after all application forms are closed. or simply deleting the file from the Solution Explorer and then let the properties application create it for us again (Yay!).Microsoft. Consider its contents.30319.AuthenticationMode.EnableVisualStyles = true Page –439– .ApplicationServices. which I recommend. we can easily recover it either by typing 4 lines of code into it (ugh!). and is in fact part of the My namespace: Namespace My ' The following events are available for MyApplication: ' ' Startup: Raised when the application starts.myapp. do not modify it directly. and so it is in fact a part of a broader MyApplication class. and I have done it enough for all of us.New(Global. one of the things you may have noticed there is an option to “View Application events”: When you select it. thus leaving this file completely blank afterwards. Partial Friend Class MyApplication End Class End Namespace The main MyApplication class is actually a tiny class defined within the Application. You can open these files from the Solution Explorer (make sure that you have the Show All Files button selected in the Solution Explorer’s Toolbar in order to see designer files from there). ' Runtime Version:4.vb file initially consists of this small bit of code: '-----------------------------------------------------------------------------' <auto-generated> ' This code was generated by a tool. and immediately close it without saving it. and Extending Application Events If you ever pay a visit to your current application’s properties (and the more you know about them and what you can do there.vb file. which is in turn a support file for an XML-formatted file named application.34014 ' ' Changes to this file may cause incorrect behavior and will be lost if ' the code is regenerated. If we did this. go to the Project Designer ' (go to Project Properties or double-click the My Project node in ' Solution Explorer). though there is no harm done.IsSingleInstance = false Me. and declares a class MyApplication.DebuggerStepThroughAttribute()> _ Public Sub New() MyBase. Unfortunately. ' UnhandledException: Raised if the application encounters an unhandled exception. the more that you will probably do so). which does nothing more than specify that its contents will be part of the My namespace.Designer. ' StartupNextInstance: Raised when launching a single-instance application and the application is already active. both of which are stored in your program’s MyProject folder.vb in your main program folder (you can afterward access it right from the Solution Explorer). To make changes.Windows) Me.Designer. You can examine their contents without consequence. For example. ' or if you encounter build errors in this file.0 – David Ross Goben Black Book Tip # 29 Taking Advantage of.0. ' </auto-generated> '-----------------------------------------------------------------------------Option Strict On Option Explicit On Namespace My 'NOTE: This file is auto-generated. Our normal reaction the first time we do this is to say. if we do that.Enhancing Visual Basic . But a good place to start is to enter “Protected Overrides ” (with a trailing space) and view all the possible code you can play with to alter and enhance ApplicationEvents. if we type “Me. we get all sorts of options we can play with.DebuggerStepThroughAttribute()> Protected Overrides Sub OnCreateSplashScreen() Me.”.MainForm = Global. But even with this recommended change.ApplicationServices.ShutdownMode.Diagnostics. In an inserted blank line above the Return statement within the OnInitialize() method. your solution might be to set the splash screen form’s TopMost property to True.Diagnostics. that everyone within earshot is intimately familiar with all the cryptic jargon they wretch from their bowels as if they were reciting from Byron.Collections.Enhancing Visual Basic .Microsoft.0 – David Ross Goben Me. a really great place to change it is in the above OnInitialize() method.Qube. it still disappears far too quickly.AfterMainFormCloses End Sub <Global.System. Normally. We also learn how the program keeps track of its main form and its optional splash screen. which will keep it above the main form. there is so very much more that you can do here.DebuggerStepThroughAttribute()> Protected Overrides Sub OnCreateMainForm() Me. the splash screen is displayed for only two or three seconds.System. which is a method that is invoked prior the splash screen or the main form being instantiated and displayed. but one of them will solve our splash screen timing issue: MinimumSplashscreenDisplayTime (see the image. To display it longer.frmQubeSplashScreen End Sub End Class End Namespace _ As you can see. selected above. But if you don your mining helmets.ReadOnlyCollection(Of String)) As Boolean Return MyBase. If we hit the Tab key here. above).SplashScreen = Global. Page –440– . such as a minimum of 5 seconds.VisualBasic. As it is. it generates the body of the OnInitialize() function: Partial Friend Class MyApplication Protected Overrides Function OnInitialize(commandLineArgs As System.frmQubeWorkspace End Sub _ <Global.Qube.OnInitialize(commandLineArgs) End Function End Class Suppose we have a splash screen that we want to display longer. most of whom seem to assume. several of the options declared in your Application Properties are embedded within the code here.ObjectModel. because although our splash screen looks fabulous. This is usually were you 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.SaveMySettingsOnExit = true Me. like the guys on Big Bang Theory.vb.ShutDownStyle = Global. For example: Consider OnInitialize. the problem may be is that it too quickly disappears behind our main form.NET Beyond the Scope of Visual Basic 6. Enhancing Visual Basic . or. but hiding the module names by preceding the declaration of the modules with “<HideModuleName()>”.0 – David Ross Goben With this. the more – the merrier. we can specify the minimum number of milliseconds for our splash screen to be displayed. you 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 you how to add custom objects to the My namespace using VB modules. and helping each other to become more proficient than we were.microsoft.ReadOnlyCollection(Of String)) As Boolean Me. the more fun we are all going to have sharing our ideas and techniques.aspx. we would set it to a value of 5000. for 5 seconds. you can extend the code to better customize it to your. But this is clearly not all we can do within the MyApplication class. I am not one to think that my domain and status is a right or something to be protected. I say. There. Consider visiting MSDN’s “Extending the My Namespace in Visual Basic” at http://msdn. our application has already taken a giant step toward looking much more professional. because there are 1000 milliseconds in one second: Protected Overrides Function OnInitialize(ByVal commandLineArgs As System.com/enus/library/bb531245.MinimumSplashScreenDisplayTime = 5000 'Set the splash screen timeout to 5 seconds (5000 miliseconds.OnInitialize(commandLineArgs) End Function Between this and the splash screen form’s TopMost property being set to True.ObjectModel. you may accidentally become a coveted guru. even within the My namespace. 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. I wonder? For them? Because the least of it is that as you explore and extend your programming prowess. For whom. For example. The more gurus there are.NET Beyond the Scope of Visual Basic 6. or your client’s needs. So.Collections. 1 second = 1000 miliseconds) Return MyBase. Page –441– . which is its default state.Right. perhaps doing something similar to what I did for a TextBox. What gives? Actually. 1=Right-Align. or create another one.0.aspx).1. www. if you have used the WordPad application in Windows 8. and 2=Center-Align. just two things stand in your way from using Full Text Justification and many other new features in your RichTextBox control. Windows XP Shipped with the new RichEdit Control 3. Anything outside their integer range will result in an exception error.85). The first is the HorizontalAllignment class that is used to service the SelectionAlignment property of the RichTextBox control. The HorizontalAlighment class is defined to support only 3 values: 0=Left-Align. However.DLL. please refer to MSDN’s “About Rich Edit Controls” at http://msdn. the text will simply remain left-aligned. The thing that is probably confusing about this was that its DLL was still named Riched20. and is fully capable of performing Full Text justification on its own (for more technical details.Enhancing Visual Basic . the control’s SelectionAlignment property does not accept anything except the values HorizontalAlignment. which performs fast and perfect full text justification on their Rich Text Format data. by default Dot NET still uses RichEdit 3. and its class name remained RICHEDIT_CLASS.0 – David Ross Goben Black Book Tip # 30 Enable Built-In Justify-Alignment in a RichTextBox from VB. and the other is turning on the control’s advanced typography options so that enhanced line-breaking and line formatting options are enabled.0 emulator. renamed Msftedit. but this enhanced functionality is actually built right into the RichTextBox control that ships with Dot Net and with Microsoft Windows ever since Windows XP was released. you may have noticed that both of these applications feature not only the standard Left. What we need to do is pass a PARAFORMAT structure (no need for the much longer PARAFPORMAT2.NET should be able to make those changes by modifying only a few lines of code. in Black Book Tip # 9 on page 365. and Center alignment options. Windows XP SP1 and all versions of Windows following it additionally featured the newest Rich Text Editor Class. the needed new Justify option is an integer value of 4 (the values actually sent to the RTB are in fact 1 higher than the HorizontalAlighment property reports). But just that is not enough. Page –442– . But even so.jarte. This is actually very easy to do. and with a new class name of MSFTEDIT_CLASS. yet clandestine functionality. but they also feature a fourth option.NET Previously. But now that you know that the RichTextBox control you might have been using for years sports this powerful.microsoft. and any developer worth the status of being just intermediately knowledgeable of Visual Basic . Without this. After all. for our purposes) to the RichTextBox control with the appropriate alignment value set. version 4.com). Right.Center.DLL. the system still sported a RichEdit 1. to additionally work with a RichTextBox control. I provided a class that you could use to easily display fully justified text from a TextBox control. you may be wondering how to access it. Sadly.0.com/enus/library/windows/desktop/bb787873(v=vs. This extended functionality featured in these text processors is not performed by auxiliary code within these applications. Justify. and HorizontalAlignment. However. we can “cheat” by simply sending it a message.NET Beyond the Scope of Visual Basic 6. or the incredible and free Jarte Rich Text Editor (“Unlock the power behind Microsoft’s WordPad”. We also need to inform the RichTextBox that we want it to enable its Advanced Typography Options that will enable the advanced line-breaking and line formatting options to be enabled. HorizontalAlignment. I have been asked by a number of people if I would also modify this class.Left. ' : Alignment -. Everyone wants to know how to do it. Lars Larson out of Denmark is the only other person who has managed to cobble together a free-ware solution. '------------------------------------------------------------------------------' Method : RTBFastSelect (as Function or Subroutine). ' : FALSE -. That being said. “I think this should work…”. ' : ' Returns : TRUE -. along with a tiny CHARRANGE structure that will specify the Selection Start index and the Selection End Index. So. TextAlign. to force a RichTextBox to justify its text. ' : featuring Left. but all of them I have seen. Right.the RichTextBox control. Page –443– . 2. Center.Enhancing Visual Basic . then apply the alignment to the entire text. and send this structure as a message to the RichTextBox control. ' : SelEnd -. I managed my own more robust version by reading the tea leaves in the MSDN documentation.Executed successfully. for as complicated as most people make it out to be. TO_ADVANCEDTYPOGRAPHY) That being said.Character length of text to apply alignment to.Justify). '------------: ' Parameters : RTBHandle -------. ' : SelectionStart --. ' : ResetSelection = True if you want to turn the selection back off (default). what we really need to do is the following: 1. mostly in C#. and I have seen quite a number of people offer up solutions. then SelEnd is ignored. which you 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. and Justify alignment. ' : If SelStart = -1. Beyond Providing Default Left.Character length of text to selection. Justify. Define a PARAFORMAT structure. actually fail. Actually.Start index of text to apply the alignment to. which allows full ' Text justification.from the TextAlign enumeration: Left.Handle of RichTextBox to select text within. though most of us seem too focused on sending a 4=Justify message to a RichTextBox. ' : Set this parameter to False if you want it left intact.Advanced options application Failed.Advanced options were applied. 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. '------------: ' Parameters : RTB -. ' ' The JustifyRTB() function provides this service: '------------------------------------------------------------------------------' Method : justifyRTB (as Function or Subroutine) ' Purpose : Performs advanced alignment features on a RichTextBox. ' Purpose : Provide Fast Text selection service for a RichTextBox. ' : 0 -----. ' and Center Alignment. Center. then select to the end of the Text. sets its length and alignment members. ' USES : RTBFastSelect() function for faster text selection without flicker.NET Beyond the Scope of Visual Basic 6. You can use it like this: justifyRTB(myRichTextBox. it is probably a good idea not to ignore it. in which they usually state. but they fail because they do not address this advanced typography issue that MSDN keeps referencing in their notes. ' : If SelEnd = -1. The listing below is my modJustifyRTB modules. command to the RichTextBox to enable advanced typography options.Handle. ' Returns : <> 0 -. ' : SelectionLength -. EM_SETTYPOGRAPHYOPTIONS. Justify. why not spiff it up further by apply these changes to the text without causing any of the usual and maddening RichTextBox flicker. Issue a SendMessageLong(RTB. when the documentation states that you must also send the RichTextBox a EM_SETTYPOGRAPHYOPTIONS message to enable advanced line formatting. Likely index or length value was invalid. a fourth feature is provided. Right.Start index of text to apply the alignment to. TO_ADVANCEDTYPOGRAPHY.Failure. ' : SelStart -. then use the SelectionStart and SelectionLength ' : properties of RTB to set the range. Right.0 – David Ross Goben For some time there has been a bit of a clatter on the web regarding this issue. and there are a lot. though he did so by brute force as he was trying to help someone else out. '------------: ' NOTE : When SelStart is set to -1 or -2. ' : If SelStart = -2. DLL version 3. ByVal wMsg As Int32. 0.Text.Enhancing Visual Basic . ByVal wParam As Int32.0 – David Ross Goben '------------------------------------------------------------------------------'------------------------------------------------------------------------------Imports System. SelEnd) 'else user speciffied both start and length End If End If Page –444– .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. '******************************************************************************* Friend Function justifyRTB(ByRef RTB As RichTextBox.NET Beyond the Scope of Visual Basic 6. ByRef lParam As CHARRANGE) As Int32 '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '******************************************************************************* ' Method : justifyRTB ' Purpose : Performs advanced alignment features on a RichTextBox. ByVal wParam As Int32. ByVal wMsg As Int32. Optional ByVal SelEnd As Int32 = -1. ByVal wParam As Int32. SelStart.SelStart) 'use selstart to end of document Else RTBFastSelect(RTB.Length .0) End Enum '------------------------------------------------------------------------------'structure used to extend text alignment in a RichTextBox '------------------------------------------------------------------------------<StructLayout(LayoutKind.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr.Text. SelStart. ByVal wMsg As Int32.Handle.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr. 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.InteropServices Module modJustifyRTB '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '------------------------------------------------------------------------------'Enumeratiuon 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.Handle. 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. ByVal Alignment As TextAlign.Handle.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 '------------------------------------------------------------------------------'p/invoke 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. Optional ByVal ResetSelection As Boolean = True) As Boolean '------------------------------------------------------------------If SelStart = -1 Then RTBFastSelect(RTB. RTB. Optional ByVal SelStart As Int32 = 0.Runtime. RTB. so deselect text End If Return Result 'True if we applied advanced options End Function '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '------------------------------------------------------------------------------'structure to specify selection range for alignment option. “myRTB. there is no screen blinking.0 – David Ross Goben '------------------------------------------------------------------Dim fmt As New PARAFORMAT 'set aside Format structure fmt.. EM_EXSETSEL.chrPosnMax = SelectionStart + SelectionLength 'mark end of text with the current selection length Return SendMessage(RTBHandle. Unlike usual selection of the text using 'the SelectionStart and Selection Length options. 0) = TO_ADVANCEDTYPOGRAPHY Then fmt. HorizontalAlignment) 'set alignment normally End If '------------------------------------------------------------------If ResetSelection Then 'should we reset the cursor selection? RTBFastSelect(RTB. If SendMessageLong(RTB. selRange) 'select range of text End If Return 0 'failure if bad indexes End Function End Module NOTE: In more recent versions of the Dot NET framework. fmt) 'apply the alignment instruction Result = True 'successfully applied advanced options End If End If '------------------------------------------------------------------If Not Result Then 'if Advanced Typography not available (unlikely) If Alignment = TextAlign. the need for the above RTBFastSelect() method has been superseded by Dot Net’s own implementation of the RichTextBox control sporting a new overload for their Select() method. EM_SETTYPOGRAPHYOPTIONS. you can provide it with the selection start index and the length of the text to select.wAlignment = CShort(Alignment) 'inform the structure we are setting alignment SendMessage(RTB. With it. 0. 0) 'yes. 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.Enhancing Visual Basic .Handle.Left End If RTB. '------------------------------------------------------------------------------<StructLayout(LayoutKind. and it will replace the need for RTBFastSelect() functionality. EM_GETTYPOGRAPHYOPTIONS.Text.Handle. EM_SETPARAFORMAT. 0.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..dwMask = PFM_ALIGNMENT 'it did. For example.chrPosnMin = SelectionStart 'mark start of text with the current selection length selRange.Handle. so we are going to set a new alignment value fmt.Select(0. If SendMessageLong(RTB.Length)” can be used to fast-select an entire document without causing any screen flicker.SelectionAlignment = DirectCast(Alignment.Justify Then 'Justify selected. Page –445– . myRTB. TO_ADVANCEDTYPOGRAPHY.Sequential)> Private Structure CHARRANGE Dim chrPosnMax As Int32 Dim chrPosnMin As Int32 End Structure '------------------------------------------------------------------------------'Constant used by RTBFastSelect() method. TO_ADVANCEDTYPOGRAPHY) <> 0 Then 'test to see if the above turned on the Advanced Typography Options. or even temporary ' blanking if the RichTextBox when a new alignment value is set.. force to Left alignment Alignment = TextAlign. 0. 0..Handle. 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.NET Beyond the Scope of Visual Basic 6. lately I have found myself using the Power Pack Shape controls more and more. of course. I am also writing several Black Book additions to this document. Indeed. and even easily create rounded rectangles. They are actually stored within a ShapeContainer control. I am slowly going blind. “Self. its width. position it. set its color. or in the Controls collection of a container control. Additionally. On top of that.NET” on page 172). such as this one. The first thing you must understand is that Power Pack Shape controls are not stored in a form’s Controls collection.Enhancing Visual Basic . once you understand how Power Pack Shapes are stored. Although I provided a number of easy methods for computing these shapes in a Paint() event within that article. However. So I thought to myself. I also explained that you could also use the Visual Basic Power Pack Shape controls that you can download for free from Microsoft. A ShapeContainer is automatically created and added to the appropriate Controls list when you add Shape controls to your form. you may encounter a lot of flicker on the form if the form updates regularly. if you have quite a number of shape controls on a form. and I am also developing a complete sample email package that takes full advantage of my email classes featured earlier in this document (see “Send (SMTP) and Retrieve (POP3) Email with Ease under VB. This is actually rather easy to do. which is in turn stored in the Controls collection of a form or other container control. or whatever. if a rectangle or ellipse background was not transparent. I will certainly always find plenty of things to do. But anyway. please. is to parse the form these controls are located on and generate Paint() event code to render them instead by drawing them ourselves. “Navigating Your Way Through Visual Basic 6. But at the same time I wished I could still enjoy the incredible speed of drawing through Paint() events. I am upgrading my supercharged VB6 VisualProCalc programmable calculator application to VB. the truth is most people do not want to be troubled by the bother. such as teach. I pointed out that I prefer to use Paint() event code to draw these shapes because they are much faster. Tab Control. or it seems complicated. No sympathy. This is eliminated by using Paint() event code. I really want to still render these shapes through a Paint() event to regain all that lost execution speed. when they simply want to “pretty” their form up with some shapes and focus on the code behind the form. I am presently developing a super spreadsheet/database application that features a rich and powerful programming language that includes its own compiler.0 to Visual Basic . For example. such as a Picture Box. I explained how to use the Paint() event of a form or control to draw lines and shapes (see “Easy Ways to Draw Lines and Shapes. a Panel. Paint() event drawing is a whole lot faster. so what do I need to do to convert all the data from these Shape controls and render them using much faster drawing techniques?” The answer. my own linedrawing paradigm has changed so that I find less time to fiddle with computing line positions.NET. and to Paint in VB.NET” on page 233).NET Beyond the Scope of Visual Basic 6.” On top of all that. For example. its fill. The advantage of using the Power Pack Shape controls is that they offer the convenience of shape controls in VB6 in that you can drop a shape on a form. All Shape Page –446– . plus for my free companion document. Since writing that article.0 – David Ross Goben Black Book Tip # 31 Easily Replace Power Pack Shape Controls with Faster Paint() Event code Earlier in this document. though this Power Pack comes pre-bundled in Visual Basic as of VB2010.NET Application Upgrades. just to spend less time computing line positions. if you later re-add new Shape controls to that surface. For example. very tight code that is our best friend when parsing such generational levels. End If Next Of course. This is because a form’s Control collection inherits from a Control’s Control Page –447– . However. which all derived objects inherit from it. and it knows the type of object the wrapped item is. One will be attached to the form and contain all the Shape controls you dropped on the form. to find these items.. So what do you do? Easy! Just treat them all instead as Control. inner levels of controls. But the end result is. a ShapeContainer does. The solution to this problem is really extremely simple: instead of treating and parsing each item held within a Controls collection as a Control. Regardless.NET Beyond the Scope of Visual Basic 6. For example. then two ShapeContainer controls will be generated. though the complaints come from developers who are trying to cast them back to a ShapeContainer.VisualBasic.Controls 'check each control in the collection If TypeOf Cntrl Is Microsoft. then once the last Shape control contained within it is deleted. RectangleShape. exploring a directory tree or a TreeView control are child’ play when using recursion. you will need to scan through not only a form’s Controls list. such as Private Sub RecursivelyScan(ByRef Cntrls As ControlCollection). so process the ShapeContainer. one ShapeContainer control will be generated for each control the shapes are dropped on. Were you to later delete all Shape controls that were stored in a ShapeContainer. but recursion is easy.Enhancing Visual Basic . which of course includes the wrapped item (all classes you create also ultimately inherit from type Object. When any item is wrapped within an Object type. to include the Controls list of further. if you drop a few Lineshape controls onto the form background. fast. if you have dropped several Shape controls on a form and its controls. a new ShapeContainer will be generated and attached to the surface Controls collection.ControlCollection lists. once we have found our ShapeContainer control. or so it seems from the complaints I have seen on the web. which is how it can be contained within a Controls list. and another ShapeContainer will be generated and attached to the Panel. Two other things have stood in the way of most people successfully parsing these controls. The trick. and you can acquire that original type through the Object’s GetType property.ControlCollection list (sounds like someone has a stutter). the Object type simply retains the item entirely intact. if you specify any field as a simple Control Collection. because this collection differs from a Control’s Controls collection). you ARE able to cast an Object to a ShaperContainer type.0 – David Ross Goben controls that are located on a particular container will feature its own ShapeContainer control. but also the Controls lists of any child control that in turn has children. 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. which means that we will be able to cast a more generic Shape object into a LineShape.PowerPacks. Some developers will break into a cold sweat when you mention recursion. 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. you are going to be treating each Controls list as a list from a Form. Because the Forms class is auto-imported into any form project. but it is one of the base requirements in .NET that all object inherit from Object). is how to process these Shape controls. the Shape controls are all derived from a base Microsoft. which is a method that can invoke itself. This would be accomplished by implementing a recursive method.ShapeContainer Then 'a shape container found? 'yes. and contain all the Shape controls you dropped on the Panel. So.ControlCollection list to a Form. without actual conversion. the interface yells at them because they cannot cast a Control. instead treat it as an even more generic Object.PowerPacks.Shape class. then that ShapeContainer control is automatically removed.Shape control list here. a Shape control does not inherit from the more generic Control class. and dropped a few OvalSape or RectangleShape controls on a Panel. The first is the fact that when a person tries to write a recursion routine to parse a Controls list. Unlike other controls. Of course. or OvalShape. This is because a Controls list for a form is different from a Controls list from a control. and will contain only those shapes that have been dropped onto that control. However.. though you do not specify it.VisualBasic. but these features are so-little used that I will not tackle them in this treatise (but when they are used.0 – David Ross Goben collection (this is starting to sound like Abbot and Costello’s famous radio broadcast. etc. its border width. if it has generated any paint event code. its border color. Change or add to this code as you please. You can place the invocation of this method anywhere. its background color if its background is opaque. to determine if it should be drawn as a solid line (typical) or some other rendition. and even then. they are checked for having children. and this is in turn passed to the recursive method. its line color. you must consider its start coordinate. the solution is simple: You simply invoke yet another method that is specifically designed to parse a Shapes collection. However. This tiny method basically passes the form’s Controls collection to a private recursive method that scans all controls (as type Object). you must consider its start and end coordinates. one named pnlScroll. to a ShapeContainer. With this information. Paint() events should only be generated if there is data for them to process. and review the listed paint event or events. its border width. and if its corners are rounded. we will list it again. • When handling an OvalShape. we test for each type of shape and process them individually. which used to be called an Ellipse in VB6. Other considerations are background image and background patterns. • When handing a RectangleShape. Another consideration is the code generation. 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. and by how much. Note that rounded rectangles are not supported by the Graphics object. I addressed all these issues and created a module named modShapeConvert that exposes a single method. If they are a normal control. not trudge off on some long explanation of its usage (such as. and the other named pnlPickKeys: Page –448– . First. though I will simply invoke it from my current method.NET Beyond the Scope of Visual Basic 6. its width. its line width (thickness). what you end up with is an exposed method the user invokes to process a form. and it’s Dash Style. For example. You can then paste this clipboard data into a text editor. There. its width. the border’s Dash Style. their objects should be disposed of only if they are created. At the end of this method. The other thing they run into is that they try to parse this Shape’s list as a Controls list. its height. the border’s Dash Style. thicknesses. “Who’s on First?”). they are a God-send). you must consider its start coordinate (the top-left corner of an imaginary bounding rectangle). which clearly will not work. but I typically place its at the end of the Form’s Load() event. which were placed on two panels. Second. its border color. what is an HDC?) – see the article above for that). but we can take full and easy advantage of my RoundedRectangle() method described earlier in the fore-mentioned article. For example. and its background color if its background is opaque. create a Brush object to render any opaque backgrounds.Enhancing Visual Basic . such as dashes. One other consideration is for the optimal definition and use of a Pen or Brush object. “Easy Ways to Draw Lines and Shapes. Lastly.. So.NET” (for easy access. You supply a form name to the method (or simply Me if you are invoking it from the form you want to scan). If a ShapeContainer is found. which we now know to be in fact a ShapeContainer. we can create a Pen object that is used to draw borders. ShapeConvert(Me). meaning that they feature a Controls collection that itself contains other controls. This keeps each method small and compact and specific to their tasks. it is passed to another method that simply parses its Shapes collection after we Directcast the Object. and to Paint in VB. or whatever. such as NotePad. it will inform you in a dialog that it has placed this code into the clipboard. and use the Graphics Drawing methods to render these objects. so we can in turn reference its Shapes collection. ShapeConvert(). each should only be defined once. only when they actually change. styles. dots. • When handling a LineShape control. though. 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. dot-dashes. their properties should be updated to new colors. and paste what portions you want into you form’s code (I typically paste it all at the end of the form’s code). so we do not end up with empty Paint() event code blocks. its height. here. 0!) g. 168) g.Graphics Dim Pn As New Pen(Color.DrawRectangle(Pn.Graphics Dim Pn As New Pen(Color.Width = 3. 2.DrawRectangle(Pn. What follows is my modShapeConvert module code: Page –449– . 240. 132.DrawLine(Pn.Dispose() End Sub These drawing instructions perfectly duplicated the operation of the LineShape and RectangleShape controls I had placed on my form.0 – David Ross Goben 'Add this code to the Paint() event for the pnlScroll Control Private Sub PnlScroll_Paint(sender As Object. 192. 132. 240.DrawLine(Pn. note that if no code was generated. 0. 96) g. 240. 240. 384.Color = SystemColors. that you will also be notified of that.DrawLine(Pn. 360) g. 96. 25. Lastly. 288. 336. so I afterward deleted all my Shape controls (after backing up my code. 360. 620.Width = 2. 168) pn. 132. 360.Black. 0.Black. 232. 360) pn. I was able to further remove the Microsoft. 360.VS reference from my application references (do this via the Unused References button. 232. 48. 96. 200. 200. 144. which will ensure the reference is indeed no longer required).NET Beyond the Scope of Visual Basic 6.DrawLine(Pn.DrawLine(Pn. 288. 0.DrawLine(Pn. 96.DrawLine(Pn.DrawLine(Pn. 240. 336. Afterward. 232.DrawRectangle(Pn. 360. 624. 48. 576. 240.Paint Dim g As Graphics = e. which in turn removed the auto-generated ShapeContainer controls. 288. 132. 232) g. 36) g. 288. 1. 288. e As PaintEventArgs) Handles pnlPicKeys. 480. 296) g. 328) g. 232) g. 388) g. 192.Dispose() End Sub 'Add this code to the Paint() event for the pnlPicKeys Control Private Sub pnlPicKeys_Paint(sender As Object. 384. 232) g. 168) g. 360. 200. 200) g.DrawLine(Pn. 240. 0. 168. Running them both together proves to me that they do not differ.PowerPacks.ControlDarkDark pn. informing you that you 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.Paint Dim g As Graphics = e. 620. 168) g. 624. 48. 576. 145. 624. 0. 48. 240.DrawLine(Pn. 296. 360) g. which could explain why you will find the clipboard void of any generated code in that case (though it might contain any data you had last placed in it).DrawLine(Pn.DrawLine(Pn.DrawLine(Pn. 1. 96. 1. 21) pn. 360) g. Note that if rounded rectangles were used. 384.0! g. 392) pn. 232) g.DrawLine(Pn.VisualBasic. 168. 132) g. 200) g.0!) g. 144. 392. 96. just to be safe.Enhancing Visual Basic .DrawLine(Pn. 240.DrawLine(Pn.DrawLine(Pn.Width = 2.DrawLine(Pn. 96) pn. of course).DrawLine(Pn. 384. 132) g. 328. a note will be added to the generated code. 328. 144.DrawLine(Pn. 576.DrawLine(Pn. 96.DrawRectangle(Pn.DrawLine(Pn. 384. 240.Width = 2.0! g. 336. 392) g. 384. 360. 36) g. perhaps due to no Shape controls being found on a form. 97) pn.DrawLine(Pn.0! g. e As PaintEventArgs) Handles PnlScroll. 96. 480.0! g. ' If an error is reported for the line below. dash style.".PowerPacks. ensure these references exist.SetText(MasterData.NET. For example.Controls. Imports PowerPack = Microsoft. 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. TextDataFormat.ControlCollection. so process the ShapeContainer.ShapeContainer Then 'a shape container found? 'yes. Note that " & "if you remove all shape controls from its wrapping auto-constructed " & "ShapeContainer. Nothing) 'process controls on the form If MasterData IsNot Nothing Then 'any data written to the output? Clipboard. MsgBoxStyle. This will build paint event code to reproduce all ' LineShape.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Option Strict On Option Explicit On 'NOTE: The following assumes you have . "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. although they can be added as needed. translating it to VB.VS. ParentControl) Else Dim Ctl As Control = CType(Cntrl.ShapeContainer).Enhancing Visual Basic . which are different from normal controls on the form ShapeConvertParseShapse(DirectCast(Cntrl. ' ' The exposed ShapeConvert() method will construct Paint Event Code for drawing lines ' rather than use shape Controls." & vbCrLf & vbCrLf & "Once you have applied these changes to your code. Draw2D = System. but an Object can. Note that we parse ' : through the controls list. color. ParentForm = Frm 'save this parent form ShapeConvertParseControls(Frm.Shape control list.OkOnly Or MsgBoxStyle. or Oval Shape controls were found on this form. Ctl) 'yes. Rectangle. so recurse. Paste them into a text editor for review " & "and edits. and make this control the parent End If End If Next End Sub Page –450– . because a ShapeContainer can be ' 'parented by a form.Shapes.".NET references in your project properties ' for System. This is ' 'needed. the ShapeContainer will be auto-removed.. What we are actually looking ' : for here are ShapeContainer controls in this method. which is required if rounded rectangles are featured.VisualBasic. RectangleShape. check out the non-shape control If Ctl.Drawing. then past the data as needed into your code.PowerPacks.Drawing (usually) and also Microsoft. PowerPack. if not transparent. ' ' Rounded Rectangle Support is provided by the RoundRectangle() method defined within ' the modRoundRect module. because it is actually the ShapeContainer wrapped ' : in a generic Object box. ' ' Certain features are not yet considered in this solution. Control) 'otherwise. Private MasterData As String = Nothing 'storage for conversion code output. which can easily be cast to the proper type.Text) 'yes. such as background images ' and patterns.Information. to include their thickness. it tended to ' blink as the lines disappeared and then redrew. '------------------------------------------------------------------------------'------------------------------------------------------------------------------Module modShapeConvert '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private ParentForm As Form = Nothing 'storage for form ID purposes. ByRef ParentControl As Control) For Each Cntrl As Object In Cntrls 'check each control in the collection If TypeOf Cntrl Is PowerPack. and OvalShape Controls from the Visual Basic PowerPacks ' faithfully.Controls.HasChildren Then 'if the form has controls. not as Control ' : objects. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub ShapeConvertParseControls(ByRef Cntrls As Control. you can " & "remove the associated shape controls from your forms.OkOnly Or MsgBoxStyle. if required.. When the form refreshed. Private LocalData As String = Nothing 'local construction for each ShapeContainder list. MsgBoxStyle.HasChildren Then 'is it a container for other controls? ShapeConvertParseControls(Ctl. 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. "Paint Event Replacement Code for Shapes Generated") Else MsgBox("No Line.VisualBasic.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.Information. because a generic Control class cannot be cast to a ShaprContainer ' : class. if not another control. and the calculator form features over 180 ' controls and about 40 lines and rectangles. and rectangle or oval ' (ellipse) background color. I was working on my VisualProCalc ' calculator. treating therm as objects. " & Y1. 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. LineWidth. " & "New Rectangle(" & X1.ToString & ".ToString & ". " & Y1.DrawRectangle(Pn. if opaque For Each Shp As PowerPack. " & Y1.BorderStyle 'get the border drawing style Dim BackColor As Color = .BorderWidth 'get how wide the border is Dim BrdrStyle As Draw2D. PowerPack.BorderWidth 'get how wide the line is Dim LineStyle As Draw2D.ToString & ".0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : ShapeConvertParseShapse ' Purpose : Parse through a ShapeContainer. PowerPack. LineStyle) '--------------------------------------------------------------'draw the line '--------------------------------------------------------------LocalData &= (" g. BrdrColor. BrdrStyle) '------------------------------------------------------------------'draw the rectangle '------------------------------------------------------------------If Crnr = 0 Then 'normal..ShapeCollection. BrdrWidth.CornerRadius 'grab corner radius in case of rounded rectangles Dim BrdrColor As Color = .Location.GetHdc. pn. " & Hght.DashStyle.Enhancing Visual Basic . " & Y2.RectangleShape Then 'if a RECTANGLE Shape.RectangleShape) Dim X1 As Int32 = .BackStyle = PowerPack.Solid 'assume solid DashStyle to be Solid (default) Dim BrshClr As Color = Nothing 'brush used to draw rectangle background.Shape control list and process all ' : LineShape and RectangleShape controls.FillRectangle(Brsh.. " & Hght. square corners = 0 LocalData &= (" g. With DirectCast(Shp.Location. but has the brush color been defined yet? BrshClr = BackColor 'no.BackStyle. building paint event code ' : for each form or control that contains them.ToString & ")" & vbCrLf) If .DashStyle = . DashStyle. pnWidth. " & Wdth. " & Y1. pnWidth.ToString & ".ToString & "))" & vbCrLf) End If Else 'Rounded Rectangle NeedModRoundRect = True 'mark that we need modRoundRect If .ToString & ".GetHdc. Color.ToString & ")" & vbCrLf) End With Page –451– .Y1 'get top coordinate Dim Y2 As Int32 = . DashStyle.Color = " & TranslateColorToText(BackColor) & vbCrLf) End If LocalData &= (" g.BorderStyle 'get the line drawing style '--------------------------------------------------------------'------------------------------------------------------------------'update pen properties as needed and output the appropriate text data '------------------------------------------------------------------ProcessPenData(pnColor. " & Wdth. " & Wdth.Shape In ShpCollection 'scan each contained shape control '=========================================================== 'RECTANGLE SHAPE '=========================================================== If TypeOf Shp Is PowerPack. " & Wdth.LineShape Then 'if a LINE Shape.X 'get the left coordinate Dim Y1 As Int32 = .ToString & ".ToString & ". " & X1.Y 'get top coordinate Dim Wdth As Int32 = . pn.BorderColor 'get the line color Dim LineWidth As Single = .ToString & ". so update to the new color and generate change for it LocalData &= (" Brsh. BackColor.ToString & ". but doe the colors differ? BrshClr = BackColor 'yes.Y2 'get the bottom coordinate Dim LineColor As Color = .Transparent.ToString & "))" & vbCrLf) End If End If End With '======================================================= 'LINE SHAPE '======================================================= ElseIf TypeOf Shp Is PowerPack..BackStyle = PowerPack.Height 'get height of rectangle Dim Crnr As Int32 = .DashStyle = .ToString & ".Opaque Then 'draw with a filled bckgroud if opaque LocalData &= (" RoundRectangle(g.NET Beyond the Scope of Visual Basic 6.X2 'get the right coordinate Dim Y1 As Int32 = .ToString & ".DrawLine(Pn.Width 'get width of rectangle Dim Hght As Int32 = .BorderColor 'get the border color Dim BrdrWidth As Single = . " & X1.X1 'get the left coordinate Dim X2 As Int32 = .BackStyle.Opaque Then 'is the background opaque (visible)? If BrshClr = Nothing Then 'yes.Size.. New Rectangle(" & X1.DashStyle = Draw2D. With DirectCast(Shp.ToString & ". " & Hght.ToString & ". " & Y1.ToString & "))" & vbCrLf) Else 'draw with a transparent background if not LocalData &= (" RoundRectangle(g.Size.ToString & ". 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.BackColor 'get the background color '------------------------------------------------------------------'update pen properties as needed and output the appropriate text data '------------------------------------------------------------------ProcessPenData(pnColor. " & "New Rectangle(" & X1. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub ShapeConvertParseShapse(ByRef ShpCollection As PowerPack.ToString & ".LineShape) Dim X1 As Int32 = . LineColor. " & Hght. " & X2. Name & ".Name & " Control" & vbCrLf & " Private Sub " & ParentControl.Y 'get top coordinate Dim Wdth As Int32 = . " & X1.BorderColor 'get the border color Dim BrdrWidth As Single = . but doe the colors differ? BrshClr = BackColor 'yes. With DirectCast(Shp.Size." & vbCrLf & " '---------------------------------------------------------------------------" & vbCrLf) NeedModRoundRect = False 'reset flag End If '----------------------------------------------------------------------'Generate simplified graphics interface '----------------------------------------------------------------------MasterData &= (" Dim g As Graphics = e. 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.Dispose" & vbCrLf) 'we know this has been generated if local data exists If BrshClr <> Nothing Then 'if Brush Color had been defined.Paint" & vbCrLf) Else 'if the parent is the form.Name & "_Paint(sender As Object.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.ToString & ". " & Wdth.FillEllipse(Brsh.BackStyle. write the event header for the form's Paint event MasterData &= (vbCrLf & " 'Add this code to the Paint() event for the " & ParentForm.Location.Size. " & X1. e As PaintEventArgs) Handles " & ParentControl.ToString & ".Enhancing Visual Basic . " & Y1.0 – David Ross Goben '======================================================= 'OVAL (ELLIPSE) SHAPE '======================================================= ElseIf TypeOf Shp Is PowerPack.Graphics" & vbCrLf & vbCrLf) '----------------------------------------------------------------------'add local data to the master data '----------------------------------------------------------------------MasterData &= (LocalData & vbCrLf) '----------------------------------------------------------------------'close out event code '----------------------------------------------------------------------MasterData &= (" pn.DashStyle = .Width 'get width of rectangle Dim Hght As Int32 = .Height 'get height of rectangle Dim BrdrColor As Color = . BrdrStyle) '------------------------------------------------------------------'draw the Ellipse (Oval) '------------------------------------------------------------------LocalData &= (" g.NET Beyond the Scope of Visual Basic 6.. " & Y1.BackStyle = PowerPack. then a Brush object had been used MasterData &= (" Brsh.BorderStyle 'get the border drawing style Dim BackColor As Color = . " & Wdth. e As PaintEventArgs) Handles Me.ToString & ".Color = " & TranslateColorToText(BackColor) & vbCrLf) End If LocalData &= (" g.Location. PowerPack.ToString & ")" & vbCrLf) If . but has the brush color been defined yet? BrshClr = BackColor 'no.. BrdrWidth. BrdrColor.X 'get the left coordinate Dim Y1 As Int32 = .Name & "Form" & vbCrLf & " Private Sub " & ParentForm.BackColor 'get the background color '--------------------------------------------------------------'update pen properties as needed and output the appropriate text data '--------------------------------------------------------------ProcessPenData(pnColor. " & Hght.BorderWidth 'get how wide the border is Dim BrdrStyle As Draw2D. so update to the new color and generate change for it LocalData &= (" Brsh. DashStyle.Name & "_Paint(sender As Object.DrawEllipse(pn.OvalShape) Dim X1 As Int32 = .Paint" & vbCrLf) End If If NeedModRoundRect Then MasterData &= (" '---------------------------------------------------------------------------" & vbCrLf & " 'NOTE: modRoundRect module is required to support RoundedRectangle() method. pnWidth.Opaque Then 'is the background opaque (visible)? If BrshClr = Nothing Then 'yes.ToString & ". write the event header for that control's Paint event MasterData &= (vbCrLf & " 'Add this code to the Paint() event for the " & ParentControl.OvalShape Then 'if Oval Shape.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 Page –452– .ToString & ". OldColor = NewColor 'assume new color and width data and define the Pen object OldWidth = NewWidth LocalData &= (" Dim Pn As New Pen(" & TranslateColorToText(NewColor) & ".Color = " & TranslateColorToText(NewColor) & vbCrLf) End If If OldWidth <> NewWidth Then 'if the new pen width does not match the old..dll" Alias "RoundRect" (ByVal hDC As IntPtr. OldWidth = NewWidth 'update the width and update the Pen object LocalData &= (" pn. _ ByVal X2 As Integer. OldStyle = NewStyle 'update the dash style and update the Pen object LocalData &= (" pn. Width.FromArgb(&H" & Hex(Clr. _ ByVal X1 As Integer. If the Pen color has not yet been ' : defined. Return its handle Private Declare Function CreatePen Lib "gdi32.Idx .ToString 'grab text version of color Dim Idx As Int32 = InStr(Value. then instantiate the Pen object with the new color and width.Idx .Enhancing Visual Basic .Transparent) ' Rect : Rectangle structure containing the start location (X.IsSystemColor Then 'is it a system color? Return "SystemColors.ToString & vbCrLf) End If End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : TranslateColorToText ' Purpose : Translate color value to SystemColor or Color Value. ByVal NewStyle As Draw2D.NET Beyond the Scope of Visual Basic 6. 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. _ ByVal crColor As Integer) As Integer 'create a solid brush object. so. which is used by the above code: Option Strict On Option Explicit On Module modRoundRect '************************************************************************************************************* 'RoundRectangle: ' Draw a Rounded Reactanle. so apply standard color naming Else Return " Color. _ ByVal CornerWidth As Integer. Return non-zero for success Private Declare Function RoundRect Lib "gdi32..DashStyle = " & NewStyle. use its ARGB value '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function TranslateColorToText(ByVal Clr As Color) As String Dim Value As String = Clr.. Idx + 1.1) 'yes. Reutn the old handle for the type of object Private Declare Function SelectObject Lib "gdi32. Return its handle Private Declare Function CreateSolidBrush Lib "gdi32.Width = " & NewWidth.ToString & "!" & vbCrLf) End If End If If OldStyle <> NewStyle Then 'if the line's border style does not match the last-set.. Idx + 1.dll" Alias "SelectObject" (ByVal hDC As IntPtr..dll" Alias "CreateSolidBrush" (ByVal crColor As Integer) As Integer 'Select/replace an object. ByVal NewColor As Color. so apply FromArgb() method End If End Function End Module Though I dislike redundancy.." & Mid(Value..ToString & "!)" & vbCrLf) Else 'or else we can assume the pen has been defined. "[") 'find bracket..0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : ProcessPenData ' Purpose : Determine how to output the Pen data.dll" Alias "CreatePen" (ByVal nPenStyle As Integer.IsNamedColor Then 'is it a known color? Return "Color. ByRef OldStyle As Draw2D. OldColor = NewColor 'update the color and update the Pen object LocalData &= (" pn. so use SystemColors naming ElseIf Clr. If OldColor <> NewColor Then 'if the new pen color does not match the old. ' : otherwise update its Color.DashStyle) If OldColor = Nothing Then 'if the pen color has not yet been defined. Width. Len(Value) . _ ByVal PixelWidth As Integer.ToArgb) & ")" 'not known.Y). _ ByVal Y1 As Integer. ' : if the color is unknown. _ ByVal CornerHeight As Integer) As Integer Page –453– . _ ByVal Y2 As Integer. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub ProcessPenData(ByRef OldColor As Color. 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. and DashStyle as needed. " & NewWidth. What follows is the modRoundedRect module listed much earlier in this document.1) 'yes. it has its merits when it comes to convenience. if it exists If Clr.. Len(Value) .." & Mid(Value. ByVal NewWidth As Single. _ 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.DashStyle. ByRef OldWidth As Single. _ rect As Rectangle.Size.Y.Height.Height. _ rect.Location. hBrush) 'draw rounded rectangle RoundRect(hDC. oldhBrush) 'Delete the resources of our Pen and Brush DeleteObject(hPen) DeleteObject(hBrush) End Sub Public Sub RoundRectangle(ByVal ByVal ByVal ByVal ByVal Dim oldhPen As Integer Dim oldhBrush As Integer hDC As IntPtr.Y. ARGBtoRGB(Pn. _ CornerRadius. _ rect. vARGB And &HFF) 'return RGB color End Function End Module Page –454– . _ rect. hBrush) 'draw rounded rectangle RoundRect(hDC.Location.X + rect. _ rect.Location. _ CornerRadius As Integer) 'store old Pen handle 'store old Brush handle 'Declare our local Pen and Brush Dim hPen As Integer = CreatePen(PS_SOLID. CornerRadius) 'restore the old Pen and Brush SelectObject(hDC. _ rect. (vARGB >> 8) And &HFF. oldhPen) SelectObject(hDC. _ ByVal rect As Rectangle.ToArgb 'convert color value to AARRGGBB Return RGB((vARGB >> 16) And &HFF.X. rect. ARGBtoRGB(Pn)) Dim hBrush As Integer = CreateSolidBrush(ARGBtoRGB(BackColor)) 'create our Pen and save its handle 'or create our color brush 'select our Pen and Brush and save the ones we are replacing oldhPen = SelectObject(hDC.X. CornerRadius) 'restore the old Pen and Brush SelectObject(hDC.Location.Location.Location. _ ByVal BackColor As Color. _ rect.Size.Width. CInt(Pn.0 – David Ross Goben '************************************************************************************************************* 'RoundRectangle Method '************************************************************************************************************* Public Sub RoundRectangle(ByVal hDC As IntPtr. hPen) oldhBrush = SelectObject(hDC.Location.Width. _ CornerRadius. hPen) oldhBrush = SelectObject(hDC.Width).Size.Size. 1. _ BackColor As Color.NET Beyond the Scope of Visual Basic 6.Color)) Dim hBrush As Integer = CreateSolidBrush(ARGBtoRGB(BackColor)) 'create our Pen and save its handle 'create our color brush 'select our Pen and Brush and save the ones we are replacing oldhPen = SelectObject(hDC. _ Pn As Color. _ ByVal CornerRadius As Integer) Dim oldhPen As Integer 'store old Pen handle Dim oldhBrush As Integer 'store old Brush handle 'Declare our local Pen and Brush Dim hPen As Integer = CreatePen(PS_SOLID.Y + rect.Location. _ ByVal Pn As Pen.Enhancing Visual Basic . oldhBrush) 'Delete the resources of our Pen and Brush DeleteObject(hPen) DeleteObject(hBrush) End Sub '************************************************************************************************************* 'ARGBtoRGB Method 'Helper function to covert Color ARGB value (AARRGGBB) to RGB (00BBGGRR) '************************************************************************************************************* Private Function ARGBtoRGB(ByVal clr As Color) As Integer Dim vARGB As Integer = clr. oldhPen) SelectObject(hDC.Y + rect.X + rect. rect. but it is not. the introduction of the UserWaitCursor property. or F# control. The problem here.MousePointer = vbHourglass to: System. perhaps in VB. But the UserWaitCursor property does not address displaying any other cursors. a UseWaitCursor property was added to all forms.NET attaches Control lists to each form as a Controls collection. Page –455– .Forms. it returned to the newly-set cursor when you move the mouse pointer back over the form background.NET is an object-oriented environment that is distinctly separate from the screen. when set to True. This is why setting the screen cursor does not affect a VB. it changed a simple VB6 screen cursor-changing command. and each of its visible controls were treated as a part of the surface of that screen. and the hourglass would display all across the form. This is because VB6 was a child of the screen surface. like: Screen. they automatically inherited.NET. Long ago it might have. you would set the screen. perhaps up until VB2005.Mousepointer property to a desired cursor. however. in-as-far as the Wait Cursor is concerned.0 – David Ross Goben Black Book Tip # 32 Dealing with the Form Cursor not displaying over Form Controls Under VB6. We did not tell it to change for its controls. Because VB. for example. However. such as panels and picture boxes.NET. you will see blogs and chat boards scattered with queries about how to in fact update across all controls on the form.Forms. because they each have their own separate cursor interfaces that affect the cursor appearance when the cursor moves over them.WaitCursor 'WaitCursor = Hourglass Probably the worst thing about this change was that it does not do anything. This might give you a blank expression and seem like a lot of work. though still named Controls. as it will on each of the form’s other controls.NET Beyond the Scope of Visual Basic 6. C#. If you look for assistance online for treating cursors other than the Wait Cursor like this.Cursor = Cursors.Windows. which can in turn sport control lists containing controls that treat those container objects as their parent.Cursor. which. and is stated to be of type ControlCollection.Enhancing Visual Basic . Because .Cursors. it is not the same type of ControlCollection as those other objects. The typical and correct answer is to propagate the new cursor setting to all of the form’s controls. you can quickly For…Each your way through them in a fraction of a millisecond. We did this simply because the cursor would actually update when it passed over the form. Some people express profound aggravation for the simple fact that a Form Control List. will display the Wait Cursor on the form and all of its child controls.WaitCursor”. not the form.Current = System. However.Windows. maybe before it was finally fully object-oriented. Because those other controls are not parented by the form. just like the Controls containers for a Panel and a picture box. they are therefore not referenced in the form’s own Controls collection.NET’s infancy. especially custom cursors. and so whatever the screen cursor was set to. we simply change the “upgraded” line to a simpler instruction. If you move over a control that is on the form. it therefore treats the screen as a wholly separate object. This makes perfect sense. Sadly. such as the hourglass. and then continued to march through the other changes needed in the application. this can get a bit trickier when the form also features container controls. changing the screen cursor will have no affect on the object-based environment of . Obviously. is that the cursor will display the new cursor value only over the form’s background. over its controls and everything. and it displays all of its forms as window objects that are anchored on the screen. because we had told it to set the form’s cursor to a wait cursor. such as “Me. When this code was upgraded to VB. but I do know that it certainly does not function as of VB2008. when we encounter them as we scour upgraded code for applying additional suggested changes. hence.NET. the cursor reverts to that control’s default cursor. Because these cursor changes are also tagged in the list of notated upgrades in code migrated to VB2008. ControlCollection. SetCursorToChildren(Me.ControlCollection object. ' : Note also that you can assign from the Cursors collection. along with its recursive support method. the System.. if you define your processing method to reference only a Control. and as such. named “SetCursorToChildren()”: 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : MeCursor ' Purpose : Set cursor to more than just the main form ' Usage : When assigning a new cursor to the form. or even an archaic ScreenCursor.Default.Cursor = Value 'set its cursor If Cntrl.HasChildren Then 'does it have child controls? SetCursorToChildren(Cntrl. It is also incredibly fast. which will be used to replace the “Me.Cursor” with “MeCursor”. Even so. Make it comfortable for you. but after you have set MeCursor to the non-default state. Value) 'process its child controls End If End If Next End Sub Normally. ByRef Value As Cursor) For Each Cntrl As Control In ControlList 'process all child controls If Cntrl. then you should set it to the non-busy state. this solution has served me very well and has never failed me.Cursor 'return the form's current cursor setting End Get '-----Set(value As Cursor) If Me.NET Beyond the Scope of Visual Basic 6.. 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property MeCursor As Cursor Get Return Me. I would replace the usual “Me. even when you pass it the Controls collection from a form. ' : Unlike the UseWaitCursor property. My standardized solution is to paste a boiler-plate Property at the top of each form that is always named MeCursor.Cursor <> value Then 'is the form cursor already set to this? Me. or load a cursor from resources. 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. not me. such as Cursors. I place the common SetCursorToChildren() support method in a common module so that all forms can share it. If you feel froggy. such a STOP button in a Search dialog. You can afterward reasign child control cursors. The confusion usually stems from the fact that within form code. even on massive forms that are heavily laden with controls and layers of controls.ControlCollection is inherited from a Control. a form contains a Forms. a Form. such as “MeCursor = Cursors.Forms. then jump.. wherever I am setting the form’s cursor.ControlCollection list. it automatically assumes the Form variation. assign it using: MeCursor = NewCursor.Windows. and the other container objects.Controls.Cursor = value 'no.. Regardless. The MeCursor property is defined below. so set cursor to the main form.WaitCursor”.Enhancing Visual Basic .Visible Then 'is the control visible? Cntrl. If Me.Cursor” part of a cursor assignment.Form class is obviously inherited. instead. everything will still be hunky-dory.HasChildren Then 'and if it also has children.0 – David Ross Goben Specifically. Page –456– . and so when you simply type an undecorated ControlCollection. being themselves controls. NOTE: Some people would prefer to name this property CurrentCursor instead of MeCursor. NOTE: If you have a control that should actually retain a non-busy cursor. Thus.ControlCollection.Controls. instead contain a Control. this meant that three long synchronization (synch) “leaders” had to be written for each item. heavier-than-lead AT&T handset pressed into a modem cradle (AT&T’s monopoly back then. the data was recorded at only 500 baud. such as adding an option to read and write tapes at a then-blazing 1500 baud on better quality tape machines. Fortunately. and was used to verify that the integrity of the data transfer was to be assumed OK. hindered telecommunications technology far more than we want to ever dare guess. and it wrote a much shorter 50-byte leader. If that was not bad enough. because the receiver. which was a value of 2.77MHz. likely in an attempt to soften the common view back then of them having been an evil monopoly. After all that. or. These used (pre-owned is the idiotic newspeak word now used in its place) devices were popular Page –457– . which medium-quality cassette tape machines could reliably support. which was also famously known as Level II BASIC. Other innovations came from developers. This was one of the main reasons for their breakup by the US Government back in 1982. different ASCII values. and additional. if you were to write three 16-bit Short Integers (considered regular Integers back in those ancient 8-bit computer days) to the cassette. Phil Allen. using a black. and used only an 16-byte synch buffer between contiguous data blocks. which is to say. far away. was a cassette interface that used a 16-bit integer length value. A Checksum in this case was usually the simple addition of all data bytes sent. One of my very first assembly language programs (written in the code the computer’s Central Processing Unit (CPU) actually understood). to the computer’s reading software. though they were made a monopoly in the first place to prevent copycat knock-offs from robbing them of all their initial technological development and deserved profits. which ripped open the flood-gates for all the regional “Baby Bells”.Enhancing Visual Basic . but surprisingly reliable. we started the entire process over again on the next data item. most of the sudden proliferation of telecommunications advances were coming from AT&T’s own Bell Laboratories. where they developed the massively powerful and feature-rich BASIC-80. New Mexico. worked for Ed Roberts at MITS in Albuquerque. running at 110 baud. to a more reliable Checksum Integer. So. and after each a 8-bit length byte was sent out. Interestingly. This was still faster than logging onto bulletin boards on the budding Internet in those days.0 – David Ross Goben Black Book Tip # 33 Passing Scalar Values and Strings to and from a Byte Array A long time ago in a galaxy far. and Monte Davidoff. though even this was tainted by corrupt politicians and money changing hands in secret. summed each value. was sent so the reader could verify the data was sent OK. representing the number of bytes to follow. which allowed me to transmit blocks of data of up to 32K at a time. and finally the two bytes of the Short Integer was sent. Cassette software innovations were quick to follow. But that still was not fast enough. we saved and read data on our personal computers on cassette tapes (does anyone except us old farts even remember cassette tapes? Or paper tapes? Or even punched cards?). and if they did not match we got the dreaded Checksum Error.NET Beyond the Scope of Visual Basic 6. fully sanctioned by the US Government. which is not all that fast. with over-flow ignored. as a later innovation. and it would compare this summation to the follow-up Checksum Byte. called a Checksum. smoke-filled rooms. larger holes were punched on either side of the sprocket track by a machine to represent. who then commenced to duplicate the tape and distribute it by the truckload. before starting Micro-Soft. It was not a perfect system. like Morse Code. Bill Gates. even on a computer that back then was running at a paltry 0. and then an additional byte. later re-monikered Microsoft. NOTE: Paper tape was durable one-inch-wide black paper ribbon that had a tiny sprocket-hole track down its middle. A problem with using cassette tapes this way was that each data item had required a leader buffer of about 150 bytes to synchronize the tape data transfer rates. and was also famously known for a paper tape copy of it being stolen at a computer exposition by some low-life hobbyists. which ultimately prevented genuine “outside” technological innovations from ever seeing the light of day). which was enough to hold the two bytes for a Short Integer. 500 bits per second. as it read each data byte. which were seldom consistent between tape machines. NET on page 93). especially in computer language development. and a paltry 1TB drive. It is simply an array of bytes. making the backup and reload processes run at relatively blazing speeds. Indeed. using them to quickly pass massive quantities of information between synchronized applications. Well…. this is one level of security already defined within this format. and extract data from. in turn a member of the System class. even as I explain that they can also embed the formatting for the stream right within the leader of the byte array itself. be it 32-bit integers. which predated the once-popular modified television monitors. I used packed string arrays in 2006 when I designed a VB6 version of a Union structure that someone needed (see the VB6 example in Implementing Unions in VB. Because only you will know the order. or 16-bit characters. All you need to do is keep track of this exacting order. business applications became viable on such now-primitive computers. I say “Cry me a river. Even better. layout and even easily encrypt them. because they will have no clue how to interpret this chain of data. like their companion teletype keyboards. when I hear youngsters wailing that they only have a crappy old 3GHz computer with a stock hi-rez graphics card. Interestingly. such as doubles or integers. But one of the innovations in BASIC-80 was the Make-String functions. and not only stuff it with various variables. is simply stacked on top of each other. and the next day we are doing everything on our personal computers and smart devices (so. until computer hobbyists. build a bridge. remembered. One day we were running to find a pay phone or thinking about shelling out $120 for a 4-function calculator. just 2GB of memory. Page –458– . not even having cassette interfaces available. I can instantly convert these byte arrays into data streams and rocket that data to and from files or memory streams. The best part is.NET Beyond the Scope of Visual Basic 6. by cracky). where the data. There is nothing magical about the data format. because you can define their data. needed an inexpensive medium to save and load their programs. Only programs designed with knowledge of the stacked data sequencing will be able to use it. eliminating the need to transmit separate formatting instructions. 8-bit bytes. though it is easy enough to work around. which I need to use a lot. And I never stopped using byte arrays. One data item ends and the other will begin immediately after it. 8-bit or 16-bit strings. 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.0 – David Ross Goben among Ham Radio Operators. or thought of using them as an easy solution to passing massive stores of data in a single stream of information. just like a structure or a spreadsheet table record. across a network. or convert a byte array to a scalar or string. which they used to record and rebroadcast information. They are too useful. and get over it!”). They look at me like I have a third eye. which caught IBM’s attention. byte array data streams are so easy to use. where you can convert any scalar or string into a byte array. 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. But the point was. and surplus used paper tape machines were once available everywhere for almost no money. One drawback. after Telegraph Wire and News Services and Military Services began retiring them. but extract them as well. 128-bit decimal values. or across the internet. which replaced the old 8-bit string arrays. whether it was scalar data. the idea of storing this data in strings or in byte arrays has never died out. 64-bit double-precision floating point values. and hence began the world-wide computer explosion. for which I have designed a number of custom in-house languages for companies over the years.Enhancing Visual Basic . This method is so convenient that Dot NET even supports byte array conversions through their BitConverter class. not the 16-bit Unicode characters we use now). they also typed on long reams of fanfold perforated paper. these things were packed together. which were used to replaced the computer panel switches to enter data. Plus. they can be a very secure means to transmitting proprietary information in a single block of data. By your program being able to know how to process this long string of data. is that they do not support the Decimal or Byte type (ironic). Even better. But I have always been fascinated that so many recent high-level developers have never heard of. and it required only a single synch leader to write all that data. add data to. Encryption makes this data even more mysterious to prying eyes. to multiple text lines. offset position in UniToByteAry("This is a string to stuff to the byte array". an intranet. you can specify a character count for either type of string.offset position in IntToByteAry(1234I. Byte. myData) now 0) myData) 'write an integer value to the byte array 'write a double value to the byte array 'write Decimal value to byte array 'send a UTF8-formatted string to byte array We can later read the above written data using the following code. or you can allow the methods to determine their sequential positioning.Enhancing Visual Basic . which often contain a number of string fields that can often be of variable length. This data can also be conveniently streamed anywhere. For the easiest possible use of these methods.4567R. myData) Dim IntOffset As Int32 = GetNextOffset(myData) 'OPTIONAL . myData) DblToByteAry(123. and can compress most-all typically 16-bit character values into an 8-bit package. myData) 'write an integer value to the Dim DblOffset As Int32 = GetNextOffset(myData) 'OPTIONAL . you can go “old-school” and specify offsets within the array to place or fetch each of the data items you require. IntToByteAry(1234I. NOTE: UTF8 8-bit characters are a very efficient format. In answer to all this. Single. We can also read the data in any order of our choosing simply by providing the desired offset in the byte array. When you specify a character count. In this case. then during writing.12345678D. This module contains 20 exposed conversion methods. and 10 methods to support writing a Decimal. Long. Long.0 – David Ross Goben Applications I like using them in is data records or information structures. After all. you are performing UTF8 operations. we must read this data in the EXACT order that we wrote it: ByteOffsetIndex = 0 ' Dim myUni As String = ByteAryToUni(myData) Dim myInt As Int32 = ByteAryToInt(myData) Dim myDbl As Double = ByteAryToDbl(myData) Dim myDec As Decimal = ByteAryToDec(myData) Dim myStr As String = ByteAryToStr(myData) 'initialize starting offset within the byte array 'pick 'pick 'pick 'pick 'pick up up up up up a normal String value from the byte array an Integer value from the byte array a Double value from the byte array a Decimal value from the byte array a UTF8 string of bytes and convert to a string If we want to record and later use the offsets to each data item. Further. to create a byte array that will automatically keep track of where to write and read data. to across a local network. Double. and a standard Unicode String from a byte array. 10 methods to support reading a Decimal. Note that because we are not supplying offsets. though you probably just think of them as ASCII. anyway). myData) StrToByteAry("Send Chars of string as UTF8 bytes". and even as an email attachment. you can ignore offsets and character counts. or not expected if reading the string. Short. UTF8 String. between computers. between computer applications via messages. and also the length and type of each item contained within it. By transmitting a stream that contains the length of a record. Integer. myData) DecToByteAry(12345678. Char. 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 . the need for maintaining fixed record lengths are a thing of the past. we do not need to initialize the ByteOffsetIndex value. though the null terminator will not be appended to the string (it automatically supplies its own internal null terminator. Byte. a null terminator is either not appended after the string if writing. Double. If you do not supply a character count for strings.NET Beyond the Scope of Visual Basic 6. and a standard Unicode String to a byte array. the entire string is saved to the byte array. I have written a VB module named modByteAryXChng. and then a null terminator is added to mark its end.offset position in Page –459– array where string is written array where Integer is written array array where Double is written . If you do not supply a character count when reading a string. or the internet. Integer. With these methods. Single. Whether you realize it or not. and you typically assign a character to a string using the CHR() and ASC() methods. UTF8 String (a string that has each of its Character elements defined from a 8-bit UTF8 value). you can defined you byte array and initialize it like this: 'initialize receiving buffer (assume its starting offset is Dim myData() As Byte = Nothing ' 'write a standard Unicode string to the buffer UniToByteAry("This is a string to stuff to the byte array". Short. the data will be read until a null terminator is encountered. most people tend to think of a Character as containing a value from 0 to 255. Char. You probably use these all the time and never realize it. myData) 'write Decimal value to byte array Dim StrOffset As Int32 = GetNextOffset(myData) 'OPTIONAL . and then follow it with a null terminator to mark its end. you can either assume this value is zero at the start. the data will simply be appended to the end of the byte array by default. UniOffset) Double = ByteAryToDbl(myData. DecOffset) String = ByteAryToStr(myData. DblOffset) 'pick 'pick 'pick 'pick 'pick up up up up up a Decimal value from the byte array a UTF8 string of bytes and convert to a string an Integer value from the byte array a normal String value from the byte array a Double value from the byte array What follows is the modByteAryXChng module: Option Strict On Option Explicit On '------------------------------------------------------------------------------'------------------------------------------------------------------------------' modByteAryXChng: Copy numeric or string data to and from a byte array. but when you later read it you do not want to have to specify a character count. ' ' 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. then do not provide ' a CharCount value with the string. you can specify a character count. ' 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. For example: Dim Dim Dim Dim Dim myDec myStr myInt myUni myDbl As As As As As Decimal = ByteAryToDec(myData. ' Note also that if the byte array is is intially defined as Nothing. ' If you do have fixed-length strings. then do not do so. IntOffset) String = ByteAryToUni(myData. myData) 'send UTF8-formatted string We can then read this data in any order we desire. The string routines will then write the ' data to the byte array. then the string will be written to the array with a null terminator tag. StrOffset) Int32 = ByteAryToInt(myData.12345678D. myData) 'write a double value to the array Dim DecOffset As Int32 = GetNextOffset(myData) 'OPTIONAL . This special variable will maintain the index for ' the next item after each invocation of a read method. 'NOTE: ' ' ' ' ' ' ' ' ' ' ' 'NOTE: ' ' ' ' ' ' If you do not want to provide an offset value. If you want to transmit a variable-length string. that it will ' be automatically initialized to accommodate this data the first time you try to ' write any data to the array. and that data will be written to the byte array without a terminating null tag. ' You will be expected to provide and offset within the buffer where you want to ' read the data into the selected data type. ' 'NOTE: For strings. or include error trapping.0 – David Ross Goben DblToByteAry(123. However. if you want to write a fixed-length string. so ' the first position in a string is position 0 in the offset. which is normally useful in pre-sized byte data arrays. ' 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. This is useful ' for reading/writing blocks of fixed data in files as streams. but still do not want to provide a CharCount ' value. So try something like this: Dim myOffset = GetNextOffset(myArray) 'get offset where next item will be written When writing strings.NET Beyond the Scope of Visual Basic 6. and a Null ' terminator will be appended to it. you can simply record the UBound+1 value of the array prior to writing the data if you initialized your array to Nothing. such as: Dim myData() As Byte = Nothing. so you ' can start out with an empty byte array. you will also be required that you provide a character count to read.4567R. then simply initially assign ByteOffsetIndex = 0 ' prior to reading any data. or a fixed-length string. specifying an optional ' offset within an array where the data is to be located (if the array is pre' sized). Note further that if you do want to record where each item will be appended to the data array.offset position in array where UTF8 string is written StrToByteAry("Send Chars of string as UTF8 bytes". Page –460– .offset position in array where Decimal is written DecToByteAry(12345678. where you can access its contained data much like a database record. The offset is zero-based. not 1-based as a VB MID() string is. If you are reading fixed-length ' string data. in this case. and the fixed-length string will be written. because trying to read a UBound of an array set to Nothing will generate an exception error. if you do not require fixed-length strings.Enhancing Visual Basic . ' The following functions are provided to extract values from a Byte array buffer. Runtime.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. Optional ByVal ByteAryOffset As Int32 = -1) As Single Return BitConverter. 4).ToUInt64(Ary. ByteAryOffset. Optional ByVal ByteAryOffset As Int32 = -1) As Int64 Return BitConverter. 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. 0) 'convert 4 byte array to Single End Function Page –461– .Enhancing Visual Basic .ToSingle(StuffFromByteAry(ByteAry.InteropServices Module modByteAryXChng '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'Create Unions to overlap data types '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ <StructLayout(LayoutKind.itmLng1 = BitConverter. Optional ByVal ByteAryOffset As Int32 = -1) As Double Return BitConverter. 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.NET Beyond the Scope of Visual Basic 6.ToDouble(StuffFromByteAry(ByteAry.ToUInt64(Ary. ' 'this value will contain the offset index for the next data item to read. 16) 'grab 16 Bytes to a Byte array st. ByteAryOffset.Explicit)> Friend Structure CharAndBytes <FieldOffset(0)> Dim itmChr As Char 'overlapping Unicode Character (16-bits) <FieldOffset(0)> Dim itmByt0 As Byte 'Byte + 0 (overlapping) <FieldOffset(1)> Dim itmByt1 As Byte 'Byte + 1 " End Structure <StructLayout(LayoutKind. 8). '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '------------------------------------------------------------------------------'------------------------------------------------------------------------------'functions to read data FROM a Byte Array '------------------------------------------------------------------------------'------------------------------------------------------------------------------'******************************************************************************* ' Method : ByteAryToDec() ' Purpose: convert byte array data to Decimal '******************************************************************************* Friend Function ByteAryToDec(ByRef ByteAry() As Byte. 8) 'convert the second 8 Bytes to an Unsigned Long Return st. 8). ByteAryOffset. ByteAryOffset. 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.ToInt64(StuffFromByteAry(ByteAry. 0) 'convert the first 8 Bytes to an Unsigned Long st.0 – David Ross Goben ' 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 ' ChrToByteAry(): convert 16-bit Unicode Character to byte array data ' StrToByteAry(): convert each character of a String to a UTF8 single byte in the byte array data ' UniToByteAry(): convert each 16-bit character of a String to consecutive two bytes in the byte array data '------------------------------------------------------------------------------'------------------------------------------------------------------------------Imports System.itmLng0 = BitConverter.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 CharCount As Int32 = -1) As String If ByteAryOffset < 0 Then 'if invoker did not specify a start offset index.NET Beyond the Scope of Visual Basic 6. ByteOffsetIndex += CharCount 'bump offset index beyond fixed-length string data Dim Str As String = Nothing 'init result For Idx As Int32 = 0 To CharCount . Optional ByVal ByteAryOffset As Int32 = -1) As Int16 Return BitConverter. ByteAryOffset.Enhancing Visual Basic .. Optional ByVal ByteAryOffset As Int32 = -1) As Char Return BitConverter..1 'grab the byte count from the array Str &= Chr(ByteAry(ByteAryOffset + Idx)) 'stuff to a string Next Return Str. ByteAryOffset = ByteOffsetIndex 'then assume we are using the ByteOffsetIndex value Else ByteOffsetIndex = ByteAryOffset 'always keey this value in check. 4). 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 Else 'if no character count supplied..1 <= UBound(ByteAry) Then 'if byte count seems to be in bounds. ByteAryOffset. 2). but this method ' : is included for completelness and it does to bounds checking '******************************************************************************* Friend Function ByteAryToByt(ByRef ByteAry() As Byte. 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. Optional ByVal ByteAryOffset As Int32 = -1) As Byte Return StuffFromByteAry(ByteAry.. 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.0 – David Ross Goben '******************************************************************************* ' Method : ByteAryToInt() ' Purpose: convert byte array data to Integer '******************************************************************************* Friend Function ByteAryToInt(ByRef ByteAry() As Byte. to prevent overflow End If If CharCount > 0 AndAlso CharCount + ByteAryOffset ..ToInt32(StuffFromByteAry(ByteAry. ByteAryOffset.ToChar(StuffFromByteAry(ByteAry.TrimEnd 'strip any space padding from its end End If End Function Page –462– . 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. 2). 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. Optional ByVal ByteAryOffset As Int32 = -1) As Int32 Return BitConverter. 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.ToInt16(StuffFromByteAry(ByteAry... Optional ByVal ByteAryOffset As Int32 = -1) StuffToByteAry(NumVal..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.0 – David Ross Goben '******************************************************************************* ' 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.itmByt0 = ByteAry(ByteAryOffset + Idx) 'stuff them to the exchange structure St. 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 .. ByteAry.1 Step 2 'grab 2 bytes at a time from the array St. Optional ByVal ByteAryOffset As Int32 = -1) StuffToByteAry(NumVal.itmByt1 = 0 Then 'If terminator.itmByt1 = ByteAry(ByteAryOffset + idx + 1) If St.itmByt0 = ByteAry(ByteAryOffset + idx) 'grab two consecutive bytes St.Enhancing Visual Basic . Optional ByVal ByteAryOffset As Int32 = -1) StuffToByteAry(NumVal..itmChr 'extract the resulting 16-bit Char to the string Next Return Str.itmByt1 = ByteAry(ByteAryOffset + Idx + 1) Str &= St... ByteAry. 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. ByteAry.. ByRef ByteAry() As Byte. 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. to prevent overflow End If If CharCount > 0 AndAlso CharCount * 2 + ByteAryOffset . ByteAryOffset = ByteOffsetIndex 'then assume we are using the ByteOffsetIndex value Else ByteOffsetIndex = ByteAryOffset 'always keey this value in check.itmByt0 = 0 AndAlso St. ByRef ByteAry() As Byte.1 <= UBound(ByteAry) Then 'if byte count seems to be within bounds. ByteAryOffset) 'send Long data to the byte array End Sub Page –463– . 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. Exit For 'then done Else Str &= St. ByRef ByteAry() As Byte.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.itmChr 'else append data End If Next Return Str.NET Beyond the Scope of Visual Basic 6. ByRef ByteAry() As Byte. Optional ByVal ByteAryOffset As Int32 = -1) StuffToByteAry(NumVal.Substring(Idx. 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.. 1)) Next End If End Sub Page –464– 'if not a fixed-length string. ByteAry.. we will append to end 'if offset not defined. Optional ByVal FixedLength As Boolean = False) If Not FixedLength Then Str &= Chr(0) End If Dim LnStr As Int32 = Len(Str) If LnStr > 0 Then If ByteAryOffset < 0 Then If ByteAryOffset < 0 Then ByteAryOffset = GetNextOffset(ByteAry) End If End If Dim UB As Int32 = ByteAryOffset + LnStr . we will append to end 'get offset to data destination 'compute require Ubound value for Array 'if greater than current. Optional ByVal ByteAryOffset As Int32 = -1) StuffToByteAry(Chr. Optional ByVal ByteAryOffset As Int32 = -1) StuffToByteAry(NumVal.. 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. 'then size to required size 'stuff each byte to byte array .. Optional ByVal ByteAryOffset As Int32 = -1. ByRef ByteAry() As Byte. 'add a null terminator to the string 'get length of string 'if string contains data.NET Beyond the Scope of Visual Basic 6. ByRef ByteAry() As Byte. 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.0 – David Ross Goben '******************************************************************************* ' Method : SngToByteAry() ' Purpose: convert Single to Byte Array data '******************************************************************************* Friend Sub SngToByteAry(ByVal NumVal As Single.Enhancing Visual Basic . ByRef ByteAry() As Byte.. ByteAry.. ByRef ByteAry() As Byte. 'if offset not defined.1 ByteAry(Idx + ByteAryOffset) = CByte(Str. 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.1 If ByteAry Is Nothing OrElse UB > UBound(ByteAry) Then ReDim Preserve ByteAry(UB) End If For Idx As Int32 = 0 To LnStr . 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. ByteAry. Optional ByVal ByteAryOffset As Int32 = -1) StuffToByteAry(NumVal. .. ByVal ByteCount As Int32) As Byte() If ByteCount > 0 Then If Offset < 0 Then 'if invoker did not specify a start offset index.Enhancing Visual Basic . ' : pick up its Ubound value + 1 to get the offset where the next data ' : item will be written to in this array.1 Ary(Idx) = ByteAry(Idx + Offset) Next Return Ary End If End If Return Nothing End Function Page –465– 'if everything seems to be in range..itmByt1 ByteAryOffset += 2 'add two bytes for each characters Next End If End Sub '------------------------------------------------------------------------------'------------------------------------------------------------------------------'Support functions '------------------------------------------------------------------------------'------------------------------------------------------------------------------'******************************************************************************* ' Method : GetNextOffset ' Purpose: Assuming the byte array to be written to is initially set to Nothing. we will append to end ByteAryOffset = GetNextOffset(ByteAry) 'get offset to data destination End If Dim UB As Int32 = ByteAryOffset + LnStr * 2 . to prevent overflow End If ByteOffsetIndex += ByteCount 'bump offset index for size of data If ByteCount + Offset . 'size the receiving array 'trasnfer needed bytes from the master array 'return the array for processing .1 <= UBound(ByteAry) Then Dim Ary(ByteCount . Idx.NET Beyond the Scope of Visual Basic 6.1 'compute require Ubound value for Array If ByteAry Is Nothing OrElse UB > UBound(ByteAry) Then 'if greater than current.. Optional ByVal ByteAryOffset As Int32 = -1. If ByteAryOffset < 0 Then 'if offset not defined. ByVal Offset As Int32.. Optional FixedLength As Boolean = False) If Not FixedLength Then 'if not a fixed-length string....1) As Byte For Idx As Int32 = 0 To ByteCount ... Str &= Chr(0) 'add a null terminator to the string End If Dim LnStr As Int32 = Len(Str) 'get length of string If LnStr > 0 Then 'if string contains data.. 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.itmChr = CChar(Mid(Str. ByRef ByteAry() As Byte. ReDim Preserve ByteAry(UB) 'then size to required size End If Dim St As CharAndBytes 'set up a byte/char exchange For Idx As Int32 = 1 To LnStr St.0 – David Ross Goben '******************************************************************************* ' 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. Offset = ByteOffsetIndex 'then assume we are using the ByteOffsetIndex value Else ByteOffsetIndex = Offset 'always keey this value in check.itmByt0 'stuff two bytes derives from character ByteAry(ByteAryOffset + 1) = St. 1)) 'stuff 16-bit unicode character ByteAry(ByteAryOffset) = St. '******************************************************************************* Friend Function GetNextOffset(ByRef ByteAry As Byte()) As Int32 If ByteAry Is Nothing Then 'if Byte Array is not yet defined. Name Ary = BitConverter.Name Dim st As DecimalAndLongs 'set up conversion structure st. ByRef ByteAry() As Byte.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). Single)) 'get byte array of Single Data Case GetType(Integer). 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.Name Ary = BitConverter.NET Beyond the Scope of Visual Basic 6.GetType.GetBytes(DirectCast(sender. Integer)) 'get byte array of Integer Data Case GetType(Short).GetBytes(DirectCast(sender.GetBytes(DirectCast(sender. Short)) 'get byte array of Short Data Case GetType(Char).Name Ary = BitConverter..GetBytes(st. Double)) 'get byte array of Double Data Case GetType(Long).GetBytes(DirectCast(sender.Enhancing Visual Basic . Long)) 'get byte array of Long Data Case GetType(Single).Name Ary = BitConverter.GetBytes(st.Name ReDim Ary(0) Ary(0) = DirectCast(sender.Name Case GetType(Decimal).. Decimal) 'grab decimal values Ary = BitConverter.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : StuffToByteAry() ' Purpose: convert Scalar Data to Byte Array data. 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.GetBytes(DirectCast(sender.Name Ary = BitConverter...GetBytes(DirectCast(sender.itmLng0) 'grab first half as a long Dim AryB() As Byte = BitConverter.Name Ary = BitConverter.itmDec = DirectCast(sender. ReDim Preserve ByteAry(UB) 'then size to required size End If '----------------------------------------------For Idx As Int32 = 0 To AryUBound 'trasnfer required bytes. Char)) 'get byte array of Char Data Case GetType(Byte). ' NOTE : AutoExpand Byte Array to fit data we are stuffing into it '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub StuffToByteAry(ByVal sender As Object. ByteAry(Idx + ByteAryOffset) = Ary(Idx) 'to destination buffer Next End Sub End Module Page –466– . all the drawing chores have already been performed. notifying the police of my whereabouts. but it was stepping on my toes. however. is to place the button’s text into its Tag property and erase its Text property. we would certainly end up with buttons without observable text.0 – David Ross Goben Black Book Tip # 34 Display Buttons Labels wider than the button normally allows Have you ever needed to define small buttons that contain little text. which makes it difficult to use during the development phase. though the text might sometimes approach edges of the buttons that Dot NET did not take kindly to. such as the labels “Style”. You can then redefine its shape. so their appearance will not look any different than other buttons that do not need to be adjusted. Page –467– . even relatively short labels that were of 8-point Consolas Bold did not fully display. For example. but only when the application is firing up. the appearance of the button tends to blend into one flat. you can create you own custom interactions. or drawing an image over its surface. lacking a defined border. which more tightly restricted the zone where text was displayable. all I was wanting to do was display some labels that would naturally fit within the bounds of the button. with a little creativity. I prefer to render all buttons on a form through the same event processes. With that. all in an attempt to minimize redrawing when things like a button was pushed. recently I needed to define a number of function buttons that had to be sized to 41 x 25 Pixels. However. though. Without this contrast. where the labels had displayed perfectly. but the buttons would also look like they were all washed out. of course. The solution to this. or releasing it. but the text is still too wide to fully display on the button? For example. “NxL” and “PvL”. This is very much how tabs (actually buttons) on tab controls are rendered. pressing it. packing a lunch. My problem was. Because I want to render all my buttons uniformly. The trick for us presently. Therefore. Were we to simply try and hide the button’s rendering of the label by setting the button’s ForeColor property to its BackColor property. This is because the ForeColor property helps define the overall appearance of the button. if you grab the Bounds of a button and draw a rectangle using the background color that the button is sitting on. What we want to do is to draw the label text ourselves. I decided to process the painting of the text for all these buttons through their Paint() Events. After putting on my mining helmet. “Prin”. It was time to take a look at how VB6 and VB. “Print”. The problem is. establishing its border. you can redraw and reshape them however you wish. A quick solution to this issue is to assign the text to the button’s Tag property. featureless field. They would instead display as “Styl” “Bee”. and the best place to do this is in the Form’s Load() Event. For example. that the text for the label has already been drawn onto the button prior to entering the paint event. because I was converting this application from VB6. When you enter the Paint() Event for a button.NET displayed label text. blending the active button into the ribbon to make it looks as though the button and the color ribbon was merged into a single object. It might make execution a whole lot faster. Grrrr… This was especially troubling. and disassembling numerous blocks of machine code. such as drawing a circle. you can render the button invisible.Enhancing Visual Basic . at that point. “NxLbl” and “PvLbl”. is to not interfere as much as possible with the normal button drawing process. and helping to define colorization of its “3D” edge effects. the point at which you enter is right after the button and its text has been drawn. one trick I like to do is place buttons above a color ribbon and redraw them as file folder tabs.NET Beyond the Scope of Visual Basic 6. “Beep”. Our problem with that is that we cannot see the button text until the application is running. leaving everything else intact. Not having any plans to put my freak on and do anything that might be diagnosed as insane. I discovered that VB6 printed label text much more freely than the more robust methods used by Dot NET. by using some creative rendering based upon whether the cursor is hovering over the button. DrawString(Txt. btn. then process its Controls list through recursion.Enhancing Visual Basic .Appearance = Appearance.Font.Paint. If a control has ' : children. ' : It will also display it perfectly vertically centered.Visible AndAlso TypeOf cntrl Is Button OrElse (TypeOf cntrl Is CheckBox AndAlso DirectCast(cntrl. has child controls. I expose a method named InitButtonControls() that takes the desired form as its parameter.-) The Paint() Event.Tag = cntrl.Button) Then 'if button or checkbox drawn as button. Control) 'grab the button or checkbox as a Control Dim Txt As String = CStr(btn. because these can suffer the same issue.MeasureText(Txt.siz. If we find a match.siz.Graphics.. even on thin buttons.Dispose() 'release allocated resources End Sub 'all done Page –468– . To handle this.Tag) 'grab its text from its Tag property Dim siz As Size = TextRenderer. simply named Button_Paint().Text 'copy its Text to its Tag property cntrl.Font) 'get the size of this text Dim X As Int32 = (btn. To do that. figures out how to center this text on the button using the TextRederer. as we have done previously in these Black Book tips and in other articles in this document. AddressOf Button_Paint 'and apply a Paint event handler to it End If End If If cntrl. even if you had aligned them top-center. is another private method.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. CheckBox). '******************************************************************************* Private Sub Button_Paint(sender As Object. Add 1 for alignment Dim Y As Int32 = (btn. you can add InitButtonControls(Me) to it. button or not. We do this by passing the form’s Controls collection to a recursive method..ControlCollection) For Each cntrl As Control In ControlsList 'process each control in the list If cntrl. Insert the following code in your form. brsh. extracts the button text from its Tag property. '******************************************************************************* Private Sub InitButtonControls(ByRef ControlsList As Control. ' : Parse through a control list and find buttons..NET Beyond the Scope of Visual Basic 6. we need to.. e As PaintEventArgs) Dim btn As Control = CType(sender. which ' : otherwise tend to push the text too far down. It might be a good idea to also test for CheckBox controls whose Appearance property is set to Button as well. we will need to process them as well.ForeColor) 'set its background brush to the ForeColor e. This is the beauty in function overloading.MeasureText() method and the dimensions of the current button. in a form’s Load() Event. We can then assign a Paint() Event handler to it so WE can paint its text. 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. because controls can sit on top of and be part of the background control collections.HasChildren Then 'if the control. Y)) 'draw text. . or on the Checkbox that is being drawn with a Button appearance. centered in the button brsh.Width .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. and then draws it using the button’s ForeColor property..Text IsNot Nothing Then 'if its Text property contains data. btn. This event handler will grab the button text from its Tag property and center it on the Button control.Height . Not only that. That… and we do not have to think up another method name.Text = Nothing 'and then erase the Text property AddHandler cntrl.Width) \ 2 + 1 'computer horizontal center. 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. It renders the generic sender parameter as a Control. cntrl.0 – David Ross Goben So what I need to do is to process all controls in the Form’s Controls collection. New Point(X. For example. This recursive method will parse each control in the provided collection and determine if it is a Button. perform function recursion.Height) \ 2 'computer vertical center Dim brsh As New SolidBrush(btn. InitButtonControls(cntrl.. If cntrl. It in turn simply passes its Controls list to a private recursive method also named InitButtonControls(). Once we have completed our behind-the-scenes work. but using no selection length .ScrollToCaret() 'scroll this selection to the top displayed line position of the control . to make it look just as it did prior to our side-changes. a ListBox. Traditionally. like so: With RTB . manipulating them becomes extremely fast (for as fast as displays have gotten. though this method only allowed one control at a time to have paint events suspended. They might also send messages via P/Invokes to get the scroll range and then get or set the current scroll position of the control.. or just save bookmarks. . (allow 4 pixels) Notice that instead of pointing to the absolute top-left corner (New Point(0..NET Beyond the Scope of Visual Basic 6.GetCharIndexFromPosition(New Point(0. and finally resume paint updates to the Rich Text control. We then need to lock paint updates to the RichTextBox control. I tend to lock paint updates to any control. if any . or whatever. a ComboBox.Select(pCaretTopLeft. which extends downward. and finally restore painting updates via the control’s ResumeLayout() method to refresh it.SuspendLayout() 'suspend repaints to this control 'Do whatever you need to do to your text here. This information allows us to fully recover the control position where we might have been typing. we can do whatever updates we need to do to it. I have seen countless examples of people intercepting the System Message Queue and trying to intercept focus (because a RichTextBox does not scroll when it does not have focus) and even intercept scroll messages to the control so they can stub them out (disable them) while they are trying to operate on the control. we need to restore the top-left corner position to the control. Because of this. we can then go about dickering away at it. a ListView. The problem with all these solutions is that they still cause sometimes severe screen jump and flicker. However. And when we are finished with our aside-changes. such as appending text. which can be applied to more than one of them. restore any previous selection. 0)). Grabbing the current selection points is easy. such as append text. To completely hide that annoying hiccup. And all those changes will also be completely invisible.SelectionLength 'save current selection and length We can also acquire the character index position in the top-left corner of the control like this: Dim pCaretTopLeft As Int32 = RTB. The advantage here is that when the updates to a control are suspended. or a TreeView control. For example: Dim pSelStart As Int32 = RTB. Dot NET emulates it with SuspendLayout() and ResumeLayout() methods for all display controls. whether it be a RichTextBox.SuspendLayout(). we instead pointed 4 pixels down.Select(pSelStart.0 – David Ross Goben Black Book Tip # 35 Performing Selections or inserting RichText Data Off-screen without Scrolling There has been a bit of consternation on the internet about the maddening habit of a RichTextBox control scrolling to its SelectStart location every time a selection is made that is not already displayed within the viewable field of that control. And once done.SelectionStart Dim pSelLength As Int32 = RTB. such as myControl. we need to reset the top-left position to the top-left of the control. to cleanly solve this issue is actually quite simple. pSelLength) 'select old text and length. they are still one of the slowest components of a computer system).ResumeLayout() 'resume repaints to this control and redraw it for current appearance End With Page –469– . First we need to save the current selection position and any selection length. But even with that. We can do that with the control’s own SuspendLayout() method. otherwise we might actually be capturing the previous line. Fortunately. After we suspend paint updates to a control. reset the previous selection. we can still get some display jump as the control momentarily scrolls to a changed position before returning the display to exactly where it was beforehand. Once we have suspended repaints to the control. a file. a regular TextBox. we can temporarily suspend paint updates to the RichTextBox. this was accomplishing using the LockWindow() P/Invoke. or colorizing lines of source code in a source code editor we might be building.Enhancing Visual Basic . and then we need to save the position of the character in the top-left corner of the control. 0) 'select old top-left character position. 4)) 'save top-left pos. we cannot easily partial-scroll. Besides. Regardless. you can either re-select positions normally. It will likely shift a pixel or two down for full exposure. len) does not work for you Or. In that case.Select(pos. The only thing you might notice is that if you initially start and the top line of text was only half-exposed.SelectionStart = pSelStart .NET Beyond the Scope of Visual Basic 6.NET. len) does not work for you 'use if . len) does not work for you and: .Select(pos.Select(pos. because painting to the control has been suspended.SelectionLength = pSelLength 'use if . Enable Built-In Justify-Alignment in a RichTextBox from VB. you might not be able to use the newer overload of the RichTextBox’s Select() method.0 – David Ross Goben This will avoid any flicker and annoying control scrolling. If you have an older version of Dot NET. on page 442. you can use the flicker-free RTBFastSelect() method featured in Black Book Tip # 30. And that is all there is to it! Page –470– .Enhancing Visual Basic .SelectionLength = 0 'use if . such as: .SelectionStart = pCaretTopLeft . setting the SelectionStart and SelectionLength properties of the RichTextBox will be fast and flicker-free anyway. len) does not work for you 'use if .Select(pos. but that sounds to me more like a useful feature. you would anticipate seeing such things as %1. as Wordpad will do. For example.IndexOf("/"c) 'if not typical. such as Shell32.ClassesRoot. such as a Try…End Try block. We can also find it reflected in the HKey_LocalMachine Hive under its Software\Classes key.. the following code will pick up this associated key for “. After all. if we were looking for the extension “.Computer. or a more robust "%1". we will usually find “txtfile” as its default key. End If If Index = -1 Then Index = AppPath.txt”. launch the associated application We should also wrap this whole thing within a error trap. If we were looking for the extension “. So.ClassesRoot. For an association with Wordpad. even though. we find the file extension associations in the HKey_Classes_Root Hive. the default value in the key for the extension is the extension plus “file”. otherwise some editors will choke when you invoke them using this command string.Enhancing Visual Basic . Normally. check for typical. this is typically "%ProgramFiles%\Windows NT\Accessories\WORDPAD. which Word uses. DisplayStyle) 'Finally. Thus: 'get the default command used for launching the associated application Dim RTFcmd As String = My. For example.ToString Notice that the GetValue() method specified acquire a Default value member. but without actually opening a file with that extension. check for slash-params. Users quite often re-associate file types when using File Explorer to be opened by their favorite editors rather than the defaults. NotePad is associated with TXT files and WordPad is associated with RTF files.IndexOf("""%1") 'check for typical decorations If Index = -1 Then Index = AppPath.. Consider the following function.TrimEnd End If Shell(AppPath. For example. by default. I would like for Jarte to open up instead of WordPad.DLL.GetValue(Nothing). this is the OPEN command that is used to open files.EXE" "%1".rtf" Dim RTFkey As String = My. Nothing as its Value member for the key. When the applications I use give me the option to open up an RTF editor.ToString NOTE: The “shell” part of the path refers to the Windows Shell.Registry.Substring(0.GetValue(Nothing).rtf”. Were we to use the RegEdit utility to examine the registry.DLL or Shell64.Computer. or even additional slash-tagged parameters following the command path. This is how we The next thing we need to do is to find the command path for this retrieved key. then trim them off AppPath = AppPath. We can trim those parameters off using the following simple bit of code: Dim Index As Int32 = AppPath. This is always found as the default value under the associated key’s “\Shell\Open\Command” sub-key.rtf”: 'get the default key associated with the extension ".Registry.NET Beyond the Scope of Visual Basic 6. End If If Index <> -1 Then 'if decorations found. that does all the things we have discussed: Page –471– .OpenSubKey(". and so such command entries should be expected to have a file or files following them. Index). which usually specifies the path to the executable and then parameters and/or optional parameters. These must be trimmed off. we will usually find “rtffile” as its default key. suppose you want to provide your user with the option to open whatever editor they have associated with TXT or RTF files.rtf")...OpenSubKey(RTFkey & "\shell\open\command"). LaunchDefaultOpenerForExt().IndexOf("%1") 'if robust type not found.0 – David Ross Goben Black Book Tip # 36 Opening an Associated Application without a File Sometimes you may want to open an application associated with a file extension. We next need to deal with decorations at the end of the retrieved path. Thus. I use Jarte as my RTF file editor. Under the extension key.ToLower) 'ensure we have just the extension End If Try 'get the default key associated with the extension Dim AppPath As String = My.Registry.GetExtension(Extension.ClassesRoot.IndexOf(".ClassesRoot. and selecting the “Choose Default Program…” option. or parameters. such as "%1".. such as “..Trim. we may also want to take a look at what application the user has decided will open a file.NormalFocus) As Boolean If Extension. End If If Index <> -1 Then 'if decorations found. such as the Jarte Rich Text Editor.OpenSubKey(Extension)." & Extension 'precede it with a ". Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle.txt” and “. For this. DisplayStyle) 'Finally.Path.rtf”. selecting the “Open With” option (you may see “Open With…” instead if it had not been selected before)." if it lacks one Else Extension = IO. we will find an OpenWithList Page –472– .GetValue(Nothing).IndexOf("/"c) 'if no typical. check for typical. Here. then trim them off AppPath = AppPath. such as /n Dim Index As Int32 = AppPath.IndexOf("%1") 'if robust type not found.OpenSubKey(AppPath & "\shell\open\command").Computer.Computer. End If If Index = -1 Then Index = AppPath.IndexOf("""%1") 'check for typical decorations If Index = -1 Then Index = AppPath. They may have done this by right-clicking the file. or False if the extension was not found or there ' : is no association. which Word uses.TrimEnd End If Shell(AppPath. we will find keys listed for all file extensions that the File Explorer browser knows about.Enhancing Visual Basic .. or the executable was not found where expected '******************************************************************************* Friend Function LaunchDefaultOpenerForExt (ByVal Extension As String. check for slash-params. a TXT file might be associated with Notepad.0 – David Ross Goben '******************************************************************************* ' Method Name : LaunchDefaultOpenerForExt ' Purpose : Launch the application that is associated with a specified file extension ' : Return True if success. we will need to instead look in the HKey_Current_User Hive under the Explorer file extensions list at “Software\Microsoft\Windows\Current Version\Explorer\FileExts”. launch the associated application Return True 'return success Catch Return False 'return failure if errors were detected End Try End Function Grabbing Open-With Preferences Now that we have looked at associations. but the user may in fact have chosen to open it by default using another utility.NET Beyond the Scope of Visual Basic 6. In this case you may want to go through just a little more work and provide a much more robust solution to this issue.Registry. For example.GetValue(Nothing).ToString 'get the default command used for launching the associated application AppPath = My."c) = -1 Then 'prep provided extension Extension = "..Substring(0. Index).ToString 'trim any trailing decorations. especially if the user has never tried to open the file with anything but the default application. check for MRUList End If Catch ExtKey = Nothing 'if not. we will also need to trim them from this data.OpenSubKey( "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" & Extension). 1)). such as "%1". contains a list of alphabet characters. 1)). the very first entry indicates the program the user wants to use to open files with the TXT extension.GetValue("MRUList"). and then from its first character of data we then grab the application that should be used to open it.OpenSubKey( "Applications\" & AppPath & "\shell\open\command"). This is used by the second line to grab the MRUList key value.Registry.Win32. In my case.ToString Because this opening command will likely hold the same possible trailing decorations as in the previous example. DisplayStyle) 'if not. ExtKey.CurrentUser.Computer.ToString. we need to find the location of that application. if you look to its “Shell/Open/Command” branch. “edgacbjif” in this case. it is not a big issue.exe”.OpenSubKey( "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\. More importantly.ToString 'get the EXE path to the application AppPath = My.Computer.Enhancing Visual Basic . even if it has an OpenWithList key it might not contain a MRUList value key.Substring(0.txt” file extension we can first find the executable defined for opening it like this: 'get the the Registry key for the sought extension Dim ExtKey As Microsoft.ToString The first line will define a registry key that is associated by Explorer with the “. Once we have the application.Computer.OpenSubKey("OpenWithList") Try If ExtKey IsNot Nothing Then 'if not nothing.Substring(0.Win32.Computer. defining the alphabet list in the order they should be displayed in a File Explorer open-with list.OpenSubKey( "Applications\" & AppPath & "\shell\open\command"). in that case we can actually open the associated application as specified in the above LaunchDefaultOpenerForExt() method: 'get the the Registry key for the sought extension Dim ExtKey As Microsoft. where we will find a key named “Jarte. Not all file extensions contain an OpenWithList key.Registry..RegistryKey = My. For example: "C:\Program Files (x86)\Jarte\Jarte. we can open it using the default End If 'get the name of the application associated with the extension Dim AppPath As String = ExtKey.exe”. the “e” value key specifies “Jarte.Registry. it indicates that the “e” entry is the application that should be used to open TXT files. especially if a user selected Open With.NET Beyond the Scope of Visual Basic 6. Its text string..RegistryKey = My. along with parameters.GetValue(Nothing).GetValue(ExtKey. this is “Jarte. The third line gathers the shell open command associated with the selected application. OpenWithList exists.GetValue(ExtKey. In this case.ToString 'get the EXE path to the application AppPath = My.ClassesRoot.ClassesRoot.ToString.Registry. How we determine which application is to open the file by default is by examining the MRUList value key. you will find that its default entry specifies the path to the executable. Programmacly.CurrentUser. Two potential problems presently exist.exe" "%1". but then aborted.txt\OpenWithList") 'get the name of the application associated with the extension Dim AppPath As String = ExtKey.ToString() 'if OpenWithList exists. If either of these scenarios fail.txt” file extension. we can open it using the default End Try If ExtKey Is Nothing Then Return LaunchAssociatedApp(Extension.GetValue(Nothing). Page –473– . Beneath it. Second. to gather all this information for the “.GetValue("MRUList"). In this example.GetValue("MRUList").exe”. Explorer does that by looking under the HKey_Classes_Root hive at its “Application” key.0 – David Ross Goben key that will contain a list of value keys that are demarked by an alphabet Value name (see the illustration). which includes the previous method..Substring(0.ToString) Catch Page –474– .. ' : if TryDefaultIfNotFound = TRUE (default). 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..Registry. Return StripExts(AppPath) 'return path to associated file.Win32.0 – David Ross Goben My more enhanced method to support all this.OpenSubKey(Extension).IsNullOrWhiteSpace(AppPath) Then 'if one was found. or the executable was not found where expected.Registry.IsNullOrWhiteSpace(AppPath) Then 'if we found it. is as follows.NET Beyond the Scope of Visual Basic 6. Note that it also allows you to grab the associated paths. DisplayStyle) 'launch the associated application.. and common support methods have been added: Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modLaunchAssociatedApp Static Class Module ' Launch app associate with a file extension. or launch the app selected to open it. such as /n Return StripExts(My. then try to launch the default associated app.GetValue(Nothing). '******************************************************************************* Friend Function LaunchSelectedOpenerForExt(ByVal Extension As String. Optional ByVal TryDefaultIfNotFound As Boolean = True) As Boolean Dim AppPath As String = GetSelectedOpenerForExt(Extension. or parameters.CurrentUser...NormalFocus.ToString 'get the EXE path to the application AppPath = My. 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. or False if the extension was not found or there ' : is no association.ToString If AppPath IsNot Nothing Then 'if we found something.GetValue(ExtKey. Shell(AppPath.ToString.. Return GetDefaultOpenerForExt(Extension) 'then try to get app associated with extension End If Catch If TryDefaultIfNotFound Then 'did not.ClassesRoot.ClassesRoot. such as "%1". less decorations ElseIf TryDefaultIfNotFound Then 'did not.GetValue("MRUList").ClassesRoot.NormalFocus) As Boolean Dim AppPath As String = GetDefaultOpenerForExt(Extension) 'get default associated app If Not String. 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 Extension = CleanExtension(Extension) 'clean up extension or extract extension 'get the default key associated with the extension Dim AppPath As String = My.Computer. then if a selected opener not ' : found. but if we should try default association.GetValue(Nothing)..Computer. Shell(AppPath.Registry. Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle.Enhancing Visual Basic ...Registry. DisplayStyle) 'launch the associated application.OpenSubKey(AppPath & "\shell\open\command"). If AppPath IsNot Nothing Then 'if we found it.OpenSubKey("OpenWithList") Dim AppPath As String 'get the name of the application associated with the extension AppPath = ExtKey. but if should we try default association.RegistryKey = My. 1)).Computer.OpenSubKey( "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" & Extension). '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modLaunchAssociatedApp '******************************************************************************* ' Method Name : LaunchSelectedOpenerForExt ' Purpose : Launch the application that is used to open a specified file extension ' : Return True if success. Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle. or the executable was not found where expected '******************************************************************************* Friend Function LaunchDefaultOpenerForExt(ByVal Extension As String.Computer. TryDefaultIfNotFound) 'get the path associated with an extension If Not String.GetValue(Nothing).. ' You can also get the app paths for the above options.ToString 'get the default command used for launching the associated application 'trim any trailing decorations.OpenSubKey( "Applications\" & AppPath & "\shell\open\command"). 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.. or False if the extension was not found or there ' : is no association. Index = SlIdx 'then try using slash index End If If Index <> -1 Then 'if decorations found.IndexOf(""""c..ClickedOnRAT %1" If Idx <> -1 Then AppPath = AppPath. check for typical.0 – David Ross Goben Return Nothing End Try End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method Name : CleanExtension (support) ' Purpose : make sure we have only an extension '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Function CleanExtension(ByVal Extension As String) As String Dim IndexOfDot As Int32 = Extension.GetExtension(Extension.IndexOf("""%") 'check for typical decorations If Index = -1 Then Index = AppPath. if it has one If Extension. 1) = """"c Then 'some are more comples.IndexOf("\"c) = -1 Then 'if there is no pathing involved.. Return IO..Trim... Return String. Index) 'then trim them off End If If String.Enhancing Visual Basic . If IndexOfDot = -1 Then 'but a dot is not found. AppPath = AppPath.Substring(0. 1) '"C:\WINDOWS\System32\rundll32..IndexOf("/"c) 'get possible slash index.LastIndexOf(".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. " "c. End If If Index = -1 OrElse (SlIdx <> -1 AndAlso SlIdx < Index) Then 'if no typical or slash index defined land less than index.." & Extension 'then precede it with a ".Substring(0..IsNullOrWhiteSpace(AppPath) Then 'some command strings are "%1" "%*" and the like."c) 'get the last index of the dot. like.Substring(0.. Idx + 1) 'strip to just base path End If End If Return AppPath..SetText(AppPath) Dim Index As Int32 = AppPath. such as "%1" and such '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Function StripExts(ByVal AppPath As String) As String Dim SlIdx As Int32 = AppPath." if it lacks one End If ElseIf IndexOfDot <> 0 Then 'if dot is not at the start of the string.Empty 'ignore these End If If AppPath...NET Beyond the Scope of Visual Basic 6.. Dim Idx As Int32 = AppPath.dll".exe" "C:\WINDOWS\System32\msrating...Path.Trim(Chr(0).IndexOf("%") 'if robust type not found. such as Word uses Clipboard. """"c) 'return the path and remove any decorations End Function End Module Page –475– .. Return ". 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. as generic color values store. ARGB=(255. For example. the comparison failed. Red. And this even works when checking against SystemColor values. Page –476– . If it is a Defined Color. Because a Defined Color’s Data Member contains an index into a pre-defined table of internal colors with names. Red. their object’s Data Members. and a generic color does not. but it was instead actually an index offset into an internal predefined table of Color Quads. which defines a color’s Alpha component (opacity. On a generic value. 255)}”. where people were looking for an explanation. but they did not know the little trick I almost absentmindedly use to get around it. 255. but when I checked them for being equal. this is what is triggering the inequity. its resident 32-bit integer Quad is simply exposed via its ToString method to the invoker. Green. rather than comparing two color values like this: If orgClrBackground = newClrBackground Then We should instead compare them like this: If orgClrBackground. Note: A Color Quad consists of four 8-bit Byte values. such as will be returned by the ColorDialog interface. 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. it is instead treated as a 32-bit integer index. But upon closer inspection. when I realized as I was quickly correcting my tests that other people are facing the same problem. where 0=transparent). and Blue). even though their numeric values might appear to be. I saw that one object reported a description of “{Name=ffffffff. and I was caught wondering why one color item I knew had an identical value as another was not considered the same value. This is merged into a single 32bit integer variable. Thus.ToArgb Then”. both containing WHITE color settings. The problem is that these color values might not be EXACTLY the same. 255.0 – David Ross Goben Black Book Tip # 37 Comparing Color Values This is one of those tips I thought was not worth adding to this collection. This actually makes them much more different than you might imagine. This is much like how SystemColor values are defined. is to instead compare the Color object’s Argb properties.ToArgb = newClrBackground. I was testing two objects of type Color. Green.NET Personal Programmable Calculator’s VPL Source Code Editor. therefore. but instead uses its 32-bit integer to store the actual color value. 255)}”. until today. each with a range of 0-255. its 32-bit integer Data Member that normally stores the Quad that would otherwise contain the ARGB value. in this case their 32-bit internal integer fields are compared against each other. With Color objects. I had seen this scattered across the web in a number of chat rooms. while the other object reported “{Name=White. 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). It was funny that I did not understand their problem. and Blue color settings. 255. ARGB=(255. because I worked through it all the time and never gave it much thought. which are simply indexes into the system theme’s color table. if it is tagged as a Defined Color. When objects are compared for equity.NET Beyond the Scope of Visual Basic 6. until today as I was working on adding a Color Settings interface for my VisualProCalc. and hence an indexed color. which contains the actual 32-bit values. 255. representing the 32-bit integer. The solution. It all has to do with how their properties are used. And that is all there is to it. and as such the integer value it is actually embedded with was not a generic 32-bit Quad definition.Enhancing Visual Basic . To do this. G=116. it actually causes our code to avoid a lot of bugs. Page –477– .ToString.ToString 'last ditch. “Red”) or its definition (ie.GetValues(GetType(KnownColor)) 'get an array of all colors known to the system We can now For-Each our way through the array.ToArgb 'get integer version of color for speed For Each KnownColor As KnownColor In [Enum]. because SystemColor values will change whenever the user changes their desktop theme. we wanted to avoid using SystemColor values as much as possible. we want to instead provide a system-provided definition string. suppose we had the following local enumeration named Accounts: Friend Enum Accounts Checking Savings Trading Annuity End Enum We can fill a ListBox named lstAccounts from this enumeration using this line: Me.ListBox1.GetValues() to Parse Enumeration Definitions Sometimes we might need to scan a local or system-defined enumeration to check its definitions. we want to use that system-defined name. to extract its member names so they can be displayed in a listing. just as we did to grab the enumeration base class. such as “Color [A=255. Return vClr.. if it is not known. Notice that we used a local variable named KnownColor of the KnownColor enumerator type. As such. or as a Hexadecimal ARGB string using its Name property. if it is not a system color.Drawing Namespace that we can take advantage of to search for a match to a Color object we might have. which is [Enum]. we can do that by embracing the enumerator’s name within square brackets. Suppose we wanted to acquire the definition name for a color our application user selected. there is an enumeration named KnownColors in the System.IsSystemColor AndAlso vClr.FromName(KnownColor.. To parse the members of any enumeration is really quite easy… once you know how to access them. we simply return the name (ie. a TextBox. “Color [A=255. or non-SystemColor End Function In the above method. B=0]”). However. or a ToolTip. Dim ClrValue As Int32 = Clr. We could assign the KnownColors enumeration list to a local array named ColorDefs like this: Dim ColorDefs As Array = [Enum]. If the color the user selected is known to the system. Notice we get to it by embracing it in square brackets. B=34]”. R=255.ToString 'return its non-SystemColor name End If Next End If Return Clr.DataSource = [Enum]. or else we might want to scan through the color values known by the system in order to acquire the name assigned to a color value. we need to access the enumeration’s base class. so they are not consistent. such “ffb97422”.Enhancing Visual Basic ..GetNames(GetType(Accounts))) 'to make ReadOnly: Me. If it is a system color. which will break down its ARGB value.ToString) 'grab color definition for the known color If Not vClr. we want to continue to scan the color table for a matching non-SystemColor value.0 – David Ross Goben Black Book Tip # 38 Use [Enum]. For example: '********************************************************************************* ' Method : GetColorDefinition ' Purpose : Return the color name or numeric definition for a color (avoid SystemColors as much as possible) '********************************************************************************* Friend Function GetColorDefinition(ByVal Clr As Color) As String If Clr. However.GetNames(GetType(Accounts)) For another example. by using the Color object’s ToString property.ToArgb = ClrValue Then 'if match but not a SystemColor. We can then access an enumeration’s members as text strings in an Array object using its GetValues() method by providing it with the enumeration’s Type definition.Red. or by comparing the color object’s Name property against each enumeration member.FromName(SystemColorName) to derive a Color object from the system color table. For example. and deliver that name to a Label.AddRange([Enum]. if we did need to access the KnownColor enumeration within a block where the KnownColor local variable is in scope.ListBox1.Items. R=185.. For example.GetValues(GetType(KnownColor)) 'parse each know color enumeration Dim vClr As Color = Color. which we can find by comparing their integer ToArgb properties.IsSystemColor Then 'if it is a SystemColor object. Although some “gurus” claim this practice will make the code confusing. Dim Red_Def As String = [KnownColor].NET Beyond the Scope of Visual Basic 6. G=0. either by using Color. Width Then 'if X offset within image not defined. they will naturally test a Color object for simply being equal to Color. fully opaque (solid).FromArgb(iClr Or &HFF000000) 'return its fully opaque ARGB color definition End If End With End If Return Nothing 'did not find a transparency color End Function NOTE: You can also test for a Color object that contains any degree of transparency by altering the test to “If (iClr And &HFF000000) <> 255 Then…”.NET Beyond the Scope of Visual Basic 6. Bitmap) 'treat the image as a bitmap (identical format) If X < 0 OrElse X >= .Transparent. X = . even when they know for a fact that the color is a transparency color. Page –478– . Consider the following method. When this color is found in the image. to test for a color being transparent.FromArgb(0.GetPixel(X. Y = .ToArgb And &HFFFFFF) = (pixelClr.Enhancing Visual Basic . which will return the renderable color value of an image’s transparency color: '********************************************************************************* ' Method Name : GetTransparencyColor ' Purpose : return the transparency color of an image as non-transparent color '********************************************************************************* Friend Function GetTransparencyColor(ByRef Img As Image. these values will be retained in order to define which color is being treated as transparent in the image.Width . The problem is. which can be located by checking for a match with its RGB value to the RGB value of the transparency color (such as “If (transpClr.ToArgb And &HFFFFFF) Then…”). this test will fail more often than not. because it will be forced to 255. or Blue definition values will not be rendered. already touched on in-code previously in the SetImageTransparency() function included in both “Setting Context Menu Icon Image Transparency at Runtime” (Black Book Tip # 25 on page 417) and “Adding Background Transparency to Images on Toolstrips and Menus” (Black Book Tip # 26 on page 421). even though its Red.... its Alpha component value is set to zero (“pixelClr = Color. Optional ByVal X As Int32 = -1. Often when they are checking to see if a color is defined as transparent in a graphics method. Y). pixelClr)”). Therefore. Green. PNG image files can feature a transparency color member.1 'then use right edge of image End If If Y < 0 OrElse Y >= . it does not matter what value the Alpha component value was previously. then color is transparent Return Color. The reason for this is obvious if you understand how an image transparency color is declared. whether it was a value of 0 to 254 (&H00 to &HFE). we simply have to test just its Alpha component member of an ARGB color for being zero (“If (pixelClr And &HFF000000) = 0 Then…”). which renders it fully transparent. but it is one that has caused countless programmers a lot of headaches.ToArgb 'get the target pixel If (iClr And &HFF000000) = 0 Then 'if the Alpha value is zero. This is a designated color value that is to be treated as transparent when the image is rendered. For example.Height . in the ARGB quad-color definition being returned. Because the result will OR an Alpha component value of 255 (&HFF) to its left-most member (&HFF000000).Height Then 'if Y offset within imaged not defined. As such..0 – David Ross Goben Black Book Tip # 39 Detecting Transparency Colors This is a minor tip. Optional Y As Int32 = -1) As Color If Img IsNot Nothing Then With DirectCast(Img.1 'then use bottom edge of image End If Dim iClr As Int32 = . Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 40 Greatly Accelerate TreeView Reflection of Directory Trees Reflecting a directory structure within a TreeView control is a time-honored tradition. The problem programmers now face is in displaying such trees for directory structures that have grown ever more massive and deep. Now-a-days, such colossal structures can typically contain several tens of thousands of folders and subfolders, and sometimes hundreds of thousands of files (I can only hope that the sleep-deprived developer who invented the original tree structure has been adequately compensated). Although computer speeds and drive sizes have grown exponentially over the years, the number of folders and files contained within them has increased exponentially as well. VB’s old Dir() function, often used to scan for such folders and files, even on a QuadCore 4GHz system, is still one if the slowest assets in your programming arsenal. When File System Objects were introduced in the Windows Script Host Object Model in 1999, it was like a breath of fresh air, because we could parse folders dozens of times faster. But sadly, the number of folders and files continue to proliferate almost geometrically as the sizes of our drives balloon, often taxing the concurrent increase in computer and disc drive speeds. With the introduction of the Dot NET System.IO namespace, directory parsing became supercharged, making FSO look like it was just creeping along, but the number of files keeps proliferating like viruses, causing even these significantly faster objects to often also appear too slow. Worse, application users have a bad habit of expecting instant gratification. The last thing they ever want to see is a WaitCursor or an AppStart cursor staring back at them, or their system simply seems to freeze up for a minute or five after they selected a drive icon because your program is too busy drilling ita way through perhaps hundreds of thousands of files on their data-filled system (see the earlier article, Comparing Visual Basic System I/O Commands on page 300 for a practical comparison example that parses a drive using Dir(), FSO, and System.IO). The trick to making your TreeView work lightening-fast is to simply address just what the user can actually see. Instead of building a full tree with every file and folder beneath the selected drive or folder, most of which the user will not bother to peruse, why not just list the drives and folders of the currently viewed folder, and then wait for them to expand a folder before actually populating it? The trick to making this look like a fully-defined tree is to ensure that subfolders of the current path that contain subfolders show expandable icons (+), indicating they can expand. The best part is, doing this is very easy. 1. Create a fresh Visual Basic Windows Forms Application and name it DirTest. 2. Finally, add the following block of code for Form1. Public Class Form1 '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private rootPath As String = "C:\" Private WithEvents TreeView1 As New TreeView 'set aside treeview control Private ImageList1 As New ImageList 'image list to store images within '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '********************************************************************************* '********************************************************************************* ' Method : Form1_Load ' Purpose : Prepare form and initial directory display '********************************************************************************* Page –479– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '********************************************************************************* 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 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 all child nodes in case we are repopulating Dim dirs() As String = Nothing 'init local directory storage Try dirs = IO.Directory.GetDirectories(parentNode.ToolTipText) 'get a list of any subfolders, 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 a faux child node to add "+" branch 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 Dim dirNode As TreeNode = parentNode.Nodes.Add(parentNode.GetNodeCount(False).ToString, IO.Path.GetFileName(dirPath), 0, 0) 'add new dir node w/closed folder dirNode.ToolTipText = dirPath 'save its folder path as its tooltip DirRecurse(dirNode, True) 'parse any of its subfolders End If Next End If '--------------------------------------------------------------------------Dim files() As String = Nothing 'init local file storage Try files = IO.Directory.GetFiles(parentNode.ToolTipText) 'get list of files in this folder, skip if protected Catch End Try If files IsNot Nothing AndAlso files.Count <> 0 Then 'if files exist... If skipDeepSeek Then 'if Referencing only... parentNode.Checked = True 'mark this folder as being unprocessed parentNode.Nodes.Add("*") 'add a faux child node to add "+" branch connector Return 'nothing else to do End If '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - For Each filePath As String In files 'else parse each file Dim fileNode As TreeNode = parentNode.Nodes.Add(parentNode.GetNodeCount(False).ToString, IO.Path.GetFileName(filePath), 2, 2) 'add new file node w/file image fileNode.ToolTipText = filePath 'save its file path as its tooltip Next Page –480– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben End If End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : InitializeImageList ' Purpose : Imitialize a provided ImageList and fill it with locally-created images '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub InitializeImageList(ByRef imgList As ImageList) imgList.Images.Clear() 'initialize image list imgList.ImageSize = New Size(16, 16) 'define 16x16 pixel images in this list '-----------------------'ImageStrip for 3 Images '-----------------------Dim strImg As String = "iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFOSURBVEhLxZNbDoMwDAR79H5xLm5GPc46BLBIgEqstHJe" & "Y5bQft7WkmiapoM/3+86brDSRWqB5vCw7vDKUVU5G0doxtm6z0NM5nlme1M3h07EuUw9XseqPGDH9FTf" & "0ptJFj5qAHs7bGKM8h75TwLD+gHJ16xGSG56Y+0xjlybAFdUYdPK5xeQSbxmRb4W1ULua4RnTl/4wQDH" & "6k2l4EdZqnibF/ncatfGwDc9HgVw9qrpQTXcxkU+t3pq3T4mAzwZHn2Ble1LjPeIcV232jPnyvPLZavH" & "8y+ARhiqIc6Kt7V7t4/hyfDoC6DgR1hxtSL/g/ZsZ+Fb1wxXAkStsCn4UdaQzQvsFfut4cIo+piffwF0" & "pYdYdyb2Mef3tbX6lIffscNStn9mQ7ovgNvwUcPq8VfVYKPOFEHRvoaYc856vCvlOYhwI1ab96S8N7Us" & "P6rUvtqZka2WAAAAAElFTkSuQmCC" Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream imgList.Images.AddStrip(Image.FromStream(memStream)) 'construct image from stream data and add to ImageList1 as an ImageStrip memStream.Close() 'release stream resources End Sub End Class As you 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). 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 subfolder 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. Page –481– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben If we are populating the node and we have sub-folders, we take this list and create child nodes with closed folder images, set its ToolTipText property to the folder’s path, and then recurse this method using the new node as a parent node. Here, we set the SkipDeepSeek parameter to True so the recursion will only find out if each child folder contains any of their own children, and sets their Checked property if so (never setting this flag to True will allow a full directory tree to be populated). We then check for child files in the parent node’s path. If we find any, like with the directory scan, we see if the SkipDeepSeek flag is set. If the flag is set, though we presently know that it contains at least one file, we will set its Checked property to True and add a faux-node so that the parent node will still display a “+” connector. We can do this because if a faux-node had already been generated by the directory check, then the parent node’s child count will not be zero. If we are populating the node and we have files, we take this list and create child nodes with File images and set its ToolTipText property to the file’s path. And that is all there is to it! About the InitializeImageList() Method Notice the final method, InitializeImageList(). It builds an ImageStrip from a Base64 string and loads the derived three images, a Open Folder, a Closed Folder, and a File (Notepad) folder, to the ImageList1 control that can then be used in this example. The topic of embedding images within source code will be covered later under a number of topics, where you will learn how to build your own in-code image lists using individually-defined images and image strips. Notice also that the two controls used, TreeView1 and ImageList1, were not controls dropped onto the form, but created within your source code. Because the TreeView control is displayed, it set its parent to the form. This allows it to become visible on the form. You can also optionally add it to the form’s Controls collection in cases where your code might need to scan the controls assigned to a form. The ImageList control did not need to go through these steps because it is not being displayed; we are using it as a disconnected but referenced class instance, like a ditty bag to hold some of our stuff. NOTES: If you will need to display CheckBoxes in your TreeView, then instead of using a folder node’s Checked property to indicate if the folder has been processed or not, you can instead place this flag in its Tag property. Of course, you will have to cast the Tag property object to Boolean in order to test it, such as CBool(parentNode.Tag). In more complex situation, you can store a small structure or class within the Tag property (this property is of type Object, si it can store (actually, point to) anything, after all), and this way you can additionally store several different flags or fields. Page –482– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 41 Sorting TreeView Directory Trees in an Orderly Fashion There are quite a number of programmers out there who want to sort their TreeView listings. Of course, once they manage to figure out how to do it (it is quite easy using the IComparer interface that we have already touched on within this document), they often abandon it because it tends to sort everything, and so folders are no longer displayed before files as they were in our last example (see Black Book Tip # 40, Greatly Accelerate TreeView Reflection of Directory Trees, on page 479). Most people want to display sorted folders first, and then the sorted file list. Others want to go an extra step and also sort the files based on their extensions. This way, all files with “.txt” are grouped and sorted together, for example. In the previous Black Book Tip, we assigned each folder an Image Index of 0 when it is collapsed (closed), and an Image Index of 1 when it was expanded (open). Files had an Image Index of 2 (or higher). Thus, to sort so that folders are always displayed first, during a comparison, if the left comparator is a folder and the right comparator is a file, then we should assume that left is less than the right. If the left is a file and the right is a folder, then the left should be assumed to be greater than the right. Otherwise, at that point, the left and right comparators are either both folders or both files. If we wanted to compare extensions of files, then when both comparators are files, we can easily grab their extensions. Using a strategy similar to testing folders against files, if the left has no extension and the right does, then assume that the left is less than the right. If the left has an extension and the right does not, then assume that the left is greater than the right. After the above tests, if we fall through that far, we will know that both files have extensions. We can then perform a string comparison on them. If they match, then we want to take the additional step of comparing their string lengths, because comparing “Test” against “Testing” will be considered equal, due to a string comparison only testing as far as the length of the shortest string. If both comparators are folders, or if both are files with identical extensions, or both are files lacking extensions, then we can perform a normal string comparison. Because comparison return the sign of the comparison, where it returns -1 if the left comparator is less than the right comparator, it returns 1 if the left comparator is greater than the right comparator, and it returns 0 if both comparators are equal, we can add an additional option and allow descending sorts by simply multiplying any comparison result by -1. All this is almost too easy. The first thing we want to do is to create our TreeView Node comparison class. For that, we create a new class. Name it TreeNodeComparer. Within its body, add “Implements IComparer” and it will automatically add the required Public Compare function that is invoked by the sorter function: Public Class TreeNodeComparer Implements IComparer Public Function Compare(x As Object, y As Object) As Integer Implements IComparer.Compare Throw New NotImplementedException() End Function End Class Here, we are provided the guts of our comparison, where x refers to the left comparator, and y refers to the right comparator. We will first need to cast them to type TreeNode so we can use them, and then get the contents of their Text properties to obtain the actual text to perform the comparison on. For example, replace the “Throw New NotImplementedException()” line within the Compare function with the following: Dim Dim Dim Dim nodeX nodeY textX textY As As As As TreeNode TreeNode String = String = = CType(x, TreeNode) = CType(y, TreeNode) nodeX.Text nodeY.Text 'convert x to a treenode 'convert y to a treenode 'grab their text Page –483– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben We now have enough information to perform comparisons. For example Public Function Compare(x Dim nodeX As TreeNode Dim nodeY As TreeNode Dim textX As String = Dim textY As String = As Object, y As Object) As Integer Implements IComparer.Compare = CType(x, TreeNode) 'convert x to a treenode = CType(y, TreeNode) 'convert y to a treenode nodeX.Text 'grab their text nodeY.Text Select Case String.Compare(textX, textY) Case -1 Return -1 Case 1 Return 1 Case Else Return textX.Length.CompareTo(textY.Length) End Select End Function 'compare strings 'textX < textY 'textX > textY 'textX = textY 'compare text lengths 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 Case Is <= FolderOpened If nodeY.ImageIndex > FolderOpened Then Return -1 End If 'both objects are folders at this point... Case Else If nodeY.ImageIndex <= FolderOpened Then Return 1 End If 'both objects are files at this point... End Select 'check for files and folders 'nodeX is a folder 'but is nodeY a file? 'yes, so make a folder lower than a file 'nodeX is a file 'but is nodeY a folder? 'yes, so make the file greater than a folder 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 Page –484– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Case 1 'extX > extY Return 1 Case Else 'extX = extY Dim tmp As Int32 = extX.Length.CompareTo(extY.Length) 'compare lengths If tmp <> 0 Then 'if lengths are not the same Return tmp 'return the sign End If End Select End If 'if we have not yet returned, the extensions are the same, so compare the whole string If we want to perform descending sorts, we could either flip the sign of the returned values (except during the node checks between folders and files so the folders will always display first), or we could multiply the comparisons and the return values against an integer variable, such as AscDecFlag, that contains a 1 for ascending, or -1 for descending. We might also want to make sorting on extensions an option. To make all these things an option, we can add our own parameterized New() constructor method for this class that will accept optional parameters so that we can set the extension sorting and sorting direction flags as desired when we invoke a sort. For example: '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private _SortExtensions As Boolean 'True if Sort also checks Extensions Private _SortDescending As Boolean 'True if Sort is in Ascending Order '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Public Sub New(Optional ByVal SortExtensions As Boolean = False, Optional ByVal SortDescending As Boolean = False) Me._SortExtensions = SortExtensions 'store extension sorting flag Me._SortDescending = SortDescending 'store Ascending/Descending sort flag End Sub Apply our custom sort comparison method would require a slight change, such as: Me.TreeView1.TreeViewNodeSorter = New TreeNodeComparer(SortExtensions, SortDescending) Of course, we would have to be able to act on these selections. For the SortExtensions option, we could wrap the pevious 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 If Me._SortDescending Then AscDecFlag = -1 Else AscDecFlag = 1 End If 'Ascending/Descending flag 'Descending? 'yes, so invert the result for Descending 'otherwise treat result normally if Ascending 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 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' TreeNodeComparer Class Module (VB.NET version) ' 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. 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 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 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 '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Page –485– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '******************************************************************************* '******************************************************************************* ' Method Name : New ' 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 = CType(objX, TreeNode) 'convert objX to a treenode Dim nodeY As TreeNode = CType(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 Page –486– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 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 –487– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Black Book Tip # 42 Taking Advantage of VB.NET Drag and Drop Features A lot of VB6 programmers have complained about how Drag and Drop, the process of being able to drag an object, such as a filename or list of files from File Explorer to a form control or drag data from one control to another, has changed between VB6 and VB.NET. When VB6 was the only game in town, developers kept complaining, “Why can’t Drag and Drop do this?”, “Why can’t Drag and Drop do that?”, or “Why can we not Drag and Drop custom classes more easily?” The list of complaints was long. Well, VB.NET’s Drag and Drop now does all that. In answer to all these feature demands, Microsoft crammed all of them into VB.NET, and that is why you now see all the differences between VB6’s and VB.NET’s implementation of that technology. In addition, VB6 used OLE (Olé; Object Linking and Embedding) to support Drag and Drop, but that is a limited and more primitive technology than the messaging system used by .NET, which is able to support all those feature demands. So, be careful of what you wish for. In truth, I think all that programmer angst was because the Upgrade Wizard did not upgrade Drag and Drop code for some reason. But the good news is, it is very easy to do! In this tome’s free companion, “Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades”, I had touched on basic drag and drop functionality, showing you how to allow dragging files (actually, just the filepath strings) of a file or files from Explorer to a list control. Basically, you had to keep in mind 3 things to allow filepath Drag and Drop: 1. You must set the AllowDrop property to TRUE on any controls you want to allow dropping items onto, such as a ListBox, though it could be even a button, a “Drag Here” panel, or anything you choose. 2. If you are dragging objects from somewhere other than the current control, which is typical, you must be sure the mouse cursor reflects whether we want to allow the type of object being dragged to be dropped onto our “AllowDrop” controls, so we need to add a simple DropEnter event handler to any “AllowDrop” controls. Remember, any control that does not have their AllowDrop property set, or does not have a DropEnter event handler (except in the event of dropping an items from one place in a container to another), will automatically show a no-entry cursor under .NET, and it will not allow drops onto them. 3. Finally, we must add a DragDrop event to handle dropping items to each “Allow Drop” control. Of course, this also involves some rather simple program logic to determine the kind of data that is being dragged and if we want to allow dropping that kind of data onto our receiver controls. For example, suppose we have a ListBox named ListBox1 on a form. The first thing we must do is to set its AllowDrop poperty to True. When we do just that, 1/3 of our battle is already won. Next, because we are dragging items from outside it, we need to add a DropEnter event for ListBox1. Because we are processing files, its interrogation code will be extremely simple. If this test passes we can display an “Allow-Copy” cursor, otherwise we specify None, which displays a “No-Entry” cursor: Private Sub ListBox1_DragEnter(sender As Object, e As DragEventArgs) Handles ListBox1.DragEnter If e.Data.GetDataPresent(DataFormats.FileDrop, True) Then 'Note that you can use "FileName" or even "FileDrop" in e.Effect = DragDropEffects.Copy 'place of DataFormat.FileDrop. Parameter TRUE allows compatible Else 'formats to be converted to the specified target format. e.Effect = DragDropEffects.None 'Put up a no-entry sign if it is not of the expected type. End If End Sub 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 to the bottom of ListBox1. Next End If End Sub Page –488– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben 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 seems 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. We can apply a similar testing technique to TreeNode objects. Thus, our test could be “If e.Data.GetDataPresent(GetType(TreeNode)) Then…”, and our item assignment is “Dim item As TreeNode = DirectCast(e.Data.GetData(GetType(TreeNode)), TreeNode)”. You can also use CType rather than DirectCast. In Black Book Tip # 40, we showed you 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 you can clearly see its logic, although for a more robust application you will need to prevent protected files and folders from be 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 a Mouse event, such as the MouseMove event for a TreeView, we can grab the TreeNode it is passing over using the TreeView’s GetNodeAt() method by providing it with the e.Location property, which delivers coordinates local to the TreeView. For example: “Dim CurNode As TreeNode = Me.TreeView1.GetNodeAt(e.Location)”. Because this handler does not expose cursor coordinates, we can get them using the TreeView’s PointToClient() method instead by using the Cursor’s Position property, 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. 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: Page –489– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben '********************************************************************************* ' 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 right-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 an 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 If e.Effect = DragDropEffects.Move Then 'if we are handling a move event... Dim destNode As TreeNode Page –490– Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben With Me.TreeView1 destNode = .GetNodeAt(.PointToClient(Cursor.Position)) 'get the node currently under the cursor End With If destNode IsNot Nothing AndAlso Not destNode.Equals(item) AndAlso destNode.ImageIndex <= 1 Then 'if we can drop it at the destination... Dim filename As String = IO.Path.GetFileName(item.ToolTipText) 'grab file's name Dim destPath As String = destNode.ToolTipText 'get the destination path Dim DestFilePath As String = destPath & "\" & filename 'build a destination filepath If IO.File.Exists(DestFilePath) Then 'if a file by that name exists there... Select Case MsgBox("The destination path already contains a file named" & vbCrLf & filename & ". Do You want to overwrite it?", MsgBoxStyle.YesNo Or MsgBoxStyle.Question, "File Already Exists at Destination") Case MsgBoxResult.No 'the user does not want to over-write it Return End Select End If Try 'Although a Io.File.Move() method seems faster, we cannot overwrite. This method is easier than deleting. IO.File.Copy(item.ToolTipText, DestFilePath, True) 'first try copying the file (can over-write) Catch MsgBox("Error moving " & filename & "to " & destPath & ". Aborting.", MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation, "Error Moving File") Return End Try Try IO.File.Delete(item.ToolTipText) 'now try deleting the original file Catch MsgBox("Error removing " & filename & "from " & IO.Path.GetDirectoryName(item.ToolTipText) & ". Aborting.", MsgBoxStyle.OkOnly Or MsgBoxStyle.Exclamation, "Error Moving File") Return End Try item.ToolTipText = DestFilePath 'assign it the new filepath Dim oldParent As TreeNode = item.Parent 'save reference to item's current parent item.Remove() 'remove the node from its current parent If oldParent.Nodes.Count = 0 Then 'it no longer has children, then close it oldParent.ImageIndex = 0 '0 = closed folder image oldParent.SelectedImageIndex = 0 End If If Not destNode.Checked Then 'if folder has been opened or it was empty... destNode.Nodes.Add(item) 'attach the detached node to its new parent End If If Not destNode.IsExpanded Then 'if the destination folder is not expanded... destNode.Expand() 'expand and possibly repopulate End If item = FindNodePath(destNode, DestFilePath) 'find the file node in its new parent If item IsNot Nothing Then 'if it is found (100% likely) TreeView1.SelectedNode = item 'select it item.EnsureVisible() 'make sure it can be seen End If TreeView1.Focus() 'set focus to the treeview End If End If End If End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : FindNodePath ' Purpose : Find the node path in a treeview based upon its filepath '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function FindNodePath(ByRef Node As TreeNode, ByVal SeekPath As String) As TreeNode If Node.ToolTipText = SeekPath Then 'if current node contains the sought path... Return Node 'return a reference to that node End If If Node.Nodes.Count <> 0 Then 'else if it has children... For Each subNode As TreeNode In Node.Nodes 'check each child If subNode.ImageIndex <= 1 Then 'if it is a folder... Dim nd As TreeNode = FindNodePath(subNode, SeekPath) 'check its node and subnodes... If nd IsNot Nothing Then 'did it find a match? Return nd 'yes, so return it End If ElseIf subNode.ToolTipText = SeekPath Then 'does the file contain the sought path? Return subNode 'yes, so return the node End If Next End If Return Nothing 'nothing found End Function NOTE: If you were providing feedback as I often do within a status bar label, you may want to implement a DragLeave event handler, which will always fire after a DragDrop event. There, you can erase any feedback. Page –491– MouseMove If Me. or it is set to Nothing if it is not. Consider the following replacement MouseDown event and the new MouseMove event: Private MouseDnNode As TreeNode '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.ExpCompNode Then 'if the current node is not the start node.MouseDnNode = Nothing 'then disable the reference End If DirectCast(sender.Left Then 'if start node is a file and left-mouse drags..MouseDnNode IsNot Nothing AndAlso Me. Me. With DirectCast(sender. The TreeView control already takes care of double-clicking on folder notes.. we can then start a Drag and Drop process. Page –492– . All we must do is make a small change the MouseDown event to simply ensure that the mouse is being pressed on a file. If we find that we have dragged the mouse to a different node (typically this will be an adjacent node).. TreeView).0 – David Ross Goben BONUS TIP: Sometime you want to double-click a file node and do something with it.Location) 'grab the possible node at the current location If SelNode IsNot Nothing AndAlso SelNode IsNot Me.Location) 'get the node the mouse was pressed on If Me. MouseDnNode = ActiveTV. and because we already know the start node (the node we pressed the mouse down on) has been verified to be a file. e As MouseEventArgs) Handles TreeView1.MouseDnNode. its opened state will be automatically flipped by a double-click.Button = MouseButtons. But what of file nodes (or folder nodes with no files)? Suppose you want to display property information on files. e As MouseEventArgs) Handles TreeView1.NET Beyond the Scope of Visual Basic 6.. or even open the file? In that case. . This is actually quite easy to do.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. you might notice that a DoubleClick or MouseDoubleClick are not recognized when a Drag and Drop operation is in progress (by necessity.Enhancing Visual Basic .ImageIndex <= FolderOpened Then 'if this is not a file.MouseDnNode IsNot Nothing AndAlso e.MouseDnNode. If a node can expand or collapse. TreeView) Dim SelNode As TreeNode = .GetNodeAt(e. it keeps very tight control of the mouse during a Drag and Drop).DoDragDrop(Me.GetNodeAt(e..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 you 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 to another. which was initiated in our MouseDown event. DragDropEffects. 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.MouseDown Me. 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).. For example: Dim pt As Point = Me.Value 'grab current date in calendar End Sub Also.DropDown Me. we need to set aside a local Date variable so we can use it to reset the control if we cancel. the calendar closes and a date is recorded. Most people simply want to accept a date when the user actually clicks on a date. or picked anywhere outside the calendar. If you want to also check for the user canceling using the Escape or Alt keys. which is ValueChanged. but in most cases all that effort is totally unnecessary. and they do not want the date that was initially in the control to change if the user selected the dropdown button in the upper right corner. For example: '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private dtpOrgDate As Date = Nothing 'original date when DTP calendar drops '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ We can set this variable when the calendar drops into view in its DropDown event: '********************************************************************************* ' Method : dtpDate_DropDown ' Purpose : The Date Picker calendar dropped down '********************************************************************************* Private Sub DateTimePicker1_DropDown(sender As Object. and click it again to bring up a list of years. The dropdown Calendar for a DateTimePicker control is 222 pixels wide and 160 pixels high for singlemonth displays. every time the date value changes within the control this event will fire. or you should be able to click the month and year header to bring up a list of months. Some industrious developers have written quite elaborate code to intercept messages for the control. My advice is just to ignore that event.Enhancing Visual Basic . except when you need to keep meticulous track of any and all date changes. e As EventArgs) Handles DateTimePicker1. Just by knowing that. there is just a tiny bit more code to add in a test. track its dropdown control. Grrr! Their problem is that they are using the default event when they double-click the control. we can totally avoid a truckload of otherwise very complicated message intercept code if we can assume the calendar is not to be resized to display multiple months. and they can get by using just the DropDown and CloseUp events with very little code. etc. Every time they pick on something.NET Beyond the Scope of Visual Basic 6.dtpOrgDate = Me. we can very easily manually calculate it directly from the Cursor itself relative to DateTimePicker1’s container. we will need to pick up the cursor position to find out where the user clicked.0 – David Ross Goben Black Book Tip # 43 Taking Advantage of the DateTimePicker Control The DateTimePicker control is great. just to provide this service. and here is where these programmers are running into trouble.DateTimePicker1. Their frustration rises from the fact that the upper left and right triangle buttons are supposed to rotate sequentially through months. If we want to reset DateTimePicker1 to the value it held when the user hit its dropdown button. They want their user to pick a date.. though many new programmers get more than a bit frustrated with it. And true to its name.PointToClient(Cursor. Create a new Windows Form project and drop a DateTimePicker named DateTimePick1 and a TextBox named TextBox1 anywhere onto the form (these names would be their defaults).Position) Page –493– 'get cursor relative to DateTimePicker container . but the control seems to close and return a date whenever the user clicks anything on the calendar after they added event code for it (it worked fine prior to that). Because the DateTimePicker control does not provide any cursor information. However..Position) 'get cursor relative to DTP container With Me. However.Left. 160) 'define bounding rectangle for calendar If rect. We can easily do that with this small addition to the code: Dim pt As Point = Me.DateTimePicker1. people will test for it within a KeyDown or PreviewKeyDown event and test for something like “e.DateTimePicker1.Keyboard.TextBox1. Technically. you will need to calculate it from that container.KeyCode = Keys.Position) 'get cursor relative to DTP container After that.PointToClient(Cursor.Alt). you will need to take a look at where this point is in relation to the DateTimePicker1 control.PointToClient(Cursor.Left.GroupBox1. Otherwise.ToLongDateString 'then set the date to the textbox Else Me. we want to reset the date of the picker.Position) 'get cursor relative to DateTimePicker container Or.> End If End With If it is within the bounds of the calendar. you can make your code much more robust by simply using the picker’s parent.dtpOrgDate = Me. . 222. e As System.Bottom. which is a 222 x 160 pixel field below that control’s bounds.DateTimePicker1. For example: Dim pt As Point = Me. 222..Enhancing Visual Basic . if you get fancy and place your control on another container.Contains(pt) Then 'if the pick is within the calendar bounds.Bounds 'using the controls bounds as a guide.DateTimePicker1. If either of these keys is pressed.Code for if user clicked on calendar goes here.DateTimePicker1.Parent. 160) 'define bounding rectangle for calendar If rect. Dim rect As New Rectangle(. we want to set the picker’s date to TextBox1.PointToClient(Cursor..0 – David Ross Goben Of course.PointToClient(Cursor. we can also test for the Alt key this way as well (Keys..Escape). such as a TabPage. so reset control to its original date Me..dtpOrgDate 'not in range.Contains(pt) Then 'if the pick is within the calendar bounds. Panel. For example: Dim pt As Point = Me.. Dim rect As New Rectangle(. we can check it right within our above test by taking advantage of the GetKeyState P/Invoke.Bounds 'using the controls bounds as a guide.DateTimePicker1.DateTimePicker1.. Normally..Parent.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 keya being pressed. . We can check for the Alt key by checking the Boolean result being True with My.EventArgs) Handles DateTimePicker1. 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.NET Beyond the Scope of Visual Basic 6.SelectedText = Me..DropDown Me.AltKeyDown.Parent.Value. we cannot check for the Escape key this way. if the returned value is less than zero then the designated key is pressed down (zero if it is up). like this: Dim pt As Point = Me..Value 'grab current date in calendar End Sub Page –494– .. we want to also reset DateTimePicker1 to the date it held when the calendar dropped down from the control.Computer. 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 : dtpDate_DropDown ' Purpose : The Date Picker calendar dropped down '********************************************************************************* Private Sub DateTimePicker1_DropDown(sender As Object. or whatever..Escape”.DateTimePicker1. GroupBox.Position) 'get cursor relative to DTP container With Me. '<.Bottom.Value = Me. Me. We can take advantage of the Bounds property of that control to check for the pick being within the bounds of the calendar. DateTimePicker1.. we can easily determine if they selected a date or not through added code at the bottom of the CloseUp event. .Bounds Dim rect As New Rectangle(. 'not in range. Me. 160) 'define bounding rectangle for calendar If rect. to pick up the date.Bounds 'using the controls bounds as a guide.Contains(pt) Then Me.DateTimePicker1.Value to acquire any selected date..Contains(pt) Then 'if the pick is within the calendar bounds.Focus() 'set focus to the date textbox End Sub End Class If you want to keep the result wholly within the DateTimePicker control’s Value property. 160) If Not rect..PointToClient(Cursor.NET Beyond the Scope of Visual Basic 6.Left.Value = Me. we must also be sure to reset the control’s Checked property to False.CloseUp If My.DateTimePicker1. 'define bounding rectangle for calendar 'if the pick is outside the calendar bounds. Note.Left.Enhancing Visual Basic .Keyboard.Escape) < 0 Then 'Alt or ESC pressed.DateTimePicker1. .Checked = False End If End With 'using the controls bounds as a guide.DateTimePicker1. so reset control to its original date 'uncheck the checkbox (set flags date change) Afterward.TextBox1. of course.dtpOrgDate 'not in range. when we are resetting the control’s value to its initial date.. 222. Dim rect As New Rectangle(..DateTimePicker1. you can examine the Date held in DateTimePicker1.. which is.SelectedText = Me.Bottom.Parent. In this case.Value = Me. that this checkbox will again be checked because the value has changed.. so see if we should record results '********************************************************************************* Private Sub DateTimePicker1_CloseUp(sender As Object. the above code within the the WITH block can be simplified to: With Me.Value = Me. though.0 – David Ross Goben '********************************************************************************* ' Method : dtpDate_CloseUp ' Purpose : The Date Picker calendar closed up. 222. Then.ToLongDateString 'then set the date to the textbox Else Me.DateTimePicker1. Page –495– . For example. once the user has closed the calendar. that when the DateTimePicker’s date value changes.Position) 'get cursor relative to DTP container With Me..dtpOrgDate 'then reset the control to its original date Me.DateTimePicker1. e As System.DateTimePicker1. In this case..Value. and you can simply test for its Checked property being True to determine if you want to use the date stored in the control.EventArgs) Handles DateTimePicker1. such as when we reset it to its original value.Checked = False 'uncheck the checkbox (set flags date change) End If End With End If Me.Checked = False 'uncheck the checkbox (set flags date change) Else Dim pt As Point = Me.. Me. as we have been doing in all of our example code.DateTimePicker1. just remove its setting of the text box data and access the Value property of the DateTimePicker.TextBox1. of type Date.Computer.AltKeyDown OrElse GetKeyState(Keys.Bottom. so reset control to its original date Me.dtpOrgDate Me. it might be even better if you set its ShowCheckBox property to True so the user can uncheck this box if they decide against applying the date in your application. This can be especially important for displaying tally reports. test again. the results would look rather ragged. we first need to determine the longest line in the text to establish the maximum line width. SmallCaptionFont. which often changes the system font used for displaying the text within menus and message boxes. such as the following: Widgets that are stored in the database Widgets not meeting requirements Widgets that meet requirements Widgets unselected by the operator ------------------------------------------------------Final tally of widgets used in this project : 106 : 46 : 60 :2 : 58 Were we to use LSet() to try to format the above in a proportionally-spaced font. Typically.DefaultFont. recompile. test. and MessageBoxFont to determine the font to display text with. DefaultFont. but menus and message boxes do not like them. by employing a simple function. For example: Dim maxLength As Int32 = TextRenderer. and StatusFont. but you want the totals to line up. Ideally. You can.Text namespace to determine the pixel length of a string. such as CaptionFont. where results are displayed on the right. contains a collection named SystemFonts that you can use to select from several pre-defined fonts.Width. IconTitleFont. 8. recompile again. System. MenuFont. people will often resort to using a new font. Page –496– . like the following (or even worse. However. which often includes a lot of trial and error and a lot of test compiles until the displayed text looks right. sysFont). edit again. alignment can become a process of frustration as we edit the source to manually insert spaces. is used by DialogFont.NET Beyond the Scope of Visual Basic 6. For example: Dim Txt As String = LSet(“Data”. But the good news is. Label. and we need the font we want to use to display it. You can even use the LSet()function to return the provided text that has a specified width. To resolve this issue when we need the text to align. and the width of a space character may even be as short as “.0!. padded on the right with spaces to fill out the required width. FontStyle. we can use the MeasureText() function of the TextRenderer class of the System. we can dynamically pad a string so that it is padded correctly. and the “Data” text is located in its first 4 character locations. But when trying to manually pad text that is displayed in a proportionally-spaced font is a real trick. For example: Dim sysFont As Font = SystemFonts.Regular).Text. MenuFont. one of the imported namespaces. we can establish out target pixel width by getting the length of the longest line in the data. this is not compatible with globalization locales. such as a TextBox or Label. Typically. which is the default system font. DialogFont. 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. based upon the user’s preferences. This creates a 20character field filled with spaces. 20).0 – David Ross Goben Black Book Tip # 44 Padding Strings and Filling Separator Lines in Proportionally-spaced Text Padding string in mono-spaced fonts like Consolas or Courier New is amazingly easy. the width of “i” is different from “W”. and sometimes mapping tab positions in a TextBox. and so on. In this example.MeasureText("This is my longest line: ". such as: Dim TestFont As New Font("Microsoft San Serif". DefaultFont. if we were able to simply add vbTab characters to the text to do this. of course. or RichTextBox seems too much of a bother or even practical for you application. or when the user changes their desktop theme. use the font that is assigned to the place where you will display your text.Enhancing Visual Basic . things would be a whole lot simpler. MessageBoxFont. However.”. With the desired font referenced. Length . Optional ByVal UseChar As Char = " "c. ' : ' : By specifying UseChar with a character value. or using a character other than a space for padding. 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). 5) 'init padding string Do While TextRenderer. “_”.. which will duplicate this list.. If you provide True for the optional PadOnLeft parameter.1) 'strip a pad char from the right end. Consider the following program code.. such as “-”. we could measure the length of each entry and add spaces until we meet the desired pixel width.. Indeed.. we could also add an additional pixel count to this result for extra padding.Width < TargetWidth 'while the text is too short. you can optionally provide a character other than a space in the optional UseChar parameter.. or “. ByVal TargetWidth As Int32.Substring(1) 'strip a pad char from the left end. properly aligned. As you can see. Text. otherwise it will by default pad it on the right. the padding will be added ' : using the provided character. in a MsgBox: Page –497– . ' : ' : This method is useful for displaying a list of result values or you need ' : a separator line that fills a required width. “+”.. If PadOnLeft Then 'if we want to pad the string on the left.NET Beyond the Scope of Visual Basic 6.Width > TargetWidth 'while the text is too long. Consider the following function: '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : PadTextString ' Purpose : Pad a text string at the end until it fits a specified pixel width. If PadOnLeft Then 'if we want to pad the string on the left..Substring(0. it will return a string padded on the right with the proper number of spaces to fill out the pixel TargetWidth. In the heading of the function you see our example list. and a font you want to establish this padding for. return the string End Function When you provide the above PadTextString() function with text to return a modification on.0 – David Ross Goben If we wanted to. ByRef TargetFont As Font. the padding will be added to the left of ' : the string. TargetFont). 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.. ' : depending on the display font. with this feature we could easily construct dividing lines separating a heading string from its list. After that. the string will be padded on the left rather than the right. We should also take into consideration padding the strings on the left instead of the right. 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 show in monispaced text. and actual character widths and of the space. End If Loop 'and try again Return Text 'finally.”.MeasureText(Text. ' : ' : By specifying PadOnLeft=True. otherwise it will default to a space. Optional ByVal PadOnLeft As Boolean = False) As String Dim pad As New String(UseChar.. Else Text = Text.MeasureText(Text.. so ' : padding can usually be a process Of trial And Error. TargetFont). Text = Text. the target width in pixels that you want to fill the text out with. The ACTUAL strings for ' : each line will have different lengths in order to match their right side.. as we build the output string.Enhancing Visual Basic . '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Function PadTextString(ByVal Text As String. Width MsgBox(Msg. "---". maxWidth.iRjctWdgts 'Total Widgets (should be defined elsewhere in the code) 'Widgets that are acceptable to the project 'compute widgets not acceptable 'widgets rejected by the operator 'compute toal widgets used in project Dim Dim Dim Dim msgFont As Font = SystemFonts.Information. maxWidth. Further. maxWidth. maxWidth. msgFont) & ": " & iBadWdgts.Replace("---".ToString & vbCrLf & PadTextString("Widgets that meet project requirements".Width 'define test width Msg As String = PadTextString("Widgets that are stored in the database". we appended “: ” after each to make sure the colons were aligned.ToString & vbCrLf & PadTextString(Nothing. msgFont) & ": " & iRjctWdgts.MeasureText(longestText. We also added a couple extra dashes to it in the above case.Information. Notice even further that we defined the initial line text as Nothing. like the following: Dim msgFont As Font = SystemFonts. once we are ready to actually display it. msgFont.ToString & vbCrLf & "---" & vbCrLf & longestText & iFinalTotal. Notice also we added a couple more dashes to dress up the line. msgFont). maxWidth. You could initialize your list like this: Dim Msg As String = "The following clients are added:" & vbCrLf & "---" & vbCrLf NOTE: Here. MsgBoxStyle. "-"c)) 'replace 3-dash text.Enhancing Visual Basic .ToString & vbCrLf MsgBox(Msg. "-"c) & "--" & vbCrLf & longestText & iFinalTotal.OkOnly Or MsgBoxStyle. msgFont) & ": " & iTtlWdgts. "Widget Results") 'display the message Notice in the above we created a separator line made of dashes. Next. "Widget Results") 'display the message Page –498– . PadTextString(Nothing. msgFont. msgFont) & ": " & iTtlWdgts.MessageBoxFont Dim maxWidth As Int32 = TextRenderer. MsgBoxStyle.MessageBoxFont 'font used by MsgBox longestText As String = "Final tally of widgets used in this project: " 'max legnth string in formatted message maxWidth As Int32 = TextRenderer. maxWidth. with full-width dashed line MsgBox(Msg.MessageBoxFont 'font used by MsgBox longestText As String = "Final tally of widgets used in this project: " 'max legnth string in formatted message maxWidth As Int32 = TextRenderer. maxWidth. to allow for the addition of the colon and space in the other lines. suppose you have a header that you want to separate from a list of items.ToString & vbCrLf & PadTextString("Widgets not meeting project requirements". msgFont) & ": " & iGoodWdgts. maxWidth. "Client Add Report") 'font used by MsgBox 'define max width 'add line and display the message We could redesign the previous example to take advantage of this enhancement as well: Dim Dim Dim Dim Dim iTtlWdgts As Integer = 106 iGoodWdgts As Integer = 60 iBadWdgts As Integer = iTtlWdgts . append your list of items to the above string and terminate each line with a vbCrLf. but you do not know the maximum width in advance (typical). msgFont). Adding One or More Fitted Separator Lines in Mid-Text You can also dynamically build separator lines within text.ToString & vbCrLf & PadTextString("Widgets that meet project requirements". msgFont). maxWidth.Replace("---".iGoodWdgts iRjctWdgts As Integer = 2 iFinalTotal As Integer = iGoodWdgts . msgFont) & ": " & iGoodWdgts. determine the over-all width of the text block and replace the "---" text within the complete message string with a fully-defined line.ToString & vbCrLf & PadTextString("Widgets not meeting project requirements".ToString & vbCrLf & PadTextString("Widgets unselected by the operator".MeasureText(Msg. MsgBoxStyle. msgFont.Width 'define test width Msg As String = PadTextString("Widgets that are stored in the database".0 – David Ross Goben Dim Dim Dim Dim Dim iTtlWdgts As Integer = 106 iGoodWdgts As Integer = 60 iBadWdgts As Integer = iTtlWdgts .ToString & vbCrLf maxWidth = TextRenderer. "-"c)).NET Beyond the Scope of Visual Basic 6. because we wanted the text to align before the colons in the lists.iGoodWdgts iRjctWdgts As Integer = 2 iFinalTotal As Integer = iGoodWdgts .OkOnly Or MsgBoxStyle. maxWidth.Information. Once finished with adding to the list. PadTextString(Nothing.MeasureText(longestText. such as one of the list members might be longer than the header. "---". msgFont). msgFont) & ": " & iBadWdgts. maxWidth.OkOnly Or MsgBoxStyle. For example.ToString & vbCrLf & PadTextString("Widgets unselected by the operator".MeasureText(Msg.iRjctWdgts 'Total Widgets (should be defined elsewhere in the code) 'Widgets that are acceptable to the project 'compute widgets not acceptable 'widgets rejected by the operator 'compute toal widgets used in project Dim Dim Dim Dim msgFont As Font = SystemFonts.Width 'define max width for text block Msg = Msg. we will replace the triple-dash text. msgFont) & ": " & iRjctWdgts. Title.Info.DialogResult = DialogResult. you usually check e. if you want to write code for the Cancel or Accept button’s Click event.Info.Application. assign any DialogResult value other than None to it. if the user hits one of those buttons.Title. Me.ToString) End Select End If End Sub Page –499– .ToString) SaveSetting(My.UserClosing. Though such things might be better served in the form’s FormClosing event. They are having fits when simply trying to define one or two default buttons for their forms. Therefore. "FormTop". skipping any code you might have assigned to the button.Application. when you let the system take care of your button support because you assigned DialogResult values to those buttons. "FormHeight".0 – David Ross Goben Black Book Tip # 45 Making Sense of Form Default Buttons and Their DialogResult Properties This is a rather simple subject. such as saving properties or the form’s screen position.Width. you should make sure that the button’s DialogResult property is set to None.UserClosing. when you process code in a FormClosing event.CloseReason Case CloseReason. "Settings". "Settings". Problems occur when the programmer wants to do after the user presses the button.Title. and especially because they cannot seem to get them to also execute code they wrote for those buttons. you can drop a button on the form. Me.Info. but to the form (which is where the button’s DialogResult property was going to be assigned to anyway. if defined). dimensions. and state.Title. "FormWidth". For example: Private Sub BrowserDialog_FormClosing(sender As Object. very nice and very convenient. they are a bit surprised when the code that they wrote for the button’s click event does not even execute. This way your custom code will in fact execute. though you may have noticed that a button assigned to the form’s CancelButton property will be automatically assigned a DialogResult of Cancel. You should be doubly sure that the DialogResult property is set to None for a Cancel button when you assign it to the form’s CancelButton property. "FormLeft".None SaveSetting(My. there might be other reasons why the programmer wants to massage some data before leaving.DialogResult = DialogResult.Close()”. and then close the form with “Me.Application. However.OK Then 'if user selected OK Select Case e. you will have to check for both values. When the program runs. This auto-assignment does not happen to a button assigned to the form’s AcceptButton property.None 'DialogResult buttons issue CloseReason. Though we generally check this value for being set to CloseReason.Top. CloseReason. At the end of it. regardless. You can even set the DailogResult properties on the buttons so that the value assigned to them will be assigned to the form when the form closes and returns control to the invoker. Thus. people set up their default buttons.Info. such as “Me. the support code for DialogResult-assigned buttons instead issue a value of CloseReason.ToString) SaveSetting(My.NET Beyond the Scope of Visual Basic 6. and go on to the next item on your task list.CloseReason to see if the user closed it normally. but before the form closes. though it will never hurt to verify it. but you did not assign them a DialogResult value. e As FormClosingEventArgs) Handles Me.Enhancing Visual Basic .Ok”. Me. the DIalogResult in the selected button will be assigned to the form and then the form will automatically close.Height.FormClosing If Me. but rather the form closes and returns the DialogResult value assigned to by the button.ToString) SaveSetting(My. and an Accept or OK button to the form’s AcceptButton property.None! As such. such as assigning a Cancel button to the form’s CancelButton property. Typically. "Settings". Me. "Settings". but it has some balding VB programmers pulling their hair out.Left. Buttons Assigned a Dialog Result Have a CloseReason of None Speaking of None.Application. What these people need to understand is that the reason why a button has a DialogResult property in the first place is so you can slap a button onto a form without having to write any event code to support it. you can assign a DialogResult value not to the button. in case any other button also closes the form. 0 – David Ross Goben Black Book Tip # 46 Quick and Easy Text-Justification for Text Boxes. After a few times doing this manually. there is yet a third option. but you can also display them with perfect ease within a system message box via MsgBox() or MessageBox(). and Dialog Boxes Previously I had shown you how to display fully justified text in a TextBox control (see Black Book Tip # 9. I’m sure most everyone has typed an extra space or two between words of a label’s text to fill the line out and make it appear justified. but it still looks absolutely fantastic! For example. This way we can easily find lines needing justification because they will end with only a vbCr. For example. I like to follow my email with this:" & vbCrLf & vbCrLf & " \|/" & vbCrLf & " ~ ~" & vbCrLf & " (@ @)" & vbCrLf & "--oOO-{_}-OOo------------------------------------------------------" & vbCrLf 'final vbCrLf for V-spacing Page –500– . 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. if set to True and the SizeMsgLine() method must add line breaks. to include submitting it not only to a TextBox or Label control.NET” on page 442). “Sizing a Label or TextBox to Fully Contain a String for Display” on page 360. This is important. I further showed you how to easily enable built-in full text justification in a RichTextBox control (see Black Book Tip # 30.NET Beyond the Scope of Visual Basic 6. For example. I realized this process could be easily automated. " & "They can be used to provide additional contact information. they will just be a Carriage return (vbCr). because the method may need to insert additional line breaks to fit the text within a maximum width specification. That solution employed a class that performed a lot of complex real-rime painting services to deliver justified text. Or. Those ending with a vbCrLf will not need justification. Labels. " & "you could simply apply your name. or even an image " & "of your signature. “Enable Built-In Justify-Alignment in a RichTextBox from VB. where the right message was auto-padded with spaces: To achieve this. This was a recent spur-of-themoment solution I wrote in about 30 minutes to justify a lot of message text I had been working with. However. due to the width of a space character for any given font at any given point size. " & "a shot of the kids doing something stupid and/or funny. This type of justification might not be straight-edge perfect. list published " & "work. UseCRonly. even if the TextBox was resized or its font changed. by adding an optional parameter. and that is especially surprising considering how many developers out on the web have wailed about wanting to do this. to add a philosophical anecdote. or anything else you wish to throw down on the end of your email. so you could include your favorite photo. I made a tiny modification to my SizeMessage() and SizeMsgLine() functions featured in Black Book Tip # 7." & vbCrLf & vbCrLf & "You can insert pictures as well. “Display TextBox Text Format-Justified at Runtime” on page 365).Enhancing Visual Basic . and this can even be applied on-the-fly to text. not the usual Carriage Return and Linefeed (vbCrLf). which. The code needed to accomplish this task is unbelievably simple. your Web page. compare these MsgBox displays. " & "to advertise your business. We can check the text’s dimensions by submitting it and the display font to the MeasureText() function of the TextRenderer class (defined within System. vbCrLf)”.Enhancing Visual Basic . We can check the Width property of the msgSize structure for the currently defined pixel width of the submitted text. basically. text-wrapping in a text box is not accounted for.0 – David Ross Goben For the above examples. like this: SizeAndJustifyMessage(msg.MessageBoxFont. SizeMessage() would return the outside rectangular dimensions that will contain this resulting form of the message text. NOTE: The MeasureText() function can also easily process multi-line text. and then checks the remainder of the line. 375 in this case. although point sizes under 16-point look best. Starting with an initial message. you can use broader dimensions. inner line group of each physical line that were broken up that were terminated by just a vbCr. If the line width is longer than our submitted limit. However. and at any desired point size. 375) 'submit message. it simply invokes the previously defined SizeMessage() function. SystemFonts.MessageBoxFont)”. it will search for where the text can be broken up to fit each segment within that limit. So. Its first point of divergence is that it tells SizeMsgLine(). 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. meaning there are vbCrLf line breaks embedded within it.NET Beyond the Scope of Visual Basic 6. to make any needed additional breaks using a single vbCr characters instead of the default vbCrLf characters. so the returned vertical height would be correct. In that case. If so. my new quick text-justification method. Also.MeasureText(Text. such as defined in the above msg definition. It then inserts a vbCr at the needed point. removes the trailing space. physical lines to the SizeMsgLine() support method. Page –501– . The above sounds much more complicated than it really is. which is SystemFonts. which is why we had to insert additional line breaks. However. we can split it on the line breaks and then process each physical line. At that point. and maximum width for message The SizeAndJustifyMessage() function checks the message for having multiple physical lines. such as “Dim Ary() As String = Split(msg. SizeAndJustifyMessage(). though it likewise acquires these dimensions. all text up to a vbCrLf is considered a primary. We then process each of these secondary array lines and check to see if each contains at least one space. display font. proportionally spaced or mono-spaced. which feeds each line to the SizeMsgLine() method to break them up to fit within the maximum limit. I submitted my message to SizeAndJustifyMessage(). For the above example. Once the SizeAndJustifyMessage() function receives the full dimensions of the current state of the message from SizeMessage(). if you are assigning your text to a label or text box. “Dim msgSize As Size = TextRenderer. we do not want to pad any lines that end physical lines (lines ending with vbCrLf). or physical line. though the results are quite impressive. I used the font you should normally use for a dialog box such as the MsgBox. These lines are all the text contained in an array element if we split this message on vbCrLf boundaries.MessageBoxFont. we must process it using the display font and a desired maximum width.Text). 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 have two levels of the text being broken up: we have the primary line group of physical lines that each terminate with a vbCrLf. Indeed. 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. It checks the submitted line for being longer than the requested maximum width value. SystemFonts. The second point of divergence occurs after all original lines are processed. were it displayed using the provided font. via its invocation of SizeMessage(). I declared a data-width limit of 375 pixels. it first will further process all lines ending in only a single vbCr to pad them with spaces to emulate justification. For example. the great thing about this quick-and-dirty solution is that it can work with any font. Further. Though a MsgBox() has a maximum message width of 375 pixels. The way we process this code is rather simple. If you look at the above msg assignment. The secondary level comes when we feed each of these primary. For all this work. it will return the widest string dimension of the text and the height of all the rows of physical lines. and then their secondary. and return a Size object that will ' provide the height and Width required to contain that message.. '------------------------------------------------------------------------------------' NOTE: You can use the returned Size structure to size a text box or a non-auto-sizing ' label. We then loop.. ' and then return a Size object that will provide the height and Width required to contain ' that message.... you can provide that control's Width parameter as the limit. If you anchor all four sides of the textbox or label to the ' form (or its container. we break that line further into yet another array. update the message as needed to add additional line breaks to keep each line within ' the limits of the privided MaxWidth in pixels parameter. The message box itself will ' acquire this structure and adjust its size as needed.Width value. for a message box. '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modComputeMsgDims '********************************************************************************* Page –502– . this time split on its spaces..Enhancing Visual Basic . If you are displaying ' data in a MsgBox..... This should be the font that ' you will in turn be displaying the resulting text with.. ' because the system fonts on the user'ssystem or theme are used. added line breaks to keep the text within the bounds of the ' specified MaxWidth parameter.. If you want the text to be limited to the width bounds of the target ' textbox or label.. we process the next secondary line. Once the primary array has completed processing all of its physical line members. ' ' The SizeAndJustifyMessage function: '----------------------------------' The SizeAndJustifyMessage function. then the text box ' or label will also-size to the size of the text data. and likewise ' subtracting the current width of the textbox or label and then adding the ' Size. ' and the maximum width of the message container. you can simply ' assign the Size structure to the Size property of your textbox or label. which will ultimately end on a physical line break of the original text (such a break is also assumed on a single line message). when the SizeAndJustifyMessage() invokes ' SizeMessage() to initially format the text... you should use SystemFonts. However.... However. and then check the resulting line size (we use a cycling index that increments to a word after each check of the text length).... '. Even so.... that displaying text with line breaks ' consisting of either vbCrLf or vbCr display no differently.. however. the secondary array is rejoined back into its primary array member. will size the message to fit that maximum ' width. except for the last (or only) one. which should likewise be anchored). and then the next primary array member is processed. each time. '------------------------------------------------------------------------------------' 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. ' ' : ByRef Fnt As Font:::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ' This is the font you wish to measure the text with.' Returns: Size structure containing the Width and Height of the text field required to ' Display the Message text...MessageBoxFont.. ' the maximum width value should be no greater than 375. The default MsgBox text field uses a maximum of ' 375 pixels. appending just a space to a single word... a font that the message will displayed with. Once this secondary array has finished processing each of its members. ' if you are formatting the text for display within a system message box. By providing these functions with the text variable ' referencing your text. ' This way it can tell which lines should be padded with spaces to format' justify its text. 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. Once our width goal is met. provided a message. a font that the message will displayed ' with. on return this will contain your text and with any needed ' alterations. ' ' : ByVal MaxWidth As Integer:::::::::::::::::::::::::::::::::::::::::::::::::: ' The maximum width can be anything. This function also makes allowances for lines beginning with spaces. You can also resize the containing form by subtracting the current height ' of the text box or label and then adding the Size.. Anything outside ' this rangetends to look bad.... because it will wrap lines ' outside this range. Otherwise.. 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. that array is finally rejoined back into the original message string.0 – David Ross Goben If a space is found in a line..Height value. ' ' : Optional ByVal UseCRonly As Boolean = False:::::::::::::::::::::::::::::::: ' By default. you do not ' need to worry about retaining the returned value... but in general a rule of thumb is to ' maintain widths in the range from 200 pixels to 550 pixels.. and the maximum width of the message container.NET Beyond the Scope of Visual Basic 6.. The SystemFonts ' class offers several system fonts and is very useful for international apps. it sets this Parameter to True. Note. provided a message.. .. Do While TextRenderer. Fnt) Loop If Idx > 0 AndAlso Idx < Len(RTrim(TextLine)) Then TextLine = TextLine. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function SizeMsgLine(ByVal TextLine As String..MeasureText(TextLine. 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)). 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.1). Idx . 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.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.Substring(0. Idx . and return a Size object that ' : will provide the height and Width required to contain that message. MaxWidth. Fnt). True) 'initially break the line up fit within our width limit '----------------------------------------------------------------------'now see about padding each line '----------------------------------------------------------------------Dim pAry() As String = Split(Message.Substring(Idz. ByRef Fnt As Font.Width > MaxWidth Idx = InStrRev(TextLine. Optional ByVal UseCRonly As Boolean = False) As Size Dim pAry() As String = Split(Message. ByVal MaxWidth As Int32. MaxWidth. Fnt. Dim sAry() As String = Split(pAry(Idx). ' : using the supplied font..Empty 'init leading space buffer Dim Idz As Int32 = 0 'init index to where nons-space text begins Do While sAry(Idy). ByVal MaxWidth As Int32.Enhancing Visual Basic ..NET Beyond the Scope of Visual Basic 6.1 'pad all except the last line (it had ended with a vbCrLf break) If Not String. vbCrLf) 'split out each line of the prompt.. " ") 'stitch secondary strings back together End If Next pAry(Idx) = Join(sAry.MeasureText(Leader & Join(tAry. ByVal MaxWidth As Int32) As Size SizeMessage(Message.. " ". lnTerm) For Idx = 1 To UBound(Ary) Ary(Idx) = SizeMsgLine(Ary(Idx). Also... which is presumed to be the the that the ' : text will afterward displayed with. in case anything changed in the above loop Return TextRenderer. Idz = 0 'then cycle index (we do not want to pad the last member word) End If Loop sAry(Idy) = Leader & Join(tAry. Fnt. UseCRonly) 'to make sure each line will wrap properly Next Message = Join(pAry. MaxWidth. vbCrLf) 'reconstruct the message. MaxWidth. Fnt. vbCrLf) 'reconstruct the array. 1) = " " 'while we have leading spaces..MeasureText(Message. UseCRonly) Next TextLine = Join(Ary. Fnt) Dim Idx As Int32 = Len(RTrim(TextLine)) Do While Idx <> 0 AndAlso TextSize. lnTerm) End If Return TextLine End Function End Module Page –503– 'get the dynamics of the individual line of text 'grab the length of the entire text line 'is its present width greater than the maximum allowed? 'track backward for a space 'recalculate that size based on the space found 'if the length of the text is to be adjusted. Idz += 1 'bump the start index Loop If Idz <> 0 Then 'if leading spaces were found. 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.MeasureText(Left(TextLine. vbCrLf) 'split out each line of the prompt into a primary array.1) TextSize = TextRenderer. pAry(Idx) = SizeMsgLine(pAry(Idx).1) & lnTerm & Trim(Mid(TextLine.0 – David Ross Goben ' 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.IsNullOrWhiteSpace(sAry(Idy)) AndAlso Trim(sAry(Idy)). 'break the text up with vbCrLf and break that down 'now break the text up into an array 'and test each higher line (index 0 is already correct) 'process and break up each part as needed (recursion) 'convert the array into a single string with linebreaks 'return the text to the invoker .. ByRef Fnt As Font. For Idx As Int32 = 0 To UBound(pAry) 'then check the width of each one... vbCrLf) 'stitch strings back together Next Message = Join(pAry. vbCr) 'break each sub-item up For Idy As Int32 = 0 To UBound(sAry) . " ") 'break logical line up at all embedded spaces 'while the final line length is within bounds.MeasureText(Message.. ByRef Fnt As Font. " ").IndexOf(" "c) <> -1 Then 'if this string contains at least 1 embedded space. '********************************************************************************* Friend Function SizeMessage(ByRef Message As String. Dim Leader As String = String. For Idx As Int32 = 0 To UBound(pAry) 'then process each line block (made by breaking up by vbCr's). Idx + 1)) Dim Ary() As String = Split(TextLine. allow for leading spaces '********************************************************************************* Friend Function SizeAndJustifyMessage(ByRef Message As String. in case anything changed '----------------------------------------------------------------------Return TextRenderer. Fnt. Idx ... Me.Label1.Enhancing Visual Basic .Width . and in all it also implements much more robust techniques to apply their functionality.Text = Msg 'stuff the updated text End Sub End Class NOTE: If you anchor Label1 to all four sides. " & "Regardless. which is something VB6 was not always good at. Of the real " & "platform deviations.Width + sz.Width = Me.Height Me.0 (VB6) to Microsoft Visual Basic . To ensure text or expanding this label will not cause it to be clipped off the form.Me. or. we assign the new size to the label. due to tight .NET cannot support them in " & "a like manner. are easy to use. the display looks great. After that." & vbCrLf & " Other disparities.NET does in " & "fact support. Running this. so in the above Form1 Load event.Object. you can easily " & "emulate ""lost"" VB6 commands. make their functionality more accessible through simpler syntax.Font. many others are simply imagined.ToString”. " & "may look to be an intimidating endeavor. you can remove the Label1. 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. we can store a copy of it in the Tag property of Label1.Font. Nevertheless. you have likely heard or read through copious magazines and " & "blogs that there are huge differences between these two developmental platforms.Label1. After all.NET using a radically different " & "access conduit than the way it may have been realized under VB6. in " & "but different forms. as in “Me. we will need to add a ReSize event for the form.Size = sz 'size the display label Me. Me. Their uses are identical.Label1.EventArgs) Handles MyBase.NET (VB. Though many of those differences " & "are real. and then adding “FormLoaded = True” before the End Sub command for the event to let all know the form has now been loaded. But. We will need to keep an original copy of the message text.Tag. in the resize event.0 – David Ross Goben The two exposed functions. in most cases. actually end up being VB6 features that VB.Load Dim Msg As String = " Transitioning from Microsoft Visual Basic 6.Size assignment because the form resizing will then automatically resize the label’s dimensions for us.Width .Left * 2”. Then. by employing " & "some simple user-defined helper functions.Label1. both major and minor.NET supports all these many differences. SizeMessage() and SizeAndJustifyMessage(). VB. NewWidth)”.Label1. at first glance. We will also initially size the label to fill most of the form. Me.Height = Me. such as will be demonstrated within this document. but we do not want it to do anything until the form has loaded. Notice further that it will leave any leading spaces on a line alone. and if so we will need to compute a new target width.Me. but may by necessity have to utilize non-VB6-style invocation rules. after we assign the message to Msg. e As System.NET )." Dim sz As Size = SizeAndJustifyMessage(Msg. except that the SizeAndJustifyMessage() function will additionally justify the text by padding embedded spaces within it. we first test to see if FormLoaded is True. you will find that.NET " & "strictly follows a stringent pattern of uniform language syntax. 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. " & "Hence.Width) 'initially format the message Me. engendered by nothing more than unapprised conjecture. a programming language feature may have to be implemented under VB. Suppose you 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. but.Height + sz.Height . overall. Page –504– . we can achieve all this with the following form code: Option Strict On Option Explicit On Public Class Form1 Private Sub Form1_Load(sender As System. you can do even more here. most are simply due to them having to be expressed differently. Next.ClientSize.Width 'initially size the form Me. we will also resize the form to accommodate the formatted data. some seen as much more profound. Next.Label1. VB. We then compute a new Size structure: “Dim sz As Size = SizeAndJustifyMessage(Msg. we get a copy of the original message from the label’s Tag property: “Dim Msg As String = Me.NET Beyond the Scope of Visual Basic 6. We can do this by defining “Private FormLoaded As Boolean = False” above and outside of the form’s Load event. if the new size does not match the label’s current size. again. plainly because VB. Me.Label1.Label1.Label1.Label1.Tag = Msg”.NET platform architectural specifications. For example: Assuming that a variable named Msg of type String contains the message we want to display. Width . most are simply due to them having to be expressed differently. actually end up being VB6 features that VB.0 – David Ross Goben Altogether.EventArgs) Handles MyBase. you should also define a minimum size for your form to prevent the code from crashing if you resize it too small (see the form’s MinimumSize parameter). a programming language feature may have to be implemented under VB. but.0 (VB6) to Microsoft Visual Basic . 'resize the display label 'update the formatted text End Class With this updated code in place.Resize If FormLoaded Then Dim NewWidth As Integer = Me.Label1.. our updated resize-capable form code would look like this: Option Strict On Option Explicit On Public Class Form1 Private FormLoaded As Boolean = False 'true when the form has loaded Private Sub Form1_Load(sender As System. in " & "but different forms. Me. or. If not. plainly because VB.NET supports all these many differences.NET Beyond the Scope of Visual Basic 6. overall. and in all it also implements much more robust techniques to apply their functionality. After all. we can now resize the form with the mouse and the label’s text will update as needed.NET (VB.Size = sz 'size the display label Me. VB.NET does in " & "fact support.Label1.Height . Page –505– . To make it more robust. in most cases.Font. " & "Regardless. such as will be demonstrated within this document. some seen as much more profound.Width) 'initially format the message Me.Height + sz.Label1.Label1. Nevertheless.ToString Dim sz As Size = SizeAndJustifyMessage(Msg.Label1. Me. e As System. or docked it.Label1.Label1. you have likely heard or read through copious magazines and " & "blogs that there are huge differences between these two developmental platforms.Tag = Msg 'save original copy of message Dim sz As Size = SizeAndJustifyMessage(Msg.NET cannot support them in " & "a like manner.Label1. e As System." & vbCrLf & " Other disparities. I am assuming that you have anchored all four sides of your label to the form.Text = Msg 'stuff the updated text FormLoaded = True 'allow resize to operate End Sub Private Sub Form1_Resize(sender As Object.Object. " & "Hence. at first glance.Label1. but may by necessity have to utilize non-VB6-style invocation rules. fully justified.Me.Size <> sz Then 'Me. many others are simply imagined. 'compute the new target width 'grab an original copy of the message 'compute new format 'if the size has actually changed. both major and minor.Width 'initially size the form Me..Tag.Label1. " & "may look to be an intimidating endeavor.Label1.Size = sz” statement.Me.Font. then uncomment the above “Me." Me.Text = Msg End If End If End Sub 'if the form has loaded.Label1. by employing " & "some simple user-defined helper functions.Me. make their functionality more accessible through simpler syntax. Of the real " & "platform deviations. you can easily " & "emulate ""lost"" VB6 commands.Left * 2 Dim Msg As String = Me. Though many of those differences " & "are real..NET platform architectural specifications.Size = sz Me. due to tight . engendered by nothing more than unapprised conjecture.ClientSize. VB. NOTE: In the above code. which is something VB6 was not always good at.NET " & "strictly follows a stringent pattern of uniform language syntax.Load Dim Msg As String = " Transitioning from Microsoft Visual Basic 6.Height = Me.EventArgs) Handles Me. you will find that..Enhancing Visual Basic .Width + sz.Width = Me. NewWidth) If Me.Width .NET ). again.Label1.Label1.NET using a radically different " & "access conduit than the way it may have been realized under VB6.Height 'Me. Me. just to add a noticeable gap between them. it is important that we have adjusted the ItemHeight property of our control to accommodate our images. whether generated for you or entered by hand. 'clear background of line position 'with the provided listbox Here.Enhancing Visual Basic . is very simple. sender and e.Y)” or “e. That can sometimes be hilariously goofy before we correct them. the default defined for SystemFonts. This is as easy as checking “e. the basic skeletal logic I add to this method is the following: Private Sub DrawListboxItem(sender As Object. Next. adding images. e.Graphics. which in this case is 16: “Me. When working with listed items. e As DrawItemEventArgs) If e. Here in the USA.Bounds.Bounds. or afraid they will mess something up. to display our images without overlapping them.Graphics. Page –506– . such as in a ListBox or ComboBox.RadioSel. there is also usually very little code to mess up in this whole owner-drawing business anyway. e. However.ToBitMap..DrawBackground()”. which you can do by simply issuing “e.DrawBackground() With DirectCast(sender. We can store class objects here instead of just strings. Fortunately. After that. In this case. Actually.Resources. TreeView. So. If we draw an image in our resources named “RadioSel”.Bounds. we should change the height to match our images.NET Beyond the Scope of Visual Basic 6. but this default font can vary between international. we could use either “e. before you actually draw anything. especially those we create on-the-fly.Index <> -1 Then e.DrawImage(My. Next. e. e As DrawItemEventArgs) End Sub NOTE: This hand-made version does not display a Handles shorthand reference. it is just like drawing to a PictureBox control using the e.. which we might define as a ListBox or ComboBox. If we are working with a ListBox.RadioSel. We can also take advantage of that feature..DrawIcon(My. we could issue the command “e. or ComboBox.Index <> -1”. NOTE: You may prefer to set the ItemHeight parameter to 17 or 18 instead of 16 if your images actually fill their 16-pixel height definition with no border.. The two parameters above.Resources. but I keep my guard up. though this depends on our system font. some basic knowledge of what the parameters provided to you in the DrawItem event handler can go a long way. we can just as easily add them to any control using AddHandler.25point Microsoft San Serif.Location)”. This is not documented – it is just something I had first noticed in VB2008. e. most programmers seem afraid of owner drawing. ListBox) 'The FUN stuff goes here.DrawImage(Me. which in this case is an index within the Items collection of the ListBox.DrawImage(My.Resources.ListBox1.Graphics.DefaultFont is 8. ListBox. drawings.Index provides us with the index to the list item we are working with. accessibility. owner-drawing this stuff is so easy it is almost stupid.Bounds. In most situations we work with 16x16-pixel images or icons.RadioSel. No bombs will go off.Images(1). e. Indeed. you know how you can really spiff a project up with ease. If this were an icon. e.Location)”. we can draw our image.Bounds. you want to prepare your drawing area by clearing it.ItemHeight = 16”. By default. For example: Private Sub DrawListboxItem(sender As Object.0 – David Ross Goben Black Book Tip # 47 Owner-Drawn Controls Made Easy If you do any owner-drawing in a ListView. etc. Sadly. your first order of business is to ensure that the index the control is sending you is valid.Location)”. checkboxes. you will only mess up its visual presentation. End With End If End Sub 'if the index is not out of bounds (pre-VB2010). Of course. If we are displaying the 2nd image in an ImageList named ImageList1. Suppose we want to display an image or an icon at the start of each line. though we could have added it.ImageList1. Tab Control. provide us with all the tools we will need. for example. A DrawItem event handler. It may have been fixed.Graphics object. The sender object is actually a reference to our control. the ItemHeight property is set to 13. 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.Graphics. we can use “e.X. and theme settings. The reason we are clinging to our e. What we need is the text.0 – David Ross Goben NOTE: Notice that we are using integer values where the documentation requests floating point values. Finally.NET Beyond the Scope of Visual Basic 6..Font. 16))) e.DrawFocusRectangle because it will be done automatically for us.Graphics. a ListBox or ComboBox line has a 3-pixel margin on each of its four sides.Bounds. which would have provided us with this method body: Private Sub ListBox1_DrawItem(sender As Object. though you can technically draw to the entire exposed list surface..Items(e. Indeed. Even though a ComboBox exposes this method.DrawFocusRectangle() 'draw a focus rectangle (not need by ComboBox) End If End With End If End Sub NOTE: When drawing to a ComboBox.X + 16 + 4. New Rectangle(e. e As System. We can do that easily by specifying “Me. This is also very easy to do.Bounds. is that its transparency color will show the background color behind it so we will not end up with little background boxes around certain images that lack a transparency color definition.Location.Graphics. e. Because we are wrapping code within a With block that defines our ListBox. e.Y)”.Images(1).Resources.DrawMode = DrawMode.X + 16 + 4.Location.Bounds. To do all these things.Index Then 'if this is the selected item. though using 32-bit PNG images with transparency works just as well.ListBox1. Because our images are 16 pixels wide.Index). The advantage of using the DrawIcon() method.DrawIcon(My. which is actually a ListBox). an additional 4-pixel buffer. or setting that parameter from the IDE.Black”. e. New Size(16.Location) 'draw our image 'e.Y) 'draw the text of the color 4 pixels beyond the image If . or.RadioNoSel. Brushes. we can reference the ListBox font using “. You may also notice it takes into consideration margin definitions.Bounds.Items(e.Forms.DrawImage(Me. we need to tie it to our ListBox. 16))”).Index).DrawBackground() 'clear background of line position With DirectCast(sender. We could have selected our ListBox and chose its DrawItem event. Next.DrawString(. .RadioSel.DrawItem End Sub And then we could have inserted the above body code within it.Items(e. this is allowed because these methods are able to do automatic type conversions via implicit casting. for appearances. we usually want to display the text assigned to the ListBox (or ComboBox).OwnerDrawFixed”.Black. Altogether.Bounds rectangle? The Bounds property just gives you the bounds definition of the line entry we are working on.DrawString(. our DrawListBoxItem() method becomes the following: Private Sub DrawListboxItem(sender As Object.Graphics.Graphics. but to the drawing surface.DrawItem. e. 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.Bounds. Brushes. AddressOf DrawListBoxItem”. Though thinly documented. We also need the coordinate of the top-left corner of the text as either a PointF structure or two single-precision X and Y values. we want to specify an X coordinate that is beyond that image. such as “New RectangleF(e.DrawItemEventArgs) Handles ListBox1.. and. if you inspect the e.Location) 'draw our image 'e.Font”. By default. And that is all there is to it! Squeezy-cheesy nice-n-easy! Page –507– .Graphics.Y value. we can grab the text for the indexed item with “. a color brush to paint the text.Font. we can tie our hand-made method to any ListBox using something like: “AddHandler Me. . it actually has no effect and can safely be ignored! But now that we have this thing. we can specify “e.ToString. and the coordinates of the top-left corner of the text as either a Point.Resources. we need to tell the ListBox that we want to draw its line items ourselves. e..Index <> -1 Then 'if the index is not out of bounds.Bounds. were you aware that you can draw outside the e. This is also easy to do. e As DrawItemEventArgs) If e. e.ImageList1.Enhancing Visual Basic .Index). even though we do need to specify both the start X and Y coordinates (or else we can specify a bounding rectangle. you do not need to issue e. ListBox) 'with the provided listbox e.Windows.Black.Bounds.Bounds.SelectedIndex = e. Finally.ToString”.DrawImage(My.ListBox1.Bounds structure is because it is not defined relative to the line. e. you will notice that it will actually specify the target coordinate relative to the top of the ListBox (or the ComboBox drop list. or else the X and Y coordinates for the top-left corner of the text. and a black brush using “Brushes.Bounds. e.ToString. New SizeF(16. the font to shape the text. Font... and.ToString). we set its Draw Mode to OwnerDrawnFixed. rect) 'fill the rectangle with the color brsh.ItemHeight . create a new Windows Form Application. except for system colors.Items(e. which discards object that are no longer referenced by anything viable.Top) Dim rect As New Rectangle(orgPt. .. AddressOf DrawList 'add and event handler for drawing list items '--------------------------------------------------------------------------For Each KnownColor As KnownColor In [Enum]. rect) 'draw a rectangle around it e. we then dock the ListBox to fit the client bounds of the form. where we will simply draw some color blocks along with their color names in a ListBox.DrawFocusRectangle() 'draw a focus rectangke around it End If End With End If End Sub End Class Notice that we did not even declare a name for our ListBox. and add an event hander '*********************************************************************************** Private Sub Form1_Load(sender As Object.Dispose() 'dispose of the created brush resource e.Black.0 – David Ross Goben For a practical example that you can slap together faster than poop through a goose.Parent = Me 'let the form adopt this new listbox .Enhancing Visual Basic .Left. so we just focus on the base color palette.Index).X + clrWd + 8. Internal references will keep it alive throughout the lifetime of the application.Margin. e As EventArgs) Handles MyBase.X + .Fill 'fill the client area with the listbox .ToString) 'then add it to the list End If Next End With End Sub '*********************************************************************************** ' Method : DrawList ' Purpose : Draw a single item in the listbox (invoked by listbox for each displayed item '*********************************************************************************** Private Sub DrawList(sender As Object. without adding any controls to it.Items(e..Margin. named or not. e As DrawItemEventArgs) If e. fill it.Margin.Graphics.ToString)) 'define a brush from the color name e. e.OwnerDrawFixed 'define owner-drawn mode with fixed-height data AddHandler . and being scooped up by the Garbage Collector when control leaves the Load event.SelectedIndex = e.Y + .Index). less T-B margins Dim clrWd As Int32 = 64 'color field width (try setting to clrHt) Dim orgPt As New Point(e.Bounds. we turn its sorted option on. . We attach it to the form by making the form its parent. Brushes. thus preventing the control.Add(KnownColor.Bounds. from going out of scope.Graphics. orgPt. NOTE: The assignment of a control’s Parent property will internally add a reference back to the control from that parent by making it a referenced member within the parent’s Controls Collection (you can find the un-named control in an enumeration of the parent’s child windows (internally.DrawRectangle(Pens.Margin.Index <> -1 Then 'if the index is not out of bounds.Load With New ListBox . narrowed-scope declarations normally do. Page –508– .ToString. so we will ignore those by not adding any that have their IsSystemColor property set. e. We next enumerate all known color names in the system to this list.Graphics. New Size(clrWd.Sorted = True 'the list will be sorted alphabetically . System colors are defined by themes..DrawBackground() 'clear background of line position With DirectCast(sender.FillRectangle(brsh..DrawString(. clrHt)) 'define a rectange to fill with a color Dim brsh As New SolidBrush(Color..DrawMode = DrawMode. and we finally attach a draw-item event to it.Index Then 'if thiis is the selected item.IsSystemColor Then 'if we did not grab a system color. which should be empty except for a blank form class body.Items. e.Black. with the following: Option Explicit On Option Strict On Public Class Form1 '*********************************************************************************** ' Method : Form1_Load ' Purpose : load the form and add a listbox to it. all controls are actually window objects)). we simply instantiate it.FromName(.. simply replace the form’s code.Y) 'draw the text of the color 4 pixels beyond the image If .NET Beyond the Scope of Visual Basic 6. ListBox) 'with the provided listbox Dim clrHt As Int32 = . orgPt.Dock = DockStyle.Top .FromName(KnownColor.ItemHeight = 24 'change color field height from 13 .Bottom 'compute color field height. as local.DrawItem.GetValues(GetType(KnownColor)) 'now parse each know color enumeration If Not Color. X + nCount * Me. then all you have to worry about is taking care of the text. simple or complex. and FolderCanOpen for the third. For example. NOTE: You can add these 3 Images to your project. e As DrawTreeNodeEventArgs) Handles tvFolderTree.Other code goes here End Sub Page –509– . That aside. to leave a 1-pixel gap above and below it. the first thing we need to do is compute the indent depth. FolderIsOpen for the second image. I created mine as PNG with Transparency files (RGB/A – 32 Bit) using Axialis Software’s IconWorkshop from an original screen capture of File Explorer. The branch lines and indenting will still be done for you. Their problem is that the bounding rectangle the event provides them encompasses the entire line for the current node to be drawn. it does allow us to compute indenting easily and it also allows us to click on our “v” or “>” images to toggle them expanded or collapsed.Node 'start with current node provided to us Do While nd. such as emulating the branching used by File Explorer. “v”. These three graphics are simple 16x16-pixel images. We could reference these as FolderSide for the first image of the on-side folder. in case I add other images and do not want to have to later change any hard-coded index numbers.Bounds.Indent 'compute starting offset from left of bounds Dim Y As Int32 = e. We can traverse the tree branches by defining a reference to the node we are working on and then check to see if it has a parent node. However. Just include the sample code listed at the end of this article. you will have to resort to taking full control by setting the TreeView’s DrawMode property to OwnerDrawAll. For example: Friend Enum FolderImages As Int32 FolderSide FolderIsOpen FolderCanOpen End Enum In our TreeView’s DrawNode event code. just like the “+” and “–” images allowed us to do in a regular TreeView. Loop 'and try again Dim X As Int32 = e. as this grants you freedom to render each line however you like.. But.tvFolderTree. nor should there be. We start our indent counter at 0 and then increment it each time we move up a node branch level. It does not include any text offsets. If it is a folder that contains no sub-folders..NET Beyond the Scope of Visual Basic 6. I also defined an enumeration to specify their image index offsets. We continue checking on a loop until no parent node is found.Parent IsNot Nothing 'while the node as a parent nCount += 1 'count a generation (indent index) nd = nd. This is very easy. some programmers get frustrated when drawing the nodes of TreeView controls whose drawing event features a DrawTreeNodeEventArgs parameter. it shows a downpointing angle bracket next to an on-side folder.Y 'starting offset from top of bounds '. we will bump the counter and point the reference to its parent node. be sure to set both the ShowRootLines and the ShowPlusMinus parameters to True.Enhancing Visual Basic .Parent 'point to parent. when a folder is open. to make all this very easy. Indeed.Bounds. it shows just an on-side folder. we can perform some really slick effects quite easily in this mode. However.. If you set the control’s DrawMode property to OwnderDrawText. it shows a faded right-pointing angle bracket next to an onside folder. For example: Private Sub tvFolderTree_DrawNode(sender As Object. if you also want to draw the branches. “>”. When the folder is closed and it contains at least one folder. though you could even create 16x16 BMP files with white backgrounds using Windows Paint and get decent results. Just center the other two. If it does. The trick is that we might need to back off one if we have the TreeView’s ShowRootLines parameter set to False.0 – David Ross Goben Black Book Tip # 48 Owner-Drawn TreeViews Made Easy In the last Black Book Tip I showed you how to do general owner-drawing of controls whose drawing event featured a DrawItemEventArgs parameter. I did shorten the height of the folder image by 2 pixels from 16pixels to 14-pixels. I saved these in an ImageList control named ImageLists..DrawNode Dim nCount As Int32 = 0 'init indent counter to 0 Dim nd As TreeNode = e. Though the TreeView will no longer draw these items due to ownerdrawing. Window Else e.FolderIsOpen).Node. which is supplied to us as e. For example: e.Node.Node. X + 16 + 16 + 4. If we did draw a selection rectangle. we can check its IsSelected property to tell if we should draw a focus rectangle. X. it will redraw text without clearing its space when you track over the +/. but I let the compiler’s optimizer worry about merging those details while on my end I keep the offset more understandable. We are now ready to draw our images.Highlight e. Y) 'draw our on-side folder (16x16) NOTE: Some would argue that instead of specifying “16 + 16 + 4” we should instead specify “+36”.DrawFocusRectangle()”. Of course.WindowText End If 'if the node is selected.ImageLists. or “>” if it is closed. For some silly reason the default FolderBrowserDialog control does not show the node if you specify an initial path in the control’s SelectedPath property.Color = SystemColors. X + 16.FillRectangle(Brush. e. By checking the node being worked on. To get the bounds of this rectangle. then I believe that it should have precedence).Graphics. we should actually draw this focus rectangle first (and we can also just clear the background). Me. Of course. we can just use e.Dispose() 'release brush resources e. We could even draw the text first if we wanted to.ImageLists.Node..Window) 'brush used for selection and text coloring If e.Enhancing Visual Basic . The second image will be our on-side folder.Color = SystemColors.Count <> 0 Then e. Now all we have left is drawing a “v” image if the node is expanded. 'draw a selection rectangle around it 'change the brush's color to white '(later code will narrow this to just the text area) 'we will draw the text normally '. because we are creating a brush resource.. a “>” image if it is not expanded but contains sub-folders.HighLight.. we then set the brush to the color White (or SystemColors. we should be sure to dispose of it when we are done with it. if we find that the node is selected.DrawString(e.Graphics. However... the background is auto-initialized because there is no e. For example: If e. we will actually draw two images.. or nothing if it is a folder with no subfolders.Other code goes here Brush. we will create a brush of SystemColors.Graphics.Images(FolderImages. Y) 'draw text beyond two possible images plus buffer Brush. which is your Documents folder (I have no problem with a RootFolder having precedence if both parameters were specified. it being pulled from my folder-only BrowserDialog control I had developed to enable the user to view the starting file path node.DrawImage(Me. Because we will always draw the second image. and then this brush can afterward be used to draw our text. If the node was not selected. e. For example: Dim Brush As New SolidBrush(SystemColors.FolderSide).tvFolderTree. effectively painting the entire node line. I would prefer the text on that line be white instead of the default black to heighten its readability. but if only SelectedPath was set by the user. This sometimes causes the user-drawn text to look slightly bolder. Y) End If Page –510– 'if the folder is expanded. The first will be the “v” image if the folder is actually opened. 'draw "V" in front of on-side folder (16x16) 'not expanded. Indeed.NET Beyond the Scope of Visual Basic 6.Text. we can draw them in any order.DrawImage(Me. If it has no subfolders. We can easily fix this by simply clearing the exact rectangular area where the text is drawn (we do this to minimize flicker). Y) ElseIf e.Dispose() 'release brush resources Here. To consolidate all our needs.Bounds.areas. X. the on-side folder.Color = SystemColors.Images(FolderImages. we use the default Black color. Fortunately. we can start by setting aside a Brush object that can be used to draw the focus rectangle or initialize it background. and then paint our images and text over it. providing this service is incredibly easy.HighLight and fill the bounds of the rectangle. If we have a folder that contains sub-folders.ImageLists.FillRectangle(Brush. though we will also need to support this feature if a node is selected.Font.IsExpanded Then e. This is due to fonts with alpha-blended text being redrawn over themselves. 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.Node. but rather it shows instead the default RootFolder node. This is what a TreeView normally does when you select its FullRowSelect property. but be careful! Although it does initialize.Bounds) Brush.FolderCanOpen).Graphics.Bounds) Brush. When doing owner drawing on a TreeView. in drawing a focus rectangle of a color such as SystemColors.DrawBackground() method available.Images(FolderImages. so draw ">" in front of on-side folder (16x16) .Graphics. Brush.0 – David Ross Goben NOTE: My particular example does not display files.. but does it have sub-folders? 'yes.Window). Also missing is “e.Nodes. we will skip 16 pixels past that space.Graphics.IsSelected Then Brush.DrawImage(Me. NET Beyond the Scope of Visual Basic 6.Graphics..IsSelected Then Brush.DrawImage(Me.myImages. can be found in Black Book Tip # 50 on page 527..Color = SystemColors.. e. which support the above code.Images(FolderImages. etc. Rect) Brush..Count <> 0 Then e.DrawString(e. pt) End If pt..Images(FolderImages.Y) Rect. get copy of line bounds.Images(FolderImages.Node. 'brush used for selection and text coloring 'if the node is selected. so draw ">" in front of on-side folder 'point to on-side folder position 'draw the on-side folder 'draw text 'release brush resources NOTE: The image to the left was captured from my FolderBrowser dialog control. which includes all the images (in-code). e As DrawTreeNodeEventArgs) Handles Dim tv As TreeView = DirectCast(sender.DrawImage(Me.Text. The complete source code for the above Folder Browser Dialog. offers a lot of potential for some fancy design work. NOTE: The fact that you are able to draw to the entire surface of a Treeview. tv. 'compute size of text area to minimize flicker...FillRectangle(Brush..Color = tv...Dispose() End Sub tvFolderTree.. 'change background to highlight color '(we could use Rect here to minimize highlight) 'change the brush's color to white 'clear JUST text area background.Font) Dim Brush As SolidBrush = New SolidBrush(tv. such as easier logical and virtual drive selection. TabPage..X + 16 + 16 + 4 Rect.FolderCanOpen). This includes the L-shaped node. However.Folderside)). tv.. and making it a bit more robust. an Up-OneFolder option. and so on. navigation history browsing.IsExpanded Then e. 'and try again 'otherwise.Enhancing Visual Basic .Node. ComboBox. Page –511– .Node.ForeColor End If If e.White Else e.DrawImage(Me. 'we will draw the text normally 'if the folder is expanded.X += 16 e.DrawNode 'get reference to TreeView container 'init indent counter to 0 'start with current node provided to us 'while the node as a parent 'count a generation (indent index) 'point back to its parent node. TreeView) Dim nCount As Int32 = 0 Dim nd As TreeNode = e.X = pt. and ShowLines. as you actually can to a ListBox.Parent Loop Dim Rect As Rectangle = e. which is typically 19 pixels).myImages.Bounds) Brush. reflecting its properties of ShowRootLines.Node. You could define additional images of a vertical line which you could draw to each preceding tree level (just use the control’s Indent parameter to set the spacing between images. pt) ElseIf e. but does it have sub-folders? 'yes.Graphics. See the subsection on the next page to see how to add the three folder images shown earlier and above. pt) e.myImages.FolderIsOpen).Highlight e.Indent. and to include displaying a starting Selected Path if I set it.Parent IsNot Nothing nCount += 1 nd = nd. Brush. branch connectors. nodes that can expand or contact.FillRectangle(Brush.Size = TextRenderer.. 'draw "V" in front of on-side folder 'not expanded. even drawing within just the provided bounding rectangle.Graphics. Rect. Rect.Graphics.BackColor) If e.Color = Color. by just invoking a simple method. You could also very easily draw these items manually.X + nCount * tv.Location) Brush. our TreeView’s DrawNode event code becomes: 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE ' Folder image Index List for User-Drawn Nodes in a Folder-Based TreeView Control 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE Friend Enum FolderImages As Int32 FolderSide FolderIsOpen FolderCanOpen End Enum '********************************************************************************* ' Method : tvFolderTree_DrawNode ' Purpose : Draw node with indent and state '********************************************************************************* Private Sub tvFolderTree_DrawNode(sender As Object.Graphics.Font..Bounds Dim pt As New Point(Rect.Node Do While nd. you can actually emulate all the TreeView effects. 'compute Top-left coordinate for drawing 'point Rect to text drawing area.. It includes all the functionality that I wish the default Folder Browser Dialog control had.0 – David Ross Goben Altogether.Text. ShowPlusMinus.Nodes.Graphics.Node.MeasureText(e. Images(2) End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : ConvertBase64ToImage (invoked by InitializeImageList) ' Purpose : Convert a Base64 String to an Image object.Images(0) '-------'Image 2 FolderIsOpen 'V' '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABUSURBVDhPYxgFVAYzZ878X15e/v/jx4//oUJgAOKDxBcs" & "WIIijgFAipycnFAMgWmGiYMV4gLoiknSDAPImnx9fUnTDANk2YwOQIYQDLRRQA3AwAAAiXFatNps77AA" & "AAAASUVORK5CYII=" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.MemoryStream = New IO. the on-side folder.Images.myImages.ImageLists)”. “InitializeImageList(Me. to see how you can embed images as text within your own source code.Clear() 'initialize image list imgList.Close() 'release stream resources Return Img 'return image End Function Page –512– . and On-Side Folder to a provided ImageList ' : ' NOTE : If you want to append the images to an existing list.ImageSize = New Size(16.Add(Img) 'add this image as Me.Add(Img) 'add this image as Me.. Black Book Tip # 49.myImages. Embedding Images within Your Source Code. See the next entry. imgList. For example. pass an ImageList control to the InitializeImageList() method below and it will fill it with these three images.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO. to your project. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function ConvertBase64ToImage(ByVal strImg As String) As Image Dim bAry() As Byte = Convert.myImages. 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.Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.Images. Optional ByVal Replace As Boolean = True) If Replace Then 'if we are filling. set the Replace ' : parameter to FALSE.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image.Images(1) '-------'Image 2 FolderCanOpen '>' '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABVSURBVDhPYxjc4OPHj/+hTNLB9evX/x84cOA/2YacOXPm" & "/4IFS8g35D8QUMUQkGaKDAFp2rRpE9iQu3fvkmYARS4AaaYoDCgOQIrTAQhQpHmoAAYGAFSwieHivX4c" & "AAAAAElFTkSuQmCC" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images. not appending images.Images. “V” and “>”.Add(Img) 'add this image as Me.FromStream(memStream) 'construct image from stream data memStream. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub InitializeImageList(ByRef imgList As ImageList.. "V". '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : InitializeImageList ' Purpose : Replace or append ">".0 – David Ross Goben Adding the Three Featured Images to the Above Project Sample Code If you would like to include the three 32-bit PNG with transparency images. to the section Translating Base64 Data Back to Its Original Format on page 255. Each of its image resource members refers to its assigned image in one of two ways. if you want to store your image as a PNG format image to a memory stream and the image’s name was srcImage. an image can easily convert to a memory stream.ImageFormat. The value assignment for the entry is not the usual relative drive path. Imaging.Imaging namespace. System.Enhancing Visual Basic .Bitmap. Converting Between an Image and a Base64 String The thing that has often frustrated programmers is first. Lots of frustration is also due to the document scarcity on this subject on MSDN’s website. but for them to use it they either had to first add a slew of images to their resources. Indeed. For example. because conversion between streams and images is not at all complicated. Page –513– . If you have had any experience with these object types. we can covert a memory stream back to an image object even more easily: Dim srcImage As Image = Image. However. but rather a raw Base64 string rendering of the Image object’s binary data.Drawing. by referencing the original image file in a local Resources folder from where it will be extracted during compilation.Drawing" mimetype="application/x-microsoft.Imaging namespace and using its WriteableBitmap() class to save the image to a memory stream.0 – David Ross Goben Black Book Tip # 49 Embedding Images within Your Source Code Have you ever wanted to send quick code to someone. Some solutions in this regard involve importing the System.Windows. but it cannot convert directly to a memory stream.base64"> <value> iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAA6SURBVDhPYxgFBMF/KE0WAGmGYZIBsmaSDcGmGYYJAmya 0DFOgE0xLowBsCkihOEAmySxeBQwMDAAAGmYP8GTgAQzAAAAAElFTkSuQmCC </value> </data> NOTE: For details on Base64 encoding. refer to the previous article. not your resources.Image" type="System. and its BitmapImage() class to read a memory stream to a bitmap.object.FromStream(memStream) 'construct image from stream data Clearly. which are interchangeable. you might be aware that a form’s “.bytearray. even my modest examples show above should help clear up many of the mysteries surrounding it. But this has me confused.NET. which is a waste if you are not using any other part of it. there is no need for any of that additional code overhead.Save(memStream.net. Send (SMTP) and Retrieve (POP3) Email with Ease under VB. If you look up some of the functions. or if you imported it directly from a file. how to convert the Base64 string back into an Image object. Here. or at least the presence of sample code for us to inspect and learn from. then it embeds the image internally as Base64 string data. or they had to download all your source code to their computer that includes those images.MemoryStream srcImage. we see that the image assigned to btnClose is reported as being encoded to a Base64 byte array. many have resorted to using a scaled-back solution that only involves images and memory streams. Likewise. nor does it require the library code overhead of the Media. the documentation related to this process will usually leave you scratching your head.Media. This is at the very heart of how to embed images into your source code. how to convert an Image object to Base64 format. Consider this ResX file segment entry: <data name="btnClose. The initial reaction is that there does not seem to be clear path between them. as a result. but maybe you do not want them to have direct access to these proprietary files? From reading this tome. but it cannot directly convert to a byte array.resx” resource file is actually a text file (well… technically it is an XML file). all you would need to do is apply two lines of code: Dim memStream As New IO. This is the point where some programmers simply give up.Png) 'memory stream to receive image data 'copy current image object to the memory stream as PGN Conversely. it is clear that a Base64 string can easily convert to a byte array. and second.NET Beyond the Scope of Visual Basic 6. ToBase64String(bAry) 'construct a Base64 string from a Byte array To convert a byte array to a memory stream. For example. to convert a Base64 string to a byte array.NET Beyond the Scope of Visual Basic 6. and they can convert to and from Base64 strings and byte arrays. As stated above.FromStream(memStream) 'construct image from stream data memStream. and also to convert the Image back to Base64. to put this first part all together. A new function.Enhancing Visual Basic . 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. It does this by non-manipulatively parsing the string 80 characters at a time.ToBase64String(bAry) 'construct a Base64 string End Function And a function to convert a Base64 string to an Image becomes: '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : ConvertBase64ToImage ' Purpose : Convert a Base64 String to an Image object '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Function ConvertBase64ToImage(ByVal strImg As String) As Image Dim bAry() As Byte = Convert. all you need to do is this: Dim memStream As IO. though if you are using VB. all you need to do is this: Dim strImg As String = Convert.Save(memStream.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.NET previous to VB2010. However. regardless that it looks a bit weird. Imaging. the returned code will be assigned to a presumed string name strImg. wrap it in quotation marks. make it an assignment to some string variable. it is easy to format using another function. it is just one long continuous line of text. For convenience. but I think the thing they have not thought through is how to convert between byte arrays and memory streams.MemoryStream 'memory stream to receive image data srcImage.MemoryStream = New IO.ToArray() 'convert the stream to a byte array So.MemoryStream = New IO. BreakUpBase64String(). all you need to do is this: Dim bAry() As Byte = Convert.Close() 'done with the memory stram Return Convert. and it would work. and we can even use the invocation to ConvertImageToBase64() as its parameter.ImageFormat. you could paste that long line into your code. to include the “ _” continuation tag) until an 80-character block is not left. people can covert to and from images and memory streams. Page –514– . wrap each consecutive 80-character segment within quotation marks and then add a CarriageReturn/Linefeed and continuation cue (“& vbCrLf &”. you will need to instead append “& vbCrLf & _”.ToArray() 'convert the stream to a byte array memStream. No need for continutations. which will simply append the remainder as a final line.Close() 'release stream resources Return Img 'return image End Function Formatting Base64 Strings for Embedding Within Source Code When you get a string from the ConvertImageToBase64() function.0 – David Ross Goben But still.Png) 'copy current image to the memory stream Dim bAry() As Byte = memStream.MemoryStream(bAry) 'convert byte array to a memory stream And to convert a memory stream to a byte array.FromBase64String(strImg) 'grab Base64 data as a byte array Conversely. Granted. This last line will just be wrapped within quotation marks. people want to convert an Image to a Base64 string. which is actually a very easy thing to do.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image. to convert a byte array to a Base64 string. is designed to take a Base64 String and break it up into multiple lines of 80 characters apiece. all you need to do is this: Dim bAry() As Byte = memStream. Length > idY + 79 'breat up into lines of 80 characters apiece Ary(idX) = Pad & """" & srcBase64. but you can in fact add an ImageList exactly like VB actually does. as you are developing this code. format.Append(BreakUpBase64String(ConvertImageToBase64(Img))) Next End With Clipboard. TextDataFormat. Of course.Substring(idY) & """" 'add final line (no need for continuation) Return Join(Ary. which is wholly in-code. and append output 'save results to Clipboard You can then paste the clipboard text to notepad or directly into your source code where you need it. For example: Dim Img As Image = Me. you will also need to save the results.PictureBox1.Text) 'grab image 'cvt & fmt Base64 'save result to Clipboard You could also shorten it to just this: Clipboard.SetText(strBase64.StringBuilder With Me.PictureBox1. indented 4 spaces Dim idX As Int32 = 1 'init array index Dim idY As Int32 = 0 'init string offset Do While srcBase64.Image))) 'save result to Clipboard Here is an example of dumping the contents of an ImageList named ImageList1: Dim Buffer As New System.Text. Suppose in your target application you were going to save this to an ImageList control. Page –515– .SetText(BreakUpBase64String(ConvertImageToBase64(Me.ImageList1 For Each Img As Image In .Images Buffer.SetText(Buffer. split by CrLf End Function With the above function at hand and provided with a Base64 string consisting of (this is the image definition of a slick little 32-bit 16x16 closed mantilla folder with transparency): iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAA CxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6YgHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPV oygBnHM7ZAMwjdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2qoSTK4sDVpo1+pGT2wgINj lrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII= BreakUpBase64String() will massage the above string and return this: strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6Y" "gHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMw" "jdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2" "qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII=" & & & & As you can see.ToString) 'define a buffer 'with a sample loaded imagelist 'process each of its iamges 'convert. The slick thing here is that you do not really need to drop an ImageList control onto your form. 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.0 – David Ross Goben The BreakUpBase64String() function is defined below: '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : BreakUpBase64String ' Purpose : Break up a Base64 string into a formatted multiline string '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Function BreakUpBase64String(ByVal srcBase64 As String) As String Dim Pad As String = Space(4) 'init padding for text lines Dim Ary(1) As String 'init with 2 elements Ary(0) = Pad & "strImg =" 'init first element. Normally I use Interactive Debug and set a breakpoint on a line where I save the data to the clipboard or else append it to a buffer that will save the results off to the clipboard once the run is completed.NET Beyond the Scope of Visual Basic 6.Enhancing Visual Basic . vbCrLf) 'make one string.Image Dim strBase64 As String = BreakUpBase64String(ConvertImageToBase64(Img)) Clipboard.Substring(idY. the above code segment clearly assumes that you have “Dim strImg As String” declared somewhere above it (this will be clarified in example code shortly). EventArgs) Handles MyBase. If they are 16x16 pixels.ImageSize = New Size(16. but name it as you please.myImages.myImages. 16) 'define 16x16 pixel images in this list We would then want to fill it. Our new Form code could look like this: Option Explicit On Option Strict On Public Class Form1 '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Dim myImages As New ImageList 'create an imagelist in-code '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '******************************************************************************* '******************************************************************************* ' Method : Form1_Load ' Purpose : Prepare form '******************************************************************************* '******************************************************************************* Private Sub Form1_Load(sender As System.myImages.Add(Img) 'add this image as Me. With the previously listed ConvertBase64ToImage() function located within the form’s class body.Images. we could add the data we saved to the clipboard: strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhPrZLRDcMgDETZqXOwRlZgFv/loxMwAQN4Ev6Y" "gHIGEgptStKcdIpwfM9WgrpNTI/YOoQQofL6WAj0Qs17X06jSrQNU+cMGbyo9+1QHMPVoygBnHM7ZAMw" "jdN+GJAM4EUKU0qDoLpJB2jX/+LUiycA6/r8D6C1Pg9gzn0AENEHACYcuPbdApBvgF+BohjUSRtjorU2" "qoSTK4sDVpo1+pGT2wgINjlrSADXpdQLQNqcGWX4V64AAAAASUVORK5CYII=" & & & & Next. add the following line: Dim myImages As New ImageList 'create an imagelist in-code This example assumes that you want your ImageList to be named myImages.myImages.Object.Enhancing Visual Basic . you should assign the image size your ImageList will store. In your Form’s Load event.myImages. 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.0 – David Ross Goben In the heading of your form.Images(0) '-------'Image 1 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADGSURBVDhPrZLNEYUgDITtyZ5owVpy8/AqoAILoBJuVhDZ" & Page –516– .ImageSize = New Size(16. add the following two other preparatory lines: Dim strImg As String Dim Img As Image 'string to be assigned image data as Base64 text 'image to receive data from the memory stream Next. as I am demonstrating in this Black Book entry.Images. e As System.Add(Img) 'add this image to the ImageList Suppose we also added a few more converted images.NET Beyond the Scope of Visual Basic 6. we invoke the ConvertImageToBase64() function to convert our Base64 string into an Image: Img = ConvertBase64ToImage(strImg) 'grab image from string data And finally we add the new image to our ImageList: Me. you would declare it like this: Me.Load Me. before your Form Load event. Add(Img) 'add this image as Me.Enhancing Visual Basic .myImages.Close() ' release stream resources Return Img End Function End Class Were we to assign the images to anything. but one great thing that resources cannot do is add or even subtract images. Page –517– .0 – David Ross Goben "8BfJMHh4O7MimeRLYNj+Kh5Uwt8UaGft+76/Q1AwCrEYY9lZlVJdTIMzxNht7+kQtMXVVpQA13V1SAME" & "st0WBiQDgpPAJ6VGUJ0Ep8Bn6n6c4tQMKwDn+UNOSmoTvJNXACLJyQCs2kiqq3YIuZEByMUoUIVKR+U6" & "qQGQ26UgUC9cAdodYFNX/a9jo4/jYO99BkhiGmf0LA6jGE+9AWbGY5mZmfkB5Q1Ux1PgyxIAAAAASUVO" & "RK5CYII=" Img = ConvertBase64ToImage(strImg) 'grab image from string data Me. which may not yet exist (we could embed them within our program as embedded resources.Add(Img) 'add this image as Me.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 Me.Images.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. such as picture boxes or buttons. it saves all the images as an embedded Image Strip. In most respects this is true.FromBase64String(strImg) ' grab Base64 data as a byte array Dim memStream As IO. the entire collection is stored in the resource file as one continuous stream of Base64 string data.myImages.NET Beyond the Scope of Visual Basic 6.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.FromStream(memStream) ' construct image from stream data memStream. you will have to resort to storing the images outside an image-container control.MemoryStream(bAry) ' convert byte array to a memory stream Dim Img As Image = Image. ImageLists versus Resources Resources are touted as being far superior to ImageList controls when it comes to storing images. when you save the source code.myImages. maybe a bit smaller than this (these are blown up 400%). Another thing about an ImageList is that if you load it with images at design-time. which I often do. which is very easy to do with an ImageList. But when you create your interface from scratch. you would find out that the images display these four 32-bit alpha-blended 16x16-pixel graphics: Well.myImages.myImages. but that is often more trouble than it is worth).myImages.MemoryStream = New IO.Images. such as an ImageList.Images.Add(Img) 'add this image as Me. Resources.Add(My.Resources.UpOneFolder) 'up one folder image .Add(My. and create our initialization method from that (actually. we initialized an ImageList named myImages with four images. I add this code. Consider the following typical example of my code. it would be much more convenient to simply invoke an Initialization method from the form’s Load event and not have to worry any further.Resources.Resources.Add(My. you can specify a transparency after the resource image: '. 16) 'set its image size to 16x16 With myImages.Add(My.FolderSide) 'folder on side .Resources.FolderIsOpen) 'V Folder Is Open .NET Beyond the Scope of Visual Basic 6.Resources.Add(My. which details 23 images in my resources.ImageSize = New Size(16. where in my heading I define my local code-only ImageList named myImages: '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private myImages As New ImageList '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Then.FolderClosed.FolderClosed) 'add images from resources . an ImageList Initialization Source Code Generator In the above form code.drv_Remv) 'removable drive .Add(My.Documents) 'my Documents . However.Resources. shown here. during development I load my resources with a whatever images I need.Desktop) 'my Desktop .Magenta) 'specify Color.Resources.Resources.Add(My. Keeping the resourced intact and the code available during application development.Add(My.Images 'if you added images with no transparency color. If you drop this control on the form and pre-load it.Add(My.Resources. the first thing we need is an ImageList.Add(My.drv_Ntwk) 'network drive . Once development is complete.Resources.Add(My. instead of adding a lot of code by hand to implement the Base64 definitions for each image. create a local ImageList.Add(My. and then invoke my code generator to build the list of images from it for my target application I will use it in).Add(My.Downloads) 'my Downloads .Pictures) 'my Pictures . When I do not need it.Recent) 'my Rcent folder . that is just fine and dandy.drv_RAMD) 'RAM drive .0 – David Ross Goben Creating BuildImageListCode().Add(My.Add(My.ShortcutClosed) 'closed shortcut folder . I can reactivate it any time I need to make a change to the image list. I simple 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.Add(My. which I actually load to my myImages ImageList: '----------------------------------------------------------------------------'Set up the image list '----------------------------------------------------------------------------myImages.Resources.FolderSideShortcut) 'folder on side shortcut .Enhancing Visual Basic .Magenta to be transparent Try .drv_FIXD) 'fixed drive .ArrowRT) 'right triangle arrow Catch End Try End With Page –518– .Add(My. and then generate the initialization source code by invoking BuildImageListCode().Add(My.FolderCanOpen) '> Folder Can Open . For example.Resources.Resources.Add(My.Music) 'my Music .Resources.drv_CDRM) 'CD drive .Resources. Therefore.Resources.Resources.Resources. Color.Resources. but I often place my build code right within my target application.Add(My. add the images to it in the order I require.ArrowLF) 'left triangle arrow . within its Load event.Resources. load it from resources.Add(My.Add(My.Resources. We can in fact create a source code generator rather easily that you would likely run from within a sample application with either a pre-loaded ImageList control or all the images we need in the resources.Resources.Resources.Videos) 'my Videos . in my Form’s Load() event.Add(My.FolderOpened) 'open folder . this last part is what I do by creating a local code-only ImageList.ShortcutOpened) 'opened shortcut folder . I can delete this commented code and remove the images from my resources.Add(My. Right below where you had disabled the invocation of ' : the BuildImageListCode() method. where it will be added to the target ImageList. if you want the target ImageList to be named 'myImageList'.Count . For example: ' : BuildImageListCode(Me. ' : define that in your program heading. paste the Clipboard code into your form's code body.FromBase64String(strImg) 'grab Base64 data as a byte array" & vbCrLf & Pad & "Dim memStream As IO. ' : ' : Next.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.ToString & ")" & vbCrLf) Next Buffer.FromStream(memStream) 'construct image from stream data" & vbCrLf & Page –519– .Images(" & idX. myImages: BuildImageListCode(myImages) 'generate initialization source code The BuildImageListCode() method is wrapped in a module named modBuildImageListCode.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.ImageSize.Images.Add(Img) 'add this image as imgList.myImageList) ' : ' : InitializeImageList() will define each image as a Base64 text string.NET Beyond the Scope of Visual Basic 6.Append(BreakUpBase64String(ConvertImageToBase64(. preferably within your Form's Load() event. enter this command: InitializeImageList() by ' : passing it the name of your target ImageList.Text.ToString Buffer. it will convert this string into an ' : Image object and return it. just do not reference it). and then pass ' : it to this method.myImageList) ' : ' : This method will save the fully-constructed VB source code it to the Clipboard. fill it with the needed images. or whatever you chose to name it.Images Dim Ln As String = "'" & New String("+"c. For example: ' : InitializeImageList(Me.Clear() 'initialize image list" & vbCrLf & Pad & Pad & "imgList. This will add a ' : Function named InitializeImageList() and a support function named ' : ConvertBase64ToImage(). this invocation will create all desired images ' : and add them to myImageList. " & srcImageList." & vbCrLf & Ln & "Private Sub InitializeImageList(ByRef imgList As ImageList. '******************************************************************************* Friend Sub BuildImageListCode(ByRef srcImageList As ImageList) With srcImageList. plus ' : any other code that may have previously initialized the ImageList.ToString & ". Optional ByVal Replace As Boolean = True)" & vbCrLf & Pad & "If Replace Then 'if we are filling.Item(idX)))) 'break up and format Base64 string Buffer. By passing ' : that to the ConvertBase64ToImage() method. providing it with a reference to my ImageList. not appending images" & vbCrLf & Pad & Pad & "imgList. ' : ' : Next. when you run your program. convert the above BuildImageListCode() command line into a comment.Append(vbCrLf & Pad & "Img = ConvertBase64ToImage(strImg) 'grab image from string data" & vbCrLf & Pad & "imgList.MemoryStream = New IO. It ' : will process all images originally defined in the initial.Height. filling it with the images you desire ' : without requiring the user to first load those images into their resources. you can test this without deleting it.Append(Pad & "'--------" & vbCrLf & Pad & "'Image " & idX. ' : ' : Now.ToString & vbCrLf & Pad & "'--------" & vbCrLf) Buffer. It also wraps the ConvertImageToBase64() function and the BreakUpBase64String() function: 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.Width.StringBuilder Dim NewSize As String = srcImageList. either in-code or manually.ImageSize.MemoryStream(bAry) 'convert byte array to a memory stream" & vbCrLf & Pad & "Dim Img As Image = Image. set the Replace" & vbCrLf & "' : parameter to FALSE.1 'process all images in the ImageList Buffer.0 – David Ross Goben The next thing I do is invoke my BuildImageListCode() method. ' : ' : For example. 81) & vbCrLf 'build a header line Dim Pad As String = Space(4) 'init padding for text lines Dim Buffer As New System. this method will create code that can ' : load up an ImageList from scratch.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 . now-not-existing ' : imagelist (well.Images. For example: ' : Private myImageList As New ImageList ' : ' : Next.Enhancing Visual Basic . ImageFormat.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.Add(Img) 'add this image as imgList.Enhancing Visual Basic .Images.Png) 'copy current image to the memory stream Dim bAry() As Byte = memStream.Close() 'done with the memory stram Return Convert.SetText(Buffer. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub InitializeImageList(ByRef imgList As ImageList.Images. the resulting code would be: '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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.Images. split by CrLf End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : ConvertImageToBase64 ' Purpose : Convert a source Image object to a Base64 String '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Function ConvertImageToBase64(ByRef srcImage As Image) As String Dim memStream As New IO.MemoryStream 'memory stream to receive image data srcImage. Imaging.Text) End With End Sub 'release stream resources" & vbCrLf & 'return image" & vbCrLf & 'save a copy of the buffer to the clipboard '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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 Dim idY As Int32 = 0 'init string offset Do While srcBase64. 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. Optional ByVal Replace As Boolean = True) If Replace Then 'if we are filling. not appending images imgList.Images(2) '-------- Page –520– .ToBase64String(bAry) 'construct a Base64 string End Function End Module Once this method executes.ImageSize = New Size(16.ToString. with my 23 images.Save(memStream.ToArray() 'convert the stream to a byte array memStream.NET Beyond the Scope of Visual Basic 6. I will find the complete initialization source code with a support function in the Clipboard.Clear() 'initialize image list imgList. In my case. TextDataFormat. set the Replace ' : parameter to FALSE. vbCrLf) 'make one string. 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.Length > idY + 79 'breat up into lines of 80 characters apiece Ary(idX) = Pad & """" & srcBase64.Substring(idY) & """" 'add final line (no need for continuation Return Join(Ary.0 – David Ross Goben Pad & "memStream.Add(Img) 'add this image as imgList.Add(Img) 'add this image as imgList.ImageSize = New Size(16.Substring(idY.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. 16) 'define 16x16 pixel images in this list End If imgList.Images.Close() Pad & "Return Img "End Function" & vbCrLf) Clipboard. NET Beyond the Scope of Visual Basic 6.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.Add(Img) 'add this image as imgList.Images.0 – David Ross Goben '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.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(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.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.Images.Images(3) '-------'Image 4 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhP5Y+xDcMgEEXZiQ3oKanpWIGWyiMg0bnIBEzA" & "APRUVB6E6J9yjmNbltMmX3qSfdz7xuLHUkoZV7zWjuGFZVkuOS2bpmm01k6FLb33D1AClwpSSiPnTEMc" & "7kV8oNZKYM85R6wFEOf5MWKMBJZ4GefeexKUUkNKSfB8/Q3At8BzCIEkFhhjDInW2ncBBwOAAhShQGtN" & "ywDvXHCQt8EhbsAipFviPix8Lf5VhHgCWJYzPQzt+z0AAAAASUVORK5CYII=" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images.Images(10) '-------'Image 11 '-------- Page –521– .Add(Img) 'add this image as imgList.Images.Enhancing Visual Basic .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.Images(16) '-------'Image 17 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABUSURBVDhPYxgFVAYzZ878X15e/v/jx4//oUJgAOKDxBcs" & "WIIijgFAipycnFAMgWmGiYMV4gLoiknSDAPImnx9fUnTDANk2YwOQIYQDLRRQA3AwAAAiXFatNps77AA" & "AAAASUVORK5CYII=" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Add(Img) 'add this image as imgList.Images.Add(Img) 'add this image as imgList.Images.Images(11) '-------'Image 12 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACFSURBVDhPxZLNDYAgDIXZiZ3YyQUcxnQUDh65If0hNgEB" & "68GXNPAS+rUU3FBbzhRmCWA/TiNEAB6sXQgAI5ggCmCDUCJkryDFvoAIALdUvXgvfk0KgOJOPgC4i78B" & "0xnQIRmYBkBkT2tP9UAbd8XHZFR9phFgqD4AP9EiIKXU3D3ALNm5C0L52yY+Ei+PAAAAAElFTkSuQmCC" & "" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.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.Images(17) '-------'Image 18 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAA7SURBVDhPY6AKmJnG8J8UDNWGACDB/2eMicKjBgy0ASB1" & "MAzVCgGkGACjoVohYNQAiAGkYqhWSgEDAwDTxNM7a88G5gAAAABJRU5ErkJggg==" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images.Images(12) '-------'Image 13 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACvSURBVDhPxZOxDcMgEEXZyRWte5degTmYAuk6F1HmoI6u" & "Z4kU8QQX/ikk4hwpil34SU9IBh7X2J3PslzkiBrYSxcI9J9gEyD+aC9YQRfg+3db8Hp7qO0bMBMEiTFK" & "Skk3bMgKusAQgszzrCtigUhyznq4vQzXdVVBF/DeyziO6jRNNTSoNkrMwlVgJqBOHIbYxyWdoq6llN+B" & "oAFWCdYLzRYCXWAP78ARX3/EaTj3BE/HY6gtq9MqAAAAAElFTkSuQmCC" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images.Add(Img) 'add this image as imgList.Enhancing Visual Basic .Add(Img) 'add this image as imgList.Images(19) '-------'Image 20 '-------strImg = Page –522– .0 – David Ross Goben strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABfSURBVDhP7ZHLEQAREAUnJznJSU6bE2bXK4zxWScHXfUu" & "6L4gDWMfb5xYPEvXcz7Ji93AwQHLj7V/VwLVosNujryTUm+FDNYjigzmkYEM+pEFGbSRHzLIkQ0ZsDiW" & "iQJ9a0WWG+2+xgAAAABJRU5ErkJggg==" Img = ConvertBase64ToImage(strImg) 'grab image from string data 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.Images.Add(Img) 'add this image as imgList.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.Images(18) '-------'Image 19 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABqSURBVDhP7YxRCoAwDEN7J+/knbyTV9hZqumWbgw3quif" & "D0IgbSKvsK2id1RqFYS6LyH9Ax8NIBiqL1PnzdERdusG6Lma8edWzMIDIKX0fIBlOjCPDLRlL9IjAxDL" & "LcjxfCVrFuxxogkiB2Z21dLgH0jYAAAAAElFTkSuQmCC" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.NET Beyond the Scope of Visual Basic 6.Add(Img) 'add this image as imgList.Add(Img) 'add this image as imgList. such as buttons and including those controls just mentioned.Images. actually featured in the next Black Book entry. is designed to work with just a single image. convert all the code working with the imgList object.Add(Img) 'add this image as imgList. plus. its module. add a form named BrowserDialog.FromStream(memStream) 'construct image from stream data memStream.MemoryStream = New IO.Add(Img) 'add this image as imgList. It offers most functionality of the default FolderBrowserDialog control. are created in the form’s Load event.resX resources file).MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image. 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 the .Images(21) '-------'Image 22 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABKSURBVDhPpYxBCgAgDMP2/08rBQdDKnZroPSUxIN1fgwC" & "ViQD40gNjCJ3oB1hAUyGyTkJJtZ9YVJOgomYjCUDSwaWDCwZWHKDiA2SBz/Bns8W7AAAAABJRU5ErkJg" & "gg==" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList. either in-code or as a control you had dropped on the form (but if you dropped it on the form. follows: Page –523– . All images it uses in its buttons. which I have also referred to previously. If you would like to take advantage of it. when it comes up. I can insert the following line to execute the new code from within the form’s Load event: InitializeImageList(Me. These 23 icons (0-22) service my BrowserDialog form. you just add a COM referencer to Shell32. and its ComboBox are all defined in-code. modBuildImageCode.Images(22) End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : ConvertBase64ToImage ' Purpose : Convert a Base64 String to an Image object '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function ConvertBase64ToImage(ByVal strImg As String) As Image Dim bAry() As Byte = Convert.Enhancing Visual Basic .myImages) 'initialize ImageList This code will assume you have defined an ImageList control name myImages. and then drop the source code onto the form’s code page. and all controls. to include the invocation of the BuildImageListCode() method to comments.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.Images. Creating Single Base64 Image Data You can create single-image Base64 code using a modified version of the BuildmageListCode method. BuildImageCode(). its TreeView.DLL.Images. not an ImageList as the other versions were designed for. Below the last line of this disabled code.NET Beyond the Scope of Visual Basic 6. The interesting thing about this form is that to create it. This new function.Close() 'release stream resources Return Img 'return image End Function I can then past this generated code into my form class.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. it will show the SelectedPath location if you had pre-set it.0 – David Ross Goben "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADBSURBVDhPtZLBDcMgDEXZqXNkDVZgFt9y6ARMwACehFM5" & "90D5xjQkNE1atV/6QSb+TxZgfiamS+6dUsqQ/n4vBLbCXoxRq1Ea7cO0cYUMtmY9HTYlwDQ2HxigJ6Cu" & "i6ZpKl+uRa/7TRYqk4QQFMBWAZjESrgZ9eDSB8A8X0dAH96FKICIXk9QCg22ejFz7fs3YH0OvVvfIWDP" & "PUAOEXeJTTGoJ+2cy977bApOniwKjHTW6EdOXiMgmORTQwL4XsY8ADUZdcZU+bcCAAAAAElFTkSuQmCC" & "" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList. vbCrLf) 'make one string.Text.MemoryStream = New IO.ToString Buffer.Close() 'release stream resources" & vbCrLf & Pad & "Return Img 'return image" & vbCrLf & "End Function" & vbCrLf) Clipboard. enter this command: ' : me. By passing ' : that to the ConvertBase64ToImage() method. ' : ' : Next.ToString.Save(memStream.MemoryStream(bAry) 'convert byte array to a memory stream" & vbCrLf & Pad & "Dim Img As Image = Image. ' : ' : Use BuildImageCode list this.Substring(idY) & """" 'add final line (no need for continuation Return Join(Ary.ToArray() 'convert the stream to a byte array memStream.Append(vbCrLf & Ln & "' Method : InitializeImage" & vbCrLf & Ln & "Private Function InitializeImage() As Image" & vbCrLf) '----------------------------------------------------------'build data '----------------------------------------------------------Buffer. convert the above BuildImageCode() command line into a comment.StringBuilder Dim NewSize As String = srcImage.Append(Pad & "Dim bAry() As Byte = Convert.Png) 'copy current image to the memory stream Dim bAry() As Byte = memStream.Width.Enhancing Visual Basic .FromStream(memStream) 'construct image from stream data" & vbCrLf & Pad & "memStream. 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.Size.MyImage) ' : ' : This method will save the fully-constructed VB source code it to the Clipboard. asuming the image is loaded in your Resources: ' : BuildImageCode(My.Height. '******************************************************************************* Friend Sub BuildImageCode(ByRef srcImage As Image) Dim Ln As String = "'" & New String("+"c.Resources. paste the Clipboard code into your form's code body.FromBase64String(strImg) 'grab Base64 data as a byte array" & vbCrLf & Pad & "Dim memStream As IO. This will add a ' : Function named InitializeImage() and a support function named ' : ConvertBase64ToImage().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.Append(BreakUpBase64String(ConvertImageToBase64(srcImage)) & vbCrLf) 'break up and format Base64 string '--------------------------------------------------------------'Finish out source code using common code '--------------------------------------------------------------Buffer. 81) & vbCrLf 'build a header line Dim Pad As String = Space(4) 'init padding for text lines Dim Buffer As New System.MyLocalImage As Image = InitializeImage() ' : ' : InitializeImage() will define the image as a Base64 text string.Substring(idY. Imaging. ' : ' : Next.SetText(Buffer. split by CrLf End Function End Module Page –524– .Length > idY + 79 'breat up into lines of 80 characters apiece Ary(idX) = Pad & """" & srcBase64.MemoryStream 'memory stream to receive image data srcImage.Close() 'done with the memory stram Return Convert. it will convert this string into an ' : Image object and return it. Right below where you had disabled the invocation of ' : the BuildImageListCode() method.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.Size. " & srcImage.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modBuildImageCode Static Class Module ' Build a Single Image Source Code Constructor '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modBuildImageCode '******************************************************************************* ' Method : BuildImageCode ' Purpose : Provided an existing Image. 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. TextDataFormat.ToString & ".ImageFormat. 81) & vbCrLf 'build a header line Dim Pad As String = Space(4) 'init padding for text lines Dim Buffer As New System.MemoryStream = New IO. such as by using the icon’s ToBitmap method.NET Beyond the Scope of Visual Basic 6. they cannot be stored in an ImageList without being converted to Images. ' : ' : Next.Text.ToString & ". or however you have your image stored.0 – David Ross Goben To use it is easy. or paste right into your code page.MyIcon) ' : ' : This method will save the fully-constructed VB source code it to the Clipboard. paste the Clipboard code into your form's code body. " & srcIcon. ' : ' : Next. 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. consider this definition for a single image. For example.png"))”. the code it built is saved to the clipboard. convert the above BuildIconCode() command line into a comment. this “BuildImageCode(Me.Size. '******************************************************************************* Friend Sub BuildIconCode(ByRef srcIcon As Icon) Dim Ln As String = "'" & New String("+"c. sometimes it is useful to embed an icon in-code.FromFile("C:\MyImage. modBuildIconCode.MyLocalImage As Image = InitializeImage() ' : ' : InitializeImage() will define the image as a Base64 text string. this: “BuildImageCode(Me. it will convert this string into an ' : Image object and return it.ListBox1.Images(0))”.Resources.Width. With the image you want to convert to a Base64 string available. Just paste it to Notepad if you want to examine it.Image)”.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO. this: “BuildImageCode(MyImage)” .Close() 'release stream resources Return Img 'return image End Function Creating Single Base64 Icon Data Icons are stored differently than Images. By passing ' : that to the ConvertBase64ToImage() method.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image.FromStream(memStream) 'construct image from stream data memStream.Resources. ' : ' : Use BuildIconCode list this. Once this method has executed. This will add a ' : Function named InitializeIcon() and a support function named ' : ConvertBase64ToIcon().StringBuilder Dim NewSize As String = srcIcon. asuming the image is loaded in your Resources: ' : BuildIconCode(My.Size. follows: Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modBuildIconCode Static Class Module ' Build a Single Icon Source Code Constructor '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modBuildIconCode '******************************************************************************* ' Method : BuildIconCode ' Purpose : Provided an existing Icon.Height. its module. If you would like to take advantage of it. 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.Append(vbCrLf & Ln & "' Method : InitializeIcon" & vbCrLf & Ln & "Private Function InitializeIcon() As Icon" & vbCrLf) '----------------------------------------------------------'build data '----------------------------------------------------------Buffer. However. You can supply the BuildIconCode() method with an icon resource and it will create single image Base64 code using a mildly modified version of the above BuildmageCode() method.Enhancing Visual Basic .MyImage)”. As such.ToString Buffer. enter this command: ' : me. pass it to the BuildImageCode() method like this: “BuildImageCode(My. this: “BuildImageCode(Image. Right below where you had disabled the invocation of ' : the BuildImageListCode() method.PictureBox1.Append(BreakUpBase64String(ConvertIconToBase64(srcIcon)) & vbCrLf) 'break up and format Base64 string '--------------------------------------------------------------- Page –525– . Close() 'done with the memory stram Return Convert.Close() 'release stream resources Return Icn 'return icon End Function Page –526– .MemoryStream 'memory stream to receive image data srcIcon.MemoryStream(bAry) 'convert byte array to a memory stream Dim Icn As New Icon(memStream) 'construct icon from stream data memStream.ToArray() 'convert the stream to a byte array memStream.Append(Pad & "Dim bAry() As Byte = Convert.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.Length > idY + 79 'breat up into lines of 80 characters apiece Ary(idX) = Pad & """" & srcBase64.Save(memStream) 'copy current icon to the memory stream Dim bAry() As Byte = memStream.Enhancing Visual Basic .FromBase64String(strImg) 'grab Base64 data as a byte array" & vbCrLf & Pad & "Dim memStream As IO.Substring(idY.NET Beyond the Scope of Visual Basic 6.MemoryStream = New IO.Close() 'release stream resources" & vbCrLf & Pad & "Return Icn 'return icon" & vbCrLf & "End Function" & vbCrLf) Clipboard.ToString.SetText(Buffer. TextDataFormat. 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. which you can assign to a variable using something like “Dim iconRed 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.0 – David Ross Goben 'Finish out source code using common code '--------------------------------------------------------------Buffer.MemoryStream = New IO. split by CrLf End Function End Module Consider the following clipboard output for my 256-color 32x32 Red Marble icon.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.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. vbCrLf) 'make one string.Substring(idY) & """" 'add final line (no need for continuation Return Join(Ary.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. etc. as demonstrated in the previous Black Book entry. replacing any pre-existing code found there. Although the Garbage Collector.RootFolder = Environment. click it to select it and place a check in its checkbox (if post-VB2010). just add a new windows form to your project and name it BrowserDialog. To use the code.Dispose() End With Page –527– 'set a description 'allow user to create a new folder 'custom start path 'allow setting general start path 'launch dialog and check result. For example.SpecialFolder.dll. you need to click that entry to bring up its properties in the Properties Window. and finally click the OK button to add it. But we also need to ensure that your application does not embed interop types for this particular class. but in this case. and. Next. It supports all properties featured in the FolderBrowserDialog control. Normally this is OK and is considered a best practice.SelectedPath) Else MsgBox("You hit Cancel") End If . Recent.OK Then MsgBox("You selected:" & vbCrLf & . you would implement it just like you would a FolderBrowserDialog control. There. attaches events. doubleclick that form to quickly bring up its code page. select References. To use it. Once created. which runs constantly in the background as of VB2010. To do that. would clean this up for us if we forgot to tidy up.ShowDialog(Me) = DialogResult.NET Beyond the Scope of Visual Basic 6. go to your Project Properties. it gives you access to both forward and backward browsing history. It also stores not text. it adds virtual folders to places like Desktop. sets them up and positions them.0 – David Ross Goben Black Book Tip # 50 Replace the BrowseFolderDialog Control with a Custom BrowserDialog Form I have already made references to my custom BrowserDialog form to replace the default FolderBrowserDialog control. OK? 'if they hit OK 'if user cancelled 'dispose of FolderBrowserDialog resources . Finally past the code at the end of this Black Book Entry to the code page.Dll. scroll to the “Microsoft Shell Control and Automation”. you need to set its Embed Interop Types property to False from its default True state. but a custom class in the ComboBox Items collection. Pictures.DLL in your application and ensure that its Embed Interop Types parameter is set to False. select the “Add…” button and then the COM tab on the Add Reference dialog.shell32. This custom folder browser does a lot of interesting things. I like to dispose of all resources I create whenever I can. these controls need to use the GUIDs (Global Unique Identifiers) as delivered by the Shell32 provider. you might normally bring up the default FolderBrowserDialog in-code like this: With New FolderBrowserDialog . To do this. though I did add a Title property so you can also change the form title if you want to.Description = "Browser Around for a new folder" . The IDE will create a non-COM version of the DLL named interop. it performs owner drawing in a Drives List ComboBox and a directory folder TreeView control. after you have added the COM reference to Shell32. It also creates all its controls from scratch. as it is now in your references. it generates all its images.SelectedPath = "C:\Windows" '.CommonDesktopDirectory If . you will first need to make a COM reference to Shell32. hit the M key to skip down to the M’s in the list.. It enumerates active logical drives.Enhancing Visual Basic . to include ensuring to dispose of the object’s resources (except if you are one to simply drop a FolderBrowserDialog onto your forms instead of creating one in-code).ShowNewFolderButton = True . It will also recreate it during any build if you ever delete it. not automatic GUIDs generated for embedded objects. Cursor = value 'no.SelectedPath) Else MsgBox("You hit Cancel") End If .. SetCursorToChildren(Me. ' : Note also that you can assign from the Cursors collection. you can make just a change in the control you reference.RootFolder=Environment. and you have the additional option to change the form’s Title from “Browse For Folder” if you want to: With New BrowserDialog . but you can load them all together in the form’s code space.Cursor 'return the form's current cursor setting End Get '-----Set(value As Cursor) If Me. Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' BrowserDialog Form Class (NOTE: You may need to add 'Imports System' if VB2010 or previous) '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Public Class BrowserDialog '--------------------------------------------------------------------------------'CLASS METHOD MAP: '$$ = fields. 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Private Property MeCursor As Cursor Get Return Me. and after that is appended another class named DriveItem that supports the logical and virtual Drive entries in the cboDrives ComboBox items list. or load a cursor from resources. You can afterward reasign child control cursors.. OK? 'if they hit OK 'if user cancelled 'dispose of BrowserDialog resources Following is the BrowserDialog form code.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. Note that a module is appended to the end of the form class named modBrowserDialogSupport. ByVal lpMessage As String.Description = "Browser Around for a new folder" ..Dispose() End With '(NEW) optionally set a title 'set a description 'allow user to create a new folder 'custom start path 'allow setting general start path 'launch dialog and check result.. ' : Unlike the UseWaitCursor property. If Me.SpecialFolder.Cursor <> value Then 'is the form cursor already set to this? Me. You do not need to break these up into separate files.Enhancing Visual Basic .SelectedPath = "C:\Windows" '.Title = "We can set the Form Title" .HasChildren Then 'and if it also has children. assign it using: MeCursor = NewCursor.OK Then MsgBox("You selected:" & vbCrLf & .0 – David Ross Goben To use the new BrowserDialog form.Controls. '** = methods.CommonDesktopDirectory If .ShowNewFolderButton = True . 'PP = Properties.NET Beyond the Scope of Visual Basic 6. '++ = support.ShowDialog(Me) = DialogResult. ByVal wStyles As MsgBoxStyle) As MsgBoxResult '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : MeCursor ' Purpose : Set cursor to more than just the main form ' Usage : When assigning a new cursor to the form. depending on if a root folder was specified Private _Root As String 'root of start path Private _RootFolder As Environment. 'MM = menu interface '--------------------------------------------------------------------------------'$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private _SelectedPath As String 'path set by user to start or result Private _StartPath As String 'path to actually focus on. so set cursor to the main form. value) 'then set its child controls as well End If End If End Set End Property Page –528– . ByVal lpTitle As String. btnMakeNewFolder.MyDocuments Else Return Me.IsNullOrWhiteSpace(value) Then If IO._Title = value Me.AutoSize Then Me.Directory.ControlCollection. Value) 'process its child controls End If End If Next End Sub 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : Title ' Purpose : Get/Set Title for form 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property Title As String Get Return Me.Enhancing Visual Basic .Text = value End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : ShowNewFolderButton ' Purpose : Get/Set ShowNewFolderButton 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property ShowNewFolderButton As Boolean Get Return btnMakeNewFolderVisible End Get '---Set(value As Boolean) btnMakeNewFolderVisible = value Me.Exists(value) Then Me._Title End Get '---Set(value As String) Me.lblDescription.SpecialFolder Get If Me. ByRef Value As Cursor) For Each Cntrl As Control In ControlList 'process all child controls If Cntrl.NET Beyond the Scope of Visual Basic 6.Visible Then 'is the control visible? Cntrl._RootFolder = Nothing Then Return Environment.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : SetCursorToChildren ' Purpose : Support MeCursor property ' : Set parent's cursor also to Child controls '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Sub SetCursorToChildren(ByRef ControlList As Control.lblDescription.SpecialFolder._SelectedPath End Get '---Set(value As String) If Not String._RootFolder End If End Get '---Set(value As Environment._SelectedPath = value Return End If End If Throw New Exception("Invalid SelectedPath value") Return End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : RootFolder ' Purpose : Get/Set RootFolder 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property RootFolder As Environment.SpecialFolder) Page –529– .HasChildren Then 'does it have child controls? SetCursorToChildren(Cntrl.lblDescription.Text End Get '---Set(value As String) If Me.AutoSize = False End If Me.Visible = value End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : SelectedPath ' Purpose : Get/Set SelectedPath 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property SelectedPath As String Get Return Me.Controls.lblDescription.Text = value End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : Description ' Purpose : Get/Set Description for header label box 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Friend Property Description As String Get Return Me.Cursor = Value 'set its cursor If Cntrl. Width . Tp = .Height Then Ht = .btnCancel 'treat this as the form CANCEL button End With '--------------------------------------------------------------------------'Set up OK button '--------------------------------------------------------------------------With btnOK .. ..MinimumSize = sz Me.Text = Me.ToString)) Dim Ht As Int32 = CInt(GetCustomSetting("FolderBrowserHeight".Width 'if width out of PrimaryScreen bounds.Height = Ht End With ''--------------------------------------------------------------------------''Set up the image list ''--------------------------------------------------------------------------InitializeImageList(Me.EdgeOfst ._StartPath = GetCustomSetting("FolderBrowserPath".btnOK.Left 'if left out of PrimaryScreen bounds.ToString)) Dim SystemDir As String = Environment.Bottom Or AnchorStyles.ToString)) Dim Wd As Int32 = CInt(GetCustomSetting("FolderBrowserWidth".Size = sz 'set size of button .Left = Me..Height Then 'if top + height is out of PrimaryScreen bounds.Left = Me.0 – David Ross Goben Try Me.Right 'anchor to bottom right Me.Top + . '----------------------------------------------------------------------If Tp < .SetToolTip(Me.SetToolTip(Me.Enhancing Visual Basic .Top. set to its top End If If Lf < .Width = Wd Me.PrimaryScreen.Width .6 'set to left of OK with a standard gap .CenterParent 'center on parent '----------------------------------------------------------------------Me.Size = sz 'initially match size with minimum size Me.StartPosition = FormStartPosition.DialogResult = DialogResult.Left 'just set left to its left.ClientSize. Me.Load Dim sz As New Size(380.ToolTips.ToString With Screen. "Reject any selections") 'set tool tip .Left .DialogResult = DialogResult.ClientSize. set to its left End If If Wd > . because width is in bounds End If If Tp + Ht > ._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.myImages) 'initialize ImageList '--------------------------------------------------------------------------'Set up Cancel button '--------------------------------------------------------------------------With btnCancel .Left End If If Ht > .OK 'auto-exit with OK Me. Me.Top 'just set top to its top._Title 'set title Me. width..NET Beyond the Scope of Visual Basic 6. "Accept selection") 'set tool tip .Left. 390) 'default startup size Me.Width += 40 'width + standard 40 pixels for spacing sz.btnCancel.btnCancel.Width..EdgeOfst 'set to right bottom .Parent = Me 'establish this control's parent . Me.Height 'if height out of PrimaryScreen bounds.Anchor = AnchorStyles.Top End If If Lf + Wd > .Bottom Or AnchorStyles.ToString)) Dim Lf As Int32 = CInt(GetCustomSetting("FolderBrowserLeft".CancelButton = Me.Height += 9 'height + 9 pixels for spacing .Top = Tp Me.Left Then Lf = .Top = Me.GetFolderPath(Environment.Height .btnCancel.WorkingArea 'compare any save parameters against the current screen bounds definition 'get saved/default windows positioning for top.MyDocuments)).Right 'anchor there Me.SpecialFolder.AcceptButton = btnOK 'treat this as the form ACCEPT button End With Page –530– .ToolTips. because height is in bounds End If '----------------------------------------------------------------------'apply valid location and sizing settings to the main form '----------------------------------------------------------------------Me. left. 'and adjust them if they are not.Width .Parent = Me 'establish this control's parent .Anchor = AnchorStyles.Font) 'get size of text sz. set to its width Lf = .Top = Me.Width Then Wd = .Left + .SystemDirectory '----------------------------------------------------------------------'make sure that these setting are within the work area of the current screen. and height Dim Tp As Int32 = CInt(GetCustomSetting("FolderBrowserTop". set to its height Tp = ..Text = "OK" .Height.MeasureText(. Me. Environment.Top 'if top out of PrimaryScreen bounds.Size = sz 'set size of button ..Left = Lf Me. e As EventArgs) Handles MyBase.. Lf = .Top 'match top to cancel button .Height .Top Then 'keep frame within client rectangle Tp = .Width Then 'if left + width goes out of PrimaryScreen bounds.Cancel 'auto-exit with Cancel Me.Text = "Cancel" sz = TextRenderer..Text. Left 'anchor to bottom left Me.Enhancing Visual Basic .Anchor = AnchorStyles.Width .Image 'image only .ImageIndex = DriveImages.ToolTips.ImageList = myImages 'make sure ImageList1 is attached .DrawMode = TreeViewDrawMode.None 'do not dock it ." AddHandler .Left = EdgeOfst 'align combobox left location .Width = Me.tvFolderTree . AddressOf tvFolderTree_BeforeExpand AddHandler .Parent = Me 'define control's parent .Bottom Or AnchorStyles.GripStyle = ToolStripGripStyle.ItemHeight = 17 'allow for 16x16 pixel icons with a 1-pixel gap .DrawItem.0 – David Ross Goben '--------------------------------------------------------------------------'Set up Make New Folder button '--------------------------------------------------------------------------With btnMakeNewFolder .DrawMode = DrawMode.Width = sz.Parent = Me 'establish this control's parent .Anchor = AnchorStyles.FolderSide 'default to folder on side ." & vbCrLf & "When enabled you can browse sequentially with the" & vbCrLf & "arrow button.Items 'now build buttons '------------------------------'Add Navigate to Previous button '------------------------------tsbNavPrev = New ToolStripSplitButton("tsbNavPrev".Top = EdgeOfst .Visible = btnMakeNewFolderVisible 'set its visibility AddHandler . Or select the dropdown to select the" & vbCrLf & "history point you want to return to.Anchor = AnchorStyles. "Add a new folder to the currently selected folder") .Text.OwnerDrawFixed 'enable drawing images into list .btnCancel. Nothing.cboDrives .Width 'position navigation toolbar above TV at right end .BeforeExpand.Top = Me.MeasureText(.DrawMode = TreeViewDrawMode.DrawNode.lblDescription. .ShowLines = True .Normal 'we do not want to draw overything End If AddHandler .Right Or AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.SetToolTip(Me.ToolTips.Left + Me.SelectedIndexChanged.Transparent 'remove any shading .LayoutStyle = ToolStripLayoutStyle.cboDrives.Top = Me.Bottom .Height = SizeMessage(Me.Top 'match top to cancel button .myImages.OwnerDrawAll 'we want to draw overything Else .ArrowLeft).Images.ButtonClick.ShowNodeToolTips = True 'Allow node Tooltips to display If Me.Font) . "tsbNavPrev") With tsbNavPrev .SetToolTip(Me.Text = "&Make New Folder" sz = TextRenderer.Left Or AnchorStyles. .Width).lblDescription.Width .Click.Width + 40 'width + standard 40 pixels for spacing .tvFolderTree.Left = Me.HorizontalStackWithOverflow 'set display style .ShowLines = False 'no need .ShowRootLines = True 'use so tabbing aligns .Anchor = AnchorStyles.Parent = Me 'define control's parent .CanOverflow = True .Hidden 'hide grip .DisplayStyle = ToolStripItemDisplayStyle.Height + EdgeOfst 'size based off text (even if empty) .EdgeOfst * 2 .Text.Dock = DockStyle.Parent = Me 'establish this control's parent .DropDownClosed.Font.Height ..Top End With '--------------------------------------------------------------------------'Set up Folder TreeView '--------------------------------------------------------------------------With Me.Parent = Me 'define control's parent .Left = EdgeOfst 'init position .DropDownOpening.Transparent 'remove any shading . AddressOf tvFolderTree_AfterSelect 'add event code triggers AddHandler .Left = EdgeOfst 'align treeview horizontal position .Right Or AnchorStyles. AddressOf cboDrives_DrawItem 'add event code triggers AddHandler .Height + 9 'height + 9 pixels for spacing .BackColor = Color.ShowPlusMinus = True 'use so user can click nodes to auto-expand and collapse .Top Or AnchorStyles.Count <> 0 Then .DropDownStyle = ComboBoxStyle.Left = EdgeOfst 'align combobox left location . AddressOf cboDrives_SelectedIndexChanged AddHandler .Width 'match description width . Me.ShowItemToolTips = True 'allow noodes to show paths .tsNavigation .ClientRectangle.Top Or AnchorStyles. AddressOf cboDrives_DropDownClosed End With '--------------------------------------------------------------------------'Set up main description '--------------------------------------------------------------------------With Me.AfterSelect.Top 'set anchoring .DropDownList 'avoid user typing into CBO's text box Me.NET Beyond the Scope of Visual Basic 6.Width = Me.lblDescription .Left Or AnchorStyles.Height = sz.Images(DriveImages. "Select a different virtual or logical drive") '----------------------------------------------------------------------AddHandler .AutoSize = False 'allow label to flex (should be already set by Description property) .btnMakeNewFolder.BackColor = Color. .Top + Me. AddressOf tvFolderTree_DrawNode End With '--------------------------------------------------------------------------'Set up Navigation panel '--------------------------------------------------------------------------With Me.tvFolderTree.ToolTipText = "Browse backward through the current browsing history.ImageList = Nothing .Anchor = AnchorStyles.Right 'anchor to top right With .myImages. AddressOf tsbNavPrev_ButtonClick 'add event code triggers AddHandler . AddressOf tsbNavPrev_DropDownOpening Page –531– .lblDescription. AddressOf btnMakeNewFolder_Click 'set up its CLICK event End With '--------------------------------------------------------------------------'Set up ComboBox to list Drives '--------------------------------------------------------------------------With Me.lblDescription. Drive.. Me.Top + .UserClosing.Left.Top + (._SelectedPath = e._Root = RemoveSlash(IO.DropDownItemClicked. AddressOf tsbNavUp_Click. AddressOf tsbNavNext_DropDownOpening AddHandler .Top = . less trailing backslash '--------------------------------------------------------------------------'populate drive combobox and then select current drive '--------------------------------------------------------------------------With Me.Left 'adjust combobox width to fill row with toolbar Me.ToString) SaveCustomSetting("FolderBrowserHeight".DisplayStyle = ToolStripItemDisplayStyle. or select the dropdown to select the" & vbCrLf & "history point you want to advance to._StartPath = Environment.DisplayStyle = ToolStripItemDisplayStyle.Add(tsbNavPrev) '------------------------------'Add Navigate UP button '------------------------------tsbNavUp = New ToolStripButton("tsbNavUp".Enhancing Visual Basic ..Node.GetDirectoryRoot(Me. Me._SelectedPath.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.cboDrives.Directory. figure out the target path.ArrowRight).myImages.Height .UpOneFolder).MyDocuments))._SelectedPath) 'save the last selected path End Select End Sub '******************************************************************************* ' Method : Reset ' Purpose : Reset Data to defaults '******************************************************************************* Friend Sub Reset() Me.Image 'image only ..cboDrives.Height) \ 2 'adjust combobox vertical size Me.Trim 'set it as the start path (SP initialize to default) End If If Me.NET Beyond the Scope of Visual Basic 6.None SaveCustomSetting("FolderBrowserTop". "tsbNavNext") With tsbNavNext .Items 'then find the desired Root drive in the list If di.ToString) SaveCustomSetting("FolderBrowserWidth"._RootFolder = Nothing Me. Nothing.AddtoList(Me.Add(tsbNavNext) End With Me.Top . and the root '--------------------------------------------------------------------------If Not String.0 – David Ross Goben AddHandler . CloseReason.tvFolderTree.ToolTipText = "Browse up one folder to the parent folder of where you are now" End With ._StartPath = GetCustomSetting("FolderBrowserPath". .ToolTipText = "Browse forward through the current browsing history. Me..Images(DriveImages.FormClosing Select Case e. Me._SelectedPath) Then 'if a selected path was supplied by user. e As FormClosingEventArgs) Handles Me.Visible = False Me.myImages.Images(DriveImages.ToString) 'save form screen positions SaveCustomSetting("FolderBrowserLeft". AddressOf tsbNavPrev_DropDownItemClicked End With .Enabled = e.btnMakeNewFolder.Top .DropDownItemClicked._StartPath = Me.tvFolderTree.Equals(Me.cboDrives.cboDrives PopulatecboDrives() 'populate the cboDrives list._RootFolder) 'then it takes presidence over any selected path End If Me.Left .ToString End Sub '******************************************************************************* ' Method : tvFolderTree_AfterSelect ' Purpose : React to a node is selected '******************************************************************************* Private Sub tvFolderTree_AfterSelect(sender As Object.None 'DialogResult buttons issue CloseReason. AddressOf tsbNavNext_DropDownItemClicked End With ._Root) Then 'if we found it.Add(tsbNavUp) '------------------------------'Add Navigate to next button '------------------------------tsbNavNext = New ToolStripSplitButton("tsbNavNext".ToolTipText 'set the selected path Me._SelectedPath) 'try to add the path to the history lkist Me. Me. Environment.Height 'adjust treeview vertical position Me.CloseReason 'check for closing reason Case CloseReason. Me.GetFolderPath(Me. "tsbNavUp") With tsbNavUp . For Each di As DriveItem In . e As TreeViewEventArgs) Me.Node. Me.Height.Description = "Folder Browser" Me. Me.Me._SelectedPath = Nothing Me..btnOK..Me.Image 'image only ." AddHandler .SpecialFolder.ToString) SaveCustomSetting("FolderBrowserPath".Top.tsbNavUp. AddressOf tsbNavNext_ButtonClick 'add event code triggers AddHandler ._RootFolder <> Nothing Then 'if a root folder specified..GetFolderPath(Environment.ButtonClick.Me.IsNullOrWhiteSpace(Me..8 End With '--------------------------------------------------------------------------'finally.tvFolderTree. Me.Width.DropDownOpening._StartPath)) 'get the Root from the path.Width = .Parent IsNot Nothing 'enable UpOnefolder button as needed End Sub Page –532– .Top = ." & vbCrLf & "When enabled you can browse sequentially with the" & vbCrLf & "arrow button.Height = Me.cboDrives. .Node...Node...X = pt...Font. get copy of line bounds.Color = tv. tv.Graphics. pt) End If pt...... 'compute Top-left coordinate for drawing 'point to text drawing area. 'we will draw the text normally 'if the folder is expanded.Font) Dim Brush As SolidBrush = New SolidBrush(tv.. '------------------------------------------------------------------MeCursor = Cursors...Highlight e... but End With End Sub '********************************************************************************* ' Method : tvFolderTree_DrawNode ' Purpose : Draw node with indent and state '********************************************************************************* Private Sub tvFolderTree_DrawNode(sender As Object.lnk files Catch End Try Dim lnkFolders As New List(Of String) 'init lnk folder list If Files IsNot Nothing AndAlso Files...CompareTo(FndIf0) = 0 AndAlso Path.....ImageIndex)... TreeView) Dim nCount As Int32 = 0 Dim nd As TreeNode = e.. pt) ElseIf e..Dispose() End Sub 'get reference to TreeView container 'init indent counter to 0 'start with current node provided to us 'while the node as a parent 'count a generation (indent index) 'point back to its parent node.'first check for link files in the current folder that reference folders '.myImages.Images(DriveImages.Text. If (Attr And (FileAttribute.Checked = False 'mark this folder as processed.BackColor Else e..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.Images(DriveImages...Images(e. pt) e...Parent Loop Dim Rect As Rectangle = e.Node.Graphics.Size = TextRenderer.Length.....Bounds) Brush..Dim Files() As String = Nothing 'init local link file storage Try Files = IO. Rect.. 'brush used for selection and text coloring 'if the node is selected.GetFiles(parentNode..Node) 'parse any of its subfolders MeCursor = Cursors...CompareTo(lnkPath) = 0 Then 'did we find it (matches length and name)? FndIf0 = 0 'yes.Parent IsNot Nothing nCount += 1 nd = nd.Nodes. 'and try again 'otherwise.Directory) = FileAttribute.Count <> 0 Then e.Add(lnkPath) 'then add it to our local list if a folder End If End If End If End If Catch End Try Next Page –533– ..Directory.Node Do While nd.. but does it have sub-folders? 'yes.Graphics.. Dim FndIf0 As Int32 = lnkPath.Volume)) = 0 AndAlso IO...DrawString(e.Y) Rect..Enhancing Visual Basic . 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.Hidden Or FileAttribute.Indent.Clear() 'Clear all child nodes in case we are repopulating '... e As TreeViewCancelEventArgs) With e. Rect) Brush...FolderIsOpen).Default 'no longer busy End If 'this will re-invoke this method...DrawImage(Me.Text...Color = SystemColors..FolderCanOpen). tv....Checked Then 'has it been processed yet? .FillRectangle(Brush.IsExpanded Then e.. Brush...WaitCursor 'show that we are busy DirRecurse(e. 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.Location) Brush.. so check them for folders Try 'trap in case it no longer exists.... 'draw "V" in front of on-side folder 'not expanded.Node..DrawImage(Me.Color = tv.. so draw ">" in front of on-side folder 'point to on-side folder position 'draw the on-side folder or on-side shortcut 'draw text 'release brush resources '******************************************************************************* ' Method : DirRecurse ' Purpose : Fill provided TreeView with folders and files as needed '******************************************************************************* Private Sub DirRecurse(ByRef parentNode As TreeNode.. "*. Rect. e As DrawTreeNodeEventArgs) Dim tv As TreeView = DirectCast(sender.X + 16 + 16 + 4 Rect... lnkFolders. Optional ByVal skipDeepSeek As Boolean = False) parentNode..myImages.Directory Then 'if it is a folder.lnk") 'get a list of all .DrawImage(Me.Exists(lnkPath) Then 'and if special attributes are not assigned..Nodes.Node If .. 'compute size of text area to minimize flicker. e.FillRectangle(Brush..X + nCount * tv.System Or FileAttribute.ToolTipText...myImages.0 – David Ross Goben '******************************************************************************* ' Method : tvFolderTree_BeforeExpand ' Purpose : React to a node expanding '******************************************************************************* Private Sub tvFolderTree_BeforeExpand(sender As Object...Bounds Dim pt As New Point(Rect..Directory.MeasureText(e.Count <> 0 Then 'do we have lnk files? For Each lnkFile As String In Files 'yes.ForeColor End If If e.......Graphics.Graphics..IsSelected Then Brush.BackColor) If e...X += 16 e....NET Beyond the Scope of Visual Basic 6....Graphics.Node. 'change background to highlight color '(we could use Rect here to minimize highlight) 'change the brush's color to white 'clear JUST text area background.Node. .Length 'then start index beyond it End If '--------------------------------------------------------------------------Do While Idx <> SeekPath......ToolTipText Then 'if we already have a match.......ToolTipText......Nodes.....ToolTipText)) 'get a list of any subfolders... Idx) 'grab the path left of it RootNode = FindNodePath(RootNode.GetFileName(dirPath)..ToolTipText = dirPath 'save its folder path as its tooltip dirNode..ToString............Count <> 0 Then 'else if it has children...LastIndexOf("\"c) Then 'if we did not find the node or we are at the target.Dim dirs() As String = Nothing 'init local directory storage Try dirs = IO.Volume)) = 0 Then 'if not special folder. parentNode.Directory.Enhancing Visual Basic ..Nodes 'check each child Dim nd As TreeNode = FindNodePath(subNode...FolderSide) 'add new dir node w/closed folder dirNode.... SeekPath) 'check its node and subnodes.LastIndexOf("\"c) 'while there is data to find Idx = SeekPath..Path.IsExpanded Then 'othewise.GetFileName(dirPath). Idx = RootNode..ToolTipText = SeekPath Then 'if current node contains the sought path.. BasePath) 'find the path to it via an overload If RootNode Is Nothing OrElse Idx = SeekPath..........GetDirectories(AddSlash(parentNode.....IndexOf("\"c... If skipDeepSeek Then 'if Referencing only......Tag = False 'tag as not a shortcut DirRecurse(dirNode..Nodes.......System Or FileAttribute.. Dim dirNode As TreeNode = parentNode.FolderSideShortcut.....Substring(0.Count <> 0 Then 'if sub-directories or lnk folders exist. IO. Return Node 'return a reference to that node End If If Node. RootNode.. ByVal SeekPath As String) As TreeNode If Node..Count <> 0 Then 'if link folder exist.ToolTipText)) Then 'if the seek path contain the root node...Checked = True 'mark this folder as being unprocessed parentNode. skip if protected Catch End Try Dim Bol As Boolean = dirs IsNot Nothing AndAlso dirs.. True) 'parse any of its subfolders Next End If End If End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : FindNodePath ' Purpose : Find the node path in a treeview '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function FindNodePath(ByVal SeekPath As String) As TreeNode Dim RootNode As TreeNode = Me... For Each subNode As TreeNode In Node. 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. DriveImages..Nodes..Expand() 'then make sure it is expanded (and populated) End If Loop Return RootNode 'finally. if tne node is not expanded.... DriveImages......Add("*") 'add a faux child node to add "+" branch connector lnkFolders..ToolTipText = dirPath 'save its folder path as its tooltip dirNode.ToString.Path.....'check sub-folders '.... DriveImages.Nodes. For Each dirPath As String In lnkFolders 'parse each lnk path Dim dirNode As TreeNode = parentNode.... Exit Do 'then done ElseIf Not RootNode........ return the target node End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : FindNodePath (overload) ' Purpose : Find the node path in a treeview '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function FindNodePath(ByRef Node As TreeNode.......Count <> 0 'True if directories exist If Bol OrElse lnkFolders.NET Beyond the Scope of Visual Basic 6.Contains(AddSlash(RootNode.....Tag = True 'tag as a shortcut DirRecurse(dirNode.FolderSideShortcut) 'add as new dir node w/closed folder dirNode. DriveImages......Hidden Or FileAttribute.GetNodeCount(False).Clear() Return 'nothing else to do End If '. so return it End If Next End If Return Nothing 'nothing found End Function Page –534– .Add(parentNode...... If nd IsNot Nothing Then 'did it find a match? Return nd 'yes...... True) 'parse any of its subfolders End If Next End If '.FolderSide.Nodes(0) 'start at the TreeView's base node If SeekPath = RootNode....tvFolderTree... IO...Add(parentNode..... Idx + 1) 'find the next backslash Dim BasePath As String = SeekPath........GetNodeCount(False)..0 – David Ross Goben End If '.....If Bol Then For Each dirPath As String In dirs 'else parse each subfolder If (GetAttr(dirPath) And (FileAttribute.If lnkFolders. 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:". newSubfolder.cboDrives.Handle.CreateDirectory(newPath) 'else try to create the folder.Add(New DriveItem("Desktop")) 'Add Desktop item Me.Items.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..IsVisible Then 'if we cannot see the node.Items. If MessageBox(Me.Items..Index).ToolTipText = newPath 'set its path '--------------------------------------------------------------------------If Not selNode. ComboBox) Dim DriveItem As DriveItem = DirectCast(cboBox.cboDrives.Exclamation) = DialogResult.EnsureVisible() 'make sure the new node is seen End If Me. Catch If MessageBox(Me.X Page –535– 'clear the background 'ignore out of range index 'get the listbox pointed to by this event 'get the Drive item from the list 'init drawing from left position .IsNullOrWhiteSpace(newSubfolder) Then 'if the user canceled.Count.cboDrives.RetryCancel Or MsgBoxStyle.tvFolderTree.ToString.. Me.0 – David Ross Goben '********************************************************************************* ' Method : btnMakeNewFolder_Click ' Purpose : Add a new folder from the current node '********************************************************************************* Private Sub btnMakeNewFolder_Click(sender As Object. Exit Sub 'then simply leave End If Else Try IO..tvFolderTree. selNode.. DriveImages. "New Email Storage Folder") 'prompt for folder If String.Items. DriveImages. DriveItem) Dim X As Int32 = e.Handle. Me. "Path Already Exists".Enhancing Visual Basic .Add(New DriveItem("Documents")) 'Add Documents item Me..FolderClosed) 'add the new node selNode.SelectedNode = selNode 'make sure the new node is selected Me. e As DrawItemEventArgs) e.Items.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.IsReady Then 'if it is ready.File.Add(New DriveItem("RecentPlaces")) 'Add Recent Places item 'Me..Add(New DriveItem("Downloads")) 'Add Downloads item Me..SelectedNode 'get the treeview node selected If selNode Is Nothing Then 'if nothing is selected.GetLogicalDrives 'get list of drives For Each Drv As String In Drives 'now check each one of them out Dim di As New IO.Add(New DriveItem("Videos")) 'Add Videos item Me.Index <> -1 Then Dim cboBox As ComboBox = DirectCast(sender. "'" & newSubfolder & "' already exists!".Directory..Add(New DriveItem("Pictures")) 'Add Pictures item Me.Focus() 'set focus to treeview End Sub '********************************************************************************* ' Method : PopulatecboDrives ' Purpose : Populate the cboDrives list '********************************************************************************* Private Sub PopulatecboDrives() Me.cboDrives.Focus() 'set focus to treeview Exit Sub 'then leave Else If folderPath.Cancel Then 'if user cancels.Nodes.Items.cboDrives. Return 'simply leave End If Dim folderPath As String = selNode. "'" & newSubfolder & "' cannot be created! It may be invalid".Add(New DriveItem("ThisPC")) 'Add Videos item '--------------------------------------------------------------------------Dim Drives() As String = Environment. MsgBoxStyle..Bounds.Add(New DriveItem("Music")) 'Add Music item Me.. MsgBoxStyle.tvFolderTree..FolderClosed.Exists(newPath) OrElse IO.Cancel Then 'if error and user cancels. "Folder Creation Error".cboDrives.Items.NET Beyond the Scope of Visual Basic 6.tvFolderTree. e As EventArgs) Dim newPath As String = Nothing 'added path '--------------------------------------------------------------------------Dim selNode As TreeNode = Me.Nodes.Add(selNode.Exclamation) = DialogResult.DrawBackground() If e.cboDrives.cboDrives...RetryCancel Or MsgBoxStyle.cboDrives..Clear() 'clear any current list Me. Exit Sub 'then simply leave End If Continue Do 'else try again End Try Exit Do End If End If Loop '--------------------------------------------------------------------------selNode = selNode.DriveInfo(Drv) 'get the info for the drive If di.cboDrives..Items.Directory.Items(e.Items.Exists(newPath) Then 'if it already exists as file or folder. e As EventArgs) With DirectCast(sender. 'draw image for ">" 'move beyond image 'Get the image stored In the ImageList 'draw image at start of line 'move beyond image with buffer 'draw the text after the bitmap(s) '********************************************************************************* ' Method : cboDrives_SelectedIndexChanged ' Purpose : A new choice was made in the cboDrives list '********************************************************************************* Private Sub cboDrives_SelectedIndexChanged(sender As Object.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.Parent IsNot Nothing 'enable UpOnefolder button as needed End If Me.Count .tvFolderTree. e As EventArgs) Me.RemoveAt(Me. Y) X += img.DrawString(DriveItem. X._HistListIndex -= 1 'naveigate back one NavFwdBack_Support() 'process selection End Sub '********************************************************************************* ' Method : tsbNavPrev_DropDownOpening Page –536– ..SelectedItem.Add(tvFolderTree.DriveType._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. CSng(Y)) End If End Sub 'if drive info and also list dropped down.tsbNavUp.tsbNavNext.1 'point to it Me.Nodes._StartPath = Me.. RootNode.Nodes.Type = IO.Directory.SelectedNode = RootNode 'select this node RootNode. Me.Count . e As EventArgs) Me.Width End If img = Me.DrawImage(img. ComboBox) If .Images(DriveImages.Drive 'grab the root path from the list object If Me.Count) Then 'if we have children._Root & "\" <> IO.Images..tvFolderTree.tvFolderTree.ItemHeight Then img = Me._HistListIndex = .Y If Me._Root. DriveItem).Add(Path) 'add a new path to the list Me.DrawImage(img.Expand() 'make sure root node is expanded End If '----------------------------------------------------------------------RootNode = FindNodePath(Me.Tag = False 'tag it as not a shortcut DirRecurse(RootNode) 'parse any of its subfolders '----------------------------------------------------------------------If CBool(RootNode.SelectedIndex <> -1 Then 'if the index is OK.Bounds.WaitCursor 'we are busy Me. Me..Enhancing Visual Basic ...EnsureVisible() 'make sure it can be seen Me.Clear() Dim RootNode As TreeNode = Me.Enabled = RootNode._HistListIndex + 1) Loop End If .tvFolderTree.Black.1 <> Me.myImages.Graphics.myImages.Focus() End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : AddtoList ' Purpose : Add an item to the Browser History list '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub AddtoList(ByVal Path As String) With Me.Enabled = False 'cannot yet navigate forward Me.NET Beyond the Scope of Visual Basic 6._HistListIndex 'otherwise._StartPath) 'open directory to the node If RootNode IsNot Nothing Then 'if we found it (very likely) Me..0 – David Ross Goben Dim Y As Int32 = e. CSng(X).Graphics.Graphics. cboBox.Count <> 0 Then Dim img As Image If Not DriveItem.tvFolderTree.ToString.Nodes._HistListIndex) = Path Then 'does it match the current path? Return 'yes. Y) X += img.ToString.myImages._Root 'then ignore the start path and begin anew End If '----------------------------------------------------------------------MeCursor = Cursors. X.ToolTipText) 'add item to list tvFolderTree.Unknown AndAlso Y >= cboBox.ToolTipText = Me.Images(DriveItem.tsbNavPrev._Root 'save its path in its tooltip RootNode._HistList If ..SuspendLayout() 'avoid flicker Me. 0.Font. remove history following .GetDirectoryRoot(Me.Enabled = Me. 0) 'create Root Node w/closed folder RootNode._StartPath) Then 'if it does not match the last start path.Count <> 0 Then 'if the history contains items If .Item(Me. Brushes.Default 'show that we are no longer busy End If Me.ResumeLayout() 'allow pending updates to process MeCursor = Cursors. so ignore it (already set) End If Do While .Count.ImageIndex) e._Root = DirectCast(.Nodes.AddtoList(RootNode.Width + 4 End If e. Me.FolderCanOpen) e. tvFolderTree.IsNullOrWhiteSpace(itm..tsbNavUp.Text = Item 'reset the empty text End If itm.GetFileName(Item)._HistListIndex = CInt(e. Item &= "\" 'add a backslash to the root path itm.Path.Count .Add(IO..SelectedNode = selNode 'select it selNode. Item &= "\" 'add a backslash to the root path itm._HistList. e As EventArgs) With tsbNavNext.Parent IsNot Nothing 'enable UpOnefolder button as needed Me.FolderSide)) If String..Add(IO.tvFolderTree.Text = Item 'reset the empty text End If itm.tsbNavNext._HistListIndex) 'grab indexed item Dim Root As String = RemoveSlash(IO.DropDownItems ._HistListIndex < Me._HistListIndex ._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.Clear() 'clear current dropdown list For Idx As Int32 = Me.Enhancing Visual Basic .Count .Path.Parent 'grab parent node Me._HistList.._StartPath)) 'get the root of the path With Me.myImages. Return 'simply leave End If selNode = selNode.ClickedItem.ToolTipText = Item 'set the path to the tooltip itm.Enabled = selNode. Me.Enabled = Me._HistListIndex = CInt(e.0 – David Ross Goben ' Purpose :Populate Nav Prev dropdown list '********************************************************************************* Private Sub tsbNavPrev_DropDownOpening(sender As Object.GetFileName(Item). e As ToolStripItemClickedEventArgs) Me.cboDrives Page –537– .GetDirectoryRoot(Me. e As ToolStripItemClickedEventArgs) Me.Tag = Idx 'save the history index Next End With End Sub '********************************************************************************* ' Method : tsbNavPrev_DropDownItemClicked ' Purpose : Scan backward through the browser selection list '********************************************************************************* Private Sub tsbNavPrev_DropDownItemClicked(sender As Object. e As EventArgs) With tsbNavPrev.Enabled = Me..EnsureVisible() 'ensure it is visble Me.Text) Then 'if it was a root drive.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._HistList(Idx) 'grab an item Dim itm As ToolStripItem = .1 To 0 Step -1 'populate with previous items from current Dim Item As String = Me.tvFolderTree.1 'set enablement for FWD button Me.SelectedNode 'get the treeview node selected If selNode Is Nothing OrElse selNode._StartPath = Me.ClickedItem._HistList(Me.ToolTipText = Item 'set the path to the tooltip itm._HistList(Idx) 'grab an item Dim itm As ToolStripItem = ._HistListIndex > 0 'set enablement of BAK button '------------------------------------------------------------------Me.NET Beyond the Scope of Visual Basic 6.myImages. Me.FolderSide)) If String.Images(DriveImages.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.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.Directory.1 'populate with following items from current Dim Item As String = Me._HistListIndex + 1 To Me.tsbNavPrev. e As EventArgs) Dim selNode As TreeNode = Me.Parent Is Nothing Then 'if nothing is selected..Images(DriveImages.DropDownItems .Clear() 'clear current dropdown list For Idx As Int32 = Me.Focus() End Sub '********************************************************************************* ' Method : tsbNavNext_ButtonClick ' Purpose : Scan forward through the browser selection list '********************************************************************************* Private Sub tsbNavNext_ButtonClick(sender As Object.Text) Then 'if it was a root drive. e As EventArgs) Me.IsNullOrWhiteSpace(itm. Enabled = NewNode.Enhancing Visual Basic .Add(Img) 'add this image as imgList.NET Beyond the Scope of Visual Basic 6. 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._StartPath) If NewNode IsNot Nothing Then Me. set the Replace ' : parameter to FALSE. ByVal Value As String) If Value Is Nothing Then 'if the Value is set to Nothing.. 'then set it as the selection 'and we are done checking the list 'find and open path to nide in treeview 'select node 'enable UpOneFolder button as needed 'set focus to treeview '********************************************************************************* ' Method : GetCustomSetting ' Purpose : Get value from Custom Section of myEmail Registry entry '********************************************************************************* Friend Function GetCustomSetting(ByVal Key As String.ImageSize = New Size(16. Nothing)) 'get the value from the key If Not String. Try DeleteSetting(My. Dim Text As String = CStr(GetCustomSetting(Key. find the desired Root drive in the list 'if we found it.Application.Focus() End Sub 'next.Images. Key. Value) 'otherwise.. ByVal DefaultValue As String) As Object Return GetSetting(My. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub InitializeImageList(ByRef imgList As ImageList.Items If di.Application. "Settings".tvFolderTree. DefaultValue) End Function '********************************************************************************* ' Method : SaveCustomSetting ' Purpose : Save value to Custom Section of myEmail Registry entry '********************************************************************************* Friend Sub SaveCustomSetting(ByVal Key As String. not appending images imgList.Images.SelectedNode = NewNode Me.Title.Info. Key) 'then simply delete the key Catch End Try End If Else SaveSetting(My.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.0 – David Ross Goben For Each di As DriveItem In .tvFolderTree.Add(Img) 'add this image as imgList.Equals(di) Then . 'if it does not match the currently selected item.Application..SelectedItem = di End If Exit For End If Next Dim NewNode As TreeNode = FindNodePath(Me.Info..IsNullOrWhiteSpace(Text) Then 'if the value is not already deleted. "Settings".Clear() 'initialize image list 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.tsbNavUp.SelectedItem.Add(Img) 'add this image as imgList.Images.Parent IsNot Nothing End If End With Me.. Key. "Settings".. 16) 'define 16x16 pixel images in this list End If imgList. 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.Drive.Images.Images(2) Page –538– .Title.Equals(Root) Then If Not .Title.ImageSize = New Size(16. Value contains data.Info.. Optional ByVal Replace As Boolean = True) If Replace Then 'if we are filling. Add(Img) 'add this image as 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.Images.0 – David Ross Goben '-------'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.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(3) '-------'Image 4 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACxSURBVDhP5Y+xDcMgEEXZiQ3oKanpWIGWyiMg0bnIBEzA" & "APRUVB6E6J9yjmNbltMmX3qSfdz7xuLHUkoZV7zWjuGFZVkuOS2bpmm01k6FLb33D1AClwpSSiPnTEMc" & "7kV8oNZKYM85R6wFEOf5MWKMBJZ4GefeexKUUkNKSfB8/Q3At8BzCIEkFhhjDInW2ncBBwOAAhShQGtN" & "ywDvXHCQt8EhbsAipFviPix8Lf5VhHgCWJYzPQzt+z0AAAAASUVORK5CYII=" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images.Images(10) Page –539– .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.Add(Img) 'add this image as imgList.Images.Images.Add(Img) 'add this image as imgList.Add(Img) 'add this image as imgList.Enhancing Visual Basic .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.NET Beyond the Scope of Visual Basic 6.Images.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(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.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.Images(18) '-------'Image 19 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABqSURBVDhP7YxRCoAwDEN7J+/knbyTV9hZqumWbgw3quif" & "D0IgbSKvsK2id1RqFYS6LyH9Ax8NIBiqL1PnzdERdusG6Lma8edWzMIDIKX0fIBlOjCPDLRlL9IjAxDL" & "LcjxfCVrFuxxogkiB2Z21dLgH0jYAAAAAElFTkSuQmCC" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images.Images.Images(15) '-------'Image 16 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABVSURBVDhPYxjc4OPHj/+hTNLB9evX/x84cOA/2YacOXPm" & "/4IFS8g35D8QUMUQkGaKDAFp2rRpE9iQu3fvkmYARS4AaaYoDCgOQIrTAQhQpHmoAAYGAFSwieHivX4c" & "AAAAAElFTkSuQmCC" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images(12) '-------'Image 13 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACvSURBVDhPxZOxDcMgEEXZyRWte5degTmYAuk6F1HmoI6u" & "Z4kU8QQX/ikk4hwpil34SU9IBh7X2J3PslzkiBrYSxcI9J9gEyD+aC9YQRfg+3db8Hp7qO0bMBMEiTFK" & "Skk3bMgKusAQgszzrCtigUhyznq4vQzXdVVBF/DeyziO6jRNNTSoNkrMwlVgJqBOHIbYxyWdoq6llN+B" & "oAFWCdYLzRYCXWAP78ARX3/EaTj3BE/HY6gtq9MqAAAAAElFTkSuQmCC" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Add(Img) 'add this image as imgList.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.Add(Img) 'add this image as imgList.Images.Images(11) '-------'Image 12 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACFSURBVDhPxZLNDYAgDIXZiZ3YyQUcxnQUDh65If0hNgEB" & "68GXNPAS+rUU3FBbzhRmCWA/TiNEAB6sXQgAI5ggCmCDUCJkryDFvoAIALdUvXgvfk0KgOJOPgC4i78B" & "0xnQIRmYBkBkT2tP9UAbd8XHZFR9phFgqD4AP9EiIKXU3D3ALNm5C0L52yY+Ei+PAAAAAElFTkSuQmCC" & "" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Add(Img) 'add this image as imgList.NET Beyond the Scope of Visual Basic 6.Images.Add(Img) 'add this image as imgList.Enhancing Visual Basic .Images.Add(Img) 'add this image as imgList.Add(Img) 'add this image as imgList.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(19) '-------- Page –540– .0 – David Ross Goben '-------'Image 11 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABfSURBVDhP7ZHLEQAREAUnJznJSU6bE2bXK4zxWScHXfUu" & "6L4gDWMfb5xYPEvXcz7Ji93AwQHLj7V/VwLVosNujryTUm+FDNYjigzmkYEM+pEFGbSRHzLIkQ0ZsDiW" & "iQJ9a0WWG+2+xgAAAABJRU5ErkJggg==" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images.Images(16) '-------'Image 17 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABUSURBVDhPYxgFVAYzZ878X15e/v/jx4//oUJgAOKDxBcs" & "WIIijgFAipycnFAMgWmGiYMV4gLoiknSDAPImnx9fUnTDANk2YwOQIYQDLRRQA3AwAAAiXFatNps77AA" & "AAAASUVORK5CYII=" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.Images. Images(21) '-------'Image 22 '-------strImg = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8" & "YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABKSURBVDhPpYxBCgAgDMP2/08rBQdDKnZroPSUxIN1fgwC" & "ViQD40gNjCJ3oB1hAUyGyTkJJtZ9YVJOgomYjCUDSwaWDCwZWHKDiA2SBz/Bns8W7AAAAABJRU5ErkJg" & "gg==" Img = ConvertBase64ToImage(strImg) 'grab image from string data imgList.MemoryStream = New IO.Images.Enhancing Visual Basic .Images.Add(Img) 'add this image as imgList.Add(Img) 'add this image as imgList.NET Beyond the Scope of Visual Basic 6.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.Images(22) End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : ConvertBase64ToImage ' Purpose : Convert a Base64 String to an Image object '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function ConvertBase64ToImage(ByVal strImg As String) As Image Dim bAry() As Byte = Convert.FromStream(memStream) 'construct image from stream data memStream.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.Close() 'release stream resources Return Img 'return image End Function End Class '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' BrowserSupport Static Class Module ' Supprt for BrowserDialog class '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module BrowserSupport 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE 'Enumerator: DriveImages 'Purpose : Reference to images in the ImageList 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE 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 Page –541– .MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image.0 – David Ross Goben '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. DriveType.Path 'then return the link's command path Else Return Nothing 'otherwise failure.Last = "\"c Then 'already have a backslash? Return strPath.Path. Shell32.0 – David Ross Goben '******************************************************************************* ' 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.ImageIndex = DriveImages. '********************************************************************************* Friend Function RemoveSlash(ByVal strPath As String) As String If strPath.ImageIndex = DriveImages.FolderItem = Folder. strPath.Length .Drive = RemoveSlash(DrvInfo.VolumeLabel 'volume label Me.DriveType. so indicate so End If End Function '********************************************************************************* ' Method : AddSlash ' Purpose : Add a terminating backslash to a drive/path if required.Type Case IO.Ram Case IO.ShellClass 'define a folder object to the link file's directory folder Dim Folder As Shell32.Network Me. such as C:\ Friend Volume As String 'volumn name for the drive Friend Type As IO.GetLink.DriveType. 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.DriveInfo) Me.Type = DrvInfo.Volume = DrvInfo.Removable Me.Last = "\"c Then 'already have a backslash? Return strPath 'yes.DriveType.Fixed Case IO.Removable Case IO. and the string you are working with may ' : or may not already have a backslash appended to it.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. return the string with a backslash End If End Function '********************************************************************************* ' Method : RemoveSlash ' Purpose : Remove any existing terminating backslash from a path.GetDirectoryName(ShortcutFilePath)) 'define a link to the shortcut file object from the folder object Dim FolderItem As Shell32. Return DirectCast(FolderItem. This function ' : is useful for building paths.ImageIndex = DriveImages.Fixed End Select End Sub Page –542– . so remove trailing slash if it exists Else Return strPath 'otherwise.Folder = Shell.Name) 'grab drive (C:\) Me.Path.. 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.NET Beyond the Scope of Visual Basic 6.NameSpace(IO.CDRom Case IO.ImageIndex = DriveImages. so simply return the string Else Return strPath & "\" 'otherwise..Ram Me. '********************************************************************************* Friend Function AddSlash(ByVal strPath As String) As String If strPath.ImageIndex = DriveImages.ImageIndex = DriveImages.CDRom Me.Network Case Else Me.ParseName(IO.Fixed Me.Substring(0.ShellLinkObject).GetFileName(ShortcutFilePath)) If FolderItem IsNot Nothing Then 'if it exists.Shell = New Shell32.dll).DriveType 'get the drive type enumeration '----------------------------------------------'now compute the image index for the drive type '----------------------------------------------Select Case Me.Enhancing Visual Basic .1) 'yes.DriveType. Unknown 'set the drive type enumeration Me.ImageIndex = DriveImages.Recent) Me.Volume = "Videos" 'volume label Me. '******************************************************************************* '******************************************************************************* Friend Sub New(ByVal DrvInfo As String) Select Case DrvInfo.NET Beyond the Scope of Visual Basic 6.DriveType.GetFolderPath(Environment.GetFolderPath(Environment.Unknown 'set the drive type enumeration Me.Volume = "Desktop" 'volume label Me.DriveType.ToLower Case "desktop" Me.Type = IO.DriveType.Type.GetFolderPath(Environment.DriveType.Drive = Environment.Type = IO.GetFolderPath(Environment.DriveType.Music 'set image index Case "pictures" Me.ImageIndex = DriveImages. etc.MyDocuments) Me.SpecialFolder.Type = IO. suck as desktop.Downloads 'set image index Case "music" Me.Unknown 'set the drive type enumeration Me.SpecialFolder.Drive = Environment.Documents 'set image index Case "downloads" Me.Type = IO.Pictures 'set image index Case "videos" Me. such as Desktop.Unknown 'set the drive type enumeration Me.Volume = "Music" 'volume label Me.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 '******************************************************************************* Public Overrides Function ToString() As String If Me.Volume = "Documents" 'volume label Me.ImageIndex = DriveImages.MyMusic) Me.Volume = "Downloads" 'volume label Me.MyPictures) Me. All rights reserved.Drive = Environment.MyVideos) Me.Videos 'set image index Case "recentplaces" Me.Volume = "Recent Places" 'volume label Me.Type = IO.ImageIndex = DriveImages.Drive = Environment. Documents.GetFolderPath(Environment.SpecialFolder.ToString & " Drive]" End If End Function End Class '****************************************************************************** ' Copyright © 2012-2015 David Ross Goben.GetEnvironmentVariable("USERPROFILE") & "\Downloads" Me.DriveType.DriveType.DriveType.Enhancing Visual Basic .Volume = "Pictures" 'volume label Me.Unknown 'set the drive type enumeration Me.Drive = Environment.Drive = Environment.Trim.ImageIndex = DriveImages.Type = IO.Volume & " (" & Me.ImageIndex = DriveImages.SpecialFolder.Volume Else 'otherwise a logical drive Return Me.ImageIndex = DriveImages.Unknown 'set the drive type enumeration Me.Drive = Environment. '****************************************************************************** Page –543– .Desktop 'set image index Case "documents" Me. etc.Unknown 'set the drive type enumeration Me. Music.GetFolderPath(Environment.SpecialFolder.Drive & ") [" & Me. Return Me.SpecialFolder.Desktop) Me.Unknown Then 'virtual drive.Type = IO.Type = IO.0 – David Ross Goben '******************************************************************************* '******************************************************************************* ' Method Name : New (overload) ' Purpose : Set up the class using a Text Name for generic items. picked up from the Aixilis WorkShop website on their page under the title. because an ImageList will store the images you add as an internal ImageStrip. then you have been using ImageStrips all along and probably never knew it. Were you to look at this control’s exposed properties by applying the dot operator to the name of an ImageList in your program code. For color depths of 24 BPP or less. 256 indexed colors (8 BPP). Page –544– . it is actually an ImageListStreamer handle. The problem with this is that it means that there is one less color in a limited pallet of colors that you can use for drawing your images. Although the documentation for this property states that “You can pass this handle to another instance of an ImageList. this color used is either a day-glow green (RGB(0. Each pixel in the image that has that color will be rendered as transparent. 2) They do not know how to create their own ImageStrips. Consider the above imagestrip examples.axialis. a fixed color is used for the transparency color. you will find a property named ImageStream.0. such as in the last two Black Book Tips. Color depths are typically 16 colors (4 Bits per Pixel. Indeed. the transparency is coded right within the Alpha component of a color and it can be used to render rounded images with smooth edges. In 32 BPP images.” Even so. or as a series of rows stacked atop each other that will in turn be treated as if they were a single row of images. but you cannot access the image data through this.255)). Creating. because the property is actually Read-Only. which in turn point to the binary data stored within your program’s resources. doing all of these things is almost insanely easy to do.com/tutorials/image-strip. # 49 and # 50. Typically. ImageList2 = ImageList1).Enhancing Visual Basic . It seems that a number of things are frustrating numerous programmers: 1) They do not know how to load an ImageStrip. and the solution to all of them is to just use the simple ol’ ImageList control. which technically indexes into an internal reference list.NET Beyond the Scope of Visual Basic 6.255.0)) or that god-awful magenta (RGB(255. or you would just like to lift an image or two and add them to your own application. RGB true color (24 BPP). Fortunately. or 3) They do not know how to split an ImageStrip and to collect specific images. If you use an ImageList control and add images to it. But some are stuck on how to do that. you can easily construct an ImageStrip image and create an ImageStrip file with surprisingly little code. as I will show you. you will see that “you cannot use this class to load images to an ImageList control by directly assigning the ImageStream property from one ImageList. and that rounded shapes do not have the smooth edges that Alpha-channel transparency offers.html) Looking at the internet. They can consist of a single row of images joined next to each other. They often have really nifty images that you would want to take advantage of. What is an Image Strip? (http://www. or BPP).0 – David Ross Goben Black Book Tip # 51 Using. and RGB true color with alpha component (32 BPP).” this is not true. it seems you can find free ImageStrips plastered everywhere. Instead you should directly assign one ImageList instance to another (for example. if you look to the documentation for an ImageListStreamer. and Embedding ImageStrips with Ease An ImageStrip is a single image object that in turn consists of two or more images that all have the same size and the same color depth. just as we have done within this document. because you do have total access to all of its images. You can load this image from a file. Because an ImageStrip is just an image file. the ImageStrip will be saved as a PNG file and you should ' : add a ".png" file extension to your file. but this is not really necessary for 16x16 images.Height) 'size the bitmap imagestrip Dim g As Graphics = Graphics.AddStrip(Image.ImageSize.ImageFormat = Nothing) If ImageType Is Nothing Then ImageType = Drawing. and even though you gave it a “.bmp")”.ImageStrip) Once the ImageStrip is loaded. New Point(Idx * imgWidth.Imaging.Save("C:\Users\JoeSchmindy\Pictures\ImageStrip. We then create a new bitmap.myImages. SaveImageStrip(): '******************************************************************************* ' Method : SaveImageStrip ' Purpose : Save the images in an ImageList to a specified file as an ImageStrip.Save(DestFilePath.0 – David Ross Goben Loading an ImageStrip This is as easy as using the AddStrip() method from the Images collection of your ImageList.Enhancing Visual Basic . we use the ImageList control to draw each of its images to a bitmap that is sized to fit all of its images size-by-side.Save("C:\Users\JoeSchmindy\Pictures\ImageStrip. Optional ByVal ImageType As Drawing. it might be nice to save them off as an ImageStrip. and then the count of images from its Images collection. we dispose of our graphics interface and save the bitmap. VB. Once done. which is much more efficient than the old default of BMP (Bitmap) used by VB6.Imaging. Image). but two or more rows of images. and downward row-by-row as though it were one long narrow image.NET saves images as PNG files (Portable Network Graphics). consider the following subroutine.Images. ' : by default. such as “Me. For example. you need to specify the Image object that contains the ImageStrip. Creating Your Own ImageStrip Once you have loaded your ImageList with images.bmp” extension. even if you saved your file from your image using code like “myImage. 16)”. which is its default.NET default for image saves is PNG End If With imgList Dim imgWidth As Int32 = . For example: Me. ImageType) Catch End Try End With End Sub Here. cast as an Image object (identical format).Dispose() 'dispose of resources Try 'save the imagestrip to a destination filepath DirectCast(newImage.ImageFormat. If you want to save it in a ' : different format.ImageFormat. let us first review how we save them.FromFile("C:\Users\JoeSchmindy\Pictures\ImageStrip. Idx) 'draw the image to the right of the previous image Next g. We next create a graphics interface for it so we can draw to it.1 'process each image to the imagestrip .Png '. we cycle through each of its images and draw them across newImage.Bmp)”.Images. newImage.Imaging. Page –545– .ImageSize. you would have to specify “myImage.Count 'get the number of images in the imagelist Dim newImage As New Bitmap(imgWidth * imgCount.Width 'get the width of a single image Dim imgCount As Int32 = . to the specified file path. By default. or load it from your resources. . ByVal DestFilePath As String.Imaging.myImageList. and by the image size height. We pick up the established individual image width from the control. Drawing. you will have to specify that format.png")) Or… Me. This is especially important for stacked images.myImages. where the strip is not just a single row of mages.FromImage(newImage) 'create a graphics handler for it For Idx As Int32 = 0 To imgCount .ImageSize = New Size(16. Now.Draw(g. Following that. You should first initialize the image sizes.AddStrip(My. shoulder-to-shoulder.Resources.bmp". So. that is sized to be the count of images times the pixel width of a single image. ' : a bitmap formap specification is "Drawing. you can use it just as though you had simply loaded a series of images.Bmp" '******************************************************************************* Friend Sub SaveImageStrip(ByRef imgList As ImageList. To actually save it as a bitmap file.ImageFormat.Images. To use the AddStrip() method.NET Beyond the Scope of Visual Basic 6. it will still be saved as a PNG file. 0). It will process this image left-to-right. Images Dim Ln As String = "'" & New String("+"c. Splitting an ImageStrip This is a moot topic by now. paste the Clipboard code into your form's code body. enter this command: InitializeImageList() by ' : passing it the name of your target ImageList. suppose we want to save its images as an ImageStrip in PNG file format.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. ' : ' : For example.NET Beyond the Scope of Visual Basic 6.png to an existing specified folder. this invocation will create all desired images ' : and add them to myImageList. filling it with the images you desire ' : without requiring the user to first load those images into their resources. This will add a ' : Function named InitializeImageList() and a support function named ' : ConvertBase64ToImage(). We can access each individual image from the Images collection of the ImageList as though we had loaded them individually. 81) & vbCrLf 'build a header line Dim Pad As String = Space(4) 'init padding for text lines Dim Buffer As New System. preferably within your Form's Load() event. where it will be added to the target ImageList. set the Replace" & vbCrLf & Page –546– . For example: ' : Private myImageList As New ImageList ' : ' : Next.ImageSize. '******************************************************************************* Friend Sub BuildImageListCode(ByRef srcImageList As ImageList. now-not-existing ' : imagelist (well. and splitting out ImageStrips. By passing ' : that to the ConvertBase64ToImage() method. but you can define them more efficiently using a single block of code. "C:\LocalImages\ImageStrip2. For example: ' : BuildImageListCode(Me. plus ' : any other code that may have previously initialized the ImageList. ' : ' : Now. " & srcImageList. but you already saw that demonstrated when the Save() method was used to save the ImageStrip Image to a file. because in the process of covering how to save an ImageStrip from an ImageList control. if you want the target ImageList to be named 'myImageList'. just do not reference it). This would save the ImageList. either in-code or manually.Height. convert the above BuildImageListCode() command line into a comment.ToString Buffer. ' : ' : Next. when you run your program.myImageList) ' : ' : InitializeImageList() will define each image as a Base64 text string. the only thing left to cover here is how to save individual images off. this method will create code that can ' : load up an ImageList from scratch. building. here named myImages. Indeed. Optional ByVal MakeImageStrip As Boolean = False) With srcImageList. you can load it from a file or from resources to another program as previously shown. to a file named ImageStrip2. it will convert this string into an ' : Image object and return it. or whatever you chose to name it. you saw that we went through no effort to split them out.ToString & ".Text.myImageList) ' : ' : This method will save the fully-constructed VB source code it to the Clipboard. Once it is saved.Enhancing Visual Basic . fill it with the needed images. It ' : will process all images originally defined in the initial. and then pass ' : it to this method.Width. Right below where you had disabled the invocation of ' : the BuildImageListCode() method.png")”.0 – David Ross Goben After our ImageList is loaded (if we did not already pre-load it at development time). and enhance it to optionally build source code to embed an ImageStrip into your source code? The advantage is you do not need to build the ImageList from individual images.StringBuilder Dim NewSize As String = srcImageList. For example: ' : InitializeImageList(Me.myImages. why not revisit Black Book Tip # 49. we could use the above method something like this: “SaveImageStrip(Me. Embedding Images within Your Source Code. you can test this without deleting it. ' : define that in your program heading. What follows is the updated BuildImageListCode program module: Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modBuildImageListCode Static Class Module ' Build Image List Source Code Constructor '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modBuildImageListCode '******************************************************************************* ' Method : BuildImageListCode ' Purpose : Provided an existing filled Image list. Updating Black Book Tip # 49 to Use ImageStrips Now that we are experts at loading.ImageSize. ' : ' : Next. ToString & " Images" & vbCrLf & Pad & "'------------------------" & vbCrLf) Buffer.MemoryStream = New IO.ImageSize.ImageSize.Enhancing Visual Basic .ImageSize.png" file extension to your file.Add(Img) 'add this image as imgList.Images. ByVal DestFilePath As String.ImageFormat.ToString.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) '--------------------------------------------------------------'build data as a single ImageStrip '--------------------------------------------------------------If MakeImageStrip Then With srcImageList Dim imgWidth As Int32 = . ' : by default. New Point(Idx * imgWidth. For example.Height) 'size the bitmap imagestrip Dim g As Graphics = Graphics.1). Image)))) 'break up and format Base64 string Buffer.Images.Append(Pad & "'--------" & vbCrLf & Pad & "'Image " & idX. .Draw(g.ImageFormat = Nothing) If ImageType Is Nothing Then ImageType = Drawing.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.1 'process each image to the imagestrip . TextDataFormat.ImageFormat.ToString & vbCrLf) End With Else '----------------------------------------------------------'build data as individual images '----------------------------------------------------------For idX As Int32 = 0 To . ImageType) Catch End Try End With End Sub Page –547– .FromImage(newImage) 'create a graphics handler for it For Idx As Int32 = 0 To imgCount .Count 'get the number of images in the imagelist Dim newImage As New Bitmap(imgWidth * imgCount." & (imgCount .MemoryStream(bAry) 'convert byte array to a memory stream" & vbCrLf & Pad & "Dim Img As Image = Image.Dispose() 'dispose of resources Try 'save the imagestrip to a destination filepath DirectCast(newImage.Width 'get the width of a single image Dim imgCount As Int32 = .Append(vbCrLf & Pad & "Img = ConvertBase64ToImage(strImg) 'grab image from string data" & vbCrLf & Pad & "imgList.Imaging. you will have to specify that format. Optional ByVal Replace As Boolean = True)" & vbCrLf & Pad & "If Replace Then 'if we are filling.FromStream(memStream) 'construct image from stream data" & vbCrLf & Pad & "memStream.Images.Images(" & idX.Append(vbCrLf & Pad & "Img = ConvertBase64ToImage(strImg) 'grab image from string data" & vbCrLf & Pad & "imgList.ImageSize.SetText(Buffer. the ImageStrip will be saved as a PNG file and you should ' : add a "." & vbCrLf & Ln & "Private Sub InitializeImageList(ByRef imgList As ImageList.Height) 'size the bitmap imagestrip Dim g As Graphics = Graphics.Append(BreakUpBase64String(ConvertImageToBase64(DirectCast(newImage. ' : a bitmap formap specification is "Drawing.Png '.Count .Draw(g.ToString & ")" & vbCrLf) Next End If '--------------------------------------------------------------'Finish out source code using common code '--------------------------------------------------------------Buffer.Images.Bmp" '******************************************************************************* Friend Sub SaveImageStrip(ByRef imgList As ImageList.FromBase64String(strImg) 'grab Base64 data as a byte array" & vbCrLf & Pad & "Dim memStream As IO.AddStrip(Img) 'add this as Images 0 .FromImage(newImage) 'create a graphics handler for it For Idx As Int32 = 0 To imgCount .0 – David Ross Goben "' : parameter to FALSE.Save(DestFilePath.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. 0).Append(BreakUpBase64String(ConvertImageToBase64(.Item(idX)))) 'break up and format Base64 string Buffer.Append(Pad & "'------------------------" & vbCrLf & Pad & "'ImageStrip for " & imgCount. New Point(Idx * imgWidth.Close() 'release stream resources" & vbCrLf & Pad & "Return Img 'return image" & vbCrLf & "End Function" & vbCrLf) Clipboard.Clear() 'initialize image list" & vbCrLf & Pad & Pad & "imgList.Images.Imaging.Imaging.Count 'get the number of images in the imagelist Dim newImage As New Bitmap(imgWidth * imgCount. Image). Optional ByVal ImageType As Drawing.1 'process each image to the imagestrip . . 0). Idx) 'draw the image to the right of the previous image Next g.Dispose() 'dispose of resources Buffer.ToString & vbCrLf & Pad & "'--------" & vbCrLf) Buffer.1 'process all images in the ImageList Buffer.NET default for image saves is PNG End If With imgList Dim imgWidth As Int32 = . Idx) 'draw the image to the right of the previous image Next g. not appending images" & vbCrLf & Pad & Pad & "imgList.Width 'get the width of a single image Dim imgCount As Int32 = . If you want to save it in a ' : different format.NET Beyond the Scope of Visual Basic 6. The second logical parameter will tell the method to build the images as a single ImageStrip.Png) 'copy current image to the memory stream Dim bAry() As Byte = memStream.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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 Dim idY As Int32 = 0 'init string offset Do While srcBase64. not appending images imgList. Optional ByVal Replace As Boolean = True) If Replace Then 'if we are filling. I could construct a new block of source code into the clipboard.Save(memStream.Clear() 'initialize image list imgList. vbCrLf) 'make one string.Enhancing Visual Basic .ImageSize = New Size(16. split by CrLf End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : ConvertImageToBase64 ' Purpose : Convert a source Image object to a Base64 String '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Function ConvertImageToBase64(ByRef srcImage As Image) As String Dim memStream As New IO. True)”.Substring(idY.Images. set the Replace ' : parameter to FALSE.Substring(idY) & """" 'add final line (no need for continuation Return Join(Ary. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub InitializeImageList(ByRef imgList As ImageList.Length > idY + 79 'breat up into lines of 80 characters apiece Ary(idX) = Pad & """" & srcBase64. ready for pasting.ImageFormat.NET Beyond the Scope of Visual Basic 6. by invoking the BuildImageListCode() method using “BuildImageListCode(Me.ToBase64String(bAry) 'construct a Base64 string End Function End Module With the above update.MemoryStream 'memory stream to receive image data srcImage.Close() 'done with the memory stram Return Convert. 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 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" & Page –548– .myImages. 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. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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.ToArray() 'convert the stream to a byte array memStream. were the images displayed below loaded into an ImageList named myImages. Imaging. 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.Enhancing Visual Basic .Close() 'release stream resources Return Img 'return image End Function Page –549– .FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image.0 – David Ross Goben "/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.NET Beyond the Scope of Visual Basic 6.FromStream(memStream) 'construct image from stream data memStream.MemoryStream = New IO. Img As Image = New To load the image from a cursor file requires just a little more work. With all those enhancements. where you saw how to efficiently embed images right within your source code file. on page 527. If the target file is an icon.NET Beyond the Scope of Visual Basic 6. It retrieves the default icon within the provided EXE or DLL file path that its developer had assigned as its default. merged all of these features and gave you a replacement Folder Browser Dialog to embed within your own applications. 16. Tip # 40 did display files in its tree view. DLLs. Black Book Tip # 50. the good news is that there is an easy way to grab those images from files. you saw how to speed up displaying a directory tree listing by delaying the parsing of folders to only when the users wants to view their contents.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!) cur. even if they do know how to rescale them. This was then followed by Black Book Tip # 49 on page 513. you saw how you can add some pizzazz to the display of your directory tree listing by owner-drawing all the elements of the TreeView.DrawStretched(g. but then we need to pick up its image and then scale it to 16x16. in Black Book Tip # 48 on page 509. Finally. because the standard Dot NET method of acquiring them returns only 32x32-pixel images. But we will deal with that shortly. Granted. 16) 'create a drawing surface Dim g As Graphics = Graphics.ToBitmap” (we will soon demonstrate image/bitmap rescaling). For example: '******************************************************************************* ' 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. it is easy enough to grab its image. For example: Dim ico As Icon = Icon. 16)) 'draw it to bmp's 16x16 surface g. and from executables associated with non-executable files. To extract the image from an EXE or DLL file is easy using the ExtractAssociatedIcon() method of the Icon object. which we can do using its DrawStretched() method. 0. crisp 16x16-pixel icons typically exist within those files. This method is very easy to use. or they do not know how to rescale them to accommodate their TreeView’s expectation of 16x16-pixel images. Later. And.ExtractAssociatedIcon(FilePath) 'grab default 32x32-pixel icon Page –550– . the one thing that was noticably lacking was descent representation of files. some of them look rather pitiful when they are scaled down 75%.Enhancing Visual Basic . from executables.0 – David Ross Goben Black Book Tip # 52 Extracting Icon Images from Files and Displaying Them in a Directory TreeView Earlier in Black Book Tip # 40 on page 479.Dispose() Return bmp 'return the bitmap/image End Function To use it. For example: “Dim Icon(IconPath). The issue has been that some programmers have often had a bit of trouble finding these file associations. the icon that should be associated with the file. but all those files shared the same boring icon. Grabbing the cursor is easy enough to do with something like “Dim Cur As New Cursor(ShortcutFilePath)”.FromImage(bmp) 'create a graphics interface for it cur. simply assign the returned bitmap to your Image variable (Type Bitmap = Type Image). which is especially frustrating when clear. New Rectangle(0. But. Some EXE files also lack icons. such as fonts. not ByVal. plus others that should act as substitutes in case those files lack an association. the path to that program will be returned instead. 0. Many of these are opened by other files.Enhancing Visual Basic . is to get them to work. the user has not yet selected a default application for that type.ToBitmap If bmp IsNot Nothing Then Dim bm As New Bitmap(16. This method can obtain access to an icon or list of icons. or so I am told. this method returns only a 32x32-pixel icon. there are a number of images that we can reserve for files we may choose not to extract images for. the path to that application will be returned. or they have types that typical interop services cannot manage. In any case. 32x32/48x48. though it is usually a service provider that has no stand-alone function.txt). The problem has been that many who I have helped were not paying attention to the fact that some parameters must be passed ByRef. And there is the issue with some rescaled 32x32-pixel images looking like crap at 16x16. some DLL files contain icons. doing this is quite easy. rescaling a bitmap is easy. so we will have to redraw it to our 16x16-pixel target image by rescaling it. or they have not installed an application that is designed to work with that file type. Actually. then we can detect that by the method returning a path of Nothing. With that method. Sometimes there are no applications associated with a file because it is either not practical. such as a picture. Fortunately. For example. but rather it exposes method and property members that act to extend another application that is able to exploit them.. If we can. and single Icon files has been ExtractIconEx(). a DLL is an EXE file. We can use the GetSelectedOpenerForExt() method from Back Book Tip # 36 on page 471 to acquire the path to the application associated with a file. 16. For example. Even some cursors cannot load without extra effort that is often not worth all the sweat to do so. and we can also use it to get the number of icons in the file. If no applications are yet associated with the file type. For example: Dim Associate As String = GetSelectedOpenerForExt(FilePath) 'get path tp application associated with FilePath We could then extract an image using the above GetImageFromExeDll() if the association exists. New Rectangle(0.. The time-tested interop P/Invoke that is most-often used to access icons in executables. DLLs.ExtractAssociatedIcon(FilePath). or both.NET Beyond the Scope of Visual Basic 6. or font. For those.DrawImage(bmp. The trick here. if the user had personally selected an application to open a file type with. 'create a new bitmap 'define a graphics interface for it 'draw 32x32 image scaled down to 16x16 'dispose of resources (DO THIS OR LOSE DRAWING!) 'return image 'report failure so default image will be used The other problem we face is that a lot of files do not have icons. But to do that we first need to access the bitmap image of that returned icon. which is a common storage house for many low-level system icons. 16) Dim g As Graphics = Graphics. either 16x16.0 – David Ross Goben However. then if there is a default application associated with it. we should have a default ‘File’ image set aside. Notepad might be associated with a text file (type extension . Technically. compressed files. We can then use the DrawIconEx() P/Invoke to draw an icon to an image (it can also be used to draw a cursor. An example is the Moreicons. or they do not contain icons. document.Dispose() Return bm End If Catch End Try Return Nothing End Function 'grab a 32x32 bitmap of an app's default icon 'if we got something. though using a cursor’s Draw() or DrawStretched() methods are easier).dll in C:\Windows\System32. the best recourse is to do a little more work and instead grab pristine 16x16 images directly from the file. such as icons having dimensions beyond 48x48. If the user has not designated an application to open it.. For example: Friend Function GetImageFromExeDll(ByVal FilePath As String) As Bitmap Try Dim bmp As Bitmap = Icon. 16)) g. but a great many do not. which we can obtain easily using the icon’s ToBitmap property. so in those cases we can access its file association. etc.FromImage(bm) g. Consider these two detailed P/Invoke definitions for ExtractIconEx() and DrawIconEx(): Page –551– . ByVal]. ByVal]. ByRef]. ' 'PARAMETERS: 'lpszFile [in. ByVal]. This parameter can be one of the following values.Zero. Type: Int32: ' The logical height of the icon or cursor. use ' -3 to extract the icon whose resource identifier is 3. ' 'yTop [in. If this parameter is IntPtr. Type: IntPtr() or IntPtr or IntPtr. ' If this value is a negative number and either phiconLarge or phiconSmall is not IntPtr. Private Declare Function ExtractIconEx Lib "shell32. ' 'xLeft [in. Type: Int32: ' The zero-based index of the first icon to extract. no large icons (32x32. ' 'phiconSmall [out.NET Beyond the Scope of Visual Basic 6. This parameter is ignored if hIcon does ' not identify an animated cursor. 48x48) are extracted from the file. ByVal nIcons As UInt32) As Int32 'INTEROP METHOD: --------DrawIconEx-------'Draws an icon or cursor into the specified device context. ByRef phiconSmall As IntPtr. ' 'diFlags [in. ByVal]. if hIcon identifies an animated cursor. Type: IntPtr: ' A handle to the device context into which the icon or cursor will be drawn. Type: IntPtr() or IntPtr or IntPtr. If this parameter is zero and ' DI_DEFAULTSIZE is not used. the return value is 1. ByVal]. the icon is drawn as a mirrored icon Page –552– . If this parameter is zero and the diFlags parameter is DI_DEFAULTSIZE. ' 'nIcons [in. optional.dll" Alias "ExtractIconExA" ( ByVal lpszFile As String.Enhancing Visual Basic . Type: In32: ' The logical y-coordinate of the upper-left corner of the icon or cursor. ' DI_NORMAL (3) Combination of DI_IMAGE and DI_MASK. and then copies the bitmap into the device context identified by hdc. Type: IntPtr: ' The logical width of the icon or cursor.ico file. or icon file from which icons will be extracted. If this parameter is NULL. ByVal]. If this parameter is zero and ' DI_DEFAULTSIZE is not used. ' 'istepIfAniCur [in. or icon file. ' DI_DEFAULTSIZE (8) Draws the icon or cursor using the width and height specified by the system metric ' values for icons. If hbrFlickerFreeDraw is a valid brush ' handle. ' the function returns the total number of icons in the specified file. If this flag ' is not specified and cxWidth and cyWidth are set to zero. and stretching 'or compressing the icon or cursor as specified. If this value is –1 and phiconLarge and phiconSmall are both IntPtr. Type: IntPtr: ' A handle to a brush that the system uses for flicker-free drawing. optional. ' --Value---Meaning-' DI_MASK (1) Draws the icon or cursor using the mask. ' 'hIcon [in. ByVal]. ' 'cxWidth [in.Zero: ' A single IntPtr or a sized IntPtr array for icon handles that receives handles to the small icons extracted from ' the file. ' 'cyWidth [in. Type: UInt32: ' The index of the frame to draw. if the cxWidth and cyWidth parameters are set to zero. DLL. the function uses the actual resource height. if this value is zero. no small icons (16x16) are extracted from the file. ByVal]. performing the specified raster operations. ByVal]. the function uses the actual ' resource size. Type: IntPtr: ' A handle to the icon or cursor to be drawn. Use the GetHdc property from your ' Graphics interface object for this. the system creates an offscreen bitmap using the specified brush for the background color.Zero. optional. ' 'hbrFlickerFreeDraw [in. If the file is an . ' 'phiconLarge [out. If this parameter is zero and the diFlags parameter is DI_DEFAULTSIZE. ByVal]. the system draws the icon or cursor directly into the device context. draws the ' icon or cursor into the bitmap. the function extracts ' the first icon in the specified file.Zero: ' A single IntPtr or a sized IntPtr array for icon handles that receives handles to the large icons extracted from ' the file. If the file is an executable file or DLL. ' 'nIconIndex [in. ByVal]. For example. If ' hbrFlickerFreeDraw is NULL. the function uses the actual resource width. ' the return value is the number of RT_GROUP_ICON resources. ByRef]. By default. This parameter can identify an animated cursor. ' DI_IMAGE (2) Draws the icon or cursor using the image. Type: Int32: ' The logical x-coordinate of the upper-left corner of the icon or cursor. ByVal nIconIndex As Int32. ' DI_NOMIRROR (16) Draws the icon as an unmirrored icon. ' DI_COMPAT (4) This flag is ignored.Zero. ByRef phiconLarge As IntPtr. Type: UInt32: ' The drawing flags. Type: String: ' The name of an executable file. ' the function uses the SM_CXICON system metric value to set the width. ' the function uses the SM_CYICON system metric value to set the width. ByVal]. the function begins ' by extracting the icon whose resource identifier is equal to the absolute value of nIconIndex. DLL. Type: UInt32: ' The number of icons to be extracted from the file.0 – David Ross Goben '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'INTEROP METHOD: --------ExtractIconEx-------' Creates an array of handles to large or small icons extracted from the specified executable file. 'PARAMETERS: 'hdc [in. For example. Zero.Zero.. 16) 'create a new 16x16-pixel drawing surface g = Graphics.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!) If Result Then 'if image was drawn.Zero. in particular the Graphics interface object. Used when extracting icons from executables ExtractIconEx(FilePath.Zero. Index. 0&. hbrFlickerFreeDraw As IntPtr. Return bmp Else bmp.0 – David Ross Goben ' if hdc is mirrored. yTop As Int32. suppose that we have a string variable named FilePath that contains the path to an executable. DI_NORMAL) 'draw icon to bitmap (DI_NORMAL is an Integer value of 3) g. This is where most programmers get into trouble. 16..GetHdc.Dispose() 'otherwise dispose of bitmap resource End If 'and fall below End If End If '--------------------------------------------------------------''failed. 16.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!) Return bm 'return image End If Catch End Try Return Nothing 'report failure so default image will be used End Function Page –553– . bmp = New Bitmap(16.. This issue usually stems from them forgetting to clear resources.FromImage(bmp) 'create a graphics interface for it Dim Result As Boolean = DrawIconEx(g.Enhancing Visual Basic .ToBitmap 'failed. Optional ByVal Index As Int32 = 0) As Bitmap Dim bmp As Bitmap Dim g As Graphics If ExtractIconEx(FilePath.. hIcon. 0. cyHeight As Int32. 16) 'create a new bitmap g = Graphics. hIcon.DrawImage(bmp. 1&) 'icon handle.. 0&. Dim hIcon As IntPtr 'icon handle. Used when extracting a single icon from a file 'get handle of file's first icon (small 16x16) Once we have the handle for the icon. Private Declare Function DrawIconEx Lib "user32" (ByVal ByVal ByVal ByVal ByVal ByVal ByVal ByVal ByVal hdc As IntPtr. so try grabbing a 32x32 icon that is the default associated with the file '--------------------------------------------------------------Try bmp = Icon. 0&.NET Beyond the Scope of Visual Basic 6. xLeft As Int32. 0&. cxWidth As Int32. istepIfAniCur As Int32. hIcon. IntPtr.FromImage(bm) 'define a graphics interface for it g.Zero. 16)) 'draw 32x32 image scaled down to 16x16 g. so try grabbing a 32x32 bitmap of associated icon If bmp IsNot Nothing Then 'if we got something. IntPtr.. we need to copy its icon image to a bitmap/image. IntPtr. IntPtr. To get the number of icons it contains. -1&. IntPtr. hIcon As IntPtr. New Rectangle(0. we would use the ExtractIconEx() method like this: Dim iconCount As Int32 = ExtractIconEx(FilePath. we could use these commands: Dim hIcon As IntPtr ExtractIconEx(FilePath. IntPtr. diFlags As Int32) As Boolean Private Const DI_Mask As Integer = 1 'Draws the icon or cursor using the mask Private Const DI_Image As Integer = 2 'Draws the icon or cursor using the image Private Const DI_NORMAL As Integer = 3 'Combination of DI_IMAGE and DI_MASK 'Private Const DI_COMPATL As Integer = 4 'This flag is ignored Private Const DI_DEFAULTSIZE As Integer = 8 'Draw icon or cursor using default system sizes Private Const DI_NOMIRROR As Integer = 8 'Draws the icon as an unmirrored icon even if HDC is mirrored '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Now..Zero. Consider the following sample method to perform this task: '******************************************************************************* ' Method : GetIconImage ' Purpose : Get an image from the EXE/DLL file icon '******************************************************************************* Friend Function GetIconImage(ByVal FilePath As String. -1&. IntPtr. 16. Dim bm As New Bitmap(16. 0&) <> 0 Then 'if it contains icons.. 1&) 'get handle of file's first or indexed icon (small 16x16) If CBool(hIcon) Then 'if it has a handle.Zero. 0&) To grab the first icon handle from the executable for a 16x16 image.ExtractAssociatedIcon(FilePath). IconFile 'Add image from icon to our image list Case ". I still use the ExtractIconEx() method because images defined at 16x16 are generally much clearer than re-scaled 32x32 images. ExtIdx = Images. if we fail to secure an image. ". Of course. ". ".dib" 'init default Icon image our image list ". We can then do a Select Case and test for various Extensions. 'hide image 'else if it has an extension. we fall back to using the ExtractAssociatedIcon() method to grab a 32x32 image and scale it down to 16x16. such as “. then we return the bitmap. which the invoker can assign to the Image property of a picture box.tiff". ". and 256x256 icons. we should immediately replace the reference to the generic default with a reference to our less generic image defined for that extension type. after disposing of it. the current modified image would be the default. Binary. but we are trying to display icons as 16x16 images in a TreeView. ". Icons/Images.zip". If the drawing succeeded (its result was True). audio.avi". any invalidation of its surface (violation or changes) is ignored and the current changes become permanently etched into the bmp drawing surface..MovieFile 'Add default or associated image from the file to Case ". by simply disposing of it. Sometimes that is all we need to do. Of course. if an extension exists. we dispose of the Graphics Interface object’s resources.SoundFile 'Add default or associated image from the file to Case ". if we were to create a brand new graphics interface for it.None ElseIf Not String. "wmv". This is fine and it is also simpler..pic".FontFile Case ". ". ExtIdx = Images. extract a cursor image.mov". grab an icon from an associated executable. because if you do not dispose of it.iso" ExtIdx = Images. For example..NET Beyond the Scope of Visual Basic 6. Fonts.txt”. or whatever we can do to graphically represent the file. ". this can all fail if we are trying to deal with Vista-style icons that contain 64x64. If we find an extension match. (Generic) Files. Extracting those requires much more work.wav". Indeed. so we should not have to worry about them.Path.ico" ExtIdx = Images. now that you know how to rescale them from the 32x32-pixel original it returns to us. Zipped.cur" ExtIdx = Images.mp3". we must try to extract an icon from them. We then create a Graphics Interface object for it named g.jpeg".CursorFile Case ". ".System)) <> 0 Then ExtIdx = Images.7z". ". I added default 16x16 images for Cursors.gif".CursorFile 'Add default or image from the cursor file to our Case ".Hidden Or FileAttribute. ". ". TrueType Fonts. 'check extension 'ICON 'init default Icon image ". RegEditing.ttf" 'if this is not an acceptable file. If that also fails.aac". and I will not deal with it here.jpg". we use the DrawIconEx() P/Invoke to draw the icon image to the bitmap.bmp". consider this extension testing prototype: If (GetAttr(FilePath) And (FileAttribute. Getting a file’s extension is easy: “Dim Ext As String = IO. This is where most everyone gets into trouble. such as for fonts and compressed files. As we parse each file in a folder. So.GetExtension(FilePath).ani" ExtIdx = Images. 128x128.chm". video.swf" 'init default Movie image our image list 'compressed file 'init default for compressed file our image list 'audio file 'init default Sound image our image list 'animated cursor 'set default image (do nothing else) 'cursor 'init default cursor image image list 'font 'set default image (do nothing else) 'TrueType font Page –554– . ".. Following that. which is File. Indeed. we start with an absolute generic default image.flv". DOS. then we create a destination bitmap surface that is 16x16-pixels. If it failed. ". Exes.ZipFile 'Add default or associated image from the file to Case ". many programmers simply forgo the time-tested ExtractIconEx() P/Invoke and use the Icon object’s ExtractAssociatedIcon() method exclusively. we dispose of the bitmap image resource and return with Nothing.mpg". the GDI drawing will be lost because the graphics object is still managing it before the Garbage Collector can get to it.bic". Next. This is how the old AutoRedraw featured worked in a VB6 PictureBox. ". ".IconFile 'Add default or associated image from the file to Case ".wma" ExtIdx = Images. ".asf". Otherwise.0 – David Ross Goben If the ExtactIconEx() functions returns a valid icon handle (hIcon <> 0). ". In my image list. ".Enhancing Visual Basic . we will need to fall back on a default image to display. and Log applications. This will return the extension with a leading dot.IsNullOrWhiteSpace(Ext) Then Select Case Ext Case ".ToLower”. DLLs.mpeg". ". We then grab the extension of each file and look for specific matches.pic".png".fon" ExtIdx = Images. oca". much of it should already be familiar because we have used a similar list previously: 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE 'Enumerator: Images 'Purpose : Reference to images in the ImageList 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE Friend Enum Images As Int32 None = -1 '---. but instead grab the index to the image for that file already stored in the collection.Folder Images FolderCanOpen FolderIsOpen FolderSide FolderSideShortcut '----.LogFile 'set default image 'Add default or associated image from the file to our image list Case ".log".TtfFile 'set default image Case ". if it already exists in the list. ".BinFile 'set default image 'Add default or associated image from the file to our image list Case ". Notice. ". As you can also see. Command file ExtIdx = Images. but we will list those shortly.bat". ". when a new image is generated.Logical Drive Types Fixed CDRom Removable Ram Network '---. It will contain the list of our default general images.ini".ExeFile 'set default image (do nothing else) Case ". They are a wrinkle that we will deal with. but for a moment we will focus on normal files.exe" 'executable ExtIdx = Images. ". default image types are referenced through an enumeration named Images. it should not re-add a duplicate image. ".nls" '----Binary/Data file ExtIdx = Images. ". What is important is the ExtIdx Integer variable.Enhancing Visual Basic .DllFile 'init default EXE image (faster if we use the default) 'Add default or image from the file to our image list Case ".NET Beyond the Scope of Visual Basic 6.cfg" '----------------------------LOG/INI file ExtIdx = Images.dep". though.0 – David Ross Goben ExtIdx = Images.RegFile 'set default image (do nothing else) Case ".reg" '--------------------------------------------Registry file ExtIdx = Images. as demonstrated in our last Back Book Tip (you will soon be provided with the definitions for all these images). ". Notice also that the above extension tests are general and you may want to amend them to include a favorite format. ". much as we have used previously.Mark end of this list _LastItem End Enum This enumeration will also provide image indexing support for a Drive Selector ComboBox and TreeView folders.Default File types (NEW STUFF) CursorFile ExeFile FileFile FontFile TtfFile IconFile ZipFile MovieFile SoundFile DllFile DosFile RegFile BinFile LogFile '---.dat". However.com" 'Batch.ExeFile 'init default EXE image 'Add default or image from the file to our image list Case Else 'process undetected types 'Add default or associated image from the file to our image list End Select End If This coding prototype will invoke a number of yet unnamed methods. this variable will be updated to the index to the new image added to the list.bin". plus any we choose to add to it as we process files.ocx". Page –555– .Virtual Drive Types Desktop Documents Downloads Music Pictures Videos Recent '----. which is an index into an ImageList repository for various images collected from files.dll" '--------------------------------------------DLL ExtIdx = Images. that Link files (shortcuts) are ignored here. This is a simple list. However. the only real snag you can run into when processing images for files are Link Files. This brings us to another issue.lnk” extension to the path to flag them). 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. or better known as Shortcut Files. or renamed.Images.lnk” file extension.Images. ShortcutTag should actually be defined as a field in the header of our program. what of additional images that we want to have a shortcut tag painted on them? This is easy to deal with.Add(Img) 'then add the image to the ImageList at that index End Sub Notice we do not paint to Img’s image object. We next assign the file a new index by grabbing the Imagelist’s current image count.myImages. Were we to draw to the Img object. moved. when matches are found.DrawImage(ShortcutTag. We can paint this small image to the bottom left corner of a 16x16-pixel image. we dispose of our copy’s graphics provider before assigning it to the image (Image and Bitmap types are identical). provided in Black Book Tip # 3 on page 351 (Get the Linked File Path from a Shortcut File). Dim bmp As New Bitmap(Img. ByVal IsLink As Boolean..Count 'update associated extension index to image index Me. we can extract the index of the image stored with the filepath in the SortedList. add a tag '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub AddRealImage(ByRef Img As Image. What adds a bit of complexity is the fact that a Link files can point to a file or to a directory folder. By storing files paths from which images were added. Consider the AddRealImage() method that will paint a shortcut tag to a copy of an existing image: '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : AddRealImage (Support) ' Purpose : Add an image to the ImageList and set the image index. but we replace it with a new Image. For example: The tag is actually a small 8x8-pixel image. You would not want to fill the ImageList up with a lot of identical images for your associated application. For example. replacing the orginal Img object.NET Beyond the Scope of Visual Basic 6. instead of storing a new image to the ImageList. its Graphics provider continues to exist. After considering the creation of a class. Additionally.0 – David Ross Goben As mentioned. New Rectangle(0.Dispose() 'dispose of graphics resource (DO THIS OR LOSE IMAGE) Img = bmp 'update the referenced image End If ExtIndex = Me. and to include those with shortcut tags added (just add a “. Also. we have already used ConvertBase64ToImage() in recent Black Book tips. sometimes their target path no longer exists because the target may have been deleted. 8)) 'draw shortcut on bitmap in its bottom-left corner g. Painting this is also very easy and is much like we had painted a 32x32-pixel icon image to a 16x16-pixel bitmap. and then append the image to the list. which is how to prevent adding a ton of duplicate images to the list. We can get the file this link actually points to by using the short method GetShortcutLinkToPath(). it would be nice to tag them with a little “shortcut tag” in our list. which have a typically hidden “. Also. If it is a link. Also. Using its IndexOfKey() method we can check for duplicates. which also inserts the image at that index. to include the last one. Page –556– .myImages. except that we will not rescale and we will use a target rectangle pointing to the lower-left corner of the bitmap. This part is no big deal.Enhancing Visual Basic .. opening a folder full of text files that are all associated with Notepad or your favorite editor. 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. 8. 16) 'create a copy of the provided image Dim g As Graphics = Graphics.FromImage(bmp) 'get graphics interface for it g. 8. 16. And. I realized that a simple SortedList collection could easily provide this service. we can detect previous additions. Therefore. ByRef ExtIndex As Int32) If IsLink Then 'if we are processing a shortcut link. marking where new file images can be appended to the list. even though the method expects it to be passed ByRef. and the images from executables associated with files. Once done. technically. (Idx)) 'add it as a shortcut-tagged version (pass Idx ByVal. In the coding prototype listing shown earlier. not ByRef) Next AddedBase = Me. Idx.0 – David Ross Goben The great thing with a sorted list is that we do not even need to attach it to the form (well. ByRef ExtIndex As Int32) As Boolean FilePath = FilePath. ByRef ExtIndex As Int32) Dim Associate As String = GetSelectedOpenerForExt(FilePath) 'is there an exe associated with this file? If Not String. or ' : the path already is registered.NET Beyond the Scope of Visual Basic 6. It stores the lowercase app filepath as its key. We first load our ImageList with default images.. in the heading of our form code.myImages.ToLower 'make sure it is lowercase Dim fp As String = FilePath 'init a match of FilePath If IsLink Then 'is it a link file? fp &= ". which would mess up our loop indexing.Count 'set index base for any addl. This way the copy will be altered instead and it will be thrown away on return.IsNullOrWhiteSpace(Associate) Then 'if there is an associated file. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : AddImageFromFile ' Purpose : Adds an image from the file and update the Item's image index. and its image index as the list’s value member. Using this can bypass the need to parse the same file extension again and again for an association. This is because we do not want the method to alter our Idx variable.lnk" 'yes.DosFile 'duplicate CursorFile to DosFile with a shortcut version Img = New Bitmap(Me. 16.myImages. we could add these declarations: Private Private Private Private myImages As New ImageList ImgSrcList As New SortedList(Of String. 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. It stores the extension as the key and the full path to the associated application as its value member.Images. the Index parameter. used to store a list of files we extracted an image from and their ImageList index. ByVal IsLink As Boolean. to avoid reparsing them time and again. images referenced in ImgSrcList collection Notice in the invocation to AddRealImage(). AddedExtList is yet another SortedList.Enhancing Visual Basic . We can even E-Z clone objects this way). adding the parentheses causes it to be treated as an expression. This would include not only EXEs and DLLs. All we need to do is maintain a reference to the object. Later. which will be used keep track of file extensions that an associated application has been found for. so append a . was actually passed ByVal. so set the image index to it '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function AddImageFromFile(ByVal FilePath As String.. ByVal IsLink As Boolean. cursors. 16) 'create a duplicate of an existing image AddRealImage(Img. so we embraced it within parentheses to actually pass a copy (a clone) of that variable ByRef to the method. if one exists '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub AddAssociatedImage(ByVal FilePath As String. but icons. we can add shortcut tagged versions for our default images. if our ShortcutTag Image variable has been assigned its image: '--------------------------------------------------------------------------------'make shortcut versions of defaults '--------------------------------------------------------------------------------For Idx As Int32 = Images.Images(Idx). True. 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. 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.lnk extension to the copy End If Page –557– . String) AddedBase As Int32 'ImageList control to store images in this form 'keep track of file paths from which we extracted images 'keep track of file extensions with application associations 'index offset for file icon images MyImages is our ImageList that will maintain all our images. For example. rather than altering the actual Idx variable (technically. ExtIndex) 'add associated image End If 'if this fails. This can be achieved by invoking one of two methods: AddAssociatedImage() and AddImageFromFile(). we will enhance it so that extensions that do not have any app association can be added to the list. we indicated that we add images from the files or from associated files. ImgSrcList is a SortedList.CursorFile To Images. AddImageFromFile(Associate. The AddedBase integer variable will contain an index within the myImages ImageList beyond the default images. Int32) AddedExtList As New SortedList(Of String. we could do the same for a sorted ListBox). For example. still to be covered. IsLink. IconFile 'init default Icon image AddImageFromFile(FilePath. ".Item(FilePath) 'otherwise.. ". ".Path.Images.flv".Add(fp.tiff". IsLink. ExtIndex = ExtIndex + Images.Dispose() 'dispose of graphics resource Img = bmp 'update the referenced image End If ExtIndex = Me.bic".AddedBase 'compute existing image index Return True 'indicate an item already exists Else Return False 'otherwise indicate the item does not yet exist End If End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : AddRealImage (Support Method) ' Purpose : Add an image to the ImageList and set the image index.MovieFile 'init default Movie image AddAssociatedImage(FilePath..._LastItem . ByRef ExtIndex As Int32) As Boolean Dim Index As Int32 = Me...GetExtension(FilePath). Dim Img As Image 'init image to set and assign to the list If IO. ".swf" '--------------------VIDEO ExtIdx = Images..ImgSrcList.. add a tag '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub AddRealImage(ByRef Img As Image.jpg".mpeg". Img = GetCursorImage(FilePath) 'then load the cursor's image Else Img = GetIconImage(FilePath) 'else load the image from a file (EXE.png". ". ExtIndex) 'then add it Return True ElseIf IsLink AndAlso ExtIndex < Images. Use ExtIndex value End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : CheckAddListForFilePath (Support Method) ' Purpose : Return True and update invoker's Extension Index if the path already exist in the list.IsNullOrWhiteSpace(Ext) Then 'else if it has an extension defined. ". ExtIdx) 'grab first icon from file and update image Case ". ExtIdx) 'grab associated file icon and update the image list Case ". 16..gif". AddRealImage(Img. "..avi".zip".NoMatches Then 'if a match was found.mpg". 8. IsLink. ". ". If it is a link. ".bmp". ExtIdx) 'grab associated file icon and update the image list Case ". ExtIndex) 'if an image was added.myImages. like so: If (GetAttr(FilePath) And (FileAttribute.Enhancing Visual Basic .iso" '--------------------compressed files (ZIP) ExtIdx = Images.ico" '--------------------------------------------ICON ExtIdx = Images. ".jpeg".pic". 16) 'create a copy of the provided image Dim g As Graphics = Graphics.asf"..ZipFile 'init default for compressed file (do nothing else) AddAssociatedImage(FilePath.0 – David Ross Goben '--------------------------------------------------------------If Me.System)) <> 0 Then 'if this is not an acceptable file.wma" '--------------------AUDIO Page –558– . ByRef ExtIndex As Int32) As Boolean If Img IsNot Nothing Then 'if image is valid. ' : If False.pic". "...ImgSrcList. Dim bmp As New Bitmap(Img.mov". 8)) 'draw shortcut on bitmap in its bottom-left corner g.Images. IsLink.None 'hide image IsLink = False 'make sure flag is reset ElseIf Not String. DLL. grab assigned index End If 'if this fails.CursorFile 'shortcut rendition of base icon End If Return False 'no image added.chm".ImgSrcList. then the associated image can be added '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function CheckAddListForFilePath(ByVal FilePath As String.mp3".IndexOfKey(FilePath) 'check for the item already existing in our list If Index <> ListBox. Icon) End If If AddImage(Img.._LastItem Then 'else compute the index of the. ".cur" Then 'if it is a cursor.myImages.ToLower. then add fp to ImgSrcList Return True 'and indicate an image was added End If Else ExtIndex = Me. ". ".FromImage(bmp) 'get graphics interface for it g. ".IndexOfKey(FilePath) = -1 Then 'if the item does not exist in our list.NET Beyond the Scope of Visual Basic 6. ExtIdx) 'grab associated file icon and update the image list Case ". add a tag '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function AddImage(ByRef Img As Image. ExtIdx = Images.Count 'update extension index in image list Me.wav".ImgSrcList. 8. ExtIndex = Index + Me. ". ".. ByVal IsLink As Boolean.7z"..aac".Hidden Or FileAttribute.Images.dib" '------------------IMAGE ExtIdx = Images.IconFile 'init default Icon image AddAssociatedImage(FilePath. Select Case Ext 'check extension Case ". ". If it is a link. IsLink.DrawImage(ShortcutTag. we can begin fleshing out our previous prototype extension testing code. ByRef ExtIndex As Int32) If IsLink Then 'if we are processing a shortcut link. 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. IsLink. IsLink. ". ExtIndex) Then 'update the imagelist if an image was grabbed Me. "wmv". ByVal IsLink As Boolean.ToLower = ". New Rectangle(0. "..Add(Img) 'then add the image to the image list End Sub With these methods. ExtIdx) 'grab associated file icon and update the image list Case "..... ExtIdx) 'grab first icon from file and update the image list Case "..None Then 'if we have an image reference..tvDirTree Dim dirNode As TreeNode = parentNode. is set to True if it is in fact a linked file.GetExtension(linkedPath)..Exists(linkedPath) Then linkedPath = Nothing End If End If If linkedPath IsNot Nothing Then IsLink = True LnkPath = FilePath FilePath = linkedPath Ext = IO. if we have a linked file. ExtIdx) 'grab first icon from file and update the image list Case Else '----------------------------------------------process undetected types AddAssociatedImage(FilePath.com" '------------------------------------Batch...Exists(linkedPath) Then linkedPath = Nothing ElseIf Not IO... IsLink....FileFile Dim Ext As String = IO...reg" '--------------------------------------------Registry file ExtIdx = Images. ".......ToString..If ExtIdx <> Images. 'mark it as a link 'save full path to lnk file 'let file assume linked Path for now 'get the actual target file's extension 'ignore if linked path is nothing .... LnkPath = FilePath 'simplify the code below by assuming it is End If With Me.DllFile 'init default EXE image (faster if we use the default) AddImageFromFile(FilePath.0 – David Ross Goben ExtIdx = Images. ExtIdx) 'grab cursor image from file and update the image list Case ". ExtIdx) 'grab associated file icon and update the image list Case ".....lnk" Then Dim linkedPath As String = GetShortcutLinkToPath(FilePath) If linkedPath IsNot Nothing Then If IO. IsLink.File.. ". a Boolean flag.....nls" '----Binary/Data file ExtIdx = Images..Nodes.cur" '--------------------------------------------CURSOR ExtIdx = Images. IsLink. "... IsLink.. IsLink..ttf" '--------------------------------------------TrueType font (TTF) ExtIdx = Images. 'kill linked path 'if it is a file and it no longer exists... ExtIdx) 'grab associated file icon and update the image list Case "..Enhancing Visual Basic .GetNodeCount(False)...bin". 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.ToolTipText = FilePath 'save its path as its tooltip End With End If '.'add a file node to our TreeView.GetExtension(FilePath).GetFileName(LnkPath)....ocx".log". ExtIdx) 'grab associated file icon and update the image list End Select End If '.. ".BinFile 'set default image AddAssociatedImage(FilePath.ExeFile 'init default EXE image AddImageFromFile(FilePath. IsLinked..Path.cfg" '-----------------------------LOG/INI file ExtIdx = Images. IsLink.......CursorFile 'set default image (do nothing else) Case ". Also. If Not IsLink Then 'if this is NOT a linked file.. "....dat". ExtIdx) 'add as new file node w/appropriate index dirNode....CursorFile 'init default cursor image AddImageFromFile(FilePath. We can pre-process the files and check for linked files by preceding the above with this block of code: IsLink = False Dim ExtIdx As Int32 = Images.Directory. 'kill linked path 'if a valid file path was returned.dep"... IO..ini".....Add(parentNode... ExtIdx.. IsLink. ". 'if it is actually a directory folder...FontFile 'set default image (do nothing else) Case ".. tvDirTree '.oca"....- Notice that we also appended code to add a node to the TreeView in the listing.... "..Path.....RegFile 'set default image (do nothing else) Case ".. We also would have to maintain a reference to the link file itself.. But as you see from the above.......ani" '--------------------------------------------animated cursor ExtIdx = Images. Command file (DOS) ExtIdx = Images..exe" '--------------------------------------------EXE ExtIdx = Images.NET Beyond the Scope of Visual Basic 6..ExeFile 'set default image (do nothing else) Case "...None Ext = Nothing End If End If Page –559– 'init flag to not being a link 'init default image to File 'get the file extension 'if this is a link file.Path.ToLower Else ExtIdx = Images. ".fon" '--------------------------------------------FONT ExtIdx = Images. which is provided by the string variable LnkPath..SoundFile 'init default image AddAssociatedImage(FilePath.TtfFile 'set default image Case "..ToLower If Ext = ".bat"...dll" '--------------------------------------------DLL ExtIdx = Images.. 'get the link's path to its target 'if the linked path exists (should).LogFile 'set default image AddAssociatedImage(FilePath. release any restrained paint events Me.lnk")). we will check the _pClosing flag and exit any and all loops as soon as we can. within our DirRecurse() directory scanning. When processing just the link files. such as “Private _pClosing As Boolean = False” in our form. We can store the references to any linked folders in a List Collection. such as displaying content after the expand. and then in our Closing() event do this: '******************************************************************************* ' Method : Form_FormClosing ' Purpose : Prepare form '******************************************************************************* Private Sub Form_FormClosing(sender As Object. as we have just covered.Directory. It is a simple 10ms timer that gives the display enough time to come up before setting focus onto the TreeView control.NET Beyond the Scope of Visual Basic 6.0 – David Ross Goben We can then wrap the above two blocks of code within a body that parses a string array named Files(). with all files (Files = IO.Enhancing Visual Basic . Page –560– .Enabled = False 'turn off any timers Me.Enabled = False Me.Enabled = False 'disable the timer Me.. End If End Sub Then. and then start a 10ms timer to let things settle before actually closing the form._pClosing = True 'flag that we want to close the form e. e As FormClosingEventArgs) Handles Me.Zero) 'yes. We can parse them to find all actual folders whose linked paths still exist.tmrScanning.lnk file when IsLink is set to True 'parse each file in folder. Once we turn the DirRecursing flag off. and then triggering an AfterExpand() event. The pPath string variable contains the parent node’s folder path.Enabled = False Me. and that is that while parsing a folder from the BeforeExpand() event in a TreeView. The tmrScanning timer is used while parsing massive directories.Cancel = True 'but cancel the actual form close for now. e As EventArgs) Me.tmrEnd. we will test for those that only refer to directory folders.tmrEnd. One other thing we have to look into is something that has driven some programmers batty.. This error occurs because the application is dependant on a sequence of processes within the TreeView..Close() 'then close shop End Sub The other timers in the above list are useful as well.tmrStart.DirRecursing = False 'beat a dead horse to ensure this is disabled Me.FormClosing If Me. What this means is that we actually need to process the file list twice._pClosing = False 'turn off our flag Me. the program will hang and not exit if the user tries to exit the application. The first time we can fill it with just link files (Files = IO. We can declare a global Boolean flag.GetFiles(pPath)). and then second time. The solution to this issue is to cancel the application exit in the FormClosing() event. we can enable the timer tmrEnd. such as by hitting the “X” button before the event has completed.. We now have files taken care of..DirRecursing Then 'are we recursing a directory branch? LockWindowUpdate(IntPtr. which is a listing of all files retrieved from a selected directory path: Dim IsLink As Boolean Dim LnkPath As String = Nothing For Each FilePath As String In Files '------------------------------'Above FilePath test code goes here. and afterward add the collected linked folders to the TreeView.Directory.GetFiles(pPath. "*. '------------------------------Next 'FilePath 'set to True if a file is a shortcut link 'path of . tmrStart is activated at the end of the Form’s Load event. which is set before we invoke scanning.. 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. but directory folders can also have shortcuts. which you will soon see. Without the arcane knowledge of File Explorer. which will close the file: '******************************************************************************* ' Method : tmrEnd_Tick ' Purpose : make sure TreeView has focus '******************************************************************************* Private Sub tmrEnd_Tick(sender As Object. for example. so that we are not wasting time trying time and again to find a file associated with it. Technically. Dim Assoc As String = Me.DllFile 'init default EXE image (faster if we use the default) 'AddImageFromFile(FilePath. An update to the AddAssociatedImage() method is: '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : AddAssociatedImage ' Purpose : Add an image from a file associated with the specified file. One example that can double the scanning speed is to use the default image for DLLs. If AddImageFromFile(Assoc. and if it was added. 1) = "-" Then 'is it actually a negative integer? ExtIndex = -CInt(Assoc) 'yes.. there are some things we can do to really speed things up. IsLink. ExtIdx) 'grab first icon from file and update the image list 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. Page –561– .NET Beyond the Scope of Visual Basic 6. IsLink. so grab it as a default extension index Else ExtIndex = Me. we assign ExtIndex to the positive of the negative value. though you are free to simplify it. Assoc..lnk" ' 'if FilePath is a Link then set additional . or if the associated file does not have an icon we can access.dll” file extension from the above file testing code: Case ".dll" '--------------------------------------------DLL ExtIdx = Images. anyway. we can actually be assigning a value to ExtIndex that it might already contain. This thing contains a lot of cool tricks.. It will also establish a minimum size in case you went insane and sized it to fit within a postage stamp. but what takes time is extracting truckloads of images for the various files.Add(Ext. all in a single source code file.IsNullOrWhiteSpace(Assoc) Then 'if there is an associated file.AddedExtList.ToLower 'check for an app associated with this file If Not String. (-ExtIndex). so add the default extension as a negative number End If End Sub In the above code. and turns its Embed Interop Types option to False. You may want to resize the form to start with a bigger surface display. unless you are looking at something with thousands of files.ImgSrcList(Assoc) 'else grab link image index associated with it End If Else Dim Assoc As String = GetSelectedOpenerForExt(Ext).ToString) 'failed.lnk extension If Me.0 – David Ross Goben You will seldom see tmrScanning in action. even though it will already know in a jiffy how many files there are in a folder. We can do that by simply disabling the invocation of AddImageFromFile() when we detect a “.ToLower & x) 'then add the extension to to association list Return End If End If 'if this fails. Even so. yet another SortedList Collection. and then add a COM reference in your Project Properties to Microsoft Shell Controls and Automation. we will use the default image Me.Item(Ext & x) 'grab associated file or ExtIndex reference If Assoc. much as we did in the last Black Book Tip. All you need to do is create a new Windows Forms Application.ToLower 'get FilePath extension Dim x As String = String. We can do this in the AddAssociatedImage() method by ensuring that if an association is not found.Empty 'init to no additional extension If IsLink Then x = ".Enhancing Visual Basic . that we will use the list without going through that process again.Add(Ext & x. 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.. overwrite its empty Form1 class shell on Form1’s code page with the file following this paragraph. but I have been toying with some alternate ideas that could take advantage of this place in the code..AddedExtList. Then. we assign it the negative value for the default image associated with that extension. if one exists '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub AddAssociatedImage(ByVal FilePath As String.Substring(0. which is why it will run slightly faster than VB. we add it to AddExtList.IndexOfKey(Ext & x) <> -1 Then 'if the extension list contains a matching extension. 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.. Another thing we can do that can triple the program’s speed is to run it without debug (Cntrl+F5. such as C:\Windows\System32. Finally. This is off by default in C#. based on how you sized your form. ByRef ExtIndex As Int32) Dim Ext As String = CleanExtension(FilePath). which includes supporting classes and modules. in the code above it. ByVal IsLink As Boolean. but instead of assigning it a path to the file that contains the associated image.AddedExtList. so I will leave it intact. Me. What follows is the complete form code. This code will create all its controls and place them properly on the form.AddedExtList. ExtIndex) Then 'then add associated image. if we have an extension that has no associated image to assign to it. or you can add a button for Run Without Debug in to your toolbar). if a match is found for the extension in the list. Only one window can be locked at a time. Type: IntPtr ' The window in which drawing will be disabled. assign it using: MeCursor = NewCursor. so set cursor to the main form.0 – David Ross Goben Option Explicit On Option Strict On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' frmTreeViewTest Form Class ' TreeView Demonstrations with folders and file icon images ' ' On a new form..Cursor <> value Then 'is the form cursor already set to this? Me.Cursor = value 'no. C:\ from C:\Windows\System32. '--------------------------------------------------Private AddedBase As Int32 'base offset to top of default iamges. ' If the function fails. 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. including images extracted from the files. ' : We will use this because SuspendLayout will not stop the treeview ' : display from clearing when it is being reset.HasChildren Then 'and if it also has children. the return value is zero (False). 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Private Property MeCursor As Cursor Get Return Me. ByVal].Controls.DLL" Alias "LockWindowUpdate" (ByVal hWndLock As IntPtr) As Boolean 'Parameters: ' hWndLock [in. Used when recursing and the user tries to close the form. indicating that an ' error occurred or another window was already locked. drawing ' in the locked window is enabled.Zero). 'Return value ' If the function succeeds..NET Beyond the Scope of Visual Basic 6. ' 'The program will crash and loop enlessly if a folder is opening ' 'then and the user tries to close the form. If this parameter is NULL (IntPtr. '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Public Class frmTreeViewTest 'make sure this name matches the form you drop this code into '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ ' DECLARATIONS '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private _StartPath As String = "C:\" 'path to actually focus on. This will prevent that.. 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 images 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 for updating folder/file reports Private tmrStart As Timer 'startup timer. SetCursorToChildren(Me. A ComboBox is provided for ' selecting logical and virtual drives.Enhancing Visual Basic . If Me. ' : Note also that you can assign from the Cursors collection. String) 'keep track of file extensions with application associations Private ImgSrcList As SortedList(Of String. '******************************************************************************* Private Declare Function LockWindowUpdate Lib "user32. or load a cursor from resources. depending on if a root folder was specified Private _Root As String 'root of start folder path. value) 'then set its child controls as well End If End If End Set End Property '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : SetCursorToChildren Page –562– . Used to set focus to treeview on startup Private tmrEnd As Timer 'closing timer. where file images can be appended 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 '--------------------------------------------------'******************************************************************************* ' Method : LockWindowUpdate ' Purpose : The LockWindowUpdate function disables or enables drawing in the ' : specified window. '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : MeCursor ' Purpose : Set cursor to more than just the main form ' Usage : When assigning a new cursor to the form.. ' : Unlike the UseWaitCursor property. the return value is nonzero (True). for example Private _pNode As TreeNode = Nothing 'base node of recursive scan. You can afterward reasign child control cursors.Cursor 'return the form's current cursor setting End Get '-----Set(value As Cursor) If Me. add all the components to display a TreeView directory structure of ' a Drive. NET Beyond the Scope of Visual Basic 6.lblFoldersHdr.ShowIcon = False 'hide any icon on the form .Text = "Subfolders:" 'assign its display text End With '------------------------------------------------------Me.Left Or AnchorStyles.Left = EdgeOfst + Me.lblFoldersHdr .Left Or AnchorStyles.Top 'align top with previous labels .Text = "Sample Directory Browser With File Icons" 'give the form a title .Top = Me. True) 'initialize ImageList (minimal init) '------------------------------------------------------'Set up report fields '------------------------------------------------------Me. displays "Files:" at bottom of screen With Me.Cursor = Value 'set its cursor If Cntrl.HasChildren Then 'does it have child controls? SetCursorToChildren(Cntrl. True.ImgSrcList = New SortedList(Of String.myImages.Text = "Files:" 'assign its display text End With '------------------------------------------------------Me.lblFoldersHdr.tmrScanning .Top 'align top with previous labels .Anchor = AnchorStyles.Left = 120 '1-inch from left .Top = Me.Bottom 'anchor bias to the bottom-left forner of the form .Anchor = AnchorStyles.Enabled = False 'make sure presently disabled AddHandler .Bottom 'anchor bias to the bottom-left forner of the form . Value) 'process its child controls End If End If Next End Sub '******************************************************************************* '******************************************************************************* ' Method : Form_Load ' Purpose : Prepare form '******************************************************************************* '******************************************************************************* Private Sub Form_Load(sender As Object. e As EventArgs) Handles MyBase.AutoSize = True 'allow label to auto-size . Int32) 'keep track of file paths from which we extracted images '------------------------------------------------------'Set up the ToolTip control '------------------------------------------------------Me.myImages = New ImageList 'ImageList control to store images in this form InitializeImageList(Me.ToolTips = New ToolTip 'tooltip control '------------------------------------------------------'Set up the image list '------------------------------------------------------Me.Parent = Me 'allow display of control by assigning it to a displayed component .lblFilesHdr .Left Or AnchorStyles.lblFoldersHdr.Visible Then 'is the control visible? Cntrl.0 – David Ross Goben ' Purpose : Support MeCursor property ' : Set parent's cursor also to Child controls '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Sub SetCursorToChildren(ByRef ControlList As Control.lblFoldersHdr = New Label 'Folder Header Label.MinimumSize = New Size(400.Height 'place at the bottom of the form .Parent = Me 'allow display of control by assigning it to a displayed component .Load With Me .Parent = Me 'allow display of control by assigning it to a displayed component .ClientSize.SizeGripStyle = SizeGripStyle.Controls. AddressOf tmrScanning_Tick 'add handler End With Page –563– .ControlCollection.Show 'show a sizing grip in the B-R corner End With '------------------------------------------------------'Set up the lists that will keep track of files and extension associations from which icons have been added '------------------------------------------------------Me.Text = "0" 'this will be updated to the actual subfolder count End With '------------------------------------------------------Me.AutoSize = True 'allow label to auto-size .Bottom 'anchor bias to the bottom-left forner of the form .lblFolders .tmrScanning = New Timer 'timer for updating folder/file reports With Me. 300) 'prevent user from sizing form too small . displays "Folders:" at bottom of screen With Me.AutoSize = True 'allow label to auto-size .Anchor = AnchorStyles.Left Or AnchorStyles.Parent = Me 'allow display of control by assigning it to a displayed component .lblFilesHdr..Width 'set to right of above label .Left = EdgeOfst 'offset from left edge of client area of form . ByRef Value As Cursor) For Each Cntrl As Control In ControlList 'process all child controls If Cntrl.Bottom 'anchor bias to the bottom-left forner of the form .Left = 120 + Me.Top 'align top with previous label . String) 'keep track of file extensions with application associations Me.Text = "0" 'this will be updated to the actual subfile count End With '------------------------------------------------------'Set up Scanning Report Timer '------------------------------------------------------Me.Top = Me.lblFoldersHdr.Anchor = AnchorStyles.AddedExtList = New SortedList(Of String.lblFolders = New Label 'subfolder count display With Me.lblFiles .Height .Width 'set to right of above label .AutoSize = True 'allow label to auto-size .lblFilesHdr = New Label 'Files Header Label.Top = Me.Enhancing Visual Basic .Tick.lblFiles = New Label 'File count display With Me. DropDownStyle = ComboBoxStyle.tmrScanning. AddressOf tmrStart_Tick .Width = Me.cboDrives..tmrEnd AddHandler ..OwnerDrawAll 'we want to draw overything 'add event handlers AddHandler .tmrEnd = New Timer With Me.ItemHeight = 17 'allow for 16x16 pixel icons with a 1-pixel gap ._Root = RemoveSlash( IO.Left Or AnchorStyles. AddressOf cboDrives_DropDownClosed End With '------------------------------------------------------'Set up tvDirTree TreeView control '------------------------------------------------------Me.tmrStart AddHandler .Enabled = False 'start the startup timer End With '------------------------------------------------------'Set up Startup Timer control '------------------------------------------------------Me.DirRecursing Then 'are we recursing a directory branch? LockWindowUpdate(IntPtr.Parent = Me 'define control's parent . .cboDrives PopulatecboDrives() 'populate the cboDrives list.AfterCollapse.Anchor = AnchorStyles.Tick.. AddressOf cboDrives_SelectedIndexChanged AddHandler .FormClosing If Me.AfterSelect.GetDirectoryRoot(Me..HideSelection = False 'do not hide selections ._StartPath)) 'get the Root from the path.Directory.cboDrives.Enabled = False 'turn off any timers Me.DropDownClosed. Release any restrained paint events Me.DrawItem. less trailing backslash With Me.Left = EdgeOfst 'align treeview horizontal position . AddressOf tvDirList_BeforeExpand AddHandler .cboDrives = New ComboBox With Me.Width .DropDownList 'avoid user typing into CBO's text box Me.cboDrives.Right Or AnchorStyles.ToolTips.Height = Me.Top = Me.ImageIndex = Images. For Each di As DriveItem In .tvDirList = New TreeView With Me.Bottom 'anchor bias to all four sides ..DrawMode = TreeViewDrawMode.ImageList = myImages 'make sure our ImageList is attached .cboDrives..Parent = Me 'establish this control's parent .Top 'set anchoring ._pClosing = True 'flag that we want to close the form e.Top + Me.SetToolTip(Me.Enabled = True 'start the startup timer End With End Sub '******************************************************************************* ' Method : Form_FormClosing ' Purpose : Prepare form '******************************************************************************* Private Sub Form_FormClosing(sender As Object.Width 'match combobox width .Zero) 'yes.Drive.Width = Me.BeforeExpand..tvDirList .EdgeOfst 'fill reset of form .cboDrives .EdgeOfst * 2 'fill width of form with spacing .SelectedIndexChanged. AddressOf tvDirList_AfterSelect AddHandler .tmrStart.Items 'then find the desired Root drive in the list If di. AddressOf tvDirList_AfterExpand AddHandler .DrawMode = DrawMode.MouseDoubleClick.ShowPlusMinus = True 'use this so user can click nodes to auto-expand and collapse .Tick. "Select a different virtual or logical drive") .Left Or AnchorStyles. e As FormClosingEventArgs) Handles Me.tmrEnd.lblFoldersHdr. AddressOf tvDirList_AfterCollapse AddHandler .AfterExpand.Interval = 10 'set interval to 10ms . End If End Sub Page –564– .ItemHeight = 18 'allow full images to have a gap between them .Top .Enhancing Visual Basic .SelectedItem = di 'select it Exit For 'and we are done checking the list End If Next End With '------------------------------------------------------'Set up End Timer control '------------------------------------------------------Me.ShowNodeToolTips = True 'Allow node Tooltips to display .ClientSize.0 – David Ross Goben '------------------------------------------------------'Set up cboDrive ComboBox control '------------------------------------------------------Me.Right Or AnchorStyles.Top Or AnchorStyles.Anchor = AnchorStyles. AddressOf tvDirList_DrawNode End With '------------------------------------------------------'populate drive combobox and then select current drive '------------------------------------------------------Me.FolderSide 'default to folder on side . AddressOf tvDirList_MouseDoubleClick AddHandler .ShowLines = False 'no need for this .Enabled = False Me.Left = EdgeOfst 'align combobox left location . AddressOf cboDrives_DrawItem 'add event handlers AddHandler .Interval = 10 'set interval to 10ms .Enabled = False Me.tmrStart = New Timer With Me.Equals(Me.Top .ShowRootLines = True 'use this so that tabbing aligns ._Root) Then 'if we found it.NET Beyond the Scope of Visual Basic 6.OwnerDrawFixed 'enable drawing images into list '----------------------------------------------------------------------AddHandler .Height 'place below combobox .DrawNode.Cancel = True 'but cancel the actual form close for now. AddressOf tmrEnd_Tick .Top = EdgeOfst 'align combobox top location . Dispose() 'ImageList control to store images in this form (dipose of last) End Sub '******************************************************************************* ' Method : tmrStart_Tick ' Purpose : make sure TreeView has focus '******************************************************************************* Private Sub tmrStart_Tick(sender As Object. CSng(X).Windows.NET Beyond the Scope of Visual Basic 6. 'lock out treeview display updates 'RE-initialize ImageList (get rid of extra images) 'grab the root path from the list object 'if it does not match the last start path.DriveInfo(Drv) 'get the info for the drive If di.ToString.cboDrives.Index <> -1 Then Dim cboBox As ComboBox = DirectCast(sender.cboDrives.tmrStart. X. e As DrawItemEventArgs) e..cboDrives.cboDrives.cboDrives. DriveItem) Dim X As Int32 = e.Directory.Handle) InitializeImageList(Me..Dispose() 'tooltip control Me.IsReady Then 'if it is ready.Add(New DriveItem("ThisPC")) 'Add ThisPC item '--------------------------------------------------------------------------Dim Drives() As String = Environment. cboBox.Font.Items. e As EventArgs) With DirectCast(sender.tvDirList._Root End If Page –565– 'clear the background 'ignore out of range index 'get the listbox pointed to by this event 'get the Drive item from the list 'init drawing from left position 'if drive info and also list dropped down._StartPath = Me.Forms._StartPath) Then Me.Dispose() 'Directory Folder/File TreeView Me.Items.lblFilesHdr.Add(New DriveItem("Documents")) 'Add Documents item Me.ToolTips.Bounds.cboDrives.myImages.Images(DriveItem. e As System..Images(Images.GetLogicalDrives 'get list of drives For Each Drv As String In Drives 'now check each one of them out Dim di As New IO.0 – David Ross Goben '******************************************************************************* ' Method : Form_FormClosed ' Purpose : Prepare form '******************************************************************************* Private Sub Form_FormClosed(sender As Object.Width End If img = Me. Y) X += img.Add(New DriveItem("Desktop")) 'Add Desktop item Me.lblFolders..Dispose() 'timer for updating folder/file reports Me...Items.Bounds.Dispose() 'report field header for Folders report Me.ItemHeight Then img = Me.Graphics. 'draw image for ">" 'move beyond image 'Get the image stored In the ImageList 'draw image at start of line 'move beyond image with buffer 'draw the text after the bitmap(s) 'if the index is OK..Enabled = False 'disable the timer Me.myImages.tmrStart.X Dim Y As Int32 = e.Clear() 'keep track of file paths from which we extracted images Me.SelectedIndex <> -1 Then LockWindowUpdate(Me.Items.Y Dim img As Image If Not DriveItem.Items.Type = IO. Brushes.Graphics.cboDrives.Add(New DriveItem("Pictures")) 'Add Pictures item Me.lblFiles.Items. ComboBox) If .Items.Clear() 'clear any current list in case re-invoked Me.Dispose() 'report number of subfolders found Me.Clear() 'keep track of file extensions with application associations Me.cboDrives.Enhancing Visual Basic .cboDrives.Add(New DriveItem("Downloads")) 'Add Downloads item Me.FolderCanOpen) e.Add(New DriveItem("RecentPlaces")) 'Add Recent Places item 'Me.Width + 4 e.cboDrives.Add(New DriveItem("Music")) 'Add Music item Me.DrawString(DriveItem. CSng(Y)) End If End Sub '********************************************************************************* ' Method : cboDrives_SelectedIndexChanged ' Purpose : A new choice was made in the cboDrives list '********************************************************************************* Private Sub cboDrives_SelectedIndexChanged(sender As Object.AddedExtList.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.Items. Y) X += img.FormClosedEventArgs) Handles Me. Used to set focus to treeview on startup Me.Items(e.Black.GetDirectoryRoot(Me.ImageIndex) e.Index)._Root & "\" <> IO.DrawImage(img.Unknown AndAlso Y >= cboBox.Focus() 'then set focus on the treeview End Sub '********************************************************************************* ' Method : PopulatecboDrives ' Purpose : Populate the cboDrives list '********************************************************************************* Private Sub PopulatecboDrives() Me.DriveType._Root = DirectCast(. e As EventArgs) Me. Me.cboDrives.Items.Dispose() 'report number of files found Me.myImages) Me.Items. 'then ignore the start path and begin anew ..lblFoldersHdr.Dispose() 'startup timer.tvDirList.ImgSrcList.Dispose() 'Drive selection ComboBox Me. X.DrawBackground() If e.SelectedItem.tmrScanning.FormClosed Me.Dispose() 'report field header for Files report Me.Drive If Me.tvDirList. DriveItem). ComboBox) Dim DriveItem As DriveItem = DirectCast(cboBox.Graphics.Add(New DriveItem("Videos")) 'Add Videos item Me.myImages.DrawImage(img. 0) 'create Root Node w/closed folder RootNode.Add(tvDirList.Node) 'parse any of its subfolders DirRecursing = False 'no longer recursing through folders MeCursor = Cursors.Zero) 'resume display updates End If Me.Nodes.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.Count) Then 'if we have children.Close() 'then close shop End Sub Page –566– . e As TreeViewEventArgs) If e.EnsureVisible() 'make sure it can be seen End If MeCursor = Cursors.tvDirList.IsExpanded Then 'only folder expand CountFoldersFiles(e.Default 'no longer busy LockWindowUpdate(IntPtr.Node) 'init scanning report DirRecurse(e. 0.SelectedNode = RootNode 'select this node RootNode. e As EventArgs) Me.Focus() End Sub '******************************************************************************* ' Method : tvDirList_AfterSelect ' Purpose : if a folder selected and open.WaitCursor 'show that we are busy DirRecursing = True 'indicating that we are scanning folders InitCounter(e._Root. e As TreeViewCancelEventArgs) With e.tvDirList. but let the timer do it End If End Sub '******************************************************************************* ' Method : tmrEnd_Tick ' Purpose : Let the display catch up and events settle down before actually closing form '******************************************************************************* Private Sub tmrEnd_Tick(sender As Object.Focus() 'set focus to TreeView End If End With '--------------------------------------------------------------------------'if the used tried to exit the app during recursion.WaitCursor 'we are busy Me._Root 'save its path in its tooltip RootNode.tmrEnd.Clear() 'erase all current nodes Dim RootNode As TreeNode = Me. RootNode.0 – David Ross Goben '----------------------------------------------------------------------MeCursor = Cursors.tvDirList.Parent IsNot Nothing Then 'if it has a parent CountFoldersFiles(e.Nodes.Tag = False 'tag it as not a shortcut DirRecursing = True 'prevent TreeView updates when recursing drive InitCounter(RootNode) 'init scanning reports DirRecurse(RootNode) 'parse any of its subfolders DirRecursing = False 're-enable updates '----------------------------------------------------------------------If CBool(RootNode.ToString.Node) 'get a count of the parent's nodes ElseIf e._pClosing Then 'we want to close the form? Me. Me.Nodes.Parent) 'get a count of the parent's nodes End If Me.Nodes.ToolTipText = Me.Enabled = True 'yes.tvDirList.Node. get its count '******************************************************************************* Private Sub tvDirList_AfterSelect(sender As Object._pClosing = False 'turn off our flag Me. e As EventArgs) Me.Enabled = False 'disable the timer Me.tmrEnd.tvDirList.NET Beyond the Scope of Visual Basic 6.DirRecursing = False 'beat a dead horse to ensure this is disabled Me._StartPath) 'open directory to the node If RootNode IsNot Nothing Then 'if we found it (very likely) tvDirList.Expand() 'make sure root node is expanded End If '----------------------------------------------------------------------RootNode = FindNodePath(Me.Node.. '------------------------------------------------------------------LockWindowUpdate(Me.Checked Then 'has it been processed yet? . avoid application crashes 'by letting the display of objects catch uup before actually disposing of them '--------------------------------------------------------------------------If Me.Node.tvDirList.Checked = False 'mark this folder as processed.Enhancing Visual Basic .Handle) MeCursor = Cursors.tvDirList.Zero) 're-enable TreeView updates Me.Node If .Default 'show that we are no longer busy LockWindowUpdate(IntPtr.Count.Focus() 'set focus to TreeView End Sub '******************************************************************************* ' Method : tvDirList_BeforeExpand ' Purpose : React to a node expanding '******************************************************************************* Private Sub tvDirList_BeforeExpand(sender As Object.. Parent) 'get a count of the parent's nodes Me.ToolTipText 'no association. pt) 'draw the on-side folder or on-side shortcut e..Color = tv...Enabled = False 'make sure the timer is off CountFoldersFiles(e..Font. e As TreeViewEventArgs) If e... get copy of line bounds.....Contains(" "c) Then 'if it contains a space...ImageIndex = -1 OrElse ImgInit OrElse DirRecursing Then 'if image index is -1 or image list is updating. Dim Path As String = GetSelectedOpenerForExt(nd..... Loop 'and try again Dim Rect As Rectangle = e.X + nCount * tv............Node.......NET Beyond the Scope of Visual Basic 6..Parent IsNot Nothing 'while the node as a parent nCount += 1 'count a generation (indent index) nd = nd..Graphics.. Rect...Node 'start with current node provided to us Do While nd...'determine the mode's indent level '..Bounds 'otherwise. if it exists '******************************************************************************* Private Sub tvDirList_AfterCollapse(sender As Object.Node.. CountFoldersFiles(e....If e..Node..X + 16 + 16 + 4 'point to text drawing area...Nodes...IsSelected Then 'if the node is selected..Location) 'draw text Brush.ForeColor 'we will draw the text normally End If '..Node....Images(Images..Node.........Contains(" "c) Then 'if the file contains a space... tv..myImages.... e As MouseEventArgs) Dim nd As TreeNode = Me.myImages....Text..Parent IsNot Nothing Then 'if this is not the root folder..Dispose() 'release brush resources End Sub Page –567– ..Contains(" "c) Then 'if it contains a space.Bounds) '(we could use Rect here to minimize highlight) Brush. tv.NormalFocus) 'shell out to it End If Me.....ToolTipText & """" 'add path w/quotes Else Path &= " " & nd.. if it exists '******************************************************************************* Private Sub tvDirList_AfterExpand(sender As Object. e As TreeViewEventArgs) Me.......Node.....Images(Images..Images(e..Parent 'point back to its parent node. Path = """" & Path & """" 'wrap it in quotes End If If nd. e.Graphics........tmrScanning.. Path &= " """ & nd..pt. Dim pt As New Point(Rect.....ToolTipText.. AppWinStyle....ToolTipText) 'get a reference path for it If Not String.....FolderIsOpen).0 – David Ross Goben '******************************************************************************* ' Method : CountFoldersFiles ' Purpose : Get folder and file count of parent node...BackColor) 'brush used for selection and text coloring If e..Count <> 0 Then 'not expanded...IsNullOrWhiteSpace(Path) Then 'if a file association exists.....X = pt....... TreeView) 'get reference to TreeView container '..Graphics.Color = SystemColors.myImages....tvDirList... Rect. Rect) 'clear JUST text area background..MeasureText(e.tvDirList.Graphics......Node.. Brush....FolderCanOpen). pt) 'yes..SelectedNode 'get node clicked on If nd Is Nothing Then 'if no node clicked.tvDirList.Node. do nothing Return End If '--------------------------------------------------------------------------If IO..Indent..File. but does it have sub-folders? e..DrawImage(Me.Enhancing Visual Basic .Focus() 'set focus to TreeView End Sub '********************************************************************************* ' Method : tvDirList_DrawNode ' Purpose : Draw node with indent and state '********************************************************************************* Private Sub tvDirList_DrawNode(sender As Object...DrawImage(Me...FillRectangle(Brush.. pt) 'draw "V" in front of on-side folder ElseIf e......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.. '...Font) 'compute size of text area to minimize flicker..White 'change the brush's color to white Else e.Focus() 'set focus to TreeView End Sub '******************************************************************************* ' Method : tvDirList_AfterCollapse ' Purpose : Get folder and file count of parent node.......Node) 'get count of sub-nodes Me...Graphics.. Rect...ToolTipText) Then 'if a node is selected and it is for a file.......FillRectangle(Brush.Node..IsExpanded Then 'if the folder is expanded. e.Y) 'compute Top-left coordinate for drawing Rect....Color = Color.X += 16 'point to on-side folder position e.Exists(nd. e As DrawTreeNodeEventArgs) If e..Size = TextRenderer. so draw ">" in front of on-side folder End If '.Graphics... Brush.DrawString(e.....Dim Brush As SolidBrush = New SolidBrush(tv... Return 'do nothing End If Dim tv As TreeView = DirectCast(sender.DrawImage(Me....ImageIndex).ToolTipText 'add path w/o quotes End If Else Path = nd. Path = """" & Path & """" 'wrap it in quotes End If End If Shell(Path... so use file itself If Path..tvDirList.....Dim nCount As Int32 = 0 'init indent counter to 0 Dim nd As TreeNode = e.Text..Highlight 'change background to highlight color e.. Brush. If Path.... .. e As EventArgs) With Me........ToString If Scanning Then 'this flag is set by tmrScanning to indicate intermediates DirCnt &= "....'first check for link files in the current folder that actually reference directory folders '.Text = "0" 'init report fields for subfolder and file counts Me." End If If Me." FilCnt &= " of possible " & _pFilCnt...Directory.Refresh() Me.lblFiles..ToString & "..Text <> DirCnt Then Me......Nodes..Text <> FilCnt Then Me....Dim pPath As String = AddSlash(parentNode....Text = FilCnt Me.. so update current counts '********************************************************************************* Private Sub tmrScanning_Tick(sender As Object..._pClosing Then 'if user closing form.Refresh() 'make sure the user sees them Me..lnk files Catch End Try Dim lnkFolderList As New List(Of String) 'init lnk folder list Page –568– ..Text = "0" Me....lnk") 'get a list of all ..... "*.lblFiles.......ToolTipText) 'get parent node's path and add a backslash Dim pKey As Int32 = 0 'initialize simple key parentNode........lblFolders....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....._pNode = ParentNode 'save parent node to keep scanning Me. 'update subfolder report 'if the file counts do not match.Dim Files() As String = Nothing 'init local file storage Try Files = IO..Equals(Me..lblFolders...._pNode) Then _pDirCnt = 0 'init folder count _pFilCnt = 0 'init file count End If '..tmrScanning.Enabled = True 'turn on the timer End Sub '********************************************************************************* ' Method : tmrScanning_Tick ' Purpose : Scanning folder.Refresh() End If End With End Sub 'if the folder counts do not match...lblFiles.0 – David Ross Goben '********************************************************************************* ' Method : InitCounter ' Purpose : Prepare to start scanning a folder '********************************************************************************* Private Sub InitCounter(ByRef ParentNode As TreeNode) Me.lblFolders....Enabled = False 'make sure the timer is off CountFoldersFiles(_pNode...ToString 'prepare report Dim FilCnt As String = Files..... True) 'disable timer and get current count . Optional ByVal skipDeepSeek As Boolean = False) If Me.tmrScanning.Refresh() End If If Me......Interval = 2000 'set interval to 2 seconds if not at this value .....lblFolders...lblFolders.tmrScanning Me....Clear() 'Clear all child nodes in case we are repopulating If parentNode.CursorFile Then 'if a folder. Optional Scanning As Boolean = False) If Me.. Return 'do nothing End If '........GetFiles(pPath.....Text = DirCnt Me. 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 .Enhancing Visual Basic ..Interval = 10 'init interval to 10ms Me...tmrScanning....Enabled Then 'do nothing here if the folder scanning is enabled Return 'note that tmrScanning turns itself off before invoking this... Dirs += 1 'bump folder counter Else Files += 1 'else bump file counter End If Next Dim DirCnt As String = Dirs.ImageIndex < Images.NET Beyond the Scope of Visual Basic 6..lblFiles.Nodes 'parse each child node immediately under parent If nd..tmrScanning... 'update files report '******************************************************************************* ' Method : DirRecurse ' Purpose : Fill provided TreeView with folders and files as needed '******************************************************************************* Private Sub DirRecurse(ByRef parentNode As TreeNode....lblFiles... Exists(linkedPath) Then 'if the path is still an existing directory folder.LastIndexOf(".Nodes....If lnkFolderList.............._pNode) Then Me........... True) 'parse any of its subfolders If parentNode...Clear() 'clear any added resources Exit For End If Next dirPath End If '.Equals(Me. Images.Clear() 'get rid of any added resources Return 'nothing else to do End If '.......... but ignore if protected End If Catch End Try Dim Bol As Boolean = dirs IsNot Nothing 'True if directories exist If Bol OrElse lnkFolderList..Nodes....DoEvents() 'let tmrScanning get a share of run-time End If ElseIf parentNode._pDirCnt = dirs..... dirs = Nothing Files = Nothing Else dirs = IO... or lnk folders exist._pClosing Then 'if the user is closing the form.........Hidden Or FileAttribute...Checked = True 'mark this folder as not yet processed parentNode...System Or FileAttribute.. Images.Directory Then 'and if special attributes are not assigned.'get list of all files. lnkFolderList..0 – David Ross Goben If Files IsNot Nothing Then 'do we have any lnk files? For Each lnkFile As String In Files 'yes.........Add(lnkFile) 'then add file it to our local linked folder list End If End If End If If Me.............Volume)) = FileAttribute.System Or FileAttribute.. lnkFolderList........ so drop one End If If Me......Count <> 0 OrElse Files IsNot Nothing Then 'if sub-directories..FolderSide) 'add new dir node w/closed folder pKey += 1 'bump key dirNode.Count 'get total directory count End If For Each dirPath As String In dirs 'then parse each subfolder If (GetAttr(dirPath) And (FileAttribute.._pClosing Then 'if user closing form. Application.Path....Normal Then 'if it is not a special folder....Count + lnkFolderList...Enhancing Visual Basic .... so check each of them 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......Equals(Me..... If skipDeepSeek Then 'if Referencing only....NET Beyond the Scope of Visual Basic 6...._pDirCnt -= 1 'rejected folder. Dim Attr As FileAttribute = GetAttr(linkedPath) 'get its attributes and check for normal directory If (Attr And (FileAttribute......Try If Me... Dim dirNode As TreeNode = parentNode.If Bol Then 'if sub-folders exist..GetDirectories(pPath) 'get a list of any subfolders.Directory......_pNode) Then Me....Dim dirs() As String = Nothing 'init local directory storage Try If Me... files.Equals(Me....... If parentNode..Add(pKey..GetFiles(pPath) 'get a list if all files in the folder End If Catch End Try '..... For Each lnkPath As String In lnkFolderList 'parse each lnk path Dim linkedPath As String = GetShortcutLinkToPath(lnkPath) 'get lnk's path to its target lnkPath = lnkPath. so we can 'still determine of the parent folder should show a '>' tag for expandability '... 4) 'remove "..._pClosing Then 'if user closing form.......Directory.Hidden Or FileAttribute....."c)...Volume)) = FileAttribute........ lnkFolderList.. Get it here in case there are no sub-folders.....Count <> 0 Then 'if link folder data exists..Directory Or FileAttribute.GetFileName(dirPath).....Tag = False 'tag as not a shortcut DirRecurse(dirNode._pNode) Then 'if we are scanning the main parent......_pClosing Then Files = Nothing Else Files = IO.....lnk" from end of folder path Page –569– .....Clear() 'clear anything from list Exit For End If Next lnkFile End If '.. IO.Add("*") 'add a faux child node to add ">" branch connector lnkFolderList...........Remove(lnkPath...........ToString. parentNode......'put that process on pause and now check for sub-folders '.....FolderSide.Directory.......ToolTipText = dirPath 'save its folder path as its tooltip dirNode. lnk" Then 'if this is a link file.Enhancing Visual Basic . Command file (DOS) ExtIdx = Images..Path..GetFileName(lnkPath)..reg" '--------------------------------------------Registry file ExtIdx = Images.........FontFile 'set default image (do nothing else) Case "....lnk" FilePath = linkedPath 'let file assume linked Path for now Ext = IO.. "...GetExtension(FilePath).Count 'update total file count End If Dim IsLink As Boolean 'set to True if a file is a shortcut link Dim LnkPath As String = Nothing 'path of .NET Beyond the Scope of Visual Basic 6. ExtIdx) 'grab first icon from file and update image Case ". ".File..oca"....... ".. ExtIdx) 'grab associated file icon and update the image list Case ".ToLower 'get the file extension If Ext = ".....CursorFile 'init default cursor image AddImageFromFile(FilePath._pNode) Then _pFilCnt = Files.mpg"....dat".. less "...dib" '------------------IMAGE ExtIdx = Images.Equals(Me. ExtIdx) 'grab cursor image from file and update the image list Case ". If IO.bat". IsLink.. "... IsLink. ".ini".IconFile 'use default icon image AddAssociatedImage(FilePath.....BinFile 'set default image AddAssociatedImage(FilePath..mp3"... ExtIdx) 'grab first icon from file and update the image list Case Else '----------------------------------------------process undetected types Page –570– . ExtIdx) 'grab associated file icon and update the image list Case ".ExeFile 'init default EXE image AddImageFromFile(FilePath..... ExtIdx) 'grab first icon from file and update the image list Case ". ".. ". IO.. 4) 'save full path to lnk file.log".. "....Directory. linkedPath = Nothing 'kill linked path End If End If If linkedPath IsNot Nothing Then 'if a valid file path was returned. ". ".wav".SoundFile 'init default image AddAssociatedImage(FilePath.Tag = True 'tag as a shortcut DirRecurse(dirNode.ToLower 'get the actual target file's extension Else ExtIdx = Images.FolderSideShortcut.com" '------------------------------------Batch...ttf" '--------------------------------------------TrueType font (TTF) ExtIdx = Images.. ".LastIndexOf(".iso" '--------------------compressed files (ZIP) ExtIdx = Images.asf".Remove(FilePath. "..System)) <> 0 Then 'if this is not an acceptable file.aac".... ".Path..mpeg".bin". IsLink..0 – David Ross Goben Dim dirNode As TreeNode = parentNode.png"......dep". ". "wmv"..exe" '--------------------------------------------EXE ExtIdx = Images..ZipFile 'init default for compressed file (do nothing else) Case ". ExtIdx = Images.GetExtension(linkedPath).chm".tiff".RegFile 'set default image (do nothing else) Case ". Images.... ".... "..jpg".. "..ToolTipText = linkedPath 'save its linked folder path as its tooltip dirNode.ExeFile 'set default image (do nothing else) Case "..ani" '--------------------------------------------animated cursor ExtIdx = Images... ". ExtIdx) 'grab associated file icon and update the image list Case ". ".pic"...ocx"..jpeg". ". ".. linkedPath = Nothing 'kill linked path ElseIf Not IO.. "....MovieFile 'init default image AddAssociatedImage(FilePath.fon" '--------------------------------------------FONT ExtIdx = Images. IsLink...Hidden Or FileAttribute.lnk file when IsLink is set to True For Each FilePath As String In Files 'parse each file in folder.dll" '--------------------------------------------DLL ExtIdx = Images.wma" '--------------------AUDIO ExtIdx = Images.7z".pic".FileFile 'init default image to File Dim Ext As String = IO.FolderSideShortcut) 'add as new dir node w/closed folder pKey += 1 'bump key dirNode.bic". ".. IsLink. IsLink... True) 'parse any of its subfolders If Me..nls" '----Binary/Data file ExtIdx = Images. "..Exists(linkedPath) Then 'if it is a file and it no longer exists.CursorFile 'set default image (do nothing else) Case "..ToString. Images....If Files IsNot Nothing Then 'if files exist If parentNode. ". ExtIdx) 'grab associated file icon and update the image list Case ".Add(pKey.Nodes.gif"._pClosing Then 'if user closing form. "..Exists(linkedPath) Then 'if it is actually a directory folder.LogFile 'set default image AddAssociatedImage(FilePath. IsLink = False 'init flag to not being a link Dim ExtIdx As Int32 = Images..zip".IconFile 'use default icon image AddImageFromFile(FilePath. ExtIdx) 'grab associated file icon and update the image list Case ". IsLink = True 'mark it as a link LnkPath = FilePath..Path.If (GetAttr(FilePath) And (FileAttribute.None 'hide image IsLink = False 'make sure flag is reset ElseIf Not String....bmp". IsLink.DllFile 'init default EXE image (faster if we use the default) 'AddImageFromFile(FilePath.TtfFile 'set default image Case ".'process any list of files '..swf" '--------------------VIDEO ExtIdx = Images.None 'ignore if linked path is nothing Ext = Nothing End If End If '. ".cfg" '----------------------------LOG/INI file ExtIdx = Images.ico" '--------------------------------------------ICON ExtIdx = Images....... Files = Nothing Exit For End If Next lnkPath End If '.IsNullOrWhiteSpace(Ext) Then 'else if it has an extension defined...avi".flv".. ". IsLink. Select Case Ext 'check extension Case ". IsLink."c).cur" '--------------------------------------------CURSOR ExtIdx = Images.. ".mov".. 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). lnk" ' 'yes.. IsLink. IO.AddedExtList.Path.... ExtIdx) 'add as new file node w/appropriate index pKey += 1 'bump key dirNode.GetFileName(LnkPath)..None Then 'if we have an image reference.ToLower 'check for an app associated with this file If Not String. ByVal IsLink As Boolean..If ExtIdx <> Images... we will use the default image Return False 'indicate an image was NOT added End Function Page –571– ..AddedExtList. so grab a default extension index Else ExtIndex = Me..Tag = IsLink 'tag true if a link file End With ElseIf parentNode.Clear() 'release link folder storage End Sub '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : AddAssociatedImage ' Purpose : Add an image from a file associated with the specified file. or ' : the path already is registered. grab assigned index End If 'if this fails. if one exists '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub AddAssociatedImage(ByVal FilePath As String. ExtIndex) Then 'then add associated image.. 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. If Not IsLink Then 'if this is not a linked file. IsLink...0 – David Ross Goben AddAssociatedImage(FilePath. Icon) End If If AddImage(Img.ToolTipText = FilePath 'save its path as its tooltip dirNode. Me.Equals(Me... Application.ImgSrcList(Assoc) 'else grab link image index associated with it End If Else Dim Assoc As String = GetSelectedOpenerForExt(Ext).... ExtIndex was set to existing extension Return End If 'if this fails.ImgSrcList. Exit For End If Next 'FilePath End If End If lnkFolderList._pNode) Then Me..Enhancing Visual Basic ....Path... ExtIndex) 'if an image was added. DLL.. so drop one End If If parentNode..ImgSrcList.. (-ExtIndex).. tvDirTree '....Add(Ext..ToString. LnkPath = FilePath 'simplify the code below by assuming it is End If With Me.ToLower = ".....Item(Ext & x) 'grab associated file or ExtIndex reference If Assoc..Item(fp) 'otherwise.IndexOfKey(fp) = -1 Then 'if the item does not exist in our list.. ByRef ExtIndex As Int32) As Boolean FilePath = FilePath..IsNullOrWhiteSpace(Assoc) Then 'if there is an associated file..lnk" ' 'if FilePath is a Link then set additional ..ToLower.IndexOfKey(Ext & x) <> -1 Then 'if the extension list contains a matching extension._pFilCnt -= 1 'rejected file.lnk extension if this is a link file '--------------------------------------------------------------If Me...AddedExtList....Substring(0..lnk extension If Me._pNode) Then 'if we are scanning the main parent....ToString) 'failed...AddedExtList.cur" Then 'if it is a cursor.ToLower 'get FilePath extension Dim x As String = String. and if it was added.DoEvents() 'let tmrScanning get a share of run-time End If If Me.... If AddImageFromFile(Assoc.. Dim Assoc As String = Me. Dim Img As Image 'init image to set and assign to the list If IO.Nodes..Add(fp._pClosing Then 'if user closing form.... Assoc... ByRef ExtIndex As Int32) Dim Ext As String = CleanExtension(FilePath).. then add fp to ImgSrcList Return True 'and indicate an image was added End If Else ExtIndex = Me.tvDirList Dim dirNode As TreeNode = parentNode.GetExtension(FilePath)..Add(pKey. ExtIdx) 'grab associated file icon and update the image list End Select End If '..ToLower & x) 'then add the extension to to association list End If 'otherwise.....Add(Ext & x. so append a .. we will use the default image Me..'add a file node to our TreeView. 1) = "-" Then 'is it actually a negative integer? ExtIndex = -CInt(Assoc) 'yes.. ExtIdx.Equals(Me... IsLink.NET Beyond the Scope of Visual Basic 6.ImgSrcList..ToLower 'make sure it is lowercase Dim fp As String = FilePath 'init a match of FilePath If IsLink Then fp &= ". so set the image index to it '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function AddImageFromFile(ByVal FilePath As String. ExtIndex) Then 'update the imagelist if an image was grabbed Me. ByVal IsLink As Boolean.... Img = GetCursorImage(FilePath) 'then load the cursor's image Else Img = GetIconImage(FilePath) 'else load the image from a file (EXE.Empty 'init to no additional extension If IsLink Then x = ". .Length 'then start index beyond it End If '----------------------------------------------------------------------Do While Idx <> SeekPath.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : AddImage (Support Method) ' Purpose : Add an image to the ImageList and set the image index.. ExtIndex = ExtIndex + Images. Dim bmp As New Bitmap(Img. add a tag '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function AddImage(ByRef Img As Image...IsExpanded Then 'othewise. '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub InitializeImageList(ByRef imgList As ImageList.Clear() 'initialize image list imgList.CursorFile 'shortcut rendition of default icon End If Return False 'use current ExtIndex value End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : AddRealImage (Support Method) ' Purpose : Add an image to the ImageList and set the image index.. ByVal IsLink As Boolean. ByVal IsLink As Boolean. IsLink._LastItem .myImages..LastIndexOf("\"c) 'while there is data to find Idx = SeekPath.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 '------------------------ Page –572– .. ByVal SeekPath As String) As TreeNode If Node. 16) 'create a copy of the provided image Dim g As Graphics = Graphics. Optional JustBasics As Boolean = False) ImgInit = True 'prevent TreeView updates while we are doing this.Images.Nodes 'check each child Dim nd As TreeNode = FindNodePath(subNode.Expand() 'then make sure it is expanded (and populated) End If Loop Return RootNode 'finally. Idx = RootNode.AddedExtList. add a tag '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Sub AddRealImage(ByRef Img As Image. If it is a link. set the Replace ' : parameter to FALSE.ImgSrcList.LastIndexOf("\"c) Then 'if we did not find the node or we are at the target. If nd IsNot Nothing Then 'did it find a match? Return nd 'yes._LastItem Then 'else compute the index of the.Nodes(0) 'start at the TreeView's base node If SeekPath = RootNode.ImageSize = New Size(16.ToolTipText..IndexOf("\"c...Count 'update extension index in image list Me..Images. For Each subNode As TreeNode In Node. ExtIndex) 'then add it Return True ElseIf IsLink AndAlso ExtIndex < Images.FromImage(bmp) 'get graphics interface for it g. Idx + 1) 'find the next backslash Dim BasePath As String = SeekPath. not appending images imgList.Images.. New Rectangle(0.NET Beyond the Scope of Visual Basic 6.Enhancing Visual Basic . AddRealImage(Img. 8. SeekPath) 'check its node and subnodes..Substring(0. BasePath) 'find the path to it via an overload If RootNode Is Nothing OrElse Idx = SeekPath...DrawImage(ShortcutTag.Dispose() 'dispose of graphics resource Img = bmp 'update the referenced image End If ExtIndex = Me. If Replace Then 'if we are filling. return the target node End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : FindNodePath (overload) ' Purpose : Find the node path in a treeview '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function FindNodePath(ByRef Node As TreeNode. Return Node 'return a reference to that node End If If Node.myImages.ToolTipText Then 'if we already have a match. if tne node is not expanded.Nodes. Optional ByVal Replace As Boolean = True.ToolTipText = SeekPath Then 'if current node contains the sought path. 16.tvDirList. 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. If it is a link. Idx) 'grab the path left of it RootNode = FindNodePath(RootNode. 16) 'define 16x16 pixel images in this list Me.Count <> 0 Then 'else if it has children... 8. 8)) 'draw shortcut on bitmap in its bottom-left corner g.Contains(AddSlash(RootNode..Clear() 'reset file extensions with application associations Me. ByRef ExtIndex As Int32) If IsLink Then 'if we are processing a shortcut link.ToolTipText)) Then 'if the seek path contain the root node. ByRef ExtIndex As Int32) As Boolean If Img IsNot Nothing Then 'if image is valid...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. Exit Do 'then done ElseIf Not RootNode. RootNode.. 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.Images.. Enhancing Visual Basic .0 – David Ross Goben '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" "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.NET Beyond the Scope of Visual Basic 6.29 Page –573– & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & .AddStrip(Img) 'add this as Images 0 .Images. Folder Images FolderCanOpen FolderIsOpen FolderSide FolderSideShortcut '----._LastItem .MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image.myImages.0 – David Ross Goben '--------' 'Image 30 ShortcutTag (special 8x8-pixel image) '-------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.FromStream(memStream) 'construct image from stream data memStream.NET Beyond the Scope of Visual Basic 6.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO. (Idx)) 'add it as a shortcut-tagged version (pass Idx ByVal.Default File types CursorFile ExeFile FileFile FontFile TtfFile IconFile ZipFile MovieFile SoundFile DllFile DosFile RegFile BinFile LogFile '---.Close() 'release stream resources Return Img 'return image End Function End Class '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' DirTreeTestSupport Static Class Module ' Supprt for Dir Tree Test '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module DirTreeTestSupport '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE 'Enumerator: Images 'Purpose : Reference to images in the ImageList 'EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE Friend Enum Images As Int32 None = -1 '---. 16) 'create a duplicate of an existing image AddRealImage(Img. images referenced 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.Images(Idx).myImages.1 'duplicate default images with shortcut versions Img = New Bitmap(Me.Images.Virtual Drive Types Desktop Documents Downloads Music Pictures Videos Recent '----. 16. not ByRef) Next End If AddedBase = Me.Mark end of this list _LastItem End Enum '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Page –574– . True.Count 'set index base for any addl.Logical Drive Types Fixed CDRom Removable Ram Network '---.MemoryStream = New IO.CursorFile To Images.Enhancing Visual Basic . Fixed Case IO.Removable Case IO. and the string you are working with may ' : or may not already have a backslash appended to it. Return DirectCast(FolderItem.VolumeLabel 'volume label Me.Trim 'then return the link's command path End If Catch End Try Return Nothing 'otherwise failure.Folder = Shell.Last = "\"c Then 'already have a backslash? Return strPath 'yes.Network Case Else Me.NameSpace(IO. so simply return the string Else Return strPath & "\" 'otherwise.DriveType.Fixed Me.Length .dll). 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.ImageIndex = Images. Shell32.Drive = RemoveSlash(DrvInfo.CDRom Me.CDRom Case IO.ParseName(IO.Name) 'grab drive (C:\) Me.Path.DriveInfo) Me.DriveType 'get the drive type enumeration '----------------------------------------------'now compute the image index for the drive type '----------------------------------------------Select Case Me.ImageIndex = Images.Type Case IO.Removable Me.NET Beyond the Scope of Visual Basic 6.Enhancing Visual Basic .0 – David Ross Goben '******************************************************************************* ' 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.Fixed End Select End Sub Page –575– .Path.Substring(0.Ram Me.1) 'yes..Network Me.Shell = New Shell32.ShellLinkObject). '********************************************************************************* Friend Function AddSlash(ByVal strPath As String) As String If strPath.DriveType.ImageIndex = Images. This function ' : is useful for building paths.GetFileName(ShortcutFilePath)) 'define a link to the shortcut file object from the folder object If FolderItem IsNot Nothing Then 'if it exists. return the string with a backslash End If End Function '********************************************************************************* ' Method : RemoveSlash ' Purpose : Remove any existing terminating backslash from a path. such as C:\ Friend Volume As String 'volumn name for the drive Friend Type As IO.GetLink.ImageIndex = Images.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. so remove trailing slash if it exists Else Return strPath 'otherwise.DriveType.DriveType. so indicate so End Function '********************************************************************************* ' Method : AddSlash ' Purpose : Add a terminating backslash to a drive/path if required.Volume = DrvInfo.Type = DrvInfo..ShellClass 'define our shell class object as a link to our operating system shell Dim Folder As Shell32.ImageIndex = Images.FolderItem = Folder.DriveType.Ram Case IO.Path. '********************************************************************************* Friend Function RemoveSlash(ByVal strPath As String) As String If strPath.GetDirectoryName(ShortcutFilePath)) 'define a folder object to the link file's directory folder Dim FolderItem As Shell32. strPath.Last = "\"c Then 'already have a backslash? Return strPath.ImageIndex = Images. and ensure its Embed Interop Types parmameter = False '******************************************************************************* Friend Function GetShortcutLinkToPath(ByVal ShortcutFilePath As String) As String Try Dim Shell As Shell32. Volume = "Documents" 'volume label Me.ico file.GetFolderPath(Environment. Type: IntPtr() or IntPtr or IntPtr.Volume = "Recent Places" 'volume label Me. DLL.Type = IO.Downloads 'set image index Case "music" Me.Drive = Environment.Drive = Environment.Volume Else 'otherwise a logical drive Return Me.ToString & " Drive]" End If End Function End Class '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modGetIconFromFile Static Class Module ' Extract Icons. For example.Zero.Drive = Environment.Desktop) Me.Unknown 'set the drive type enumeration Me.Zero: ' A single IntPtr or a sized IntPtr array for icon handles that receives handles to the small icons extracted from ' the file.DriveType.Unknown 'set the drive type enumeration Me. the return value is 1.Zero.Unknown 'set the drive type enumeration Me.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 '******************************************************************************* Public Overrides Function ToString() As String If Me.DriveType. ' the function returns the total number of icons in the specified file.Volume = "Videos" 'volume label Me. ' 'phiconSmall [out.Desktop 'set image index Case "documents" Me.SpecialFolder.SpecialFolder.DriveType.Volume = "Music" 'volume label Me.GetFolderPath(Environment.Type = IO.Recent) Me.DriveType.NET Beyond the Scope of Visual Basic 6. ' the return value is the number of RT_GROUP_ICON resources.ImageIndex = Images. If this parameter is IntPtr. ByRef]. For example.MyDocuments) Me. ' 'nIconIndex [in.Type = IO. If this value is –1 and phiconLarge and phiconSmall are both IntPtr.Music 'set image index Case "pictures" Me. suck as desktop.0 – David Ross Goben '******************************************************************************* '******************************************************************************* ' Method Name : New (overload) ' Purpose : Set up the class using a Text Name for generic items. ByVal].Volume = "Downloads" 'volume label Me. if this value is zero. optional.ImageIndex = Images. optional.Type.GetFolderPath(Environment. Cursors from a Filepath '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modGetIconFromFile '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'INTEROP METHOD: --------ExtractIconEx-------' Creates an array of handles to large or small icons extracted from the specified executable file.Unknown 'set the drive type enumeration Me.GetFolderPath(Environment.Unknown Then 'virtual drive.ImageIndex = Images. If the file is an executable file or DLL. ' 'nIcons [in.GetFolderPath(Environment. ByVal].Volume = "Desktop" 'volume label Me.SpecialFolder.Pictures 'set image index Case "videos" Me.MyMusic) Me. no large icons (32x32.Drive = Environment. use ' -3 to extract the icon whose resource identifier is 3. etc.Drive = Environment.Videos 'set image index Case "recentplaces" Me. ByVal].Drive & ") [" & Me.ImageIndex = Images.SpecialFolder. Page –576– .Trim.Type = IO.ImageIndex = Images. ' 'PARAMETERS: 'lpszFile [in. the function extracts ' the first icon in the specified file.Type = IO. no small icons (16x16) are extracted from the file.Zero. such as Desktop.Unknown 'set the drive type enumeration Me. ' 'phiconLarge [out. etc.MyPictures) Me.Drive = Environment.MyVideos) Me. Type: IntPtr() or IntPtr or IntPtr.Unknown 'set the drive type enumeration Me.Volume = "Pictures" 'volume label Me. 48x48) are extracted from the file. If this parameter is NULL. ' If this value is a negative number and either phiconLarge or phiconSmall is not IntPtr. Type: String: ' The name of an executable file. Return Me.Documents 'set image index Case "downloads" Me.Zero: ' A single IntPtr or a sized IntPtr array for icon handles that receives handles to the large icons extracted from ' the file.ImageIndex = Images. Music.SpecialFolder. '******************************************************************************* '******************************************************************************* Friend Sub New(ByVal DrvInfo As String) Select Case DrvInfo.DriveType. Type: UInt32: ' The number of icons to be extracted from the file. Images. Documents.Enhancing Visual Basic .DriveType.Type = IO.ImageIndex = Images. or icon file.GetEnvironmentVariable("USERPROFILE") & "\Downloads" Me.DriveType. Type: Int32: ' The zero-based index of the first icon to extract. ByRef]. If the file is an .GetFolderPath(Environment.Unknown 'set the drive type enumeration Me.Type = IO.SpecialFolder. the function begins ' by extracting the icon whose resource identifier is equal to the absolute value of nIconIndex.DriveType. or icon file from which icons will be extracted. DLL.ToLower Case "desktop" Me.Drive = Environment.Volume & " (" & Me.Type = IO. ' the function uses the SM_CYICON system metric value to set the width. ByRef phiconLarge As IntPtr. ' 'hIcon [in. Private Declare Function DrawIconEx Lib "user32" (ByVal hdc As IntPtr.Zero. the function uses the actual resource height. ByVal cxWidth As Int32. Type: UInt32: ' The index of the frame to draw. ByVal]. ' 'hbrFlickerFreeDraw [in. ' DI_IMAGE (2) Draws the icon or cursor using the image. ByVal xLeft As Int32. If this flag ' is not specified and cxWidth and cyWidth are set to zero. Type: UInt32: ' The drawing flags. ByVal nIconIndex As Int32. Type: Int32: ' The logical x-coordinate of the upper-left corner of the icon or cursor. ByVal hIcon As IntPtr. This parameter is ignored if hIcon does ' not identify an animated cursor. the function uses the actual ' resource size.0 – David Ross Goben Private Declare Function ExtractIconEx Lib "shell32. ByVal]. performing the specified raster operations. ' 'xLeft [in. if hIcon identifies an animated cursor. Type: IntPtr: ' The logical width of the icon or cursor. 'PARAMETERS: 'hdc [in. ' 'cxWidth [in. ByVal]. ByVal hbrFlickerFreeDraw As IntPtr. ' 'cyWidth [in. 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. ' --Value---Meaning-' DI_MASK (1) Draws the icon or cursor using the mask.dll" Alias "ExtractIconExA" ( ByVal lpszFile As String. ByVal cyHeight As Int32.Enhancing Visual Basic . If ' hbrFlickerFreeDraw is NULL. the icon is drawn as a mirrored icon ' if hdc is mirrored. If hbrFlickerFreeDraw is a valid brush ' handle. ' DI_COMPAT (4) This flag is ignored. IntPtr. ByVal]. ByVal istepIfAniCur As Int32. if the cxWidth and cyWidth parameters are set to zero. optional. Type: IntPtr: ' A handle to the icon or cursor to be drawn. ByVal diFlags As Int32) As Boolean Private Const DI_Mask As Integer = 1 'Draws the icon or cursor using the mask Private Const DI_Image As Integer = 2 'Draws the icon or cursor using the image Private Const DI_NORMAL As Integer = 3 'Combination of DI_IMAGE and DI_MASK 'Private Const DI_COMPATL As Integer = 4 'This flag is ignored Private Const DI_DEFAULTSIZE As Integer = 8 'Draw icon or cursor using default system sizes Private Const DI_NOMIRROR As Integer = 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. the system creates an offscreen bitmap using the specified brush for the background color. ByVal yTop As Int32. This parameter can identify an animated cursor. 0&) End Function Page –577– . ' 'yTop [in. ' 'diFlags [in. -1&. and then copies the bitmap into the device context identified by hdc. draws the ' icon or cursor into the bitmap. This parameter can be one of the following values. ' DI_DEFAULTSIZE (8) Draws the icon or cursor using the width and height specified by the system metric ' values for icons. ByVal]. If this parameter is zero and ' DI_DEFAULTSIZE is not used. ByVal]. Type: Int32: ' The logical height of the icon or cursor. Type: IntPtr: ' A handle to a brush that the system uses for flicker-free drawing. ' 'istepIfAniCur [in. the system draws the icon or cursor directly into the device context. ' DI_NOMIRROR (16) Draws the icon as an unmirrored icon. Use the GetHdc property from your ' Graphics interface object for this. IntPtr. Type: In32: ' The logical y-coordinate of the upper-left corner of the icon or cursor. and stretching 'or compressing the icon or cursor as specified. Type: IntPtr: ' A handle to the device context into which the icon or cursor will be drawn. the function uses the actual resource width. ByVal].Zero. ByVal nIcons As UInt32) As Int32 'INTEROP METHOD: --------DrawIconEx-------'Draws an icon or cursor into the specified device context. ByVal]. ByRef phiconSmall As IntPtr. By default.NET Beyond the Scope of Visual Basic 6. ' DI_NORMAL (3) Combination of DI_IMAGE and DI_MASK. ByVal]. If this parameter is zero and ' DI_DEFAULTSIZE is not used. If this parameter is zero and the diFlags parameter is DI_DEFAULTSIZE. .DrawImage(bmp. Dim hIcon As IntPtr 'icon handle.NET Beyond the Scope of Visual Basic 6. Return bmp Else bmp. Dim bm As New Bitmap(16.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!) Return bm 'return image End If Catch End Try Return Nothing 'report failure so default image will be used End Function '******************************************************************************* ' Method : GetCursorImage ' Purpose : Get a cursor as a 16x16 image '******************************************************************************* Friend Function GetCursorImage(ByVal FilePath As String) As Bitmap Dim cur As Cursor Try cur = New Cursor(FilePath) 'grab cursor from file Catch Return Nothing 'some cursors are incompatible End Try Dim bmp As New Bitmap(16.. then try to launch the default associated app. DI_NORMAL) 'draw icon to bitmap g. 16)) 'draw it to a 16x16 surface g. 16) 'create a drawing surface Dim g As Graphics = Graphics.. IntPtr. 0&.FromImage(bmp) 'create a graphics interface for it cur.NormalFocus. IntPtr. or False if the extension was not found or there ' : is no association. 16) 'create a new 16x16-pixel drawing surface g = Graphics. DisplayStyle) 'launch the associated application. or launch the app selected to open it. or the executable was not found where expected. 16..Dispose() 'otherwise dispose of bitmap resource End If 'and fall below End If End If '--------------------------------------------------------------''failed.FromImage(bm) 'define a graphics interface for it g.Dispose() Return bmp 'return the bitmap/image End Function End Module '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modLaunchAssociatedApp Static Class Module ' Launch app associate with a file extension. bmp = New Bitmap(16. Optional ByVal TryDefaultIfNotFound As Boolean = True) As Boolean Dim AppPath As String = GetSelectedOpenerForExt(Extension.Enhancing Visual Basic . New Rectangle(0. TryDefaultIfNotFound) 'get the path associated with an extension If Not String. 0. so try grabbing a 32x32 icon that is the default associated with the file '--------------------------------------------------------------Try bmp = Icon.IsNullOrWhiteSpace(AppPath) Then 'if one was found.0 – David Ross Goben '******************************************************************************* ' Method : GetIconImage ' Purpose : Get an image from the EXE/DLL file icon '******************************************************************************* Friend Function GetIconImage(ByVal FilePath As String. hIcon. 16) 'create a new bitmap g = Graphics. Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle. Index.FromImage(bmp) 'create a graphics interface for it Dim Result As Boolean = DrawIconEx(g. 16. '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Module modLaunchAssociatedApp '******************************************************************************* ' Method Name : LaunchSelectedOpenerForExt ' Purpose : Launch the application that is used to open a specified file extension ' : Return True if success.ExtractAssociatedIcon(FilePath). less extensions Return True 'return success Else Return False 'fail End If End Function Page –578– . Optional ByVal Index As Int32 = 0) As Bitmap Dim bmp As Bitmap Dim g As Graphics If GetIconCount(FilePath) <> 0 Then 'if it contains icons. hIcon. 0&.. Used when extracting icons from executables ExtractIconEx(FilePath.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!) cur.. 0.DrawStretched(g.Zero. 1&) 'get handle of file's first or indexed icon (small 16x16) If CBool(hIcon) Then 'if it has a handle. Shell(AppPath.. then if a selected opener not ' : found. ' : if TryDefaultIfNotFound = TRUE (default). New Rectangle(0. 0&.ToBitmap 'failed. 16.. 16)) 'draw 32x32 image scaled down to 16x16 g.Dispose() 'dispose of resources (DO THIS OR LOSE DRAWING!) If Result Then 'if image was drawn.GetHdc.Zero. '******************************************************************************* Friend Function LaunchSelectedOpenerForExt(ByVal Extension As String... 16. so try grabbing a 32x32 bitmap of associated icon If bmp IsNot Nothing Then 'if we got something. ' You can also get the app paths for the above options. such as "%1".IsNullOrWhiteSpace(Extension) Then 'if the data does not exist. Optional ByVal DisplayStyle As AppWinStyle = AppWinStyle.Computer.GetValue(Nothing). such as Word uses Dim Index As Int32 = AppPath.OpenSubKey(AppPath & "\shell\open\command"). End If If Index = -1 OrElse (SlIdx <> -1 AndAlso SlIdx < Index) Then 'if no typical or slash index defined land less than index.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(AppPath) Then 'if we found it.. 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..0 – David Ross Goben '******************************************************************************* ' Method Name : LaunchDefaultOpenerForExt ' Purpose : Launch the application that is associated with a soecified file extension ' : Return True if success. DisplayStyle) 'launch the associated application. or parameters.Registry.NormalFocus) As Boolean Dim AppPath As String = GetDefaultOpenerForExt(Extension) 'get default associated app If Not String.LastIndexOf(". If IndexOfDot = -1 Then 'but a dot is not found.GetValue(ExtKey.IndexOf("%") 'if robust type not found..Empty Else Dim IndexOfDot As Int32 = Extension.ClassesRoot.IndexOf("/"c) 'get possible slash index. Index = SlIdx 'then try using slash index End If Page –579– .Computer.. less decorations ElseIf TryDefaultIfNotFound Then 'did not.ToString) Catch End Try Return String.. Return GetDefaultOpenerForExt(Extension) 'then try to get app associated with extension End If End Try Return String.ClassesRoot.. such as "%1" and such '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Friend Function StripExts(ByVal AppPath As String) As String Dim SlIdx As Int32 = AppPath.OpenSubKey(Extension)..Path. or the executable was not found where expeced '******************************************************************************* Friend Function LaunchDefaultOpenerForExt(ByVal Extension As String.GetExtension(Extension. Return StripExts(AppPath) 'return path to associated file..Computer. Return GetDefaultOpenerForExt(Extension) 'then try to get app associated with extension End If Catch If TryDefaultIfNotFound Then 'did not. check for typical...Win32..OpenSubKey( "Applications\" & AppPath & "\shell\open\command").ToString 'get the EXE path to the application AppPath = My. such as /n Return StripExts(My...OpenSubKey( "Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\" & Extension). 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.. Return IO..ToString If AppPath IsNot Nothing Then 'if we found something. if it has one If Extension. but if we should try default association.ClassesRoot.NET Beyond the Scope of Visual Basic 6.GetValue("MRUList")...Substring(0.ToString. Return String. or False if the extension was not found or there ' : is no association.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. but if should we try default association.CurrentUser.." if it lacks one End If ElseIf IndexOfDot <> 0 Then 'if dot is not at the start of the string.Registry.IndexOf("""%") 'check for typical decorations If Index = -1 Then Index = AppPath. 1)).Enhancing Visual Basic ."c) 'get the last index of the dot.OpenSubKey("OpenWithList") Dim AppPath As String 'get the name of the application associated with the extension AppPath = ExtKey.Trim. Shell(AppPath.." & Extension 'then precede it with a ".. Return ".Registry.IndexOf("\"c) = -1 Then 'if there is no pathing involved.Registry.GetValue(Nothing).Computer.RegistryKey = My.ToString 'get the default command used for launching the associated application 'trim any trailing decorations.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.GetValue(Nothing). . """"c) 'return the path and remove any decorations End Function End Module '****************************************************************************** ' Copyright © 2015 David Ross Goben.ClickedOnRAT %1" If Idx <> -1 Then AppPath = AppPath.IsNullOrWhiteSpace(AppPath) Then 'some command strings are "%1" "%*" and the like.Enhancing Visual Basic .IndexOf(""""c..0 – David Ross Goben If Index <> -1 Then 'if decorations found. Return String. like. AppPath = AppPath.Empty 'ignore these End If If AppPath.Substring(0.Trim(Chr(0). " "c.Substring(0. 1) = """"c Then 'some are more comples. All rights reserved.. Idx + 1) 'strip to just base path End If End If Return AppPath.Substring(0.NET Beyond the Scope of Visual Basic 6.. 1) '"C:\WINDOWS\System32\rundll32.exe" "C:\WINDOWS\System32\msrating.. '****************************************************************************** Page –580– . Index) 'then trim them off End If If String..dll". Dim Idx As Int32 = AppPath. But the truth is.Computer. once again using a Dial-up service and implementing commando-style internet connection techniques: “get on. To check for the existence of this DLL is really easy.GetFolderPath(Environment. This function also returns a Boolean flag that indicates if the internet was detected or not. internet connections are so common anymore that it surprises many. For example: '******************************************************************************* ' Method : HaveWinInetDll ' Purpose : Return True if the computer has WinInet.System) & "\wininet. Thus. we should dig deeper into the system. that is still no guarantee that the system presently has it.File.NET Beyond the Scope of Visual Basic 6. do one’s business furiously. The second integer is reserved and you should always set it to zero. If the computer has any kind of internet service it will sport a DLL in its system folder (Environment. and so you can invoke a simple function like the following: '******************************************************************************* ' Method : CheckNetworkConnection ' Purpose : Return True if the computer is connected to a network '******************************************************************************* Friend Function CheckNetworkConnection() As Boolean Return My. This DLL is loaded when one installs internet service or when your system recognizes that internet service is available.0 – David Ross Goben Black Book Tip # 53 Determining If a Computer Has an Internet Connection As opposed the dial-up-only connections of the early 1980s and 1990s. If the system does not find this file. then it has determined that the system has a live internet connection. you can rely on the fact that most-all networks anymore make an internet connection available to their clients. but at least you can use this DLL’s InternetGetConnectedState() method it to easily check for the system’s current internet status. The first should be an integer variable that will receive status flags. suiting Jeff Foxworthy’s remark. If this result is True. then it does not have internet service available. “You might be a redneck if… directions to your house include. from landlines.dll") End Function If you do have it.SpecialFolder. and then get off.dll.Exists(Environment. But when I first moved from Southwest Florida to Central Florida in 2005.Network. only after I moved “to town”.System)) named WinInet. we cannot always guarantee that any computer running our software is in fact connected to the internet. wireless phone. then you are connected to a network. except for on-demand Dial-up or satellite internet service. so maybe we need to check before our app tries to connect the user to an online site. For example: Page –581– . to satellite.DLL available '******************************************************************************* Friend Function HaveWinInetDll() As Boolean Return IO. returning to the world of super-fast and always-on cable internet connections did my previous residence finally get DSL service (Digital Subscriber Line) made available as its old copper wires were updated to fiber-optic cables. The DLL’s InternetGetConnectedState() function takes two Integer parameters. ‘Turn off the paved road…’” I had gone from being on-line all the time with a fast cable internet service to living under ‘primitive’ telecommunications conditions. in some instance you are sometimes actually in a home network that might not have an active internet connection via cable or DSL. cable. However.IsAvailable() End Function If invoking the function CheckNetworkConnection() returns True.SpecialFolder.” As one would expect. Most times.Enhancing Visual Basic . I lived for almost a year “out in the sticks”. providing internet service that the thought of needing to check for an internet connection seems an antiquated idea. having become accustomed to all telecom services.GetFolderPath(Environment. The Ics integer will be non-zero if there is any sort of internet connection state. No internet service is active End Function Of course. ByVal dwReserved As Integer) As Boolean '******************************************************************************* ' Method Name : SystemHasInternet ' Purpose : Return True if the system is connected to the Internet '******************************************************************************* Friend Function SystemHasInternet(ByRef Ics As Integer) As Boolean Ics = 0 'init Internet Connection State to no connection If HaveWinInetDll() Then 'see if wininet.Length = 0 Then sName = "Not Configured" End If Page –582– . has become unreliable.icLAN) Then sName &= "LAN " End If If CBool(Ics And icState. Even so. bit 3 has been retired and another.dll file is available.. it is also useful to know what is going on.dll" (ByRef icsFlags As icState.icRASINSTALLED)) = 0 AndAlso (Ics And icState. whether we succeed or fail to find an active Internet connection. even if it is to report that it exists but is presently offline.dll" (ByRef icsFlags As Integer. The first 7 bits of this status field are set aside for status flags..UNRELIABLE/NOT NEEDED IF LAN/RAS DETECTED) End Enum We can check for any of these bits by performing an AND command on the returned Ics integer: Dim sName As String = String.Not Configured" End If End If If sName. If CBool(Ics And icState. These 7 bits are defined as follows: INTERNET_CONNECTION_MODEM = 1 INTERNET_CONNECTION_LAN = 2 INTERNET_CONNECTION_PROXY = 4 INTERNET_CONNECTION_MODEM_BUSY = 8 INTERNET_RAS_INSTALLED = 16 INTERNET_CONNECTION_OFFLINE = 32 INTERNET_CONNECTION_CONFIGURED = 64 '00000001 '00000010 '00000100 '00001000 '00010000 '00100000 '01000000 binary binary binary binary binary binary binary – – – – – – – Bit Bit Bit Bit Bit Bit Bit 0 1 2 3 4 5 6 - Dial-up or DSL Local Area Network Network server funnels service to clients (NO LONGER USED) Remote Access Service Not presently connected (UNRELIABLE/NOT NEEDED IF LAN/RAS DETECTED) We can better manage these in an enumeration and apply it to our P/Invoke declaration: Private Declare Function InternetGetConnectedState Lib "wininet.icPROXY) Then sName &= "Proxy " End If If CBool(Ics And icState.0 – David Ross Goben Private Declare Function InternetGetConnectedState Lib "wininet.icOFFLINE) Then sName &= ".Empty 'init text result If connectionState Then 'if there is a connection..Offline " End If If (Ics And (icState. of these 7 defined bits.icMODEM) Then sName &= "Modem " End If If CBool(Ics And icState. ByVal dwReserved As Integer) As Boolean Friend Enum icState As Int32 icNONE = 0 'No connection icMODEM = 1 '00000001 binary (INTERNET_CONNECTION_MODEM) icLAN = 2 '00000010 binary (INTERNET_CONNECTION_LAN) icPROXY = 4 '00000100 binary (INTERNET_CONNECTION_PROXY) 'icBUSY = &H8 '00001000 binary (INTERNET_CONNECTION_MODEM_BUSY .NO LONGER USED!) icRASINSTALLED = 16 '00010000 binary (INTERNET_RAS_INSTALLED) icOFFLINE = 32 '00100000 binary (INTERNET_CONNECTION_OFFLINE) icCONFIGURED = 64 '01000000 binary (INTERNET_CONNECTION_CONFIGURED .Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6. bit 7. with the dawn of cable-based internet.icRASINSTALLED) Then 'then build a report of the type sName &= "RAS " End If If CBool(Ics And icState. leaving room for possible later expansion in the other bits. A value of zero indicates that there is no service.icLAN Or icState. 0) 'check connection and return True if we have the Internet Catch End Try End If Return False 'failed..icCONFIGURED) = 0 Then sName &= ". Try Return InternetGetConnectedState(Ics. Exists(Environment. ' : 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" (ByRef icsFlags As icState. such as "Modem" or "LAN".SpecialFolder. '******************************************************************************* ' 'Ics (Internet Connection State) return flags (do binary AND on values to verify): ' icState. 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. ByVal dwReserved As Integer) As Boolean Friend Enum icState As Int32 icNONE = 0 'No connection icMODEM = 1 '00000001 binary (INTERNET_CONNECTION_MODEM) icLAN = 2 '00000010 binary (INTERNET_CONNECTION_LAN) icPROXY = 4 '00000100 binary (INTERNET_CONNECTION_PROXY) 'icBUSY = 8 '00001000 binary (INTERNET_CONNECTION_MODEM_BUSY . ' CheckInternetConnectName(): Get the Connection Name as a string (i. ' HaveWinInetDll() : Return TRUE if the computer has WinInet.icNONE Dim connectionActive As Boolean = ComputerHasInternet(Ics) sConnectionName = String.File. Try Return InternetGetConnectedState(Ics.. '******************************************************************************* Friend Function InternetConnected(ByRef eConnectionInfo As icState. Optional ByRef sConnectionName As String = Nothing) As Boolean Dim Ics As icState = icState.0 – David Ross Goben 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 4 useful functions: ' ' CheckNetworkConnection() : Return TRUE if the computer is connected to a network.icCONFIGURED 01000000 binary (64 Decimal) (should always be set on valid connections) '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ '************************************************************************** ' P/Invoke and structure required to gather information for an internet connection '************************************************************************** Private Declare Function InternetGetConnectedState Lib "wininet.dll file is available.DLL available '******************************************************************************* Friend Function HaveWinInetDll() As Boolean Return IO.Computer.Empty If connectionActive Then If CBool(Ics And icState.icLAN 00000010 binary (2 Decimal) Local Area Network ' icState.icOFFLINE 00100000 binary (32 Decimal) Not Presently Connected ' icState..Enhancing Visual Basic . ' : It also optionally returns a connection type in its sConnectionName string.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.IsAvailable() End Function '******************************************************************************* ' Method : HaveWinInetDll ' Purpose : Return TRUE if the computer has WinInet.icRASINSTALLED) Then sConnectionName &= "RAS " End If If CBool(Ics And icState.icMODEM 00000001 binary (1 Decimal) Dial-Up or DSL ' icState.icPROXY 00000100 binary (4 Decimal) Proxy Server ' icState. LAN).System) & "\wininet.icMODEM) Then sConnectionName &= "Modem " End If 'init Internet Connection State to no connection 'see if internet service is available 'init text result 'if it is.dll") End Function '******************************************************************************* ' Method Name : ComputerHasInternet ' Purpose : Return TRUE if the computer is connected to the Internet.icRAS 00010000 binary (16 Decimal) Remote Access Service ' icState.Network.NET Beyond the Scope of Visual Basic 6.. 'then build a report of the type Page –583– .e. ' CheckInternetConnect() : Return TRUE if the computer is connected to the internet (simple) ' CheckInternetConnectType(): Get Internet Connection flags as type icState..GetFolderPath(Environment.NO LONGER USED!) icRASINSTALLED = 16 '00010000 binary (INTERNET_RAS_INSTALLED) icOFFLINE = 32 '00100000 binary (INTERNET_CONNECTION_OFFLINE) icCONFIGURED = 64 '01000000 binary (INTERNET_CONNECTION_CONFIGURED . ' : It also returns status flags in its eConnectionInfo parameter.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.. Not Configured" End If If sConnectionName. '******************************************************************************* Friend Function GetInternetConnectName() As String Dim Ics As icState 'flag field to resutn Internet Connection State Dim sName As String = Nothing 'connection name If InternetConnected(Ics.icLAN) Then sConnectionName &= "LAN " End If If CBool(Ics And icState. ' : A result of 0 means no connection. 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.---' 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 commection 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.icOFFLINE) Then sConnectionName &= ". you can check specific states. HaveWinInetDLL().Offline " End If If (Ics And (icState.icCONFIGURED) = 0 Then sConnectionName &= ".icPROXY) Then sConnectionName &= "Proxy " End If If CBool(Ics And icState. and ComputerHasInternet() functions that we have already outlined.icLAN Or icState. It invokes ComputerHasInternet() (which in turn invokes HaveWinInetDLL()). sName) MsgBox(sName) 'Internet Connection State 'result string 'get status 'display connection type Page –584– . '******************************************************************************* Friend Function GetInternetConnectType() As icState Dim Ics As icState 'flag field to resutn Internet Connection State If InternetConnected(Ics) Then 'if the check successed. icState) 'return IC flags ' &H77 = 0111 0111 binary ' ---. InternetConnected() is a general purpose method.NET Beyond the Scope of Visual Basic 6. By ANDing values specified in icState. and if it is not online or is not connected.. You might want to check for an internet connection ' : first using CheckInternetConnect() above.0 – David Ross Goben If CBool(Ics And icState. sName) Then Return sName Else Return Nothing End If End Function 'determine whether we have a connection: 'we do 'we do not End Module The above module provides the CheckNetworkConnection().TrimEnd 'get optional result text eConnectionInfo = DirectCast(Ics And &H77. For example: Dim Ics As icState Dim sName As String = Nothing Dim Connected As Boolean = InternetConnected(Ics.Length = 0 Then sConnectionName = "Not Configured" End If sConnectionName = sConnectionName.icRASINSTALLED)) = 0 AndAlso (Ics And icState. It also features four other methods. but then it evaluates the returned ICS (Internet Connection State) flags and builds a status string that informs you of what type of connection you have.Enhancing Visual Basic .. NET Beyond the Scope of Visual Basic 6. Cable internet providers typically deliver service by way of a Remote Access Service within a Local Area Network. you have more than enough in your toolbox to easily determine the user’s internet connection state. Page –585– . by ANDing members of the icState enumeration to check specific flags.delivering only a Boolean result.0 – David Ross Goben If you have a Dial-up Modem or DSL service. If the Ics integer is zero. CheckInternetConnect() is a simpler version of ComputerHasInternet(). then there is no connection. which holds the Internet Connection State flags of the computer. then the user does not have an active internet connection. but it delivers only a string result. but it returns an integer of type icState. or else “Modem – Offline”. If you see “Not Connected”. which described the connection type. it might report “Modem” if it is connected. and so you will likely see a status of “RAS LAN”. Of course.Enhancing Visual Basic . you can also ascertain this from the Boolean result stored in the Connected integer. You can compare the value set in the Ics integer variable. CheckInternetType() is also a simple version of ComputerHasInternet(). as demonstrated above. CheckInternetConnect() is another a simple version of ComputerHasInternet(). If the Ics integer is zero. then there is no connection. You can compare this value by ANDing members of the icState enumeration to check specific flags. With these methods. indicating if we have an active internet connection or not. and RR = 2 hex digits for the Red color value (0-255).Color 'init new ARGB structure with Royal Blue 'init Light Blue with Alpha Component at 25% opacity 'init Light Megenta at 50% opacity 'init Magenta with full opacity 'get integer value of Royal Blue 'replace Light Blue with Royal Blue 'replace Light Blue with Argb integer version of Royal Blue 'win32 RGB Blue (&H00FF0000) to Argb Blue (&HFF0000FF) 'convert Argb (&H80FF00DC) to RGB (&H00DC00FF) 'replace Blue 255 value with 128 'change Alpha Component from 255 to 64 'transform changes to a color value The modARGB module containing the ARGB structure is defined on the next page: Page –586– . you can very easily define it as a class. properties. it is easy enough to do by hand.RGB = &HFF0000 Dim vRgb As Integer = ClrC. GG = 2 hex digits for the Green color value (0-255). and constructors. manipulate it at will. an ARGB object is very easy to manipulate. 240) Dim ClrD As New ARGB(255. instead. However.RoyalBlue) Dim ClrB As New ARGB(64. The great thing about structures under Dot NET is that they can feature methods. In that case. The problem is if we want to manipulate a value. or even set or extract a Win32 RGB color value.RGB ClrD. as a color. It is very easy to do. but easier! For example: Dim ClrA As New ARGB(Color. to set the variable Clr of type Color to an Alpha component value of 64 (1/4 transparent) is as simple as “Clr = Color. Although ARGB is a structure. set. I will leave that to you to do if you choose to make it a class.Enhancing Visual Basic . Green. An Argb value is structured in the hexadecimal format &HAARRGGBB. I created a structure named ARGB that can deliver all these services. it would be easier to have a ready object where we can inspect. 0.0 – David Ross Goben Black Book Tip # 54 Manipulating Color Value Members with Ease The ToArgb property of a Color or SystemColors structure is really useful to break individual color values out of a color and to also extract the actual color values defined in a SystemColors member.&hFF. 255) Dim vArgb As Integer = ClrA.Color ClrB. A Win32 Rgb value is structured in the hexadecimal format &H00BBGGRR. Even as a structure. especially when you are performing a dithering operation on the colors of a landscape and you need to add or subtract an offset. where BB = 2 hex digits for the Blue color value. 0.LightBlue) Dim ClrC As New ARGB(128. and then gather the result as an integer. Clr)”. it starts getting a bit convoluted when we try much more than that. and alter its individual color member at will. and BB = 2 hex digits for the Blue color value. To do just a bit of this. You can create a new ARGB structure and initialize it with a color. I still see many gurus on the web advising people “that all you need to do is AND &HFFFFFF to a color’s ToArgb value to get a Win32 RGB value. to give the object persistence as long as it is referenced by something. such as manipulating its individual Red.Argb = vArgb ClrB. Even so. which is structured differently than a Color’s Argb value.Alpha = 64 Dim clr As Color = ClrD. You can even create it much as you would a regular Color structure. RR = 2 hex digits for the Red color value (0-255). such as change its Alpha component value to make a color more transparent.” Maybe that is why there is a lot of anger on some of those forums when that advice leads to some interesting.NET Beyond the Scope of Visual Basic 6. You might notice from the above that in the Argb and Rgb definitions that color values Red and Blue are swapped.FromArgb(64.Argb ClrB. GG = 2 hex digits for the Green color value (0-255). For that reason. Color. and then stuff the value back into a color variable using its FromArgb() method. For example. 255. cleaning up easily in memory when it goes out of scope.Color = ClrA.Blue = 128 ClrD. and Blue color values. though not expected results. where AA = 2 hex digits for the Alpha component value (&h00 . 0-255). You can even set and get color values used by Win32 ' RGB integer values.Blue = Blue 'set Blue color Me. and Blue values (Alpha set to 255. ByVal Blue As Integer) 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 = 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. Red. Argb integer.ToARGB function provides a convenient tool for converting separate ' Alpha. Green. and Blue colors into an Int32 value.Argb = Argb End Sub '******************************************************************************* ' Property : Alpha ' Purpose : Get/Set Alpha Blend value (0-255. 255) '******************************************************************************* Friend Sub New(ByVal Alpha As Integer._argbAlpha = 255 'set fully opaque Me. &H00 . or ' individual color values to the structure. ByVal Green As Integer. and return them ' as you require them. fully opaque) ' Example : Dim Clr As New ARGB(64.Alpha = Alpha 'set Alpha blend Me.&HFF) '******************************************************************************* Friend Property Alpha As Integer Get Return Me. but there is no ' ready way to take an integer value and break these elements back out.Alpha = Alpha End Sub '******************************************************************************* ' Structure: New ' Purpose : Create a new ARGB structure and initialize its values with a ' : specified Alpha blend.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. 128.Blue = Blue 'set Blue color Me. ' ' This structure is rich in properties to assign colors.Enhancing Visual Basic .Convert an ARGB color value back to individual colors._argbAlpha = value And &HFF End Set End Property Page –587– . Green. ByVal Red As Integer._argbAlpha End Get Set(value As Integer) Me. and Blue values ' Example : Dim Clr As New ARGB(255.BackColor) '******************************************************************************* Friend Sub New(ByVal Color As Color) Me. '******************************************************************************* '******************************************************************************* ' 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. ByVal Blue As Integer) Me. Me. The intrinsic ' Color. 255) '******************************************************************************* Friend Sub New(ByVal Red As Integer.0 – David Ross Goben Option Strict On Option Explicit On Module modARGB '******************************************************************************* ' Convert an ARGB color value to and from individual colors '******************************************************************************* ' modARGB . 128. 64.Red = Red 'set Red color Me.NET Beyond the Scope of Visual Basic 6. ByVal Green As Integer.BackColor) '******************************************************************************* Friend Sub New(ByVal Alpha As Integer. Red. Green. manipulate them.Color = Color Me.Red = Red 'set Red color Me. ByVal Color As Color) Me. _argbRed = (value >> 16) And &HFF 'extract red value Me. Me.&HFF) '******************************************************************************* Friend Property Blue As Integer Get Return Me._argbRed End Get Set(value As Integer) Me._argbAlpha._argbAlpha << 24 + Me._argbRed = value And &HFF 'extract blue value from RGB End Set End Property End Structure End Module Page –588– .NET Beyond the Scope of Visual Basic 6._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._argbGreen << 8 + Me.&HFF) '******************************************************************************* Friend Property Red As Integer Get Return Me._argbGreen._argbBlue << 16 + Me. &H00 ._argbBlue End Get Set(value As Integer) Me._argbGreen = (value >> 8) And &HFF 'extract green value from RGB Me._argbBlue) End Get Set(value As Color) Me._argbGreen 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._argbGreen << 8 + Me._argbAlpha = 255 'set fully opaque Me.Enhancing Visual Basic ._argbAlpha = (value >> 24) And &HFF 'extract alpha value Me._argbRed << 16 + Me._argbGreen = (value >> 8) And &HFF 'extract green value Me._argbRed. &H00 . Me.Argb = value._argbBlue = value And &HFF 'extract blue value End Set End Property '******************************************************************************* ' Property : RGB ' Purpose : Get/Set Color using Win32 RGB colors (&H00BBGGRR) '******************************************************************************* Friend Property RGB As Integer Get Return Me._argbBlue = (value >> 16) And &HFF 'extract red value from RGB Me. Me._argbRed End Get Set(value As Integer) Me.&HFF) '******************************************************************************* Friend Property Green As Integer Get Return Me. &H00 .0 – David Ross Goben '******************************************************************************* ' Property : Red ' Purpose : Get/Set Red Color value (0-255._argbGreen = value And &HFF End Set End Property '******************************************************************************* ' Property : Blue ' Purpose : Get/Set Blue Color value (0-255.ToArgb End Set End Property '******************************************************************************* ' Property : Argb ' Purpose : Get/Set Color using Argb integer value (&HAARRGGBB) '******************************************************************************* Friend Property Argb As Integer Get Return Me. or Terabytes.. we can simply invoke it with our long value as a parameter and it will report the size.137..0 – David Ross Goben Black Book Tip # 55 Converting Byte Sizes to Formatted Byte. which we can make even easier by using any previous calculation to simplify the next one.. then MSize. which is clearer but not much better. 'initialize with that size 'if the 'return 'if the 'return 'if the 'return 'if the 'return value is a report value is a report value is a report value is a report in Terabyte range.0# (or 0.. then we have a value that is less than 1 Kilobyte. KB. True)” if we want the byte size to be included in the report. for that size in Megabyte range.. then GSize.00") & " KB" & Result Else Return ByteSize End If 'format Byte size 'init result string 'if we want to report the byte size. which contains the FormatKB() method.122 Bytes)” would make much more sense to them. such as using this function: Private Function GetFileSizeString(ByVal FilePath As String) As String Try If IO. Gigabytes. which returns the size of the file in bytes. Actually. we will want return a result with the appropriate size reported.. Dim fil As New IO.FileInfo(FilePath) 'get file info object for it Dim Amount As Long = fil... such as we get from a System..0# Then Return MSize.Empty If ShowByteSize Then Result = " (" & ByteSize & ")" End If If Fix(TSize) <> 0. GB.0# Then Return GSize.Enhancing Visual Basic .ToString("0.ToString("0. so we simply want to report its Byte size.122”. if all these tests fail. modFormatKB. Even if you go to the trouble to format it.. “6.IO.00") & " TB" & Result ElseIf Fix(GSize) <> 0. KB.. then we will check the MSize value.##0")” in the previous GetFileSizeStreing() method with “Return FormatKB(Amount)”. if “Fix(TSize)”. and finally KSize.0# GSize / 1024.. and so on down the line to KSize.File. is not equal to 0. is to determine which format we should use.Exists(FilePath) Then 'if file exists.956. this is very easy.ToString("0. for that size in Kilobyte range.NET Beyond the Scope of Visual Basic 6. The trick for many. such as replacing the “Return Amount. whether it is TB.0# Then Return KSize.48 GB (6. MB. and if that fails.##0") 'return it as a formatted string End If Catch End Try Return "0" 'fail (file not found End Function It will report “6. for that size 'else < 1KB. or “Return FormatKB(Amount.00") & " GB" & Result ElseIf Fix(MSize) <> 0. If we do find a non-zero whole number. For the user.0# KSize / 1024.0# MSize / 1024. which returns a whole number value of type Double. Megabytes. For example: Dim ByteSize As String = Amount. We first need to break the Long Integer down into the various sizes. MB.ToString("#. for that size in Gigabyte range.Length 'grab byte size of the file Return Amount. a report of “6. For example. it is more useful to report this size in manageable terms. For example: Dim Dim Dim Dim KSize MSize GSize TSize As As As As Double Double Double Double = = = = CDbl(Amount) / 1024. GB. such as Kilobytes. so return Byte size Putting this altogether in a function named FormatKB(). better. Thus.00") & " MB" & Result ElseIf Fix(KSize) <> 0. though.956.0R). My module.FileInfo object’s Length property.ToString("#.##0") & " Bytes" Dim Result As String = String. we would first check TSize. For example.ToString("#. However.0# Then Return TSize. or even just a byte count if it is under 1KB (1024 bytes). or TB Strings When processing file sizes.ToString("0. reporting 6956137122 for a file size might not make much sense to the user.48 GB” or.0# 'compute 'compute 'compute 'compute size size size size in in in in Kilobytes Megabytes Gigabytes Terabytes The next step is to determine the largest result that has a whole number value that is non-zero. follows: Page –589– .137. so return Byte size . There are ' two versions of this method...0 – David Ross Goben Option Strict On Option Explicit On Module modFormatKB '~modFormatKB.70 MB (3. then set optional parameter ByteSize to True.0# 'compute size in Gigabytes Dim TSize As Double = GSize / 1024.Print(FormatKB(MySize..NET Beyond the Scope of Visual Basic 6..##0") & " Bytes" Dim Result As String = String.ToString("0..00") & " TB" & Result ElseIf Fix(GSize) <> 0. for that size 'else < 1KB.ToString("0.0# Then Return KSize.0# Then Return MSize. 'initialize with that size 'if the 'return 'if the 'return 'if the 'return 'if the 'return value is a report value is a report value is a report value is a report in Terabyte range. and the other will accept a Long ' Integer value.0# 'compute size in Terabytes Dim ByteSize As String = Amount. True)) ' The result prints "3. Optional ByVal ShowByteSize As Boolean = False) As String Dim KSize As Double = CDbl(Amount) / 1024.32 MB.882. One will accept an Integer value.0# Then Return GSize.41 KB" '**************************************************** ' modFormatKB: ' The provided FormatKB functions helps you convert file sizein bytes into proper strings such as ' "1. for that size in Gigabyte range. 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.bas..70 because 1KB=1024 bytes.32 MB.32 MB". Optional ByVal ShowByteSize As Boolean = False) As String Return FormatKB(CLng(Amount).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.00") & " GB" & Result ElseIf Fix(MSize) <> 0.0# 'compute size in Megabytes Dim GSize As Double = MSize / 1024.41 KB" or "1." ' : To also include byte size (if not < 1K).ToString("0.445 Bytes)" ' 'NOTE: It printed 3." ' : To also include byte size (if not < 1K)..IO.0# Then Return TSize.Enhancing Visual Basic .0# 'compute size in Kilobytes Dim MSize As Double = KSize / 1024.. for that size in Kilobyte range.ToString("0. '**************************************************** Public Function FormatKB(ByVal Amount As Long.FileInfo object. '**************************************************** Public Function FormatKB(ByVal Amount As Int32.00") & " MB" & Result ElseIf Fix(KSize) <> 0..00") & " KB" & Result Else Return ByteSize End If End Function End Module Page –590– 'format Byte size 'init result string 'if we want to report the byte size. 'Convert file size in bytes into proper strings such as "1. for that size in Megabyte range. 1MB=1. as might be returned by a System. then set optional parameter ByteSize to True.41 KB" or "1.Empty If ShowByteSize Then Result = " (" & ByteSize & ")" End If If Fix(TSize) <> 0. An optional Boolean flag allows the byte size to be appended.ToString("#..048. ' 'EXAMPLE: ' Dim MySize As Long = 3882445 ' Debug. or BPP. 8 bpp.768 colors and 216 = 65.ToString & " Colors" Select Case BPP Case 1. 22 = 4 colors. is that most everyone defines the DEVMODE structure required by the method incorrectly.294.Bounds. with the advent of digital monitors.536 colors. using the VBFixedString declaration works fine.0#. True Color values are 224 = 16.ToString & "x" & . and we can also compute its color depth using its BitsPerPixel parameter.) For translating CHAR.ToString & " pixels. and so data is incorrectly copied into the structure. But the mapping is already off.ToString & "x" &.PrimaryScreen Dim BPP As Int32 = . 24 = 16 colors. color format unknown" Else Return . This value can be 1. " & BPP.296 colors. a value of 32 is becoming typical. nn bpp. or they need to change these settings. 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”. might not seem easy to do. there is a P/Invoke named EnumScreenSettings() that most everyone knows about that that can be used to acquire available screen settings for a system’s display. (CCHDEVICENAME and CCHFORMNAME are integer constants set to a value of 32. 256 Colors" End If End With End Select End With End Function Even so.ToString & " bpp. Page –591– . it does work. If we raise 2 to the power of the BPP value. you will get the number of colors available.Pow(2. The problem. 16 : Return Result & " (High Color)" Case 24. which this structure does not provide. Actually. 21 = 2 colors. this function will return a text report of the resolution settings for the current display: '******************************************************************************* ' Method Name : GetCurrentDisplaySize ' Purpose : Return the settings for the current display as a string. 15.777. 2.0 – David Ross Goben Black Book Tip # 56 Getting. 8. 4. or 32. The bits per pixel value. We are able to collect values for the current display’s resolution in pixels using the Screen. sometimes developers also require the display frequency or the fixed display rendering mode. or they simply need to gather more information than they can pick up from the Screen.Bounds.Height.Width. 4.Height. and quite well. But to get that information.967. perhaps to offer them as display options for a game so the user can play them in a higher or lower resolution.Width. but most people who use it claim it does not work.BitsPerPixel Dim Result As String = . formatted ' : "XXXxYYY pixels. you get an unhandled exception error because the structure ends up being one character too long for each string. can be used to easily compute how many possible colors can be assigned to each pixel on the display for the setting it is assigned to. 8 : Return Result Case 15. however. Picture Element) from a 32-bit integer to a 16-bit short integer. as I might guess from the large number of people on the web who have been asking how to do it. 24.Enhancing Visual Basic . If you pass a DEVMODE structure through the EnumScreenSettings() function with its string members declared with VBFixedString.216 colors and 232 = 4. " & Math. though of late.ToString & ". BPP). For example.PrimaryScreen structure.PrimaryScreen structure’s Width and Height parameters. High Color values are 215 = 32. and 28 = 256 colors. Enumerating. and Changing Screen Settings Often developers need to get a list of all possible screen settings available for the system running their application.NET Beyond the Scope of Visual Basic 6. 2.Bounds If BPP > 0 Then Return Result & ". 16. Many people try to “fix” it by changing the dmBitsPerPel member (Pel is an older form of Pixel. This is not so for types TCHAR or BYTE. nnnn Colors" '******************************************************************************* Friend Function GetCurrentDisplaySize() As String With Screen. Granted. 32 : Return Result & " (True Color)" Case Else 'last ditch effort With . union { DWORD dmDisplayFlags. short dmScale.. DWORD dmPanningHeight.0 – David Ross Goben Worse.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 Page –592– . short dmTTOption..NET Point structure (two-Int32 values. short dmPrintQuality. short dmDuplex. and long pointer DEVMODE If we understand that WORD and short represent VB Short integers (Int16). WORD dmLogPixels. but not with this //WORD = Short Integer or Int16 //NOTE: ANSI TCHAR is equivalent to BYTE... }. DWORD dmDisplayFixedOutput.Runtime.) <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. selects the size of the paper to print on <FieldOffset(4)> Friend dmPaperLength As Int16 'For printer only. 'create a 16-byte union of 8 int16 printer values over a Point (8 bytes) and 2 int32 values (8 bytes) <StructLayout(LayoutKind.. they typically use a DEVMODE structure that is formatted for a printer. // the following declarations are used only by a printer device. let us first examine how MSDN (Microsoft Development Network) defines the DEVMODE structure in C++ in their WINGDI. 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. TCHAR dmFormName[CCHFORMNAME]. DWORD dmReserved1. DWORD dmPelsHeight.. pointer PDEVMODE. WORD dmDriverVersion. short dmCopies. dmNup for a printer) <StructLayout(LayoutKind. To clarify this point. WORD dmSpecVersion. short dmColor. #if (WINVER >= 0x0500) || (_WIN32_WINNT >= 0x0400) DWORD dmPanningWidth. DWORD dmDisplayOrientation. }. DWORD dmReserved2.Explicit)> Friend Structure DEVMODE_union1 ' struct { <FieldOffset(0)> Friend dmOrientation 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. short dmYResolution.. #if (WINVER >= 0x0400) DWORD dmICMMethod. short dmCollate. we can construct our DEVMODE structure properly. *LPDEVMODEA.2. Following is the corrected DEVMODE structure. Be sure to also include “Imports System. DWORD represents VB Integers (Int32).1. //presently must be set to 0 //presently must be set to 0 //if Window98/WinNT40 or greater. short dmPaperSize. and the Union allows more than one set of variables to occupy the same space. 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=. POINTL is a VB Point structure. union { struct { short dmOrientation. //presently must be set to 0 (used by printers) //presently must be set to 0 (used by printers) //assign structure to DEVMODE. WORD dmSize. struct { POINTL dmPosition. vbFixedString works with CHAR. consuming 16 bytes //this second structure also consumes 16 bytes //POINTL is the same as a . DWORD dmICMIntent. WORD dmDriverExtra. //Declare a Structure of type _devicemode //TCHAR = Unmanaged Type ByValTStr.InteropServices” at the top of your class or module. DWORD dmDisplayFrequency. DWORD dmMediaType. The Unicode version is WCHAR //DWORD = Integer or Int32 //This union will allow the following two structures to use the same space //this first structures declares 8 short integers. including the definition for the EnumScreenSettings() P/Invoke. using 8 bytes) //this union allows dmDisplayFlags and dmNup to occupy the same space //if WinNT40 or greater.Enhancing Visual Basic .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. DWORD dmDitherType. }. short dmPaperLength. *PDEVMODEA. that TCHAR (same as BYTE) should be interpreted as an unmanaged ByVal Fixed-Length String.. short dmPaperWidth. selects the orientation of the paper <FieldOffset(2)> Friend dmPaperSize As Int16 'For printer only. short dmDefaultSource. }. the orientation at which images should be presented <FieldOffset(12)> Friend dmDisplayFixedOutput As Int32 'For fixed-resolution displays. DWORD dmNup.NET Beyond the Scope of Visual Basic 6. DWORD dmPelsWidth. not the rendering of it that is designed for displays. #endif #endif } DEVMODEA. DWORD dmFields.h header file: typedef struct _devicemodeA { TCHAR dmDeviceName[CCHDEVICENAME]. DWORD dmBitsPerPel. ' To retrieve information for all the graphics modes of a display device.ByValTStr. the same BPP. dmDisplayFlags. which properly translates type TCHAR (and BYTE). What is going on. ' This method sets the dmBitsPerPel. you should notice an 8-short integer block (16 bytes) that differs between printers and displays. in bytes.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)>. in hertz (cycles per second). consume the other 8 bytes. not for a display screen. SizeConst:=CCHFORMNAME)> Friend dmFormName As String Friend dmLogPixels As Int16 'The number of pixels per logical inch. of the visible device surface Friend dmPelsHeight As Int32 'Specifies the height. "Letter" or "Legal" <MarshalAs(UnmanagedType. of the visible device surface '--------------------------------------Friend u2 As DEVMODE_union2 '--------------------------------------Friend dmDisplayFrequency As Int32 'Specifies the frequency.0 – David Ross Goben <StructLayout(LayoutKind. their next complaint is usually that there are a number of duplicate display settings. such as none. and the two additional 32-bit integer members. 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 Friend Const DMDFO_CENTER As Int32 = 1 Friend Const DMDFO_STRETCH As Int32 = 2 'The display's default setting. make a series of invocations to this function. For printers. ByRef lpDevMode As DEVMODE) As Boolean This version of the DEVMODE structure actually does work. course. the system examines this member to determine how to handle ICM support Friend dmICMIntent As Int32 'Specifies which color matching method. such as 60hz. For people who had managed to get DEVMODE to work with the P/Invoke. Printer drivers do not use this member Friend dmBitsPerPel As Int32 'Specifies the color resolution. should be used by default Friend dmMediaType As Int32 'Specifies the type of media being printed on. or grayscale Friend dmReserved1 As Int32 'Not used. in pixels. this 16-byte field is consumed by 8 short 16-bit integers that are specifically geared to printers. dmPelsWidth. dmDisplayOrientation and dmFixedDIsplayOutput. ByVal iModeNum As Int32. The use of vbFixedString only works with type CHAR (though. but usually unseen. fine. 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 displays. incrementing from 0. of the display device in a particular mode 'All the following are for Image Color Management For printers Friend dmICMMethod As Int32 'For ICM applications. or transparency Friend dmDitherType As Int32 'Specifies how dithering is to be done. Private Declare Function EnumDisplaySettings Lib "user32. and this will throw a fixed-size structure’s footprint off). For this. or intent. Must be ByValTStr) <MarshalAs(UnmanagedType. and for both printers and display devices. which have the same X and Y values. must be zero Friend dmReserved2 As Int32 'Not used. But the truth is that they are not duplicates. please refer to the dmDisplayFixedOutput member. The function will return TRUE if the index is valid. is that additional versions are set aside for lowerresolution modes being displayed on higher-resolution displays. glossy. in pixels. not with TCHAR or BYTE. such as standard. FALSE indicates the index was out of range. dmPelsHeight. in dots per inch. the dmPosition member’s Point structure consumes 8 bytes. most structures do tend to use type CHAR). 'The low-resolution image is stretched to fill the larger screen space. If you examine all of the presumed “duplicates”. lineart. for example.Enhancing Visual Basic .ByValTStr. using iModeNum ' as an index. Also. 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 '--------------------------------------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. mind you.NET Beyond the Scope of Visual Basic 6.DLL" Alias "EnumDisplaySettingsA" ( ByVal lpszDeviceName As IntPtr. and even the same display frequency. 'The low-resolution image is centered in the larger screen space. 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. Notice that the two string members are declared as Unmanaged Type ByValTStr. 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. of the display device Friend dmPelsWidth As Int32 'Specifies the width. you will in fact find each is sporting an entirely different dmDisplayFixedOutput value. in bits per pixel. 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). Page –593– . and dmDisplayFrequency members of the DEVMODE structure. course.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. Provide the DEVMODE structure to the lpDevMode ' parameter.NET Beyond the Scope of Visual Basic 6. such as standard. ' To retrieve information for all the graphics modes of a display device. dmNup for a printer) <StructLayout(LayoutKind. 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=.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. using iModeNum ' as an index. dmPelsWidth. Printer drivers do not use this member Friend dmBitsPerPel As Int32 'Specifies the color resolution. and dmDisplayFrequency. set the desired member values (if not already set) to dmBitsPerPel. 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. The function will return TRUE if the index is valid. in pixels. ByVal iModeNum As Int32. overrides the length of the paper specified by the dmPaperSize member <FieldOffset(6)> Friend dmPaperWidth As Int16 'For printer only. ' This method sets the dmBitsPerPel. dmDisplayFlags. in bytes. of the display device in a particular mode 'All the following are for Image Color Management For printers Friend dmICMMethod As Int32 'For ICM applications. FALSE indicates the index was out of range. valid DEVMODE structure. Private Declare Function EnumDisplaySettings Lib "user32.Enhancing Visual Basic . or transparency Friend dmDitherType As Int32 'Specifies how dithering is to be done. the system examines this member to determine how to handle ICM support Friend dmICMIntent As Int32 'Specifies which color matching method. ByRef lpDevMode As DEVMODE) As Boolean ' The ChangeDisplaySettings function changes the settings of the default display device to the specified graphics mode.DLL" Alias "ChangeDisplaySettingsA" ( ByRef lpDevMode As DEVMODE.) <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. or grayscale Friend dmReserved1 As Int32 'Not used. of the display device Friend dmPelsWidth As Int32 'Specifies the width. or intent. in hertz (cycles per second).InteropServices Module modEnumScreenResolutions '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'create a 16-byte union of 8 int16 printer values over a Point (8 bytes) and 2 int32 values (8 bytes) <StructLayout(LayoutKind. 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. such as none. and dmDisplayFrequency members of the DEVMODE structure. glossy.ByValTStr. DM_DISPLAYFREQUENCY. dmPelsHeight.. SizeConst:=CCHFORMNAME)> Friend dmFormName As String Friend dmLogPixels As Int16 'The number of pixels per logical inch.. of the visible device surface '--------------------------------------Public u2 As DEVMODE_union2 '--------------------------------------Friend dmDisplayFrequency As Int32 'Specifies the frequency. dmPelsWidth.ByValTStr. in bits per pixel. Refer to the DISP_CHANGE constants for result flags. 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.0 – David Ross Goben Consider my module. should be used by default Friend dmMediaType As Int32 'Specifies the type of media being printed on. Be sure to apply each associated flag to the dmFields member: DM_BITSPERPEL. ByVal dwFlags As Int32) As Integer Page –594– . ' dmDisplayFlags. lineart. must be zero Friend dmReserved2 As Int32 'Not used. fine. modEnumDisplaySettings: Option Strict On Option Explicit On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modEnumScreenResolutions Static Module Class ' Enumerate current system's screen resolututions '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Imports System. ' DM_PELSHEIGHT. DM_PELSWIDTH. in dots per inch. dmPelsHeight. Public Declare Function ChangeDisplaySettings Lib "user32. DM_DISPLAYFLAGS. and the appropriate CDS constants to the dwFlags parameter.DLL" Alias "EnumDisplaySettingsA" ( ByVal lpszDeviceName As IntPtr. make a series of invocations to this function. or DM_POSITION (OR multiples).2. incrementing from 0. Must be ByValTStr) <MarshalAs(UnmanagedType. "Letter" or "Legal" <MarshalAs(UnmanagedType. the orientation at which images should be presented <FieldOffset(12)> Friend dmDisplayFixedOutput As Int32 'For fixed-resolution displays.1. ' In an existing.Runtime. for example. selects the size of the paper to print on <FieldOffset(4)> Friend dmPaperLength As Int16 'For printer only. selects the orientation of the paper <FieldOffset(2)> Friend dmPaperSize As Int16 'For printer only.Explicit)> Friend Structure DEVMODE_union1 ' struct { <FieldOffset(0)> Friend dmOrientation As Int16 'For printer only. in pixels. 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. of the visible device surface Friend dmPelsHeight As Int32 'Specifies the height.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)>. 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 '--------------------------------------Public 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. 4 bpp.dmPelsWidth. Only valid when specified with ' 'the CDS_UPDATEREGISTRY flag. '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '******************************************************************************* ' Method : EnumScreenResolutions ' Purpose : Build a List of type DEVMODE of all screen resolutions avaiable for the primary display. ' 'This is useful when testing different resolutions. then 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. then assume single selection Return lst 'return empty list if out of range End If '----------------------------------------------------------------------Do While EnumDisplaySettings(IntPtr.Initialize() DM. ' If you supply ENUM_CURRENT_SETTINGS (-1). Friend Const CDS_FULLSCREEN As Int32 = &H4 'The mode is temporary in nature.dmSize = CShort(Len(DM)) 'set size of structure (important!) Dim iModeNum As Int32 = SelectIndex 'init DevMode index to base of list (typically 0) If SelectIndex < ENUM_REGISTRY_SETTINGS Then 'if out of range. 16 Colors Page –595– . It is the default. so simplify access Dim result As String = DM. ' NOTES: If you supply a non-zero Selected Index. " & bpp. Friend Const DISP_CHANGE_BADPARAM As Int32 = -5 'An invalid parameter was passed. Friend Const CDS_NORESET As Int32 = &H10000000 'The settings will be saved in the registry. Friend Const DMDO_90 As Int32 = 1 'The display is rotated 90 degrees (clockwise) from DMDO_DEFAULT.967.Enhancing Visual Basic . This can include an invalid flag Or combination Of flags. Display Fixed Output = Default. Friend Const CDS_RESET As Int32 = &H40000000 'The settings should be changed.ToString & " bpp. even if the requested settings are the same as the ' 'current settings. Friend Const DISP_CHANGE_NOTUPDATED As Int32 = -3 'Unable to write settings to the registry. 'change flags user assigns to dmFields in DEVMODE structure when applying a new setting to a display. only the current display setting will be returned. '0 'The graphics mode for the current screen will be changed dynamically. Friend Const CDS_SET_PRIMARY As Int32 = &H10 'This device will become the primary device. Default Orientation" '******************************************************************************* Friend Function EnumScreenResolutions(Optional SelectIndex As Int32 = 0) As List(Of DEVMODE) Dim lst As New List(Of DEVMODE) 'init list to return to invoker Dim DM As New DEVMODE 'structure to receive each valid screen mode 'DM. System Default dpi. Friend Const DMDO_270 As Int32 = 3 'The display is rotated 270 degrees (clockwise) from DMDO_DEFAULT 'data for DEVMODE's dmDisplayFixedOutput member Friend Const DMDFO_DEFAULT As Int32 = 0 Friend Const DMDFO_CENTER As Int32 = 1 Friend Const DMDFO_STRETCH As Int32 = 2 'The display's default setting. Friend Const DM_BITSPERPEL As Int32 = &H00040000 'apply setting assigned to dmBitsPerPel Friend Const DM_PELSWIDTH As Int32 = &H00080000 'apply setting assigned to dmPelsWidth Friend Const DM_PELSHEIGHT As Int32 = &H00100000 'apply setting assigned to dmPelsHeight Friend Const DM_DISPLAYFLAGS As Int32 = &H00200000 'apply setting assigned to dmDisplayFlags Friend Const DM_DISPLAYFREQUENCY As Int32 = &H00400000 'apply setting assigned to dmDisplayFrequency Friend Const DM_POSITION As Int32 = &H00000020 'apply setting assigned to dmPosition 'dwFlags parameter values for ChangeDisplaySettings().294. Friend Const ENUM_REGISTRY_SETTINGS As Int32 = -2 'Retrieve the settings for the display device that are stored in the registry. ' The returned List will contain one or more DEVMODE structures. Friend Const CDS_GLOBAL As Int32 = &H8 'The settings will be saved in the global settings area.0 – David Ross Goben 'optional Indexes for EnumDisplaySettings()'s iModeNum parameter Friend Const ENUM_CURRENT_SETTINGS As Int32 = -1 'Retrieve the current settings for the display device.dmBitsPerPel 'commonly used value. The mode information is stored in the USER profile. Friend Const DISP_CHANGE_RESTART As Int32 = 1 'The computer must be restarted for the graphics mode to work. 'The low-resolution image is stretched to fill the larger screen space. only the display setting defined in the registry will be returned.000") & " Colors" 'ie: 640x480 pixels.ToString & " pixels. Friend Const DISP_CHANGE_FAILED As Int32 = -1 'The display driver failed the specified graphics mode.NET Beyond the Scope of Visual Basic 6. Friend Const CDS_TEST As Int32 = &H2 'The system tests if the requested graphics mode could be set. If you change to and from another desktop. 'result flags returned by ChangeDisplaySettings() Friend Const DISP_CHANGE_SUCCESSFUL As Int32 = 0 'The settings change was successful. " & Math.dmPelsHeight.Add(DM) 'add acquired DEVMODE structure to list If SelectIndex <> 0 Then 'if user specified an index. 'orientation setting for DEVMODE's dmDisplayOrientation member Friend Const DMDO_DEFAULT As Int32 = 0 'The display is in the natural orientation. 32 bpp. so the "current" might not ' 'be the setting saved off in the registry. Only valid when ' 'specified with the CDS_UPDATEREGISTRY flag. 4. it will return a list containing only that indexed resolution. You can feed any one of them to the ' GetResolutionString() function to get a string representation of the display.Pow(2. 'The low-resolution image is centered in the larger screen space. this mode ' 'will not be reset. bpp). 'OR constants for each associated dm-parameter you assign using ChangeDisplaySettings(). Friend Const DISP_CHANGE_BADFLAGS As Int32 = -4 'An invalid set of flags was passed in. ' If you supply ENUM_REGISTRY_SETTINGS (-2). DM) 'grab the indexed resolution while the index is valid lst. Friend Const DMDO_180 As Int32 = 2 'The display is rotated 180 degrees (clockwise) from DMDO_DEFAULT.296 Colors (True Color). such as: ' "1920x1080 pixels. Friend Const DISP_CHANGE_BADDUALVIEW As Int32 = -6 'The settings change was unsuccessful because the system is DualView capable. Friend Const CDS_UPDATEREGISTRY As Int32 = &H1 'The graphics mode for the current screen will be changed dynamically and the graphics ' 'mode will be updated in the registry.ToString("0. Friend Const DISP_CHANGE_BADMODE As Int32 = -2 'The graphics mode is not supported. but will not take effect.0#. iModeNum.ToString & "x" & DM.Zero. 60 hz. Display Fixed Output = Center. 4.296 Colors (True Color). Default Orientation 5. Display Fixed Output = Default. Display Fixed Output = Center. 4. System Default dpi. 32 bpp. 4.967. Display Fixed Output = Default. Default Orientation 36. 32 bpp. Default Orientation 21. 4.296 Colors (True Color).967. Display Fixed Output = Default.296 Colors (True Color). 1280x720 pixels.294.296 Colors (True Color). Default Orientation 35.294. System Default dpi. " End If If DM. Landscape" Case Else result &= ". Display Fixed Output = Stretch. System Default dpi. System Default dpi. 4.296 Colors (True Color). 59 hz. 60 hz.967.296 Colors (True Color). Display Fixed Output = Default. Display Fixed Output = Default. 1280x800 pixels.967. Default Orientation 29. 32 bpp.294.296 Colors (True Color).967. Default Orientation 8. 32 bpp. 4. " Else result &= ". 4.294. Default Orientation 43.296 Colors (True Color). 60 hz. 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.967. System Default dpi.296 Colors (True Color).294.NET Beyond the Scope of Visual Basic 6. Display Fixed Output = Stretch.296 Colors (True Color). Display Fixed Output = Default.296 Colors (True Color).967.967.296 Colors (True Color).296 Colors (True Color). Display Fixed Output = Default.967. Display Fixed Output = " Select Case DM. 640x480 pixels. 4. System Default dpi. Default Orientation 33. Default Orientation 11.294.967. System Default dpi. 59 hz. 60 hz. 32 bpp.ToString("00") & " hz" End If result &= ". 4. 32 bpp. 1920x1080 pixels.ToString & " dpi. System Default dpi.. 1920x1080 pixels.967. 1024x600 pixels. Display Fixed Output = Default.967.294. 60 hz. 720x576 pixels. 1280x1024 pixels. 32 bpp. 32 bpp.296 Colors (True Color). System Default dpi. 4. 60 hz. Display Fixed Output = Default. System Default dpi. " & DM. System Default dpi. 1920x1080 pixels. 60 hz. 1280x720 pixels. 720x576 pixels. Display Fixed Output = Default. Default Orientation 44.296 Colors (True Color).294. System Default dpi.967. 56 hz. Display Fixed Output = Default. 60 hz. Display Fixed Output = Stretch.dmDisplayFrequency = 1 Then result &= "Hardware default" Else result &= DM. Default Orientation 6. 32 bpp. 4. 32 bpp.dmLogPixels = 0 Then result &= ". 1600x900 pixels. 1280x768 pixels.294. 32 bpp. 32 bpp. 56 hz.294.296 Colors (True Color). System Default dpi. 32 bpp.296 Colors (True Color). Default Orientation 28. 640x480 pixels. Display Fixed Output = Default. 4. Default Orientation 7. Display Fixed Output = Default. Default Orientation 12. Display Fixed Output = Default. Default Orientation 31.294. Display Fixed Output = Default. 30 hz.294.294.294. " & GetResolutionString(DM)) Idx += 1 Next 'enumerate all available display settings 'init index 'now list them all in Debug 'display string report for each gathered DEVMODE structure 'bump index The EnumScreenResolution() method will return a strong list of type DEVMODE for all settings available for your display. Default Orientation 14.294. 1024x768 pixels. System Default dpi. 50 hz. 60 hz. 4. 60 hz. Default Orientation 22. 800x480 pixels.294. 800x480 pixels. 60 hz. 1920x1080 pixels. 720x480 pixels.294. System Default dpi. Display Fixed Output = Default. 4. System Default dpi. 1280x960 pixels.296 Colors (True Color). 59 hz. Portrait" Case DMORIENT_LANDSCAPE result &= ". 32 bpp. 1600x900 pixels. 50 hz. 59 hz. Default Orientation 4. 800x600 pixels. 1280x768 pixels. 32 bpp.294.296 Colors (True Color). 4. 1400x1050 pixels. 1024x600 pixels. 4. System Default dpi.967. 32 bpp.u1. Default Orientation 23. 60 hz. 32 bpp. 800x480 pixels. System Default dpi. 60 hz. 32 bpp. 32 bpp. 60 hz. 32 bpp. 1024x600 pixels.296 Colors (True Color). Default Orientation 42. For example: Dim lst As List(Of DEVMODE) = EnumScreenResolutions() Dim Idx As Int32 = 0 For Each DM As DEVMODE In lst Debug. etc) 'how low-res mode is displayed in hi-res display 'do as setting require 'The low-resolution image is centered in the larger screen space 'The low-resolution image is stretched to fill the larger screen space 'return formatted string To take advantage of this module is really easy. 32 bpp.967.294. Display Fixed Output = Default.294. 32 bpp. Default Orientation 32.296 Colors (True Color).296 Colors (True Color).294.967. System Default dpi. Default Orientation 26. Display Fixed Output = Center. System Default dpi.296 Colors (True Color). 60 hz.294. Display Fixed Output = Default.967.296 Colors (True Color).967.967.296 Colors (True Color). 4. 32 bpp. 56 hz. System Default dpi.296 Colors (True Color). System Default dpi. System Default dpi. Default Orientation 25.967. Default Orientation 39. Display Fixed Output = Center. System Default dpi. 32 bpp. Default Orientation 19. System Default dpi. Default Orientation 18. System Default dpi.296 Colors (True Color). Default Orientation" End Select Return result End Function End Module 'check dots per inch 'if display frequency is set to its hardware default. 4. Default Orientation 3.296 Colors (True Color). 4. 32 : result &= " (True Color)" End Select If DM. Display Fixed Output = Default.296 Colors (True Color).296 Colors (True Color). 60 hz. 1400x1050 pixels. Default Orientation 38.294. 4. Default Orientation 1.296 Colors (True Color). 32 bpp.ToString & ".967. System Default dpi.967. 1280x720 pixels.294.296 Colors (True Color). 720x576 pixels.967. 32 bpp.294. Display Fixed Output = Default. System Default dpi. 4. System Default dpi.294.296 Colors (True Color). 60 hz. 60 hz. System Default dpi. 60 hz. Display Fixed Output = Default.967. 4. 4.294. 1280x768 pixels. 60 hz. Display Fixed Output = Stretch. 4. 800x480 pixels.967. 60 hz. 32 bpp. Default Orientation 37.296 Colors (True Color).294.296 Colors (True Color).294.967. 4. 4.Print(Idx.294. 4.296 Colors (True Color). 60 hz..294. Display Fixed Output = Default. 60 hz. Default Orientation Page –596– . System Default dpi.967. 1680x1050 pixels. 4. 32 bpp. Display Fixed Output = Center. 'else display frequency (ie. Display Fixed Output = Default.967. 32 bpp. 4. System Default dpi. 1776x1000 pixels.dmDisplayOrientation Case DMORIENT_PORTRAIT result &= ". 32 bpp. 720x480 pixels. System Default dpi. 32 bpp.967.967. 60 hz. 32 bpp. 800x480 pixels.294.294. 32 bpp. 720x576 pixels.294. 16 : result &= " (High Color)" Case 24. 4. my Display has the following 45 settings (indexed 0-44): 0.294. System Default dpi. 32 bpp. 4.294. Default Orientation 34. 4. 32 bpp. Default Orientation 41. Display Fixed Output = Default. 1440x900 pixels. System Default dpi.967.296 Colors (True Color).967.296 Colors (True Color). System Default dpi. 1600x1200 pixels.967. Default Orientation 2.0 – David Ross Goben Select Case bpp Case 15. 1152x648 pixels. Display Fixed Output = Default. Default Orientation 17. 50 hz. 60 hz. 4.296 Colors (True Color). 32 bpp. 32 bpp. 4. Display Fixed Output = Default.967. Default Orientation 15. 32 bpp. 4.dmLogPixels. 60 hz. 800x600 pixels. 60 hz. Default Orientation 20.dmDisplayFrequency.967. Display Fixed Output = Default. Default Orientation 13. 4. For example. 60 hz.294.296 Colors (True Color). System Default dpi. System Default dpi.Enhancing Visual Basic .296 Colors (True Color). Display Fixed Output = Default. 1400x1050 pixels. System Default dpi. 32 bpp. Display Fixed Output = Center. System Default dpi.296 Colors (True Color). 4. Display Fixed Output = Stretch.967. 32 bpp. 1920x1080 pixels. 32 bpp. 800x480 pixels.967.967.296 Colors (True Color). System Default dpi.967.294.967. 60 hz.294. Default Orientation 30. 50 hz. System Default dpi. Default Orientation 10.294. 4.u1. System Default dpi. 4. 60 hz. 32 bpp.967.967. 56 hz.dmDisplayFixedOutput Case DMDFO_DEFAULT result &= "Default" Case DMDFO_CENTER result &= "Center" Case DMDFO_STRETCH result &= "Stretch" End Select Select Case DM. Default Orientation 9. Default Orientation 40. Display Fixed Output = Default. System Default dpi. Display Fixed Output = Default. System Default dpi. 60 hz. 4. 60 hz.294. Display Fixed Output = Default. Display Fixed Output = Stretch. 29 hz. System Default dpi.296 Colors (True Color). 32 bpp. 50 hz. 1600x900 pixels. 32 bpp. Default Orientation 16.294. Display Fixed Output = Stretch. 56 hz.294. Default Orientation 24.967. Display Fixed Output = Center. 4. 4. 4.294. 32 bpp.967. Default Orientation 27.296 Colors (True Color).294.967. 4. 4. 32 bpp. 4.. in case the display is not able to support the selected display resolution for some reason.296 Colors (True Color). 'OR constants for each associated dm-parameter you assign using ChangeDisplaySettings(). Mine returns: 1920x1080 pixels. 10. so the “current” resolution might not be the one currently set in the registry. CDS_TEST) If Result = DISP_CHANGE_SUCCESSFUL Then Result = ChangeDisplaySettings(DMtemp.") DMorg.967.294.dmFields = DM_PELSWIDTH Or DM_PELSHEIGHT ChangeDisplaySettings(DMorg.NET Beyond the Scope of Visual Basic 6. you can change from one to six member values and you must also assign each changed member’s associated flag to dmFlags using: 'change flags user assigns to dmFields in DEVMODE structure when applying a new setting to a display. Also notice that after the ChangeDisplaySetting() method was invoked without the CDS_TEST parameter.296 Colors (True Color).296 Colors (True Color). 60 hz. CDS_UPDATEREGISTRY Or CDS_RESET) End If End If 'get current setting (grab only DEVMODE entry in list) 'make a copy for changing resolution 'change to 800x600 resolution 'inform system these parameters are to change 'first test changes to see if they are OK. as an optional parameter to the EnumScreenResolutions() method (EnumScreenResolutions(-1)) to gather this data. such as: 4.. System Default dpi. ENTER to reset. or -2. modify it. CDS_UPDATEREGISTRY Or CDS_RESET) If Result = DISP_CHANGE_SUCCESSFUL Then MsgBox("800x600.dmPelsHeight = 600 DMtemp.294.967. 60 hz. This is always a safe practice.Enhancing Visual Basic .dmFields = DM_PELSWIDTH Or DM_PELSHEIGHT Dim Result As Int32 = ChangeDisplaySettings(DMtemp. Default Orientation If you are changing the resolution. you can also see that some of the entries.296 Colors (True Color). If you send a non-zero index as a parameter to EnumScreenResolutions(). I tend to just use a copy of the current setting. As shown in the module. it will return the Selecting a New Display Setting You are able to select a new display setting by using one of the DEVMODE structures that was returned in a list from the EnumScreenResolution() method.. 720x576 pixels. 32 bpp. 60 hz. 'actually apply new settings to the display 'if we succeeded in the test. System Default dpi. 32 bpp. As such. Default Orientation (dmDisplayFixedOutput = 0) 5.0 – David Ross Goben Apart from noticing that my 32-inch 16:9 digital monitor features only 32-bit color settings. Default Orientation (dmDisplayFixedOutput = 1) 6. you can gather the settings directly from the registry by sending ENUM_REGISTRY_SETTINGS. which one might at first assume to be duplicates. System Default dpi.294. 720x576 pixels. For example: Dim DMorg As DEVMODE = EnumScreenResolutions(ENUM_CURRENT_SETTINGS)(0) Dim DMtemp As DEVMODE = DMorg DMtemp. registry-based settings. You must then inform the system of all parameters you want to change that differ from the current setting by applying the appropriate flags to the DEVMODE’s dmFields member. you perform a bit-wise OR on them.294. Display Fixed Output = Default. 4. actually do have a different Display Fixed Output setting assigned to them. or -1. Page –597– .. it was changed back to its original setting. 4.967. 32 bpp. 'pause to admire the change 'we are resetting the screen dimensions to originals 'reset display to original setting 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. Afterward.. Display Fixed Output = Default. Display Fixed Output = Center.. System Default dpi. instead (EnumScreenResolutions(-2)).dmPelsWidth = 800 DMtemp. Gathering registry setting might be required if you have temporarily changed the current display settings from their original. Friend Const DM_BITSPERPEL As Int32 = &H00040000 'apply setting assigned to dmBitsPerPel Friend Const DM_PELSWIDTH As Int32 = &H00080000 'apply setting assigned to dmPelsWidth Friend Const DM_PELSHEIGHT As Int32 = &H00100000 'apply setting assigned to dmPelsHeight Friend Const DM_DISPLAYFLAGS As Int32 = &H00200000 'apply setting assigned to dmDisplayFlags Friend Const DM_DISPLAYFREQUENCY As Int32 = &H00400000 'apply setting assigned to dmDisplayFrequency Friend Const DM_POSITION As Int32 = &H00000020 'apply setting assigned to dmPosition To use multiple settings. and then flag each change to dmFields. such as display setting that is the 11th setting (offset from zero) in the internal list. Default Orientation (dmDisplayFixedOutput = 2) But what if you want to get such details for the current display? As you can see from the module listing. Display Fixed Output = Stretch. your screen changed to 800x600 resolution. 'if we succeeded in the test.967. you can send a value of ENUM_CURRENT_SETTINGS. 60 hz. 4. 720x576 pixels. 'The settings change was unsuccessful because the system is DualView capable. 'The display driver failed the specified graphics mode. which is also handy if the games crashes and you must reboot. and “changedisplaysettings msdn”. 'The computer must be restarted for the graphics mode to work.h file. This can include an invalid flag Or combination Of flags. or at C:\Program Files (x86)\Windows Kits\10\Include\10. Most constants and structures are defined in Visual C’s WinGdi. because we are using an existing DEVMODE structure that already has the pertinent fields filled. 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 Friend Const DISP_CHANGE_RESTART As Int32 = 1 Friend Const DISP_CHANGE_FAILED As Int32 = -1 Friend Const DISP_CHANGE_BADMODE As Int32 = -2 Friend Const DISP_CHANGE_NOTUPDATED As Int32 = -3 Friend Const DISP_CHANGE_BADFLAGS As Int32 = -4 Friend Const DISP_CHANGE_BADPARAM As Int32 = -5 Friend Const DISP_CHANGE_BADDUALVIEW As Int32 = -6 'The settings change was successful.0\um for VS2015).10240. thus ChangeDisplaySettings(DMtemp. Additional information can be found by simply doing web searches for “enumscreenresolutions msdn”.NET Beyond the Scope of Visual Basic 6. we could have set dmFields to all these associated settings and updated the display dynamically with a value of Zero for the dwFlags parameter.0. 0) would have done the trick. and so not bothered with any CDS flags. dmPelsHeight. dmDisplayFlags.0 – David Ross Goben Actually. This will of course not update the registry. dmBitsPerPel. “devmode msdn”. 'An invalid parameter was passed. In this case the display will come back up in its registry-defined settings rather than the settings you may have meant to only temporarily assign.Enhancing Visual Basic .h file. and dmDisplayFrequency. 'An invalid set of flags was passed in. (found at C:\Program Files (x86)\Microsoft Visual Studio xx.0\VC\include for Visual Studio previous to VS2015. though the above results for ChangeDisplaySettings() are declared in the WinUser. dmPelsWidth. 'The graphics mode is not supported. Page –598– . 'Unable to write settings to the registry. it was very much like displaying text on the screen under DOS Basic. and even to print. Instead of the simplicity that VB6 offered. apply new settings.NET. because they were ranting. once you gain some VB. you can use a PrintDocument control dropped onto a form. Once we have that working and understood.NET. instantiate a new PrintDocument object.PowerPacks.NET Although printing using the VB6-style interface is acceptable. We will also throw font selection in just for fun. To do anything. page setup. you can draw shapes. If you wanted to do anything more complicated. and then you use that new object to send text to the current printer.NET interface due to the greater control I have over the document. Printing Simple Text under VB.NET printer interface out to be. and be able to print to them exactly like they can to a form or to a PictureBox. Using VB6 Printer Functionality under VB.NET. Even so.Printer”. However. or you can create your own class that inherits from it and wrap all the printing duties within it. which was the very thing in VB6 they were frustrated with.Compatibility. and frothing at the mouth over VB6’s hated simplistic and primitive printer support. Page –599– . can often get a bit vocal in their frustration when they are using.NET programming moxie. and then create your printer object using “Dim Printer As New PowerPacks. though in most cases we actually need only two of them.NET VB6 had a very easy interface. you can control exactly how and where text is rendered on a printed page. you simply have to add a reference to “Microsoft. but you will create them all in-code and with absolute ease.NET Beyond the Scope of Visual Basic 6. Of course. but how do we do it? For as complicated as many people make the VB. They should beware of what they wish for.NET. because the printer selector. it all starts with a PrintDocument interface declared within the System. we can turn to the task of selecting printers (really easy).Vs”. such as select a printer. Apart from just printing a document. it is actually all very simple.VisualBasic. they got total. when migrating over to VB. I much prefer the VB. or to a printer selected with a PrintDialog control. they wanted unrestricted access to their printer devices. text. they got exactly what they were demanding. 4 of which you normally need concern yourself with. For using the VB6-style printer interface under VB. You can now print using it just like you did under VB6. If you are new to printing under VB. this all sounds like a lot of fun.VB6. setting up page settings (really easy). it is probably easiest to start out by just dropping a PrintDocument control onto your form. But now that they have the level of control they wanted. or format their printed text. especially when printing raw text. Let us start at the beginning. or at least trying to use the printer interface offered by VB. perform a print preview.0 – David Ross Goben Black Book Tip # 57 Printing Plain Text and Formatted Rich Text with Ease VB6 programmers. This way you have instant access to 5 event methods. it also eliminated the need to resort to using a lot of P/Invokes to do any sort of complex drawing. created using something like “Dim Printer As New Printer”. and performing a print preview (amazingly easy). Just like drawing to a PictureBox. though it is still something easily managed. you will not bother with dropping such non-window controls onto your form.NET. they are frustrated because printing is no longer as simple as it was under VB6. unrestricted access to their printers and as a bonus. such broad control consequently requires a much more complex printer interface. set up the page format.Enhancing Visual Basic . You would start with a Printer object.Drawing. or images on each printed page. VB.Printing. With VB. The first thing we need to do is to just print text to the default printer (easy). However. Indeed. raving. Even better.Printing Namespace. Without it. you had to resort to P/Invokes. you cannot do all the other exciting things we can otherwise do.NET still supports the simpler VB6-style interface for those who want to use it. I find this odd. and print preview interfaces all require us to have a working PrintDocoment object through which they can inspect settings. Cancel property to True.PageSettings Page –600– . EndPrint.PrintPage 'NOTE: This event is required so we can print pages End Sub Private Sub Print_Document_QueryPageSettings(sender As Object. In the heading of our code. For example. This is important because the PrintPage() event fires for each page in the document. The print job can also be canceled by setting the e.QueryPageSettings 'NOTE: This event is optional End Sub The BeginPrint() event is fired when a print job begins. The QueryPageSettings() event fires prior to a PrintPage() event.Cancel property to True. or open/rewind a FileStream to print. e As PrintEventArgs) Handles Print_Document. The print job can be canceled by setting the e. you will find five available events in the displayed list: BeginPrint. PrintPage. e As PrintEventArgs) Handles Print_Document.Drawing.Graphics.Enhancing Visual Basic . define a new PrintDocument instance named Print_Document. like so: Private WithEvents Print_Document As New PrintDocument 'printer I/O interface We added the “WithEvents” verb so we are able to afterwards select events for our Print_Document object through the dropdown lists at the top of our code page.NET. The default is False. to draw a line of text on the page to be printed.DrawString().EventArgs) Handles Print_Document. indicating that there are no pages left. Before any form class methods. To write to the page. The EndPrint() event is fired after a print job is completed.PageSettings. you should set the e.EndPrint 'NOTE: This event is optional End Sub Private Sub Print_Document_PrintPage(sender As Object. It can be useful to change how a particular page will be formatted. before the first page is printed. set an index to the start of your text. After rendering a page. The e parameter is defined as QueryPageSettingsEventArgs. but in hundredths of inch units (0. that coordinates used here are not in pixels.Printing 'import printer interface namespace Even if you are not yet software savvy with VB.Graphics). however. or by assigning a pre-set PageSettings object to it (e. such as close a FileStream if you are printing from a file.Disposed 'NOTE: This event is optional End Sub Private Sub Print_Document_EndPrint(sender As Object. draw it using e. though I can clearly see its use in much more complex printer itnerfaces. Individual page settings can be modified through the PrintDocument object’s DefaultPageSettings member. I have yet to have a need for it. include: Imports System.HasMorePages property to True if there are still more pages to print. if you select Print_Document from the left dropdown. e As PrintPageEventArgs) Handles Print_Document. You might need to rewind a pointer or other things. Disposed. let us try and make you savvier. You modify the members of e. To print each document page using different settings. e As QueryPageSettingsEventArgs) Handles Print_Document.NET Beyond the Scope of Visual Basic 6.01). so you must save your place between pages. e As System. Here you can initialize your print job.0 – David Ross Goben Our first step in this process is to import the Printing namespace. Disposed() should be employed if you need to know when your PrintDocument object was disposed of. At the top of our code page. handle that in the QueryPageSettings() event. and QueryPageSettings: Private Sub Print_Document_BeginPrint(sender As Object. which fires immediately before the PrintPage() event. use the Graphics object of the PrintPageEventArgs object (e. Use it as you would to draw to the form or to a PictureBox.BeginPrint 'NOTE: This event is typically required so we can prepare to print End Sub Private Sub Print_Document_Disposed(sender As Object. The PrintPage() event is fired for each page to print. Be mindful. and then drop the right dropdown. where you might want to make sure other references to it are detached (set to Nothing). by the people. then we should set a default. to be dedicated here to the" & " unfinished work which they who fought here have thus far so nobly advanced. We also want to check the font. conceived in Liberty. Further suppose that we initialized it like this: Me._prtFont = New Font("Verdana". = If we want to control the font or maintain an index into a block of text. The world will" & " little note._prtText Is Nothing Then 'if no text to print._prtIndex = 0 'init index to start of text If Me. The print job can be canceled by setting the e. can long endure. In most cases. who struggled here. We have come to dedicate a portion of that field. living and dead. but it can never forget what" & " they did here. far above our poor power to add or detract. in a larger sense. then also be sure that the BeginPrint() event can re-access that data. _prtText to All we have left is the process of actually printing each page.this ground. We are met on a great" & " battle-field of that war. use a default Me. For example: In our BeginPrint() event. _prtFont. we first want to check to see if _prtText contains data.we can" & " not hallow -." & vbCrLf & vbCrLf & "Now we are engaged in a great civil war. It is altogether fitting and proper that we should do this. such as a string or a TextBox. Consider the following BeginPrint() event code (we do not require an EndPrint() event with this simple interface): '******************************************************************************* ' Method Name : Print_Document_BeginPrint ' Purpose : Initialize for starting print job '******************************************************************************* Private Sub Print_Document_BeginPrint(sender As Object. e As PrintEventArgs) Handles Print_Document. not the PrintDocument object’s own PageSettings member.BeginPrint If Me. we will need to set aside some variables accessible to our events. but by the PrintPage() event in particular." & " for the people. setting the start of the text. shall not perish from the earth. we can not dedicate -. then cancel e. which stores default settings.and that government of the people. _prtText will hold the text we want to print.that from these honored dead we take increased devotion to that cause for" & " which they gave the last full measure of devotion -.we can not consecrate -. where we might print one line at a time. 10) End If End If End Sub Some like to clear out their text buffer in the EndPrint() event to release resources.Cancel = True Else Me." & vbCrLf We can also set the font we want to print with by assigning a new font to “Me. such as set Nothing.NET Beyond the Scope of Visual Basic 6. or any" & " nation so conceived and so dedicated. and dedicated to the proposition that" & " all men are created equal. nor long remember what we say here." & " It is rather for us to be here dedicated to the great task remaining before us" & " -. print a whole page at a time. rather. The brave men.Enhancing Visual Basic .Cancel property to True. we will want to initialize the index for the printing process to Zero. 10) Protected _prtIndex As Integer = 0 'text to be printed 'font used for printing 'index into text where we are printing from Here. we want to cancel the print job and leave.that we here highly resolve" & " that these dead shall not have died in vain -. If it has not been set." & " as a final resting place for those who here gave their lives that that nation" & " might live.that this nation. but if you do that. Page –601– ._prtFont = New Font("Times New Roman". If it does not. It is for us the living. for simple print jobs. under God. we need only the BeginPrint() and PrintPage() events. If it does contain data. testing whether that nation._prtFont Is Nothing Then 'if no font is selected. shall" & " have a new birth of freedom -. Changes made to e’s PageSettings affects only the current page. though we may need the EndPrint() event if we want to close a FileStream we opened in the BeginPrint() event. 14)”. or even process HP/GL code to render a blueprint. Suppose we set aside these variables: Protected _prtText As String = Nothing Protected _prtFont As Font = New Font("Times New Roman".0 – David Ross Goben myPageSettings)._prtText = "Four score and seven years ago our fathers brought forth on this continent." & " have consecrated it." & vbCrLf & vbCrLf & "But." & " a new nation. This can take a lot of forms. IsNullOrWhiteSpace() method.Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.Substring(Me.Substring(Me. such as printing an array. Notice that I checked for more pages using the String. such as a StringFormat parameter.Substring(Me.Margins. printableWidth. This same technique can also be applied in more sophisticated tasks. we might use the linesFitted variable to bump an array index._prtText.PrintPage Dim marginLeft.LineLimit for the text layout of our document. e As PrintPageEventArgs) Handles Print_Document. By default layout continues until ' the end of the text.Right 'Note that all dimensions are in 0. Keep track of each page using and updating _prtIndex '******************************************************************************* Private Sub Print_Document_PrintPage(sender As Object.marginRight printableWidth = . Me. and _prtIndex are defined as shown on the previous page. if there are any additional pages._prtIndex). PrintDocument). marginTop As Integer 'left. strFormat.Margins. by using the following PrintPage() event: '******************************************************************************* ' Method Name : Print_Document_PrintPage ' Purpose : Print a page. printableHeight). we can draw the text. right. then swap Height and Width printableHeight = .Width . such as to print a selected block of text to a particular rectangular area on the target page.marginTop . charsFitted.Margins.HasMorePages = Not String.PaperSize. marginTop._prtIndex.Width .Margins. We can take the charsFitted parameter and add it to our current character index Page –602– . linesFitted) 'receive number of characters that fit 'receive number of lines that fit 'Print the current page e.marginLeft .Black. Dim charsFitted As Int32 Dim linesFitted As Int32 e.Left 'also determines start of printable area marginRight = . Notice further that instead of measuring the text using the usual TextRenderer. When that happens.Margins. specify this value and be careful to provide a formatting rectangle at least as ' tall as the height of one line. charsFitted). marginRight.0 – David Ross Goben Drawing Simple Text a Full Page at a Time To draw a full page of text without needing to print each line individually is really rather easy. or until no more lines are visible as a result of clipping._prtFont. strFormat) Me.PaperSize. plus variables to receive the number of characters fitted into the provided target rectangle.. We just simply leave the event.PaperSize. Were we processing each line at a time.Graphics.DefaultPageSettings 'Set print area size and margins marginLeft = ._prtIndex)) 'Detemine if there is 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. no matter how long or how many pages it requires.DrawString(Me.Height .Bottom Else printableWidth = .01-inch increments marginTop = .MeasureString(Me. With this setting: ' Only entire lines are laid out in the formatting rectangle.Top If .Landscape Then 'if landscape. printableHeight). New RectangleF(marginLeft.Graphics..LineLimit) 'See how many characters we can stuff within the print area.PaperSize. and our PrintBegin() and PrintEnd() events have been defined as shown there. 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. and top margins Dim printableWidth. Assuming the local variables _prtTest._prtText.marginLeft .Height . 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. we use the MeasureString() method from PrintPageEventArgs (e). _prtFont. that page is sent to the printer (or at least to the spooler).._prtText.MeasureString() method. printableHeight As Integer 'printable field dimensions on sheet With DirectCast(sender. Notice even further that this method contains some additional features. is processed. and another integer variable to receive the number of lines printed. whichever ' comes first._prtIndex += charsFitted 'bump to the next page e.marginRight printableHeight = .IsNullOrWhiteSpace(Me. Brushes..marginTop . To ensure that only whole ' lines are seen._prtFont. Me. and then the next page. Dim strFormat As New StringFormat(StringFormatFlags. such as around a rendered image.Bottom End If End With 'Use StringFormatFlags. New Size(printableWidth. " & " have consecrated it.Graphics. conceived in Liberty. in a larger sense.txtData.Text Is Nothing Then 'if no text to print.Print()”. and dedicated to the proposition that" & " all men are created equal. who struggled here. but it can never forget what" & " they did here." & " for the people. shall not perish from the earth.Text = "Four score and seven years ago our fathers brought forth on this continent. testing whether that nation._prtIndex = 0 'init index to start of text If Me. and top margins Dim printableWidth. or any" & " nation so conceived and so dedicated. where a btnPrintText_Click() event services a button named btnPrintText. Keep track of each page using and updating _prtIndex '******************************************************************************* Private Sub Print_Document_PrintPage(sender As Object. marginTop As Integer 'left.DrawString() method. That is as easy as entering “Me." & vbCrLf End Sub '******************************************************************************* ' Method Name : Print_Document_BeginPrint ' Purpose : Initialize for starting print job '******************************************************************************* Private Sub Print_Document_BeginPrint(sender As Object.and that government of the people.Load 'Fill Form TextBox with some data Me. living and dead." & vbCrLf & vbCrLf & "Now we are engaged in a great civil war.we can" & " not hallow -. Consider the following. to be dedicated here to the" & " unfinished work which they who fought here have thus far so nobly advanced. could easily be the contents of FileStream instead and the font could be the font assigned to the TextBox. 10) 'font used for printing Protected _prtIndex As Integer = 0 'index into text where we are printing from Protected _prtColor As Color = Color." & " It is rather for us to be here dedicated to the great task remaining before us" & " -. right." & " as a final resting place for those who here gave their lives that that nation" & " might live. far above our poor power to add or detract. e As PrintEventArgs) Handles Print_Document. can long endure.Print_Document. rather. under God. e As EventArgs) Handles MyBase.Drawing. nor long remember what we say here. We are met on a great" & " battle-field of that war." & vbCrLf & vbCrLf & "But.0 – David Ross Goben to point to the start of the text for the next page (Me. use a default Me. It is altogether fitting and proper that we should do this._prtFont Is Nothing Then 'if no font is selected. printableHeight As Integer 'printable field dimensions on sheet Page –603– . It is for us the living. shall" & " have a new birth of freedom -." & " a new nation. e As PrintPageEventArgs) Handles Print_Document. We can also use this variable to specify the number of characters to draw in the e. The brave men. txtData. Note that the TextBox. And now that we have all these parts in place.Printing Public Class frmPrintTest '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private WithEvents Print_Document As New PrintDocument 'printer I/O interface '--------------------------------------------------------------Protected _prtFont As Font = New Font("Times New Roman". marginRight. then cancel e._prtFont = New Font("Verdana".we can not consecrate -.Enhancing Visual Basic .Cancel = True Else Me. or a font selected from a FontDialog: Option Explicit On Option Strict On Option Infer Off Imports System.that this nation. it would be a really good idea to actually be able to start a print job.BeginPrint If Me. We have come to dedicate a portion of that field._prtIndex += charsFitted).PrintPage Dim marginLeft.Black 'print color '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '******************************************************************************* ' Method Name : Form_Load ' Purpose : Sample method to print text to the default document '******************************************************************************* Private Sub Form_Load(sender As Object.txtData. we can not dedicate -.that from these honored dead we take increased devotion to that cause for" & " which they gave the last full measure of devotion -.this ground.that we here highly resolve" & " that these dead shall not have died in vain -. by the people.NET Beyond the Scope of Visual Basic 6. The world will" & " little note. 10) End If End If End Sub '******************************************************************************* ' Method Name : Print_Document_PrintPage ' Purpose : Print a page. Margins.Object. By default layout continues until ' the end of the text. Me.Width . If we want to compute 0.. printableWidth.Width .._prtFont.DefaultPageSettings.Margins.PrintableArea.DefaultPageSettings 'Set print area size and margins marginLeft = .Text.txtData.Print_Document. Dim strFormat As New StringFormat(StringFormatFlags._prtFont. New Size(printableWidth. linesFitted) 'Print the current page e.5 x 11-inch page is 850 units wide by 1100 units long.Right Dim tMargin As Int32 = Me. then swap Height and Width printableHeight = . we could do this: Dim lMargin As Int32 = Me.01-inch height of the font . charsFitted).Substring(Me.01-inch units 'if printing landscale. whichever ' comes first.DefaultPageSettings.Left Dim rMargin As Int32 = Me.Click Me. 'swap prtWd and PrtHt 'get the 0.Print_Document.PaperSize.0 – David Ross Goben With DirectCast(sender.DefaultPageSettings.01-inch increments marginTop = .Print_Document. and so coordinates should always be computed in 0.Height . printableHeight).Height) If Me.Margins. With this setting: ' Only entire lines are laid out in the formatting rectangle.01-inch units.Right 'Note that all dimensions are in 0.Top Dim bMargin As Int32 = Me.Height .Substring(Me.01-inch units 'printable height in 0.Bottom Dim prtWd As Int32 = CInt(Me.PrintableArea.Margins.01-inch units 'top margin in 0. not pixels. we have the dimensions of the page in 0.Print_Document.Black. PrintDocument).Margins.txtData.01inch units.Print_Document.EventArgs) Handles btnPrintText._prtIndex += charsFitted 'bump to the next page 'Detemine if there is more text to print e. charsFitted.Margins.Graphics..marginLeft .Print() End Sub End Class Drawing Simple Text on a Page One Line at a Time Using the DefaultPageSettings object of our Print_Document control.IsNullOrWhiteSpace(Me. New RectangleF(marginLeft. Me.marginTop .HasMorePages = Not String.Margins.Width) Dim prtHt As Int32 = CInt(Me.Print_Document.PaperSize._prtFont.Left 'also determines start of printable area marginRight = . specify this value and be careful to provide a formatting rectangle at least as ' tall as the height of one line.txtData.Text. marginTop.marginRight printableHeight = .NET Beyond the Scope of Visual Basic 6.01-inch units 'printable width in 0.Height / 0.LineLimit) 'See how many characters we can stuff within the print area.Margins._prtIndex.LineLimit for the text layout of our document.DefaultPageSettings.DefaultPageSettings.. To ensure that only whole ' lines are seen.marginTop . which means 1-inch contains 100 measurement units.Bottom Else printableWidth = .Substring(Me.01-inch increments.PaperSize._prtIndex)) End Sub '******************************************************************************* ' Method Name : btnPrintText_Click ' Purpose : Print the document '******************************************************************************* Private Sub btnPrintText_Click(sender As System.DefaultPageSettings.Top If .01-inch units 'right margin in 0.01-inch units 'bottom margin in 0.Print_Document. strFormat. 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.PaperSize.Graphics. e As System.MeasureString(Me.DefaultPageSettings.01-inch dimensions of our print area.._prtIndex).Print_Document. What this comes down to is an 8. printableHeight).Margins.Enhancing Visual Basic . or until no more lines are visible as a result of clipping.DrawString(Me.Landscape Then Dim tmp As Int32 = prtWd prtWd = prtHt prtHt = tmp End If Dim LineHeight As Int32 = CInt(Me.marginLeft .marginRight printableWidth = .Bottom End If End With 'Use StringFormatFlags. Brushes.Landscape Then 'if landscape. Note the fact that units measured with PrintPageEventArgs are also measured in 0.01-inch units. Margins are likewise measured in 0. Dim charsFitted As Int32 'receive number of characters that fit Dim linesFitted As Int32 'receive number of lines that fit e. strFormat) Me.96) Page –604– 'left margin in 0.Text.. if there is one. Using either its SizeMessage() or SizeAndJustifyMessage() methods.Printing Public Class frmPrintTest '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private WithEvents Print_Document As New PrintDocument 'printer I/O interface '--------------------------------------------------------------Protected _prtTextAry() As String = Nothing 'Formatted text to be printed Protected _prtFont As Font = New Font("Times New Roman". shall" & " have a new birth of freedom -. on page 500. It is for us the living. they can automatically force the text to fit within the left and right margins of the printable area." & " It is rather for us to be here dedicated to the great task remaining before us" & " -.NET Beyond the Scope of Visual Basic 6. to be dedicated here to the" & " unfinished work which they who fought here have thus far so nobly advanced. The brave men.Drawing.we can not consecrate -. It is altogether fitting and proper that we should do this.that from these honored dead we take increased devotion to that cause for" & " which they gave the last full measure of devotion -. e As EventArgs) Handles MyBase." & " for the people. because it would not take much for the text to bleed off the right margin if we are not vigilant. The internal print process running in the background after we issue a Print_Document." & " a new nation. The trick with drawing one line at a time is usually line width.0 – David Ross Goben This gives us all the information we need to render text. images. in a larger sense.HasMorePages to True. testing whether that nation." & vbCrLf & vbCrLf & "Now we are engaged in a great civil war." & " have consecrated it. not pixels. 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. but it can never forget what" & " they did here. we just have to keep in mind that we are processing measurements in 0. To end the page.and that government of the people.Text = "Four score and seven years ago our fathers brought forth on this continent. Again.Print() instruction and starts processing the first PrintPage() event.Load 'Fill Form TextBox with some data Me. and Dialog Boxes. we can not dedicate -. can long endure. Labels.txtData. nor long remember what we say here. Consider this updated form code to support this: Option Explicit On Option Strict On Option Infer Off Imports System. and then the next page. or any" & " nation so conceived and so dedicated.Enhancing Visual Basic . However. Quick and Easy Text-Justification for Text Boxes." & vbCrLf End Sub Page –605– . far above our poor power to add or detract. The world will" & " little note.that we here highly resolve" & " that these dead shall not have died in vain -. conceived in Liberty.Black 'print color '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '******************************************************************************* ' Method Name : Form_Load ' Purpose : Sample method to print text to the default document '******************************************************************************* Private Sub Form_Load(sender As Object. we would set e. shall not perish from the earth. and drawings to the printed page if you want to compute your drawing placement manually. If we have more text to render. If our Y position exceeds tMargin+prtHt. We can advance to a new line by resetting our X position to lMargin and increasing our Y position by lineHeight." & vbCrLf & vbCrLf & "But. We have come to dedicate a portion of that field. we are done with this page. and dedicated to the proposition that" & " all men are created equal. If X exceeds lMargin+prtWd. 10) 'font used for printing Protected _prtIndex As Integer = 0 'index into text where we are printing from Protected _prtColor As Color = Color.this ground. rather.that this nation. we need to back off our index until it fits.01-inch units.we can" & " not hallow -. under God. living and dead. is rendered through subsequent PrintPage() event invocations. to the rescue is the updated modComputeMsgDims module listed in Black Book Tip # 46. who struggled here." & " as a final resting place for those who here gave their lives that that nation" & " might live. We are met on a great" & " battle-field of that war. we simply return from the event. by the people. _prtTextAry(Me. Me._prtFont Is Nothing Then 'if no font is selected. e As PrintPageEventArgs) Handles Print_Document.Cancel = True Else Me. then cancel e.EndPrint Erase Me. Keep track of each page using and updating _prtIndex '******************************************************************************* Private Sub Print_Document_PrintPage(sender As Object._prtFont.Top .txtData.Graphics.Margins.Dispose() 'dispose of created resources e.01-inch units.Enhancing Visual Basic .01-inch units Dim aryUB As Int32 = UBound(Me. printableWidth) 'format text to fit within width Me. right. e As PrintEventArgs) Handles Print_Document.Left . vbCrLf).._prtFont. New Point(marginLeft.Bottom) * 0.DefaultPageSettings Dim printableWidth As Int32 If ._prtIndex += 1 'bump array index Y += LineHeight 'bump Y index by one printed line Loop Brsh.Text Is Nothing Then 'if no text to print.Width ._prtFont = New Font("Verdana".0 – David Ross Goben '******************************************************************************* ' Method Name : Print_Document_BeginPrint ' Purpose : Initialize for starting print job '******************************************************************************* Private Sub Print_Document_BeginPrint(sender As Object._prtIndex). 10) End If With DirectCast(sender. Brsh. vbCr) 'break up text into an array End With End If End Sub '******************************************************************************* ' Method Name : Print_Document_EndPrint ' Purpose : Finished Printing '******************************************************************************* Private Sub Print_Document_EndPrint(sender As Object.Left 'also determines start of printable area marginRight = . use a default Me.96) End If Dim tmp As String = Me. PrintDocument).PaperSize._prtIndex <= aryUB 'while we can print lines and lines are left..Margins.Height .Right marginTop = .NET Beyond the Scope of Visual Basic 6.Margins.96) 'get the height of font in 0.Height / 0.PrintPage Dim marginLeft.PaperSize.Margins..HasMorePages = Me.Click Me.. Me.Text 'get an alterable copy of the text SizeAndJustifyMessage(tmp.marginTop .Margins..Top If .Margins. If you have drawn to a form or a PictureBox.96) Else printableWidth = CInt((._prtColor) 'select color.Bottom End If End With Dim Y As Int32 = marginTop 'init to top of page Dim Ylimit As Int32 = marginTop + printableHeight 'compute bottom text limit Dim LineHeight As Int32 = CInt(Me. e.Height .Print_Document._prtTextAry 'remove any formatted text data End Sub '******************************************************************************* ' Method Name : Print_Document_PrintPage ' Purpose : Print a page._prtTextAry = Split(Join(Split(tmp. vbCr). you already know how to draw to the printer.Width .DrawString(Me. And I will beat the poor exhausted horse by reminding you once again that measurements are in 0.PaperSize. e As EventArgs) Handles btnPrintText.Margins.marginRight Else printableHeight = .Right) * 0. e As PrintEventArgs) Handles Print_Document. then swap Height and Width printableHeight = . marginTop As Integer 'left.Landscape Then 'if landscape. in case user did not choose black Do While Y < Ylimit AndAlso Me._prtFont.txtData.BeginPrint If Me. PrintDocument).. Page –606– ._prtTextAry) 'get the upper bounds of the array Dim Brsh As New SolidBrush(Me.Margins.Landscape Then 'use height of landscape to compute pixel width used by SizeAndJustifyMessage() printableWidth = CInt((.Print() End Sub End Class There are many ways to draw data._prtIndex = 0 'init index to start of text If Me.PaperSize. lines. Y)) Me._prtIndex <= aryUB End Sub '******************************************************************************* ' Method Name : btnPrintText_Click ' Purpose : Print the document '******************************************************************************* Private Sub btnPrintText_Click(sender As Object. images.. to include drawing shapes.DefaultPageSettings 'Set print area size and margins marginLeft = . marginRight. and top margins Dim printableHeight As Integer 'printable field domensions on sheet With DirectCast(sender.marginLeft . and changing colors. e As EventArgs) Handles btnPageSetup. if displayed in millimeters. 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.NET Beyond the Scope of Visual Basic 6.OK Then 'show dialog. has updated your PrintDocument object automatically for you as long as you provide it with your PrintDocument reference (some “gurus” will advise you to supply it with new instances of PageSettings and PrinterSettings objects.Document = Me. it will be converted to and from hundredths of an inch . The first level of frustration comes because when they try to update items.Dispose() 'dispose of PrintDialog resources End With End Sub Selecting Page Setup Options The part that runs a lot of programmers new to VB. they keep running into exception errors.Enhancing Visual Basic . e As EventArgs) Handles btnPrintText.Click 'the PageSetupDialog control enables users to change page-related print settings. anyway) . there is no need to update any settings after the dialog has closed. This error.NET printing into trouble is updating page settings and printer settings from the PageSetupDialog control.Print_Document.0 – David Ross Goben Selecting a Printer Using the PrinterDialog control is easy. 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. including margins and paper orientation With New PageSetupDialog 'allow setting margins section of the dialog box .UseEXDialog = True 'show enhanced Printer dialog (False = simple (old) dialog) 'Hitting the Print button returns a result of DialogResult.EnableMetric = True 'Show the network as needed in the printer dialog (shown if local network available) .AllowSomePages = True 'allow user to select pages to print . but when they try to show the control. For example.AllowOrientation = True 'allow setting the paper section of the dialog box (paper size and paper source) .Print_Document 'PrintDocument object used to collect print settings . for those who bother reading the error dialog’s description before crying “Stupid computer!” is due.IsAvailable() 'PrintDocument object to use to format data .Print_Document .ShowDialog() 'Show the dialog . such as the width and height of paper.AllowPaper = True 'allow selecting the Printer from this dialog (this shows only on versions of Windows prior to Vista) . but it is actually quite easy to understand and use.OK.Document = Me.ShowNetwork = My. but this is simply because they are not assigning their PrintDocument object to the control’s Document property. With a button named btnPageSetup. but I will show that to be very bad advice in a moment).Click With New PrintDialog 'instantiate a new PrintDialog instance . Setting up a PageSettingsDialog is more elaborate than the other dialogs. like the PrintDialog.ShowHelp = False 'do not show the Help button (shown on old form dialog.AllowMargins = True 'allow setting the orientation section of the dialog box (landscape versus portrait) . Further. to them not setting the control’s Document property to the PrintDocument object being used.ShowDialog(Me) = DialogResult. a programmer tries setting up a PageSetupDialog control.Computer. Though some programmers get frustrated trying to figure out how to assign the dialog’s options to their PrintDocument object.Dispose() 'dispose of dialog resources End With End Sub Page –607– . like with the PrintDialog. it blows up on them. Because we typically invoke the PrintDialog when we choose to print our document (probably why the dialog has a Print button rather than an OK button). otherwise DialogResult.Cancel.AllowPrinter = True 'During margin settings. change Print_Document setting if user hits Print Me.Network.Print() 'print the document End If . consider the following Click() event code: '******************************************************************************* ' Method Name : btnPageSetup_Click ' Purpose : Set up a printed page format '******************************************************************************* Private Sub btnPageSetup_Click(sender As Object. If . Some programmers complain that the dialog keeps reporting an unhandled exception error when they try to use it. because it. if you do not have a local network setup (I am not talking about your Cable or DSL Internet LAN).Color = Me. This is extremely important! In most examples on blogs and forums. what follows is event code for it: '******************************************************************************* ' Method Name : btnSelectFont_Click ' Purpose : Select a font to print with '******************************************************************************* Private Sub btnSelectFont_Click(sender As System. as we are supposed to.OK Then 'did user chose accept selection? Me. for use by the PrintPage event.Font 'yes. such as the Printer button.0 – David Ross Goben There is nothing complicated about this dialog._prtFont IsNot Nothing Then 'if a font is presently defined. and so they assume they are set to Nothing.ShowColor = True 'allow user to select a color If Me.Object. Me. so save the font locally. With a button named btnSelectFont on the form.ForeColor = .Color 'along with the color selection End If RemoveHandler . ByVal e As EventArgs) With DirectCast(sender. FontDialog) 'access the provided FontDialog control Me. in most cases you should ignore bothering to assign new instances of PageSettings and PrinterSettings objects and just assign your PrintDocument object to the dialog’s Document property if you want to use its settings.Click With New FontDialog ._prtFont = .ShowDialog(Me) = DialogResult.txtData.FontMustExist = True 'all listed fonts must exist . It also makes more sense to take advantage of the setting we might already have. I think the reason people try to set them is that if they try to examine them in code.Enhancing Visual Basic .ShowApply = True 'show apply button AddHandler . Indeed. AddressOf FontDialog_Apply 'detach Apply event handler .Apply.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. Actually.ShowEffects = True 'show effect options ._prtFont 'then use it (otherwise use default) End If If . or had previously set during a previous dialog session.Font = Me. Actually.Font = .. . in our case _prtFont and _prtColor._prtColor = . they are not permitted to.Color 'set the font color to the textbox End With End Sub We save the font and the font color selection to local storage. you will not see a network button. the reason these properties are exposed at all is if you need to apply new default settings._prtColor 'init to current color ..txtData. but these properties are actually meant to reflect those objects from our own PrintDocument object if we simply assign it to the Document property. we might as well provide a simple interface to the FontDialog control.. Page –608– . e As EventArgs) Handles btnSelectFont.Font 'set the font to the textbox Me.NET Beyond the Scope of Visual Basic 6. It just has several options. when the truth is that it just funnels assignment to what meant to be stored in the PrintDocument object. It will just not permit access to your PrintDocument object’s members by drilling through it via the dialog’s Document property (just use the PrintDocument object).Apply.. AddressOf FontDialog_Apply 'attach Apply button to event handler . and all will work perfectly! Selecting a Printer Font and Font Color Now that we are all experts at printing. Also. As such. you are told to assign new instances of PageSettings and PrinterSettings objects to this dialog to keep it from crashing. Notice we set the Document property to our PrintDocument object. even if you enable it. the yellow-highlighted options do not show on newer operating systems. or re-access it in BeginPrint().Enhancing Visual Basic .. but because I write event headers all the time from scratch.End With blocks makes this very easy to do. Actually. we do not need to declare a control name.Click With New PrintPreviewDialog 'process print preview . is to get the printing processes correct. we could have instantiated a new instance of a FontDialog “WithEvents”. Performing a Print Preview I have probably seen more consternation on the web over this than I have seen on any of the other topics. the print preview uses the printing interface to render its previews. e As EventArgs) Handles btnPrintPreview.Print_Document 'PrintDocument object to use . However. you must either maintain the data. but I just like to keep my code clean. Taking advantage of With.NET Beyond the Scope of Visual Basic 6. using the PrintPreviewDialog is a snap. Assign your PrintDocument object to the the control’s Document property. notice that I am applying this to a new instance of the FontDialog control that has not even been named.0 – David Ross Goben We also demonstrate the APPLY button on a FontDialog. for me to type “Private Sub FontDialog_Apply(ByVal sender As Object.Dispose() 'dispose of dialog resources End With End Sub Like with the PrintDialog and the PageSetupDialog controls. You can even print your document to the default printer from within the dialog. The interesting thing is. In this case.Document = Me.ShowDialog(Me) 'show print preview. and then we could access its Apply() event from the dropdowns at the top of the code page. everything else is easy. Once the support structure for a PrintDocument object is in place.UseAntiAlias = True 'smooth font display . It will be removed anyway when everything goes out of scope or is disposed of. which seems to be the source of a lot of frustration to many users. such as “Private WithEvents Font_Dialog As New FontDialog”. NOTE: A lot of programmers get themselves into trouble here because in their EndPrint() events they will flush the printed text data to clear resources. maybe set the UseAntiAlias property to True to smooth the edges of the screen-displayed font in the print preview. add the following Click() event code to the form: '******************************************************************************* ' Method Name : btnPrintPreview_Click ' Purpose : Do a print preview '******************************************************************************* Private Sub btnPrintPreview_Click(sender As Object. Also. Notice finally that I detached the event handler from the control before leaving the Font Dialog event. Even after programmers figure out that they have to use an Apply() event for their FontDialog control. Like everything else. ByVal e As EventArgs)”. Also. though. they think they can only use it if they drop the control onto the form. like with out Print_Document object. and hitting the PRINT button might then not find any text when it re-runs the print interface and at that point the BeginPrint() event might not find anything to print. Page –609– . but to simply cast the sender object to a FontDialog. and you are off to the races. be sure to assign your PrintDocument object to its Document property. is almost too easy. . especially with autocomplete and intellisense active. Even in the Apply() event. re-running the Print Preview will not show data if they had flushed it in the EndPrint() event and the BeginPrint() event is not set up to reaccess it. or rewind. re-open.. With a button on the form named btnPrintPreview. it is one of the easiest dialogs to support! The trick. saving it to a class file so we can add it to other projects and take advantage of it. and OnEndPrint(). the font._prtFont End Get Set(value As Font) If value Is Nothing Then Me.Enhancing Visual Basic .Black 'print color '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : Text ' Purpose : Get/Setthe text that we want to print 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Public Property Text As String Get Return Me. instead of using the regular control events.Empty 'Property variable for the text to be printed Protected _prtIndex As Integer = 0 'index into text where we are printing from Protected _prtFont As Font = New Font("Times New Roman".Printing. 10) Else Me. we will take advantage of class-bound ON-events._prtFont = New Font("Times New Roman"._prtText = Value End If End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : Font ' Purpose : Get/Set the Font that we want to print with 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Public Property Font As Font Get Return Me. and the font color. 10) 'Property variable for the Font the user wishes to use Protected _prtColor As Color = Color.PrintDocument '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Protected _prtText As String = String. OnPrintPage(). let us encapsulate our simple text printing interface._prtText End Get Set(ByVal Value As String) If Value Is Nothing Then Me._prtColor = value End Set End Property Page –610– .Drawing Public Class PrintText Inherits Printing.0 – David Ross Goben Creating a Print Document Class Prior to creating a printer interface for a RichTextBox. The only things that are different are that we will add properties to the class to set the text to print. such as OnBeginPrint()._prtText = String. It is not difficult at all because we have already written all the code it requires._prtFont = value End If End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : FontColor ' Purpose : Get/Set the Font Color that we want to print with 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Public Property FontColor As Color Get Return Me. Also. Consider my PrintText class that can be used in place of a PrintDocument object: Option Explicit On Option Strict On Option Infer Off 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' PrintText Class (Inherits from System.Drawing._prtColor End Get Set(value As Color) Me.NET Beyond the Scope of Visual Basic 6.PrintDocument ' Simple Print Document Class using a single font 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Imports System.Empty Else Me. 0 – David Ross Goben '******************************************************************************* ' Method : New ' Purpose : optional constructor to assigned text we want to print '******************************************************************************* Public Sub New(Optional ByVal Text As String = Nothing) Me. strFormat.ForeColor 'grab forecolor Me._prtIndex = 0 'init index to start of text If Me.Text = Text End Sub '******************************************************************************* ' Method : New ' Purpose : optional constructor to assigned font we want to print with..marginRight printableHeight = .LineLimit) 'See how many characters we can stuff within the print area.MeasureString(Me. charsFitted.PaperSize.OnBeginPrint(e) 'do base processing If Me.Cancel = True Else Me. 'then swap dimensions for printable area 'Use StringFormatFlags.NET Beyond the Scope of Visual Basic 6.marginTop .Margins.FontColor = TextBox.Font = Font Me.Width . use a default Me.Font) 'brain-dead simple Cloning of Font object Me.PrintPageEventArgs) Dim marginLeft. To ensure that only whole ' lines are seen.marginLeft .01-inch increments marginTop = .Top printableWidth = .Graphics.OnPrintPage(e) 'do base processing With MyBase. whichever ' comes first.Margins._prtText Is Nothing Then 'if no text to print. specify this value and be careful to provide a formatting rectangle at least as ' tall as the height of one line..DefaultPageSettings. New Size(printableWidth.. 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.Text 'grab text End If End Sub '******************************************************************************* ' Method : OnBeginPrint ' Purpose : Override the default OnBeginPrint method of the PrintDocument '******************************************************************************* Protected Overrides Sub OnBeginPrint(e As Printing. linesFitted) Page –611– ._prtFont Is Nothing Then 'if no font is selected. or until no more lines are visible as a result of clipping.Text = TextBox.Margins._prtFont = New Font("Times New Roman". and top margins Dim printableWidth.PaperSize._prtText. Dim charsFitted As Int32 'receive number of characters that fit Dim linesFitted As Int32 'receive number of lines that fit e.Enhancing Visual Basic . 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..Font.Substring(_prtIndex). Optional ByVal Text As String = Nothing) Me.Height .DefaultPageSettings 'Set print area size and margins marginLeft = .Font = (TextBox.. printableHeight).Left 'also determines start of printable area marginRight = .PrintEventArgs) MyBase. right.Right 'Note that all dimensions are in 0. Dim strFormat As New StringFormat(StringFormatFlags.Bottom End With If MyBase. By default layout continues until ' the end of the text.Landscape Then Dim swap As Integer = printableHeight printableHeight = printableWidth printableWidth = swap End If 'if the user selected to print in Landscape mode. With this setting: ' Only entire lines are laid out in the formatting rectangle. printableHeight As Integer 'printable field domensions on sheet MyBase.Margins. marginTop As Integer 'left. marginRight. Me. then cancel e.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.LineLimit for the text layout of our document. and text we want to print '******************************************************************************* Public Sub New(ByVal Font As Font. ShowDialog(Me) = DialogResult. It is for us the living.Dispose() 'dispose of created resources 'Detemine if there is more text to print e.Enhancing Visual Basic . and dedicated to the proposition that" & " all men are created equal. we can not dedicate -. living and dead. If ." & vbCrLf & vbCrLf & "But. Brsh. conceived in Liberty.IsNullOrWhiteSpace(Me.txtData. testing whether that nation.Print_Document.UseEXDialog = True 'show enhanced Printer dialog (False = simple dialog) 'Hitting the Print button returns a result of DialogResult. or any" & " nation so conceived and so dedicated. Consider the updated form: Option Explicit On Option Strict On Option Infer Off Imports System. who struggled here.HasMorePages = Not String.Substring(_prtIndex." & " have consecrated it. charsFitted).DrawString(Me. but it can never forget what" & " they did here.Print_Document.OK Then 'show dialog. The brave men.Graphics.Print_Document 'PrintDocument object used to collect print settings . We are met on a great" & " battle-field of that war." & " as a final resting place for those who here gave their lives that that nation" & " might live. strFormat) _prtIndex += charsFitted 'bump to the next page Brsh.Dispose() 'dispose of PrintDialog resources End With End Sub Page –612– ._prtText.NET Beyond the Scope of Visual Basic 6.Object._prtIndex)) End Sub End Class With this class. otherwise DialogResult.Printing Public Class frmPrintTest '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private WithEvents Print_Document As New PrintText 'printer I/O interface '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '******************************************************************************* ' Method Name : Form_Load ' Purpose : Sample method to print text to the default document '******************************************************************************* Private Sub Form_Load(sender As System." & " a new nation.this ground. because the new class object will take care of all that for you.Text 'update text.and that government of the people.Document = Me.Text = Me." & " It is rather for us to be here dedicated to the great task remaining before us" & " -.Load 'Fill Form TextBox with some data Me.that this nation.Print() 'print the document End If . by the people. in case of a change .Text = Me. to be dedicated here to the" & " unfinished work which they who fought here have thus far so nobly advanced.Print_Document. shall not perish from the earth." & vbCrLf Me. in a larger sense. under God.0 – David Ross Goben 'Print the current page Dim Brsh As New SolidBrush(Me. printableWidth.Text = "Four score and seven years ago our fathers brought forth on this continent._prtText. Me.txtData. We have come to dedicate a portion of that field. can long endure.we can not consecrate -.txtData. far above our poor power to add or detract. New RectangleF(marginLeft.ShowHelp = False 'do not show the Help button .AllowSomePages = True 'allow user to select pages to print . we can clean up our form by not needing to include PrintDocument event handlers. e As System.Cancel." & " for the people.Drawing.OK. marginTop._prtColor) 'select color." & vbCrLf & vbCrLf & "Now we are engaged in a great civil war.that from these honored dead we take increased devotion to that cause for" & " which they gave the last full measure of devotion -. rather. in case user did not choose black e.EventArgs) Handles MyBase.that we here highly resolve" & " that these dead shall not have died in vain -. printableHeight). It is altogether fitting and proper that we should do this. e As EventArgs) Handles btnPrintText.we can" & " not hallow -. shall" & " have a new birth of freedom -. nor long remember what we say here.Substring(Me. change Print_Document setting if user hits Print Me.Font. The world will" & " little note.Text 'copy to PrintDocument object End Sub '******************************************************************************* ' Method Name : btnPrintText_Click ' Purpose : Print the document by calling up the PrintDialog '******************************************************************************* Private Sub btnPrintText_Click(sender As Object.Click With New PrintDialog 'instantiate a new PrintDialog instance Me. 0 – David Ross Goben '******************************************************************************* ' Method Name : btnPageSetup_Click ' Purpose : Set up a printed page format '******************************************************************************* Private Sub btnPageSetup_Click(sender As Object. in case of a change With New PrintPreviewDialog 'process print preview .NET Beyond the Scope of Visual Basic 6.ShowDialog(Me) = DialogResult.Text = Me.ShowEffects = True 'show effect options .Print_Document.txtData.txtData.Click Me.. e As EventArgs) Handles btnPageSetup.Color = Me.AllowPaper = True 'allow selecting the Printer from this dialog (this shows only on versions of Windows prior to Vista) .FontColor 'init to current color .Font 'user it (otherwise use default) End If If .FontMustExist = True 'all listed fonts must exist . e As EventArgs) Handles btnSelectFont. try updating the PrintText class to print one line at a time and using the SizeAndJustifyMessage() or the SizeMessage() methods from the modComputeMsgDims module.Color End With End Sub '******************************************************************************* ' Method Name : btnPrintPreview_Click ' Purpose : Do a print preview '******************************************************************************* Private Sub btnPrintPreview_Click(sender As Object. as demonstrated earlier.ShowDialog(Me) 'show print preview. .Apply.AllowMargins = True 'allow setting the orientation section of the dialog box (landscape versus portrait) .Font = .Text 'update text. so save the font.ShowColor = True 'allow user to select a color If Me.IsAvailable() 'PrintDocument object to use to format data .Computer.Network.Print_Document .AllowOrientation = True 'allow setting the paper section of the dialog box (paper size and paper source) .Font = Me.Print_Document 'PrintDocument object to use . including margins and paper orientation With New PageSetupDialog 'allow setting margins section of the dialog box . FontDialog) Me.Font 'yes.ShowNetwork = My. ByVal e As EventArgs) With DirectCast(sender.ShowDialog() 'show the dialog ..ForeColor = . AddressOf FontDialog_Apply 'attach Apply button to event handler .Print_Document.txtData.Click 'the PageSetupDialog control enables users to change page-related 'print settings.AllowPrinter = True 'During margin settings. AddressOf FontDialog_Apply 'detach Apply event handler .Print_Document.Print_Document.Dispose() 'dispose of dialog resources End With End Sub End Class As an exercise.FontColor = . e As EventArgs) Handles btnPrintPreview.Print_Document.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. if displayed in millimeters.Enhancing Visual Basic .EnableMetric = True 'Show the network as needed in the printer dialog (shown if local network available) .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.Font IsNot Nothing Then 'if a font is presently defined .Font = . Page –613– .Print_Document.UseAntiAlias = True 'smooth font display . Me.OK Then 'did user chose accept selection? Me.Document = Me.Click With New FontDialog .Color 'and the color End If RemoveHandler . it will be converted to and from hundredths of an inch .ShowApply = True 'show apply button AddHandler .Font Me.Document = Me.Apply. consider this modified version of 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.HelpRequest.OK. and you provide support through their HelpRequest() events just like you do with the PrintDialog. .Document = Me. However. on the other hand. such the ShowHelp properties of the PrintDialog. which is why the UseEXDialog property exists. Page –614– . it will not even be displayed. you must provide event code for the dialog control to react to it being pressed. e As EventArgs) Handles btnPrintText.” So. You can easily throw up a more elaborate MsgBox. open a HTML or MHTML document. but to try and convince your application’s users that you are really a nice person.Dispose() 'dispose of PrintDialog resources End With End Sub '******************************************************************************* ' Method Name : PrintDialog_HelpRequest ' Purpose : Provide Help for using the Print Dialog '******************************************************************************* Private Sub PrintDialog_HelpRequest(sender As Object. a help form.Text = Me.Cancel. The PageSetupDialog and FontDialog controls. if you do not set this property to True. in case of a change . If .Print() 'print the document End If RemoveHandler . a text file in Notepad. AddressOf PrintDialog_HelpRequest 'attach HelpRequest handler '. For the PrintDialog control. and the FontDialog controls is a good idea.Click With New PrintDialog 'instantiate a new PrintDialog instance Me. the PageSetupDialog. "Print Dialog Help") End Sub Of course.Print_Document. Microsoft has reported “This property was marked obsolete in Windows 2000 and does not affect this and later versions of Windows. you will in fact see it.NET Beyond the Scope of Visual Basic 6. With that in mind. but you must supply that help.ShowHelp = True 'show the Help button AddHandler .Print_Document 'PrintDocument object used to collect print settings . otherwise DialogResult.txtData. change Print_Document setting if user hits Print Me. because so many developers still wanted that older dialog functionality. Actually.Print_Document.HelpRequest. Even more. you must provide help through a HelpRequest() event for the controls. still actively support their Help buttons. or a RTF file in WordPad. it is because this help does not come automatically. if you enable the control’s UseEXDialog property by setting it to True.Enhancing Visual Basic .AllowSomePages = True 'allow user to select pages to print . but then they get frustrated when pressing the displayed button does not do anything.Text 'update text.OK Then 'show dialog. so you will see the older-style printer interface. e As EventArgs) MsgBox("Just choose a stupid printer!".ShowDialog(Me) = DialogResult. NOTE: This concept also applies to the Apply button in the PageSetupDialog control.0 – David Ross Goben Supporting the Help Buttons on Dialogs A lot of programmers think adding the HELP button on forms. AddressOf PrintDialog_HelpRequest 'detach HelpRequest event handler .UseEXDialog = True 'show enhanced Printer dialog (False = simple dialog) 'Hitting the Print button returns a result of DialogResult. As I had demonstrated with event support for the Apply button in the FontDialog control code. you might want to be a bit less of a jerk in your help. to have a RichTextBox control format a range of text for a specific device. The FORMATRANGE structure is defined like this: 'Information that a rich edit control uses to format its output for a particular device. plus a couple of RECT structures and a CHARRANGE structure. shown previously. the devil is in the details.NET RichTextBox” (https://msdn. and I will cut out the additional stuff that is not necessary. I will apply Martin’s knowledge to regular RichTextBox controls you might already have placed on your application forms. to print or print-preview its contents. Public chrg As CHARRANGE 'The range of characters to format. Martin Müller of 4voice AG wrote an article for MSDN Magazine in their January 2003 issue named “Getting WYSIWYG Print Results from a . to print the contents of a RichTextBox control. The only additional task you must perform is to assign the RichTextBox control you want to use with it to the object’s RichTextControl property (note that you can therefore actually process several different RichTextBox controls using the very same PrintRichText object!). effects and such through P/Invokes and structures. your printer. say. especially because it listed C++ and C# code mixed right in with VB code. Public rcPage As RECT 'The entire area of a page on the rendering device. End Structure As you can see. Its default state is False. Also. and forget about using P/Invokes for doing a lot of things that are more easily done though the control’s own functionality. which is a good example. point sizes.NET” on page 442). EM_FORMATRANGE. his approach required creating a new RichTextBox class that inherits from the RichTextBox control. such as your printer.com/enus/library/ms996492. when set to True. The fact is. styles. Public rc As RECT 'The area within the rcPage rectangle to render to. This simple-looking structure requires that we provide it with a Device Context for the device we want to render to. though.Sequential)> Private Structure FORMATRANGE Public hdc As IntPtr 'A HDC for the device to render to. along with a FORMATRANGE structure. complete with images and formatted text (even Full justification if you take advantage of Black Book Tip # 30. An additional property. and sizes that must be accounted for. is getting there. You instantiate it in place of a regular PrintDocument object. In that light. but it was really confusing in the parts where it is formatting fonts. because you have various fonts. copying. <StructLayout(LayoutKind. will display blue corner tags on the displayed sheet to mark where the margins are located. most developers want to know exactly how to get into the root essentials of how to do what they need to do. requires we send it a EM_FORMATRANGE message. 'This structure is used with the EM_FORMATRANGE message. “Enable Built-In Justify-Alignment in a RichTextBox from VB. Also. effects. and de-allocating memory (this may have been necessary until VB2005 came along). which is required in C++.NET Beyond the Scope of Visual Basic 6. Page –615– . I still wrap all this added functionality in a class so you do not have to worry about any of it. In order to get a RichTextBox to render its display output to another device. rather than using the built-in functionality of the VB and C# RichTextBox control. along with a FORMATRANGE structure. but within a new PrintDocument object named PrintRichText. if being used to send the output to a device.Enhancing Visual Basic .microsoft.0 – David Ross Goben Printing WYSIWYG RichTextBox Text Printing What-You-See-Is-What-You-Get Rich Text might seem to be a more trying endeavor. but I think it greatly confused readers because it got a bit thick. as previously mentioned. only involves sending a message. The trick. just as you just did with the PrintText class. I will also simplify the code using overloaded methods so we can remove unnecessary tasks like allocating. This is an excellent article. Units are measured in twips.aspx). but most people want to simply add this functionality to code and RichTextBox controls they already have in place without needing to redesign their software more than they need to. Public hdcTarget As IntPtr 'An HDC for the target device to format for. ShowMarginTags. Units are measured in twips. and the other specified an lParam as our above FORMATRANGE structure. We service these using the following properties: Page –616– .Zero. ByVal wParam As Int32.Runtime.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr. ByVal wParam As Int32. because a Rectangle structure’s Right and Bottom properties are read-only. used to pass a value of IntPtr. because even though it would in fact work correctly when we passed the Rectangle to a P/Invoke.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr. and a flag that will allow the printer interface to display margin markers.InteropServices Namespaces and then set aside the following common fields (variables): Protected _prtRTB As RichTextBox Protected _prtIndex As Integer = 0 Protected _prtShowMargins As Boolean = False 'RichText Control 'index into text where we are printing from 'show margin indicators on page Here. <StructLayout(LayoutKind.0 – David Ross Goben Normally I would use a Dot NET Rectangle in place of a RECT structure. so we would otherwise have to assign these values instead to the Rectangle’s Width and Height properties. Private Declare Function SendMessage Lib "user32. Notice further that it is passed ByRef.Enhancing Visual Basic . The SendMessage function calls the window procedure for the specified 'window and does not return until the window procedure has processed the message. specifying the Start and End positions in the document for printing: 'Specifies a range of characters in a rich edit control.Printing. This simple feature allows us to bypass any need to copy the FORMATRANGE structure to an allocated neutral memory location. if we were to look at the Right and Bottom properties. even though the core integers stored in its footprint are actually correct. ByVal wMsg As Int32. System. and System.MarginBounds). the range is empty. it will simply be much easier to just define RECT. 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. we store the RichTextBox control we are working with.Sequential)> Private Structure CHARRANGE Public cpMin As Int32 'Character position index immediately preceding the first character in the range Public cpMax As Int32 'Character position immediately following the last character in the range End Structure The SendMessage() P/Invoke and the EM_FORMATRANGE message are declared like this: 'Sends the specified message to a window or windows.Drawing. 'If the cpMin and cpMax members are equal. ByVal wMsg As Int32. The range includes everything if cpMin is 0 and cpMax is –1. that would invite bugs into later code enhancements. we provided two different versions of the SendMessage() P/Invoke. they would appear to be reporting too-large values. As you can see.NET Beyond the Scope of Visual Basic 6. To me. the index into the text of the RichTextBox where the print process maintains an index to the start of each page.PageBounds) and the printable region on the page (PrintPageEventArgs. we must import the System. ByVal lParam As IntPtr) As Int32 Private Declare Function SendMessage Lib "user32. <StructLayout(LayoutKind. and afterward de-allocate it (I be lovin’ overloads when they cut out a lot of work). The first specifies a lParam as an IntPtr. All that is left to do is to render it. which will be used to reset the affected RichTextBox and cause it to again render to its own display interface.Sequential)> Private Structure RECT Public left As Int32 Public top As Int32 Public right As Int32 Public bottom As Int32 End Structure This structure will be used to provide the page’s boundaries (PrintPageEventArgs. The CHARRANGE structure is simpler. which does not settle well with me. but because of the way we are using it. Within our PrintRichText class. which inherits from PrintDocument.Drawing. tPage. 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.Bottom End With Dim lMargin.IsDisposed Then 'if no RichText to print.Left tMargin = .NET Beyond the Scope of Visual Basic 6.OnEndPrint(e) 'perform base class dutues 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.Right bMargin = bPage . rPage. rMargin. and PrintPage() event duties needed by the PrintDocument object as contained within the PrintRichText class by using the ON-versions of these events.0 – David Ross Goben 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : RichTextControl ' Purpose : Get/Setthe RichText Control that we want to print 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Public Property RichTextControl As RichTextBox Get Return Me.MarginBounds lMargin = .Top rPage = .Bottom End With Page –617– 'get margin sizes ._prtRTB Is Nothing OrElse Me. then cancel e._prtShowMargins = value End Set End Property As with the PrintText class. EndPrint().OnBeginPrint(e) 'do base class duties If Me.Right bPage = .._prtRTB = value End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : ShowMarginTags ' Purpose : Get/Setthe Show Margin Tags on Page 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Public Property ShowMarginTags As Boolean Get Return Me.OnPrintPage(e) 'do base class duties If ShowMarginTags Then 'if we should show margin markers._prtRTB End Get Set(value As RichTextBox) Me._prtShowMargins End Get Set(value As Boolean) Me. It is employed exclusively by the class.Cancel = True Else Me.._prtRTB. bMargin As Int32 With e._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.Left tPage = ..PageBounds 'get page bounds lPage = . The BeginPrint().Enhancing Visual Basic .Top rMargin = rPage . we do not expose the _prtIndex field to the user. Dim lPage.. bPage As Int32 With e. tMargin. Blue. lPage + lMargin.tMargin. lPage + lMargin \ 2.PageBounds. tPage + tMargin) 'top-left e.bottom = hInchToTwips(e.DrawLine(Pens.top = hInchToTwips(e.Graphics. 0. ByVal charTo As Int32) As Int32 Dim charRange As CHARRANGE With charRange . e.right = hInchToTwips(e.Blue.DrawLine(Pens. lPage + lMargin.bMargin. 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. lPage + lMargin.bMargin. rPage . .PageBounds.MarginBounds.Top) .MarginBounds.Graphics.Print_Document._prtRTB.PageBounds. bPage .Right) End With 'Specify the area inside page margins in twips Dim rectPageArea As RECT With rectPageArea .DrawLine(Pens.bottom = hInchToTwips(e.TextLength 'check if there are more pages to print End Sub 'bottom-right 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.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. rPage . lPage + lMargin \ 2. End With Dim wParam As Int32 = CInt(Not measureOnly) And 1 'Non-Zero wParam means render.cpMax = charTo End With 'Specify which characters to print 'Character position index immediately preceding the first character in the range 'Character position immediately following the last character in the range Dim rectInsidePageMargins As RECT With rectInsidePageMargins . . bPage .Graphics._prtRTB.Zero) End Sub '******************************************************************************* ' Method : FormatRtbRange ' Purpose : Calculate or render the contents of our RichTextBox for printing ' ' Parameter "measureOnly": If true.Right) End With 'Specify the page area in twips Dim hdc As IntPtr = e.FormatRtbRange(False.Bottom) .ShowMarginTags = True).GetHdc() 'Get device context of output device 'convert 0.TextLength) e.Graphics._prtRTB.Bottom) . Me.rMargin. bPage .Top) .Enhancing Visual Basic . tPage + tMargin \ 2.Graphics.left = hInchToTwips(e. tPage + tMargin.bMargin) e. bPage .Blue. . Units are measured in twips. Zero means measure Page –618– .hdcTarget = hdc 'An HDC for the target device to format for.right = hInchToTwips(e. ByVal e As PrintPageEventArgs. . bPage .Graphics.0 – David Ross Goben e.bMargin \ 2) 'bottom-left e. lPage + lMargin. tPage + tMargin) e. Me.cpMin = charFrom .DrawLine(Pens. tPage + tMargin) 'top-right e.rMargin. Units are measured in twips.rMargin \ 2.MarginBounds. rPage . rPage .PageBounds.bMargin) e. ByVal charFrom As Int32._prtIndex = Me.Graphics. IntPtr. rPage .rMargin.Handle.Blue. EM_FORMATRANGE. rPage .rMargin. rPage .DrawLine(Pens. 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.DrawLine(Pens.01-inch units to twips Dim fmtRange As FORMATRANGE 'Fill in the FORMATRANGE structure With fmtRange .DrawLine(Pens.Left) . only the calculation is performed._prtIndex < Me.chrg = charRange 'The range of characters to format._prtIndex.rMargin \ 2.bMargin.hdc = hdc 'A HDC for the device to render to. tPage + tMargin.Blue.top = hInchToTwips(e.Blue.Graphics. lPage + lMargin.rMargin.NET Beyond the Scope of Visual Basic 6.Left) . lPage + lMargin.left = hInchToTwips(e.HasMorePages = Me. tPage + tMargin \ 2.rMargin. tPage + tMargin) e.MarginBounds.Graphics. rPage .Blue. if being used to send the output to a device. bPage . bPage .DrawLine(Pens. bPage .Blue.rcPage = rectPageArea 'The entire area of a page on the rendering device.rc = rectInsidePageMargins 'The area within the rcPage rectangle to render to. After that. Notice also that the FormatRtbRange() method has its own support metod.ReleaseHdc(hdc)).ToInt32(hInch * 14. If we do not release it. When we send the EM_FORMATRANGE message along with the FORMATRANGE structure.Runtime. ' "Getting WYSIWYG Print Results from a . we first load up a CHARRANGE structure with the start and end text position indexes of the document. What follows is my complete PrintRichText class: Option Explicit On Option Strict On Option Infer Off 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC ' PrintRichText Class (Inherits from System. the method is nice enough to return an index to the start of the next page.Printing._prtRTB. fmtRange) e. hInchToTwips().Sequential)> Private Structure RECT Public left As Int32 Public top As Int32 Public right As Int32 Public bottom As Int32 End Structure Page –619– .Printing. Even so. We next do likewise for the margin bounds.PrintDocument ' RichText Print Document Class ' Extrapolated from Martin Müller's January 2003 on MSDN Magazine.Drawing. this should not be considered a dangerous or even a scary thing.01-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). used by Win32 API calls) ' ' Parameter "hInch": Value in 1/100 inch ' Return value : Value in twips '******************************************************************************* Private Function hInchToTwips(ByVal hInch As Integer) As Int32 Return Convert.0 – David Ross Goben Dim result As Int32 = SendMessage(Me. which defines the printable area on the page.ReleaseHdc(hdc) Return result End Function 'Send Win32 message and get start of next page 'and release the device context 'return start of next page '******************************************************************************* ' Method : hInchToTwips ' Purpose : Convert between 1/100 inch (unit used by the . which you had borrowed it from.NET RichTextBox" 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC Imports System.NET Beyond the Scope of Visual Basic 6. your PrintDocument object will become inoperable and an unhandled exception error might result.GetHdc()) of our PrintDocument object (PrintRichText in this case).Graphics. System. EM_FORMATRANGE. just hang until you stop it. We can use it on subsequent invocations to the OnPrintPage() event.Drawing.Handle. We then format one RECT structure with with the page range (dimensions).Graphics.NET framework) ' : and twips (1/1440 inch.Graphics. As you can see. so you must actually just give control of it back to the system.Enhancing Visual Basic . and use it to determine if we are finished printing. wParam.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.InteropServices Public Class PrintRichText Inherits PrintDocument '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Protected _prtRTB As RichTextBox 'RichText Control Protected _prtIndex As Integer = 0 'index into text where we are printing from Protected _prtShowMargins As Boolean = False 'show margin indicators on page '--------------------------------------------------------------------------------<StructLayout(LayoutKind. rendering all members from 0. which is used to convert the 0. or worse.01-inch units to twips. by releasing it once you are done using it. just good manners. that we must be sure to release it when we are done with it (e. we have all we need to fill our FORMATRANGE structure. because only one thing can control a Device Context at a time. Be sure to notice that when we grab the Device Context (Dim hdc As IntPtr = e. System.Drawing. OnBeginPrint(e) 'do base class duties If Me. then cancel e. The SendMessage function calls the window procedure for the specified 'window and does not return until the window procedure has processed the message.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr.Sequential)> Private Structure CHARRANGE Public cpMin As Int32 'Character position index immediately preceding the first character in the range Public 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. Units are measured in twips. Public rc As RECT 'The area within the rcPage rectangle to render to. Private Declare Function SendMessage Lib "user32. Units are measured in twips. 'This structure is used with the EM_FORMATRANGE message._prtRTB End Get Set(value As RichTextBox) Me. ByVal wMsg As Int32._prtRTB Is Nothing OrElse Me. the range is empty. ByVal wMsg As Int32. ByVal wParam As Int32._prtShowMargins End Get Set(value As Boolean) Me. End Structure 'Sends the specified message to a window or windows. <StructLayout(LayoutKind. '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : RichTextControl ' Purpose : Get/Setthe RichText Control that we want to print 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Public Property RichTextControl As RichTextBox Get Return Me.0 – David Ross Goben 'Specifies a range of characters in a rich edit control.NET Beyond the Scope of Visual Basic 6._prtRTB = value End Set End Property 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp ' Property : ShowMarginTags ' Purpose : Get/Setthe Show Margin Tags on Page 'ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp Public Property ShowMarginTags As Boolean Get Return Me. ByVal wParam As Int32. ByVal lParam As IntPtr) As Int32 Private Declare Function SendMessage Lib "user32. Public chrg As CHARRANGE 'The range of characters to format._prtShowMargins = value End Set End Property '******************************************************************************* ' Method : OnBeginPrint ' Purpose : Override the default OnBeginPrint method of the PrintDocument '******************************************************************************* Protected Overrides Sub OnBeginPrint(e As PrintEventArgs) MyBase.Enhancing Visual Basic . Public rcPage As RECT 'The entire area of a page on the rendering device. if being used to send the output to a device._prtRTB.DLL" Alias "SendMessageA" (ByVal hwnd As IntPtr.OnEndPrint(e) 'perform base class dutues FormatRtbRangeDone() 'release rendering to our PrintRichText object End Sub '******************************************************************************* Page –620– . 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.Cancel = True Else Me.IsDisposed Then 'if no RichText to print.Sequential)> Private Structure FORMATRANGE Public hdc As IntPtr 'A HDC for the device to render to. 'If the cpMin and cpMax members are equal. The range includes everything if cpMin is 0 and cpMax is –1. <StructLayout(LayoutKind. Public hdcTarget As IntPtr 'An HDC for the target device to format for._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. Left tPage = .Blue._prtIndex = Me.GetHdc() 'Get device context of output device Page –621– 'convert 0.DrawLine(Pens.Blue.rMargin \ 2.Blue. tPage + tMargin \ 2.bMargin.bottom = hInchToTwips(e. EM_FORMATRANGE.Left) . bPage . Me. lPage + lMargin \ 2.. tPage + tMargin \ 2.Graphics.DrawLine(Pens. bPage .bMargin.Top rMargin = rPage .DrawLine(Pens. bPage .top = hInchToTwips(e. ByVal charTo As Int32) As Int32 Dim charRange As CHARRANGE With charRange .top = hInchToTwips(e. rPage . bMargin As Int32 With e. rPage . Me.Graphics. e.bMargin \ 2) 'bottom-left e. bPage As Int32 With e.rMargin.rMargin \ 2._prtIndex < Me._prtIndex.Blue.Top) .01-inch units to twips ..Right bPage = ._prtRTB.left = hInchToTwips(e. 0.rMargin. rPage .Left) .DrawLine(Pens.cpMax = charTo End With 'Specify which characters to print 'Character position index immediately preceding the first character in the range 'Character position immediately following the last character in the range Dim rectInsidePageMargins As RECT With rectInsidePageMargins .Zero) End Sub '******************************************************************************* ' Method : FormatRtbRange ' Purpose : Calculate or render the contents of our RichTextBox for printing ' ' Parameter "measureOnly": If true. bPage . ByVal e As PrintPageEventArgs. tPage + tMargin.rMargin.Graphics.DrawLine(Pens.cpMin = charFrom . bPage . rPage .PageBounds. bPage . tPage + tMargin) 'top-left e.left = hInchToTwips(e. lPage + lMargin. rPage . tPage._prtRTB.DrawLine(Pens.Graphics.HasMorePages = Me.bottom = hInchToTwips(e.Top) . tPage + tMargin) e.rMargin. rPage.Right) End With 'Specify the area inside page margins in twips Dim rectPageArea As RECT With rectPageArea .Handle. 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.Blue. rPage .Bottom End With Dim lMargin.MarginBounds.Bottom) .PageBounds 'get page bounds lPage = .MarginBounds.TextLength 'check if there are more pages to print End Sub 'bottom-right '******************************************************************************* ' Method : FormatRtbRangeDone ' Purpose : Free cached data from rich edit control after printing '******************************************************************************* Private Sub FormatRtbRangeDone() SendMessage(Me.PageBounds.Blue.Right) End With 'Specify the page area in twips Dim hdc As IntPtr = e.Graphics.Graphics.tMargin.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..Blue.Graphics.Bottom End With 'get margin sizes e.rMargin.Left tMargin = . ByVal charFrom As Int32.MarginBounds.FormatRtbRange(False. rPage .rMargin.right = hInchToTwips(e. bPage .NET Beyond the Scope of Visual Basic 6.MarginBounds.MarginBounds lMargin = .Bottom) . tPage + tMargin) e. rPage .. lPage + lMargin.DrawLine(Pens. tPage + tMargin) 'top-right e. IntPtr. tMargin. Dim lPage.Blue. lPage + lMargin.DrawLine(Pens.Enhancing Visual Basic .PageBounds. only the calculation is performed.bMargin) e.bMargin._prtRTB.Graphics.Graphics.Right bMargin = bPage . lPage + lMargin.TextLength) e. lPage + lMargin. tPage + tMargin. lPage + lMargin. bPage . lPage + lMargin \ 2.PageBounds.Top rPage = . rMargin.OnPrintPage(e) 'do base class duties If ShowMarginTags Then 'if we should show margin markers.right = hInchToTwips(e.bMargin) e.0 – David Ross Goben ' Method : OnPrintPage ' Purpose : Override the default OnPrintPage method of the PrintDocument '******************************************************************************* Protected Overrides Sub OnPrintPage(ByVal e As PrintPageEventArgs) MyBase. under God. in case of a change .Document = Me. in a larger sense. with a RichTextBox control named rtbData. .hdcTarget = hdc 'An HDC for the target device to format for. who struggled here.Printing Public Class frmPrintTest '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ Private WithEvents Print_Document As New PrintRichText 'printer I/O interface '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '******************************************************************************* ' Method Name : Form_Load ' Purpose : Sample method to print text to the default document '******************************************************************************* Private Sub Form_Load(sender As System.that from these honored dead we take increased devotion to that cause for" & " which they gave the last full measure of devotion -. e As EventArgs) Handles btnPrintText. if being used to send the output to a device. used by Win32 API calls) ' ' Parameter "hInch": Value in 1/100 inch ' Return value : Value in twips '******************************************************************************* Private Function hInchToTwips(ByVal hInch As Integer) As Int32 Return Convert.we can" & " not hallow -." & " It is rather for us to be here dedicated to the great task remaining before us" & " -._prtRTB. EM_FORMATRANGE.Print_Document. far above our poor power to add or detract.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.Handle. Units are measured in twips. . The world will" & " little note.rc = rectInsidePageMargins 'The area within the rcPage rectangle to render to. can long endure.chrg = charRange 'The range of characters to format.NET Beyond the Scope of Visual Basic 6.Text = Me." & " for the people. conceived in Liberty.Print_Document. rather.Enhancing Visual Basic . e As System. Zero means measure Dim result As Int32 = SendMessage(Me.ToInt32(hInch * 14." & vbCrLf Me." & vbCrLf & vbCrLf & "But.hdc = hdc 'A HDC for the device to render to." & " a new nation. btnPageSetup. wParam.Load 'Fill Form TextBox with some data Me.Print_Document 'PrintDocument object used to collect print settings .that this nation.Click With New PrintDialog 'instantiate a new PrintDialog instance 'Me. End With Dim wParam As Int32 = CInt(Not measureOnly) And 1 'Non-Zero wParam means render. by the people. we can not dedicate -.txtData.RichTextControl = Me. shall" & " have a new birth of freedom -.rtbData.that we here highly resolve" & " that these dead shall not have died in vain -.Text 'update text. Units are measured in twips.Drawing.Text = "Four score and seven years ago our fathers brought forth on this continent. but it can never forget what" & " they did here. labeled however you desire.AllowSomePages = True 'allow user to select pages to print . shall not perish from the earth.UseEXDialog = True 'show enhanced Printer dialog (False = simple dialog) Page –622– .Graphics. The brave men.and that government of the people. fmtRange) 'Send Win32 message and get start of next page e. nor long remember what we say here.rcPage = rectPageArea 'The entire area of a page on the rendering device. We have come to dedicate a portion of that field. and dedicated to the proposition that" & " all men are created equal. .EventArgs) Handles MyBase. testing whether that nation." & " as a final resting place for those who here gave their lives that that nation" & " might live.Object.4) End Function End Class Assuming we have a form named frmPrintTest.we can not consecrate -. to be dedicated here to the" & " unfinished work which they who fought here have thus far so nobly advanced. We are met on a great" & " battle-field of that war. It is altogether fitting and proper that we should do this." & vbCrLf & vbCrLf & "Now we are engaged in a great civil war.this ground. and btnPrintPreview. and 3 buttons named btnPrintText. living and dead. ." & " have consecrated it. or any" & " nation so conceived and so dedicated. consider the following form class code (paste whatever Rich Text you want into the RichTextBox): Option Explicit On Option Strict On Option Infer Off Imports System.0 – David Ross Goben Dim fmtRange As FORMATRANGE 'Fill in the FORMATRANGE structure With fmtRange .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. It is for us the living. Print_Document 'PrintDocument object to use .Print_Document. If .Dispose() 'dispose of dialog resources End With End Sub End Class You might notice in many ways that printing from a Rich Text Control is simpler and easier than rendering plain text from a TextBox. Go figure.AllowPrinter = True 'During margin settings.EnableMetric = True 'Show the network as needed in the printer dialog (shown if local network available) .Print_Document 'provide a reference to our PrintDocument object .Print_Document. e As EventArgs) Handles btnPageSetup.Print_Document.Click 'the PageSetupDialog control enables users to change page-related 'print settings. which you will have to still supply if you are writing a Rich Text editor.ShowDialog(Me) 'show print preview.ShowNetwork = My.AllowOrientation = True 'allow setting the paper section of the dialog box (paper size and paper source) . otherwise DialogResult. e As EventArgs) Handles btnPrintPreview.IsAvailable() 'PrintDocument object to use to format data .Network.AllowPaper = True 'allow selecting the Printer from this dialog (this shows only on versions of Windows prior to Vista) .Computer. will still be supported when rendering to the printer or to a print preview.ShowDialog(Me) = DialogResult.ShowMarginTags = False 'disable displaying margin corner tags .AllowMargins = True 'allow setting the orientation section of the dialog box (landscape versus portrait) . change Print_Document setting if user hits Print Me.ShowDialog() 'show the dialog .Cancel. Notice further that any extensions you apply to your RichTextBox controls.ShowMarginTags = True 'enable displaying margin corner tags .Dispose() 'dispose of dialog resources End With End Sub '******************************************************************************* ' Method Name : btnPrintPreview_Click ' Purpose : Do a print preview '******************************************************************************* Private Sub btnPrintPreview_Click(sender As Object.0 – David Ross Goben 'Hitting the Print button returns a result of DialogResult.Document = Me. OK = printed document Me.Print() 'print the document End If . including margins and paper orientation With New PageSetupDialog . such as full text justification. Any complexity comes for formatting the text and images in the control.OK. it will be converted to and from hundredths of an inch . Page –623– .Document = Me.OK Then 'show dialog.Enhancing Visual Basic . if displayed in millimeters.NET Beyond the Scope of Visual Basic 6.Click With New PrintPreviewDialog 'process print preview .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.UseAntiAlias = True 'smooth font display Me. FromImage(. meaning that its progress is updated every 1/10th of a second. and how often the timer should actually “tick”: Private ProgressImage As Image Private Const ProgressInc As Int32 = 4 Private Const ProgressSpeed As Int32 = 20 'progress image 'how many pixels to imcrement image rightward 'how often our timer should run We can load our image.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 –624– & & & & & & & & & & & & & & & & & & & & . We can capture a lot of slick-looking progress bars. and using a timer set anywhere from 15ms to 100ms. Instead of using hard-coded numbers.Image). We could add it to our application resources and then paint it to a PictureBox’s Image object at a starting offset of -110 (starting fully left of the PictureBox). from a file. a new PictureBox has its Image property set to Nothing. but a shorter period often looks much better. At that point we can reset its starting index back to -110 and keep rolling it rightward into and out of view. keep bumping its left position on every tick event by 4 pixels or so until we pass the right edge of the PictureBox with the left edge of our progress image. However.Clear(. initializing this drawing canvas is also very easy to do. taking advantage of the ConvertBase64Image() function we have used in previous Black Book Tips: (the following definition is for the above incremental light-blue image): Me.Height) Graphics. It would look much slicker if we could add our own image to it. to do this we must also make sure that our PictureBox has an image upon which we can paint! By default. we should set aside constants to define the number of pixels to increment the image during each timer tick.Image = New Bitmap(.picProgress . its MarqueeAnimationSpeed property has its value set to 100ms. I had captured this progress image from an application I was installing: This image is dimensioned 110x22 pixels. We could do something like the following to initialize our PictureBox. it is easy enough to draw our own progress marquees using a PictureBox and Timer. We must also maintain a reference and position offsets for the “progress” image we will paint on the control.Enhancing Visual Basic . . which is Control by default. it looks a bit lame with a plain green block sliding across it. or define it from a Base64 binary conversion string. Though it does the job. This is fine.Width. Of course. For example.BackColor) End With 'create a new image space (this property was set to Nothing by default) 'paint a blank background on the image using a temp Graphics interface This will define a blank image set to the PictureBox’s BackColor property. then make sure your application properties has its Enable XP Visual Styles option checked (default) and that you also set your ProgressBar’s Style property to Marquee. but until then. ProgressImage. from our resources.0 – David Ross Goben Black Book Tip # 58 Creating Fancier ProgressBar Marquees and Enhanced ProgressBars If you want to display a ProgressBar Marquee that loops continuously until your application finishes doing whatever it is taking an indeterminate time doing. Trying to paint to that will only result in an exception error.NET Beyond the Scope of Visual Basic 6. By default. which I here named picProgress: With Me. Width. Next. . To demonstrate how easy this all really is.Tick Me.tmrProgress.ProgressImage. Idx = -Me.tmrProgress..Image where we want to draw ProgressImage to.BackColor) 'first clear any previous drawing.Width Me.Refresh() 'update the display of the drawing Application.Clear(Me.Size.Image.Load Me. ByVal e As EventArgs) Handles MyBase.Clear(.picProgress..ProgressImage.Width .ProgressImage.Height = ProgressImage.Enabled = True 'set ProgressImage startup position (note the (-) unary minus) '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.NET Beyond the Scope of Visual Basic 6.Location = New Point(12.BackColor) 'paint a blank background on the image using a temp Graphics interface End With Page –625– .Height) 'create image space (this property was set to Nothing by default) Graphics. draw the image to the PictureBox Image. set to an interval of 20 (ProgressSpeed).Width = Me.Enabled = False 'disable the progress timer for now Dim g As Graphics = Graphics.Tag = -Me. 0) 'draw the progress image to the picturebox g..Enabled = True 're-enable the progress timer End Sub Here.Image = New Bitmap(. Dim Idx As Int32 = CInt(Me.. and finally re-enable the timer so it can keep progressing the image. 12) 'position it in top-left of form .Height 'set picture holder height to that of the progress image .Left * 2 'size it across the form and keep left and right margins uniform .DrawImage(Me. let other system messages process by taking a pause with DoEvents(). We could set aside a field variable for this.ProgressImage = InitializeImage() 'grab progress image With Me.ProgressImage.Image) 'get a graphics imterface to the image to draw to g.Size.ProgressImage. 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 Private Const ProgressInc As Int32 = 4 'how many pixels to imcrement image rightward Private Const ProgressSpeed As Int32 = 20 'how often our timer should run Private WithEvents tmrProgress As New Timer 'timer to imcrenement ProgressImage in picProgress Private WithEvents picProgress As New PictureBox 'picturebox to represent the continuos 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.0 – David Ross Goben We should save the horizontal position on picProgress.Tag = Idx 'Save the updated left index Me.Enhancing Visual Basic . ByVal e As EventArgs) Handles tmrProgress.Image). create a new Windows Form project.Width 'reset the X index back to being fully left of the image End If Me.FromImage(Me.FromImage(. named tmrProgress: Me. With it. If the new update exceeds the size of the target image..FixedSingle 'make border more defined (not flat or borderless) . we first cleared any drawing we may have already done on the image.ProgressImage.tmrProgress.DoEvents() 'let other things happen Me. we first disabled the timer to reset it.Dispose() 'release drawing interface (this locks in the change) Idx += ProgressInc 'bump the X offset If Idx > Me.picProgress.picProgress.Anchor = AnchorStyles. but I just stuff it into ProgressImage’s Tag property. We then grab a graphics interface for the Image object in our PictureBox. we reset its index to the left of the image.ClientRectangle.Tag) 'grab the current X offset for rendering the image g. We must also enable our timer. Idx.BorderStyle = BorderStyle. We next refresh the PictureBox to display the new drawing.Top Or AnchorStyles.Left 'anchor it . and then bump the X offset.Parent = Me 'set parent .Width Then 'if it exceeds the drawing surface. we pick up our X offset value from the Tag property of our Progress Image.picProgress.picProgress . Top + Me.tmrProgress..Tag = -Me.BackColor) 'first clear any previous drawing.Click If Me.Text = "Start/Stop Test" 'give it some text End With Me.Width 'set ProgressImage startup position Me.Tag) 'grab the current X offset for rendering the image g.Tag = Idx 'save the updated left index Me.ProgressImage. Me.Close() 'release stream resources Return Img 'return image End Function End Class Page –626– ...MemoryStream = New IO.picProgress.Clear(.tmrProgress.Enabled = False 'disable the progress timer for now Dim g As Graphics = Graphics.Width Then 'if it exceeds the drawing surface.Image) 'get a graphics interface to the image to draw to g.picProgress.tmrProgress.DrawImage(Me.Clear(Me.Enabled = False 'turn off timer With Me.FromStream(memStream) 'construct image from stream data memStream.ProgressImage.Tick Me.ProgressImage.Size.tmrProgress. ByVal e As EventArgs) Handles btnStartStop.ProgressImage.FromImage(Me.Height + 12) . 24) 'make bug enough to accomodate text .DoEvents() 'let other things happen Me.Image).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..Enabled Then 'if the timer is currently running.Image.FromImage(.Refresh() 'update its display End With Else Me.ProgressImage.0 – David Ross Goben With btnStartStop 'button to start and stop the sample .picProgress.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.picProgress.Interval = ProgressSpeed 'set timer interval to 15 ms End Sub '********************************************************************************* ' Method : btnStartStop_Click ' Purpose : Start and stop the custom Progress bar '********************************************************************************* Private Sub btnStartStop_Click(ByVal sender As Object.tmrProgress.Width 'reset X back to the left of the image End If Me.Parent = Me 'set parent . ByVal e As EventArgs) Handles tmrProgress. 0) 'draw the progress image to the picturebox g.BackColor) 'initialize its image to blank .Enhancing Visual Basic .picProgress.Refresh() 'update the display of the drawing Application.picProgress Graphics.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.Dispose() 'release drawing interface (this locks in the change) Idx += ProgressInc 'bump the X offset If Idx > Me. Idx = -Me. Dim Idx As Int32 = CInt(Me.Location = New Point(12.tmrProgress..NET Beyond the Scope of Visual Basic 6..ProgressImage.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image. Me. Idx.Size = New Size(100.picProgress. 0) 'draw the image to the picturebox Idy += Me. ByVal e As EventArgs) Handles tmrProgress.ProgressImage.DoEvents() 'let other things happen Me. In your own application.Enabled = True 're-enable the progress timer End Sub Notice first that we do not initially clear the drawing surface because we are going to completely repaint it. That is really easy to do. we will draw a copy of the progress image one after the other until the image rightward of the Idx index can be filled. Idy -= PgrssW 'tile left g.picProgress. While Idy is less than the width of the PictureBox image.Width 'tile right Loop Idy = Idx 'reset secondary index to start point Do While Idy > 0 'while it is greater than zero.ProgressImage.DrawImage(Me. button to start and stop the demonstration.tmrProgress.picProgress.Image) 'get a graphics interface to the image to draw to Dim Idx As Int32 = CInt(Me. it is reset to Idy.0 – David Ross Goben You should also experiment using a different progress image that you have either captured or created.Tag) 'grab the current X offset for rendering the image Dim ImageW As Int32 = Me. Consider the following in-code image definitions for a simple Green Aero Glare image suitable for a Tiled ProgressBar Marquee: Page –627– .Width 'save image width Dim Idy As Int32 = Idx 'set secondary index to start point Do While Idy < ImageW 'while we have not exceeded the display area. 0) 'draw the image to the pciturebox Loop g. because you as a user do not have to first drop any controls on the form or rename them to get the demo code to work. Notice that I created my PictureBox progress bar. Consider these changes to the tmeProgress_Tick() event code. which while tile the ProgressImage across the whole of the PictureBox.picProgress. Idy.NET Beyond the Scope of Visual Basic 6. It will do it for you. Idy. We next set up a secondary index. We can tile the image to fill the PictureBox.ProgressImage.Enhancing Visual Basic .ProgressImage.tmrProgress. Idy.Enabled = False 'disable the progress timer for now Dim g As Graphics = Graphics. you may want to just drop controls on your form and deal with them more conveniently that way. Idx = Idy 'reset X origin to <= 0 End If Me.DrawImage(Me.Tag = Idx 'save the updated left index Me. which is less than or equal to zero.Tick Me. Enhancing Our Custom ProgressBar Marquee by Tiling It But enhancements do not have to end with a custom Image for our Continuous ProgressBar...Refresh() 'update the display of the drawing Application.Image. We then reset the secondary index and draw leftward until Idy is less than zero. that will be updated so we can tile the image.Width 'save picturebox width Dim PgrssW As Int32 = Me.Dispose() 'release drawing interface (this locks in the change) Idx += ProgressInc 'bump the X offset If Idx > ImageW Then 'if it exceeds the drawing surface.. This will mean that we have now drawn to the entire drawing surface. no matter where the start index is located: '********************************************************************************* ' Method : tmrProgress_Tick ' Purpose : Update for a Tiled Continuous ProgressBar '********************************************************************************* Private Sub tmrProgress_Tick(ByVal sender As Object. and my timer all in-code. g.FromImage(Me. as I have demonstrated elsewhere in this document.ProgressImage.. Once Idx exceeds the drawing surface. and animate it to roll endlessly across it.ProgressImage.. but creating them in-code sure makes demo code easier.. Close() 'release stream resources Return Img 'return image End Function Emulating the Aero Glass ProgressBar Marquee If you would like a ProgressBar Marquee that emulates the Aero Glass ProgressBar Marquee.Enhancing Visual Basic . across the width of the PictureBox.FromStream(memStream) 'construct image from stream data memStream. Page –628– . Consider the following demonstration code that will perform these tasks. you can easily do that with two images. and then draw the Aero Glass Progress Image over the top of it. which is rather small.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image. You would first tile a small image to the PictureBox to represent the general background for the ProgressBar emulation.MemoryStream = New IO. It will tile the general background image.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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. This simple effect is incredibly Impressive. and then paint another “glare” image on top of that to create the Aero Glass Glare progress effect that slides across it.NET Beyond the Scope of Visual Basic 6. moving it from left to right in a continuous motion. .tmrProgress..ProgressImage. Me.Tick Me.tmrProgress.Height) 'create image space (was set to Nothing) Graphics.picProgress.ProgressBack.Width = Me.Image). Idx.ClientRectangle.tmrProgress.Height + 12) .ProgressImage.Anchor = AnchorStyles.Height 'set picture holder height . Me.Left * 2 'size it across the form .NET Beyond the Scope of Visual Basic 6.FixedSingle 'make border more visible .Enabled = False 'turn off timer With Me.Image).Width 'set ProgressImage startup position Me.Tag) 'grab the current X offset the progress glare image g..Size = New Size(100.Left 'anchor it .ProgressBack.Width Dim PgrsiW As Int32 = Me.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.Width. 0) 'draw the progress image to the pciturebox g.Location = New Point(12.Tag = Idx 'update the left index Page –629– .ProgressImage.Enabled = False 'disable timer for now Dim g As Graphics = Graphics.tmrProgress..Parent = Me 'set parent .BorderStyle = BorderStyle.ProgressBack = InitializeProgressBackImage() 'grab progressbar background image Me.picProgress Graphics.Click If Me. .Location = New Point(12.Text = "Start/Stop Test" 'give it some text End With Me.ProgressImage.picProgress.Size.BackColor) 'initialize the image to a blank background End With With btnStartStop 'button to start and stop the sample . g.Tag = -Me. Idy.Interval = ProgressSpeed 'set timer interval to 10 ms End Sub '********************************************************************************* ' Method : btnStartStop_Click ' Purpose : Start and stop the custom Progress bar '********************************************************************************* Private Sub btnStartStop_Click(ByVal sender As Object.Size.ProgressImage.FromImage(. 24) 'make bug enough to accomodate text . 12) 'position it in top-left of form ..Clear(.Enhancing Visual Basic . ByVal e As EventArgs) Handles tmrProgress..Enabled Then 'if the timer is currently running..Image = New Bitmap(.DrawImage(Me.Top Or AnchorStyles.Parent = Me 'set parent . Idx = -PgrsiW 'reset the X into to the left of the image End If Me. ByVal e As EventArgs) Handles btnStartStop.Clear(.0 – David Ross Goben 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 imcrement image rightward Private Const ProgressSpeed As Int32 = 20 'how often our timer should run Private WithEvents tmrProgress As New Timer 'timer to imcrenement ProgressImage in picProgress Private WithEvents picProgress As New PictureBox 'picturebox to represent the continuos 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.Image) 'get a graphics interface to the image to draw to Dim ImageW As Int32 = Me.Dispose() 'release drawing interface Idx += ProgressInc 'bump the X offset If Idx > ImageW Then 'if it exceeds the drawing surface.Width .Refresh() 'update its display End With Else Me.picProgress.Height = ProgressImage.Image.DrawImage(Me.Top + Me.BackColor) 'initialize its image to blank . 0) 'tile the progress background image to the picturebox Idy += PgrsbW 'tile rightwardly Loop Dim Idx As Int32 = CInt(Me.Load Me.tmrProgress.picProgress.ProgressImage = InitializeProgressBarImage() 'grab progressbar glare image With Me.Width Dim Idy As Int32 = 0 'set secondary index to start point Do While Idy < ImageW 'while we have not exceeded the display area.Width Dim PgrsbW As Int32 = Me. ByVal e As EventArgs) Handles MyBase.ProgressImage.picProgress .FromImage(.FromImage(Me. picProgress.tmrProgress.NET Beyond the Scope of Visual Basic 6.Enhancing Visual Basic .MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image.Refresh() Application.Enabled = True End Sub 'update the display of the drawing 'let other things happen 're-enable timer '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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.FromStream(memStream) 'construct image from stream data memStream.MemoryStream = New IO.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.0 – David Ross Goben Me.Close() 'release stream resources Return Img 'return image End Function '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : InitializeProgressBarImage .MemoryStream = New IO.DoEvents() Me.FromStream(memStream) 'construct image from stream data memStream.Close() 'release stream resources Return Img 'return image End Function End Class Page –630– .FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image. .Enhancing Visual Basic .Anchor = AnchorStyles.ClientRectangle. How we did that was to take advantage of the many overloads available to the Graphics interface’s DrawImage() method.DrawImage(Me.Height 'drawing area height Dim PgrsbW As Int32 = Me. were we examine the second listed overload. We have been using the template Graphics. TargetX.. or we can specify the target size. using its original physical size.FixedSingle . this version of the method will draw the specified Image at a specified location and to a specified target size.Location = New Point(12. If the size of the target rectangle differs from the original image.Image. width.Width = Me.ProgressImage. stretching or compressing it as needed in order to automatically shoehorn it into place. TargetY). adapted for different bar heights '********************************************************************************* Private Sub tmrProgress_Tick(ByVal sender As Object.0 – David Ross Goben Drawing Custom ProgressBars of User-Defined Heights So far we have drawn custom ProgressBars whose heights have been dictated by the height of the progress Image. We can of course set our custom ProgressBar to any height we choose.Image = New Bitmap(. Extracting Icon Images from Files and Displaying them in a Directory TreeView on page 550. which draws the specified image.picProgress. What we need to do is to stretch the image to fill the new height of the PictureBox and its Image property.BorderStyle = BorderStyle.Tag) 'grab the current X offset for rendering the progress image Page –631– . In particular we had stretched images from Icons in Black Book Tip # 52.FromImage(. It is our previous image. 0.picProgress.Clear(.Height) Graphics. but as an example I made it easy by setting the picProgress PictureBox’s height like so: With Me. as we have been currently doing. But.picProgress. but we can also draw only portions of the source image (a technique called Cropping). TargetY.ProgressImage. but displayed three times as tall. Graphics.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. ImageH) 'tile the progress background image to the picturebox Idy += PgrsbW 'tile rightwardly Loop Dim Idx As Int32 = CInt(Me.FromImage(Me. ByVal e As EventArgs) Handles tmrProgress.Enabled = False 'disable timer for now Dim g As Graphics = Graphics..Width .Left * 2 .Top Or AnchorStyles.picProgress . the DrawImage() method will rescale the rendered image to fit within the target rectangle. PgrsbW. Idy.drawImage method” to find references to these overloads on MSDN).Left .DrawImage(Image. This method currently has 30 overloads defined (Google or Bing “graphics. Of the DrawImage() overloads. Thus.ProgressBack. as we had done when we were rescaling icon images.Image) 'get a graphics interface to the image to draw to Dim ImageW As Int32 = Me.Height * 3 . they allow us not only to draw the image starting at a specified starting point that specifies its top-left corner. to a location specified by a coordinate pair. Consider the image to the right. either as a rectangle.Width. TargetWidth. g. it would certainly display a taller PictureBox. but the progress images would still be drawn to their original defined heights.Width 'drawing area width Dim ImageH As Int32 = Me. 12) .NET Beyond the Scope of Visual Basic 6.Height = Me.Width 'progress background image width Dim PgrsiW As Int32 = Me. But suppose we need a ProgressBar that is much taller or even narrower than the source image? This is also easy to do. apart from specifying the height of our PictureBox that stands in for a ProgressBar.Image). TargetHeight).Parent = Me . Other drawing methods also feature a rich repertoire of overloads that is well worth the while to explore.ProgressBack.DrawImage(Image. top.Size. Previously we had demonstrated stretching images. if we then ran the code. TargetX.Tick Me.tmrProgress.Image. or by specifying the offsets for the left.BackColor) End With 'set parent 'position it in top-left of form 'size it across the form 'set picture holder height 'make border more visible 'anchor it 'create image space (was set to Nothing) 'initialize the image to a blank background But were we to make only this simple little change. we can update the tmrProgress_Tick() event code to accommodate whatever size we specify for the ProgressBar: '********************************************************************************* ' Method : tmrProgress_Tick ' Purpose : Update the Continuous ProgressBar. and height of the target area..ProgressImage. clipping it as needed so it will never draw beyond the edge of our progress indicator bar limit. 0. by sliding ProgressImage across it. a glare.NET Beyond the Scope of Visual Basic 6.DoEvents() Me. it is also interesting that the real reason why this technology is redialing back to these more “ancient” techniques is not because it is more “trendy”.ProgressImage. their image rendering technology will become more sophisticated. but using custom images. and at which time such 3D effects and rendering. will suddenly become “the new black”. Page –632– . To do so requires just a little simple math and the ability to clip images. presently standard on PCs. 0. we may have simply painted a color rectangle to draw the actual progress bar that creeps across the PictureBox. assuming all members are integers: ProgressPercent = TasksCompleted * 100 \ AllTasksToComplete. 0. the consumers.Enhancing Visual Basic . and in time you will see that as these devices become faster and faster.tmrProgress. All that aside. The trick to this is that we will not draw all the way across the PictureBox except when the value is at 100. wanted this (when what the consumers really want is the same 3D effects on their portable devices as they have on their PCs). PgrsiW.Refresh() Application. Idx. eventually evolving to rival desktop and laptop computers.Enabled = True End Sub 'draw the progress image to the picturebox 'release drawing interface 'bump the X offset 'if it exceeds the drawing surface. The fact is. but because the graphics interfaces of these new devices are so mind-numbingly slow when it comes to graphics rendering. In our picProgress_Tick() event method. where “flat is the new black” of the graphical fashion world. we will start out by rendering just our background image. ProgressBack. Watch. we would have to first determine if we need to update our image dimensions.0 – David Ross Goben g. If we do. Once we have that mastered. and then assign this result to the ProgressPercent variable using the following simple template formula. we must calculate the percentage of the length of the PictureBox we would need to draw from the left of our progress bar. This algorithm will store an integer value from 0 to 100 in the ProgressPercent variable. but using our own images.Dispose() Idx += ProgressInc If Idx > ImageW Then Idx = -PgrsiW End If Me. we will then add a real-time progress effect. Even more. Under older operating systems. ImageH) g. For example. let us now emulate a standard ProgressBar..ProgressImage. ProgressPercent To keep things simple. 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. and using ad campaigns to convince the consumers that not the vendors. 'reset the X into to the left of the image 'update the left index 'update the display of the drawing 'let other things happen 're-enable timer Emulating the Standard ProgressBar Using Images We can also emulate the standard (regular) ProgressBar. and so we must assume that we will just render to a target rectangle that is only as wide as PgrsAct (ProgressPercent * ImageW \ 100). ImageH) 'draw a blue rectangle to display progress indicator bar Though this procedure is so “old school”. it is interesting to note that this technique has actually made resurgence with the advent of smart-devices and pads.DrawImage(Me.FillRectangle(Brushes.Tag = Idx Me. PgrsAct. For example.picProgress. but that they. it would calculate the percentage of the full task completed. suppose we had a local Integer variable name ProgressPercent. those of us who have been in the business long enough noticed right away that this is the very same graphical evolution that earlier desktop computers went through from its boring flat graphics to its eye-popping hi-rez 3D image rendering.Blue.. defined like so: Private ProgressPercent As Int32 = 0 'percentage of tasks completed (0 to 100) Whenever our code progressed in its task. no matter how their vendors might try to mask that fact behind “cool” transitions. effects. Idx. Dim g As Graphics = Graphics.Clear(.picProgress. we can choose from a number of DrawImage() method overloads. PgrsBkH. if needed. We can do this easily enough by dividing the PgrsAct value by the width of the background image. 0. For that.Image.DoEvents() 'get a graphics interface to the image to draw to 'drawing area width 'drawing area height 'progress background image width 'progress background image height 'progress bar image width 'progress bar image height 'compute pixel width of progress indicator bar 'compute bar as rendered by full copies of background 'set the index to start point 'initialize the image to a blank background 'if not 0%. srcUnit As GraphicsUnit). and the height. which is.picProgress g.picProgress.picProgress.0 – David Ross Goben The trick to tiling the background image is that if we always use the full width of this image..Pixel). we must also specify the Graphics Units that we are using. we should only need to specify pixels (GraphicsUnit. 0. GraphicsUnit.FromImage(Me.. srcHeight As Int32.Image. held by Remainder. we specify the source image.DrawImage(Me. and finally the width and height to use from the source image. presently held by Idx.. though I will focus here on DrawImage(Image As Image...ProgressBack. the above becomes: Dim Remainder As Int32 = PgrsAct Mod PgrsBkW 'compute # pixels to draw after whole images tiled If Remainder <> 0 Then 'if we have something left. 0. we might spill over where we want the right edge of the progress indicator bar to actually be. the destination rectangle.Width Dim ImageH As Int32 = Me. the width we want to render. and finally the height of the background image. 'tile the progress background image to the picturebox 'tile rightwardly 'release drawing interface 'update the display of the drawing 'let other things happen The next thing we need to do is determine if we need to additionally paint a final. As you can see. Our target rectangle will specify the starting left index.Dispose() Me. As such.. These last four parameters allows us to specify a rectangular portion of the source image to draw from.. as we have already demonstrated. This seems like a mouthful. srcY As Int32. held by ImageH.Refresh() Application.picProgress.Image) Dim ImageW As Int32 = Me. first. 0. which is the height of the target Image.ProgressImage. and then specify the width we want to render..ProgressBack. DestRect As Rectangle. but it is actually quite simple. Remainder.. and then the top-left X and Y offsets to start rendering from the source image. g. We next have to specify the portion of the background image we want to render from. New Rectangle(Idx..Height Dim PgrsBkW As Int32 = Me. held by the variable PgrsBkW: If ProgressPercent < 0 OrElse ProgressPercent > 100 Then Return End If 'if there is no need to update. So. We will need to draw a clip of our background image to a target location with a specified size. which we would do immediately after the above Loop instruction: Dim Remainder As Int32 = PgrsAct Mod PgrsBkW If Remainder <> 0 Then 'draw a partial image here.Pixel) 'draw a portion of the Image to a location and size End If Page –633– .. PgrsBkH. and then clip (partially paint) a final rendering. as demonstrated earlier. We will always begin painting from its top-left corner. ImageH).ProgressImage. Thus.Enhancing Visual Basic .Height Dim PgrsBrW As Int32 = Me. the top of the target Image. PgrsBkW. To keep everything simple. Remainder. we should only tile full images for as far as we can. 0. srcWidth As Int32.0.Height Dim PgrsAct As Int32 = ProgressPercent * ImageW \ 100 Dim AbsoluteWd As Int32 = (PgrsAct \ PgrsBkW) * PgrsBkW Dim Idx As Int32 = 0 With Me. which is the next pixel after the previously rendered background image (if any). 'while we have not exceeded the full display area. ImageH) Idx += PgrsBkW Loop End With g.. we should compute how many full background images we can initially tile. which is coordinate 0.BackColor) If ProgressPercent > 0 Then Do While Idx < AbsoluteWd g.NET Beyond the Scope of Visual Basic 6.ProgressBack. End If 'compute # pixels to draw after whole images tiled 'if we have something left. Remainder.ProgressBack. With it. srcX As Int32.Width Dim PgrsBrH As Int32 = Me.DrawImage(Me. partial (clipped) image of the background. This is important if we have also changed the height of our emulated ProgressBar PictureBox.Width Dim PgrsBkH As Int32 = Me. it would look even better if we animated it with a cycling glare effect. g. 0. If it does.NET Beyond the Scope of Visual Basic 6. or clipped images. so far.Enhancing Visual Basic ..Clear(... 0.Refresh() 'update the display of the drawing Application. SrcW.0 – David Ross Goben Therefore..ProgressImage.DrawImage(Me.Tick If ProgressPercent < 0 OrElse ProgressPercent > 100 Then 'if there is no need to update.DoEvents() 'let other things happen Me. New Rectangle(Idx.Pixel) 'draws a portion of the Image to a location and size End If '<<-We will render the animated progress glare image in this space->> End If End With g.ProgressBack. To do that. we may need to clip the animated glare image.FillRectangle(Brushes. SrcW. ByVal e As EventArgs) Handles tmrProgress. Remainder. our tmrProgress_Tick() event code becomes: '********************************************************************************* ' Method : tmrProgress_Tick ' Purpose : ProgressBar emulation.Enabled = True 're-enable timer End Sub Now that we can emulate a simple ProgressBar using a custom image..PgrsAct) End If g. ImageH). 'trim the Glare Image width to what we can draw of it 'draws a portion of the Glare Image to a location and size Altogether this becomes: Page –634– .picProgress g.Blue.Image. we simply check to see if its right edge will extend beyond the right extent of our progress indicator. or at least comfortable with rendering partial.Pixel) 'grab the current X offset for the glare image 'init ScrW to the full width of progress Glare Image 'if the Glare Image end will exceed desired bar width.BackColor) 'initialize the image to a blank background If ProgressPercent > 0 Then 'if not 0%.ProgressBack. ImageH) 'draw rectangle to display progress indicator bar Do While Idx < AbsoluteWd 'while we have not exceeded the full display area.ProgressImage. which is firing every 20 milliseconds (or whatever you set variable ProgressSpeed to). 0. Remainder.DrawImage(Me... 0. ImageH) 'tile the progress background image to the picturebox Idx += PgrsbW 'tile rightwardly Loop Dim Remainder As Int32 = PgrsAct Mod PgrsbW 'compute # pixels to draw after whole images tiled If Remainder <> 0 Then 'if we have something left. if its right edge exceeds the right limit of the progress indicator bar. Like with drawing the background image to the right edge of the desired progress indicator dimensions.Width 'progress bar image width Dim PgrsiH As Int32 = Me. GraphicsUnit.. PgrsiH. PgrsbH. especially because it is being constantly redrawn anyway within our timer event.ProgressImage. this additional step should be child’s play: Idx = CInt(Me.Dispose() 'release drawing interface Me. 0.picProgress.tmrProgress.Width 'progress background image width Dim PgrsbH As Int32 = Me.picProgress..Width 'drawing area width Dim ImageH As Int32 = Me.Tag) Dim SrcW As Int32 = PgrsiW If Idx + SrcW > PgrsAct Then SrcW -= (Idx + SrcW .Height 'drawing area height Dim PgrsbW As Int32 = Me. 0. we need to clip it back to stay within bounds.tmrProgress. PgrsbW.. using just a background image '********************************************************************************* Private Sub tmrProgress_Tick(ByVal sender As Object. GraphicsUnit.Image. g.Enabled = False 'disable timer for now Dim g As Graphics = Graphics. Idx. ImageH).Height 'progress bar image height Dim PgrsAct As Int32 = ProgressPercent * ImageW \ 100 'compute pixel width of progress indicator bar Dim AbsoluteWd As Int32 = (PgrsAct \ PgrsiW) * PgrsbW 'compute bar as rendered by full copies of background Dim Idx As Int32 = 0 'set the index to start point With Me.FromImage(Me. PgrsAct.Image) 'get a graphics interface to the image to draw to Dim ImageW As Int32 = Me.ProgressImage. As we had done previously.ProgressBack. Return End If Me.Height 'progress background image height Dim PgrsiW As Int32 = Me.picProgress.picProgress. Since we are all experts by now. New Rectangle(Idx. 0.DrawImage(Me. ProgressImage.ProgressBack. 0. We will also need to determine if we must clip this image when we render it. we maintain the index for the starting left edge of our glare image in the Tag property of our foreground glare image. 0. 'g. above. Idx = -PgrsiW 'reset origin to < 0 End If Me..Width 'progress bar image width Dim PgrsiH As Int32 = Me.Tick If ProgressPercent < 0 OrElse ProgressPercent > 100 Then 'if there is no need to update..Width 'progress background image width Dim PgrsbH As Int32 = Me.Refresh() 'update the display of the drawing Application. SrcW. New Rectangle(Idx.DrawImage(Me..Width . Y)) fnt.DrawImage(Me. g. 0.. PgrsiH.sz. just before you dispose of the Graphics Interface...Enhancing Visual Basic .picProgress. SrcW -= (Idx + SrcW .Pixel) 'draws a portion of the Glare Image to a location and size End If End With g.Black.FromImage(Me. 12.Image) 'get a graphics interface to the image to draw to Dim ImageW As Int32 = Me.picProgress. PgrsbH.ProgressImage.NET Beyond the Scope of Visual Basic 6.Image.ToString & " %" Dim sz As Size = TextRenderer. Do While Idx < AbsoluteWd 'while we have not exceeded the full display area. ImageH). ImageH) 'tile the progress background image to the picturebox Idx += PgrsbW 'tile rightwardly Loop Dim Remainder As Int32 = PgrsAct Mod PgrsbW 'compute # pixels to draw after whole images tiled If Remainder <> 0 Then 'if we have something left. FontStyle.Height) \ 2 g.picProgress.ProgressImage.Dispose() 'release drawing interface Idx += ProgressInc 'bump the offset If Idx > ImageW Then 'if it exceeds the drawing surface.Pixel) 'draws a portion of the Image to a location and size End If Idx = CInt(Me.Height 'progress background image height Dim PgrsiW As Int32 = Me. 0. GraphicsUnit.Height 'progress bar image height Dim PgrsAct As Int32 = ProgressPercent * ImageW \ 100 'compute pixel width of progress indicator bar Dim AbsoluteWd As Int32 = (PgrsAct \ PgrsbW) * PgrsbW 'compute bar as rendered by full copies of background Dim Idx As Int32 = 0 'set the index to start point With Me..ProgressBack. You can render this effect easily using the graphics interface’s DrawString() method right within the Tick() event method. between the highlighted End If and the End With.Image. Return End If Me. 0. fnt. Remainder. SrcW.Tag = Idx 'update the index Me. much as older systems used to do.. ImageH).DrawImage(Me.DrawString(pCent. ByVal e As EventArgs) Handles tmrProgress. g.MeasureText(pCent.PgrsAct) 'trim the Glare Image width to what we can draw of it End If g.Dispose() End With g.. 0.Bold) Dim pCent As String = ProgressPercent.picProgress. 0.Enabled = False 'disable timer for now Dim g As Graphics = Graphics.Width 'drawing area width Dim ImageH As Int32 = Me.ProgressBack. 0.ProgressBack.ProgressBack.Tag) 'grab the current X offset for the Glare Image Dim SrcW As Int32 = PgrsiW 'init SrcW to the full width of progress Glare Image If Idx + SrcW > PgrsAct Then 'if the Glare Image end will exceed desired bar width. Remainder.picProgress g.Enabled = True 're-enable timer End Sub All that is left to do is perhaps displaying the percentage completed within the center of the ProgressBar..Width) \ 2 Dim Y As Int32 = (.Height 'drawing area height Dim PgrsbW As Int32 = Me.ProgressImage.Dispose() Page –635– 'font to use 'define percentage string 'find its dimensions 'compute how to center it horizontally 'compute how to center it vertically 'draw the string to the progressbar 'release created font 'release drawing interface .DoEvents() 'let other things happen Me.0!. using a background image with an animated glare '********************************************************************************* Private Sub tmrProgress_Tick(ByVal sender As Object.Height .Clear(. Brushes. PgrsbW.tmrProgress.. like so: End If Dim fnt As New Font("Arial".ProgressImage.sz. fnt) Dim X As Int32 = (.ProgressImage.tmrProgress.. New Rectangle(Idx. 0.0 – David Ross Goben '********************************************************************************* ' Method : tmrProgress_Tick ' Purpose : ProgressBar emulation. GraphicsUnit. Idx. New Point(X.BackColor) 'initialize the image to a blank background If ProgressPercent > 0 Then 'if not 0%. all we need to do is keep track of the width of all three images. Middile = 28x40 pixels. we will clip the middle image as needed so that the over-all target width will be satisfied. or just a portion of the left image. If the result is less than then width of the left image. you will find a quite massive variety of them. we subtract the width of the right image from the target width. we determine if we can render all. If you look for images of buttons in the internet. Create a new project and drop the form code on the next page into the form’s code page. and Right = 17x40 pixels. if the width limit is tiny. With the mouse. The middle image will be tiled to fill out the target width. such as if you Google or Bing “Glossy Button Images” or even “Button Images”. a left. As frustrated as some people get over trying to do this. if required. and then run it. and are presented in a variety of colors. and then render a final or only clipped portion.Enhancing Visual Basic . moving it rightward just as we did with placing our glare effect image on the PictureBox. it is surprisingly easy to do. middle. and right image. Just as we did earlier with clipping images. Using MS Paint and some patience.NET Beyond the Scope of Visual Basic 6. we will try to tile the middle image as needed. We would start by rendering the right image. Thus. such as “UpdateProgress(Me. Plus. Once we have rendered it. such as the following three segments: NOTE: These three images are dimensioned as follows: Left = 17x40 pixels. and if the result is greater then zero. They are three-dimensional and fun-looking. 75)”: Page –636– . 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. a Jelly image is actually composed of three separate parts.0 – David Ross Goben Custom ProgressBars With More Complex Images One popular image format for progress bars is the so-called Glossy Buttons or Jelly images.PicProgress. but so that its right edge matches the desired progress target width. and we can keep track of them with only a small amount of additional math. most of the right image could be hidden beyond the left ledge of the PictureBox. If we can render the full left image and there is still a positive result after subtracting its width. The featured UpdateProgress() method will take a reference to the target PictureBox and the percentage (0-100) to render to it. we move the origin of the left image leftward until its right end will join the right image. rendering them is actually quite easy to do. Actually. until the center area between the left and right images is filled. but less than just one of the end images. less the width of the left and right images. assuming the form is named Form1. click within the PictureBox and watch the displayed progress match it. you can capture them and break them up into segments that can be blended together. Typically. Width Me.Width. imgMdlW.DrawImage(Me. 0.. .X * 100 \ pic. 0.imgRgtW 'compute start of right image g.Width = Me.ProgressLft.ProgressRht.FromImage(..BorderStyle = BorderStyle.FixedSingle .NET Beyond the Scope of Visual Basic 6.. e As EventArgs) Handles MyBase.Image = New Bitmap(. 0.Parent = Me .progressMdl.Height . imgLftW.Clear(. ImageH).Load ProgressLft = InitializeLeftImage() 'build progress left image progressMdl = InitializeMiddleImage() 'build progress middle image ProgressRht = InitializeRightImage() 'build progress right image With Me.Top Or AnchorStyles.Clear(pic.DrawImage(Me. X.Left * 2 .Image.progressMdl. X. ImageH) 'draw what we can of the right image If X > 0 Then 'if we have room left over.ProgressLft.Height) Graphics.DrawImage(Me.Width 'drawing area width Dim ImageH As Int32 = pic..Height = progressMdl.Width Me. 0..Width Me.Image). 12) . X -= imgLftW 'compute start for left image g. e.Height 'get widths of images 'get height of middle image Dim X As Int32 = PgrsAct .BackColor) If PgrsAct > 0 Then Dim imgLftW As Int32 Dim imgMdlW As Int32 Dim imgRgtW As Int32 Dim imgMdlH As Int32 'first clear any previous drawing.Size.ProgressLft.Pixel) 'draw a portion of the Image to a location and size End If End If End If Page –637– .progressMdl. = = = = Me. New Rectangle(X. ImageH) 'tile the middle image to fill space X += imgMdlW 'bump offset Cnt -= 1 Loop If Remainder <> 0 Then 'if we have something left.Anchor = AnchorStyles.0 – David Ross Goben 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 continuos progress bar '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '********************************************************************************* '********************************************************************************* ' Method : Form_Load ' Purpose : Initialize Form '********************************************************************************* '********************************************************************************* Private Sub Form1_Load(sender As Object.FromImage(pic.Width ..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.Enhancing Visual Basic . ImageH) 'draw what we can of the left image Else 'we have room for middle image data.MouseClick Dim pic As PictureBox = DirectCast(sender.picProgress ..Location = New Point(12.DrawImage(Me. PictureBox) 'get referenced PictureBox UpdateProgress(pic. so update progress to that point '********************************************************************************* Private Sub picProgress_MouseClick(sender As Object. 0.BackColor) End With End Sub 'set parent 'position it in top-left of form 'size it across the form 'set picture holder height 'make border more visible 'anchor it 'create image space (was set to Nothing) 'initialize the image to a blank background '********************************************************************************* ' Method : picProgress_MouseClick ' Purpose : User picked on the image.progressMdl. imgMdlH. 0. Remainder.ProgressRht. imgRgtW. g.DrawImage(Me.Left . imgLftW.Image) 'get a graphics interface to the image to draw to Dim ImageW As Int32 = pic.Height 'drawing area height Dim PgrsAct As Int32 = Percent * ImageW \ 100 'compute required pixel width of progress indicator g.Image.. Remainder. ImageH) 'so draw the left image all the way left X -= imgLftW 'figure how much room is left Dim Remainder As Int32 = X Mod imgMdlW 'compute # pixels to draw after whole images tiled Dim Cnt As Int32 = X \ imgMdlW 'compute how may whole middle images can be tiled X = imgLftW 'init start position beyond left image Do While Cnt <> 0 g. X. ByVal Percent As Int32) Dim g As Graphics = Graphics... g. GraphicsUnit. If X <= imgLftW Then 'if we can show just the left image.Image..ClientRectangle. 0. 0. e As MouseEventArgs) Handles picProgress. Enhancing Visual Basic . FontStyle.sz.NET Beyond the Scope of Visual Basic 6.Refresh() End Sub 'font to use 'define percentage string 'find its dimensions 'compute how to center it horizontally 'compute how to center it vertically 'draw the string to the progressbar 'release created font 'release drawing interface 'update the display of the drawing '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' 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 Page –638– & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & & . Yf)) fnt.ToString & " %" Dim sz As Size = TextRenderer.0!.sz. Brushes.Height) \ 2 g.Black.Width) \ 2 Dim Yf As Int32 = (ImageH .0 – David Ross Goben Dim fnt As New Font("Arial". New Point(Xf.MeasureText(pCent.Dispose() pic.DrawString(pCent.Dispose() End If g. fnt) Dim Xf As Int32 = (ImageW . 12.Bold) Dim pCent As String = Percent. fnt. FromStream(memStream) 'construct image from stream data memStream.0 – David Ross Goben '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : InitializeRightImage '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function InitializeRightImage() As Image Dim strImg As String = "iVBORw0KGgoAAAANSUhEUgAAABEAAAAoCAIAAACXeobIAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" "wwAADsMBx2+oZAAAB6RJREFUSEuF0flX0nsex/Hvn3DvsZqWW1O3blmWS2lO2W27Zd2pSc00Ky3XtGzS" "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.Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6.FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO.Close() 'release stream resources Return Img 'return image End Function End Class Page –639– .MemoryStream = New IO.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image. but it is totally unnecessary. but the length returned is zero. IntPtr. But if they declared them as Friend. however.Enhancing Visual Basic . we also explored how to access the TextBox control that is embedded in a ComboBox. COMBOBOXINFO.Zero. which is why the Len() command normally has no problems with them. its length with natural alignments accounted for). Here is the COMBOBOXINFO structure and the GetComboBoxInfo() P/Invoke: 'Used by GetComboBoxInfo() to get ComboBox references <StructLayout(LayoutKind. As such. Extending VB. far. in order to display the whole of its member items. setting it to the maximum width of its widest member might not be feasible. or even use EnumChildWindows() to get its handle. there will be no error reported.DLL" (ByVal hwndCombo As IntPtr.ComboBox1. However. in this case your users will still not be able to see the full text of those members of the dropdown list that exceed its width. to work. and so we are not able to use the FindWindowEx() function. is that they have trouble getting the associated structure. As a result. is not as easy to grab because it is not defined as a child of the ComboBox control as the TextBox and the dropdown button are. though setting it wider nonetheless would make it much more manageable and practical for you and your application users. you can resolve these problems by simply using the runtime interop Marshal. where we employed the system’s FindWindowEx() P/Invoke (Dim lhWnd As IntPtr = FindWindowEx(Me. you can get away with just setting it to 64. many programmers have often jumped through hoops in order to trap this list control. unless you are able to also add a horizontal scroll bar to that dropdown list. So. because a P/Invoke exists named GetComboBoxInfo() that can gather all that information. To get the handle for the associated ListBox.Handle. Private Declare Function GetComboBoxInfo Lib "user32. ByRef pcbi As COMBOBOXINFO) As Boolean Page –640– . you were shown how to adjust the width of the dropdown list from a ComboBox to a different. because of non-common gaps in the natural alignment of members that is not a typical of Win32 structures. I have found one issue that is at the root of this trouble.Sequential)> Private Structure COMBOBOXINFO Friend cbSize As Int32 'size of this structure must be set before using (use Marshal.NET Beyond the Scope of Visual Basic 6. also a member of the above article in the subsection “Adding a CheckBox to the Edit Field of a ComboBox” on page 227. such as monitoring the message queue and finding the first message to a ListBox after the dropdown message is intercepted.SizeOf() method to get the proper length (well. and that is setting its cbSize member to the size of the structure. "EDIT". the dropdown list. and typically a wider size. But even so. under the subsection “Sizing the ComboBox DropDown List Width” on page 228. vbNullString)) to acquire its handle in order to adjust its margins so that a CheckBox could rest within its left side and not cover displayed text. it causes a Len() command to issue an exception error when they declared the structure’s members as Dim or Public.0 – David Ross Goben Black Book Tip # 59 Adding a Horizontal Scrollbar to a ComboBox DropDown List In a previous article. That is a lot of work. it is a separate control that is simply associated with it and displayed next to it. which has a class name of COMBOLBOX (notice the embedded “L”). Previously.SizeOf()) Friend rcItem As Rectangle Friend rcButton As Rectangle Friend stateButton As Int32 'state of the dropdown button Friend hwndCombo As IntPtr 'handle of ComboBox Friend hwndEdit As IntPtr 'handle of child EditBox (Class name EDIT) Friend hwndList As IntPtr 'handle of associated (but not child) ListBox (class name COMBOLBOX) End Structure 'Retrieves information about the specified combo box.NET Controls Functionality. However. Due to the less-than-robust design of the COMBOBOXINFO structure. this is not always practical because the list of items might be exceedingly long. The problem programmers have with this method. such as Vertical and Horizontal Scrollbars on a PictureBox. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As IntPtr. assuming that the namespace System. maxWidth. ByVal dwNewLong As Integer) As Integer 'Retrieves information about the specified window. but we can get the handle of the dropdown ListBox as well in the structure’s hwndList member. ByVal nIndex As Integer) As Integer ' Sends the specified message to a window or windows. ByVal lParam As Integer) As Integer 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. We can do this by measuring each member of the ListBox’s data list and determining the longest member.. GWL_STYLE) 'get current combobox listbox style If (Styl And WS_HSCROLL) = 0 Then 'if it does not yet have a horizontal scrollbar. but we will need three more P/Invokes and three more constants in order to interrogate the control associated with this acquired handle: 'Changes an attribute of the specified window.MeasureText(itm.. we will apply that Style to it: Dim Styl As Int32 = GetWindowLong(cInfo. SetWindowLong(cInfo. End If 'get reference to affected ComboBox 'storage for combobox info 'get structure length 'if we grabbed combobox info. What we need to do is grab the current Style set for the ListBox.InteropServices is also imported to support the structure and the Marshal class: Dim cboBox As ComboBox = DirectCast(sender. 'then update the max width 'set the horizontal extent of the listbox And that is all there is to it! Consider the module modCboAddHrzScrollbar. Private Declare Function SetWindowLong Lib "user32. We can implement the above by using the following simple code.DLL" Alias "GetWindowLongA" (ByVal hwnd As IntPtr. ByVal wMsg As Integer.0 – David Ross Goben As you can see.Width For Each itm As String In cboBox.Enhancing Visual Basic . ComboBox) Dim cInfo As COMBOBOXINFO cInfo. which is what a ListBox really is on its surface (its text is simply painted onto the PictureBox sureface).SizeOf(cInfo) If GetComboBoxInfo(cboBox.hwndList.Width If maxWidth < ln Then maxWidth = ln End If Next SendMessage(cInfo.hwndList.Handle. End If Once we have applied a Horizontal Scrollbar to the ListBox control. Private Declare Function GetWindowLong Lib "user32. in using this structure and P/Invoke.... we can not only also get the handle to the EDIT window. ByVal wParam As Integer. You just invoke its AddHrzScrollbar() method with the selected ComboBox as a parameter to apply a Horizontal Scrollbar to it.hwndList above. Styl Or WS_HSCROLL) 'apply a horizontal scrollbar to it 'we need to still define a maximum horizontal extents so the new horizontal scrollbar will be displayed.. how can we use that to add a horizontal scrollbar to the ListBox? Without much effort really... Page –641– . The function also retrieves 'the 32-bit (DWORD) value at the specified offset into the extra window memory.Font). and see if it has a Horizontal Scrollbar Style assigned to it.Items Dim ln As Int32 = TextRenderer..NET Beyond the Scope of Visual Basic 6. and finally setting the horizontal extent of the ListBox from that: Dim maxWidth As Int32 = cboBox. LB_SETHORIZONTALEXTENT. The function also sets the '32-bit (long) value at the specified offset into the extra window memory.. If not.hwndList.Runtime.. ByVal nIndex As Integer. The SendMessage function ' calls the window procedure for the specified window and does not return ' until the window procedure has processed the message. cInfo) Then 'if we captured the structure. listed on the next page. GWL_STYLE.cbSize = Marshal. 'get the length of each member 'if the member is longer than the current max. cboBox. Now that we have access to the Handle for the dropdown ListBox. do more here. we need to increase the horizontal extent of the ListBox so that the new Horizontal Scrollbar will actually be displayed. referenced by cInfo.DLL" Alias "SetWindowLongA" (ByVal hwnd As IntPtr. 0) 'get the current width of the combobox as a base 'check each member of the combogbox... do more here.Sequential)> Private Structure COMBOBOXINFO Friend cbSize As Int32 'size of this structure must be set before using (use Marshal. The function also sets the '32-bit (long) value at the specified offset into the extra window memory. ByVal dwNewLong As Integer) As Integer 'Retrieves information about the specified window.NET Beyond the Scope of Visual Basic 6. Dim Styl As Int32 = GetWindowLong(cInfo. Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As IntPtr. so that you will ' need to update the extents of the ComboBox... Private Declare Function SetWindowLong Lib "user32.SizeOf()) Friend rcItem As Rectangle Friend rcButton As Rectangle Friend stateButton As Int32 'state of the dropdown button Friend hwndCombo As IntPtr 'handle of ComboBox Friend hwndEdit As IntPtr 'handle of child EditBox (Class name EDIT) Friend hwndList As IntPtr 'handle of associated (but not child) ListBox (class name COMBOLBOX) End Structure 'Retrieves information about the specified combo box.SizeOf(cInfo) If GetComboBoxInfo(cboBox.DLL" (ByVal hwndCombo As IntPtr. ByVal nIndex As Integer. ' If True Then '------------------------------------------------------------------------------------'$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 'Used by GetComboBoxInfo() to get ComboBox references <StructLayout(LayoutKind. ByRef pcbi As COMBOBOXINFO) As Boolean 'Changes an attribute of the specified window.DLL" Alias "GetWindowLongA" (ByVal hwnd As IntPtr.hwndList. cInfo) Then 'if we captured the structure. ByVal wParam As Integer.. 'get the length of each member 'if the member is longer than the max..cbSize = Marshal.. simply invoke the GetComboBoxInfo() ' method with the ComboBox you want to add a Horizontal Scrollbar to as a parameter. Private Declare Function GetComboBoxInfo Lib "user32.Width If maxWidth < ln Then maxWidth = ln End If Next SendMessage(cInfo. LB_SETHORIZONTALEXTENT. ByVal wMsg As Integer.MeasureText(itm.Enhancing Visual Basic . Private Declare Function GetWindowLong Lib "user32. 'apply a horizontal scrollbar to it 'get the width of the combobox as a base 'check each member of the combogbox.Width For Each itm As String In cboBox..InteropServices Module modCboAddHrzScrollbar '------------------------------------------------------------------------------------' To add a Horizontal Scrollbar to a ComboBox..0 – David Ross Goben Option Strict On Option Explicit On '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------' modCboAddHrzScrollbar Static Module Class ' Add a Horizontal Scrollbar to a ComboBox '------------------------------------------------------------------------------------'------------------------------------------------------------------------------------Imports System..Handle. ' ' NOTE: If you will be adding or removing members from this ComboBox.Items Dim ln As Int32 = TextRenderer.hwndList.Runtime. ' ' This method will set the horizontal extent (the maximum you can scroll to the right ' to) from the longest member in the ComBoxBox. 'then update the max width 'set the horizontal extent of the listbox .hwndList. 0) End If End If End Sub End Module Page –642– 'storage for combobox info 'get structure length 'if we grabbed combobox info. maxWidth. ByVal nIndex As Integer) As Integer ' Sends the specified message to a window or windows... 'get current combobox listbox style 'if it does not have a horiz scrollbar. you will want to replace the following line: ' If (Styl And WS_HSCROLL) = 0 Then 'with.. ByVal lParam As Integer) As Integer Private Const LB_SETHORIZONTALEXTENT As Int32 = &H194 'adjust horizontal extent of a listbox Private Const GWL_STYLE As Int32 = -16 'get/set a window style Private Const WS_HSCROLL As Int32 = &H100000 'apply a Horizontal Scroll style '$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ '******************************************************************************* ' Method : AddHrzScrollbar ' Purpose : Add a Horizontal Scrollbar to a ComboBox '******************************************************************************* Friend Sub AddHrzScrollbar(ByRef cboBox As ComboBox) Dim cInfo As COMBOBOXINFO cInfo.. GWL_STYLE.. cboBox. The function also retrieves 'the 32-bit (DWORD) value at the specified offset into the extra window memory.Font). Styl Or WS_HSCROLL) Dim maxWidth As Int32 = cboBox. GWL_STYLE) If (Styl And WS_HSCROLL) = 0 Then SetWindowLong(cInfo. The SendMessage function ' calls the window procedure for the specified window and does not return ' until the window procedure has processed the message.DLL" Alias "SetWindowLongA" (ByVal hwnd As IntPtr. is greatly disappointing to many developers who do not want to have to wait for smart-device processors to finally evolve to much faster speeds in order to get their old 3D effects once again back on their PCs.PointToClient(Cursor.Height))) 'draw border by filling all brsh.Dispose() 'Dispose of the pen resource brsh. such as Windows 8/8.DrawLine(pn.ControlDark) 'draw darker line near top for beveled look e. emulating that appearance is relatively easy. 3). .Color = . 0.sz.ControlLight 'color for lower half (why is this darker?) e.Height \ 2))) 'draw upper half of button brsh. New Rectangle(New Point(0. . sadly.Font. and just assign it to the button’s background. What we could do to get an easier and faster effect is to employ a simple image of a 3D curved surface.Width . but it also involves an outer border.Height .Width .PaintEventArgs) Handles Button1.FillRectangle(brsh.Highlight) 'button edge will be washed-out highlight End If e.FillRectangle(brsh. (.Width) \ 2.. 3) If . or perhaps anything that soon comes after them.6. Defining a 3D Image Template All we really need is to define a 3D curved surface background image that we can assign to buttons and other controls.Forms.ForeColor 'color to paint the text e.sz.FromArgb(8. this result is much better than a boring ‘Standard’ Button control. (.Color = SystemColors. New Rectangle(New Point(3. named Img3D: With it.Height) 'compute rect to center the text in the button brsh. 0)..6) \ 2))) 'draw lower half of button Dim pn As New Pen(SystemColors.1. New Size(. New Rectangle(0.Width . For example: Private Sub Button1_Paint(sender As Object.Enhancing Visual Basic . This is part of Microsoft’s strategy to dial back the Desktop and Laptop imaging technology in order to be be more compatible and more easily blend with the graphically much slower smart-devices and pads that are not yet capable of PC-level graphics acceleration.Graphics. we could assign it to the button’s BackgroundImage property and also set the button’s BackGroundImageLayout property to ImageLayout.Height . SystemColors.FillRectangle(brsh.ControlLightLight) 'button edge if not selected If . .Position)) OrElse . enhancement lines.Width .Height) \ 2.Control 'color for top half of button e.Location) 'center the text in the button pn. New Size(. or Windows 10. e As System.MeasureText(. Button) Dim brsh As New SolidBrush(SystemColors.Bounds.BackgroundImage = Img3D .NET Beyond the Scope of Visual Basic 6.DrawString(. Rendering the effect using a Paint() event is a bit cumbersome.Color = SystemColors.Color = SystemColors. End If e. and such. New Rectangle(New Point(3..Height . Button controls and TabPage tabs are now rendered with a flat appearance.6.Stretch End With 'assing 3D image to button background 'make sure it is scaled to the button Page –643– . rct.BackgroundImageLayout = ImageLayout.Paint With DirectCast(sender.Color = Color. losing their older ‘3D’ curved-surface appearances.Button1 .Highlight 'border highlight if selected..Contains(. Even so. as you can see in the Paint() event listing below.Graphics. Button2.Focused Then 'if mouse is hovering or selected. .Dispose() 'dispose of the brush resource End With End Sub This is a lot of effort to do basically a mostly two-tone image with some beauty edging and support for mouse-over enhancement.Text. Suppose we had the image shown to the right. .Paint.Graphics.1)) 'draw border of button Dim sz As Size = TextRenderer. This. .Parent.Graphics.0 – David Ross Goben Black Book Tip # 60 Restoring 3D Curved-Surface Appearances for Buttons and Tabs If you have an operating system that came after Windows Vista.Width. sz.Graphics.Text. Even so. sz. brsh. because it is not simply a matter of drawing the upper half a lighter color than its lower half.Focused Then pn.Windows. . 3.Width .Height \ 2 + 1).Graphics. New Size(. 3.6.Width. brsh. .Stretch: With Me.DrawRectangle(pn.Font) 'get size of text Dim rct As New Rectangle((.1. .Regular) 'and ensure that the tab font is normal (in case it is not) e.Index Then 'is the current tab to paint also the active tab? With tabRect 'for the line below to work. ' : Be sure that [TabControl].NET Beyond the Scope of Visual Basic 6. use a grayed background color. make sure TabPage.GetTabRect(e.Close() 'release stream resources Return Img 'return image End Function You can even render this image to different-sized targets.txtSize.Backcolor is not set to Transparent. FontStyle.Navy 'and use Navy for its text. such as the tabs on a TabControl.. tabRect) 'now.Index) 'get the tab rectangle for the current tab e. txtFont = New Font(TabPage.txtSize. Taking advantage of the BuildImageCode() method outlined in the Creating Single Base64 Image Data subsection of Black Book Tip # 58 on page Error! Bookmark not defined... modified to use the above image. txtFont) 'get the width and height of the text to render in pixels Dim Y As Int32 = tabRect.OwnerDrawFixed.Font. txtFont = New Font(TabPage. or to the background color that you will be using for the selected tab. txtFont.DrawMode = TabDrawMode.X.Graphics.Dispose() 'dispose of the created pen resources End With bgBrush = Brushes. CntrRect) 'draw the tab text txtFont.0 – David Ross Goben This would be all we would have to do with all our buttons.0!) 'create a 2-pixel pen to cover the bottom border e.. or it will not seem to work Dim tPen As New Pen(TabPage.Height + 1.MemoryStream = New IO. .OwnerDrawFixed '********************************************************************************* Private Sub DrawOnTab(sender As Object. TabControl) 'get the tab control being processed Dim TabPage As [TabPage] = TabControl.MemoryStream(bAry) 'convert byte array to a memory stream Dim Img As Image = Image. so use White for the tab background.Button1 . but ' : it is used to render its tabs and their text.Y + . I rendered such an image..BackgroundImage = Img3D .Width .DrawString(TabPage.Index) 'get the tab page being processed Dim tabRect As Rectangle = TabControl. Consider the following drawing method used if the TabControl had its DrawMode property set to TabDrawMode.LightGray 'otherwise. txtBrush..DrawItem Dim TabControl As [TabControl] = DirectCast(sender.Text.DrawRectangle(Pens.X + (tabRect.Graphics.Height + 1) 'draw a thick.Graphics. covering white line beneath the tab tPen. But the next trick is where to get the image.Black 'use our default text color. featured in Black Book Tip # 13 on page 374): '********************************************************************************* ' Method : DrawOnTab ' Purpose : Owner draw each individual tab on a TabControl. Else bgBrush = Brushes..Stretch End With 'build 3D image 'assing 3D image to button background 'make sure it is scaled to the button Here is the definition of InitializeImage(): '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ' Method : InitializeImage '+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Private Function InitializeImage() As Image Dim strImg As String = "iVBORw0KGgoAAAANSUhEUgAAAJwAAAAuCAIAAABS2OdkAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO" & "wwAADsMBx2+oZAAAAPxJREFUeF7t0TlOQwEUQ1Hvf1es4DNDGBo6xhAkqjQpvrOE+ClXPp0rW7IuCkf7" & "wtF/4eivcHoqUE8F0q5weipQTwXSb5A7p3kNV/TUytA2yJ3TvIYrempl9FQg/QS5c5rXcOk7yJ3TvIYr" & "empl6CtorXSok4meekY+j5zPirw9Yq10qJOJnloZ+igcvRWOXgtHL4Wj58LpqUA9FUhPQe6c5jVc2gS5" & "c5rXcOkxyJ3TvIZLD0HunOY1XLoPcuc0r+GKnloZPRVIt0HunOY1XLoJcuc0r+HSdZA7p3kNV/TUyuip" & "QLoKcuc0r+GKnloZuiwc3RWO3gtHS8EsywH82Th4DckCBQAAAABJRU5ErkJggg==" Dim bAry() As Byte = Convert. txtBrush = Brushes. fill the tab with our selected background color..FromBase64String(strImg) 'grab Base64 data as a byte array Dim memStream As IO. tabRect) 'render/stretch button in 3D End If Dim txtSize As Size = TextRenderer. tabRect) 'draw the tab rectangle Dim bgBrush As Brush Dim txtBrush As Brush Dim txtFont As Font 'store to desired tab background color 'store to desired font color 'store the draw font as bold or normal If TabControl. FontStyle.Enhancing Visual Basic .Graphics.FillRectangle(bgBrush.FromStream(memStream) 'construct image from stream data memStream.SelectedIndex = e..Y + .TabPages(e.Height .X + .Bold) 'then define the Bold font..Y + (tabRect. remember to set the BackColor property of each TabPage to White. ' : This is actually a TabControl object method.Dispose() 'and finally dispose of the created resource End Sub NOTE: For the blending of the selected tab to the background to work.Graphics.Font.MeasureText(TabPage.Silver. Page –644– .Height) \ 2 'computer vertical centering start location Dim X As Int32 = tabRect. e.. (this is the DrawOnTab() event code. . txtSize) 'computer new centering rectangle with text width/height limits e. e As DrawItemEventArgs) Handles TabControl1.Text.BackgroundImageLayout = ImageLayout. txtBrush = Brushes. such as: Dim Img3D As Image = InitializeImage() With Me. Y)..Width) \ 2 'compute horizontal centering start location Dim cntrRect As New Rectangle(New Point(X.DrawLine(tPen.White 'yes.Width.BackColor.DrawImage(Img3D.. . which you can grab by invoking the resulting InitializeImage() function and assigning its returned image to local field storage in your application. 2. under the sub-heading Emulating What VB6 Image Controls Do Using a Borderless Form on page 81.NET Beyond the Scope of Visual Basic 6. that is assigned as its Transparency color. the control has a fixed width when auto-scaled. Note that these are treated as extended behaviors beyond the control’s defaults.BackColor = Color.0 – David Ross Goben Black Book Tip # 61 Allowing a Form’s BackColor Property to Accept Transparency Colors This particular issue was first addressed in the much earlier article. If there was an exact RGB match. the control's Width remains unchanged. Normally. Clr) 'remove Clr transparency and assign result to form's BackColor property NOTE: Images. This can be used to make any control a container. though clearly many controls have these flags set or reset by default to establish their behavioral characteristics. the control is redrawn when it is resized. If true. such as Icons and PNG Images with Transparency. have a color. For example. the system would pick up color values and just compare their Red.NET. which the above Control reference actually implies). you 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 recently by just thinking about how transparency can best be handled. This is typically a normal RGB color. it offers some general advice that is too vague to provide any real help at all toward solving the issue. then that color would be rendered transparent. There are actually two solutions to this problem. the control is a container-like control.SupportsTransparentBackColor. if a layout operation attempts to rescale the control to accommodate a new Font. even though you might notice that general controls do not sport a SetStyle() or GetStyle() method. This enumeration features a number of useful flags. One is documented. which a form is actually derived from. The reason for this is that the addition of these auxiliary behaviors also eats more CPU cycles. The documented.Enhancing Visual Basic . as its name would imply. which I derived from the MSDN website and added additional notes as needed. ignoring its Alpha component. if you set a transparency color to the BackColor property of a form. or when resizing has completed before being redrawn. This can speed processing and reduce flicker if there are a lot of these. and Blue values (RGB). This style only applies to classes derived from Control. Doing a lot of forced image resizing introduces flicker. The way I figured it. the provided error message does not even mention the documented solution.SetStyle(ControlStyles. but by controls. but the easy one is not.FromArgb(&HFF. Emulating VB6 Image Control Features in VB. as listed below. Opaque ResizeRedraw FixedWidth Page –645– . However. If false. The ControlStyles enumeration is. If true. and render transparent any that do. the control waits for normal intervals to resize. If true. By default. to 255 (opaque) so that the Form’s BackColor property would be able to accept it without Style changes. assuming variable Clr is a variable of type Color: Me. but this issue is one prevalently discussed on the internet by users wanting to know how to work around the issue. such as Black or Magenta. the control is drawn opaque and its background color is not painted. the Paint event is not raised. you will get an exception error when you run the code. but forms do (as do User-Defined Controls. Green. If true. This works because the system will strip the Alpha component from the BackColor and test the other colors on its assigned surface for a matching RGB value. its transparency value. though obscure solution is to modify the form’s Style to support transparency colors: Me. I address it again here because many readers may have skipped over that article. but with its Alpha component set to 0. This is useful if the control’s background is not normally visible. but in most controls are not needed: Member name Description ContainerControl UserPaint If true. the control paints itself through its Paint event rather than the operating system doing so (default). For example. designed for use by not only forms. True) Afterward. As an experiment. I forced the Alpha component. If you set it to False. If you set this property to true. the control can receive focus. This is similar to the Opaque Style. Specifies that the value of the control's Text property. the control accepts a BackColor with an alpha component of less than 255 to simulate transparency. This is its default behavior. the control has a fixed height when auto-scaled.Paint() at the top of your Paint() event code). This property was not actually lost. The AllPaintingInWmPaint Style. Normally. If true. Page –646– . the control implements the standard DoubleClick behavior. if implemented. Setting it to true improves performance. setting this Style also requires that the UserPaint (default = True) and AllPaintingInWmPaint (default = False) Styles also be set to True. allows you to provide painting services. Doublebuffering prevents flicker caused by the redrawing of the control.0 – David Ross Goben Member name Description FixedHeight If true. the control implements the standard Click behavior. they should simply set the form’s DoubleBuffered property to True to get the exact same effect as they had under VB6. even if it contains transparency colors.NET have wailed and gnashed their teeth over the “loss” of this DoubleBuffer property in PictureBoxes. and after it completes. Transparency will be simulated only if the UserPaint bit is set to true (default) and the parent control is derived from Control. This style defaults to false. For example. you should also ensure that both UserPaint and AllPaintingInWmPaint are also set to true. If you set DoubleBuffer to true. greatly reducing or eliminating any refresh flicker that might normally occur. Thus. even though Dot Net PictureBoxes seldom need this feature. especially because we seldom access a control’s Text property while concurrently altering it in some obscure background process. If true. because multiple PictureBox controls no longer need to maintain their own DoubleBuffer caches. any Paint event. will solely handle painting services. when set to True. If true. the OnNotifyMessage method is called for every message sent to the control's WndProc. the control does its own mouse processing. Many VB6 developers moving to VB. will not be raised and the operating system. but will be completely covered by whatever the control is drawing over the top of itself. This is useful if the background will not be seen.Enhancing Visual Basic . If true. apart from also not drawing its background. such as an image that fills a PictureBox. via the control’s base class. the result is output to the screen. and a form’s OnPaintBackground and OnPaint events. the control ignores the window message WM_ERASEBKGND (erase/repaint background of control). This can be useful on a form with a lot of such controls. if a layout operation attempts to rescale the control to accommodate a new Font. the operating system paints the control. If true. you should first issue MyBase. the control is first drawn to a buffer rather than directly to the screen. will cause the background of the control not to be cleared during a repaint of the control. will be invoked directly from the system’s WM_PAINT event processing for the control. but was just moved 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 now take advantage of this feature.NET Beyond the Scope of Visual Basic 6. StandardClick Selectable UserMouse SupportsTransparentBackColor StandardDoubleClick AllPaintingInWmPaint CacheText EnableNotifyMessage DoubleBuffer OptimizedDoubleBuffer UseTextForAccessibility Notice the DoubleBuffer style. and mouse events are not handled by the operating system. but makes it difficult to keep the text synchronized. except when you intercept it by providing your own Paint() event for it (if you want the OS to continue to provide default painting services for features you do not want to deal with. though under most situations this might not ever be an issue. If true. If true. to reduce flicker. which can reduce flicker. NOTE: You can use the GetStyle() method to check a Style bit for being set or reset for a property. when set to True (default). except that the Opaque Style. will render all its data fully opaque. Note that setting the form’s exposed DoubleBuffered property ensures that all these thing are set for us in one command. This style should only be applied if the UserPaint bit is also set to true. EnableNotifyMessage does not work in partial trust. drawing is performed in a buffer. The UserPaint Style. the control's Height remains unchanged. If true. If true. determines the control's default Active Accessibility name and shortcut key. you should also set the AllPaintingInWmPaint to true. if set. even if you wrote a Paint() method to support the event. This style is ignored if the StandardClick bit is not set to true. apart from setting to the form causes the process to use a lot less resources. the control keeps a copy of its Text property rather than getting it from the Handle each time it is needed. Note further that. fully Opaque. If true. This style defaults to false. by rendering all of its color’s Alpha components to 255. As I have mentioned elsewhere within this manual. Microsoft has also released snippet editors since VB2005. where the ignorable code rests between Try and Catch. allowing easy alignment of form controls.DeleteValue(Key) Catch End Try Else gAppBaseSettings. 'then simply delete the registry key 'otherwise.. and there is actually quite a number of situation where this is the required case. Also available in VB. lead any complex code that should not actually require it with “On Error Resume Next”. Visual Studio includes quite a number of predefined snippets for common tasks. and which Microsoft says is safer. and you can define your own snippets. if you do make a wrong assumption about the type when using DirectCast. symbolic rename. Anything less is “pretend” code. such as Snaplines. which is a still a monumental step down from software engineer). which frees developers from writing code to deal with form resizing. which intelligently renames all occurrences of an identifier (for example. 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. Value contains data.. Their latest snippet editor is a Muti-Visual Studios version that can work with several different versions of Visual Studio. relieving you of having to juggle complicated syntax and brackets.NET are code snippets. Besides. finding that the object is of the provided type..com/B08B0375-139E-41D7-AF9BFAEE50F68392. available at https://visualstudiogallery. Code should always have strict type checking enforced.microsoft. as before.Catch. they are not. the CType directive can treat the process just like a DirectCast directive in that. No. plus.0 – David Ross Goben Closing Remarks The Visual Basic editor includes additional features. instead use the more structured Try. Control Anchoring. or worse. the Catch definition is blank and is in fact simply the command Catch. Option Explicit to Off. Please. though for me it is a pointless waste of compile time if I know exactly what type the object is. This is immensely helpful in debugging that wrong assumption within the code. code should never allow the compiler to have to make any assumption whatsoever about types. an error event is automatically triggered.NET Beyond the Scope of Visual Basic 6. a LOT. I use it everywhere where I know exactly what the generic object being provided to me actually is.msdn. 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. code should always provide explicit type definitions. but one that is a useful tool that helps me beef up the robustness of my code. If you actually do have code that does not care about the result of an operation. and built-in support for connecting forms to XML services..End Try body. It is free. ActiveX controls support. Granted. a variable). if you have optimization on. and code should always provide explicit error trapping. it will simply do the same as DirectCast.SetValue(Key. Option Infer to On. which should always be followed by “On Error Goto 0” behind the ignorable code. which are blocks of code for common tasks that you can insert into the code editor from the context menu. It even works with the Visual Basic Express editions. DirectCast is a whole lot faster than CType because DirectCast does not actually generate even one byte of compiled code. Value) End If 'if the Value is set to Nothing. instead of using the archaic. So I see the DirectCast directive as not only a faster and optimal command. and who harrumph and whine that they are professionals. I get a lot of flak from amateur and hobbyist programmers who claim this is OK (I refuse to acknowledge them as developers. and Catch is immediately followed by End Try: If Value Is Nothing Then Try gAppBaseSettings.Enhancing Visual Basic . I tend to use the DirectCast directive. do not set Option Strict to Off. instead of the more common CType directive. so save it to the registry Page –647– . from my perspective. making editing snippets a breeze. also within your code. These are purely amateur techniques that have no place in professional code. almost dinosaur-trodden “On Error Resume Next”. 0 – David Ross Goben For much more detailed help. 2016. It is immediately accessible.microsoft.com). Between their often anonymous posts on blogs and support sites.NET Beyond the Scope of Visual Basic 6. Note that these come standard as of VB2010.net/davidrossgoben) for instant answers on how to easily fix and eliminate most upgrade warning comments inserted in your upgraded code.0 to Visual Basic .com Please feel free to inform me of any errors or omissions. let them import their published programs and run them on their own computers. a free. David Ross Goben Last update: Wednesday. If you have youngsters. easy pre-Visual Basic development environment and compiler. their pushing Microsoft for more functionality.aspx). Also see my free companion pamphlet. Page –648– .ross.com/en-us/vbasic/aa701257. OvalShape. and featuring examples of implementation. and just begs you to try programming in it. and turned the VB. 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. or even a non-programmer friend who want to learn how to develop their own applications and games. the fruit of which makes this document more complete and more useful to those who are now struggling to master the VB. The optional Visual Basic Power Packs controls include LineShape. In addition to duplicating the behavior of the VB6 Line and Shape controls. Be sure to visit http://msdn.Enhancing Visual Basic . without subscription restrictions. run-time selection. developers.NET and C# implementation of P/Invoke formats. In a matter of a few lines of code. and even run time events (the Visual Basic Power Packs are available online for free.slideshare.aspx. 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.NET.goben@gmail. a beginner programmer will be well on their way to creating their very own game. February 24. these controls add many new capabilities. I humbly thank you for your unfettered and generous giving and sharing of knowledge. and RectangleShape controls that can be used to replace the VB6 Line and Shape controls. and software engineers who have poked.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. or of topics that I should additionally cover. a fantastic and easy avenue is Microsoft’s Small Basic.microsoft. from The Microsoft Developer Network (MSDN) at http://msdn. and their drive to make VB. please be sure to visit Redgate Software’s www.NET Application Upgrades” (www. “Navigating Your Way through Visual Basic 6. Special thanks goes out to all the dedicated amateur programmers. Another place to get great upgrade information is VB Migration Partners. and you will find a vast reservoir of help in converting VB6 code to . These include gradient fills. 8:28:54 AM Contact: david. Although products and services are sold here. For excellent help (and participation) in the VB. prodded.vbmigration.NET platform.pinvoke. it also features a wealth of freely available information (www. They can even share their programs with their friends.com/en-us/beginner/ff384126.net.NET a powerful RAD platform that surpasses my muchbeloved C++. The Electric Universe. He has written professional code in FORTRAN. built them. ancient cultural thinking. Expansion Tectonics. Of Jewish descent. language designer. He says that he has been designing software solutions since dinosaurs walked the Earth.NET Beyond the Scope of Visual Basic 6.Enhancing Visual Basic .goben@gmail. Particle Physics. His other interests include Cosmology. and others he wants to forget. he has been expected to think entirely out of the box and use intuitive perception to develop solutions to problems that were often assumed impossible. many not credited. Astrophysics. Nuclear Physics. C. He has written numerous books. being a systems designer. C++. the Global Warming Myth. and moved people's furniture across the country). Forth. manuals. and a compulsive developer (oh. and some he has successfully forgotten.0 – David Ross Goben About the Author David Ross Goben is an independent researcher who is obsessive about details. human-machine interaction. and exploring the ancient practice of Dream Walking. and he has also painted houses. exploring the glaring flaws in current Darwinian theory and Mendelian Inheritance. USA david. which had resulted in his seminal work: A Gnostic Cycle: Exploring the Origin of Christianity. Quartz Technology. Perpetual Energy Technology. Quantum Physics. or authored under pen names. a professional software engineer.ross. FL. and magazine articles.com Page –649– . living his life in glorious anonymity. studying the bio-mechanical origins of life. COBOL. the real truth of history. and ancient slang for over three decades. VB. various assembler languages. Pascal. His goal is to become as close as he can possibly be to a Universal Scholar. he has extensively explored Biblical history. David Ross Goben Lady Lake. As a software engineer. and author. real or imagined. Since their initial proposals. a great deal of which had in the past often been intentionally obscured. We will compare the evidence between the Electric Universe and the Gravity-Based Universe. In these last few decades. and reputations have often been decided wholly upon which ideas. We will also explore the long-held Prime Matter (Aether) theory that can strengthen the liquefacting sand upon which Particle Physics now finds itself. SlideShare.com.google. or worse. and Exploring How These Theories Connect (152 pages). dares to declare favor for one or the other.com. funding.com/open?id=0B_Dj_dKazINlZTFjOWZmMWItNTg3ZS00MDcxLWE3NDctNDhhZGY2Y2JkOTRk Also available for free on Scribd. and respond with typically scripted salvos of protest. And it may be no wonder. their acceptance of such ideas was a matter of personal or professional survival. So. like South African meerkats alerted to an impending threat. They are: Open Letters Sent to Advocates for the Electric Universe and Expansion Tectonics Theories. and which also strongly links the Electric Universe with Expansion Tectonics and makes these two models all the more plausible. gathered evidence has only strengthened the Electric Universe and the Expansion Tectonics theories. View and download this PDF document for free at: https://docs. and through web searches. each time additional evidence for either of them surfaces. too frequently droning the same old and practiced slogan-laced retorts like Gregorian Chants.0 – David Ross Goben Free Online PDF Documents Available by David Ross Goben Four ready-to-read PDF documents are available for both online viewing and free downloading from Google Docs. especially those of important and augustly respected note. prepare yourself for a Gnostic rollercoaster ride through an extremely thick ocean of information and history. or another of a fast-growing body of scientists. Page –650– . as if their need to deny the public’s access to. are accepted by the general public. select advocates for presumed ‘standard’ theories pop up. and between Expansion Tectonics and Plate Tectonics.Enhancing Visual Basic .NET Beyond the Scope of Visual Basic 6. tenures. In spite of this. most of them did not have the first clue.Enhancing Visual Basic . They did not grok the fact that in order to provide them with exactly what they were keenly expecting would also clearly necessitate colossal changes to their beloved VB so that it would be a fully integrated. they could simply continue on their merry little way. and expecting no less than 100% unrestricted object-oriented programming language capabilities. This manual makes dealing with those many changes a snap. such as C++. This requires perfect synchronicity between all those languages. When the majority of VB6 developers demanded the ability to build VB code in a common language IDE (Integrated Development Environment). and. relentless. oh yes. that each of them could clearly understand and use objects from each other without the slightest misconstruction. using the very same often non-uniform syntax that they had been using before.com.NET Application Upgrades (88 pages). of the earth-shattering impact their passionate. and through web searches. they expected there would be a few additional commands here and there to address full class inheritance. not the slightest understanding. View and download this PDF document for free at: https://docs.0 to Visual Basic . and also allow seamless access to methods whose source code was written in some other programming language.com/file/d/0B_Dj_dKazINlMjViMGUzZTUtMWFiZS00ZGNhLWE1NjEtMDQ4NjcwNmNiOTFm/edit?pli=1 Also available for free on Scribd. spittle-laced howling would have on their beloved VB. writing VB code exactly as they had done before. object-oriented environment that would also interoperate with and act exactly like the other Visual Studio languages (or other . naively believed that after such a necessarily monumental undertaking.0 – David Ross Goben Navigating Your Way Through Visual Basic 6.NET-compliant languages). An immense host of them.google. Page –651– . SlideShare. mostly amateur programmers and hobbyists. This is powerful lap reference and resource for those needing to upgrade their old VB6 application to the object-oriented universe of Microsoft Dot NET technology. have 100% unfettered cross-language interoperability.com.NET Beyond the Scope of Visual Basic 6. 0 – David Ross Goben Enhancing Visual Basic . Though some of those differences are real. and in all it also implements much more robust techniques to apply their functionality. actually end up being VB6 features that VB.NET using a radically different access conduit than the way it may have been realized under VB6.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. Of the real platform deviations. may look to be an intimidating endeavor. Other disparities.com.0 (VB6) to Microsoft Visual Basic . and it also provides you with a toolkit you can build yourself.com/file/d/0B_Dj_dKazINlRi1JWW42UXFzVG8/edit?pli=1 Also available for free on Scribd. but to really make your VB.NET platform architectural specifications. After all.0 (654 pages). some seen as much more profound. at: https://drive. due to tight . Nevertheless. again. by employing some simple user-defined helper functions. but being addressed by the time of the initial product launch. you will find that.NET strictly follows a stringent pattern of uniform language syntax.google.NET.NET supports all these many differences. you have likely heard or read through copious magazines and blogs that there are huge differences between these two developmental platforms. most are simply due to them having to be expressed differently.NET. both major and minor. VB.google.NET does in fact support. in later releases.com. This book helps you to not only break yourself free of that mold. The biggest problem here is that most new VB. Transitioning from Microsoft Visual Basic 6. or. which is something VB6 was not always good at. make their functionality more accessible through simpler syntax. most others are simply imagined. SlideShare. a programming language feature may have to be implemented under VB. or.NET (VB.NET Beyond the Scope of Visual Basic 6. Many of the “major” differences bemoaned by many VB6 purists no longer exist.NET Applications Far Beyond the Scope of Visual Basic 6.NET developers still approach problems the same way as they may have faced them under VB6.NET cannot support them in a like manner. in most cases. but may by necessity have to utilize non-VB6-style invocation rules.Enhancing Visual Basic .com/file/d/0B_Dj_dKazINlMlA5UUx6R2JLOG8/view?usp=sharing Page –652– . engendered by nothing more than unapprised conjecture. at first glance. having existed only in Beta releases of VB. in more complex cases. plainly because VB. Source code is available in a PDF document. you can easily emulate “lost” VB6 commands. overall. Code Excerpts for Enhancing Visual Basic .NET). or had been trained by VB6-savy developers. but. VB. such as will be demonstrated throughout this document. Regardless. View and download this PDF document for free at: https://docs. and through web searches. in but different forms. and customized to your needs so that you can really make your applications stand out from the rest of the pack. Hence. slideshare. 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.0 – David Ross Goben Doom 3 Walkthrough and Strategy Guide (512 pages). 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.com: Scribd: http://www. secrets and caches not mentioned in any other guide. 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.NET Beyond the Scope of Visual Basic 6. through sometimes pure luck or random fortune. offering two fast and very easy solutions that have worked on every system they have ever been tried.scribd.net/DavidRossGoben/doom-3-walkthrough-and-strategy-guide Page –653– . This walkthrough takes you through the Doom 3 adventure at the Veteran Difficultly level. perhaps one of the scariest and most densely detailed first-person-shooter escapades ever conceived. and how to easily play Doom 3 BFG Edition on otherwise uncooperative Windows 8. but with quick thinking and actual military techniques.com/file/d/0B_Dj_dKazINlY3pJTXVjd1FWWlE/edit?pli=1 Also available for free through web searches. weaknesses.1 systems (not an issue under Windows 10). strengths. a dense listing of useful console command codes. but did you know that it is actually stupidly easy to defeat them pain-free. two heads. and custom modifications are covered. and on Scribd.com and SlideShare. add and/or alter custom keyboard commands and toggles. and panther-like ferocity) at the end of the Alpha Labs – Sector 1 level because they always seem to come to great harm unless.google. For example. adversaries.Enhancing Visual Basic . many players dread facing off with the six maggots (tall man-like demons with razor-sharp talons.com/doc/263771244/Doom-3-Walkthrough-and-Strategy-Guide SlideShare: http://www. for both the original offering from Id Software and their later Doom 3 BFG Edition. and on top of that you can do it in perfect safety and at your leisure? Full descriptions of items. locations. View and download this PDF document for free at: https://docs. such as how to play at your monitor’s maximum resolution. all without cheat codes. This is an enhanced novelized exploration into the dark horrors of the 2004 Doom 3 adventure. The reason we think non-precious metals like gold and silver valuable is spiritual. but are in fact twice the value of a man.0 – David Ross Goben Also Available From the Author A Gnostic Cycle: Exploring the Origin of Christianity (712 pages).aspx?bookid=33204 (ph: 1-888-519-5121) http://www. and are separate to this day in the Hebrew Bible. rules governing the Qumrân Nazarite (pronounced “Nazareth”) Order. not 110. the heroes and villains are not always who they seem to be. Mary Magdalene was the first Pope of the Church Jesus personally established. The Eucharist is an ancient ceremony. yet his sway is more powerful today. Jesus married Mary Magdalene. invented religious and political dogma. rampaging armies.authorhouse. digging deep beneath the thick layers of misunderstood traditions. Judas Iscariot was hung on a tree (ancient slang for a crucifix). by the Dead Sea. because it was believed he was his Second Coming. not of Nun.NET Beyond the Scope of Visual Basic 6. The Apostle John Mark was actually Mary Magdalene. spiritual salvation. not from a rope. This book is available from your favorite book seller. not to record concise history. • Moses was based upon Pharaoh Akhenaten (Amenhotep IV). from the Hebrew Bible.uk/Bookstore/ItemDetail. • Joshua (Ye-ho-shua) was based upon Pharaoh Tutankhamun (Amenhotep V). Women are not half the value. glorious victory..50USD or for £13. Christianity and Judaism are polytheists. The Jerusalem Church was actually located in Qumrân. The Father in Heaven (Hebrew Adon) and Yahveh are two competing deities. from ancient Mesopotamia. Lazarus raised Jesus from the grave after his crucifixion.authorhouse. the Mother Goddess.Enhancing Visual Basic . Like any really good whodone-it.90UK: http://www. hateful murder. • King Solomon was based upon Pharaoh Amenhotep III. you are going to explore the bare-boned facts behind a broad range of Biblical mysteries.co. • Jesus was a dynastic king. world-changing events in history that had been willfully suppressed or misrepresented. or directly from Authorhouse or Authorhouse UK Ltd. The Antichrist was born 8 years after Jesus. Listed below is a short list of the hundreds of things you will herein unearth: • • • • • • • • • • • • • • • • • • The Hebrew Bible was written to build self-esteem. semantic misconceptions. Simon Zealot was of the Magi. • King David was based upon Pharaoh Thutmosis III. The Holy Spirit is the Jewish Shekinah (Presence of God). and their offspring live today. the Beloved Disciple. True royal blood has different genetic markers than the common population. worshipping to this day multiple Gods. and breath-taking. • Jesus was named for Joshua. and lived only until he was 19. In this book. you will read tales of sinister deception. • Joshua was the son of Moses. a direct descendant of the Scythian Anointed Kings.com/Bookstore/ItemDetail. and was one of the most brilliant men of all time.aspx?bookid=33204 (ph: 0800-1974150) Page –654– . The Apostolic Church has tried for centuries to exterminate Jesus’ Family Line. using bread mixed with white powder gold. a Royal Princess. Original Christian doctrine comes from Chapter 6 of Numbers. as you would anticipate in such mysteries. for either $17. wholesale genocide. intimate love. and.
Copyright © 2024 DOKUMEN.SITE Inc.