Questions & Answers - Windows

Q:

I'm writing a Windows application that I want to run single-instance-only, and I'm having two problems. First, it appears that a user can circumvent the WinMain

hPrevInstance argument mechanism (by which Windows informs an application that a previous instance is already running) by simply renaming the appplication's EXE file. I would like the user to be able to rename the EXE to whatever he or she wants, but running an accidental second instance would corrupt a dedicated set of files that the application uses. If I can't count on hPrevInstance, what can I do?

Second, if I can successfully retrieve a previous instance's hPrevInstance, how can I use hPrevInstance to make the previous instance of the application the active application instead of starting a second instance? I've noticed that the Windows 3.0 Control Panel does this under normal circumstances, and it's a nice effect.

Bill Dow

Alameda, CA

A:

Under the WindowsÔ graphical environment versions 1.x and 2.x, it was indeed possible to circumvent the hPrevInstance mechanism by renaming the EXE file. In Windows1 3.0, simply renaming the EXE file will no longer work, but it is still possible to trick Windows (intentionally or accidentally) by another method.

All that Windows compares to detect previous instances are the up-to-eight-character external name (the EXE filename, ignoring any path) and the up-to-eight-

character internal name (the NAME in the application's DEF file, which becomes embedded in the EXE file). Obviously, they may differ. For the external names, it is important to note that the path leading to the EXE file is totally ignored in previous-instance comparisons, since there is no path associated with the internal name that could be used in the comparison. To see a list of internal names, run the Windows Software Development Kit HeapWalker program and scan the OWNER-NAME column.

Earlier versions of Windows compared only the external name of the application to be loaded with the internal name of all the applications already loaded. If a match was found, Windows assumed that a second instance of the first application was to be loaded. Windows did not need to reopen the EXE file for the second instance and look for its internal name, because it was already opened. If no match was found, Windows assumed that the application being loaded was a first instance, with the result being an hPrevInstance equal to 0. This is why renaming the application's EXE file would trick earlier versions of Windows into loading duplicate code segments and would fool the second instance of "single-instance" applications into always thinking it was the first instance.

In Windows 3.0, renaming the EXE file will no longer cause that problem, because Windows now remembers the external names along with the internal names of all running applications. Renaming an EXE no longer matters; the second instance of your application will be able to use hPrevInstance to detect that it is the second instance. This new method does lead to an interesting side effect. Copy NOTEPAD.EXE to your temporary subdirectory and rename it CONTROL.EXE. Run it explicitly, that is, run C:\TMP\CONTROL.EXE. Now try to run Control Panel from the Program Manager. You'll get a second instance of Notepad!

So how do you trick Windows 3.0 into running two copies of a single-instance application? Simply make a second copy of the single-instance application and give it a new name. Then rename the original application to a third name. The application then has two different external names that are both different from the internal name of the application. You can then run both EXE files as if they were different applications, even though they are not.

You should ignore the WinMain hPrevInstance argument entirely, and go directly for the hWnd of the first instance. The first few lines of your WinMain function should look like this (assuming that your main window's class is named KillerApp).

auto HWND ahWndPrev;

if (ahWndPrev = FindWindow("KillerApp", NULL))

{

BringWindowToTop(ahWndPrev);

return 0;

}

All single-instance applications should do this! Not only does it ensure that only one instance of the application runs, it is much friendlier to the user than simply terminating or putting up a rude message box that the application is already running. This elegantly takes care of the situation where the first instance's window may be completely covered by another window.

Q:

I have an application in which I need to display a number of different bitmaps both in the client area and on paper. I need to display them isotropically; in other words, the aspect ratio of the bitmaps needs to be maintained. My problem is that the bitmaps are of various sizes, known only at run time when the bitmaps are read, and are generated on devices with different resolutions and aspect ratios, also known only at run time. Windows offers the isotropic mapping mode, but I can't figure out a way to fit the bitmaps into that mapping model all at the same time.

Rick Mosebach

New York, NY

A:

I've written a short function named MakeIsotropic that will perform the isotropic calculations for you (see Figure 1). Do the isotropic calculations manually with MakeIsotropic, and then use BitBlt to draw the bitmaps with the width and height values generated by the function. You should do all this in the default MM_TEXT mapping mode, so that you have complete control over the coordinate system.

The problem you describe involves eight variables, which result from the permutations for each bitmap of the source/destination, the pixels/pixels-per-inch in each direction, and the horizontal/vertical width/height:

DPX: destination pixels horizontal DPY: destination pixels vertical DIX: destination pixels per inch horizontal DIY: destination pixels per inch vertical SPX: source pixels horizontal SPY: source pixels vertical SIX: source pixels per inch horizontal SIY: source pixels per inch vertical

Given these variables, the following formula will make them perfectly isotropic.

DPX DIY SPX SIY

-- x -- = -- x --

DPY DIX SPY SIX

In your case, though, and in Windows in general, the DPX and DPY are a function of how the user sizes the destination. You have to convert these formulas so that you can calculate a new DPX or DPY based on the requested size of the target. You can calculate either DPX or DPY using the following formulas, which are essentially how the isotropic calculations are performed.

DIX SPX SIY

DPX = DPY x -- x -- x --

DIY SPY SIX

DIY SPY SIX

DPY = DPX x -- x -- x --

DIX SPX SIY

MakeIsotropic works as follows. The developer must specify pointers to DPX and DPY and the values of the other six variables. Either DPX or DPY is decreased so that the resultant isotropic rectangle can fit in the original rectangle specified by these two variables.

The while tests solve the potential overflow problem when calculating the numerator and denominator. For each component of the numerator and denominator, they check to see if the variables are not odd (therefore, even); while both are even, each is divided by two. Theoretically you could perform nine while tests for each numerator/denominator-component permutation, but these five should be sufficient.

The numerator and denominator are then calculated, and another while loop is executed to reduce both of them to prevent overflow. Once this is done, a temporary value for DPY is calculated and compared to the original DPY. If the original value of DPY is less than the new isotropic value, DPX must be recalculated. Otherwise, the new lower value of DPY replaces the original value. DPX and DPY are now an isotropic rectangle within the original DPX and DPY.

Q:

How does SMARTDRV.SYS operate at a low level, and how can it be best used?

T. J. Kelly

Atlanta, GA

A:

SMARTDRV.SYS is a disk-caching device driver that operates with both DOS and Windows. It minimizes the number of times that your physical disks must be accessed as DOS/Windows is running, since disks are relatively slow. Generally, there are four ways in which disks can be cached: by sector, by cluster, by track, or by cylinder. SMARTDRV operates as a track cacher.

Because it's a track cacher, SMARTDRV deals with tracks when it talks to the disk controller. When an application asks DOS or Windows to read a sector, SMARTDRV intervenes. It asks the disk controller for the entire track that the sector resides on, and passes the sector back to the application. This way, the entire track that the disk's read/write heads are over is obtained while the heads are there, minimizing head movement. (Head movement is fairly slow.) Then when the application asks for the next sector, SMARTDRV already has it in its memory buffers and returns it immediately.

There are two considerations when using SMARTDRV (or any track cacher, for that matter) that users generally do not appreciate. The first is that not all disks are interleaved optimally, so reading an entire track as the disk spins may take longer than is actually possible. The second is that a high degree of file fragmentation destroys the benefits of a track cacher, since a particular file may be broken up inordinately across many tracks.

Interleaving is the low-level way to refer to specific sectors on a hard disk. Standard IBM PC hard disks have 17 sectors per track. COMPAQ PCs are a little more varied, with sectors per track at the following points: 17, 25, 26, 33 and 34. A 1:1 interleave means that, on each track, sector 2 physically follows sector 1, sector 3 physically follows sector 2, and so on.

1, 2, 3, 4, 5, 6, 7, 8, 9, 10,...

A 2:1 interleave on a 17-sectors-per-track disk would physically be laid out like the following:

1, 10, 2, 11, 3, 12, 4, 13, 5, 14, 6,...

And a 3:1 interleave on a 17-sectors-per-track disk would physically be laid out like this:

1, 7, 13, 2, 8, 14, 3, 9, 15, 4,...

The result of all this is that a 2:1 interleaved disk takes twice as long to read as a 1:1 interleaved disk, because the disk has to rotate twice for each complete track to be read sequentially by sector, a 3:1 interleave takes three times as long, and so on. The reason that disks are interleaved at ratios other than 1:1 is that not all disk controllers in combination with the BIOS are fast enough to read 1:1 interleaved disks. Sector 2 on a 1:1 interleaved disk may already be passing by a slow controller when the controller needs to retrieve it after sector 1, thereby having to wait an entire disk revolution for it; a 2:1 interleave would be a better choice for that disk using that controller.

Unfortunately, not all manufacturers calibrate the disks with the installed controllers. Many simply choose a safe 3:1 interleave. Programs such as Gibson Research SpinRite II can perform a nondestructive low-level format that takes all of this into account. On one of my computers, I went from a 3:1 interleave to a 1:1 interleave because of a fast new $125 Western DigitalÔ WD1006V-MM2 controller I bought-and Windows performance doubled! The disk light stayed on for a third of the time it used to each time SMARTDRV retrieved a track.

The second consideration is that a lot of file fragmentation destroys the benefits of SMARTDRV, because files may be broken up inordinately across many tracks. If you specify a 512Kb-minimum SMARTDRV on a 17-sectors-per-track (8.5Kb-per-track) disk, SMARTDRV can cache 60 tracks (512Kb / 8.5Kb = 60). An 8Kb contiguous-sector file may reside entirely on a track, at best; at worst, if the file was highly fragmented, it may be spread across 4 tracks (on a 2Kb-per-cluster disk). In the worst case, an 8Kb file would be using up 34Kb (6.7 percent) of SMARTDRV's cache memory.

Since Windows does a lot of object (code, icon, cursor, etc.) discarding and reloading, you must minimize file fragmentation to receive the benefits of SMARTDRV. To see how much your hard disks are fragmented, you can download CHKFRG.ZIP (which contains CHKFRAG.EXE) from the PC MagNet Utilities forum on CompuServe (GO PCMAGNET). Programs such as PC Tools can periodically unfragment your hard disks and improve the performance of SMARTDRV, DOS2, and Windows.

Q:

Some aspects of dialog boxes confuse me. Why are DialogBox dialog boxes modal and CreateDialog dialog boxes modeless? What is the difference between the return value of a DlgProc and the value specified in EndDialog, and why is the separate function necessary?

Alan Cooper

Menlo Park, CA

A:

When you call DialogBox, it first creates a dialog pop-up window using its own internal DlgProc (not to be confused with the DlgProc you supply), then creates all the control child windows using the standard internal CtlProcs (such as Button, Edit, Static). Next, DialogBox calls the DlgProc you furnished with a WM_INITDIALOG message, displays the dialog box window, and finally goes into a GetMessage / TranslateMessage / DispatchMessage loop. This loop is essentially in the dialog manager, and it is in control as far as you are concerned. By disabling the owner of the dialog box and going into its own loop, the dialog that appears is modal, since only a few messages (WM_PAINT, for example) get dispatched by the dialog manager to your WndProcs.

When you call CreateDialog, it follows the same procedure as above, but does not go into a similar GetMessage / TranslateMessage / DispatchMessage loop. Instead, it returns control immediately to the routine containing the CreateDialog call. Because it doesn't go into the loop, the dialog box that appears is modeless and all messages are dispatched by your main GetMessage / TranslateMessage / DispatchMessage loop as they would normally. This is why you have to add IsDialogMessage calls to you main loop for modeless dialog boxes. When you make an IsDialogMessage call, the dialog manager manages the dialog controls just as it does in its internal loop in the DialogBox code. The dialog manager essentially becomes a stub off your main loop.

In both cases, your DlgProc becomes a stub off the dialog manager code. The dialog manager handles most activities associated with the dialog using its own internal DlgProc, but calls your DlgProc in cases where you need to take some action. This is why you do not call DefWindowProc in your DlgProc; the DefWindowProc call is done inside the dialog manager's internal DlgProc.

The return value of your DlgProc simply communicates information back to the dialog manager. In the case of DialogBox, you need to communicate explicitly to the dialog manager's message-processing loop when to terminate; this is done by the EndDialog function. Since the dialog manager's loop is not in control of dialog boxes created with CreateDialog, you do the DestroyWindow call yourself instead of calling EndDialog.

1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally.

2 As used herein, "DOS" refers to the MS-DOS and PC-DOS operating systems.

Figure 1. MakeIsotropic

extern int FAR PASCAL MakeIsotropic(

WORD FAR * afpwDPX, // dst horz pixels

WORD FAR * afpwDPY, // dst vert pixels

WORD awDIX, // dst horz pixels per inch

WORD awDIY, // dst vert pixels per inch

WORD awSPX, // src horz pixels

WORD awSPY, // src vert pixels

WORD awSIX, // src horz pixels per inch

WORD awSIY) // src vert pixels per inch

{

auto DWORD adwNum, // DIY SPY SIX

adwDen; // DPY = DPX x --- x --- x ---

auto WORD awTmp; // DIX SPX SIY

if (*afpwDPX == 0 || awDIX == 0 || awSPX == 0 || awSIX == 0||

*afpwDPY == 0 || awDIY == 0 || awSPY == 0 || awSIY == 0)

return -1;

while (!(awDIX & 1) && !(awDIY & 1)) { awDIX /= 2; awDIY /= 2; }

while (!(awSPX & 1) && !(awSPY & 1)) { awSPX /= 2; awSPY /= 2; }

while (!(awSIX & 1) && !(awSIY & 1)) { awSIX /= 2; awSIY /= 2; }

while (!(awDIX & 1) && !(awSIX & 1)) { awDIX /= 2; awSIX /= 2; }

while (!(awDIY & 1) && !(awSIY & 1)) { awDIY /= 2; awSIY /= 2; }

adwNum = (DWORD)awDIY * awSPY * awSIX;

adwDen = (DWORD)awDIX * awSPX * awSIY;

while (!((WORD)adwNum & 1) && !((WORD)adwDen & 1))

{ adwNum /= 2; adwDen /= 2; }

awTmp = (WORD)((DWORD)*afpwDPX * adwNum / adwDen);

if (*afpwDPY < awTmp)

*afpwDPX = (WORD)((DWORD)*afpwDPY * adwDen / adwNum);

else

*afpwDPY = awTmp;

return 0;

}