Bruce McKinney's Hardcore Visual Basic

 

 

 

 

 

 

Hardcore Visual Basic (cont'd)

Bug Fixes, Corrections, and Additions
The update part of this document has a little bit of everything--corrections of typos, irrelevant rants on obscure subjects, bug fixes in code, stuff I forgot to put in the book, apologies for misunderstandings, tributes to hardcore programmers, inside information and misinformation. Some of it you may want to mark in the paper copy of your book. Some of it you'll just read and shake your head.

The update has to accommodate readers of the online versions who have no page numbers and readers of foreign translations who have different page numbers. Therefore I use section titles in addition to page numbers when I reference the book.

A few of the minor wording changes mentioned in the update are also being made in the latest printing of the book, which will be available sometime after this update is published. Future readers may see errors described here that do not actually occur in their copy of the book. These differences won't be significant, since the only mistakes that could be corrected in the book were minor wording errors that did not change page count.

How to get the sample programs
I've provided the sample programs on the Web site as separate zip files. I've broken these programs up into several different files so that you don't have to download everything at once:

File

Description

HardCore3.zip

Source for sample program and components plus compare files, image files, and the Windows API Type Library--everything you need to build the VB components and samples.

HardCore35.zip

Same as above, but for VB5.

WinTlb3.zip

Source files for the Windows API Type Library--download only if you're interested in Interface Description Language (IDL).

CppForVB.zip

Some C++ articles included on the book CD--download only if you're interested in C++ for Visual Basic.

WinTlbU.zip

Unicode version of the Windows API Type Library--slightly more efficient for programs that will run only on Windows NT.

ComponentD.zip

Debug versions of the built components.

ComponentR.zip

Release versions of the built components.

Exes.zip

Sample programs as EXE files.

ComponentD5.zip

Debug versions of the built VB5 components.

ComponentR5.zip

Release versions of the built VB5 components.

Exes5.zip

Sample programs as VB5 EXE files.

The Component and Exe files are for your convenience only. You could build the same files from the source. The only required file is HardCore3.zip or HardCore35.zip.

Installation for VB6

Here is my recommended method of installation:

  1. Create a directory called Hardcore3 on your C drive. Actually, you can probably call it anything you want and put it on any drive you want, but this is the location I used. For owners of the second edition, I don't recommend installing over the original Hardcore2 directory, although I haven't tried it.
  2. Download HardCore3.zip and any of the optional zip files to the directory created in step 1.
  3. Unzip the files with the options that support creating files in their original directories. The options may vary depending on what zip tool you use. For example, if you're using a recent version of PkZip, the command line is pkzip -ext -dir Hardcore3.zip. If you don't unzip into the correct directories, nothing will work. Also, use a recent zip tool that understands long file names. If you unzip to short file names, nothing will work.
  4. Run the RegisterForVB batch file. This file registers the Windows API type library and copies the Cards32.dll and PSAPI.dll files used by some programs to the appropriate locations.

At this point you can use any of the VBG files for sample programs. You cannot use most of the sample VBP files because they reference components that haven't been registered. It's a good idea to get all the component files so that you can run any sample EXE or use any sample VBP. You can get the components in three ways:

  1. Download and unzip a component file (ComponentD.zip or ComponentR.zip). Again, make sure you unzip from the Hardcore3 root directory using options to create the correct directories. Run the ToDebug.bat or ToRelease.bat files to register all components. I recommend ToDebug initially. You can run ToRelease when preparing programs that use my components for distribution. These batch files (and UnregisterAll.bat) assume that RegSvr32.exe is in your path. If it isn't, you can either move it to a path directory or edit the batch files to point to the correct location. (By the way, I apologize for the original ToDebug and ToRelease, which assumed RegSvr32.exe was in a directory that existed only on my hard disk, and also ignored OCX files despite a message to the contrary.)
  2. Run the batch file BuildAll.bat. This will build either the Debug or Release components and executables. Pass d or r to the batch file to specify Debug or Release. If you didn't install Visual Basic in the default location, you'll have to specify the location in a VBEXE environment variable before you can build the files. The batch file identifies syntax problems and gives instructions if you get the syntax wrong. You can give a second argument to the batch file to build a group of files (components, controls, or clients) or to build an individual program.
  3. You can build each component project separately from its VBP file or from the VBG of a program that uses it. Most of the programs only require VBCore.dll and SubTimer.dll. Eventually you'll want to build VisualCore.dll, Notify.exe, and the controls--ColorPicker, DropStack, Editor, ListBoxPlus, and PictureGlass. The SieveCli program uses additional controls and components that have no practical use outside the program.

Installation for VB5

Some readers may not choose to update to VB6 since it is a minor release that offers as many problems as benefits for the hardcore programmer. All my programs are created with and tested under VB6, but it is possible to make everything work under VB5. There are several differences that must be fixed:

  1. VBP files created with VB6 have some statements that aren't recognized by VB5.
  2. CLS files have some properties that aren't recognized by VB5.
  3. I have used a few functions that take advantage of VB6 features.
  4. VBP and FRM files from VB6 use the new common control files that weren't available in VB5.

Most of these difficulties can be removed programmatically by running the VB6ToVB5.EXE program. That's how I created the files in Hardcore35.zip. Originally I was going to provide only one version of the source files and tell VB5 users to run VB6ToVB5.EXE to convert them. Ultimately, this strategy fell apart. There were too many minor incompatibilities. Instead I used the conversion program to create the files myself, fixed various build problems, and put the files in separate zip files. The code in these two zip files is supposed to be identical (and I think it really is). The references, objects, and attributes in the VBP files, and the objects in the FRM files are the only differences.

Visual Basic should have a predefined compile-time constant for the version number like civilized development environments. It doesn't, so I added one. All my VBP files define iVBVer = x. You can see the entry in the Make tab of the Project Properties dialog for any of my projects. The x is 6 in the originals, but the VB6ToVB5 utility changes the version number to 5. My components have conditional statements that disable VB6-specific code. I also have a module, VB6Funcs.bas, containing implementation of VB6 functions for VB5.

Once you download the correct VB5 files, the installation procedure is the same as for VB6.

Directory organization
Keith Pleas, my would-be co-author, was horrified at the original organization of the directories from the second edition. When I looked at the 386 files in the original Hardcore2 directory, I couldn't argue. Yes, that's a lot of unrelated files to cram in one directory. I admit it was rude. I guess I had so many other things to worry about that I didn't stop to think how intimidating this organization might look to users.

I've tried to improve the way I organized the directories for this release. The new organization may not be enough change to satisfy Keith, but it is an improvement. I want to find a compromise between books that have every program in a separate directory and those (like my second edition) that throw everything together in one big mess. Here's the new organization:

Hardcore3

Sample programs

Components

Source for components

Controls

Source for controls

LocalModule

General purpose private classes and standard modules

Compare

Compatibility compare files

Release

Release versions of components and controls

Debug

Debug versions of components and controls

Exes

Executable sample programs

TlbSource

Source for the Windows API Type Library

CPP

C++ articles

Cpp4VB

Four articles in HTML format on writing C++ DLLs

Sieve

First VBPJ article on writing ATL servers

ShortCutSvr

Second VBPJ article on writing ATL servers

This new organization still leaves a lot of files in the root directory, but at least they're all for programs you can run.

It's not my fault
I don't suppose you want to hear my
excuses if the installation procedure described above fails on your machine. But here they are anyway. I'm trying to be compatible with three operating systems--Windows 95, Windows 98, and Windows NT. Each of these systems may have different combinations of service packs or fixes contributed by different versions of Internet Explorer. In addition I want to be compatible with VB5 and VB6, knowing that VB5 has several service pack fixes and suspecting from the instability of VB6 that it will soon have a service pack or update too. Furthermore, I'm trying to juggle builds from the IDE with VBGs or VBPs and builds from batch files with VBPs even though VB's build mechanism is unstable. Finally, since I decided not to write a Setup program in order to "save time," I have to do everything from the wretched DOS batch language.

Don't be too shocked if some of my projects don't build correctly on the first try. They worked on my machine, and they'll work on yours eventually if you tinker enough with the project files.

Chapter 1

Page 3. Near the end of the "While/Wend" section I claim that the Exit Loop statement allows you to escape a loop cleanly. Ha! Fooled you, didn't I? There is no Exit Loop statement, only an Exit Do. But I didn't fool Yong Wang, who was the first to notice my error.

Page 5. The "Rem" section complains about the stupid, but harmless Rem comment syntax that nobody (well, hardly anybody) uses anymore. But the real problem with Visual Basic comments is the lack of a block comment syntax. In the old language wars, some languages had block comments (C and Pascal) while others had line comments (Basic and FORTRAN). Well, the opposing languages figured out that both forms are useful and adopted line comments to go along with their block comments. Visual Basic keeps upholding the faith, unaware that the other side has moved on. Instead of adding a multi-line block comment syntax (perhaps Comment/End Comment), Visual Basic gives us toolbar buttons that comment and uncomment selected text.

I would have killed for those buttons in VB3, but they're the wrong idea now--although they fit the general pattern of adding wizards instead of fixing the language. If I want to comment out code, I put it in an #If 0 Then/End If block. Unfortunately, VB won't let you put plain text in a false conditional. It checks the syntax of statements even when it knows they won't be compiled. We need a block comment syntax for function headers and other blocks of descriptive comment text. Maybe then the Visual Basic wizards would use the new feature instead of writing single line comments that stretch farther east than even the widest monitor could show.

Page 16. I'd like to add one more fruitless plea to the flame against those of you using the Form1/Command1 variable naming convention (not to mention the Project1/Module1 file naming convention). Every time I try to unzip one of your demo files into my test directory, your Form1 overwrites the Form1 of the last thoughtless correspondent. I have to live with your style if I voluntarily downloaded your sample from a Web site, but my patience wears thin if you sent me this sample in hopes that I'll ignore your rudeness and help you fix a bug. Rule one when asking help from authors: don't violate that person's publicly stated pet peeves.

Never mind. The Command1 fight is a hopeless battle. So is my futile attempt to promote a sensible Hungarian for Visual Basic (see "Basic Hungarian" on the same page). I see a lot of code with different naming conventions, but I don't think I have ever received code from a reader who followed the Basic Hungarian proposed in my book. Instead, the sloppy perversion of Hungarian in the Visual Basic manuals seems to be unstoppable. Well, there's something to be said for standardization. Perhaps a bad standard is better than no standard at all.

Page 23. The dialog between a Delphite and a VBer in the "Efficient Code" section turned out to be prophetic. The publication of my article in Delphi Informant magazine (April, 1998) sparked a mini language war with approximately the same level of sophistication shown in my imaginary debate. Some Delphites posted a quote from the article (out of context) bragging that the "Author of Hardcore Visual Basic goes to Delphi."

Well, rumors of my defection to Delphi are greatly exaggerated. Here's a real quote from the article:

"There's no clear winner when you compare Automation and other COM tasks in VB and Delphi. VB makes it easier to create and use simple Automation objects. It does this by hiding the irrelevant details. But once you move to more complex Automation problems, you may find that those details aren't as irrelevant as they first seemed; you may want to see and control them. If you're clever enough, you can usually hack your way around VB limitations, but in Delphi you can usually solve the problems without arcane hacks."

That may not be entirely complimentary to Visual Basic, but it's a lot tamer than many of the criticisms in my book. Whatever your terms of comparison, I think Visual Basic programmers can be thankful for the existence of Delphi. We can even wish that Delphi were more of a force in the marketplace, so that Visual Basic designers would have to compete more seriously in the areas where Delphi has a technological lead. Delphi 4 is going up against VB6, and in the area of language enhancements, VB will be the loser by a wide margin--although I doubt the difference will show up as even the tiniest blip on a graph showing comparative sales volumes of the two products.

Delphi has one important advantage over VB. It's written in Delphi. VB isn't written in VB. In fact, some of the C++ developers who create it don't even know Basic. Back when I was a Microsoft employee, I remember explaining Basic syntax to a VB developer who was trying to fix a bug I had reported. That's not so unusual. I used to be a developer of FORTRAN even though I didn't know any FORTRAN when I started the job and was only marginally proficient when I finished. That's not the case with Delphi developers. If they want to add some feature to their IDE, they first add that feature to the language library. To borrow a phrase from developers of Microsoft Visual C++ (who do write C++ in C++), they "eat their own dog food." Remember when VB5 shipped with IE-style flat toolbars in the IDE but no way to create them in Visual Basic? You won't see that in Delphi.

For example, Delphi 4 supports the wheel introduced on the Microsoft Mouse several years ago. Visual Basic might get around to supporting it in the VB7 IDE and perhaps a MouseWheel event will appear in the language in VB8. Similarly, multiple-monitor support is touted as an important feature of Windows 98 and Windows 2000 (formerly Windows NT 5.0). Does Visual Basic support it with an array of Screen objects? No, but Delphi 4 has a language feature that enables it. The list goes on. Visual Basic still has the most limited runtime library of any major language. The language itself still appears to be based on Windows 3.1, ignoring most 32-bit operating system features not directly related to databases or Internet.

None of this changes my mind about Delphi. I admire many parts of it, but I still think the overall model is crude and old-fashioned. I prefer the VB programming model--which is why I feel so bitter that Visual Basic hasn't lived up to its potential. Frankly, I won't be using either of these languages for my future development work. I've looked at Eiffel and Java, but haven't made a final decision.

By the way, after my Delphi article was published, a Delphi program manager sent me mail asking if I would be interested in writing a white paper for Inprise (Borland at the time) comparing the two languages. My response: "Trust me. You don't really want to pay me to write a Delphi-VB competitive white paper. I have a reputation for biting the hand that feeds me." Visual Basic program managers might be disappointed that I didn't go over to Delphi and harass them for a while. Perhaps when designers of other languages such as Java and Eiffel hear that I'm looking for a new language, they'll be pointing at each other saying, "choose them."

Page 26. "Everybody wants efficient code, and starting with Visual Basic 5 all you had to do to get it was click a few buttons to turn on native code with maximum optimization." If you believe that myth, perhaps you also believe in the code fairy who helps all good little programmers ship their products on time as long as they use Option Explicit and put a comment in front of every statement. The last paragraph of "Compiled Code--Not a Panacea" states that the next version of VB had better have a full-featured compiled language with no major compromises. Alas, VB6 doesn't have even obvious fixes to compiler limitations.

One of the most obnoxious limitations is that the Advanced Optimizations are on a dialog that applies to the whole project even though most of these optimizations should be applied at module or even procedure level. They come from Microsoft's C++ compiler, which applies them at a module level. The only reason Visual Basic can't do the same is because no one wrote a user interface to enable it. This might have been an acceptable compromise for the first version with a compiler, but there's no excuse in the second version.

For example, consider my VBCore component and the Remove Array Bounds Checks optimization. This component has about 100 modules and most of them have no need to check array bounds. But the Vector classes described in "Vectors As Resizable Arrays," Chapter 4, page 179, specifically make use of error trapping to identify and handle out-of-bounds array accesses. I can't apply this optimization to those classes, and therefore I can't apply it to the many other modules where it would give a surprisingly large speed improvement. Several of the advanced optimizations specifically disable important features of Visual Basic. You never want to apply these optimizations indiscriminately to everything in a large project. But that's still the only way Visual Basic lets you apply them.

Of course it's not really Visual Basic that does the optimization. It's C2.EXE, the second pass of the C++ compiler. VB invokes this program with different options to produce different optimizations. You can see exactly how this process works and even change it using a technique I first saw described in Advanced Visual Basic 5, the Mandelbrot Set, 1997. The author, Peter Morris, renames the real C2.EXE to a different name and replaces it with his own C2.EXE, which writes all the command lines it gets from VB to a log file and then passes the same command line on to the real renamed C2.EXE. By studying the log file, you can see exactly what VB is doing when you set different optimizations. It turns out that VB generates a temporary intermediate file (probably the same kind generated by the first pass C1.EXE of the C++ compiler) and passes the name of that file to C2 for compilation.

Hardcore programmer Donald Moore took that process a step further. He inserted his own options in the command line. His C2 added the /FAcs option to generate an assembly language source file for each compilation. This technique is extremely cool for an old assembly hacker like myself. I have a jerry-rigged system that allows me to generate and examine assembly language files any time I need to know what VB is really doing. I didn't polish this system to the point where I feel comfortable passing it along with this update, but you probably want something different anyway.

It wouldn't take too much effort to create a real option system that bypasses VB's half-baked options dialog and allows you to apply optimizations at the module level. First you need an add-in with a dialog that allows you to select any module in the project and apply any optimization you want. This is a relatively simple user interface problem. This add-in should write its results to a file or the registry-someplace where the fake C2 can find them. The fake C2 must then parse the command line generated by VB and replace it with a doctored command line that gives the optimizations requested for the current module. This should be an interesting little weekend project for some hardcore programmer.

Page 29. That last bulleted item in "What You Don't Know Hurts You" asserts that "anything you do to time your code will itself take time, thus changing your results." I stand corrected. The Heisenburg Uncertainty Principle doesn't apply here except in its most philosophical interpretation. If you use my ProfileStart and ProfileStop procedures, you'll see no noticeable loss of accuracy due the act of measuring. This case is unfortunate because I really liked the second and third sentences of this item. Too bad they aren't true.

Page 29, 30. "Examining Code" describes one way of examining p-code. Although the suggested method works after a fashion, it's rude and inconvenient. The book suggests putting a DebugBreak statement just before the code you want to examine, and then running the program in the Visual Basic IDE. When you hit the breakpoint, you'll see a prompt that allows you to break into the debugger that has been assigned as the system debugger. One problem with this technique is that the messages you see on your way into the debugger can easily give the impression that your program crashed, although it hasn't. Also, it's likely you'll never come back to the Visual Basic IDE. In my experience, it usually terminates when you unload the system debugger.

A much better way to debug a program is to compile it to p-code and then load the resulting EXE directly into a debugger. For example, you could enter a command like the following from a DOS command line or from the Run menu command on the Start menu:

"C:\Obnoxiously long path with spaces\msdev.exe" myprog.exe

If you're using Microsoft Visual C++ as your debugger, you can also start the C++ IDE and specify your program in the executable for the debug session in the Debug tab of the Project settings dialog. The commands will vary slightly if you use a different debugger.

Page 31. The program described in "The Time It Application" has some new tests and fixes for old tests.

Page 33-34. "Short-Circuiting Logical Expression: A Timing Example" explains why Visual Basic can't short circuit logical expressions with the And and Or operators. But there's an easy fix for this problem that made its way into a Visual Basic spec several years ago, and then failed to make it into the product. All you need are new operators that do logical rather than bitwise evaluation. For example, the C++ language has & and | respectively as its bitwise AND and OR operators; it has && and | | as its logical AND and OR operators.

A logical operator bases its result on whether the entire value of an expression is True or False rather than on the combination of bits. This kind of operator can be safely short-circuited. If I remember correctly, the original proposal was to add new Visual Basic operators called AAnd and OOr as logical operators. I'm not too fond of those names, but I do like the concept and the names don't matter.

While they're adding operators, the Visual Basic designers ought to add shift operators (Shl and Shr). Surely these operators would be more useful than VB's obscure Eqv and Imp operators, which I have never seen used in a real program.

Page 34. All the timing numbers in Performance sidebars should be taken with a grain of salt. The measurements were done with VB5 on a 90 MHz Pentium, which would now be obsolete in many shops. On the other hand, I don't think there's anything in VB6 that would significantly change the relative percentages.

Page 40-42. In "Other Debug Messages," I say that I don't implement event logging in my custom assert and message system in the Debug module. Well, now I do. I could write a better and more reliable logging system than VB's except for one problem--the Visual Basic library provides no means of committing file writes. Most runtime libraries have a commit function that forces a write of any data cached in buffers. Visual Basic has zip. You used to be able to fake it by getting the file handle from FileAttr and passing it to the Windows FlushFileBuffers function, but the designers broke that late in the VB4 beta cycle (breaking some of my code in the process). Since the VB file I/O system is written in C++, I'm sure it would take somebody on the library team at least five minutes to write a VB wrapper for the C++ _commit function.

In the meantime, if you're debugging a crashing program (the kind you most need to debug with a log system), you should flush all your log writes as soon as you make them. Otherwise, if you crash, handles won't be closed, buffers won't be flushed, and your data won't be written to the files. But there is no way to flush the buffers, and that's where the VB log system comes in. The system is not very good, but sometimes it's all you have (even though you only have it in compiled EXE programs).

So I enhanced BugMessage so that it would initialize logging on first call and write messages to either the NT event log or to a file, depending on the value assigned to the afDebug constant in the Conditional Compilation field of the Make tab in the Project Properties dialog. I'm not going to show the ugly enhancements to BugMessage here, but I will tell the values you need to Or into the afDebug constant:

afLogfile = 1

 

Value

Description

&H1

Write to my log file (appname.dbg)

&H2

Write to message boxes

&H4

Write to the debug window

&H8

Write to their log file (appname.log)

&H10

Write to the NT event log

&H18

Write to event log on WinNT or to log file on Win95

I used BugMessage with the NT event log to debug very messy problems in the Notify server as described in notes to Chapter 11.

Page 31-44. The debug and profile system described in the "Timing in Basic" and "Assert Yourself" section has a risk that I can't really explain, but that I must warn you about. When Paul Thomas reported this problem, I have to admit I thought he might be a little crazy or at the least confused. But when I got the same error in similar circumstances, I began to suspect that VB was the one suffering delusions.

Paul reported that when he ran the Notify server (Chapter 11, page 640) for a long time (about 20 minutes), he got an error claiming that an imprecise value had been assigned to a floating point variable. This error is a little weird, considering that Notify doesn't use any floating point variables. In fact, the closest it comes to a floating point variable is a fixed-point Currency variable called secFrequency in the Debug.bas module. This variable is used by the profiling procedures, none of which are used in the Notify server code. The Notify server did, however, use the BugAssert procedure, and thus had to include Debug.bas in the project.

Paul found the one currency value in the project and changed its type from Currency to Double. The bug disappeared. He sent me mail describing the problem. I replied that his eyes were deceiving him and that he could not have gotten that message. If he wanted to be sure there were no floating- or fixed-point variables in the program, he could remove the BugAssert statements from the Notify code and remove Debug.bas from the project. Paul replied that with the type changed from Currency to Double, he now got the error after running Notify for four to five hours. He then removed Debug.bas and all the BugAsserts and the bug went away completely--even after runs of 17 hours.

Well, this is not the kind of bug any programmer likes to think about. What was I going to do about it? Run Notify for four hours until the bug appeared for me? What would the error message tell me that I hadn't already heard and disbelieved? So I did what any self-respecting programmer would do. I forgot all about it. Or at least I tried. Then several months later I left the VB6 version of Notify running overnight for a completely different reason. When I returned to my computer, there it was--the floating-point error I had doubted.

I recently made some minor changes to Debug.bas, and now the bug seems to have gone away. I ran Notify overnight without any bad effects. I can't say I fixed the bug because I don't understand what caused it.

I don't know the moral to this story. Perhaps the error message was a judgment on me for the filthy hack to fake 64-bit integers with Currency as described in "Large Integers and Currency," Chapter 2, page 62. Perhaps it's just a general punishment for questioning the design of Visual Basic. Perhaps it only happens to Aquarians on days divisible by nine that fall on Thursday. All I can suggest is, if you get it, remove any calls to my debugging procedures and delete Debug.bas.

Page 39. Now it can be told. The sidebar "Conditional Compilation for Blockheads" was censored. The original title was "Conditional Compilation for Dummies," but my editor feared this title might violate some sort of copyright or trademark rights of IDG Books Worldwide, publisher of the "Dummies" series. I don't think so. And even if it did, IDG would be more likely to thank me than prosecute me.

Chapter 2

Page 46. "Never Write Another Damn Declare Statement" praises type libraries, but based on mail from readers, some of you aren't buying it. Apparently some programmers just like writing Declare statements. Maybe you're a control freak who has to write your own, and since I don't explain how to write type libraries, you're stuck with Declare statements. Maybe you're a double hardcore programmer who skips all the wimpy stuff in the first few chapters and goes directly the hardest techniques you can find in the final chapter, and then sends me mail when you can't figure out where I hid the Declare statements. Maybe you don't believe that type library entries add no overhead to your programs except for the entries you use. Or perhaps you believe, despite assurances to the contrary, that you have to ship the type library to clients. For whatever reason, many of you insist on using Declares, although no one has ever given me a valid reason other than the incompleteness of my type library.

Well, that's okay with me. Use Declare statements if you like for whatever reasons you have, but don't ask me to write them for you. Almost all my samples depend on the Windows API Type Library, and if you'd rather use my code with Declare statements, you'll have to write them yourself. And don't complain to me if the weird declare choices in Win32API.txt don't match the ones in my type library.

By the way, the TimeIt application has a test that proves that type library entries are faster than equivalent Declare statements. This is not, however, a reason not to use Declares. I had to call GetVersion 300,000 times to get a measurable difference. That kind of performance enhancement isn't likely to matter in a real application.

One more bit of type library trivia: I recently discovered that the Windows API Type Library from the second edition is distributed as part of Windows 98. It's not installed by default, but if you check the disc you'll find it as part of the Microsoft Windows 98 Resource Kit Sampler. If you install the sampler, it will be copied to your system directory. As far as I can tell, the documentation provided does not give any clue of what the library is or what it can do. Ordinary Windows 98 users aren't likely to discover the library. Why and how it got there are beyond me. I did send the complete library with source over to the Visual Basic department before I left Microsoft. They could have updated it and provided it with VB6, but apparently they turned it loose to wander the halls until it landed on the desk of someone preparing the resource kit.

Page 50. Visual Basic now comes with a new and improved version of the WIN32API.TXT file described in "Rolling Your Own." Alas, many of the bugs and weird design decisions are unchanged. For example, WriteProfileString has been updated with a sensible style, but WritePrivateProfileString still has a VB3-style declaration. I recommend that you never use anything from this file without inspecting it carefully to make sure it makes sense.

Page 62. The technique described in "Large Integers and Currency" is somewhat risky, as described earlier. This whole stupid hack should be unnecessary. Visual Basic should have added a 64-bit integer type by now. Visual Basic is built with Microsoft's C++ compiler, which has a native 64-bit integer type. It should be easy to map this internal type to a new type called VeryLong or whatever. As 64-bit integers become more important for database work and for measuring disk sizes that now commonly exceed four gigabytes, most languages (including Delphi 4) already have this type. Visual Basic should join them.

Page 63. The text in "Large Integers and Currency" speaks of a type library constant called CURRENCY-MULTIPLIER. No such constant exists. The correct name is CURRENCY_MULTIPLIER (with an underscore rather than a hyphen).

Page 70. The note in "Typeless Variables" refers you to page 240 for a sidebar called "Better Basic Through Subclassing." The sidebar doesn't actually start on page 240 and I'm not going to tell you what page it does start on because I now regret writing it. Renaming built-in functions with your own versions is so questionable that I suggest you pretend this note and that sidebar don't exist.

Page 71. A cross-reference in the "Typeless Variables" section refers you to Chapter 6 for more information about bitmap handles. The information is actually in Chapter 7.

Page 77. The last paragraph of the "Null and Empty" sidebar claims that you can save space in your EXE program by using string constants rather than literal strings. I tested this claim when I wrote it, but I think that was back in the VB4 era. When I did a similar test with VB6, I could not detect any difference. One reader actually reported a size increase from using string constants on a very large program. There may be good reasons to use constants rather than strings in code, but EXE size is apparently no longer one of them. A smart compiler should be able to detect and consolidate repeated string literals, and apparently VB now does this.

Page 78. The "Wrapping String Functions" section says that optional arguments initialized to vbNullString don't work. The example shown is:

Optional Class As String = vbNullString

Fortunately this VB5 bug has been fixed in VB6.

Pages 78, 79. The VBGetWindowText example in "Wrap It Up" illustrates a bad coding habit. The error also occurs earlier in a code fragment in "Getting Strings from the Windows API." The first problem with VBGetWindowText is that the function was renamed after I wrote this section and now appears in the WinTool module as WindowTextFromWnd. But that's not the real problem.

I discovered this mistake in two stages. First hardcore programmer Mauro Puddinu reported an overflow in WindowTextFromWnd. When I checked the code, I immediately saw what I thought was the source of the problem:

Function WindowTextFromWnd(ByVal hWnd As Long) As String
    Dim c As Integer, s As String
    c = GetWindowTextLength(hWnd)
    If c <= 0 Then Exit Function
    s = String$(c, 0)
    c = GetWindowText(hWnd, s, c + 1)
    WindowTextFromWnd = s
End Function

What, I wondered, was that Integer doing there? It's almost always wrong to use Integer rather than Long when dealing with the Windows API. I'll go further. It's almost always wrong to use Integer in 32-bit programming. There are exceptions. Some parameters in VB's event procedures are Integer, so you should use matching Integer variables to store copies. You can also use Integer in UDTs to save storage space. But the default size of an integer (note the lowercase) in 32-bit operating systems is Long, and you should use that size unless you have a good reason to do otherwise. I suspect that in this case my only reason for using Integer was negligence. I just left the code unchanged from the first edition of the book. Fortunately I didn't repeat the mistake in the "Life as a string" figure on page 80.

I was so excited about finding this error that I didn't notice the other weird thing about this function. Why is it testing for negative lengths and exiting early when it finds them? I eventually found out the hard way. It seems that some versions of Windows sometimes return bogus numbers from the GetWindowsTextLength function. When this happens, these numbers tend to be very large.

Apparently when I first encountered the problem (back when winhexadesaurus roamed the earth), the number must have been greater than 33,767 and less than 65,667--which would wrap into a negative Integer. When Mauro got the error, it was greater than 65,535 and caused an Integer overflow. He may have been hitting the same window I encountered where the GetWindowsTextLength function claimed it had a string 285,212,672 characters long. That's one heck of a long string. In fact, it's so long that when you try to create a string of that length with the String$ function, Visual Basic gives up with an "out of string space" error.

Unfortunately, this error occurred when I was executing the recursive IterateChildWindows function in WinWatch. It can be very difficult to track down memory bugs in recursive functions. You can't just set a breakpoint, because you may have to call the function thousands of times before you hit the problem. At first I blamed VB6 for what I assumed was a memory change that would cause any deep recursion to fail. Fortunately, I encountered the bug again in a situation that was easier to debug (the TEnum.vbg project). Based on my experiments, Windows seems to give completely wrong results in these situations, so it's best to bail out rather than truncating the string. Here's the fixed version of the code:

Function WindowTextFromWnd(ByVal hWnd As Long) As String
    Dim c As Long, s As String
    c = GetWindowTextLength(hWnd)
    ' Some windows return huge length--ignore 0 <= c < 4096
    If c And &HFFFFF000 Then Exit Function
    s = String$(c, 0)
    c = GetWindowText(hWnd, s, c + 1)
    WindowTextFromWnd = s
End Function

I'm setting a relatively high maximum to accommodate windows that play tricks with the window text, using it as the entire contents of the window rather than just the caption.

Back to the original point: it's not a good idea to use Integer variables for variables that Windows will fill with Long values. Unfortunately, I found several Integer variables in WinTool and other modules when I searched the rest of VBCore. They're fixed now.

Page 83. The "Unicode Versus Basic" section has a bulleted list telling how various operating systems and programs handle Unicode. Time has passed and it is now necessary to add two more items to this list:

  • Windows 98. Windows 98 handles Unicode exactly the same as Windows 95, warts and all. Even the serious Unicode limitations of Windows 95 that I'll flame about in Appendix A are unchanged. The only difference is that a certain controversial program that used to stand on its own has become "part of the operating system," and that program, with all its DLLs, handles Unicode better than the rest of the system.
  • Microsoft Internet Explorer. IE was mostly developed by the same team that did the new Windows shell for Windows 95, which was later retrofitted to Windows NT 4.0. Somewhere along the line they must have learned Unicode, because much of the underlying functionality of IE (in both its standalone and Windows 98 incarnations) exposes both ANSI and Unicode entry points. Hardcore programmers who want to experiment with calling functions in IE DLLs (such as SHLWAPI.DLL) can take advantage of the more efficient Unicode entry points even under Windows 95 or 98. We'll be looking at SHLWAPI in detail in Appendix B.

By the way, the introduction of Windows 98 raises some vexing problems for writers. How do you refer to the two operating systems that are essentially the same for most programmers? It gets cumbersome to say that Windows 95 and Windows 98 behave one way, but Windows NT behaves another. I used to abbreviate these systems as Win95 and WinNT. Can I now say Win95/98? Or should I say Win9x? Is this terminology going to crash in the year 2000? Maybe it's more Y2K-compliant to say Winxx. I'm told that Windows 98 is the last of its line, and that all future versions of Windows will use the NT codebase. If that's true, let's hope that the dumb idea of naming operating systems after years will die and be buried in the same grave with the last fragments of MS-DOS legacy code.

(I wrote the preceeding paragraph before I learned that Microsoft has decided to rename their operating system in order to associate it in the public mind with the end of the world. At first I thought the name Windows 2000 must be a hoax perpetrated by the National Enquirer, but truth is stranger than fiction.)

Page 84. The first code fragment in the "What Is Unicode?" section can be improved. Try this code in the Immediate pane or in a program:

For i = 0 To 255
    Debug.Print i & vbTab & Chr$(i) & vbTab & Hex$(AscW(Chr$(i)))
Next

I claim that the results will show non-zero high bytes in characters 145-156 and character 159. Well, that is a common result, but I've gotten other results on different machines. Try it for yourself if you really want to know.

Page 87, 88. The "Unicode API Function" section shows you how to write functions that register a type library. These functions were used in the RegTlb program provided with my book. I don't want to mess with that code because it illustrates several alternative ways of handling Unicode strings in API functions. But if you look at RegTlb as a utility rather than a teaching tool, you'll find a much easier way to register type libraries, which I use in the new RegTlb program. The old code shown in the book is moved to a separate program called RegTlbOld. Study it as an example, but use the new RegTlb to register type libraries.

The new method uses the TypeLib Information component (TLBINF32.DLL) provided with Visual Basic. This component is the one that makes Visual Basic's object browser work, and that you can use to write your own object browsing tools--if you can figure out how it works without documentation. I mention it in Chapter 10, "What's next," page 588. I wanted to use it to create a class inheritance wizard, but unfortunately never found time to figure out the details. The one downside of using this DLL in your programs is that you must ship it to customers.

Fortunately, hardcore programmer Martin Naughton pointed out that you can easily use TLBINF32 to register and unregister type libraries. Unregistering type libraries is a messy business. You can do it with an API, but the API requires the GUID rather than the file name of the server to be unregistered, and you have to do messy hacking (which I never did) to get the GUID from the file name. I was relieved to find that TLBINF32 already handles it for you. This code registers type libraries:

Function RegTypeLib(sLib As String) As String
    Dim mgr As TLIApplication
    On Error GoTo FailRegTypeLib
    Set mgr = New TLIApplication
    mgr.TypeLibInfoFromFile(sLib).Register
    Exit Function
FailRegTypeLib:
    ' Pass error message back to caller
    RegTypeLib = Err.Description
End Function

Unregister is the same except that it uses the Unregister method.

Page 88. The RegTypeLib sample near the end of the "Unicode API Functions" section has a minor change. The tlb variable in the ordTypeLib conditional block now has type IVBTypeLib rather than ITypeLib. This is due to a change in the Windows API Type Library in order to be consistent with naming conventions described in Chapter 3.

Page 92. The description of how VarPtr works on strings in the last paragraph of the "Bring Your Hatchet" section is completely wrong. It states incorrectly that VarPtr returns a pointer to the temporary ANSI string created when calling API functions. In fact, VarPtr returns the address of a pointer to the string data (a BSTR). I stand corrected due to Francesco Balena's article "Play VB's Strings" in the April, 1998, issue of Visual Basic Programmer's Journal. I hereby award Balena the Joe Hacker Guru award for this article and for other hardcore discoveries he has publicized recently.

Once you understand what VarPtr really returns, you can come up with techniques to swap strings by swapping their addresses. The pointers and addresses in the following SwapStrings procedure were so confusing to me that I had to draw pictures in the comments to figure out what I was doing:

Sub SwapStrings(s1 As String, s2 As String)
    Dim pTmp As Long, pAddr1 As Long, pAddr2 As Long
    ' Save pointer to first string in variable
    pTmp = StrPtr(s1)
    ' 1000+---------+   <== VarPtr(s1) = 1000
    '     |  2000   | s1    StrPtr(s1) = 2000 ==>  "String 1"
    ' 1004+---------+   <== VarPtr(s2) = 1004
    '     |  3000   | s2    StrPtr(s2) = 3000 ==>  "String 2"
    ' 1008+---------+
    '     |  2000   | pTmp
    '     +---------+
 
    ' Copy the pointer at second address to first address
      (both ByVal)
    CopyMemory ByVal VarPtr(s1), ByVal VarPtr(s2), 4
    ' 1000+---------+
    '     |  3000   | s1
    ' 1004+---------+
    '     |  3000   | s2
    '     +---------+
 
    ' Copy pointer in pTmp (ByRef) to second address (ByVal)
    CopyMemory ByVal VarPtr(s2), pTmp, 4
    ' 1000+---------+
    '     |  3000   | s1
    ' 1004+---------+
    '     |  2000   | s2
    '     +---------+
 
End Sub

Page 94. In the section "Fixed-Length Strings in UDTs," the function BytesToStr is wrong. Although the function shown would do something, it wouldn't solve the problem of converting fixed-length UDT fields to strings. The BytesToStr function in the Bytes module actually looks different and performs a different task. Its real purpose (converting an ANSI string stored in byte array into a VB Unicode string) is discussed in notes on Chapter 5.

We need a different function for padded UDT byte array fields. The ByteZToStr function extracts the meaningful part of a null-terminated string stored in a fixed-length byte array. After an API call, this kind of field will generally contain a short string followed by lots of padding. File name fields, for example, have to be large enough to accommodate the largest possible string--260 characters--but you don't often see file names that long.

The FindFirst example shown in the text would normally look like this:

hFind = FindFirstFile("*.*", fnd)
Debug.Print ByteZToStr(fnd.cFileName)
 
The code for ByteZToStr looks like this: 
 
Function ByteZToStr(ab() As Byte) As String
    If UnicodeTypeLib Then
        ByteZToStr = ab
    Else
        ByteZToStr = StrConv(ab, vbUnicode)
    End If
    ByteZToStr = Left$(ByteZToStr, lstrlen(ByteZToStr))
End Function

The lstrlen function and the other string conversion issues are discussed in a later note.

Page 95. The technique described in "Variable-Length Strings in UDTs" works for the situations I had encountered, but hardcore programmer Brent Langdon described a situation that requires a different technique. My technique works correctly if you are passing strings into an API function that reads the value, or an API function that fills the buffer that you give it with a string. It does not work in cases where the API reassigns a string pointer.

Brent encountered this problem with the TOOLTIPTEXT structure passed to the TTN_NEEDTEXT message. These items have been superceded by the NMTTDISPINFO structure and the TTN_GETDISPINFO message in the latest COMMCTRL files. Since these aren't implemented in my type library, I'll show the structure as a UDT:

Type NMTTDISPIFNO 
    hdr As NMHDR
    lpszText As Long   ' Would be String using technique in book
    szText As String*80
    hinst As Long
    uFlags As Long
    lParam As Long 
End Type

In this case, you can leave the lpszText parameter blank when you pass the UDT to SendMessage with the TTN_GETDESPINFO message. Windows will assign a pointer to a string to the field. This technique wouldn't work with a String parameter because Windows wouldn't know how to assign a VB String. When the SendMessage call returns, you'll have a pointer to a string, but you'll have to handle it as a pointer. You can solve this problem using the PointerToString function, as described in "Strings in Callback Procedures," page 105.

Page 96. The last paragraph of the "Variable-Length Strings in UDTs" section says you can't define fields of type String in a type library. That means you can't replace LPSTR types with String as you can in UDTs defined in VB. I attempted to do this in the previous version of the Windows API Type Library by giving these fields the BSTR type. Since the VB String type is equivalent to the C++ and IDL BSTR type, this technique ought to work. Unfortunately it doesn't. Visual Basic treats BSTRs in MIDL-generated type libraries differently than Strings in UDTs, expanding the Unicode once too many times to create garbage strings. I don't know why. Perhaps some reader can explain the incompatibility, or perhaps figure out a way to work around it.

In the meantime, I have changed the definitions of LPSTR in these types from BSTR to PTR (an alias for Long). You can use the PointerToString and StringToPointer functions described later to deal with the strings as pointers. This method can get messy, and I prefer to write the appropriate types, declares, and constants in VB, as I did with the Common Dialog functions.

Page 98. The discussion introduced in the "Typeless Strings" section barely scratches the surface in its introduction to CopyMemory, StrPtr, and pointer arithmetic. Appendix B contains a more complete discussion and includes many facts I didn't know when I wrote Chapter 2.

Page 99. The second sentence in "The Ultimate Hack: Procedure Pointers" says that John Kemeny would be shocked if he saw the AddressOf operator. My exact words (which the book editors considered too tasteless for print) were that he would "roll over in his grave." Privately, I expressed the hope that Kemeny would do more than that. I want him to come back and administer punishment. I don't consider this little joke disrespectful to one of my programming heroes (see http://math.dartmouth.edu/history/TBasic).

Page 106. The last sentence of the "Strings in Callback Procedures" section defers discussion of resources until "later." The discussion is actually in Chapter 8.

Page 105, 106. The description of the PointerToString function in the "Strings in Callback Procedures" section is wrong. It says that the type library entry for lstrlen could be ANSI or Unicode, depending on whether you use Win.tlb or WinU.tlb. That description was true in the old version, which was based on a misunderstanding. Actually, lstrlen is one of the few functions that has both ANSI and Unicode versions in both Windows 95/98 and Windows NT. If both versions are available, you should use the Unicode version, since Visual Basic strings are always Unicode. This eliminates unnecessary ANSI to Unicode conversion.

In both current versions of the type library, the lstrlen function uses the Unicode version. I can't think of a reason to pass a String (actually a Unicode BSTR) to lstrlen. I can think of reasons to use the ANSI version of the lstrlenPtr alias, which takes a pointer variable. There are cases in which Windows might pass you a pointer to an ANSI string, and you'll have to convert it to a Visual Basic String. To cover all my bases, I now have multiple versions of String and Byte Array conversion functions:

Function

Description

lstrlen

Gets length of a Unicode String to first null character

lstrlenPtr

Gets length of ANSI or Unicode string pointer to first null character

lstrlenAPtr

Gets length of ANSI string pointer to first null character

lstrlenUPtr

Gets length of Unicode string pointer to first null character

lstrlenByte

Gets length of ANSI or Unicode byte array to first null character

lstrlenAByte

Gets length of ANSI byte array to first null character

lstrlenUByte

Gets length of Unicode byte array to first null character

LenZ

Gets length of String to first null character (VB wrapper for lstrlen)

StrZToStr

Returns String truncated at first null character

ByteZToStr

Returns ANSI or Unicode byte array truncated at first null character

AByteZToStr

Returns ANSI byte array truncated at first null character

PointerToString

Converts ANSI or Unicode string pointer to String

APointerToString

Converts ANSI string pointer to String

UPointerToString

Converts Unicode string pointer to String

You won't need to use most of these functions very often. Some of them never appear in my code. But I do frequently use StrZToStr, ByteZToStr, and PointerToString.

Here's the new code for PointerToString based on what I've learned since I wrote the original:

Function PointerToString(p As Long) As String
    If UnicodeTypeLib = 0 Then
        PointerToString = UPointerToString(p)
    Else
        PointerToString = APointerToString(p)
    End If
End Function
 
Function UPointerToString(p As Long) As String
    Dim c As Long
    ' Get length of Unicode string to first null
    c = lstrlenUPtr(p)
    ' Allocate a string of that length
    UPointerToString = String$(c, 0)
    ' Copy the pointer data to the string
    CopyMemory ByVal StrPtr(UPointerToString), ByVal p, c * 2
End Function
 
Function APointerToString(p As Long) As String
    Dim c As Long
    ' Get length of Unicode string to first null
    c = lstrlenAPtr(p)
    ' Allocate a string of that length
    APointerToString = String$(c, 0)
    ' Copy the pointer data to the string
    CopyMemoryLpToStr APointerToString, ByVal p, c
End Function

I hope these changes also fix a bug reported by Gwyshell (the English nickname of Guo Wen Yang). Gwyshell reported problems running the TFolder project on a Chinese-language version of Windows 95. His detailed report, complete with screen shots and hex dumps of Chinese text, helped me narrow the problem down to the ANSI version of lstrlen. It wasn't reporting the correct length for some double-byte character set (DBCS) Chinese strings. This problem is very difficult for me to test since I wouldn't know how to write Chinese text even if I were using a computer that was set up to enter and display it. Gwyshell was very helpful in testing the problems, but I'm still not very confident that all of my code works properly on DBCS text.

Chapter 3

Page 110-114. Visual Basic 6 raises my object-oriented rating in "The Three Pillars" from a measly 1.9 to an impressive 1.91. This great leap forward comes in the Encapsulation area. There are no new features in Reusability or Polymorphism. The rating for class encapsulation goes up from .8 to .81 in VB6 because you can now use Public UDTs--both as parameters and as return values. This is a nice feature--but one that most other object-oriented languages already had. Besides, the implementation is flawed, as we'll see later. You don't get many points for adding late what you should have added early.

I also considered adding points for the new ability to persist classes, but after studying the feature, I decided it's not a fundamental addition to the class model--nor is it that new. The Persistable property provides a standardized way to do something you could have easily done before by saving and restoring appropriate data in your own format in the Initialize and Terminate events. The new servername parameter of CreateObject is also a useful addition, but it improves the COM model rather than the class model.

Perhaps I'm judging harshly because I was hoping for inheritance, constructors, static members, and other significant object-oriented improvements in this release.

Page 130. The note at the end of the "Objects from CreateObject" section makes the mistake of discussing VBScript even though I have never used that language. Experts tell me I was blowing smoke. If you have the paper version of my book, please use whiteout to remove the note. If you're reading the book online, use virtual whiteout.

Page 132. In the "Dual Interfaces and IDispatch" section I say that I have yet to see a practical example of using IDispatch-based late binding in Visual Basic. This statement is still true, but only because I have specifically not looked for such examples. Some of my readers and other experts have found them. In fact, anybody who wants to use Visual Basic's new ability to create controls at run time will be using late binding. Late binding is a fine technique that some Visual Basic programmers have requested for a long time. I can only attribute my distaste for the idea to an early-bound case of v-table chauvinism.

Page 134. The "Dual Interfaces and IDispatch" section states that the only development tools that create dispatch-only ActiveX components are the Microsoft Foundation Class (MFC) and Delphi 2. While true at the time, this statement is no longer strictly accurate. Delphi 3 and 4 from Inprise (formerly Borland) create dual interface components by default. Microsoft provides the Active Template Library (ATL) as its most efficient way to create dual interface ActiveX components in C++. MFC still creates dispatch-only interfaces by default, but it is possible to use ATL dual interfaces in MFC projects.

Page 139-147. The Drive and Drives classes of the new FileSystemObject class partially duplicate the CDrive class described in "Implementing CDrive" and the CDrives class discussed in Chapter 4, "The CDrives class, version 2," page 205. You can compare the new classes to mine and judge for yourself. I will say that mine have better names. Fortunately the new drive and drives objects don't follow the idiotic naming precedent set by the FileSystemObject itself. If they did, they would have been called the DriveObject class and the DrivesObjects class so that you could talk about DriveObject objects and DrivesObjects objects in the same way you talk about FileSystemObject objects. Instead the designers repeat the mistake I warned about (Chapter 1, "Class prefixes," page 22) of choosing class names that use up the most obvious variable names (drive and drives). Of course this issue doesn't matter in their examples (from VBScript) which have descriptive variable names like d and dc. Their examples also use Variants for all variables, which forces unnecessary late binding.

Page 143-147. I can't say how shocked I am to see the Dialog Box From Hell (DBFH) changed, but unchanged, in version 6. Everything I criticized in the "Default members" section is still broken. Somebody actually went in and modified this dialog (adding the Update Immediate checkbox) without fixing any of its other problems. This atrocious design shows a lack of pride. I assured Visual Basic program managers many months before version 5 shipped that I would ridicule them in print if they didn't fix it. Most developers hate being made fun of in public, but these developers don't seem to mind--and not only by me. Jim Karabatsos, a writer for the Australian Visual Developers Forum (http://www.avdf.com/), ripped the DBFH with words so similar to mine that I incorrectly suspected him of plagiarism.

Apparently the VB designers think it is more important to add new features than to waste time fixing a feature that is stupid, but mostly harmless. I suppose in the larger scheme of things this dialog isn't such a big problem for Hardcore programmers. It doesn't stop us from doing what we have to do, and I'm usually the first to argue for substance over style. But I expect any part of the public interface of a public product to meet minimal standards of professionalism. Furthermore, the dialog turns out to be not as harmless as I supposed--see the earlier explanation of problems with my SubTimer component.

Page 147. Insert the following section called "UDTs in Classes" as a subsection in the "Other Class Features" section.

In prior versions of Visual Basic, classes and UDTs were kept in separate cages for fear of inappropriate contact. Merging the two in VB6 seemed like a good idea. Unfortunately, the results are disappointing. Let's look at an example.

Assume we have a public CFileData class that defines the following UDT:

Public Type TFileInfo
    Attribs As Long
    LastWrite As Date
    Creation As Date
    LastAccess As Date
    Length As Long
End Type

By defining the type in a public class, we make it available to any client of that class. Of course, defining the type isn't the same as defining a variable or a property of that type. That's a separate step. It might be nice to define a property of TFileInfo type using this syntax:

Public FileInfo As TFileInfo

Unfortunately, VB doesn't allow this syntax. You get an error saying that UDTs aren't allowed as Public members in object modules. This limitation isn't too surprising--the same limitation applies to arrays. You can get around the limitations with arrays by defining Property procedures, and you can use same workaround for UDTs. Here's the classic Get/Let pair:

Private fi As TFileInfo
 
Property Get FileInfo() As TFileInfo
    ' Validate output here
    FileInfo = fi
End Property
 
Property Let FileInfo(fiA As TFileInfo)
    ' Validate input here
    fi = fiA
End Property

Now how do clients access this property? Here's one scenario:

Dim fd As New CFileData
With fd
    ' Assign FileInfo members
    .FileInfo.Length = 22
    .FileInfo.Attribs = vbReadOnly
    .FileInfo.LastWrite = Now
    ' Use FileInfo members
    Debug.Print .FileInfo.LastWrite
End With

This syntax is perfectly legal. The statements execute without errors. There's only one problem: they don't work. The assignments have no effect whatsoever. Here's how you have to do it:

Dim fd As New CFileData, fi As TFileInfo
With fi
    ' Assign to a temporary UDT variable
    .Length = 22
    .Attribs = vbReadOnly
    .LastWrite = Now
End With
' Assign the variable to the property
fd.FileInfo = fi
' Now you can access fields of the property
Debug.Print fd.FileInfo.LastWrite

The first syntax seems logical and I wish it would work, but I can see why it might be difficult to implement. But if assignment of fields through properties isn't going to work, it doesn't seem like it should be legal. If you step through one of those assignments, you'll see that you're executing the Property Get, not the Property Let. In other words, you're assigning to the return value of the Property Get. What does that mean? Where is the assignment going? Straight into the ozone, apparently. Don't look for an explanation of this mystery in the documentation.

A related new feature of UDTs is that you can now store them in Variants. This radical idea promises interesting new ways of storing data in Variant collections of different kinds. You can store UDTs in the Collection class, the new Dictionary class, and in my CVector, CList, and CStack classes (see Chapter 4). Imagine the new organization tools you now have for all those data storage tasks that aren't quite difficult enough to justify a full database treatment. The TDictionary project discussed in Appendix A illustrates one such use with the Dictionary class.

Unfortunately, you have to work through one seemingly random limitation. Variants can contain UDTs, but only if those UDTs are defined in public classes. Don't look for an explanation of this feature. The only documentation is in the error message that you get if you try to violate the unstated rules, and even then the help on the error message just rewords the text of the message. I'm all in favor of reusing code, and it's often a good idea to put reusable code in components. But it seems a little extreme to say that I can't use a language feature except in components.

What if I wanted to build a quick-and-dirty music organizer by storing TAlbum variables in a Collection? For such a simple project the TAlbum type is so data-specific that I wouldn't expect to ever reuse it for another project. I have no practical need for a separate component, but the only place I can legally define my TAlbum type is in a public class in a component. I have two alternatives:

·  Put that type in an ActiveX DLL component whether I want one or not. Perhaps my component will consist of only one class containing only that one UDT-no methods or properties. Or maybe I'll randomly move half the functionality into methods and properties of the component in order to avoid the humiliation of being caught with a no-code, no-data component.

·  Make my application into an ActiveX EXE server. This alternative allows me to define my one public UDT in a public class, but I don't have to make anything else public. If I mark the server to run standalone and don't tell anyone it's a server, perhaps no one will ever notice. That's how the TDictionary project in Appendix A works.

I've thought quite a bit about this strange limitation, but I can't think of a reason for it. No reason is required. Just remember this simple rule: public UDTs can't be defined in forms or private classes, but they can be defined in standard modules or public classes, but only the ones defined in public classes can be stored in Variants, except that the green ones don't come with extra soy sauce.

Page 151. OleView is still provided with VB6, but not necessarily in the directory described in the "Interfaces" section.

Page 155. The FilterTextFile function shown in "Using an interface class" has a bug fix and an enhancement. But first let's talk about that Goto. I claimed in Chapter 1, "Goto," page 4, that I had hidden a Goto somewhere in a sample in the book. Karl Westberg sent mail pointing to this function and demanding the promised bucket of bits. Well, okay. In the main loop, in the Select Case, there's a Case with a constant called ecaAbort. If I get that constant, I jump out to an error handler just as if an error had been raised. Now as I told Karl, I don't think the idea behind this Goto is all that bad. The error traps are already there. Why not just Goto one if you need to? Efficiency would overrule political correctness in this case except for one minor problem.

The Goto doesn't work. If you study the code carefully, you can see that it will close the files properly, but will then raise an error. That's not what I had in mind. None of the filters I wrote using this code used the ecaAbort constant. I just threw it in because I thought it would be handy. Well, in writing new filters for the VB6ToVB5 program, I thought I needed to abort processing. What does the ecaAbort code mean? It could mean 1) stop processing the file and throw away results so far; 2) stop processing but keep the translation so far; 3) the translation is finished, so keep what's done, but copy the rest of the file unchanged. I thought I wanted 2, but it turned out I needed 3. The code as written came closest to 1, but still raised an inappropriate error. In the end, I changed the ecaAbort code to mean 2 and added a new code ecaTranslateAll to mean 3.

It turns out that Karl's idea of changing the Goto to an Exit Do was correct. If you just get out of the loop, you'll save the lines you have translated so far in the destination file. Saving the changes you've made so far and copying the rest unchanged takes additional logic. Here's the new main loop from the FilterTextFile function:

Dim sLine As String, iLine As Long, eca As EChunkAction
Dim fDoAll As Boolean
Do Until EOF(nIn)
    Line Input #nIn, sLine
    iLine = iLine + 1
    If fDoAll = False Then
        eca = filter.Translate(sLine, iLine)
    End If
    Select Case eca
    Case ecaAbort
        Exit Do             ' Stop processing
    Case ecaTranslate
        Print #nOut, sLine  ' Write modified line to output
    Case ecaSkip
                            ' Ignore
    Case ecaTranslateAll
        Print #nOut, sLine  ' Write modified line to output
        fDoAll = True       ' Translate the rest without filter
    Case Else
        BugAssert True      ' Should never happen
    End Select
Loop

I guess the moral of this story is that if you feel the need to use a Goto, check your code at least three times before giving in to temptation. I'm still not ready to say never, but I'm even more wary of Goto than I was before.

Page 167. The limitations on standards interfaces described in "Implementing Windows and COM classes" haven't changed much, but I do see evidence that Microsoft is glacially moving toward language independence in new interfaces. I wish Visual Basic program managers would go to bat for us on this issue, but most of the positive signs are actually coming from an unexpected source--Java. I haven't studied Microsoft's Windows-specific implementation of Java, but based on the Java-oriented type libraries and documentation I've seen, J++ programmers appear to need the same things VB programmers need--BSTR instead of LPSTR, VARIANT instead of void *, int instead of unsigned, and even IDispatch support.

Now why would the needs of a few hundred thousand customers of one language carry more weight than the same needs of a few million customers of another? Java programmers even get a complete Windows API class library--something VB programmers would kill for. And yet many Java programmers specifically don't want this class library because it defeats one of the primary goals of Java-system independence. VB programmers don't have any dreams of system independence. Why do Java programmers get what they don't want and we don't get what we want?

I'm discouraged. The move by the operating systems group away from APIs toward interfaces seems to be a wonderful opportunity for Visual Basic. Wouldn't it be nice if the whole operating system was just one big VB library? We're just a few steps away, but nobody wants to take those steps.

I once had a dream that Visual Basic designers added an interface equivalent to Declares so that we could write type libraries for standard interfaces in Visual Basic. Then I woke up. Realistically we can only hope for better support for type libraries written in IDL or ODL. Let's review the problems with interfaces and look at what could be done about them.

Many Windows interfaces don't come with type libraries. This is slowly changing, partly due to Java. New interfaces are more VB-friendly, and some of them even come with type libraries. My updated Windows API type library adds some new VB-friendly interfaces and improves support for old ones. I've heard rumors that the designers of DirectX (one of the most VB-hostile interfaces) are actually planning to create a VB-compatible type library. It's about time.

VB doesn't recognize unsigned integers (long or short) in type libraries. This problem has not been fixed in VB6. The workaround is to lie in your type library, claiming that the unsigned integer is actually signed. Given the standard disclaimer that I haven't seen VB source code and don't know exactly what could be done with it, I believe that this problem could be easily fixed by Visual Basic. In fact, I suggested the fix to VB program managers a long time ago. VB could simply recognize unsigned integers from type libraries as signed integers. The bits are the same. A few strategically placed typecasts in the VB code that handles type libraries could eliminate this problem forever.

I was told that they don't want to make this change because some future version of VB might support unsigned integers. In fact, early descriptions of COM+, the dream enhancement that would solve all COM problems while putting a stop to death and taxes, implied that someday all Microsoft languages would support a set of standard types that included unsigned integers of various sizes. Well, folks, it's time to put an end to this idiotic idea once and for all. The designers of Basic didn't just forget to add the unsigned integers. They left them out for a good reason--the same reason that Java doesn't have unsigned integers even though it's based on C++, which does. High level languages don't have unsigned integers because most of the time the convenience of fewer simple types overrides the occasional inconvenience of working without unsigned integers for a few uncommon operations. If Visual Basic designers would respect one of the fundamental design principles of their language, this problem could go away for good.

Many standard interfaces use LPSTR or LPWSTR instead of the BSTR type recognized by VB. This is no problem if Windows implements the interface and you use it. If necessary VB will do the same Unicode conversions that it does for Declare statements. It is a problem if you implement the interface and someone else calls it. In that case, you have to define string types as Long pointers and use CopyMemory to copy from the pointer to internal VB strings. The problem is the same as for callbacks used with the AddressOf operator, as described in Chapter 2, "Strings in Callback Procedures," page 104. This problem is annoying, but fortunately Java also prefers BSTR parameters, and some interface designers are using them to accommodate Java programmers.

Many standard interfaces use structure types as parameters. Until VB6, structure types were not a valid type for IDispatch or dual interfaces, but VB could also use or implement interfaces for use with pure COM servers. You could use structures (UDTs) for system interfaces that didn't go through IDispatch (such as DirectX interfaces). Fortunately, VB6 now recognizes public UDTs and this problem, which wasn't very common to begin with, now goes away. That doesn't mean VB knows how to deal with any kind of structure an interface might define. Some structures contain C-specific members that still require you to lie about types and use CopyMemory. Again, the solution is for interface designers to stick to language-independent designs.

Some interface methods return [out] parameters which VB can't handle. This problem is the result of a bad design decision in some of the original COM interfaces. There are two kinds of reference parameters in type libraries: [in,out] parameters, which can receive valid data, modify it, and send it back to the caller; and [out] parameters that are assumed to contain garbage on input but return valid data to the caller. VB handles both kinds exactly the same with simple reference parameters. The problem is that some C chauvinist decided that if C and C++ had pointers, all languages must have pointers, so it would be nice to take a pointer-based shortcut. In interfaces that use this shortcut, a null pointer sent in by the caller is a signal that the callee should ignore the argument.

If you are implementing an interface, you are the callee and you have to be ready to test for a null pointer. But in VB, a reference parameter is transparent. You simply assign to it without any concern that you're actually assigning through a pointer. Unfortunately if the caller sends you a null pointer and you assign to it, you'll crash. So you have to lie in the type library and claim that the parameter is a ByVal Long. When you get the pointer parameter, you check to see if it's zero. If it is zero, you ignore it. If it's not zero, you use CopyMemory to copy the data through the pointer. For example, the Next method of IEnumVARIANT has one parameter called celt that the caller uses to tell how many elements it can receive. The callee returns the number of elements it actually got in an [out] parameter called pceltFetched. But if the caller is only requesting one element at a time, the number returned isn't that relevant so instead the caller sends a null pointer. A more language-independent design would be to return 1 through the reference parameter whether it's relevant or not.

Interfaces that use this dumb shortcut usually say so in their documentation (or at least they ought to). If the documentation doesn't say that the [out] parameter pointer can be null, you can simply change the type library attribute to [in, out] and use a normal reference parameter. Fortunately, new interfaces don't seem to be using this shortcut much. VB designers couldn't do much to fix this problem except try to persuade interface designers not to perpetuate it.

Some interface methods return a positive HRESULT. All methods for VB-friendly interfaces should return an HRESULT, which VB will turn into the Err object. Unfortunately, VB does not handle one of the more obscure features of HRESULTs. Normally, HRESULTs come back with either a 0 (the constant S_OK) to indicate success or a negative number to indicate failure. There are many kinds of failure, and different negative numbers can indicate what kind of failure occurred. But it's technically possible to have different kinds of success. A convention used by some interfaces is to return 0 (S_OK) for True, 1 (S_FALSE) for False, or a negative number for an error. The problem is that VB throws away this information. VB does not have any way to detect the difference between a 0 and a 1 return. This problem is the only one that can't be hacked around by lying and cheating in type libraries. Any interface that uses this convention can only be implemented using the gawdawful v-table hack described at the end of Chapter 4.

My last official act as a VB6 beta user was to enter a bug report against this problem and to request a new feature that would make it unnecessary. I suggested that the Err object should have a new HResult property that returned the raw error value. You could turn off error trapping and use this raw data for the few cases where VB's high-level error wrapping gets in the way. You would then be able to implement IEnumVARIANT and similar interfaces with minor hacks instead of major ones.

Chapter 4

Page 173. The second to last sentence in the first paragraph of "Using a List Iterator Class" suggests that if you're not convinced by my analogy, you should "hold your piece." I'm still trying to figure out whether I wrote this accidentally or on purpose, and if on purpose, how it got past all my editors and proofreaders. Some readers may be packing, but even they should holster their pieces rather than holding them in this context.

Page 194-96. The problems described in "Collections 201--Indexing" are all still there, all still unnecessary, and all easily fixable. The Dictionary class described in Appendix A avoids the same mistakes. Collection could be made just as powerful without losing any of its current features. Just imitate the Dictionary and add Keys, Key, Exists, CompareMode, and RemoveAll members to Collection, and change the Item method to a read/write property. This fix should have been done long ago.

Page 196. Insert a new section entitled "The Dictionary Class" after the end of "The Collection Class" and before "What Makes a Collection Walk?" Because this new section is long and stands alone, it is located in Appendix A rather than here.

Page 202. I'm sorry to say that the flame about using -4 as a magic number at the end of "Delegating the iterator" is still true. This is a continuation of the flame about the DBFH. As I said then, I expect any part of the public interface of a public product to meet minimal standards of professionalism. This doesn't.

Page 205-19. There's a serious problem with all my code for walking through collections. The error only shows up once in the book, but it occurs in all the collection classes in VBCore. Worse yet, the Collection Wizard generates the same bad code, meaning that any collections that you generated have the problem. When I get something wrong, I get it wrong consistently. Kai Wagner was the first to identify the problem and suggest a solution. Elliot Witticar also noticed it, and suggested a better solution.

Fully understanding this bug requires you to understand a complicated system that I don't describe completely even in the book. I'm only going to explain the part that relates directly to the bug. The system starts in the CDrives class as described in "The CDrives class, version2," page 205-207. CDrives has a NewEnum method, which is called by Visual Basic whenever you start a For Each block. This NewEnum function is supposed to return an IEnumVARIANT object that walks the collection, but for reasons described in the text, it can't do this directly. Instead it calls the NewEnum method of a CDriveWalker class, which returns a CEnumVariant object, which implements IVBEnumVARIANT, which is equivalent to the real IEnumVARIANT. You don't really want to know why.

The actual bug is in the Initialize event shown in the "The CDriveWalker class," page 207 and 209:

' Delegate to class that implements real IEnumVARIANT
Private vars As CEnumVariant
. 
. 
. 
Private Sub Class_Initialize()
    ' Initialize position in collection
    i = 1
    ' Connect walker to CEnumVariant so it can call methods
    Set vars = New CEnumVariant
    vars.Attach Me
    Debug.Print "CDriveWalker:Initialize"
End Sub

Notice that the Initialize event sets a new vars object of type CEnumVariant. It then calls the CEnumVariant Attach method:

Private connect As IVariantWalker
 
Sub Attach(connectA As IVariantWalker)
    Set connect = connectA
End Sub

Eventually the vars variable is returned to the CDrives class through the NewEnum function:

' Return IEnumVARIANT (indirectly) to client collection
Friend Property Get NewEnum() As IEnumVARIANT
    Set NewEnum = vars
End Property

The problem occurs in the Initialize event where the CDriveWalker has a reference to CEnumVariant through the vars variable, but that object also has a reference to the CDriveWalker class as set by the Attach method. In other words, we have a circular reference. Since the vars object in CDriveWalker is at module level, it isn't going to go out of scope until CDriveWalker is destroyed. But the connect object in CEnumVariant is also at module level and it isn't going to go out of scope until CEnumVariant is destroyed. Worse yet, CDrives has a reference to CDriveWalker, and it can't terminate until CDriveWalker terminates. None of the three objects involved is going away until Visual Basic terminates and automatically cleans up all stray references that sloppy programmers like me haven't cleaned up themselves. This situation is described in Chapter 10, "Not reference counting," page 658.

Fortunately, we don't have to use the ObjPtr hack described in Chapter 10 to solve this problem. We just have to make the vars variable a local variable that goes out of scope automatically after it's used. We do this by moving the variable and the code that sets it out of the Initialize even and into the NewEnum function. The new version should look like this:

' Return IEnumVARIANT (indirectly) to client collection
Friend Property Get NewEnum() As IEnumVARIANT
    ' Delegate to class that implements real IEnumVARIANT
    Dim vars As CEnumVariant
    ' Connect walker to CEnumVariant so it can call methods
    Set vars = New CEnumVariant
    vars.Attach Me
    ' Return walker to collection data
    Set NewEnum = vars
End Property

A chain of destruction results that allows all three objects to clean themselves up appropriately. Visual Basic's For Each mechanism has an indirect reference to the CEnumVariant object that is actually doing the walking. When For Each is done, it releases the CEnumVariant object, which allows the CDriveWalker reference to be released, which allows the CDrives object to be released. Everyone lives happily ever after--except me.

I have to live with the embarrassment of having created a bug that left other people's memory stranded. Visual Basic's backup object cleanup mechanism probably prevented any real disasters, and the actual memory wasted probably didn't cause any great performance losses. But this sloppy coding should have been avoided. My only excuse is that this piece of code is probably the most complicated I have ever written in Visual Basic, and I suppose I'm lucky to get away with only one bug.

As a penalty for being careless, I had to apply the fix to every collection walker in VBCore including CDriveWalker, CListWalker, CRegItemWalker, CRegNodeWalker, and seven different vector walkers. I also had to fix CListItemWalker in the ListBoxPlus control. Finally, I had to apply it to the Collection Wizard so that I wouldn't create any more bad code.

Most of the work was just tedious replacement, but in fixing the CListWalker class, I found another bug. The Attach method shown in "Walking the CList collection," page 217, was all wrong. The NewEnum function of CList did not need to call the CListWalker Attach method. The following Attach method replaces the code in the book:

' Attach a list to iterator
Sub Attach(connectA As CList, Optional
fEnumerate As Boolean = False)
    ' Initialize position in collection
    Set connect = connectA
    ' The fEnumerate parameter was a mistake, but can't remove it
    ' without breaking compatibility
End Sub

Page 210,11. I really wanted to write the following sentences and have them be true: "The bizarre techniques described in 'Stupid code tricks' are no longer necessary. VB6 now allows you to easily write your own collections using intuitive language features." Unfortunately, the sentences are not true. Nothing has changed. Admittedly, some of the problems with IEnumVARIANT (C-style arrays and [out] parameters) are inherent and VB couldn't do anything to fix them. But some of the problems (unsigned integers and positive HRESULTs) could have been fixed. See the notes to Chapter 3 about these problems.

In particular, if Visual Basic had a way to recognize positive HRESULTs, the v table hack would be unnecessary. That doesn't mean writing collections would be easy. The rest of the implementation would still be a mess, although significantly simpler than the current mess. Better yet, VB could provide a simpler collection interface, similar to my IVariantWalker, and hide the details of how it is connected to IEnumVARIANT.

Page 218,19. You can add one more entry to the Performance sidebar at the end of the chapter. Immediately after entry 6, "For Each on Integer collection," write a new entry called "For Each on Dictionary." The timing is a bit of a problem because you're unlikely to have exactly the same computer I tested with and VB6 won't give exactly the same results as VB5, but you can adjust the number of iterations until you get comparable numbers. I can give you a preview of the results. You'll see approximately the same results for iterating a Dictionary with For Each as for doing the same on a Collection. But as Appendix A explains, you won't often need to iterate a Dictionary by item, so the speed doesn't really matter. I commented this test out in the VB5 version, but users who have the Scripting Runtime can put the code back in and use it with VB5. The Scripting Runtime comes with Windows 98 and with the Windows NT Option Pack, or you can download the latest version of the scripting host from http://msdn.microsoft.com/scripting/.

Chapter 5

Page 223-25. "The VBCore Component" describes two ways of using the VBCore library functions. First, you can reference the VBCore component and get everything you need from the DLL. You can also use just the modules you need--the private versions are in the LocalModule directory. But it turns out that some readers want to use a third method that never occurred to me. They want to cut out the functions they need and paste them into their own code.

Some of those readers who try method three are just overly enthusiastic hardcore programmers. They can't be bothered with all that wimpy crap in the first 10 chapters and skip directly to the hardest code they can find in Chapter 11. They paste this code into their existing code without any clue that it might be dependent on the Windows API Type Library or other modules in VBCore. Some of them send me mail asking where I hide my constants and declares. If you're one of those hardcore programmers, let me say this once: Look at the description of the type library at the beginning of Chapter 2 before you do anything else.

But some of you use method three on purpose. You really don't want your programs to have any outside dependencies, and you don't want one line of your project code to be unreferenced. If I'm describing you, I doubt that anything I could say about the tradeoff between programming time and program data is going to change your mind. But let me suggest that there's a better way of getting what you want than cutting and pasting from each module.

Instead of using method three, try a modified version of method two. Create your project in a separate directory and copy the modules you need from the LocalModule directory to that directory. Start with the modules containing the functions you want. You'll probably also have to add some other modules that those modules depend on. For example, many modules use code from the MUtility and MBytes modules. This process is going to take some trial and error (though you could write a wizard to eliminate the trial). You'll also need to reference the Windows API Type Library, since most of my modules use it. If you don't believe my claim (start of Chapter 2) that using the type library has no cost, you can write your own declares, but don't expect the crude and buggy ones in WIN32API.TXT to work with my code without modifying them.

Once you get everything working, go back and delete or comment out all the unreferenced procedures. I've heard that tools exist that automatically check for and remove unreferenced procedures, but I haven't used one. I'm not endorsing this method or claiming it will make a noticeable difference in the size of your executable, but it will save you some time.

Page 228. The flame just before the "Instance data versus shared data" section is still true. I was hoping the VB designers would do something to fix the horrible usability problems with global modules, but they're still the same.

Page 231. "The Global Wizard" describes a tool that can save you from many of the differences between global classes and standard modules, but there's one difference you can't escape. If you use an interface from VBCore (a public class), its interface event procedure may be slightly different than the same interface generated with a private class. For example, if you implement the VBCore version of IUseFile, VB will generate an interface event procedure in this format:

Private Function IUseFile_UseFile(UserData As Variant, _
                                  FilePath As String, _
                                  FileInfo As VBCore.CFileInfo _
                                  ) As Boolean

But if you use the private version of IUseFile in the LocalModule directory, your IUseFile will look like this:

Private Function IUseFile_UseFile(UserData As Variant, _
                                  FilePath As String, _
                                  FileInfo As CFileInfo) As Boolean

Notice the VBCore qualification on the CFileInfo type. You can remove the qualifying library name and things will still work. This problem isn't major, but it's symbolic of the troubles you'll have trying to maintain compatible sets of global classes and standard modules.

The Global Wizard and the behavior it attempts to fix are a house of cards, waiting for any excuse to tumble. Global classes have more exceptions than rules. In hindsight, I'd say that writing and maintaining a large function library in Visual Basic is not worth the trouble.

Page 237. The random number functions in the "Really Random" sidebar don't quite do what the comments say they do. Lynn Torkelson pointed out some off-by-one errors in the optional parameters, resulting in a Random function that could never return zero, even though the comments say that zero is in the possible range. Unfortunately, it's tough to verify the fix. What are you going to do? Generate 2,147,483,647 numbers and see if any of them are zero? Even then you couldn't be sure. Nevertheless, I think I've got it right now. I adjusted some boundary code, added GetSeed function to save the last seed for replay, and added a check to raise an error if the minimum and maximum parameters are too far apart. For most uses, the Random function works exactly the same as before--except more random.

Page 242. The project behavior described in the flame in "How they do it" no longer works as reliably as the text implies. Since this feature is undocumented, I suppose the Visual Basic designers didn't feel obligated to keep it working. But being able to switch between a source component and a compiled component by switching between VBG and VBP was so handy that I vainly hoped they would actually make the feature official by documenting it. No such luck. The only change is that this wobbly system is even less stable in version 6 than in version 5. See the description of my build problems.

Page 244-45. I wish I hadn't written the "Better Basic Through Subclassing" sidebar. It's all true, but the techniques described are…uh, uncouth. If you use them, don't tell anyone I told you how.

Page 249-251. The code examples in "How to Raise Errors" do not exactly match the real code in the Utility module. After I finished writing the chapter, I added errors to the Enum, VBCore's resource table, and the local ErrRaise procedure. This doesn't make any difference to the points I'm making, but I thought I should fess up in case some eagle-eyed reader noticed the difference.

Page 252. The ErrMsg program described in the note in "Turning API Errors into Basic Errors" has been enhanced. In addition to looking up the error message associated with an error number, the program also attempts to look up the constant name for that error number. If it finds a standard constant name (in the format ERROR_INVALID_DATA), it copies the name to the clipboard so that you can easily paste it into your program. In addition, the program is now partially language independent--it recognizes hexadecimal numbers in the Basic, C, and Pascal formats.

Page 253. Some readers may be disappointed in my cavalier treatment of error handling in "Handling Errors in the Client." I simply claim that handling errors is a user interface problem, not a programming problem. Untrue. Handling errors (as opposed to raising them) is a programming topic worthy of being discussed in an advanced Visual Basic book. For example, the problem of creating a global error handler that can be called from any local error handler has been discussed a lot. You'll notice, however, that my sample applications don't use a global error handler and don't handle errors in a consistent way. It's rare for me to encounter a topic on which I have no opinion, but I'm afraid this topic is it. Fortunately, other VB authors discuss the topic exhaustively.

Page 253-62. It's time to add another chapter to the strange tale told in "Code Review." The story itself is just as instructive and timeless as ever, but I now have a new version of GetToken that runs about three times faster than the one developed in this section. Pointer arithmetic and some new API calls make the faster code possible. Also, VB6 adds a new Split function that can parse huge files almost as fast and accurately as Joe Hacker's empty GetToken presented near the end of the section (page 262). I tell the whole story in Appendix B.

Page 262. The next to the last sentence in the final Archaeologist's note in "Code Review" is wrong and should be deleted. It says that Parse.cls delegates its implementation to Parse.bas. Not so. Parse.cls does no delegation. It is simply a global class version of Parse.bas created with the Global Wizard.

Page 262-70. "Fixing the Windows API" and its subsections are made at least partially obsolete by the FileSystemObject class (which I'm going to call FSO because I can't stand the stupid class name). Most of the functions described in this section could be emulated with a few lines of FSO code.

It's about time. You shouldn't have to use an advanced book like mine to figure out how to test whether a file exists or to split a full path name into parts. Unfortunately, the FSO is incomplete in its coverage directory and file operations. It can't search directory paths as SearchPath does (page 266). It doesn't follow the Win32 standard of always terminating paths with a backslash, thus requiring the cheesy BuildPath to combine a path and file name reliably (similar to my NormalizePath). Its most serious limitation is that it can only process text files. This may be good enough for VBScript, but it isn't much of a challenge for the real class libraries of Java, C++, and Delphi. Finally, like the rest of Visual Basic, FSO is completely unaware of the new 32-bit operations such as navigating the shell namespace and of new I/O operations such as processing streams and storages. And of course advanced 32-bit features such as asynchronous I/O or memory mapped files are out of the question.

Still, the FSO model has some nice features. The different classes fit together nicely, solving several glaring VB5 limitations such as the inability to return the creation or last access dates of a file. You can't expect too much from the first version of a library borrowed from VBScript. On the other hand, you may not want to throw out the corresponding functions in VBCore. The problem is that the FSO isn't part of Visual Basic. You have to ship the Scripting Runtime (SCRRUN.DLL) unless you're sure all your customers have the latest Microsoft operating system.

I could have reimplemented most of my FileTool module and some parts of the Utility module using FSO instead of API functions. The code would have been shorter, but I didn't want to make VBCore dependent on the Scripting Runtime. The only thing I found in FSO that wasn't in VBCore is that its Drive class returns the file system name, but my CDrive class doesn't. I could have added that feature, but I think FSO is entitled to its one advantage. On the other hand, VBCore has quite a few file and path operations that FSO lacks (see Chapter 11).

While on the subject of path functions, I'll add that the SHLWAPI DLL discussed in Appendix B provides several new API functions related to paths, filenames, and directories. Hardcore programmers who find FSO and VBCore lacking may find what they need in SHLWAPI.

Page 273. The LoWord4 function in "Five Low Words" is missing a comma. Change this line:

CopyMemory LoWord4, dw 2

To this:

CopyMemory LoWord4, dw, 2

Page 274, 275. "Five Low Words" contradicts itself in the last two paragraphs of the section. First it claims that the C++ version is slower, then in the next paragraph it claims that the C++ version is faster. Which is it? The C++ version is slower than some native code versions, but faster than all p-code versions. It's a little bit faster than the version used in VBCore. But as I stated, any speed differences won't matter except in the most deeply nested loops. Avoiding an extra C++ DLL is more important than the speed differences. Yuri Kuptzevich reported this error.

Page 279. The BytesToStr function shown in "Reading and Writing Blobs" is wrong. The correct code is:

' Convert an ANSI string in a byte array to a VB Unicode string
Function BytesToStr(ab() As Byte) As String
    BytesToStr = StrConv(ab, vbUnicode)
End Function

The error was caused by my confusion between the BytesToStr function (used for processing blobs) and the ByteZToStr function (used for processing fixed-length byte arrays from API calls). See notes on Chapter 2 for more details on the other use.

Page 279. "Reading and Writing Blobs" mentions a function called IsArrayEmpy that can identify an empty array such as this example:

Dim ab() As Byte

The text says that "the error trapping in this function is too obscene to show in this family-oriented book, but you can look it up in the Utility module." It turns out there was another good reason not to show the code: It didn't always work. This adults-only article can show the original code:

Function IsArrayEmpty(va As Variant) As Boolean
    Dim v As Variant
    On Error Resume Next
    v = va(LBound(va))
    IsArrayEmpty = (Err <> 0)
End Function

Torsten Rendelmann found two cases where this code would fail. If the array was not empty and its first element was an object, the assignment would fail because it would need a Set statement. The error trap would catch the failure and report that the array was empty even though it wasn't. The function would also fail on arrays with multiple dimensions. Torsten suggested the following fix:

Function IsArrayEmpty(va As Variant) As Boolean
    Dim i As Long
    On Error Resume Next
    i = LBound(va, 1)
    IsArrayEmpty = (Err <> 0)
End Function

An empty array has no dimensions, so assignment of the lower bound of the first dimension fails. Non-empty arrays always have a first dimension, so the assignment succeeds.

Torsten also suggested a GetArrayDimensions function that uses a similar technique to count the dimensions of an array. This function has been added to the Utility module along with Torsten's fixed IsArrayEmpty.

Page 280-81. Many Visual Basic programmers have been waging a losing battle to fix the problem described in the "Evil Type Conversion" sidebar. For example, VB writer Jim Karabatsos of the Australian Visual Developers Forum (http://www.avdf.com/) says he wants a new Option statement called Option Strict-And-I-Really-Mean-It. Microsoft has heard this request from many sources in many different ways, but VB6 still offers no way to turn off Evil Type Coersion (ETC).

Page 282-96. VBCore has several new functions related to those functions in "Sorting, Shuffling, and Searching." Appendix A mentions new filter functions that take an array as an argument and return a sorted or shuffled copy of the array. I wrote these functions for use with the Dictionary class, but they may be handy in other contexts.

Chapter 6

Page 297. The introduction to Chapter 6 was supposed to have been rewritten and entered in the Harry's Bar & American Grill Imitation Hemingway Competition:

"I do not order you to do the windows. If you cannot do the windows, do the forms. The forms are always possible in this language. Hernando did the windows rather than the forms, but he was Hernando. I do not know if you can follow Hernando."

"Do not worry about the windows. I will do them. It is not the windows I fear. It is the processes. Even Hernando had a dread of the processes…"

Unfortunately, I never found time for the rewrite.

Page 301, 2. "Inside CWindow" says that the secret private variables shown are from the Windows 95 version of the CWindow class. Although my inside sources have dried up, I have every reason to believe that these are essentially the same in Windows 98.

Page 302. "A Basic CWindow class" claims that the TWindow project was included on the second edition CD. It wasn't. It is included in this update. Murat Sabuncu reported this bug.

Page 307. In the ClassNameFromWnd function shown in "Getting Windows string data," the type of the cName variable has been changed from Integer to Long for the same reason discussed in a note to Chapter 2.

Page 309. "The Windows Word and the Windows Long" has an incorrect comment in one of the code fragments. The comment for the constant GWL_USERDATA should be "User-defined data," not "Window style."

Page 310. "Style bits" claims that the WinWatch program uses the GetWndView function to create a View heading. This was true in the 16-bit WinWatch program created for the first edition, but there is no View heading and GetWndView is not called from the 32-bit WinWatch in the second edition. If I were going to write a third edition, I'd delete the GetWndView function and all references to it from the text, although I'd leave it in VBCore. Yuri Kuptzevich reported this error.

Page 311. Now it can be told. My original source for the techniques used in "Style bits" was Keith Pleas--my co-author on the aborted third edition of Hardcore Visual Basic. As the note at the end of the section states, Keith originally described the technique for Visual Basic Programmer's Journal under the pseudonym Escher.

Page 326, 327, and 334. The CreateProcessList function show in "The process list--Windows 95 view" opens a snapshot handle with the CreateToolhelp32Snapshot function, processes the snapshot, and then fails to close the handle. Richard Hardman and Eric Parker noticed my sloppy coding. I'm embarrassed beyond words. I made the same stupid mistake in the ExePathFromProcID function in "More About Handles and Process IDs," page 334. In fact, when I checked the code closely, I found that I didn't close any of the eight snapshots I created in the ModTool module. A comment next to the Declare in the same module said that snapshot handles should be destroyed with CloseHandle, but I failed to follow my own advice. Those handles finally get closed properly in the update.

Page 327-29. The Windows NT branch of the CreateProcessList function shown in "The process list--Windows NT view" has a mistake just as stupid and embarrassing as the one in the Windows 95 branch. Once again I create handles and fail to close them. John Sanders sent me a bug report showing that when he called CreateProcessList in an endless loop, it would eat handles and memory until it crashed. A look at the code shows why this happened. I created a handle for each process with the OpenProcess API, but then failed to close those handles when I finished with them.

The solution is simple in concept. The handles are opened in a process loop. Simply call CloseHandle at the end of the loop block. The fix was complicated by my use of GoTo to fake the Continue statement that Visual Basic ought to have, but doesn't. A Flame at the end of the section justifies my use of GoTo in this context. But when I closed the handle at the end of the loop block, I ended up with a different organization. Even if VB had a Continue statement, I wouldn't be able to use it here. GoTo may be justifiable to fake a missing language statement, but nothing would save me from the structured programming police if I used GoTo to jump to a label in the middle of the block. I had to reorganize by adding additional levels of indentation. Here's the new code:

    Else
        ' Windows NT uses PSAPI functions
        Dim i As Long, iCur As Long, cRequest As Long, cGot As Long
        Dim aProcesses() As Long, hProcess As Long, hModule As Long
        ' Guess at maximum number and loop until guess is enough
        cRequest = 96       ' Request in bytes for 24 processes
        Do
            ReDim aProcesses(0 To (cRequest / 4) - 1) As Long
            ' Fill an array with process IDs
            f = EnumProcesses(aProcesses(0), cRequest, cGot)
            If f = 0 Then Exit Function
            If cGot < cRequest Then Exit Do
            cRequest = cRequest * 2
        Loop
        cGot = cGot / 4     ' From bytes to processes
        If cGot Then ReDim Preserve aProcesses(0 To cGot - 1)
As Long
        
        ' Create CProcess object for each process
        For i = 0 To cGot - 1
            hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or _
                                   PROCESS_VM_READ, 0, _
                                   aProcesses(i))
            ' Processes that fail probably don't
have security rights
            If hProcess Then
                ' Get first module only
                f = EnumProcessModules(hProcess, hModule, 4, c)
                If f Then
                    sName = String$(cMaxPath, 0)
                    c = GetModuleFileNameEx(hProcess, hModule, _
                                            sName, cMaxPath)
                    ' Put this process in vector and count it
                    Set process = New CProcess
                    process.Create aProcesses(i), Left$(sName, c)
                    iCur = iCur + 1
                    Set vec(iCur) = process
                End If
                CloseHandle hProcess
            End If
        Next
    End If
    Set CreateProcessList = vec
End Function

Page 333. "More About Handles and Process IDs" has an entire paragraph about instance handles. Unfortunately, most of this paragraph is technically questionable and largely irrelevant. The paragraph follows immediately after the InstFromWnd function sample and starts with the sentence "Very simple, and very useful--in 16-bit Windows." The first three sentences of this paragraph should be retained, but the rest can be deleted. Yuri Kuptzevich pointed out the problems with the paragraph.

Page 341. The last example in "The SendMessage function" is wrong. It claims that you can send the WM_UNDO message to a textbox window with the following line:

Call SendMessage(txtEditor.hWnd, EM_UNDO, 0&, 0&)

I said that this works, and I usually test code before I make such statements, so it probably does work. But if so, it's not for the reasons claimed. Windows probably ignores the last two parameters with the EM_UNDO message. I claim in the text that since Visual Basic can't send constants by reference, it creates temporary variables containing zeros and passes those variables by reference. This is true, but it means that what Windows sees is not the zero, but the temporary address of the temporary variable containing the zero. Documentation for EM_UNDO says wParam and lParam must be zero, but apparently it doesn't check.

My mistake illustrates how easy it can be to get wParam and lParam wrong in SendMessage and the many other functions with typeless parameters. CopyMemory and CallWindowProc are two other common examples. Many messages are less forgiving than EM_UNDO. I get questions about similar parameter problems all the time, and I can't swear I always give the correct answers. There's no substitute for figuring it out yourself.

For example, don't use Declare statements out of WIN32API.TXT and expect them to have the same parameter definitions as the ones I use in my samples. When there are several possible ways to write a Declare, my choices seldom match those of the authors of this file.

Another common mistake is to use 0 rather than 0& as a constant. Unfortunately, the first 32-bit Visual Basic (version 4) made the silly mistake of making constants Integer rather than Long. If Windows expects a Long, but you pass an Integer zero, you could be in serious trouble, because the other half of the Long value that Windows looks at could be the last random value that happened to be on the stack. This is a difficult bug to verify, but I will show an example in notes to Chapter 11. The zero constants from my type library such as hNull and pNull have Long type and are always safe.

Page 346. The SysMenuProc in "Your own window procedure" caused trouble for some online readers. I provided the last part of Chapter 4 as a book sample on my Web site. Readers of the real book had the TSysMenu project described in the text on their CD. Online readers had the key code and a description of how to create the rest, but they didn't have my Windows API Type Library. So some enterprising readers tried my code with declares out of WIN32API.TXT. They failed because my CallWindowProc is different than their CallWindowProc. Mine is in the type library, but an equivalent declare would look like this:

Public Declare Function CallWindowProc Lib "user32" _
    Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, _
    ByVal hwnd As Long, ByVal Msg As Long, _
    ByVal wParam As Long, lParam As Any) As Long

Notice that wParam is ByVal As Long, but lParam is ByRef As Any. This is an unexpected choice, but one that is consistent with the normal use of CallWindowProc and with the SysMenuProc sample. Unfortunately, it's not consistent with the comparable SendMessage declare shown a few pages earlier (page 340) or with the one in WIN32API.TXT. Those who guessed wrong and crashed as a result have a good excuse. To eliminate all confusion, I added the declare above (commented-out) to the online text and the updated sample.

Page 350. The last two paragraphs of "Encapsulating Message Traps" discusses two examples--CSysMenu and CMinMax. Actually, VBCore has two other classes that implement ISubclass. CPalette will be discussed in notes to Chapter 8. CTrayIcon is mentioned in Chapter 11, "More Interface Stuff," page 652, but its implementation is not shown or discussed.

Page 350, 351. The section "Encapsulating Message Traps" and its subsections refers several times to Figure 6-3, but there is no such figure. The figure was intended to be a screen dump of the Test Messages program (TMessage.vbp). The figure is not crucial to understanding the description. You can run the program to see what it looks like.

Page 353. A paragraph in the middle of the "System menu code in the CSysMenu class" (just after the Create example) says that my subclassing system expects only one message per class. Fortunately, this statement is false, as the CPalette class in VBCore proves (see notes in Chapter 8). A class that implements ISubclass can handle as many messages as it wants as long as it attaches each of them with AttachMessage and handles them in the ISubclass_WindowProc method. In other words, the subclassing mechanism is even more flexible than I thought when I wrote the code. The second limitation described in the same paragraph--that you couldn't write different classes to handle the same message--is true in my subclassing system, but need not be. Steve McMahon had a special need to have different classes handle the same message, so he modified a private copy of the SubTimer class to add this feature. I never found time to fully evaluate this code or to integrate it into the official version, but I can tell you that it extended my GetProp/SetProp techniques to save and restore additional window information. I'm sure that's enough information for those Hardcore++ users who really need that feature.

Page 354, 55. The title of the "System menu code in the CMinMax class" is wrong. There is no system menu code in this class. A better title would be "Window limitations code in the CMinMax class." In addition, this class had a bug (reported by Alex Nichols) in its MinWidth and MinHeight Property Let procedures. I didn't show the original code in the book, so there's no need to show the fixed code here, but the class works better now.

Page 361. "Debugging subclassed windows procedures" talks about the relationship between VBCore.dll and SubTimer.dll, but questions from readers indicated that there may be some confusion about the dependency between these components. Do you need to ship SubTimer to any client that gets VBCore? I believe you need to ship SubTimer only if your client program uses a VBCore class that uses SubTimer--CMinMax, CSysMenu, CTrayIcon, or CPalette.

If the "I believe" in the statement above seems uncharacteristically vague and timid, that's because I have no real experience with setup. Book authors have an unusual opportunity to ignore the whole issue. All my customers have VB by definition, so I don't need to ship a setup program with each of my sample programs. I have never built a setup program to ship VBCore with a program, and so have no experience in an area that "real" VB programmers have to cope with all the time.

Chapter 7

Page 364. I think I've found the origin of the term canvas used in "Device Contexts and Canvas Objects." In Delphi the surface of any form or control is called a canvas and can be accessed through a Canvas property of type TCanvas. Could it be that the Visual Basic developers who use this term have been sneaking looks at the competition? If so, I wish they'd look a little harder and borrow some of Delphi's best ideas.

Page 372. Windows 98 and the forthcoming Windows 2000 make VB's Screen object even less complete than described here. Both operating systems support multiple monitors. But how will you know the size of each monitor and how will you specify which one to print or draw to? This is an interesting problem for Hardcore programmers.

Page 377. The Warning near the end of "The Basic Way of Drawing" notes that VB4 and VB5 couldn't handle unqualified Line, PSet, and Circle statements in With blocks. Neither can VB6.

Page 377 and 415. The "Hardcore Drawing" sidebar describes the OpenGL library as the best way to draw 3-D shapes while the "Hardcore Painting" sidebar presents DirectX as the best way to do advanced blitting. That was correct when I wrote it, but the situation is more complex today. DirectX now includes Direct3D, which supports 3D drawing. Many other painting, drawing, and music technologies have been added to DirectX. There is also an initiative to merge the OpenGL and DirectX technologies.

If you're interested in writing games with Visual Basic (as many of my readers are), DirectX is the most promising way to do it. Unfortunately, DirectX is designed by and for C++ programmers. The technology is based on COM interfaces, and suffers from all the problems described at the end of Chapter 3 of my book and in notes in this document. Among other problems, Microsoft provides no official VB-friendly type library for DirectX. It's a real hardcore project to make DirectX accessible to VB programmers, but a few hardy souls have taken on the challenge. I haven't investigated DirectX personally, but I have assisted readers who are working on it. Some of their work may eventually be made public. But there's also a rumor that the DirectX team has somehow become aware of the existence of two million potential customers and may do something to make their technology available to us.

Page 381. The "Three Ways of Scaling" sidebar says (third paragraph) that Visual Basic doesn't recognize MM_HIMETRIC as a scale mode value. This is no longer true in VB6. There is a vbHiMetric constant for high metric, and two other new scale mode constants. The vbContainerPosition constant represents the units used by a control's container to determine the control's position. The vbContainerSize constant represents the units used by a control's container to determine the control's size. I haven't experimented with these new modes, but they look useful.

Page 383. The DrawBezier function in "Bezier curves" is changed because of the change in painting model described earlier.

Page 384. The Declare shown at the bottom of the sidebar "MoveTo Comes Full Circle" is not equivalent to the type library entry as claimed. Hardcore programmer Gideon Fraser figured this bug out the hard way. In the type library, the last parameter is ByRef As Any initialized to a Long 0 (a null pointer). The Declare syntax doesn't provide a reliable way to do this. Technically the last parameter should be:

Optional lpPoint As Any = ByVal 0&

But ByVal is illegal when specifying a default value. Here is the closest reliable version of the Declare:

Declare Function MoveTo Lib "gdi32" Alias "MoveToEx" ( _
   ByVal hdc As Long, ByVal x As Long, ByVal y As Long, _
   Optional ByVal lpPoint As Long = 0&) As Long

This isn't exactly equivalent to the type library entry because it wouldn't allow you to receive a POINTL argument by reference. With the type library entry you can call the function like this:

MoveTo hDC, x, y, ptPrev  ' Receive last position in POINTL variable

Page 395. The Note just before the "StretchBlt Versus PaintPicture" section says MaskBlt and PlgBlt don't work for Windows 95. They don't work for Windows 98 either.

Page 396. The cmdStretch_Click procedure in the "StretchBlt Versus PaintPicture" section is changed because of the change in painting model described earlier. The new code is:

Private Sub cmdStretch_Click()
   If chkBitBlt.Value = vbChecked Then
      ' Stretch inside out
      Call StretchBlt(hDC, ScaleX(Width, vbTwips, vbPixels) * 0.97, _
                      ScaleY(Height, vbTwips, vbPixels) * 0.9, _
                      -dxBlt * 3.5, -dyBlt * 6.5, _
                      pbSrc.hDC, 0, 0, dxBlt, dyBlt, vbSrcCopy)
      ' Compress backward
      Call StretchBlt(hDC, ScaleX(Width, vbTwips, vbPixels) * 0.75, _
                      ScaleY(Height, vbTwips, vbPixels) * 0.8, _
                      -dxBlt * 0.9, dyBlt * 0.5, _
                      pbSrc.hDC, 0, 0, dxBlt, dyBlt, vbSrcCopy)
      ' This line required in VB6 because of change in painting model
      Refresh
      . . . 

The Refresh statement at the end is now required to make the StretchBlt calls visible.

Page 406, 407. "Using CPictureGlass" fails to mention one minor fact about the class. The third parameter of the Create method is a color that specifies the background color that will be transparent. I don't mention that this color must be a solid color. Windows often combines colors with a process called dithering. If you look closely at a dithered color, you'll see that it alternates two or more colors in adjacent pixels. A background color should not be a dithered color.

Page 418. The "Card Functions" section claims that the Declares and Enums for cards are in Cards.bas on the CD. In fact, my samples use entries from the Windows API Type Library. This was a change from the first edition, but I forgot to correct it in the text. The Cards.bas file is now provided, even though it is redundant. Oliver Reichert reported this bug.

Unfortunately, Cards.bas was necessary for certain types of card games because the Windows API Type library had an incorrect definition for the cdtDrawEx function. If you tried to use it, you got a "Bad DLL calling convention" error. David Mekelburg and Mark DiPippo both reported this bug. The entry is now fixed in the type library.

Page 423. The "No Timer Control" section mentions the CTimer class, but doesn't discuss its implementation. Unfortunately, I've had a lot of trouble with bugs in this class.

The cleanup code for deleting timer objects in the CTimer class from the SubTimer component failed because it was based on a poorly designed algorithm. Steve McMahon identified this problem and sent a solution. My solution is a little different from his, but it now works correctly. I also enhanced the timer sample, TTimer, to prove that timers can be removed successfully.

Next, Andy Hopper downloaded the fixed CTimer control and found another bug. If you destroyed a timer by setting its interval to zero (the way I did it), everything was fine. But if you destroyed a timer by setting it to Nothing (the way Andy preferred), everything wasn't fine, although it might look fine for a while. It's not nice to force others to follow your programming style, especially if you don't document what that programming style should be.

The problem was that the CTimer code kept an internal array of all the timer objects created. For each timer object a user created, there would be two references to that object--an external one known to the user and an internal one stored in the object array. If the user set the external timer to nothing it would go away, but the internal version would still exist. This means that the reference count would never reach zero and the Class_Terminate event--with its code to destroy the associated Windows timer--would never be called. The best you could hope for would be an unused timer wasting memory. But depending on your code, you might get a much worse result.

I discuss problems with reference counting in a different context in Chapter 10, "Reference Counting and Not Reference Counting," page 564-70. This section also describes the solution now used in CTimer. Instead of storing a CTimer object, I store an object pointer created with the ObjPtr function. This pointer won't be reference counted. When I need to use the object pointer (in the timer callback procedure of CTimer), I perform a gross CopyMemory hack on the pointer to create a temporary CTimer object. I use this temporary object to do what needs to be done (create a timer event) and then destroy it with another gross CopyMemory hack. You can see the details in the Timer.bas and Timer.cls files of the SubTimer project. Thanks, Andy, for reporting this bug and for pushing me to do the real fix (change the code) rather than the documentation fix (tell users to follow my programming style or die).

Page 426-27. The AnimateBacks procedure shown in "Animating Card Backs" had a bug. An early exit through a conditional Exit Sub failed to restore the ScaleMode saved at the beginning of the procedure. The results of drawing in twips mode on a form that is still in pixel mode aren't pretty. This is the Case Else block with the correction:

    . . .
    Case Else 'ecbRobot, ecbCastle, ecbBeach, ecbCardHand
        ' Step through animation states
        If cdtAnimate(Me.hDC, ecbBack, _
                      (dxCard * 0.1) + (X * dxCard * 1.1), _
                      (dyCard * 0.1) + (Y * dyCard * 1.1), iState) Then
            iState = iState + 1
            ScaleMode = ordScale
            Exit Sub    ' Don't move to next card until final state
        End If
        iState = 0
    . . .

Chapter 8

Page 437-44. "Using Your Own Resources" describes a method of resource handling that may or may not be obsolete because of the introduction of the resource editor in Visual Basic. To give the new Editor a test, I renamed my TRes sample to TRes2, deleted its resource file, and set about to create an equivalent .RES file with the new resource editor. The results were interesting. My conclusion was that the new editor is not a good tool for creating a program used by Swinish and American users, but it might be OK if the languages you write for actually exist. Even then, however, you'd probably want to organize your resources a little different than I did in the original sample.

Here are some of the things I learned:

  1. I had no trouble adding the American resources, but to my surprise Swinish wasn't in the list of supported languages and the editor wouldn't let me add it. This is probably not a great loss in functionality. Rather than abandon my project, I pretended that Swinish was Swedish. I hope readers of Swedish heritage will forgive this liberty. After making that choice, I was able to go ahead with the resource file. The American version worked fine by default. To test the Swinish version, I ran the Regional Settings applet from Control Panel and selecting Swinish (actually Swedish) as my national language. When I ran the program again in the IDE, it used the Swinish string data, but continued to use the American bitmap, cursor, icon, wave, and custom data. But when I built the EXE and ran that, it used the Swinish data as expected.
  2. The resource editor doesn't provide a way to enter raw binary data in hex format. I did this in my sample, but I doubt it's a commonly requested feature for resource editors. I cheated by putting my "binary" data in text files and reading the files into the resource editor.
  3. The resource editor has a very handy way of entering a string table in which the strings for each language have the same numbers. Unfortunately, this convenient system doesn't work for bitmaps, cursors, icons, and other resources. Why wouldn't I want a multilanguage bitmap or icon table? You can get this behavior despite the editor by giving your resources the same number but different languages. Unfortunately, the user interface gets in your way constantly, assigning bad constants and making you enter the language over and over rather than just once. Finally, the IDE doesn't recognize what you're doing and uses the wrong resources despite the language setting.
  4. The resource editor assumes that you already have your bitmaps, icons, and cursors. If you want to create them on the spot, as in the resource editors of all competing language products, you're out of luck. If Visual Basic can't provide a decent built-in image editor, it should at least provide a setting where you could specify a command line to invoke your own image editor.

Would I use the resource editor in real life? I've never translated string tables into multiple languages for a large program. Since I'd probably have to hire separate translators for each language, it might be easier to have each of them write a separate resource script. Resource scripts also enable you to do tricks with conditional compilation and include files. On the other hand, an interactive tool that doesn't allow invalid entries has its advantages. As long as I'm not set on emulating the rather artificial resource organization used in my sample program, the resource editor might be more convenient.

My real complaint is that this is a crude mapping of C-style resources into a language that deserves better. I don't want to deal with resources. I want to deal with bitmaps, icons, cursors, and string tables. If Visual Basic wants to use resources behind the scenes to make these things work, fine, but I don't want it in my face. For example, when I enter the caption for a control, I want to be able to specify the text for each language right in the Caption property. When I click the Icon property for a form, I want more from those three dots than a file open dialog. I should also be able to create an icon from scratch with a built-in image editor. If I want a different icon for each language, I should be able to create them all in the same place. It shouldn't be my problem how Visual Basic makes all this stuff work so long as the right stuff appears in the right languages.

Page 442. The sidebar "Cursors Eat Mouse Icons for Lunch" states that the ImagEdit program was located in the \Tools\ImagEdit directory of the Visual Basic CD. This was not the correct location for some versions of Visual Basic 5, and it is not correct for any version of Visual Basic 6. In fact, in my version of Visual Studio version of VB6, ImagEdit is so carefully hidden that I thought it was no longer provided. That would make Visual Basic the only Windows development tool that provides no way of editing bitmaps, icons, and cursors instead of the one with the worst image editing tools. Fortunately, ImagEdit does exist, hidden away on CD 3 of Visual Studio. I assume it must also exist on the standalone Visual Basic products. Look for a \common\tools\vb directory that contains all the miscellaneous tools and unsupported samples provided with VB5. Incidentally, for those of you who prefer resource scripts to Visual Basic's resource editor, the resource compiler, RC.EXE, is in the same location.

ImagEdit is better than nothing, but someday Visual Basic ought to make at least a minimal effort to match its competition with a decent integrated image editor.

Page 450. The lstResource_Click procedure near the end of "Retrieving the resource" plays a dirty trick on programmers who want to embed AVI files in resources. Hardcore programmer Ian Boggs wanted to do just that, and he thought he had the solution when he saw this code. The Select Case block in the example has a Case "AVI" followed by a call to a PlayAvi procedure. But when Ian checked the code of for PlayAvi, he found it blank in the middle. It gets a pointer to the resource data, but then doesn't do anything with the pointer. That's because I couldn't find an API function that can play an AVI from a pointer to its data. I spent an hour or so on this when writing the second edition, but when I couldn't solve the problem under my tight deadline, I abandoned the idea and deleted all the incomplete AVI code. Or at least that's what I intended to do.

It turns out that I left the incomplete code as a teaser. Or perhaps as a challenge. Although AVI resources seem logical, they are not very common. In all my testing of different EXE files with WinWatch, I never encountered one with an embedded AVI resource. But it's certainly no problem to put AVI files to your own resources using either the resource editor or resource scripts. Playing them later is a different matter. I saw one magazine article that played MIDI resources (which would probably work the same as AVIs) by writing the binary resource data to a temporary file, playing the file, and then deleting it. If I were driven to a hack that disgusting I certainly wouldn't confess to it here.

Ian did solve the problem of displaying AVI files embedded in resources. I'll let him tell the tale:

"One of my friends is a Delphi programmer, and when I mentioned the problem to him, he smiled smugly and mentioned that Delphi already has a control that does exactly what I wanted. It takes files or resources as its source and plays them. Not only that, it also has, built in, the common AVI files that ship with VB."

"Ten minutes later he had produced an OCX wrapper for the control and exported it. I now have a new animation control that not only takes file names and resources but, with the quick setting of a property, has all the AVI files available that I was using anyway!"

"The down side? Well, the OCX is 350Kb+, a little on the large side. If fact it's almost half the size of the ActiveX server EXE that uses it…"

Visual Basic has a Multimedia MCI control, but it doesn't allow you to load AVI files from resources as the Delphi version does. I'd prefer not to use a control for this, anyway. You can easily play AVI files with API calls, and I still think there must also be an API that will play AVIs from resources or from memory. I just couldn't find it. Maybe you can.

Page 451-52. "Using Resources the Windows Way" mentions the Picture Browser application and shows a picture of it. The Picture browser is also shown in Chapter 11, "The Client Side of File Notification," page 641. Most of the code for this application is not discussed or shown, but some readers found it useful and reported bugs on it. Also note that the VisualCore component has a COpenPictureFile class that creates a dialog very similar to the Picture Browser application. Although not mentioned in the book text, COpenPictureFile is demonstrated in the Hardcore.vbg project. When you're dealing with graphic and sound files, it's a good alternative to the VBGetOpenFileName function discussed in Chapter 9, "Using Common Dialogs," pages 511-17.

Unfortunately, the Picture Browser program ignored GIF and JPG files, even though Visual Basic knows how to display them. Edward Armelino was the first to notice this bug. The COpenPictureFile class in the VisualCore component is based on similar code and had some of the same problems.

These limitations were caused by two separate bugs. First, some of entries in the file types combobox did not include GIF and JPG files, so they wouldn't be listed even if they existed. This has been fixed by adding *.GIF and *.JPG to the appropriate file patterns.

After fixing the patterns, GIF and JPG files were listed, but their pictures still weren't displayed because of a separate bug. The UpdateFile sub in the section where bitmap files are handled opened picture files with the LoadImage API instead of with VB's LoadPicture. But LoadImage doesn't know anything about GIF or JPG files. The code has been changed to use LoadPicture for .GIF and .JPG files. Note that although VB can read these files, it converts them into BMP files and cannot write them back in their original format.

VB6 documentation says that Visual Basic recognizes the following graphic formats: bitmap (.BMP) files, icon (.ICO) files, cursor (.CUR) files, run-length encoded (.RLE) files, metafiles (.WMF), enhanced metafiles (.EMF), GIF files, and JPEG (also called JPG) files. In addition, it recognizes .DIB files. The .DIB extension (abbreviation for Device Independent Bitmap) is rarely used; most bitmaps in this format use .BMP. The .JPEG extension is extremely common, especially in files originating on operating systems that never had a three-character limit on extensions. I have fixed the Picture Browser and the COpenPictureFile class to recognize all these extensions. I've never seen a .RLE file (I suspect run-length encoded files are also stored in .BMP files), so I wasn't able to test this format, but the others work.

Page 456. The BitmapToPicture function shown in "Handle to picture and back" has been changed, as have the similar IconToPicture and MetafileToPicture functions. The GUID is now a private module-level variable initialized in Class_Initialize using the IIDFromString function. This is a cosmetic change that doesn't effect the behavior, so I'm not showing the code here.

Page 461, 62. The "Palette tricks" section was written on faith several weeks before I actually solved the problem of palettes. In fact, the CPalette class and the TPalette test program were finished in the wee hours of the morning of the day we shipped the second edition. A few additional comments made it into the ReadMe, but to really learn what I know about palettes, you had read comments in the code.

What I wrote for the book wasn't too bad, but it was wrong in a few details. Subclassing is mandatory, not optional, and what I said about the AutoRedraw property is irrelevant and wrong.

The TPalette example consists of a big form covered by a colorful bitmap of a painting by the Japanese artist Katsushika Hokusai. It has some controls that allow you to pick a method of cycling through the palette colors, or to open your own bitmap file to rotate its palette. There's no point in showing a screen dump, because dynamic movement is the point.

The code works by creating a CPalette object, which fills an array of colors representing the color palette of a selected bitmap. Since VB doesn't have constructors, CPalette has to rely on a convention--you must call the Create method before any other.

' Create the palette and initialize the color array
cPal = pal.Create(Picture.hPal, hWnd, aColors, iFormFrom, iFormTo)

The first two parameters are the palette to be manipulated and the handle of the window it will be displayed on. The third parameter is an unsized array of OLE_COLOR. The Create method will size and fill it. If you have inside information about the size of the palette and any colors that should be reserved, you can pass the first and last valid palette entries in the last two parameters. If you don't know about the palette, pass 0 and -1 and the Create method will fill these reference parameters with its best guess. The Create method returns the number of palette entries. By experimenting with the settings of a bitmap in the TPalette program, you can determine the best parameters to pass to the Create method in your own programs.

By the way, you should trap errors from the Create method. Otherwise, you may get the same rude surprise I got when I tried to run TPalette on a system with a very high color resolution. I got an unhandled error telling me the bitmap had no palette--even though it does with most graphics adapters.

Once you have a valid palette array that has been processed by CPalette, you can manipulate it by changing the color entries. The PalTool module has one palette manipulation tool, RotatePaletteArray, that you can use as a model. It rotates a given palette array by one position. You could write similar procedures that fade in or out by mathematically reducing or increasing intensity of each color, one step at a time. After changing the palette colors, you must call the ModifyPalette method to make your changes stick.

' Index from ECycleDirection enum: left, right, inside out,
outside in
RotatePaletteArray aColors, Index
pal.ModifyPalette aColors

If you want to understand how it works, you'll have to study the CPalette code and the Win32 API documentation on palette functions--GetPaletteEntries, SetPaletteEntries, RealizePalette, and SelectPalette. You may also want to read up on the WM_PALETTECHANGED and WM_QUERYNEWPALETTE messages. CPalette implements the ISubclass interface described in Chapter 6, "Encapsulating Message Traps," page 349-362. It attaches these two messages and processes them in a window procedure. This takes only a few lines of code, but I'd hate to guess how many hours it took me to write those lines. You, on the other hand, can ignore the implementation of CPalette and concentrate on writing cool palette manipulation procedures.

By the way, if the naming conventions, captions, or coding style in this example don't entirely match my normal style, it's probably because solving the palette problem was a joint effort by me and former VB documentation guru Glenn Hackney. VB developer Yann Christensen answered our questions about VB's implementation of palettes. Glenn's departure from Microsoft after VB5 shipped is one of the reasons that VB6 documentation has gone downhill so rapidly.

One other point about CPalette. The error IDs in the VBCore.rc supplied with the second edition didn't match the corresponding error constants in CPalette's error enumeration. The problem has been fixed in this update.

Page 464-68. When Søren Bødker reported that he couldn't get a large icon to display in the TIcon project described in "The New Windows icons versus Visual Basic icons," I didn't believe it. I knew I had tested the program on several machines when I wrote the program, and in eight months since publication, no one else had complained. Unfortunately, when I tried TIcon myself, I found that it didn't work on either of my machines (one Windows NT, one Windows 95 at the time, now Windows 98). I was mortified when I discovered the program had not one, but several stupid bugs.

The first possible problem is that TIcon loads the file i.ico and expects it to be in the current directory. This would be the directory where the Hardcore VB files were installed by setup, but if your Visual Basic IDE is set to start in another directory, the file will not be found. You can fix this by adding the following line to the start of Form_Load:

ChDir App.Path

Not finding files stored in the startup directory is a common problem with an easy solution. Fortunately it doesn't happen very often with compiled programs that are launched from a shortcut or from Windows Explorer. Still, it's a good idea to add the statement above to any program that needs to find a specific data file in its startup directory. Better yet, store the data in a resource rather than in a file.

The second possible problem is that you might get the message "The system cannot find the file specified" when you hit the following line in the TIcon form file:

Set img(eipShell).Picture = LoadAnyPicture("i.ico", eisShell)

To find the bug, I needed to step into LoadAnyPicture in the VBCore component. To debug VBCore, I needed to load TIcon.vbg instead of TIcon.vbp. But when I tried it, I found that the TIcon.vbg on the CD was wrong. I can't imagine how a VBG file full of garbage got past our original testing, but TIcon.vbg has been corrected in the current update.

After fixing the VBG file, I could step into LoadAnyPicture in PicTool.cls. The failure comes with the constant eisShell, which gets the icon size set by the shell (the one that you can change by modifying the icon size in the Appearance tab of the Display Properties dialog). The bug occurs when LoadAnyPicture (shown on page 468) calls the GetShellIconSize function (mentioned on page 466) in the same module:

Function GetShellIconSize() As Long
#If 1 Then
    ' Grabbing size out of registry works, but might change
    Const sMetrics = "Control Panel\Desktop\WindowMetrics"
    GetShellIconSize = MRegTool.GetRegStr(sMetrics, "Shell Icon Size")
#Else
    ' Recommended way of getting size doesn't work until after login
    Dim hImlst As Long, fi As SHFILEINFO, cx As Long, cy As Long
    hImlst = SHGetFileInfo(".", 0, fi, Len(fi), _
                           SHGFI_SYSICONINDEX Or SHGFI_SHELLICONSIZE)
    If ImageList_GetIconSize(hImlst, cx, cy) Then
        GetShellIconSize = cx
    Else
        GetShellIconSize = -1
    End If
#End If
End Function

Comments in this function indicate that there are two ways of getting the shell icon size. The first technique involves getting the size from the registry. The comment notes that this works now, but is an undocumented technique that might change. Apparently it's undocumented with good reason. The "Shell Icon Size" setting doesn't exist in the registry on either of my machines, although it certainly existed on my test machines when I wrote the book.

The other annoying thing is that Windows reports the error as API error 2: "The system cannot find the file specified". This is a pretty dumb error to return to indicate that the RegQueryValueEx could not find something in the registry.

You can switch to the second method by changing the "#If 1" at the top of the function to "#If 0". The second technique involves getting the size using SHGetFileInfo, but as the comments indicate, I discovered some situations in which this didn't work correctly, either. I wrote those comments a long time ago and now I don't remember exactly what I meant or what caused the problem, but I was reluctant to switch to the other method. So I fixed the problem with this code instead:

#If 1 Then
    ' Grabbing size out of registry works, but might change
    Const sMetrics = "Control Panel\Desktop\WindowMetrics"
    On Error Resume Next
    GetShellIconSize = MRegTool.GetRegStr(sMetrics, "Shell Icon Size")
    ' If size isn't in registry, assume the default size
    If Err Then GetShellIconSize = 32

I trap the error and give the icon the default size of 32. My theory is that by default the shell icon size is 32 and there is no "Shell Icon Size" entry in the registry. If a user changes the shell icon size, an entry is made to the registry. Since I changed the default size on my test machines (as suggested on page 464), I never encountered a registry with no entry. Even if there were machines with no registry entry but a size other than 32, my code wouldn't fail, although it might return the wrong size.

As far as I can tell, the bottom line is that Windows provides no reliable way to get the shell icon size. This is pretty dumb, but since I didn't do my testing very well, I guess I'm in no position to complain.

Page 467. VB6 has enhanced the LoadPicture function so that on paper it appears to have even more features than the LoadAnyPicture function described in "A better LoadPicture." Unfortunately, as far as I can tell, most of these wonderful new features work only intermittently.

I enhanced the TIcon project to take advantage of all the new LoadPicture optional parameters that allow you to specify the size and color depth of icons and cursors. The bottom part of the sample still illustrates LoadAnyPicture, but the top part shows LoadPicture with combo boxes where you can change the size and color depth parameters and text boxes for X and Y sizes. You can try every possible combination for yourself. I got different results depending on whether I had changed the shell size of icons in the Appearance tab of the Windows Display Properties dialog.

Here's the magic version that always displayed small icons regardless of the setting:

Set pb.Picture = LoadPicture("i.ico", vbLPCustom, vbLPDefault, 16, 16)

From reading the documentation, you would expect this statement to always show a small icon:

Set pb.Picture = LoadPicture("i.ico", vbLPSmall)

Sometimes it does. Sometimes it doesn't. Nor does the following statement consistently show the shell icon size as the documentation claims:

Set pb.Picture = LoadPicture("i.ico", vbLPLargeShell)

Perhaps they've got something else in mind for the LoadPicture function that they're not telling in the documentation. Perhaps it would be of more value if you had an icon with 15 images with different sizes and color depths. Fortunately, you'll have a hard time finding such icons. In the mean time, use LoadAnyPicture.

By the way, if LoadPicture is worth enhancing, why isn't LoadResPicture worth enhancing in the same way? But given the quality of the LoadPicture enhancements, it's just as well they forgot LoadResPicture.

To continue beating a dead horse, why did they make all the new arguments of LoadPicture Variants? Why not use the LoadPictureSizeConstants and LoadPictureColorConstants enums for the second and third parameters so that you could see the constant values in the editor?

Finally, for a good laugh, click the Example button in the LoadPicture help topic. Where would they find a writer who illustrates LoadPicture by loading a cursor file into a PictureBox?

Page 469. The LoadAnyResPicture function has serious problems in addition to the ones mentioned in "A little better LoadResPicture." The function does not work when used from the VBCore DLL component. The problem is that when you call from your application to LoadAnyResPicture in VBCore, the function looks for your resource in the component rather than in the calling application. Of course, your resource is unlikely to be in VBCore, and you'll get an error message.

The reason LoadAnyResPicture assumes the DLL rather than the application is because it passes App.hInstance to the LoadImage API, but the App.hInstance is for the DLL rather than the application. If you use LoadAnyResPicture by including the PicTool.bas module (from the LocalModule directory) in your project, the App.hInstance used will be the one in your application and everything will work fine. You can also cut and paste the function into your own code (as long as the functions and constants it uses are available).

Confusion over when and how you can use LoadAnyResPicture could have been avoided by putting it in a conditional compilation block:

#If fComponent = 0 Then 
Function LoadAnyResPicture(. . .) As Picture
. . .
#End If

This would have enabled the standard module version, but prevented the invalid DLL version from being compiled. Unfortunately, it's too late to do this now because the buggy LoadAnyResPicture has already been shipped and is thus part of the public interface even though it doesn't work. So I have fixed the code to do this:

Function LoadAnyResPicture(. . .) As Picture
#If fComponent Then
    ' Implementation is here
#Else
    BugAssert True, "Can't use from DLL"
#End If
End Function

It didn't work before and it still doesn't work, but now the assert tells users why.

Page 479, 80. The CVersion class described in "CVersion class implementation" didn't work for certain executable files. Several readers reported variations of this problem, including Eugene Lewis, Gerry Thomas, and Martin Naughton. Martin did more than report the problem; he identified part of the cause.

Since I didn't show or discuss the code in the book, I won't get into detail about the fixes. The problem Martin pointed out is that at one point I asked the system for a block of language data, and then assumed I could copy that data into the four bytes of a Long. This worked fine in most cases, because the data is stored in four-byte chunks and there is usually only one chunk. But on files that stored multiple language blocks, my code copied that extra data into random memory. So my choice was either to extend CVersion so that it could handle multiple language blocks, or to modify my code to copy only the first block. I took the easy way out. Martin extended CVersion to handle multiple languages and sent me the code, but I didn't have time to integrate it into VBCore for this update.

James Musial had another excellent suggestion--he said that I should read the version data into a byte array rather than into a string. The CVersion class was written for VB4 and I didn't change it for VB5. I can't remember my original rationale for reading the data into a string. I'm sure it worked fine under 16-bit Windows, but reading binary data into Unicode BSTRs in 32-bit VB is risky business that probably contributed to occasional failures in the original.

Unfortunately, even after both of these bug fixes, CVersion sometimes fails to extract version data that you can get successfully from the Properties dialog in Windows Explorer. I spent several hours trying to figure out why, but never did. It looks right. I think I'm making all the right calls in the right format. But the data I expect doesn't always come back.

Chapter 9

Page 486 or thereabouts. Edwina's sizing logic in Form_Resize was wrong. Neil Pelletier pointed out that resizing the editor too small caused an "invalid procedure" error. It turned out if you sized the window below a certain size, my code passed a negative number to the Move method. I fixed the bug by carefully rethinking the logic and adding flags to prevent inappropriate recursion, but this problem raises other interesting questions.

For example, how small is too small? The XEditor has sizing logic that prevents it being resized too small, but the smallest reasonable size for an XEditor control is a lot smaller than the smallest reasonable size for an editor with a status panel and toolbars. Once I chose my minimum size, I could have enforced it more smoothly with the CMinMax class described in Chapter 6. I took the shortcut of detecting small sizes and resizing to the minimum.

Another question is, should I have a global error handler? No matter how well I test (and I apparently didn't test very well), an error could slip through. This is one of the advantages of writing samples for a book rather than writing production code. I hate bugs, but a problem in Edwina isn't as serious as a problem in your new commercial word processor.

Page 489. The timer hack shown in "Initialize It" does not appear in the XEditor code, nor in the code for any other control provided with my book. I know I wrote this code at one point, based on suggestions from VB5 designers and documentation writers. I don't remember how it disappeared from my code and stayed in my book. The problem described doesn't appear to happen in VB6, but I can't be sure.

Page 491. The last paragraph of "Initialize It" says that the XEditor control has a dependency on VBCore.dll. This is no longer true for the reasons described earlier.

Page 494. I'd like to add some further comments on the "Wizard or Secretary" sidebar. One of the problems with the Visual Basic wizards is that they use the step-by-step wizard model originally designed for unsophisticated users of applications. You want a spreadsheet for analyzing the migration patterns of Arctic Terns? The Wildlife Migration Wizard will take you through the steps. Users of this type of wizard are interested in the result, not the process.

Application development is more complex. The process is always important, and you don't do design just once. It's not a step-by-step operation. That's why the sophisticated wizards in other languages work two ways. They generate code from a visual interface, but they also generate a visual interface from code. You take a first stab with the wizard. Then you make some revisions in the code. Then you go back to the wizard and see the results of your code changes.

Visual Basic invented this model with its property window in VB1. The idea was refined with property pages in the ActiveX control architecture. Other languages couldn't just generate property data behind the scenes the way Visual Basic does, so they invented wizards that generated code to manipulate properties. Now Visual Basic is imitating the imitators.

Some of VB's wizards shouldn't be wizards--for example, everything the Toolbar wizard does just once should be done as many times as necessary in property pages. Other wizards do useful code generation, but they follow an inappropriate one-time-only model that doesn't match the normal work methods of real programmers.

Page 496. The FilePath property shown in "Replace It" has a typo. It incorrectly assigns to FileName instead of FilePath. The code should be:

Property Get FilePath() As String
    FilePath = sFilePath
End Property

Page 499, 50. The DirtyDialog function shown in "The Dirty Bit" has a subtle bug reported by Rick Cardarelle. The problem is in the FileSave method called when the user clicks Yes to save the dirty file. FileSave looked like this:

Sub FileSave()
    If FileName = sEmpty Then
        FileSaveAs
    Else
        SaveFile FileName
    End If
    DirtyBit = False
End Sub

The problem is that if the user opened the file with a full path, the SaveFile statement saves the FileName (just the basename) in the wrong directory. I fixed this problem for the update by saving the whole file path:

        SaveFile FilePath

Page 501. The TextMode Property Let in "Text and Rich Text Modes" has been enhanced to strip out formatting when you switch from Rich Text to Text mode:

 
Property Let TextMode(ByVal fTextModeA As Boolean)
    ' Change to TextMode removes formatting
    If Not fTextMode And fTextMode <> fTextModeA Then
        ' Remove all formatting
        With txt
            Dim i As Long, c As Long
            i = .SelStart
            c = .SelLength
            .SelStart = 0
            .SelLength = Len(.Text)
            .SelText = .Text
            .SelStart = i
            .SelLength = c
        End With
    End If
    fTextMode = fTextModeA
    PropertyChanged "TextMode"
End Property

This change automatically sets the dirty bit, so there's no need to set the DirtyBit property as shown in the book.

Note: The TextRTF Property in "Text and Rich Text Modes" had a bug in the second edition of Hardcore Visual Basic that survived into the code for this update. John Cullen reported this bug after the update was published. I can't fix the code because I don't use VB anymore, but I can add this note to the text. The code for TextRTF property wasn't shown in the book text because it was too simple to have any mistakes. Nevertheless, it had them, and as a result calling the property in the XEditor control caused ugly crashes. Here is the corrected code:

 
Property Get TextRTF() As String
    If Not TextMode Then
        TextRTF = txt.TextRTF
    ' Else return empty
    End If
End Property
 
Property Let TextRTF(sTextRTFA As String)
    If fTextMode = rtfRTF Then
        txt.TextRTF = sTextRTFA
    ' Else ignore
    End If
End Property

The original Property Get had the sense of the logical test reversed. The Property Let attempted to assign to TextRTF rather than txt.TextRTF.

Page 502. The RightMargin property mentioned in "The text file format," as well as the SelTabs property required the control container to use Twips as its ScaleMode. If the container sets its ScaleMode to another mode such as Pixels, setting these properties had unexpected results. When Tim Fischer reported the problem, I suggested that as a workaround he could convert between the container's ScaleMode and Twips. This is not very polite behavior for a control. I've fixed it for RightMargin and SelTabs, but I'm not swearing that all properties deal with this problem correctly. Here's the RightMargin property as an example of how you can use ScaleX to handle the problem:

Property Get RightMargin() As Single
    RightMargin = ScaleX(txt.RightMargin, vbTwips, Parent.ScaleMode)
End Property
 
Property Let RightMargin(ByVal rRightMargin As Single)
    If rRightMargin < 0 Or rRightMargin > 65535 Then
        rRightMargin = 65535
    End If
    txt.RightMargin = ScaleX(rRightMargin, Parent.ScaleMode, vbTwips)
    PropertyChanged "RightMargin"
End Property

Incidentally, while testing this fix, I discovered that my LeftMargin property has no effect whatsoever. It calls SendMessage with the EM_GETMARGINS or EM_SETMARGINS message. I'm not sure if a fix is possible. The RichTextBox control doesn't support setting the left margin, and maybe the authors know something I don't know about why. I couldn't remove the LeftMargin property without breaking compatibility, but I did comment out the ineffective code to make it a dummy property.

Page 504 or somewhere thereabouts. When discussing XEditor properties, I didn't bother to mention or show the EditCopy, EditCut, and EditPaste commands because the implementation seemed too obvious. Besides, I always use Ctrl+Ins, Ctrl+Delete, and Shift+Insert to copy, cut, and paste. Perhaps I never tried the Copy, Cut, and Paste from the menu or toolbar, or if I did try them, I didn't do it in Rich Text mode with formatting. But David Harris did. He found that my commands copied the text, but not the formatting.

My code used the Clipboard object to cut, copy, and paste, but it didn't specify the clipboard format. I didn't study the Clipboard.SetText method and didn't notice that it has an optional parameter where you can specify formats, including RTF. From a reading of the documentation, it looks like you should copy in text or RTF format with code like this:

Sub EditCopy()
    ' Copies text or text and formatting, but doesn't work
    Clipboard.SetText txt.SelText, IIf(fTextMode, vbCFText, vbCFRTF)
End Sub

Unfortunately, this doesn't work on the RichEdit control. I suspect that the RichEdit control has its own cut-and-paste system that is independent of the Clipboard. In any case, you can copy and paste formatting by sending messages to the control window. Here is the new code:

' Cut, copy, paste, and delete methods
Sub EditCopy()
    ' Copies text and formatting
    Call SendMessage(txt.hWnd, WM_COPY, ByVal 0&, ByVal 0&)
End Sub
 
Sub EditCut()
    ' Cuts/copies text and formatting
    Call SendMessage(txt.hWnd, WM_CUT, ByVal 0&, ByVal 0&)
End Sub
 
Sub EditPaste()
    ' Pastes text and formatting
    Call SendMessage(txt.hWnd, WM_PASTE, ByVal 0&, ByVal 0&)
End Sub
 
Sub EditDelete()
    ' Deletes text and formatting
    Call SendMessage(txt.hWnd, WM_CLEAR, ByVal 0&, ByVal 0&)
End Sub

But even before I made this fix, I realized that I had other cut and paste problems. Everything worked fine when I used Ctrl+Ins, Ctrl+Delete, and Shift+Insert, but it failed when I used the menus or buttons. Apparently the underlying RichEdit control intercepts and handles the shortcut keys internally. It also handles the Ctrl+X, Ctrl+C, Ctrl+V, and Del shortcuts, which led to another weird result. Since I had included these as menu shortcut keys, pressing them resulted in operations being done twice--once by the system and once by my command.

To make Edwina handle the cut and paste commands just once, I had to take them off the menu shortcuts, but that left the user with no visual clue that the shortcuts were available. I solved this problem by putting a tab followed by the shortcut key name after the command name in the menu captions. The control keys appear to be installed as shortcuts, but they are just there as part of the menu text. Unfortunately, the VB menu editor provides no way to enter a tab in a caption, so I had to add the captions in Form_Load.

Incidentally, the underlying control also handles the Ctrl+Z (Undo) and Ctrl+A (Select All) commands, so I had to fix them on the menu in the same way.

Page 504 or somewhere thereabouts. I didn't discuss drag and drop anywhere in the book because I delegated all the related methods and events to the RichTextBox control. So I was surprised when Carlos Gonzalez sent me a report saying that he got a type mismatch followed by a series of random errors (including crashes in some tests) when he tried to drag and drop text in Edwina. But when I looked at the code reporting the type mismatch, there didn't seem to be much room for error.

Private Sub txt_OLEStartDrag(data As RichTextLib.DataObject, _
                             AllowedEffects As Long)
    RaiseEvent OLEStartDrag(data, AllowedEffects)
End Sub

What could go wrong with that? Well, I saw the answer when I looked at the event declaration.

Event OLEStartDrag(data As DataObject, AllowedEffects As Long)

The event declaration used type DataObject, but the event procedure used type RichTextLib.DataObject. So what's the difference between these two types? The two names are aliases for the same type, so why would VB care about the wording? And how did they get in XEditor in the first place? This code was generated by a wizard, so why doesn't it work? I can't answer any of those questions. All I know is that everything worked fine when I added RichTextLib to the type in the OLEStartDrag event declaration and in several related declarations that have the same error.

I've experienced this problem in other contexts. It looks like Visual Basic qualifies most control-specific types with the name of the control type library. For example, most of the common controls that have an OLEStartDrag event will generate one that looks like the event procedure above, except that the qualifying library may be MSComctlLib or MSComCtl2. But there is one exception. The Coolbar control generates event procedures without the type library name:

Private Sub cool_OLEStartDrag(Data As DataObject, _
                              AllowedEffects As Long)
. . .

Most of my code created with VB5 doesn't have the type library name (for reasons long lost from memory), but sometimes when I had the build problems described earlier, I would lose a control and have to put a new copy on the form. When that happened, the new version would appear with the type library name in event procedures, making them incompatible with any code that used the unqualified type names. To say this is disconcerting and confusing is an understatement. It can take hours to track down this kind of problem if you don't know what's happening. I hope my pain can prevent yours.

I also discovered the hard way that the library name for VB5 common controls is ComctlLib, but the name for VB6 controls is MSComCtlLib. This caused me all sorts of problems when trying to move my VB6 code to VB5.

Page 509. You may have trouble finding the Microsoft Dialog Automation Objects component (GLDOBJS.DLL) mentioned in "Common Dialog Extensions." The Unsupprt directory is buried on CD 3 in \common\tools\vb on the Visual Studio release discs. It may be easier to find on the standalone Visual Basic products. I expressed the hope that this component would be enhanced and made a supported part of Visual Basic, but no such luck in VB6.

Page 512. The FileOpen function shown in "Using Common Dialogs" has been changed to improve its handling of default extensions. The DefaultExt and FilterIndex parameters are given as shown below. The same changes were also made to the FileSaveAs function.

Function FileOpen() As Boolean
    Dim f As Boolean, sFile As String, fReadOnly As Boolean
    f = VBGetOpenFileName( _
            FileName:=sFile, _
            ReadOnly:=fReadOnly, _
            Filter:=FilterString, _
            DefaultExt:=IIf(TextMode, ".txt", ".rtf"), _
            FilterIndex:=IIf(TextMode, 1, 2), _
            Owner:=hWnd)
    If f And sFile <> sEmpty Then
        TextMode = Not IsRTF(sFile)
        LoadFile sFile
        If fReadOnly Then Locked = True
        FileOpen = True
    End If
End Function

Page 513. The Challenge in "The OPENFILENAME UDT" was met by Don Bradner. He published his customized file dialog in the January 1998 issue of Visual Basic Programmer's Journal. Don saw the challenge and sent me mail saying he had met it, but I think he had the idea long before he read my book. His dialog uses the hook and template fields of the OPENFILENAME UDT to create a dialog that displays a preview window for graphics files. Apparently, this looks like a very fancy version of the dialog in my COpenPictureFile class, but its implementation is very different. Don extends the system's dialog rather than creating his own. Among the hacks necessary to make this work is creating and attaching a resource dialog template just the way those poor C++ suckers have to do for all their dialogs. Congratulations on an impressive hack, Don.

Page 521. The Sidebar "Search and Edit in Different Universes" refers to the FSearch form in HardControl, but this unexplained reference is the only mention of HardControl in my book or CD. What the heck is it? Well, originally I was going to put all my controls--XEditor, XDropStack, XListBoxPlus, XColorPicker, and XPictureGlass--in one control project called HardControl. There are some things to be said for packaging related controls in one file, and some things to be said for packaging each separately. Eventually I decided on separate packages, but one reference to the old packaging scheme slipped through.

Page 508. The Flame near the end of the "Status Methods and Properties" section claims that you can't right-justify integers with Visual Basic's formatting features. Well, yes, you can, sort of. I suggest the following in the book:

Right$(Space$(iWidth) & iVal, iWidth)

That's fine as far as it goes, but then I said, "Just try doing that with the Format function…" So Karl Westberg did try it and succeeded with this code:

Format$(iVal, String$(iWidth, "@"))

Well, I'll be damned. I'm embarrassed, but not too embarrassed. It's pretty hard to figure this out from reading the VB documentation. The @ is a string formatting symbol, and there's no mention of it in the numeric formatting section of the help. Furthermore, it doesn't seem to be usable with other common numeric elements such as zero, period, and comma. So I'd still have trouble right justifying 1,743,534 and 14,578.32. Of course that limitation also applies to my solution.

In a followup, Karl pointed out that you can right-justify with commas and decimals using code like this:

Format$(Format$(iVal, "#,##0.00"), String$(iWidth, "@"))

Or:

Right$(Space$(iWidth) & Format$(iVal, "#,##0.00"), iWidth)

He tested these two versions for speed and found the second to be much faster.

I still think Format$ is pretty weak in comparison to formatting in other languages, but that doesn't excuse my getting it wrong in the flame.

VB6 adds several new formatting functions, including FormatNumber. Alas, it offers no help. FormatNumber, FormatDateTime, FormatCurrency, and FormatPercent seem to have come from VBScript, which has no Format$ function (probably because Format$ uses a Basic-specific syntax rather than a standard procedure syntax). They certainly offer no new alignment features and provide little new value over Format for the Visual Basic programmer. I've heard, however, that they are faster than Format$.

Page 532. I warned in "Finding and replacing text" that searching is subject to bugs because it is such a tedious programming task. I didn't want to be a prophet, but it looks like I am. Cesar Labitan reported that my FindNext didn't find the next string if the string had only one character. Sure enough, I had an off-by-one error. Visual Basic search strings are 1-based. The SelStart property is 0-based. I failed to adjust them properly. I believe I've got them right in the update.

I also had to adjust my searching code to take advantage of the new VB6 InStrRev function. Since VB5 had no backward search function, I wrote my own InStrR function using a crude brute force algorithm. I foolishly modeled the signature of my function on the weird signature of InStr, which unlike any other function I know of, allows you to skip the first optional argument, thus moving all the other arguments one place to the left. Fortunately, VB wisely abandoned this design with InStrRev and put the position argument at the end where it belongs. I left the old InStrR in VBCore for compatibility, but it's just a wrapper for InStrRev. I don't call InStrR anymore; all my search code has been changed to call InStrRev directly. The excellent performance of InStrRev is noted in Appendix B.

To enable it to work with VB5, I had to keep the old InStrR version in a conditional block and write an InStrRev using the same technique. VB5 users will just have to live with the bad performance. There's no InStrRev for VB5, but I modified my InStrR code to create one. InStrRev is defined in a conditional block (#If iVBVer <= 5) in the new VBFuncs.bas module.

VB6 has a new Replace function, but after a preliminary look I decided it wouldn't fit in with my system. Or at least that's my excuse. Actually, I just didn't want to mess with working code.

Page 533. The old controls such as KEYSTA32.OCX mentioned in the Note in "The Keyboard Object" may be buried in some obscure place on your Visual Basic CD. The location varies depending on which version you have. If you're lucky, you won't be able to find them.

Chapter 10

Page 551 and 556. The Active Template Library has changed a lot since I wrote the SieveATL server described in the "C++ Sieve Servers" sidebar and tested a few pages earlier in a Performance sidebar. For one thing, the name of the library has changed from ActiveX Template Library. For another, the library code has been enhanced and the ATL wizard has been improved. I didn't think it would be fair to continue testing with my old version created with Visual C++ 4.2 and ATL 1.1.

I couldn't find a tool for porting old ATL code, so I had to recreate a new server with the wizard, and then change the code, the GUIDs, and the procedure IDs to match my old ones. After all that, the minor improvements in performance were barely measurable. That's not much of a surprise since the sieve doesn't do much that would be affected by new features in ATL or in the VC++ 6.0 compiler. Incidentally, the SieveATL server works on my computer, but I've had trouble registering it on some other systems. I hope it works on yours.

I checked MFC documentation to see if MFC had any new features that would change my SieveMFC server, but there didn't appear to be any except the new possibility of putting ATL classes in an MFC server. There would be no point in that for this sample, so I recompiled the old version with VC++ 6.0. It compiled, but it wouldn't run under the Sieve Client program for reasons unknown. This may have been my fault, and perhaps I could have fixed it with enough time. MFC has an incomplete server registration mechanism that I couldn't get to work consistently on different systems. Finally I gave up and commented SieveMFC out of the SieveCli program. The whole point of the MFC server was to demonstrate that you shouldn't write servers with MFC. If you don't want to take my word for it, you can try registering the old SieveMFC and putting it back in the program.

Although the book doesn't discuss them, the Performance sidebar shows timings as global functions in DLLs. Unfortunately, I broke the compatibility of the native and p-code DLLs containing the functions. That's because I made the mistake of giving the function the same name, SieveGlobal, in both DLLs. Under VB5 I was able to use these functions by qualifying the function name with the component names. That didn't work under VB6, so I changed the function names to SieveGlobalP and SieveGlobalN. I'm not saying VB6 is wrong, although its behavior does seem a little strange. It was a mistake on my part to give these functions the same name, so I'm in no position to cast stones (although that hasn't stopped me in other situations). If you installed the second edition CD, you should unregister the old SieveBasGlobalN.dll and SieveBasGlobalP.dll with RegSvr32.exe. I try to do it in the BuildAll batch file, but can't be sure of the results because I don't know what directory you installed in.

Page 557. "The XSieve Control" explains by example why you shouldn't make the sieve a control. Someone who cared more than I do about different levels of inappropriateness could add a new sample showing how the new windowless controls compare to windowed controls. I have no doubt that a windowless control would be faster, and I'm mildly curious about how much faster. But not curious enough to write the test myself.

Page 559. I talk about the GUID type in "The Name of the Class" and other sections in Chapter 10 as if this were a legal name for a type. Unfortunately, it isn't.

The GUID problem occurs because VB's standard type libraries define incompatible entries named GUID. To see what I'm talking about, load the object browser, right click in the main window, and select Show Hidden Members from the context menu. Search for GUID. You'll find two entries. The first is a GUID UDT in the STDOLE type library, but if you examine the Data1, Data2, or Data3 members, you'll find the type is "unsupported variant type." This means unsigned long, a C type that VB can't recognize. The STDOLE library is written by and for C programmers and is incompatible with VB. Since STDOLE is a required part of VB, this illegal definition will override any legal definition you put in a type library. The other incompatible definition is a Guid Sub in the hidden __MSJetSQLHelp module of the VB type library. I have no idea what this is or whether it causes a problem, but I can say that giving the name Guid to a Sub, hidden or not, is a dumb idea.

The bottom line is that you can't define a GUID type in a type library. Unfortunately, I didn't completely understand this problem when I wrote the type library for the second edition. The old version had separate CLSID, UUID, and IID structures that were compatible with VB, but unfortunately they weren't used consistently. For example, the CLSIDFromString used the illegal GUID type, while the StringFromCLSID function used the legal CLSID type.

The updated Windows API Type Library fixes the problem, but in a way that could possibly break some existing code. I've defined a single GUID structure called UUID. UUID (Universally Unique ID) is a common synonym for GUID (Globally Unique ID) on some systems. In the type library I use UUID as an alias for CLSID (a GUID for a class and IID (a GUID for an interface). In the previous version I had separate CLSID and IID types. These aliases are recognized as equivalent in IDL, but unfortunately they don't come through as aliases in Visual Basic.

This could cause a minor problem for anyone who used the IID or CLSID types from my previous type library. I had to change code in a few source files to use UUID consistently, and some users might have to do the same.

While dealing with the GUID/UUID problem, I experimented with the Win32 functions that convert GUIDs between string and binary formats. The most useful of these for Visual Basic programmers are the ones that convert from strings to binary GUIDs. Entering the parts of a GUID in each field of a UUID type is tedious and prone to errors. Usually when you need to create a GUID variable, the documentation for the class or interface in question will show the GUID in a string format that looks like this:

{c200e360-38c5-11ce-ae62-08002b2b79ef} 

It's easier to initialize the GUID from the string format or, in the case of a CLSID, from the Program ID format (the "server.class" format recognized by VB's CreateObject). Here are the signatures for the Win32 functions in Basic syntax:

Sub CLSIDFromString(sGUID As String, clsid As UUID)
Sub IIDFromString(sGUID As String, iid As UUID)
Sub CLSIDFromProgID(sProgID As String, clsid As UUID)

For example, the FoldTool module needs an interface ID for IShellFolder. In the last version, I initialized this GUID one field at a time. In the updated version I initialize from the string:

' Initialize IShellFormat GUID constant from string
IIDFromString "{000214E6-0000-0000-C000-000000000046}", iidShellFolder

You can also convert from GUIDs to strings, although Visual Basic programmers will find less reason to do so. The conversion functions are unnecessarily complicated because of their use of pointers. I have added Basic-friendly wrappers to the Utility module. Here are the signatures:

Function CLSIDToString(clsid As UUID) As String
Function IIDToString(iid As UUID) As String
Function GUIDToString(uid As UUID) As String
Function CLSIDToProgID(clsid As UUID) As String

To give an idea of the implementation difficulties, here's the implementation of IIDToString:

Function IIDToString(iid As UUID) As String
    Dim pStr As Long
    ' Allocate a string, fill it with GUID, and return a pointer to it
    StringFromIID iid, pStr
    ' Copy characters from pointer to return string
    IIDToString = PointerToString(pStr)
    ' Free the allocated string
    CoTaskMemFree pStr
End Function

One final GUID function you might occasionally need is the one that creates GUIDs from scratch. Here is its Basic signature:

Sub CoCreateGuid(uid As UUID)

You know you're hardcore when you create your own objects from scratch in VB and have to generate GUIDs for them.

Page 575. A reader recently sent me a false alarm claiming that the VB5GetRefCount function shown in "Can I get the reference count?" did not work in VB6. I did some simple tests and determined that the function appears to work the same in VB6 as in VB5. The reader who reported the problem did a second check and confirmed my results. As stated in the book, there is no guarantee that it will work in all situations or for future versions.

Page 576, 76. In the section "Why can't I pass structures as public method parameters or properties?" I hint that some future version of COM Automation (and thus of Visual Basic) might allow UDTs in public parameters and properties. Well, that feature is available in VB6, but as explained in notes to Chapter 3, it won't solve all your problems or make you want to sprinkle UDTs throughout your classes. In many cases, the advice in the last paragraph of this section is still good. It may still be better to redesign your UDTs as classes.

Page 577. In "IShellLink--Shortcuts the Long Way" I ask why Visual Basic doesn't provide a statement or procedure to create shortcuts. Let's get more specific. Why doesn't the FileSystemObject class provided with VB6 provide a method to create shortcuts?

Page 578. In "IShellLink--Shortcuts the Long Way" I note that there are samples illustrating interfaces on the Visual Basic CD. The Unsupprt\ShellLnk and Unsupprt\IHandler directories won't be in the location stated in the book and may in fact be buried in an unexpected location in some versions of VB6. The ShellLnk directory has a sample showing the IShellLink interface, and the IHandler directory has a sample showing the IExtractIcon interface. Incidentally, it was inexcusably unprofessional to put these samples on the VB5 CD and to leave them on the VB6 CD with no Readme.txt file to explain what they are. I don't care if the directory is unsupported. It's rude to force users to read code with inadequate comments just to figure out what the sample does.

Page 585. "Interface casting" describes the casting functions I create in the GCasts global module to fake the interface casting syntax Visual Basic should have provided. I have added several new casting functions for new IVB interfaces added to the Windows API type library.

The presence of an interface in the type library or of its type casting function in the GCasts module does not mean that the interface has been tested or that it can even be used in Visual Basic. Many of these interfaces were created from VB-hostile IDL files using mechanical translation techniques. It would take a lot of the trickery described in chapters 3 and 4 to make them work. Some such trickery comes from lying in the type library, but the only way to figure out the true lies is to use each and every method in a sample. I didn't have time to do that. You're on your own if you want to implement IVBDispatch or IVBCreateTypeInfo.

The story of how interface casting failed to get into the product is a discouraging one that was a turning point for me in my decision to abandon Visual Basic. Late in VB5 development I had an email exchange with a VB program manager about why interface casting hadn't been added to the language. It turns out that they had considered the issue and even had a syntax for it.

For example, assume you were using a class called CSoccerPlayer. Since this class was written in Visual Basic, it automatically implements the default interface _CSoccerPlayer. The designer of the class also used the Implements statement to add an IPayable interface (which also happens to be implemented by CBaseballPlayer and CBasketballPlayer). Now let's say you had some variables of type CSoccerPlayer:

Dim ronaldo as New CSoccerPlayer, mckinney as New CSoccerPlayer

You could use the IPayable interface like this using the proposed Visual Basic syntax:

ronaldo@IPayable.Salary = 700000

My alternative syntax, which I fake by writing my own casting functions, looks like this:

IPayable(mckinney).Salary = 1000000

Without either of these syntaxes, you have to create a dummy variable:

Dim payment As IPayable
Set payment = mckinney
payment.Salary = 1500000

This last syntax is clumsy, but it's the only one built into the language. I was told that the reason they didn't implement the casting syntax is because they were afraid that VB programmers would use it indiscriminately, thus compromising performance. For example, it would be inefficient to write code like this:

ronaldo@IPayment.Salary = 700000
ronaldo@IPayment.Performance = 93.5
iBonus = ronaldo@IPayment.Bonus

I pointed out that it would be easy for VB programmers to avoid this foolish mistake if the VB documenters simply explained the feature adequately and gave examples of its proper use. For example, the With statement would make efficient interface casting easy and much more convenient than creating a dummy variable to hold the interface object. You could write the code above like this:

With ronaldo@IPayment
    .Salary = 700000
    .Performance = 93.5
    iBonus =.Bonus
End With

The program manager admitted that there was some merit in my argument and said they would reconsider adding the casting syntax in the next version of VB. Well, VB6 is here and interface casting isn't.

On its technical merits, this isn't the most important language feature they've refused to give us, but the reasons are discouraging. They don't respect us. When I protested that interface casting wouldn't be too difficult for me, the program manager said I wasn't a typical VB user. In other words, they think the hordes of unsophisticated VB programmers are incapable of learning good programming habits. If so, I've wasted the last few years of my life. I don't believe it. There are many advanced and intermediate VB programmers out there, and even beginners can write good code at their level if someone bothers to teach them good habits.

It's discouraging and confusing. On the one hand they say we're not capable of using reasonable intermediate techniques like interface casting. On the other hand, they give us atrocious advanced features like AddressOf. I don't think they know who their audience is or who they want it to be.

Page 587, 88. "What's next" describes some of the interface problems you might want to solve. Many readers have asked for more information about IStream and IStorage. A few have tried to use them, with varying levels of success. The latest type library has IVBStream and IVBStorage interfaces that I have used successfully to create a simple storage. Unfortunately, I didn't have time to polish this rough example to the level where I could provide it with this update.

This section also mentions the TypeLib Information Library (TLBINF32.DLL). Some readers have experimented with this component more than I did. You can see a simple example of its use in a note in Chapter 2. Until recently, programmers who wanted to experiment with TlbInf32 had to figure it out on their own. But now the author, Matt Curland, has written a help file describing it. The helpfile is available for downloading at the Visual Basic web. Matt also described some of the more interesting features in the November 1998 issue of Visual Basic Programmer's Journal.

Page 589. If you don't care for the code described in "Registry Tools," you can now get registry tools from Visual Basic. Download RegObj.dll from somewhere on the Visual Basic Web site (the location is long lost since I wrote these words).

This ActiveX server provides an object hierarchy for using the registry that appears to be similar in concept to mine, although different in detail. Theirs is written in some language other than Visual Basic (probably C++), and there is no source code. I haven't used it, but it appears to offer most of the registry features you might need.

Page 591-93. The GetRegValueNext function shown in "Reading values" has a bug, and the GetRegNodeNext function (not shown in the book) makes the same mistake. I've never seen this bug in the debugger, but I know it's there because both Rob Gamlin and Magnus Lindström reported it. I believe the source of the problem is my attempt to cache reusable data in static variables. This is always a dangerous thing in functions that might be called recursively. To understand the problem, let's look at the original code:

Function GetRegValueNext(ByVal hKey As Long, _
                         i As Long, _
                         sName As String, _
                         vValue As Variant) As Long
    Dim cName As Long, cData As Long, sData As String
    Dim ordType As Long, cJunk As Long, ft As FILETIME
    Static hKeyPrev As Long, cNameMax As Long
    ' When enumerating, cache required data the first time
    If hKeyPrev <> hKey Or cNameMax = 0 Then
        hKeyPrev = hKey
        GetRegValueNext = _
            RegQueryInfoKey(hKey, sNullStr, cJunk, pNull, _
                            cJunk, cJunk, cJunk, cJunk, _
                            cNameMax, cJunk, cJunk, ft)
        If GetRegValueNext Then Exit Function
    End If
    
    ' Get the value name and type in the first call
    vValue = Empty
    cName = cNameMax + 1
    sName = String$(cName, 0)
    GetRegValueNext = _
        RegEnumValue(hKey, i, sName, cName, _
                     pNull, ordType, pNull, cData)
    If GetRegValueNext Then
        If GetRegValueNext <> ERROR_MORE_DATA Then
            Exit Function
        End If
    End If
    sName = Left$(sName, cName)
    
    ' Handle each type separately
    . . .

What I'm trying to do here is call the RegQueryInfoKey function to get the length of the longest name value in the registry node. I only need this once for the node, so I cache the value in a static variable. I don't want to call RegQueryInfoKey to get the same information for every iteration. This problem is extemely hard to debug, so I'm still not sure exactly what goes wrong, but my theory is that in some recursive situations one level of recursion caches a short name length that is later reused by another node that has a longer name length. The result is a run-time error indicating that "more data is available."

I believe I've fixed the problem by changing to a different algorithm. Instead of trying to get the maximum name length with RegQueryInfoKey, I assume that the longest reasonable name is 256 characters. That's a pretty safe guess, and I expect that it will be correct 99.9 percent of the time. But since anybody can put any damn fool thing in the registry, I have to handle node or value names of any length. Here's how I do it:

Function GetRegValueNext(ByVal hKey As Long, _
                         i As Long, _
                         sName As String, _
                         vValue As Variant) As Long
    Dim cName As Long, cData As Long, sData As String
    Dim ordType As Long, cJunk As Long, ft As FILETIME
    ' Get the value name and type in the first call
    vValue = Empty
    ' Name buffer of 256 should be big enough for most
    Do
        cName = cName + 256
        sName = String$(cName, 0)
        GetRegValueNext = RegEnumValue(hKey, i, sName, cName, _
                                       pNull, ordType, pNull, cData)
        ' Repeat with bigger buffer in unlikely
event name is too long
    Loop While GetRegValueNext = ERROR_MORE_DATA
    ' Fail for other errors
    If GetRegValueNext Then Exit Function
    sName = Left$(sName, cName)
 
    ' Handle each type separately
    . . .

The new code is certainly simpler, and I imagine it's more efficient. This is one of those cases where it's better to make an educated guess and adjust if you're wrong than to take the extra trouble to find the truth--especially if you can't handle the truth anyway. There is probably some deep philosophical truth here that applies to real life as well as programming.

Page 592-93. The ExpandString function mentioned in "Reading values" had a bug that has been fixed in the update. The original version returned strings that often appeared to be correct, but had extraneous trailing nulls.

Page 593-95. The CRegNode class had a bug in its Key method. This bug showed up in unexpected contexts, especially for Windows NT users. Johan Mutsaerts tried to use the CRegNode class on a Windows NT workstation, but reported that he couldn't make it work because his machine didn't have privileges to do some registry operations. My code ignored his attempts to specify limited access.

To understand this bug, you have to look at the Create and Key methods of CRegNode. The "Accessing the registry" section shows different ways of creating registry nodes using Create and Key. For example, you can create a new node with code like this:

Dim regnode As CRegNode
Set regnode = New CRegNode
Call regnode.Create("Software\Netscape", HKEY_LOCAL_MACHINE, KEY_READ)

The last argument of Create is the optional access parameter, which you may sometimes leave blank to use the default KEY_ALL_ACCESS value. The default may work for a standalone machine, but it is often inappropriate for a workstation that does not have full access to network resources. A Windows NT workstation may have permission to read certain registry values, but no permission to modify them. If such a machine asks for all rights, it will fail even if it has permission to do the operation being performed. Therefore, it should request specific access such as KEY_READ rather than general access such as KEY_ALL_ACCESS.

Unfortunately, a bug in my code prevented iteration through multiple levels of registry nodes. You could open a parent node with the Create method, requesting KEY_READ access as shown above, but when you tried to iterate through child nodes, the class would fail with a permission denied error. For example, you might use the following code to iterate through child nodes:

For iNode = 0 To regnode.NodeCount - 1
    Debug.Print regnode.Nodes(iNode).Name
Next
 
This code uses the Nodes method, which was written like this: 
 
Function Nodes(vIndexA As Variant) As CRegNode
    Dim node As CRegNode
    Set node = New CRegNode
    node.Key(hKey) = vIndexA
    Set Nodes = node
End Function

This code created new nodes for iteration, but each new node would get the default access mode (KEY_ALL_ACCESS) instead of the mode of the parent. I fixed the bug by passing the parent access to the child Nodes:

Function Nodes(vIndexA As Variant) As CRegNode
    Dim node As CRegNode
    Set node = New CRegNode
    ' Pass parent access to child
    node.Access = afAccess
    node.Key(hKey) = vIndexA
    Set Nodes = node
End Function

For those who want to examine further, you can see that this technique was already used correctly in the AddNode method, but I neglected to do the same in the Nodes method. The update has the correction. If you use the Nodes method specifically rather than behind the scenes in an iteration, you may need to specifically set the mode using the Access property. Note that the Test Registry sample program (TReg.vbg) may fail on workstation machines that do not have permission to change or create registry nodes.

Page 601. The REGSVR32.EXE program mentioned in "Registering components" is still provided with Visual Basic, but not in the location shown. The location may vary depending on which VB edition you have.

Page 602. The section called "The Windows Programs in the Registry" (which shouldn't have "The" in the section name) talks about the App Paths entry in the registry. The first edition of my book had more details on this subject and included a program called AppPath that added or deleted App Path entries through a command-line interface. For the second edition I planned to enhance this program, giving it a GUI user interface that would show all the App Path entries and make it easy to change them or add new ones. It never happened. In fact, I never even found time to update the old command-line interface and AppPath got left behind in the second edition. Later I needed an AppPath program for something unrelated to the book, and the only way to get one was to fix my old program. So now it's back--but only in the original command-line version. The GUI version is still up to you.

In the process of writing AppPath, I added several new functions to the RegTool module. The SetRegStr and SetRegInt functions add a string or integer item to a given key in the registry. These subs are the opposites of the GetRegStr and GetRegInt functions. I also added the GetAppPath, SetAppPath, and RemoveAppPath functions to deal specifically with App Path registry entries.

Chapter 11

Page 609. The VisualCore component mentioned at the end of "Using the CAbout Class" probably wouldn't be necessary if I were designing my components today. I could have put the visual classes in VBCore instead of VisualCore. The reasons are explained later in this chapter. Of course, it's too late now. I couldn't make the change now without breaking clients.

Page 610, 11. The Form_Load method (for the form used by CAbout) shown in "A Form Wrapped by a Class" has a mistake. In the original a constant sInfo gave the directory path where the MSInfo program used to be located. I didn't know how to find a reliable location for the program, so I guessed and tried to handle any possible failure as politely as possible. Naturally, they changed the location and the code failed to find the information program under VB6. I didn't want to hardcode the new location for fear the same thing might happen again.

Usually in cases like this, Windows stores the directory somewhere in the registry. I don't know why I didn't think to check when I wrote the original, but this time I did. Finding the entry wasn't easy. I first found the location of the MSInfo program on my disk and then searched the registry for a value matching the file name. Sure enough, I found an MSInfo entry buried deep in the HKEY_LOCAL_MACHINE node. Here's a fragment from Form_Load that shows how to find and use MSInfo:

. . . 
lblApp.Caption = .Title
Dim sInfo As String
On Error Resume Next
sInfo = GetRegStr("SOFTWARE\Microsoft\Shared Tools\MSInfo", _
                  "Path", HKEY_LOCAL_MACHINE)
' Allow override because some customers might not have MSINFO
If InfoProg = sEmpty Then InfoProg = sInfo
If ExistFile(InfoProg) = False Then cmdInfo.Visible = False
        
' Icon from first form is application icon
. . . 

This works everywhere I've tried it. I don't know how long that entry has been in the registry. It might turn up nothing on some of the old machines your customers use, but once Windows starts putting something in the registry, it doesn't usually stop. I expect this will work on all the new machines your customers buy in the future.

Page 619, 20. The Add method of the XListBoxPlus control shown in "Finding Items" has an extra optional parameter that isn't shown in the listing. The new parameter is an ItemData member that allows you to add string items and numeric item data with one statement. Instead of this:

lst.AddItem "WindowText"
lst.ItemData(lst.NewIndex) = hWnd

Do this:

lst.Add "WindowTest", , hWnd

The same listing also fails to call the PropertyChanged method to let the property page know that the List property has changed. Here is a fragment showing code changes to the Add method:

Sub Add(sItem As String, Optional iPos As Integer = 1, _
        Optional iItemData As Long)
With lst
    ' Adding differs depending on the mode
    Select Case esmlMode
    . 
    . 
    .
    End Select
    .ItemData(.NewIndex) = iItemData
    PropertyChanged "List"
End With
End Sub

Pages 623. The WaitOnProgram function in "Using Shell" fails to check the process handle after calling OpenProcess. Frank Garabo discovered the hard way that this can cause confusing problems. Some processes can execute so fast that, in the time it takes to enter WaitOnProgram, the process can exit, resulting in OpenProcess returning a zero value for hProg. In particular, this can happen if the programmer single steps into WaitOnProgram. Here's a fragment that fixes the code:

. . . 
' Get process handle
hProg = OpenProcess(PROCESS_ALL_ACCESS, False, idProg)
' Failure probably means process has terminated or ID was invalid
If hProg = hNull Then
    WaitOnProgram = -1
    Exit Function
End If
. . .

You might still argue that this is sloppy error handling. It doesn't distinguish between different kinds of failure. For example, you'll get the same result if you enter the function too slowly (as Frank did) or if you simply give a random number as the ID argument. Unfortunately, there's no working around this problem because any valid process ID you pass becomes just another random number the instant the process terminates.

Page 622. In "Using Shell," I claim that a Visual Basic program can't return an exit code. I am happy to report that I was wrong. I said that I tried ExitProcess, but I must not have tried it correctly. It turns out that ExitProcess works fine--with one major caveat. You should call ExitProcess only when all possible objects have been destroyed. In other words, call it only at the very end of Sub Main in a standard module after you have explicitly destroyed all forms or other objects with Set obj = Nothing. If you call ExitProcess with live objects, those objects will be aborted without going through normal cleanup such as Class_Terminate and Form_QueryUnload. This means you can't use a form as your startup object. This is no great loss since you don't generally need to return exit codes from interactive programs.

I learned this from a usenet discussion. Bob Butler gave the correct answer, although he says this VB technique isn't original with him. In any case, I use it successfully in RegTlb.exe. You can attempt to register a type library in a batch file, and take action based on testing the errorlevel. Here's a fragment of a batch file:

RegTlb %1 
if errorlevel 1 goto Fail
echo Success
goto Done
:Fail
echo Failure
:Done

There's one other minor side effect that you should keep in mind. If you execute an ExitProcess statement when running in the IDE, guess which process gets exited. That's right. The IDE itself suddenly disappears with no warning. You can easily protect yourself by commenting out the ExitProcess until you're done debugging, or you can use the new and improved IsExe function (which, unlike the old one, recognizes future versions such VB8 and VB10):

If IsExe Then ExitProcess 1

It shouldn't be necessary to call an API function (and a rather dangerous one at that) under artificial circumstances just to set the exit code. The App object ought to have an ExitCode property that you could set. Visual Basic could pass this value to ExitProcess itself after it has completed its entire normal cleanup.

Pages 630, 633. The CSharedString Create method shown in "Shared Memory Through Memory-Mapped Files" has a minor correction. The call to CreateFileMapping should pass the pNull constant by value rather than by reference:

h = CreateFileMapping(-1, ByVal pNull, PAGE_READWRITE, _
                      0, 65535, sName)

A sample call to CreateFile and another to CreateFileMapping early in the section need the same correction. The reason for these corrections is that the type library has changed in how it handles the SECURITY_ATTRIBUTES UDT. Ironically, the original code worked, but not for the obvious reasons.

Win32 has a long list of functions that can use the SECURITY_ATTRIBUTES type under Windows NT to control access to services. Windows 95 doesn't handle security attributes. You should always pass a null pointer to indicate you're not interested. Even if you're programming for Windows NT, you may not always want to specify security attributes. The trick in defining these functions is to allow programmers to supply the security attributes for programs that need them, but pass a null pointer for programs that ignore them. VB's type safety makes it difficult to pass a Long or a SECURITY_ATTRIBUTES UDT to the same parameter.

Unfortunately, I didn't handle these functions consistently in the type library. I used two different strategies. For some APIs I created two functions--one for normal use and one that recognized the security attributes. For example, here are the VB signatures for CreateDirectory:

Function CreateDirectory(lpPathName As String, _
                         ByVal lpSecurity As Long) As Long
 
Function CreateDirectorySec(lpPathName As String, _
                            lpSecurity As SECURITY_ATTRIBUTES) As Long

You could pass a zero to the plain version or a security variable to the aliased version. With other functions, I gave the type library equivalent of As Any (void *) for the second parameter. I'm going to use CreateDirectory to illustrate, although I didn't use this form in the type library:

Function CreateDirectory(lpPathName As String, _
                         ByVal lpSecurity As Any) As Long

You could call this function like this:

Dim saDir As SECURITY_ATTRIBUTES
f = CreateDirectory(sPath, ByVal pNull)
f = CreateDirectory(sPath, saDir)

The danger of this approach is that users may forget (or not understand) the need to pass the constant as a ByVal Long zero. They might forget the ByVal, or they might pass 0 instead of 0& as the constant.

After some experimenting, I came up with a type library definition that would work for almost any possible argument. VB has no Declare syntax for this format, but if it did, the signature would look something like this:

Function CreateDirectory(lpPathName As String, _
                    Optional lpSecurity As Any = ByVal 0&) As Long

For those who care about IDL, the type library definition looks like this:

BOOL WINAPI CreateDirectory(
        LPCTSTR lpPathName,
        [in, defaultvalue(0)] LPVOID lpSecurity
        );

The big advantage of this version is that for normal use you can ignore the security parameter--the optional zero argument takes care of it. This works particularly well for functions like CreateDirectory where the security attribute is the last parameter. Since this version needed to work for old code, I tried every variation I could think of:

Dim f As Long, sa As SECURITY_ATTRIBUTES
f = CreateDirectory("FooBar1")              ' No problem
f = CreateDirectory("FooBar2", ByVal pNull) ' No problem
f = CreateDirectory("FooBar3", pNull)       ' No problem
f = CreateDirectory("FooBar4", ByVal 0)     ' No problem
f = CreateDirectory("FooBar5", 0)           ' ERROR_UNKNOWN_REVISION
f = CreateDirectory("FooBar6", 0&)          ' No problem
f = CreateDirectory("FooBar7", sa)          ' No problem

All these variations pass under Windows 98, and all but one pass under Windows NT. My guess is that Windows 98 ignores this parameter--pass whatever you want. Unfortunately, the one call that failed under Windows NT would have been legal with the old type library definition shown earlier. With the security parameter defined as a ByVal Long, you could pass an Integer 0 and VB would automatically convert it to a Long 0&. For the sake of consistency and convenience, I decided to change all the API functions with security parameters to the new syntax, even at the risk of breaking some code written by existing type library users.

For those of you who wonder why so many calls with questionable syntax worked (including the CreateFileMapping call shown earlier), I have a theory related to the definition of SECURITY_ATTRIBUTES. A Basic version would look like this:

Type SECURITY_ATTRIBUTES 
    nLength As Long    ' Length of the structure
    lpSecurityDescriptor As Long
    bInheritHandle As Long
End Type

When you pass a ByRef zero, Windows NT thinks you're passing a SECURITY_ATTRIBUTES variable and that the zero is the value in the nLength field. If the zero you passed is a Long, Windows accepts and ignores the zero-length structure. But if the zero you pass is an Integer, Windows combines it with whatever other junk happens to be in the adjacent Integer. Seeing that the structure has an unrecognizable length, it fails with ERROR_UNKNOWN_REVISION.

Pages 636. "Threads and Synchronization" notes that the VBCore is marked for Unattended Execution, while VisualCore is not. This was a requirement for the original VB5 because at that time you couldn't reliably set Unattended Execution in components with visual interfaces. I'm told that starting with VB5 Service Pack 2 and continuing in VB6 they made threading enhancements in VB's library code so that visual interface code can be unattended. The change came too late to affect my components, so I didn't spend any time researching the differences or new possibilities.

Page 636-40. The sections on multithreading starting with "Where Angels Fear to Thread" present a simplistic view of multithreading with Visual Basic. Some readers have reported crashes with this code. I consistently crashed when the program terminated on one machine, although it works on other machines I tried. I've heard that changes introduced with the VB5 Service Pack 2 cause incompatibilities. I'm not shocked to hear this. I warned that this code was iffy, and that serious threading code would have to take a more comprehensive approach.

In fact, some people have taken a more comprehensive approach to multithreading. One of them is Dan Appleman, author of Dan Appleman's Visual Basic 5.0 Programmer's Guide to the Win32 API. Dan wrote an article that anyone who's serious about multithreading in Visual Basic ought to read. You can find the article entitled "A Thread to Visual Basic" at his Web site: www.desaware.com/articles/default.htm.

Dan doesn't mince words in this article. He shows a simplistic threading approach not unlike the one in my book and then clarifies why it doesn't work reliably: "The problem is that the above code is garbage." He's not talking about my code specifically, but he could have been. Lest anyone accuse him of timidity, he goes on: "This approach is programming alchemy. It is irresponsible and no programmer should ever use it. Period." Don't spare me, Dan. I can take it.

The point of the article is that code like mine doesn't follow the COM rules. It doesn't concern itself with COM initialization, thread marshalling, and other important techniques. He's right. I didn't understand the issues at the time. My only excuse is that I didn't promise much for this code, and it did deliver what little I promised. Nevertheless, I ought to make amends by revising my thread sample to use all the techniques suggested in his article. Instead, I'm pointing you to his Web site to learn the right way from the expert.

Page 640-47. I rewrote the server described in "File Notification" in Delphi 3 and described the differences between the two versions in an article for the April 1998 issue of Delphi Informant. See the beginning of this article for more on what I learned.

As if to illustrate my point about how difficult some tasks can be in Visual Basic, I discovered that the original Notify server lived forever--even after all clients had disconnected. At first I couldn't believe that this was my bug. I could have sworn it worked when I finished the second edition. I wanted to blame the problem on a change in behavior introduced with a Visual Basic service pack, but when I tried it with the original VB5, the problem was still there. I have no one to blame but myself.

After hours of struggle, I determined that I could make Notify go away by changing the Instancing property of the CNotify class from MultiUse to SingleUse, but that means every client gets its own copy of the Notify server. That's not the architecture I had in mind. One server should be able to serve all the notification needs of all clients on the system.

If you study the code in this section, you can see that my management of server lifetime is a gross hack. The Sub Main started a timer to get the notification loop going without blocking Main from returning control to the client. I didn't have to use the timer in the VB4--just start the loop from Sub Main--but I had to hack the hack for VB5. When you go this far beyond documentation, you can hardly complain to a higher authority. If I tried to report this as a bug to technical support, I'd probably make the departmental bulletin board as the geek call of the week. Outlaws have to live beyond the law. When your hack breaks, you figure out a different hack.

By writing to the NT event log, I could see what was happening. Visual Basic creates multiple threads for out-of-process objects, but it apparently also creates a master thread to manage the others. Each one of these threads runs its own Sub Main. If I had only one object, I still got two threads and both of them started a notification loop. The one that managed the object would be destroyed gracefully when the last object disconnected, but the other one would run forever, keeping the server alive.

After thinking about this for a while, I decided to launch the wait loop from the Class_Initialize event. Since object creation only happens in the object threads, the master thread wouldn't get a copy of the wait loop. After a few false tries, I got CNotify working correctly with MultiUse instancing. I define an fLooping flag in the .BAS file, and add the following code to the Connect method of the class. Notice that I still have to use SetTimer to start the wait loop indirectly. Otherwise, Class_Initialize wouldn't finish executing and the object wouldn't be created.

Private Sub Class_Initialize()
    BugMessage "Initialize object"
    If fLooping = False Then
        ' Start the wait loop
        Call SetTimer(hNull, 0, 200, AddressOf WaitForNotify)
        BugMessage "Started Timer"
        fLooping = True
    End If
    ' Count this object
    cObject = cObject + 1
End Sub

Although I use a thread pool for Notify, I believe the same strategy would work with the thread-per-object setting. Either way, the wait loop is created once for each thread that can create objects. Although this hack works with VB6 and with the service pack update of VB5, Notify still waits forever with the original VB5. I don't know what causes this problem, and I didn't care to spend any more debugging time on a version of VB that can be updated.

This illustrates my original point about Delphi. I didn't have to hack to accomplish the same thing in Delphi because the Delphi class library provides complete facilities for managing server lifetime. I had to do some digging through inadequate documentation to figure this out, but the mechanism itself is simple. On the other hand, it's a real annoyance to resolve simple things like creating classes and using interfaces in Delphi. Visual Basic's system is much easier as far as it goes. It just doesn't go as far.

Page 641. Problems with the Picture browser shown in "The Client Side of File Notification" are discussed in notes for Chapter 8.

Page 651. The TranslateColor function shown in "System Colors and Sizes" has a different implementation in the updated version. The function works the same, so those who don't care about the idiosyncrasies of IDL can just use it and skip the following story.

Changing TranslateColor took several tries. This was my original code from the second edition:

Function TranslateColor(ByVal clr As OLE_COLOR, _
                        Optional hPal As Long = 0) As Long
    If OleTranslateColor(clr, hPal, TranslateColor) Then
        TranslateColor = CLR_INVALID
    End If
End Function

It was based on the following IDL definition:

LONG WINAPI OleTranslateColor(OLE_COLOR clr, HPALETTE hpal,
                        COLORREF* lpcolorref);

A COLORREF in this definition is an alias for Long. The return value from OleTranslateColor is actually an HRESULT, but I changed it to LONG and then tested for zero in the Basic wrapper. This is kind of a rude definition. Visual Basic expects HRESULT returns and converts them into Err objects. Eliminating the HRESULT disables an important VB feature. It's better to return the HRESULT in IDL, so I changed the definition to this:

HRESULT WINAPI OleTranslateColor(OLE_COLOR clr, HPALETTE hpal,
                     COLORREF * lpcolorref);

When you return an HRESULT from a type library function, Visual Basic sees the result as a Sub. The Basic signature looks like this:

Sub OleTranslateColor(ByVal clr As OLE_COLOR, ByVal hpal As Long, _
                      ByRef lpcolorref As Long)

This change broke the original Basic code, which I had to change to this:

Function TranslateColor(ByVal clr As OLE_COLOR, _
                        Optional hPal As Long = 0) As Long
    TranslateColor = CLR_INVALID      ' Assume failure
    On Error Resume Next              ' Ignore errors
    OleTranslateColor clr, hPal, TranslateColor
End Function

That raises another question. If VB expects HRESULT returns, and if HRESULT returns convert Functions to Subs, how do some VB procedures get to be Functions? OleTranslateColor is the perfect example because it's easy to transform it into a VB Function by changing the IDL definition to this:

HRESULT WINAPI OleTranslateColor(OLE_COLOR clr, 
                  [in, defaultvalue(0)] HPALETTE hpal, 
                  [out, retval] COLORREF* lpcolorref);

The retval attribute turns the COLORREF pointer value into the Function return value. The defaultvalue attribute makes the hpal parameter optional with a default value zero. The Basic signature now looks like this:

Function OleTranslateColor(ByVal clr As OLE_COLOR, _
                           Optional ByVal hPal As Long = 0) As Long

Given that definition, you can simply call OleTranslateColor directly without wrapping it in a Basic TranslateColor. We must retain the old TranslateColor function for compatibility and for its slightly different error return. The implementation changes to the following, which is how it appears in the update:

Function TranslateColor(ByVal clr As OLE_COLOR, _
                        Optional hPal As Long = 0) As Long
    TranslateColor = CLR_INVALID      ' Assume failure
    On Error Resume Next              ' Ignore errors
    TranslateColor = OleTranslateColor(clr, hPal)
End Function

Page 652. The first bulleted item in "More Interface Stuff" describes my BrowseForFolder function, based on the SHBrowseForFolder API function. Unfortunately, the code was broken--the dialog title input string and the display name output string didn't work. Magnus Lindström, Jon Evans, and Gadi Naveh identified this problem. Magnus and Jon even provided fixes--although they were different from each other. But the more important limitation was that you couldn't specify the initial directory. Magnus showed me how to fix that problem using a callback function. I had seen this callback documented, but hadn't figured out how to use it. Basically, you give Windows the address of the function you want it to call back. Within this function, you recognize data messages from Windows, and then use SendMessage to tell the dialog how to respond. It sounds messy, and it is. You can see the final code in the FoldTool and BrowseBack modules.

While SHBrowseForFolder is better than nothing, you could make a much better directory browsing tool with the ImageCombo control and the WalkAllFolders function described in "Walking Through Folders," page 674. It would be relatively easy to create a FolderCombo control that delegates to an ImageCombo and fills it with folders to look exactly like the combo box in the File Open dialog or in Windows Explorer. It would be coded much like the TreeView window in the Meriwether program described in "Famous Explorers and Common Controls," page 680. Windows should have provided this functionality in a windows control long ago, and Visual Basic should have wrapped it in an OCX for VB5 or VB6. Failing that, I should have created the control for this update. I guess it's up to you.

Page 656. The Challenge at the end of "The XColorPicker Control" was met even before the second edition was published. Randy Russel's System color components is a scrolling system color list like the one in VB's Properties Window. It comes either in a flat control or a dropdown form wrapped in a class.

Page 658-61. VB's FileDateTime still has no knowledge of the file creation time or last access time. The FileAnyDateTime is described in "File Dates and Times" is still useful. But you can also get the same information through the FileSystemObject, which can handle all three file times through the DateCreated, DateLastAccessed, and DateLastModified properties of the File class.

The FileSystemObject also understands all possible file attributes, including compressed and temporary, through the Attributes property of the File class. The FileAttribute enum used by the Attributes property includes all possible attributes, but the VbFileAttribute enum used by the GetAttr function has still not been updated to include the compressed and temporary attributes.

Page 661-65. The FileSystemObject can copy, move, rename, and delete files, but it does so in the crude way of Windows 3.1. The more user-friendly techniques described in "Politically Correct File Operations" are not supported.

Page 663. The ReplaceFile sub in the "Busted" sidebar has the error tests reversed on two API calls (SetFileTime and SetFileAttributes). Here is the corrected code from the end of the sub:

   . . . 
    f = SetFileTime(hOld, fnd.ftCreationTime, _
                    fnd.ftLastAccessTime, fnd.ftLastWriteTime)
    If f = 0 Then ApiRaise Err.LastDllError
    lclose hOld
    f = SetFileAttributes(sOld, fnd.dwFileAttributes)
    If f = 0 Then ApiRaise Err.LastDllError
    . . .

The entire procedure appears in the FileTool module. This embarrassing error was copied from the first edition of my book. Magnus Lindstrom was the first to notice this bug almost three years after I created it. Perhaps one reason it slipped through is that this is one of the few pieces of code that appears in the book, but is not used in any of the sample programs.

Page 664, 65. The CopyAnyFile function shown in "Politically Correct File Operations" and the similar MoveAnyFile, RenameAnyFile, and DeleteAnyFile functions have a potential problem. These functions are wrappers for the Win32 SHFileOperation function. Although in my implementation they are designed to be called with a single file, the SHFileOperation function can handle lists of files separated by null characters. According to the documentation, the list should be terminated with double null characters. Theoretically, a single file (a list of one) should also be terminated with double nulls.

So why did my function work fine with only the single null automatically appended to all Basic strings? Benoit Cerrina was the first to ask that question. I don't know the answer. Perhaps it was just luck. Maybe all the strings I ever passed just happened to have an extra null as the next random character in memory. In any case, I've appended an extra null to all those input strings just in case. It can't hurt, and it might help. By the way, although my functions are designed to operate on just one file, there's nothing to prevent you from passing a list of files separated by nulls. I tried it, and it works. Here's the new code:

Function CopyAnyFile(sSrc As String, sDst As String, _
                     Optional Options As Long = 0, _
                     Optional Owner As Long = hNull) As Boolean
    If MUtility.HasShell Then
        Dim fo As SHFILEOPSTRUCT, f As Long
        fo.wFunc = FO_COPY
        ' Make sure all strings are double-null-terminated 
        ' as required
        fo.pFrom = sSrc & vbNullChar
        fo.pTo = sDst & vbNullChar
        fo.fFlags = Options
        . . .

Page 665-70. The file information system described in "Getting File Interface Information" is not supported by the FileSystemObject. FSO provides no means of getting the icon or the display name of a file.

Page 671. The performance sidebar has the wrong timings in the p-code column. It might be nice if p-code were the same speed as native code, but the correct numbers are:

Problem

Native code

P-code

Find files with Basic Dir$

39 seconds

49 seconds

Find files with FindFirstFile API

9 seconds

13 seconds

Use the Windows Explorer Find dialog

6 seconds

 

These are the original figures for VB5 taken at the time the second edition was written. The numbers might be a little different under VB6.

Page 674-79. The IShellFolder code in FoldTool and related modules has gone through a lot of minor changes. This extremely complex code was written at the last minute for the second edition. In looking over it again, I found more problems than I care to describe here. Some were bugs that I encountered during testing. Others were simply improved error handling or code simplifications based on things I have learned since I wrote the original. Working on this code reminded me of how truly bizarre and unintuitive the IShellFolder and related data structures are.

Page 676. The Allocator object described in "Folders and items" is probably not necessary. Windows provides the CoTaskMemAlloc, CoTaskMemRealloc, and CoTaskMemFree API functions. The documentation indicates that these are equivalent to the IMalloc.Alloc, IMalloc.Realloc, and IMalloc.Free methods. I was not aware of these functions when I wrote the second edition, and I would probably not have added the MAllocator class to VBCore if I had. I use the global allocator to free PIDL objects as shown here:

Allocator.Free pidl

But it appears I could have done this more easily with an API function:

CoTaskMemFree pidl

Page 679. The ContextPopMenu function mentioned at the end of "Doing something with folders" doesn't display any items on its Send To submenu, and I don't know why. Eric Daniel was pretty excited when he found this code in the FoldTool module, but his enthusiasm turned to disappointment when it failed to do what he needed. I had never noticed what was or wasn't on the Send To submenu when testing the code, but I assumed the problem was due to some stupid bug on my part. Well, maybe. If so, I'm not alone.

I checked all the code I could find on the MSDN CDs dealing with IContextMenu. All the samples I found seemed to be doing the exact same thing I was doing. Of course, they were doing it in C or C++, but I couldn't find a problem with my translation to Basic. It's a puzzle I haven't solved.

I got this technique from the ExpMenu program written in C++ by Jeff Prosise. The book says his sample came from a PC Magazine article, but in fact it comes from the Wicked Code column in the April 1997 issue of Microsoft Systems Journal. The Send To menu in his program doesn't show any entries either.

Pages 680-81. I'm sorry to say that the Meriwether Explorer application described in "Famous Explorers and Common Controls" isn't much better than the pitiful Columbus application provided with the first edition. In fact, this version does even less, although it has more potential. I got the program working just in time to get a screen dump for the second edition. I planned to spend more time polishing it before the CD shipped, but other things came up, and I didn't work more with Meriwether then or since. Readers have pointed out several cosmetic problems with the program, but I didn't do anything about them.

Fortunately, this version is based on a more flexible architecture. Unlike Columbus, it shows the whole namespace and does so at an acceptable speed. You should be able to enhance it into a usable replacement for Windows Explorer. I was hoping some reader would complete this project for me, but so far no one has. Perhaps the reason is that programmers are beginning to feel that the whole namespace and shell system can be written off as a lost cause.

When you study the documentation for IShellFolder and the related interfaces introduced with Windows 95, you can faintly see a grand vision for a new kind of document-oriented user interface. If programmers could have been shown the value of this new organization and persuaded to program to it, the new system might be pervasive today. But it isn't. Few programmers write shell extensions or take full advantage of the shell namespace. The "new" interface can hardly be called new anymore. If it hasn't caught on yet, it probably never will. What went wrong?

My theory is that they made it too damn hard. The programming model is language-specific, making use of pointers in a way that makes shell programming difficult in high-level languages. The original samples were written in C, not C++. There were few C++ classes written to encapsulate the new concepts. Wizards weren't written to make it easy to generate shell extension DLLs. The programming model was difficult to use even if you could interpret the poorly written and incomplete documentation. If few people can figure out how to program the shell in C++, what chance does it have in Visual Basic, Java, and Delphi? The Visual Basic run-time library is a prime example. It doesn't even create shortcuts, much less support the more difficult aspects of shell programming.

Ideally, any programmer in any language who creates a new document type would have a check list of shell interface tasks. Does your new document type have a hierarchical structure? Click here to generate a name space extension shell so that your document can be represented hierarchically in the browser. Do your documents have some special operations that might commonly be performed on them? Check here to generate a context menu handler or a drop handler. Would users want to view your documents in a graphical format? Check here to generate a file viewer. None of that is available for Visual Basic or for any other language I know of. A hardcore programmer who was more interested in shell programming than Microsoft seems to be could write those tools. But don't hold your breath. I'm afraid the window of opportunity for easy shell programming has closed.

Pages 680. In "Famous Explorers and Common Controls" I noted that VB5 added several new GUI controls and I predicted that the next version might introduce controls for more of the doodads used in Internet Explorer. Call me a prophet. The first of these new doodads, the Coolbar control, was released on Microsoft's Web site long before VB6. New doodads added in VB6 include the MonthView, DateTimePicker, FlatScrollbar, and ImageCombo controls.

I'm not the only one who thinks Microsoft did a poor job of converting the Windows common controls to ActiveX controls. The Common Controls Replacement Project was created by a group of hardcore programmers who wouldn't take no for an answer. Here's how they describe their project:

The Common Controls Replacement Project is a new and unique project, combining the extensive talents of Windows developers whose aim is to provide smaller, faster, more fully featured and free replacements to the Microsoft's provided Common Controls and Common Dialogs.

Check it out at www.mvps.org/ccrp. My only complaint is that no one on the team has signed up to write a TreeView replacement.

Pages 682. The note in "The ImageList Control" describes my CSplitter class that creates a splitter bar between controls. Unfortunately, I seem to have made more than my share of bugs in this class, and helpful readers have pointed them out.

The private version of CSplitter is created by the Global Wizard (see Chapter 5) and is stored in the LocalModule directory in the P_Splitter.cls file. Unfortunately, I never tested this private module after generating it with the wizard. I failed to notice that it loads the vertical and horizontal splitter cursors from resources embedded in the VisualCore DLL. Since these resources don't automatically exist in a client that is using the private class, they won't be loaded. Of course, it's possible to add the same resources to the client file, but a class should stand alone with no outside dependencies. Minh Nguyen tried to use the private class and reported the problem.

I fixed the private version of the class by putting the cursor data into arrays and creating a cursor from the data with the CreateCursor API function. The code to do this is enclosed in #If blocks so that it won't be compiled when the class is a public part of VBCore. Instead the public version continues to use embedded cursor resources. The difficult part of the fix was initializing the cursor data in arrays. First I did a hex dump of the .CUR files. A cursor file consists of some header information followed by the cursor data in two chunks--the XOR data that will be visible as the cursor, and the AND data that will mask out the parts that aren't part of the cursor. Chapter 7 explains how XOR and AND masks work in the CPictureGlass class. The CreateCursor API expects these two chunks to be in arrays. Since Visual Basic has no data format for initializing arrays (see the bitter complaints about this throughout my book), I had to initialize the data with code, one element at a time, copying the data byte by byte from the hexdump to code. It wasn't easy, but the resulting class looks fine even when the class is used without VBCore. The TSplitter project shows the splitter from VBCore; the TSplitter2 project uses the private class.

Jon Evans found several other problems with the CSplitter class and suggested additional features. One of the new features is an optional ShowDrag parameter, which causes both panes to resize automatically as the splitter is moved. When ShowDrag is False (the default), splitter lines move, showing where the controls will move when the drag is finished. This is how most splitters work. The ShowDrag parameter usually causes too much flicker when used with controls that have a lot of text such as TextBoxes and ListBoxes, but it can give an interesting effect with graphic controls such as PictureBoxes.

Jon also found that the AutoBorder property didn't work well with forms that have toolbars or status bars. The old algorithm didn't provide a way to make the bottom border appear above the status bar. In the new algorithm, you must specify the bottom border by the position of the bottom right (southeast) control. This might break some old client code because the previous algorithm calculated the bottom border from the top border. You can't easily use the BorderPixels parameter on forms that have a toolbar or status bar.

Page 683. "The Overlay method" describes a bug in the Overlay method of the ImageList control. I personally reported this bug during VB4 beta and described it in the first edition of my book. It's still there in VB6.

Page 686-87. "The Toolbar and StatusBar Controls" and the accompanying flame describes a few of my complaints with the unintuitive Toolbar control. Well, VB6 includes a Toolbar Wizard that claims to solve those problems. I didn't get off to a good start when I tried to use this wizard the first time. First, I couldn't find it. Documentation said it was an add-in, but it didn't appear in the list of add-ins. I couldn't figure out how to install it. It turns out that the Toolbar Wizard is part of the Application Wizard, which you have to install, whether you want it or not, to get the Toolbar Wizard. This behavior is so counterintuitive that I skipped right over the documentation and had to figure it out by trial and error. But it is documented, so I suppose I have no grounds for complaint.

As for the wizard itself, I suppose I could get picky. I could complain that it gives you no opportunity to override the idiot Toolbar1 name that it likes and writes code for. When the wizard is done, if you change the name to something more reasonable, you also have to go into the code and change the name of the Toolbar1_ButtonClick that it creates for you. I could point out that you still have to plan everything out carefully in advance, because once the wizard is through creating its toolbar, you can't use it to make any changes. You have to make any further changes in the property pages, which is where all the functionality of the wizard should be, but isn't. Despite the problems and the clumsy design, the wizard is still a time-saving improvement.

Unfortunately, the new ImageCombo control has the same stupid design as Toolbar, and has just as much need of a good set of property pages for laying out the pictures and text at design time. In fact, the ImageCombo property pages are even weaker than those of the Toolbar. If the control designers can't do the property pages, they should at least do a wizard. Instead, ImageCombo must be put together at run-time, which is a fine if you're creating items dynamically, but is inconvenient for the many situations where you know exactly what you want from the start.

If you think the Toolbar and ImageCombo controls are poorly designed, take a look at the new Coolbar control. But this isn't supposed to be a flame about Coolbar's design or documentation. It's a note about a note warning that Coolbar requires Internet Explorer 3.0 or higher on any computer to which you distribute it.

Whoa! If my new interest calculator has a Coolbar, I have to distribute Internet Explorer? Sounds like I'm being recruited as a soldier to help Microsoft crush Netscape. This seemed too bizarre to believe. I sent mail to a source on the VB team and learned that the situation isn't quite as weird as it sounds, although it is pretty weird.

What your customers need to use a Coolbar is a recent version of COMCTL32.DLL--one shipped after Internet Explorer introduced Coolbars. You have a recent version because you got it with Visual Basic. All your customers with Windows 98 or Windows NT 4.0 have it. Everybody who bought a computer with Windows 95 installed in the last couple years has it. But those foolish enough to wait in line all night to buy the original Windows 95--remember that hoopla?--may be in trouble. If they have never downloaded Internet Explorer 3.0 or 4.0 and still have the original DLLs from the operating system, they don't have the required COMCTL32.DLL. You can't legally give it to them because this DLL isn't in the list of redistributable files in REDIST.TXT on the Visual Basic or Visual Studio CD.

My source assured me that this has absolutely nothing to do with browser competition. If every application tried to redistribute the system DLLs it requires, users would likely end up with unmatched DLLs, resulting in all the computers of the world crashing even before the year 2000. Therefore Microsoft wants you to get all your system DLL updates from one official source. And for reasons that again have absolutely nothing to do with browser competition, Internet Explorer is that one and only official source for Windows 95 system DLL updates.

This policy is not all that popular with the Visual Basic design team. They're selling languages, not browsers, and would like to see an official service pack update that doesn't require IE. In fact, they went to a great deal of trouble to avoid the problem. Instead of being thin wrappers for Windows controls in COMCTL32.DLL, the VB6 common controls have the control code statically linked into the OCX files. So there are two ways you can use and distribute common controls. You can use the new files as Microsoft recommends:

MSCOMCTL.OCX    1,062,704
MSCOMCT2.OCX      644,400
                ---------
                1,707,104  

Or you can use the old files:

COMCTL32.OCX      609,584
COMCT232.OCX      161,800
                ---------
                  771,384

That looks like a pretty clear winner for the old system until you consider that the old files require the updated version of COMCTL32.DLL that can be distributed only with Internet Explorer.

So far we've got a reasonable choice (although one requiring a lot of disk space) if we don't want to require our customers to have IE. But there's one more file to consider:

COMCT332.OCX      369,696

This library apparently contains only one common control--Coolbar. Somehow it slipped through the cracks when they were reorganizing the control files. It is the only common control that depends on a recent COMCTL32.DLL. You might be fooled if you check it in the Dependency Walker, which only shows statically linked DLLs. Apparently it links with COMCTL32.DLL at run time.

Now before we get to what you can do if you want to use Coolbar, keep in mind that the same issues apply to SHLWAPI.DLL. This DLL has a lot of useful functions that are particularly valuable for Visual Basic programmers, but the potential distribution problems are even greater than for Coolbar. Every 32-bit Windows user has a COMCTL32.DLL even if it's not the one containing Coolbar functionality. But original Windows 95 users may not even have a SHLWAPI.DLL on their disks.

None of this will be a problem for the many VB programmers developing in-house software for companies that have standardized on Internet Explorer, Windows 98, or Windows NT 4.0. But what if your client company uses Windows 95 and Navigator? And what if you're developing shrink-wrapped software for unknown customers? Somewhere out there you have some customers who bought Windows 95 back in 1995 and haven't upgraded since. They're just doing whatever they do with their computers, and they don't care about your distribution problems. They may not even have Internet access and may never use a browser. Or they may be Navigator fans. You don't know how many there are, or how much trouble they can cause, but you do know that if you use Coolbar controls (or SHLWAPI functions), your programs will fail on their machines.

Here are some choices:

  1. Don't use Coolbar. You could try to find a third-party control that does the same thing. But one of the main reasons for upgrading to VB6 is to get updated controls. You paid for Coolbar and should be allowed to use it.
  2. Ship IE with your product. If the product is a major new word processor with lots of Internet features, maybe. But if it's a mortgage calculator or a new solitaire game, you don't want to ship a large unrelated product just to use one control.
  3. Put "Requires Internet Explorer 3.0 or higher" as a requirement on the box. Tell people where to download IE on the net and let them handle it. This is obnoxious in every way. Customers must install a program they may not want just to use your program. It takes up their time and their disk space. Furthermore, you appear to be taking Microsoft's side in the browser war.
  4. Ship COMCTL32.DLL or SHLWAPI.DLL with the product and let Microsoft sue if they don't like it. Call it civil disobedience. Realistically, Microsoft already has enough legal problems. I doubt they're going to call attention to what looks like an anticompetitive policy. But who knows. If they did take sue, winning might cost as much as losing. Besides there's a real risk of causing the incompatible DLL problems Microsoft is trying to prevent.

Maybe Microsoft would be satisfied with a virtual IE installation. During setup check the registry to see if a recent IE is installed, and if not, put up a message saying "Virtually Installing Internet Explorer." Copy a recent COMCTL32.DLL and SHLWAPI.DLL to the user's disk. Then put up a message saying "Virtually Uninstalling Internet Explorer." Create and delete some temporary files in a loop to make the disk grind for a few seconds, and then continue as if finished.

No! Forget I said that. It's a bad solution. Unfortunately, I can't think of a good solution.

Note: This summary is based on a mail exchange with a VB program manager in September, 1998. It was accurate when I wrote it. Fortunately in November, 1998, Microsoft released an update of the common controls DLL that partially corrects the situation. The COMCTL32.DLL update is available somewhere on the Microsoft web site. By shipping this update with your product, you can ensure that your customers have the latest version. This means that the Coolbar control will work reliably and that you can use the smaller COMCTL32.OCX rather than MSCOMCTL.OCX. The update does not fix similar problems with SHLWAPI.DLL. According to Microsoft notes on the update, it was designed not to solve our distribution problems but to address known Year 2000 problems. Perhaps the end of the world isn't going to be so bad after all.

Page 687-90. I specifically described the problems with TreeView described in "The TreeView Control" to the VB program manager in charge of this control after VB5 was completed. The VB6 TreeView has a few cosmetic enhancements, but none of its real problems are fixed. I expect the reason is that these are such fundamental flaws in the design that you can't just fix TreeView. You have to start over and redesign the control from scratch. I don't ever expect to see a redesigned TreeView from Microsoft, but perhaps you can buy one from some other company.

Page 692. "The ListView Control" shows a sample function called ListItemFromLinePosition, but the code is incorrect. The correct code is:

Function ListItemFromLinePosition(lvw As ListView, _ 
                                  ByVal y As Single) As ListItem
    Dim rc As RECT, i As Long, c As Long, dy As Long
    c = lvw.ListItems.Count
    If c = 0 Then Exit Function
    ' Get the height of a single item
    rc.Left = LVIR_BOUNDS
    SendMessage lvw.hWnd, LVM_GETITEMRECT, ByVal 0&, rc
    dy = rc.bottom - rc.Top
    ' Calculate the index of the item under the mouse pointer
    i = lvw.GetFirstVisible.Index - 1 + _
        ((y \ Screen.TwipsPerPixelY) - (dy / 2)) / dy
    ' Return the item (if any)
    If i > 0 And i <= c Then
        Set ListItemFromLinePosition = lvw.ListItems(i)
    End If
End Function

The code that calls this function later on the page should be adjusted accordingly:

Private Sub lvwFiles_DblClick()
    ' Use module-level yItem saved from MouseDown event
    Dim item As ListItem
    ' Determine if user double-clicked anywhere on the line
    Set item = ListItemFromLinePosition(lvwFiles, yItem)
    If Not item Is Nothing Then
        Debug.Print "Double-clicked on the line of item: "
& item.Text
    End If
    ' Make any click on the line select the item
    Set lvwFiles.SelectedItem = item
End Sub

Page 693. The last text paragraph of the book (before the final sidebar) discussed the problem of figuring out which column a user clicked on. Martin Naughton came up with a solution that works under VB6. The new code uses the new VM_SUBITEMHITTEST message, which appeared in recent updates to COMCTL32.DLL (including the version supplied with VB6). They also exist in the new MSCOMCTL.OCX. Unfortunately, the VB6 ListView was not updated to take advantage of this new message. There is no new SubItemClick event or other feature for identifying column clicks. You have to hack with SendMessage.

The MSDN help viewer used by VB6 recognizes and handles clicks in any column. For example, select Index from VB's Help menu. In the MSDN help viewer, type in Else. You'll get a dialog box with a list of several contexts for Else. If you click in the second column of the second item, you'll get the second item. One of my pet peeves with the VB5 version of the MSDN reader as that if you clicked in any column other than the first and you'd get the first item no matter which item you clicked. My bet is that they used VB_SUBITEMHITTEST to fix this in the new version. Unfortunately, the MSDN viewer developers didn't pass this tip on to the VB control developers.

In any case, here is a procedure that tells you everything you wanted to know about what the user clicked:

Sub SubItemHitTest(lvw As ListView, _
                   ByVal x As Single, ByVal y As Single, _
                   iRow As Long, Optional iColumn As Long, _
                   Optional afWhere As Long)
                   
    Dim hittest As LVHITTESTINFO
    ' Set x and y before sending message
    hittest.pt.x = (x \ Screen.TwipsPerPixelX)
    hittest.pt.y = (y \ Screen.TwipsPerPixelY)
    Call SendMessage(lvw.hWnd, LVM_SUBITEMHITTEST, 0&, hittest)
    ' Return the data from the message in reference parameters
    
    ' Convert to 1-based row and column
    iRow = hittest.iItem + 1
    iColumn = hittest.iSubItem + 1
    ' Indicates what was clicked: LVHT_NOWHERE,
LVHT_ONITEMLABEL, etc.
    afWhere = hittest.Flags
End Sub

This function could be called from ListView event MouseDown or from the DoubleClick event if the x and y positions have been saved elsewhere. In this example the optional afWhere argument has been omitted:

Private Sub lvwFiles_DblClick()
    Dim iRow As Long, iColumn As Long
    ' Use module-level xItem and yItem saved from MouseDown event
    SubItemHitTest lvwFiles, xItem, yItem, iRow, iColumn
    Debug.Print " Row = " & iRow & " : Column = " & iColumn
End Sub

Author Bio
My email address is given in the author bio, which comes after the index in the paper version of the book or at the end in the online version. I'm not changing my address, but please don't use it to send me email about Visual Basic or to report bugs in the book or in this update. The Visual Basic portion of my life has come to an end. I've done my best over the last few years to raise the bar for Visual Basic programming. I tried to promote good programming practice. I researched the more obscure areas of Windows and Basic programming. I lobbied for improvements in the language. I tried to be a mentor for less experienced programmers. Sometimes I was successful; sometimes not. I had some fun. I had some disappointments and embarrassments.

For better or worse, that's all over. I'm moving on to new challenges in other languages. If you send me Visual Basic questions, arguments, bug reports, flames, diatribes, or compliments, I'll just send you a generic email response directing you to my Web site at www.pobox.com/HardcoreVB and to this update.

Mail about soccer is still welcome.

Other Code and Type Library Fixes
This section lists some of the Basic and ID code fixes that didn't correspond to any page number in the book. Not every bug fix or code change is listed.

Swap procedures fail. The Utility module has SwapBytes, SwapIntegers, and SwapLongs procedures for exchanging variables. Lynn Torkelson noticed that I had carelessly passed the input variables ByVal instead of ByRef, thus swapping temporary stack copies of the Bytes, Integers, and Longs in question. Furthermore, the Integer and Long versions used Byte for the type of the temporary variable. I never used these functions in my code, and thus never realized that they were so badly broken. They're fixed in the update.

Donald Moore reminded me of an old assembly language trick for swapping variables without using the temporary variables. It turned out, however, that the tricky code using the Xor operator was slower than the obvious code using a temporary variable. Judge the two techniques for yourself:

Sub SwapIntegers(w1 As Integer, w2 As Integer)
#If 1 Then
    ' Obvious way uses temporary variable
    Dim wTmp As Integer
    w1 = wTmp
    w2 = w1
    w1 = wTmp
#Else
    ' Tricky way uses Xor operator
    w1 = w1 Xor w2
    w2 = w1 Xor w2
    w1 = w2 Xor w1
#End If
    ' Obvious way is faster
End Sub

LineWrap and WordWrap problems. The Utility module had two functions, WordWrap and LineWrap, that attempted to do exactly the same thing--convert a string of words into a justified lines of text separated by carriage return/line feeds. Unfortunately, LineWrap didn't work at all, and WordWrap didn't handle errors and unexpected situations very well. In the updated version, WordWrap is fixed, and LineWrap is simply a wrapper for WordWrap. Lynn Torkelson noticed the problem with LineWrap, and I discovered the rest of the story when I tried to fix it.

GetExtPos fix. The GetExtPos function in the Utility module is supposed to return the position of the extension within a file name. It didn't work correctly, but has been fixed.

RegTlb fix. The RegTlb program has been changed to use GetQToken rather than GetToken to parse command lines. This allows it to recognize quoted long file names with embedded spaces. Several other programs that accepted command lines have been changed in the same way.

ERROR constant removed. The Windows API Type Library (Win.tlb and Winu.tlb) contained a constant called ERROR that conflicted with the Visual Basic Error function. As a result, you couldn't use the Error function when the type library was loaded. You had to resolve the conflict by qualifying with the appropriate library name:

Dim s As String, i As Long
s = VBA.Error(4)    ' Use Visual Basic function
i = Win.Error       ' Use type library constant

The ERROR constant is an obscure GDI constant used in region handling. Windows should never have used this common word for such an obscure constant, and fortunately it provides RGN_ERROR as an alias (the value is 0). In the unlikely event that you need to use this constant, use RGN_ERROR. Pete from Wales (no last name given) was the first to report this.

SystemParametersInfo function fixed. The SystemParametersInfo function did not have ANSI and Unicode versions, and therefore did not work. Readers had to write a Declare statement to override the faulty type library entry. Even when the entries were added, the function did not work if the third parameter pvParam was a string. This is because the parameter was coded in IDL as void *, an approximate equivalent of VB's As Any. This issue is discussed in Chapter 2, "Typeless Strings," page 97. My solution is to provide a separate alias for strings. Use the SystemParametersInfoStr function when you need to give a string for the third argument. Don Brader and Kirk Roybal pointed out that the function didn't work, and Kirk also noted the problem with string parameters.

New Common Controls code added. The type library did not provide many of the entries for common controls (from the C header file COMMCTRL.H). Visual Basic comes with controls that implement tree views, list views, and most of the other common controls. Therefore, I gave these parts low priority and didn't include them. They were important to Michael Robert and Don Brader, who added the common control code to their private versions and sent me source code that I use in the current version.

NT-Only functions marked. Generally I tried to avoid putting functions that work only for Windows NT in my type library because I didn't want to set a precedent that would require adding hundreds of new functions. Lots of Visual Basic programmers send me interesting questions about how to program for Windows NT, but my answer is always the same: Hardcore Visual Basic for Windows NT is a different book written by somebody else.

Russell Davis pointed out my inconsistency. My type library had quite a few NT-only functions that weren't identified as such. These were added accidentally, but I didn't want to remove working functions. I have added a note in the help text for the NT-only functions I could identify. These include: BeginUpdateResource, UpdateResource, EndUpdateResource, IsTextUnicode, ArcTo, CancelDC, MaskBlt, and PlgBlt.

HFONT type incorrect. The HFONT type was defined to a typedef that evaluated to void *, which showed up in the object browser as As Any. The problem was described earlier. The type of HFONT is Long in the current update.

Illegal field in BITMAP structure. The BITMAP structure defined the field bmBits with LPVOID (an alias for void *). This shows up as As Any in the object browser and causes a crash if a programmer attempts to use the field. A similar problem occurred with the SYSTEM_INFO structure and in several other structures. These caused numerous problems in VB6 (as described earlier in this document). I have attempted to find change all these structures in the current version. I give the fields the PTR type (an alias for Long). In some cases, it is possible to use these fields in Visual Basic by using CopyMemory to copy the data in the pointer references to a string or array that can be manipulated in VB. Eric Daniel reported this bug.

Illegal unsigned integers. Some of the more obscure structures in the standard include files required by MIDL contain unsigned integers. This didn't affect most users because these fields are seldom used. But hardcore programmer Brian Wood isn't an ordinary programmer. He was trying to implement a type library routine that would retrieve the helpstring. He needed to use the TYPEATTR structure, which used the IDLDESC structure, which contained unsigned long integers. In the current version, I believe that I have removed all unsigned integers, even the obscure ones.

Unnecessary attribute on registry functions. The type library uses the IDL [usesgetlasterror] attribute on functions to tell Visual Basic to store the error value from the GetLastError function into the LastDllError property of the Err object. This storage increases the overhead of API calls by a small but measurable amount. The registry functions are almost unique among non-COM API functions in that they put the error in a Long return value rather than storing it for retrieval with the GetLastError function. Roy Raby pointed out that the overhead of [usesgetlasterror] is redundant for registry functions, so I have removed them. I applied the same fix to the many COM functions that return HRESULT.

Questionable includes removed. The type library had some include statements that include the files pshpack2.h, but I didn't include that file with my source. This caused problems for programmers trying to build my type library. The pshpack include files have pragma statements that control packing of structures. I removed the include files and replaced them with the equivalent pragma packing statements.

Miscellaneous functions fixed. WinExec, cdtDrawExt, GetCursorPos, ClipCursor, and GetClipCursor were defined incorrectly and have been fixed. The following functions were added based on user requests: DrawText, SetMapMode, SetSysColors, PtInRect, FillRect, RegisterClipboardFormat, CreateWindowEx, DrawText, GetCurrentThreadId, AttachThreadInput, and SetMapMode. I also added some new constants and types.

No Rect functions were provided for the second edition because I didn't give them high enough priority. They have now been added. Note, however, that PtInRect cannot be implemented as it appears in the Windows API because it takes a POINT structure passed by value. VB does not support passing ByVal UDTs, as explained in Chapter 6, "Window position and size," page 313. Therefore, PtInRect takes its arguments as separate X and Y values rather than a single POINT UDT. The function signature looks like this:

PtInRect(lprc As RECT, ByVal x As Long, ByVal y As Long) As Long

Among those reporting incorrect or missing functions were Jan-Christof von Halle, Gabor Kalman, David Smith, Don Bradner, Ben Curthoys, Russell Davis, David Mekelburg, Bennoit Cerrina, Daniel Mailman, and Eugene Chernyak.

Next Page   Previous Page   Table of Contents