Ask Dr. GUI #39

Oddly enough, Dr. GUI doesn't have a lot to say this month besides questions and answers. Maybe it's because he speaks his piece every week in his Web column (http://microsoft.com/msdn/). Recent topics include performance, XML, COM, using DHTML as your user interface, and J/Direct. Check out the archive at http://microsoft.com/msdn/archive/bycolumn.htm.

By the way, the final versions of add-ins for Microsoft Visual C++ and Visual Basic that enable you to develop for Windows CE–based devices (including Handheld PC, Palm-size PC, and Auto PC) are now available. The add-in for Visual J++ is in beta as of this writing. For more information, visit http://microsoft.com/windowsce/developer/.

And now for your questions . . . 

Separating Siamese twin threads

Dear Dr. GUI,

I used to think that I had a basic (no pun intended) understanding of threads. But, the more I read, the more confused I get. My understanding is that my main app has to be an ActiveX .exe. No problem. But, I have DLLs that I want to run in their own threads. But, I am now starting to understand that in Visual Basic, a DLL will only ever run in the thread that instantiated it. Is this correct?

What I am trying to do is:

I have a point of sale (POS) system that prints out products (say a lift ticket at a ski resort or amusement park). I want the POS to send a class containing all the pertinent data to the "PrintEngine" DLL, which SHOULD be running in its own thread!!! So, the POS is not held up waiting for the printing process. Note: the printing process does NOT return any acknowledgment back to the POS, so no synchronization is necessary.

BUT, from what I am reading, my PrintEngine.DLL will only execute in the thread that instantiated it.

Sample of PrintEngine instantiation:

dim oMyPrintEngine as object
set oMyPrintEngine = CreateObject("MyPrintEngine.Engine")

Question: How should I build my PrintEngine? Leave it as a DLL? Make it something else? Just how do I get my PrintEngine to live off in its own corner away from my POS?

Thanks!
Robert G. Shurtleff III

Dr. GUI replies:

It's been a long time since Dr. GUI has had to separate Siamese twins. As you well know, such operations are always difficult because you have to figure out how to deal with shared organs.

So it is with Siamese threads: it takes careful work to separate the work you're doing into tasks that can be done with separate threads.

A component provided by a Visual Basic dynamic-link library (DLL) server never runs in its own thread: in-process components will always run on a client thread. Which thread the DLL will run on depends on whether the DLL is multithreaded or not. If the DLL is not multithreaded all components within that server will run on the thread of the first process to call the DLL server. If the DLL server is multithreaded, the component will run on the thread that created the component. Note that in neither case will the DLL's components somehow run on their own thread.

In order for the PrintEngine to run in its own thread, you will need to make the PrintEngine an out-of-process ActiveX .exe. Components in an ActiveX .exe have the ability to run in their own threads. If you create PrintEngine as a multiuse, multithreaded component, then each component in the ActiveX .exe will get its own thread and will execute in its own little corner away from the POS.

A word of caution with objects created by an ActiveX .exe. If you use the New operator to create the object (that is, Set objWidget = New Widget1) it is possible for the object to share a thread with another object created by the ActiveX .exe. To guarantee that each and every object created by the ActiveX .exe runs in its own thread, use the CreateObject function.

Another possible solution is to write the PrintEngine in Visual C++, where you can access the full power of Windows and COM and start any threads you like. That sure beats trying to separate Siamese threads!

Ya gotta follow da rulz

Dear Dr. GUI,

I am writing some code to add rules to a user's inbox. I have followed the steps outlined in the MAPI docs for creating a rule exactly, but when I finally insert the rule I only get partial success.

If I read a rule and then reinsert it, it works fine, but if I change the ACTION structure to mine

(HrStringToAction(......,"forward \"Calladine,Graham (Taverns)\"..) 

I get the partial success message.

Can you help?
Graham Calladine

Dr. GUI replies:

Well, if you want Microsoft Exchange to follow da rulz, ya gotta follow da rulz.

Believe it or not, the reason for the partial success is because the ACTION structure is incorrect for a forward action even though it is a valid ACTION structure. There are a couple of changes that need to be made in the code. First, we actually have to modify the Restriction by adding the FL_LOOSE option to the ulFuzzyLevel. For example:

lpRes->res.resContent.ulFuzzyLevel = 
FL_SUBSTRING | FL_IGNORECASE  | FL_LOOSE;

After your call to HrStringToAction(), you need to modify the address list returned in your ACTION structure. You need to add the PR_RECIPIENT_TYPE to the properties of the address list. The cValues of the address list is incremented by one. Below you will find an example that makes the necessary modification:

lpActs->lpAction->lpadrlist->aEntries->ulReserved1 = 1L;
lpActs->lpAction->lpadrlist->aEntries->cValues += 1;
lpActs->lpAction->lpadrlist->aEntries->
   rgPropVals[0].Value.MVbin.lpbin
+=
            sizeof(SPropValue)/sizeof(SBinary);
memset(&(lpActs->lpAction->lpadrlist->aEntries->rgPropVals[
            lpActs->lpAction->lpadrlist->aEntries->cValues-1
            ]), 254, sizeof(SPropValue));
lpActs->lpAction->lpadrlist->aEntries->rgPropVals[
      lpActs->lpAction->lpadrlist->aEntries->cValues-1
      ].ulPropTag = PR_RECIPIENT_TYPE;
lpActs->lpAction->lpadrlist->aEntries->rgPropVals[
      lpActs->lpAction->lpadrlist->aEntries->cValues-1
      ].Value.ul = MAPI_TO;

So with a little work, it is indeed possible to make Exchange follow da rulz.

Who is that masked control?

Dear Dr. GUI,

I want to set my masked edit control to read-only just like I did to the Textbox control. How can this be done in Visual Basic 5.0?

Aaron T.

Dr. GUI replies:

What, you want masks and locks? This is starting to sound like a psychological problem, no? Well, Dr. GUI's no psychologist, but he can answer this.

Ideally, MaskEdBox would support a Locked property as does TextBox, and you'd just set this property to lock the control. Note that MaskEdBox does support the Enabled property, but disabling the control also disables the ability to tab to the control—so you can't copy text in the control to the clipboard. If you don't need to be able to tab to the control, just disable it—it's much simpler.

But if you really want the locked read-only behavior, it's gonna take a little work. But relax: it's a quick surgery and you'll be home in no time.

One approach would be to use a roving TextBox with its locked property set to true and move/resize it over the MaskEdBox you wish to lock. This works, but it's somewhat cumbersome to implement—especially so if you have a lot of masked edit controls you want to "lock."

At first glance it appeared the answer would be to use SendMessage to send an EM_SETREADONLY message to the MaskEdBox. This works only if no mask is set—but what's the point of using the control if you don't set a mask?

On the other hand, why do you need a mask if you're not going to change the control? Since the EM_SETREADONLY message has no effect when a mask is on, how about storing the mask value, setting the control to use no mask, and then sending an EM_SETREADONLY message?

When you try this, you'll run into a few problems—especially when your MaskEdBox is bound to a DataControl. Changing the text of the MaskEdBox to display the same characters as were displayed when the mask was active causes the DataChanged property to be set to true. This can be troublesome if any of the displayed fields are not updateable. Fixing this requires checking the DataChanged property before canceling the mask, then resetting it after you cancel the mask.

But if you do this, you'll end up with a routine that can lock and unlock any number of MaskEdControls on a form. The lock status can be changed at any time, even after some fields have been updated. You could even write this as a subroutine that accepts a form as a parameter, then move it out of the form and into a .BAS module where all of your forms could access the same code.

See? That didn't hurt, now, did it?

Who's doing what when?

Dear Dr. GUI,

I'm having a problem with programming Schedule+ objects.

What I need to achieve is a weekly printed schedule of what "everyone" in the company is doing in the current week.

My problem at the moment is being able to access everyone's schedule. I'm trying to use the ScheduleForUser function/method but am a bit stuck on the how to get all the parameters for all the users which I need to pass it. How do I find the UserEntryID for each user in the system?

I'm sure there is something I have to do MAPI, but what?

Nigel Kitcher

Dr. GUI replies:

You wanna know where everyone is, huh? Even Dr. GUI's not omniscient. But, with Collaboration Data Objects (or CDO, formerly Active Messaging) you can at least know what people say they're doing in their schedules.

I'm assuming that you have a list of user names somewhere. (If you don't, I'll give you a way to get such a list later.) Given this list, there are a bunch of steps involved—too many to list here. But the key steps are to retrieve the EntryID for each user name, then pass it to the ScheduleForUser method of the Schedule+ application object. For the exact list of steps to take and sample code, please see article Q177216 in the Microsoft Knowledge Base, "HOWTO: Open Another User's Schedule Programmatically" (http://support.microsoft.com/support/kb/articles/q177/2/16.asp).

It's a little more work if you want to get a list of users from the system. The exact steps depend on how your global address list is set up, but they go something like this. From the Session object, open the AddressLists collection. Find the "Global Address List" and iterate through its AddressLists collections and AddressEntries collections to gather a list of all users. Each user would be an AddressEntry in one of the AddressEntries collections.

Note   Using this method, you are directly accessing a resolved AddressEntry, so you do not have to go through the steps of creating a message, adding a recipient and resolving the recipient. Once you get to the AddressEntry level when iterating through the lists, you have the ID property needed for the ScheduleForUser method.

For example (much left out):

Set AllAddressLists = Session.AddressLists
   For Each OneAddressList in AllAddressLists
   For Each OneAddressEntry in OneAddressList.AddressEntries
      ' Add code to process the user
      ' The properties required for ScheduleForUser are:
      '   OneAddressEntry.Type
      '   OneAddressEntry.Address
      '    OneAddressEntry.Name
      '   OneAddressEntry.ID
   Next
   Next

Have a MAPI day

Dear Dr. GUI,

I have a Windows for Workgroup application that uses simple MAPI to send fax via the built-in fax support, now I am working on the migration to Windows 95.

I want to use OLE-Messaging but I cannot figure out how to install the OLE-Messaging library. WIN.INI says OLEMessaging=1, MAPI=1, MAPIX=1, CMC=1, but the file MDISP.TLB is not on the disk so I can't use MAPI.Session, etc.

I have installed the MAPI 1.0 PDK for Windows NT on the 4.0 beta that was sent on the MSDN. After that, OLE-Messaging is present on the NT machine, but I want it on Windows 95 and I would like to know how to get it there.

Regards,
Lars Lundqvist

Dr. GUI replies:

Dr. GUI empathizes with you: it's important to get all of your nutrients, including your daily requirement of MAPI. Thankfully, your MAPI supplements are as close as the Web. The Active Messaging libraries (the new name for OLE Messaging) are available from the Microsoft Exchange "Application Farm" at http://backoffice.microsoft.com/downtrial/moreinfo/appfarm.asp. From this page, you can select download and the second paragraph of the resulting page contains a link to the Active Messaging Library download page. The file that you download not only contains the Active Messaging Library, but also a Help file containing information and examples for the object model and a readme.txt file with instructions for installation.

For more information on Active Messaging and the new Collaboration Data Objects, please see the following articles in the Microsoft Knowledge Base:

Q176916, "INFO: Active Messaging and Collaboration Data Objects (CDO)" (http://support.microsoft.com/support/kb/articles/q176/9/16.asp)

Q171440, "INFO: Where to Acquire the Active Messaging Libraries" (http://support.microsoft.com/support/kb/articles/q171/4/40.asp)

TURN THAT (*&Q(*&#!@#(*& SOUND DOWN!

Dear Dr. GUI,

I cannot find any DLL call or OCX for setting the general volume in windows like the system mixer play control does. I developed an application with ActiveMovie OCX and WebBrowser Control and both are playing sounds. I have to implement a general volume control (two buttons, one vol-up, one vol-down) which are controlling the whole sound system´s volume out of Visual Basic 5.0.

Hope that you can help me out of this frustration.

Any help appreciated!

Thanks in advance,
Albert Fischlmayr

Dr. GUI replies:

Dr. GUI is gratified that you're concerned for your users' hearing. It speaks volumes for your intelligence and compassion. There are few things more annoying than an application that's just too noisy. It's almost as bad as stealing focus from the foreground application.

Unfortunately, there is no simple API call to get or set the volume. It requires iterating through four layers of functions and structures to get or set the system volume.

First, call mixerGetNumDevs to locate the correct mixer device. Next, call mixerGetLineInfo to determine which line of the device controls the volume to the speakers. Then, call mixerGetLineControls to determine which control on that line lets you change the volume. Finally, call mixerGetControlDetails to obtain the structure containing the volume setting.

To actually change the volume you must work around Visual Basic's limited ability to handle pointers. (Or you could write a COM object in Visual C++ to do this.) The availability of the VarPtr() function in Visual Basic 5.0 smooths the way considerably. Note that you'll have to declare some of the structures and functions yourself because of bugs in the WIN32API.TXT file that shipped with Visual Basic.

By the way, it would probably be a good idea to put the volume back when your application exits—changing it permanently for the user is kinda rude.

Yo! App! Are you there?

Dear Dr. GUI,

Hi. I need to see if an application is running; if so I put it in the foreground, otherwise I run this program from disk. I can do this in Win16. How can I do the same thing in Win32?

Farioli Roberto

Dr. GUI replies:

Activating the window is easy—once you have a handle to it. It's finding the handle to it that's a bit tricky.

The most common and simplest approach is to use the FindWindow() and SetForegroundWindow() Win32 APIs. FindWindow() is used to determine if a particular application is running, and to get its top-level window handle. SetForegroundWindow() is used to activate the window. Note that this steals focus and activation from whatever application currently has it.

For example,

HWND hwnd = FindWindow("CalcWindowClassName", "Exact Title");
  if(hwnd) {
   // It was already running, so bring to foreground...
   SetForegroundWindow(hwnd);
  }
  else {
   // It wasn't running, so start it up...
   ShellExecute(NULL, "open", "calc.exe", NULL, "C:\\", 1);
  }

This will find the window created with the window class, CalcWindowClassName, which has the caption "Exact Title." If you wrote the application in C or C++, you can set the exact class name you want when you register your window class. If you didn't, you can use Spy++ to find the class name the author used. Or you can use NULL rather than a class name—which will find all top-level windows with that title regardless of class.

Potential problem: if there is more than one window with the class and caption you specify, you might not get the right one and might therefore activate the wrong window. That's why it's really nice if you can register your own window class with a unique name.

Another potential problem is that you don't know the exact title of the window you want to activate. If you have the class name and it's unique, you're ready to operate—see above. But, if the class name isn't unique, you can search through the top-level windows using the technique described in Microsoft Knowledge Base article Q147659 "HOWTO: Get a Window Handle Without Specifying an Exact Title" (http://support.microsoft.com/support/kb/articles/q147/6/59.asp). This article describes a FindWindowLike function that you can write.

A much harder and rarely used technique is to search for the process by its module name (See Microsoft Knowledge Base Q175030, "HOWTO: Enumerate Applications in Win32" http://support.microsoft.com/support/kb/articles/q175/0/30.asp). There is no portable way to implement this on both Windows 95 and Windows NT. You will have to write separate code for each platform. In Windows 95, you will need to use the Tool Help Library of functions, and in Windows NT, the PSAPI.DLL functions. After you get the process handle, you will then need to find a top-level window via EnumThreadWindows() to call SetForegroundWindow() on.

Help! I'm broken!

Dear Dr. GUI,

Why does MSDN break the Developer Studio Help? I installed Visual C++ 5.0 to have all the help I needed on my hard drive, so I did not need the CD. When I installed the MSDN, Developer Studio started to always demand the CD for any help! I could find no options to control this at all. Uninstalling and reinstalling Developer Studio did not help! How can I install Developer Studio to work off the local hard drive and only go to the CD when specific MSDN queries are called for?

Steve Rodkey

Dr. GUI replies:

What's going on here is that MSDN is replacing the help you installed when it installs. Usually, this is a good thing: it's handy to have all your help integrated in one place, and MSDN is a superset of the help that ships with Visual C++. However, in your case, this is a problem.

The root of this problem lies in the registry key: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\InfoViewer\5.0. The InfoViewer is a tool used to view Developer Studio's Online help. This registry key contains all of the information that the InfoViewer needs to organize the table of contents, create an index, and find help topics. When you installed the MSDN Library, it contained an updated version of the topics already on your hard drive. This caused the registry to point to the MSDN Library CD rather than your hard drive. You may be able to reset this key to the old values. It wouldn't be hard to have a couple of files that switch this registry key back and forth.

At this time, there is no supported way to install some topics on your hard drive, leaving others on the CD. Fortunately, there is a way to place all of the MSDN topics on your local hard drive. During MSDN Library setup, you are provided a checkbox to install "Books Online." Note that this requires approximately one gigabyte of space on your hard drive! Keep in mind that the majority of the MSDN files are copied to your <DeveloperStudio Path>\MSDN directory, even if you specify a different location in setup. However, if you are able to find the room on your hard drive, you should never be asked for an MSDN CD again.

If you don't have space to place the entire MSDN Library on your hard drive, consider placing it on a network drive. Typically the MSDN Library consists of two CDs. Both CDs should be copied to the same directory on the network. From your computer, map this network directory to a letter drive on your machine. Then launch SETUP.EXE to begin the MSDN installation. This time, do not check "Books Online." By installing from a network location, all MSDN topics will be retrieved across the network. Although the InfoViewer speed may decrease, this method avoids using the MSDN CD and saves local drive space. This is actually the method that Dr. GUI uses here at Microsoft.

Thanks!

Dr. GUI thanks his team of specialists for this column: Robert Jacik, Catherine Granger, Dennis Hanson, Chris Jensen, Diane Shytle, John Eikanger, Joe Crump, and Jason Roth. And he gives special thanks to the specialist coordinator, Rick Caudle, without whom this column would not be possible.