Holy cow, I wrote a book!
If you play around with resizing the window, you can get a fractional line to appear at the bottom of the screen. This is not normally a problem until you scroll to the very end of the list, say, by pressing the End key, at which point an ugly blank space appears at the bottom. This ugly blank space is particularly disturbing when the fractional line is very nearly an entire line, because it looks like there is an off-by-one bug in the code somewhere.
End
We can fix this by forcing the window size to be an exact integral multiple of the line height. Like adding scrollbars, there is the basic idea, followed by a lot of detail work to get it just right.
The basic idea is to enforce integrality in the window resize code. The right place to do this is in the WM_WINDOWPOSCHANGING handler, which allows you to adjust the placement of the window before it is actually moved. This avoids flicker.
We'll break the bulk of the work into a helper function, which will prove useful later.
void AdjustSizeRectangle(HWND hwnd, WPARAM wmsz, LPRECT prc) { RECT rc; int cyClient; int cyAdjust; /* Compute the resulting client height */ SetRectEmpty(&rc); AdjustWindowRect(&rc, GetWindowStyle(hwnd), FALSE); cyClient = (prc->bottom - prc->top) - (rc.bottom - rc.top); /* Compute the number of fractional pixels */ cyAdjust = cyClient % g_cyLine; /* * Remove the fractional pixels from the top or bottom. */ switch (wmsz) { case WMSZ_TOP: case WMSZ_TOPLEFT: case WMSZ_TOPRIGHT: prc->top += cyAdjust; break; default: prc->bottom -= cyAdjust; break; } }
The WM_WINDOWPOSCHANGNG handler then check if the window size is changing, in which case we adjust the size rectangle to enforce integrality of the client area. We say that the adjustments should be taken from the bottom of the window.
BOOL OnWindowPosChanging(HWND hwnd, LPWINDOWPOS pwp) { if (!(pwp->flags & SWP_NOSIZE)) { RECT rc = { 0, 0, pwp->cx, pwp->cy }; AdjustSizeRectangle(hwnd, WMSZ_BOTTOM, &rc); pwp->cy = rc.bottom; } return 0; } /* Add to WndProc */ HANDLE_MSG(hwnd, WM_WINDOWPOSCHANGING, OnWindowPosChanging);
What is the significance of the (int) cast in the computation of dLines?
(int)
dLines
It isn't. I got the question wrong. The real question should have been "What is the significance of the (int) cast in the computation of g_iWheelCarryover?"
g_iWheelCarryover
The answer is to ensure that the computation is performed with signed integers throughout. If the cast were missing, then the computation would have been unsigned (since mixing signed and unsigned yields unsigned). dLines is a signed integer, let's say it's -1. Multiply by WHEEL_DELTA yields -120. Now convert it to an unsigned integer and you get a number in excess of four billion. Divided by uScroll (typically 3) yields a number greater than one billion, which is incorrect.
-1
WHEEL_DELTA
-120
uScroll
Assuming you don't have a high-resolution wheel mouse, how would you test that your sub-detent mouse wheel handling was working properly?
This is an easy one. Insert temporary code at the top of the OnMouseWheel function that says zDelta /= 4. Ta-da, your mouse now has quadruple its original resolution.
OnMouseWheel
zDelta /= 4
Useless trivia day.
Why is a registry file called a "hive"?
Because one of the original developers of Windows NT hated bees. So the developer who was responsible for the registry snuck in as many bee references as he could. A registry file is called a "hive", and registry data are stored in "cells", which is what honeycombs are made of.
The mouse wheel is tricky because the mouse wheel UI guidelines indicate that you should scroll by a user-specified amount for each "click" of the mouse, where one click is WHEEL_DELTA mouse units (called a "detent"). There are two subtle points about the above requirement: First, that the amount of scrolling is a user setting which must be respected, and second, that the wheel can report values that are not perfect multiples of WHEEL_DELTA.
In particular, there is the possibility that a high-resolution mouse wheel will report wheel scroll units smaller than WHEEL_DELTA. For example, consider a wheel mouse that supports "half-clicks". When you turn the wheel halfway between clicks, it reports WHEEL_DELTA/2, and when you reach a full click, it reports another WHEEL_DELTA/2. To handle this properly, you need to make sure that by the time the full click is reached, the window has scrolled by exactly the amount it would have scrolled if the user had been using a low-resolution wheel that reported a single wheel motion of WHEEL_DELTA.
(I once referred to this in email as a "sub-detent wheel" and was accused of coining a phrase.)
To handle the first point, we requery the user's desired scroll delta at each mouse wheel message. To handle the second point, we accumulate detents as they arrive and consume as many of them as possible, leaving the extras for the next wheel message.
int g_iWheelCarryover; /* Unused wheel ticks */ LRESULT OnMouseWheel(HWND hwnd, int xPos, int yPos, int zDelta, UINT fwKeys) { UINT uScroll; if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &uScroll, 0)) { uScroll = 3; /* default value */ } /* * If user specified scrolling by pages, do so. */ if (uScroll == WHEEL_PAGESCROLL) { uScroll = g_cLinesPerPage; } /* * If user specified no wheel scrolling, then don't do wheel scrolling. * This also avoids a divide-by-zero below. */ if (uScroll == 0) { return 0; } zDelta += g_iWheelCarryover; /* Accumulate wheel motion */ /* * See how many lines we should scroll. * This relies on round-towards-zero. */ int dLines = zDelta * (int)uScroll / WHEEL_DELTA; /* * Record the unused portion as the next carryover. */ g_iWheelCarryover = zDelta - dLines * WHEEL_DELTA / (int)uScroll; /* * Do the scrolling. */ ScrollDelta(hwnd, -dLines); return 0; } /* Add to WndProc */ HANDLE_MSG(hwnd, WM_MOUSEWHEEL, OnMouseWheel);
Exercise: What is the significance of the (int) cast in the computation of dLines?
Exercise: Assuming you don't have a high-resolution wheel mouse, how would you test that your sub-detent mouse wheel handling was working properly?
Ancient history lesson.
When Win9x loads a 32-bit DLL, it creates a shadow 16-bit DLL so 16-bit code (like USER) can access resources in it.
The shadow DLL is effectively a resource-only 16-bit DLL, created by taking the 32-bit resources and converting them to 16-bit format. If the resources cannot be converted to 16-bit format, the DLL will not load.
The 16-bit resource file format specifies resource sizes by combining a DLL-wide shift value with a 16-bit per-resource scaled size. So, for example, if the shift value were 2, and the per-resource scaled size were 8, then the actual resource size would be 8 << 2 = 32.
Windows 95 has a bug in the way it calculates the scaled size.
If the Windows 95 kernel decided that it needed to use a nonzero shift value because the 32-bit DLL contains a resource larger than 64K, it scaled the 32-bit values down to 16-bit values and rounded down rather than up. So, for example, if a resources were 65537 bytes in size and the shift factor were 1, then the scaled-down value would be 65537 >> 1 = 32768. After scaling back up, the result would be 32768 >> 1 = 65536. Notice that the value is too small; the last byte of the resource has been truncated.
Consequently, if you have a 32-bit DLL with resources 64K or larger, you must pad those resources to prevent this truncation from happening. In the above example, you would have to pad the resource to 65538 bytes, so that the scaled-down value would be 32769, which scales back up to 65538.
I believe this bug was fixed in Windows 98 but I'm not sure. There is a little program in the SDK called fixres95 that generates the necessary padding.
Other limitations of the 16-bit resource file format you may run into:
Note that so far, the scrollbar is accessible only with the mouse. Our next step is to add keyboard access to the scrollbar. Fortunately, this is not all that difficult. We merely map some keystrokes to equivalent scrollbar actions.
void OnKey(HWND hwnd, UINT vk, BOOL fDown, int cRepeat, UINT flags) { if (fDown) { switch (vk) { case VK_UP: ScrollDelta(hwnd, -cRepeat); break; case VK_DOWN: ScrollDelta(hwnd, +cRepeat); break; case VK_PRIOR: ScrollDelta(hwnd, -cRepeat*g_cyPage); break; case VK_NEXT: ScrollDelta(hwnd, +cRepeat*g_cyPage); break; case VK_HOME: ScrollTo(hwnd, 0); break; case VK_END: ScrollTo(hwnd, MAXLONG); break; } } } /* Add to WndProc */ HANDLE_MSG(hwnd, WM_KEYDOWN, OnKey);
Note that this doesn't make our sample program fully accessible; this just makes the scrollbars accessible. Full accessibility will be covered in a (much) later blog entry. Right now, I'm just focusing on scrollbars.
This is the answer I give to IT people when they ask if it's okay to customize the Windows boot logo.
DO NOT DO THIS ON A PRODUCTION MACHINE OR YOU WILL REGRET IT.
If you hack the bitmap everything will seem fine until six months down the road when you decide to install the latest service pack. The service pack installer will not upgrade your ntoskrnl because it looks at the file and says "Hm, this isn't the standard uniprocessor ntoskrnl, it's not the standard multiprocessor ntoskrnl, it's not the standard advanced server ntoskrnl, I can't tell what this is, so I don't know which version of ntoskrnl to update it to. I'll just leave it alone."
If you are lucky you will bluescreen at boot because the old ntoskrnl is incompatible with some other critical part of the service pack.
If you are unlucky, your machine will appear to run normally when in fact it is quietly corrupting itself, and then it will keel over or generate bogus data when you least expect it.
If you planned ahead, you will have quit your job and moved to Hawaii so the disaster falls on your replacement's head to clean up while you sit on the beach sipping a pina colada.
Try it: Change the value of g_cItems to 100000 and watch what happens.
The fix is to ignore the pos passed to the message and instead get it from the scrollbar. This helper function will prove useful.
pos
UINT GetTrackPos(HWND hwnd, int fnBar) { SCROLLINFO si; si.cbSize = sizeof(si); si.fMask = SIF_TRACKPOS; if (GetScrollInfo(hwnd, fnBar, &si)) { return si.nTrackPos; } return 0; }
Change the two case statements in OnVscroll as follows:
OnVscroll
case SB_THUMBPOSITION: ScrollTo(hwnd, GetScrollPos(hwnd, SB_VERT)); break; case SB_THUMBTRACK: ScrollTo(hwnd, GetTrackPos(hwnd, SB_VERT)); break;