by Fran Finnegan
Fran Finnegan is chairman of Finnegan O'Malley & Company Inc., a Windows consulting and contract programming firm that is developing E-Mail ManagerÔ, an electronic-mail application.
QI’m implementing a custom control, which I call a scrolling button. It’s going to be used just like a scroll bar’s buttons to scroll through a large database. It differs from normal scroll buttons, though, because it understands a double-click means "go to the end" of the list. Therefore, it can command three different actions (within the DoubleClickSpeed time):
Button | Action | Meaning | Visuals |
down only | held-down | show more | "down" bitmap |
down-up | single-click | show next | "down" then "up" bitmap |
down-up-down | double-click | go to end | "down" then "up" bitmap, but without flashing |
I’m having two problems related to the double-click. It seems that the WM_LBUTTONDBLCLK message is always preceded by the WM_LBUTTONDOWN and WM_LBUTTONUP messages, which are really part of the entire double-click action. This causes it to process a "show next" before a "go to end," which takes time because the large database is accessed via a remote procedure call. "Show next" is not what the user intended. It also causes the button bitmap to flash, because it’s repainted on the WM_LBUTTONDOWN and WM_LBUTTONUP messages. How can I use just the WM_LBUTTONDBLCLK messages for double-clicks and eliminate the irritating button flashes?
Cathy Clark
Newington, CT
AMy solution is shown in Figure 1. There are probably other equally valid ways of doing this but this is how I would do it. The code would be part of your scrolling button window procedure. It solves both of your problems.
The first thing to do is to remove the CS_DBLCLKS class style from your scrolling button window class, since you will be determining the occurrence of double-clicks yourself.
On the WM_LBUTTONDOWN, note whether it is the first "down" or the second. If it’s the first, create a timer based on the WIN.INI [Windows] DoubleClickSpeed entry, capture the mouse, and show your pushed-button bitmap. If it’s the second, destroy the timer, post yourself a WM_USER_DOUBLECLICK, release the mouse, and show the unpushed-button bitmap. Process the double-click.
On WM_LBUTTONUP, simply note that it occurred. If a WM_LBUTTONDOWN follows, it’s a double-click (above); otherwise it’s a single-click, to be determined when processing a subsequent WM_TIMER message.
When the WM_TIMER message is received, kill the timer, post a WM_USER_SINGLECLICK (if a WM_LBUTTONUP message was flagged), or send a WM_USER_HELDDOWN message (if there was no WM_LBUTTONUP) to process it immediately. Then release the mouse, and show the unpushed-button bitmap. Process the single-click, if that was what was requested.
On the WM_USER_HELDDOWN that may have been sent, do a single held-down action in a loop along with a PeekMessage loop. This will then yield to other applications in a friendly manner as you scroll. You will break out of this loop when the WM_LBUTTONUP is received. (I wish more applications yielded like this.)
One thing you don’t monitor is WM_MOUSEMOVE messages. Since you’ve captured the mouse, all mouse messages will be sent to your scrolling button window procedure. The mouse coordinates may stray outside your window’s client area, in which case you may want to adjust the behavior and visuals accordingly. You might want to show the "unpushed" bitmap, or ignore subsequent WM_LBUTTONDOWN and WM_LBUTTONUP messages. You can either watch the WM_MOUSEMOVEs or check the lParam on the WM_LBUTTONDOWN and WM_LBUTTONUP messages.
Figure 1 Processing Button Clicks for a Scrolling Button Control
#define ID_TIMER_LBUTTON 1
#define WM_USER_HELDDOWN (WM_USER+0)
#define WM_USER_SINGLECLICK (WM_USER+1)
#define WM_USER_DOUBLECLICK (WM_USER+2)
// the following code would go in the window proc of the button
// NOTE: no CS_DBLCLKS style in this window proc’s class
static BOOL sbDown = FALSE, sbUp = FALSE;
case WM_LBUTTONDOWN:
if (!sbDown) // begin held-down, single-click or double-click
{
sbDown = TRUE;
SetTimer(ahWnd, ID_TIMER_LBUTTON, // success assumed
GetProfileInt("Windows", "DoubleClickSpeed", 452),
NULL); // Windows’ default speed: 452ms
SetCapture(ahWnd); // capture the mouse
.
. // show pushed-button bitmap (after SetTimer) here
.
}
else // definitely double-click (unwind everything)
{
KillTimer(ahWnd, ID_TIMER_LBUTTON);
PostMessage(ahWnd, WM_USER_DOUBLECLICK, 0, 0L);
sbDown = sbUp = FALSE;
ReleaseCapture(); // release the mouse
.
. // show unpushed-button bitmap here
.
}
break;
case WM_LBUTTONUP: // now either single-click or double-click
if (sbDown)
sbUp = TRUE;
break;
case WM_TIMER: // now either held-down or single-click
if (awParam = = ID_TIMER_LBUTTON) // not double-click
{
KillTimer(ahWnd, ID_TIMER_LBUTTON);
if (sbUp) // definitely single-click (unwind everything)
PostMessage(ahWnd, WM_USER_SINGLECLICK, 0, 0L);
else // definitely held-down (don’t unwind ‘til done)
SendMessage(ahWnd, WM_USER_HELDDOWN, 0, 0L);
sbDown = sbUp = FALSE;
ReleaseCapture(); // release the mouse
.
. // show unpushed-button bitmap here
.
}
break;
case WM_USER_HELDDOWN:
while (TRUE) // held-down, but be friendly by yielding
{
auto MSG aMsg;
.
. // do a *single* held-down action (if any left) here
.
while (PeekMessage(&aMsg, 0, WM_NULL, WM_NULL, PM_REMOVE))
{
TranslateMessage(&aMsg);
DispatchMessage(&aMsg); // looking for WM_LBUTTONUP
}
if (sbUp) // got it, so now go back and unwind everything
break;
}
break;
case WM_USER_SINGLECLICK:
.
. // do single-click action here
.
break;
case WM_USER_DOUBLECLICK:
.
. // do double-click action here
.
break;
QThe code fragment shown in Figure 2 is a very stripped-down version of an application we are writing to learn about dragging and releasing screen objects via the mouse. The program originally allowed a bitmap to be dragged around on the screen from the time the left mouse button had been depressed over it until the mouse button was released. The program display worked fine, but we got an Unrecoverable Application Error (UAE). We could not figure out what was wrong, so we began taking things out. We found that if the SelectObject call (marked with the //UAE) is removed, so is the UAE.
What are we doing wrong?
Mike Hare
Alan McCollum
Via Internet
Figure 2 Problematic Code
case WM_MOUSEMOVE:
if (bDragging)
{
hDcWnd = GetDC(hWnd);
hDcMem = CreateCompatibleDC(hDcWnd);
hBmpTmp = CreateCompatibleBitmap(hDcWnd, 32, 32);
SelectObject(hDcMem, hBmpTmp); // UAE
o
o // BitBlt code removed from here
o
DeleteObject(hBmpTmp);
DeleteDC(hDcMem);
ReleaseDC(hWnd, hDcWnd);
return 0L;
}
break;
AWhen you get a UAE message, you know that your application or the MicrosoftÒ WindowsÔ graphical environment has violated one of the protection rules of the IntelÒ 80x86 microprocessor. Of course, if you really do leave a // UAE in your code, your program will certainly hit a UAE. (Just kidding.)
Your code is causing a problem because it violates one of the implied symmetry rules of certain Windows function calls. In Windows, when you create something, you should subsequently destroy it, just as when you allocate memory, you should later free it. This symmetry extends to when you select objects into a device context (DC), in which case you subsequently need to select the original object back into the DC. The problem with this code is that it does not have a symmetric SelectObject call to replace the bitmap in the DC. The UAE occurs for one of the following reasons:
The DeleteObject call deletes an object in an active DC.
The DeleteDC function, when called, will try to do the same thing, that is, delete the bitmap.
The original bitmap that came with the DC will still be around in GDI’s local data segment, eating up Free System Resources (FSRs). When this goes on long enough and FSRs get to 0%, applications or Windows will crash.
The proper way to write this code is shown in Figure 3. Note the symmetric SelectObject calls and the reuse of the hBmpTmp variable. By making the SelectObject calls this way, you are essentially swapping bitmaps twice, and using hBmpTmp to hold the HBITMAP of both the new and old bitmaps.
Figure 3 Symmetric SelectObject Calls
case WM_MOUSEMOVE:
if (bDragging)
{
hDcWnd = GetDC(hWnd);
hDcMem = CreateCompatibleDC(hDcWnd);
hBmpTmp = CreateCompatibleBitmap(hDcWnd, 32, 32);
hBmpTmp = SelectObject(hDcMem, hBmpTmp); // use new bmp
o
o // BitBlt code goes here
o
hBmpTmp = SelectObject(hDcMem, hBmpTmp); // use old bmp
DeleteObject(hBmpTmp); // delete new bmp
DeleteDC(hDcMem);
ReleaseDC(hWnd, hDcWnd);
return 0L;
}
break;
QI need to know something that I haven’t seen documented anywhere. How can you restart Windows the same way that Windows Setup does, without exiting to DOS?
Don Elustondo
Rockville Centre, New York
ATo initiate the normal shut-down procedure, execute the following function call:
ExitWindows(0x42L, 0);
This function carries out the tasks performed when you click on the OK button of Program Manager’s or File Manager’s Exit Windows dialog box, or the Restart Windows button of Setup. This essentially means that Windows broadcasts a WM_QUERYENDSESSION window message to all running applications. You will not regain control unless an application replied "no" to the WM_QUERYENDSESSION by returning 0L.
Setting the first argument to 0x42L tells WIN.COM to restart Windows. After painting the Microsoft logo, WIN.COM loads one of the three Windows 3.0 kernels (KERNEL.EXE, KRNL286.EXE, or KRNL386.EXE) and waits for a return code. If a 42H is returned, it jumps back and reloads the same kernel, restarting Windows.
WINDOWS.H in the Windows 3.1 beta II release Software Development Kit actually contains two new manifest constants for ExitWindows, the meaning of which should be obvious.
#define EW_RESTARTWINDOWS 0x42L
#define EW_REBOOTSYSTEM 0x43L
Q I’m trying to build an application so that it is very tailored to the various ways that Windows can receive input: with a keyboard, mouse, or pen.
If the user has a mouse, the help line at the bottom of the main window might offer clues on how to use it, as opposed to telling what keys to hit if the user only has a keyboard.
If the user doesn’t have a mouse, the cursors are meaningless, as are any instructions to click or double-click somewhere.
If the application is run on pen computers, text that tells the user to type or click is kind of stupid, and function-key accelerator references in menus are dumb. And having all those underscores in dialog boxes just clutters things up and makes no sense.
I’m trying to come up with a systematic way to incorporate this sensitivity into my application. The problem is that the resource file is becoming unmanageably large (multiple dialogs, menus, strings, and so on), as is the code, which has to do a lot of switching when loading resources based on the environment. Is there any way to make this easier?
Tom Tinsley
Houston, Texas
AI’d approach this the same way I would for internationalization. Create separate resource-only DLLs that are specific to each environment you want to run in.
MYAPPKBD.DLL | Keyboard-only environment |
MYAPPK&M.DLL | Keyboard-and-mouse environment |
MYAPPPEN.DLL | Pen environment |
The DLLs would contain parallel resource information for all the dialogs, menus, strings, and so on. For a very simple example, the STRINGTABLE in each might look like this:
Keyboard-only environment
STRINGTABLE
BEGIN
ID_HELP, "Hit F1 for help."
END
Keyboard-and-mouse environment
STRINGTABLE
BEGIN
ID_HELP, "Hit F1 or Click here for help."
END
Pen environment
STRINGTABLE
BEGIN
ID_HELP, "Press here for help."
END
Once the application starts and establishes the environment it is running in, it simply loads the appropriate DLL. The instance handle of the appropriate DLL is kept in a global variable and is used for each resource-related call:
HANDLE hInstResDLL; // handle of resource DLL
LoadString(hInstResDLL, ID_HELP, szText,
sizeof(szText));
For safety in case a DLL is not found, the most likely resource file (which would become the default resource file) could be kept bound into the application itself. The global variable (hInstResDLL) would then be set to the instance handle of the application itself, and the resources would be found in the EXE file.
QI’m trying to invert the color of certain parts of a window using DSTINVERT. This worked great under Windows 2.x. Red, green, and blue became cyan, magenta, and yellow, and vice versa. But this no longer works in Windows 3.0! All of the opposite colors now come out much darker than before. Did they break DSTINVERT or change its meaning?
Rory M. Pastorius
Melbourne, Florida
AThe inverse color generated by DSTINVERT is not really the opposite color that you would expect when expressed as an RGB value (see Figure 4). When you use DSTINVERT, you are not inverting the RGB value of the destination pixel, as incredible as that may sound. What you are "inverting" is the index of that pixel into the System Palette of RGB values. The inverse of the first index is the last index, the inverse of the second index is the next to last index, and so on, for the relevant display driver. Using the 256-color palette as an example, bright red (index 249), bright green (250), and bright blue (252) become slate (6), lavender (5), and pea green (3), which are the dark versions of the cyan (254), magenta (253), and yellow (251) that you might have expected.
Why does it work this way? If you referenced the RGB value and inverted it, the inverse RGB value isn’t guaranteed to be in the System Palette, which is all that you have to choose colors from. For example, the inverse of RGB(192, 192, 192) (light gray) is RGB(63, 63, 63), and it does not exist elsewhere in the palette. Even if you were to solve this problem by finding the closest inverted RGB value in the System Palette and changing the pixel color to it, the DSTINVERT process would be quite slow for a large screen area.
So why were the inverted RGB values opposite in Windows 2.x? In Windows 2.x, the standard EGA/VGA display drivers displayed only eight pure colors. These eight colors were the bright versions of the primary colors and variations. Inverting these color indexes would give you predictable results, with the resulting RGB values being neatly opposite, simply because of the ordering of the colors in the palette, as you can see in Figure 4.
In Windows 3.x, the standard EGA/VGA display drivers were modified to display 16 pure colors. The original eight colors were retained and the dark versions of these bright colors were added, along with two grays. They were ordered so that inversion now provides a more reasonable visual effect. Inverting a bright color now gives you the dark version of the opposite color, and vice versa. Unfortunately, this algorithm isn’t perfect. In the center of the table, you can see that the opposite of pale green (256-color index 8) and pale blue (9) are medium gray (247) and off white (246), which aren’t even close to being inverse RGB values.
Also in Windows 3.x, the 256-color display drivers (and drivers with more colors) can have their System Palettes modified when applications use the Palette Manager. Applications can easily modify all of the colors except the 20 basic system colors in Figure 4, which are called the Default Palette. (Actually, 18 of these 20 colors really can be modified, but only with more effort.) The first ten and the last ten RGB values of the System Palette, however large it may be, are set to the first ten and the last ten values of the Default Palette. To display more than these 20 pure colors on a palette-based (256 or more colors) device, you have to use the Palette Manager to change the System Palette. Prior Windows Q&A columns have discussed this.
That brings up an interesting point for when you do modify the System Palette. If you change the System Palette, you should order the colors yourself so that the opposite index results in an opposite RGB value that makes sense.
Figure 4 The Windows 3.x System Colors
Windows | Windows 3.x |
2.x Index | VGA Index |
[0] | [0] |
-- | [1] |
-- | [2] |
-- | [3] |
-- | [4] |
-- | [5] |
-- | [6] |
-- | [7] |
-- | -- |
-- | -- |
-- | -- |
-- | -- |
-- | [8] |
[1] | [9] |
[2] | [10] |
[3] | [11] |
[4] | [12] |
[5] | [13] |
[6] | [14] |
[7] | [15] |
†Some drivers implement Pale Blue as RGB(164, 200, 240). |