Have you ever written a Windows application that makes generous use of color only to find that the output looks crummy on 16-color and 256-color video adapters? There's not a whole lot you can do about it when the adapter itself supports only 16 colors, but you can do plenty to improve output on 256-color devices. The key to better color output is MFC's CPalette class. Before we get into the specifics of CPalette, let's briefly review how color information is encoded in Windows and what Windows does with the color information that you provide.
One of the benefits of a device-independent output model is that you can specify the colors an application uses without regard for the physical characteristics of the output device. When you pass a color to the Windows GDI, you pass a COLORREF value containing 8 bits each for red, green, and blue. The RGB macro combines individual red, green, and blue values into a single COLORREF. The statement
COLORREF clr = RGB (255, 0, 255); |
creates a COLORREF value named clr that represents magenta—the color you get when you mix equal parts red and blue. Conversely, you can extract 8-bit red, green, and blue values from a COLORREF value with the GetRValue, GetGValue, and GetBValue macros. A number of GDI functions, including those that create pens and brushes, accept COLORREF values.
What the GDI does with the COLORREF values you pass it depends on several factors, including the color resolution of the video hardware and the context in which the colors are used. In the simplest and most desirable scenario, the video adapter is a 24-bits-per-pixel device and COLORREF values translate directly into colors on the screen. Video adapters that support 24-bit color, or true color, are becoming increasingly common, but Windows still runs on millions of PCs whose video adapters are limited to 4 or 8 bits per pixel. Typically, these devices are palletized devices, meaning that they support a wide range of colors but can display only a limited number of colors at one time. A standard VGA, for example, can display 262,144 different colors—6 bits each for red, green, and blue. However, a VGA running at a resolution of 640 by 480 pixels can display only 16 different colors at once because each pixel is limited to 4 bits of color information in the video buffer. The more common case is a video adapter that can display more than 16.7 million colors but can display only 256 colors at once. The 256 colors that can be displayed are determined from RGB values that are programmed into the adapter's hardware palette.
Windows handles palletized devices by preprogramming a standard selection of colors into the adapter's hardware palette. A 256-color adapter is preprogrammed with the 20 so-called static colors shown in the following table. The four colors marked with asterisks are subject to change at the operating system's behest, so you shouldn't write code that depends on their presence.
Static Palette Colors
Color | R | G | B | Color | R | G | B |
---|---|---|---|---|---|---|---|
Black | 0 | 0 | 0 | Cream* | 255 | 251 | 240 |
Dark red | 128 | 0 | 0 | Intermediate gray* | 160 | 160 | 164 |
Dark green | 0 | 128 | 0 | Medium gray | 128 | 128 | 128 |
Dark yellow | 128 | 128 | 0 | Red | 255 | 0 | 0 |
Dark blue | 0 | 0 | 128 | Green | 0 | 255 | 0 |
Dark magenta | 128 | 0 | 128 | Yellow | 255 | 255 | 0 |
Dark cyan | 0 | 128 | 128 | Blue | 0 | 0 | 255 |
Light gray | 192 | 192 | 192 | Magenta | 255 | 0 | 255 |
Money green* | 192 | 220 | 192 | Cyan | 0 | 255 | 255 |
Sky blue* | 166 | 202 | 240 | White | 255 | 255 | 255 |
*Denotes default colors that are subject to change.
When you draw on a palletized device, the GDI maps each COLORREF value to the nearest static color using a simple color-matching algorithm. If you pass a COLORREF value to a function that creates a pen, Windows assigns the pen the nearest static color. If you pass a COLOREF value to a function that creates a brush and there isn't a matching static color, Windows dithers the brush color using static colors. Because the static colors include a diverse (if limited) assortment of hues, Windows can do a reasonable job of simulating any COLORREF value you throw at it. A picture painted with 100 different shades of red won't come out very well because Windows will simulate all 100 shades with just two reds. But you're guaranteed that red won't undergo a wholesale transformation to blue, green, or some other color, because the static colors are always there and are always available.
For many applications, the primitive form of color mapping that Windows performs using static colors is good enough. But for others, accurate color output is a foremost concern and 20 colors just won't get the job done. In a single-tasking environment such as MS-DOS, a program running on a 256-color adapter can program the hardware palette itself and use any 256 colors it wants. In Windows, applications can't be allowed to program the hardware palette directly because the video adapter is a shared resource. So how do you take advantage of the 236 colors left unused in a 256-color adapter after Windows adds the 20 static colors? The answer lies in a GDI object known as a logical palette.
A logical palette is a table of RGB color values that tells Windows what colors an application would like to display. A related term, system palette, refers to the adapter's hardware color palette. At an application's request, the palette manager built into Windows will transfer the colors in a logical palette to unused entries in the system palette—a process known as realizing a palette—so that the application can take full advantage of the video adapter's color capabilities. With the help of a logical palette, an application running on a 256-color video adapter can use the 20 static colors plus an additional 236 colors of its choosing. And because all requests to realize a palette go through the GDI, the palette manager can serve as an arbitrator between programs with conflicting color needs and thus ensure that the system palette is used cooperatively.
What happens if two or more applications realize logical palettes and the sum total of the colors they request is more than the 236 additional colors a 256-color video adapter can handle? The palette manager assigns color priorities based on each window's position in the z-order. The window at the top of the z-order receives top priority, the window that's second gets the next highest priority, and so on. If the foreground window realizes a palette of 200 colors, all 200 get mapped to the system palette. If a background window then realizes a palette of, say, 100 colors, 36 get programmed into the unused slots remaining in the system palette and 64 get mapped to the nearest matching colors. That's the worst case. Unless directed to do otherwise, the palette manager avoids duplicating entries in the system palette. Therefore, if 4 of the foreground window's colors and 10 of the background window's colors match static colors, and if another 10 of the background window's colors match nonstatic colors in the foreground window, the background window ends up getting 60 exact matches in the system palette.
You can see the palette manager at work by switching Windows to 256-color mode, launching two instances of the Windows Paint applet, loading a different 256-color bitmap in each, and clicking back and forth between the two. The bitmap in the foreground will always look the best because it gets first crack at the system palette. The bitmap in the background gets what's left over. If both bitmaps use similar colors, the background image won't look too bad. But if the colors are vastly different—for example, if bitmap A contains lots of bright, vibrant colors whereas bitmap B uses primarily earth tones—the image in the background window might be so color-corrupted that it's hardly recognizable. The palette manager's role in the process is to try to satisfy the needs of both programs. When those needs conflict, the foreground window receives priority over all others so that the application the user is working with looks the best.
Writing an application that uses a logical palette isn't difficult. In MFC, logical palettes are represented by the CPalette class and are created and initialized with CPalette member functions. Once a logical palette is created, it can be selected into a device context and realized with CDC member functions.
CPalette provides two member functions for palette creation. CreatePalette creates a custom palette from RGB values you specify; CreateHalftonePalette creates a "halftone" palette containing a generic and fairly uniform distribution of colors. Custom palettes give better results when an image contains few distinctly different colors but many subtle variations in tone. Halftone palettes work well for images containing a wide range of colors. The statements
CPalette palette; palette.CreateHalftonePalette (pDC); |
create a halftone palette tailored to the device context pointed to by pDC. If the device context corresponds to a 256-color device, the halftone palette will also contain 256 colors. Twenty of the colors will match the static colors; the other 236 will expand the selection of colors available by adding subtler shades of red, green, and blue and mixtures of these primary colors. Specifically, a 256-color halftone palette includes all the colors in a 6-by-6-by-6-color cube (colors composed of six shades each of red, green, and blue), plus an array of grays for gray-scale imaging and other colors handpicked by the GDI. Passing a NULL DC handle to CreateHalftonePalette creates a 256-color halftone palette independent of the characteristics of the output device. However, because CPalette::CreateHalftonePalette mistakenly asserts in debug builds if passed a NULL DC handle, you must drop down to the Windows API to take advantage of this feature:
CPalette palette; palette.Attach (::CreateHalftonePalette (NULL)); |
::CreateHalftonePalette is the API equivalent of CPalette::CreateHalftonePalette.
Creating a custom palette is a little more work because before you call CreatePalette, you must initialize a LOGPALETTE structure with entries describing the palette's colors. LOGPALETTE is defined as follows.
typedef struct tagLOGPALETTE { WORD palVersion; WORD palNumEntries; PALETTEENTRY palPalEntry[1]; } LOGPALETTE; |
palVersion specifies the LOGPALETTE version number; in all current releases of Windows, it should be set to 0x300. palNumEntries specifies the number of colors in the palette. palPalEntry is an array of PALETTEENTRY structures defining the colors. The number of elements in the array should equal the value of palNumEntries. PALETTEENTRY is defined like this:
typedef struct tagPALETTEENTRY { BYTE peRed; BYTE peGreen; BYTE peBlue; BYTE peFlags; } PALETTEENTRY; |
peRed, peGreen, and peBlue specify a color's 8-bit RGB components. peFlags contains zero or more bit flags describing the type of palette entry. It can be set to any of the values shown here, or to 0 to create a "normal" palette entry:
Flag | Description |
---|---|
PC_EXPLICIT | Creates a palette entry that specifies an index into the system palette rather than an RGB color. Used by programs that display the contents of the system palette. |
PC_NOCOLLAPSE | Creates a palette entry that's mapped to an unused entry in the system palette even if there's already an entry for that color. Used to ensure the uniqueness of palette colors. |
PC_RESERVED | Creates a palette entry that's private to this application. When a PC_RESERVED entry is added to the system palette, it isn't mapped to colors in other logical palettes even if the colors match. Used by programs that perform palette animation. |
Most of the time, peFlags is simply set to 0. We'll discuss one use for the PC_RESERVED flag later in this chapter, in the section on palette animation.
The PALETTEENTRY array in the LOGPALETTE structure is declared with just one array element because Windows has no way of anticipating how many colors a logical palette will contain. As a result, you can't just declare an instance of LOGPALETTE on the stack and fill it in; instead, you have to allocate memory for it based on the number of PALETTEENTRY structures it contains. The following code allocates a "full" LOGPALETTE structure on the stack and then creates a logical palette containing 32 shades of red:
struct { LOGPALETTE lp; PALETTEENTRY ape[31]; } pal; LOGPALETTE* pLP = (LOGPALETTE*) &pal; pLP->palVersion = 0x300; pLP->palNumEntries = 32; for (int i=0; i<32; i++) { pLP->palPalEntry[i].peRed = i * 8; pLP->palPalEntry[i].peGreen = 0; pLP->palPalEntry[i].peBlue = 0; pLP->palPalEntry[i].peFlags = 0; } CPalette palette; palette.CreatePalette (pLP); |
Like other GDI objects, logical palettes should be deleted when they're no longer needed. A logical palette represented by a CPalette object is automatically deleted when the corresponding CPalette object is deleted or goes out of scope.
How many entries can a logical palette contain? As many as you want it to. Of course, the number of colors that can be mapped directly to the system palette is limited by the capabilities of the video adapter. If you realize a palette containing 1,024 colors on a 256-color output device, only the first 236 will be mapped directly; the remaining colors will be matched as closely as possible to colors already in the system palette. When you use logical palettes (especially large ones), it's helpful to arrange the colors in order of importance, where palPalEntry[0] defines the most important color, palPalEntry[1] defines the next most important color, and so on. The palette manager maps palette colors in array order, so by putting important colors first, you increase the chances that those colors will be displayed in their native form. In general, you shouldn't make a logical palette any larger than it has to be. Large palettes take longer to realize, and the more palette colors a foreground window uses, the fewer colors the palette manager can make available to palette-aware windows lower in the z-order.
After a palette is created, you can retrieve individual palette entries with CPalette::GetPaletteEntries or change them with CPalette::SetPaletteEntries. You can also resize a palette with CPalette::ResizePalette. If the palette is enlarged, the new palette entries initially contain all 0s.
For a logical palette to be effective, it must be selected into a device context and realized before any drawing takes place. The current logical palette is a device context attribute, just as the current pen and brush are device context attributes. (In case you're wondering, a device context's default logical palette is a trivial one whose entries correspond to the static colors.) The following OnPaint handler selects the logical palette m_palette into a paint device context and realizes the palette before repainting the screen:
void CMainWindow::OnPaint () { CPaintDC dc (this); CPalette* pOldPalette = dc.SelectPalette (&m_palette, FALSE); dc.RealizePalette (); // Do some drawing. dc.SelectPalette (pOldPalette, FALSE); } |
In this example, the pointer to the default palette is saved and used later to select m_palette out of the device context. Note that palettes are selected with CDC::SelectPalette instead of CDC::SelectObject. The second parameter is a BOOL value that, if TRUE, forces the palette to behave as if it were in the background even when the window that selected it is in the foreground. Background palettes can be handy in applications that use multiple palettes, but normally you'll specify FALSE in calls to SelectPalette. CDC::RealizePalette realizes the palette that's currently selected into the device context by asking the palette manager to map colors from the logical palette to the system palette.
Once you create a palette, select it into a device context, and realize it, you're ready to start drawing. If you use CDC::BitBlt to display a bitmap, the realized colors are used automatically. But if you're drawing images with brushes or pens or using functions such as CDC::FloodFill that use neither a brush nor a pen directly but do accept COLORREF values, there's something else you must consider.
The RGB macro is one of three macros that create COLORREF values. The others are PALETTEINDEX and PALETTERGB. Which of the three macros you use determines how the GDI treats the resultant COLORREF. When you draw with a COLORREF value created with the RGB macro, the GDI ignores the colors that were added to the system palette when the logical palette was realized and uses only the static colors. If you want the GDI to use all the palette colors, use the PALETTERGB macro. PALETTERGB creates a palette-relative color. The PALETTEINDEX macro creates a COLORREF value that specifies an index into a logical palette rather than an RGB color value. This value is called a palette-index color value. It's the fastest kind of color to draw with because it prevents the GDI from having to match RGB color values to colors in the logical palette.
The following code sample demonstrates how all three macros are used:
void CMainWindow::OnPaint () { CPaintDC dc (this); // Select and realize a logical palette. CPalette* pOldPalette = dc.SelectPalette (&m_palette, FALSE); dc.RealizePalette (); // Create three pens. CPen pen1 (PS_SOLID, 16, RGB (242, 36, 204)); CPen pen2 (PS_SOLID, 16, PALETTERGB (242, 36, 204)); CPen pen3 (PS_SOLID, 16, PALETTEINDEX (3)); // Do some drawing. dc.MoveTo (0, 0); CPen* pOldPen = dc.SelectObject (&pen1); dc.LineTo (300, 0); // Nearest static color dc.SelectObject (&pen2); dc.LineTo (150, 200); // Nearest static or palette color dc.SelectObject (&pen3); dc.LineTo (0, 0); // Exact palette color dc.SelectObject (pOldPen); // Select the palette out of the device context. dc.SelectPalette (pOldPalette, FALSE); } |
Because pens use solid, undithered colors and because its COLORREF value is specified with an RGB macro, pen1 draws with the static color that most closely approximates the RGB value (242, 36, 204). pen2, on the other hand, is assigned the nearest matching color from the static colors or m_palette. pen3 uses the color in the system palette that corresponds to the fourth color (index=3) in the logical palette, regardless of what that color might be.
When you write an application that uses a logical palette, you should include handlers for a pair of messages named WM_QUERYNEWPALETTE and WM_PALETTECHANGED. WM_QUERYNEWPALETTE is sent to a top-level window when it or one of its children receives the input focus. WM_PALETTECHANGED is sent to all top-level windows in the system when a palette realization results in a change to the system palette. An application's normal response to either message is to realize its palette and repaint itself. Realizing a palette and repainting in response to a WM_QUERYNEWPALETTE message enables a window that was just brought to the foreground to put on its best face by taking advantage of the fact that it now has top priority in realizing its palette. Realizing a palette and repainting in response to a WM_PALETTECHANGED message enable background windows to adapt to changes in the system palette and take advantage of any unused entries that remain after windows higher in the z-order have realized their palettes.
The following message handler demonstrates a typical response to a WM_QUERYNEWPALETTE message:
// In the message map ON_WM_QUERYNEWPALETTE () BOOL CMainWindow::OnQueryNewPalette () { CClientDC dc (this); CPalette* pOldPalette = dc.SelectPalette (&m_palette, FALSE); UINT nCount; if (nCount = dc.RealizePalette ()) Invalidate (); dc.SelectPalette (pOldPalette, FALSE); return nCount; } |
The general strategy is to realize a palette and force a repaint by invalidating the window's client area. The value returned by RealizePalette is the number of palette entries that were mapped to entries in the system palette. A 0 return value means that realizing the palette had no effect, which should be extremely rare for a foreground window. If RealizePalette returns 0, you should skip the call to Invalidate. OnQueryNewPalette should return a nonzero value if a logical palette was realized and 0 if it wasn't. It should also return 0 if it tried to realize a palette but RealizePalette returned 0. The return value isn't used in current versions of Windows.
WM_PALETTECHANGED messages are handled in a similar way. Here's what a typical OnPaletteChanged handler looks like:
// In the message map ON_WM_PALETTECHANGED () void CMainWindow::OnPaletteChanged (CWnd* pFocusWnd) { if (pFocusWnd != this) { CClientDC dc (this); CPalette* pOldPalette = dc.SelectPalette (&m_palette, FALSE); if (dc.RealizePalette ()) Invalidate (); dc.SelectPalette (pOldPalette, FALSE); } } |
The CWnd pointer passed to OnPaletteChanged identifies the window that prompted the WM_PALETTECHANGED message by realizing a palette. To avoid unnecessary recursion and possible infinite loops, OnPaletteChanged should do nothing if pFocusWnd points to its own window. That's the reason for the if statement that compares pFocusWnd to this.
Rather than perform full repaints in response to WM_PALETTECHANGED messages, applications can optionally call CDC::UpdateColors instead. UpdateColors updates a window by matching the color of each pixel to the colors in the system palette. It's usually faster than a full repaint, but the results typically aren't as good because the color matching is done based on the contents of the system palette before it changed. If you use UpdateColors, maintain a variable that counts the number of times UpdateColors has been called. Then every third or fourth time, do a full repaint and reset the counter to 0. This will prevent the colors in a background window from becoming too out of sync with the colors in the system palette.
The OnQueryNewPalette and OnPaletteChanged handlers in the previous section assume that the window to be updated is the application's main window. In a document/view application, that's not the case; the views need updating, not the top-level window. The ideal solution would be to put the OnQueryNewPalette and OnPaletteChanged handlers in the view class, but that won't work because views don't receive palette messages—only top-level windows do.
What most document/view applications do instead is have their main windows update the views in response to palette messages. The following OnQueryNewPalette and OnPaletteChanged handlers work well for most SDI applications:
BOOL CMainFrame::OnQueryNewPalette () { CDocument* pDoc = GetActiveDocument (); if (pDoc != NULL) GetActiveDocument ()->UpdateAllViews (NULL); return TRUE; } void CMainFrame::OnPaletteChanged (CWnd* pFocusWnd) { if (pFocusWnd != this) { CDocument* pDoc = GetActiveDocument (); if (pDoc != NULL) GetActiveDocument ()->UpdateAllViews (NULL); } } |
Palettes are a little trickier in MDI applications. If each open document has a unique palette associated with it (as is often the case), the active view should be redrawn using a foreground palette and inactive views should be redrawn using background palettes. Another issue with MDI applications that use multiple palettes is the need to update the views' colors as the user clicks among views. The best solution is to override CView::OnActivateView so that a view knows when it's activated or deactivated and can realize its palette accordingly. For a good example of palette handling in MDI applications, see the DIBLOOK sample program provided with Visual C++.
Now that you understand the mechanics of palette usage, ask yourself this question: How do I know if I need a logical palette in the first place? If color accuracy is of paramount concern, you'll probably want to use a logical palette when your application runs on a palletized 256-color video adapter. But the same application doesn't need a logical palette when the hardware color depth is 24 bits because in that environment perfect color output comes for free. And if the application runs on a standard 16-color VGA, palettes are extraneous because the system palette contains 16 static colors that leave no room for colors in logical palettes.
You can determine at run time whether a logical palette will improve color output by calling CDC::GetDeviceCaps with a RASTERCAPS parameter and checking the RC_PALETTE bit in the return value, as demonstrated here:
CClientDC dc (this); BOOL bUsePalette = FALSE; if (dc.GetDeviceCaps (RASTERCAPS) & RC_PALETTE) bUsePalette = TRUE; |
RC_PALETTE is set in palettized color modes and clear in nonpalettized modes. Generally speaking, the RC_PALETTE bit is set in 8-bit color modes and clear in 4-bit and 24-bit color modes. The RC_PALETTE bit is also clear if the adapter is running in 16-bit color ("high color") mode, which for most applications produces color output every bit as good as true color. Don't make the mistake some programmers have made and rely on bit counts to tell you whether to use a palette. As sure as you do, you'll run across an oddball video adapter that defies the normal conventions and fools your application into using a palette when a palette isn't needed or not using a palette when a palette would help.
What happens if you ignore the RC_PALETTE setting and use a logical palette regardless of color depth? The application will still work because the palette manager works even on nonpalettized devices. If RC_PALETTE is 0, palettes can still be created and selected into device contexts, but calls to RealizePalette do nothing. PALETTEINDEX values are dereferenced and converted into RGB colors in the logical palette, and PALETTERGB values are simply treated as if they were standard RGB color values. OnQueryNewPalette and OnPaletteChanged aren't called because no WM_QUERYNEWPALETTE and WM_PALETTECHANGED messages are sent. As explained in an excellent article, "The Palette Manager: How and Why," available on the Microsoft Developer Network (MSDN), "The goal is to allow applications to use palettes in a device-independent fashion and to not worry about the actual palette capabilities of the device driver."
Still, you can avoid wasted CPU cycles by checking the RC_PALETTE flag and skipping palette-related function calls if the flag is clear. And if your application relies on the presence of hardware palette support and won't work without it—for example, if it uses palette animation, a subject we'll get to in a moment—you can use RC_PALETTE to determine whether your application is even capable of running on the current hardware.
An equally important question to ask yourself when considering whether to use logical palettes is, "How accurate does my program's color output need to be?" Applications that draw using colors that match the static colors don't need palettes at all. On the other hand, a bitmap file viewer almost certainly needs palette support because without it all but the simplest bitmaps would look terrible on 256-color video adapters. Assess your program's color needs, and do as little work as you have to. You'll write better applications as a result.
The application shown in Figure 15-1 demonstrates basic palette-handling technique in a non-document/view application. PaletteDemo uses a series of blue brushes to paint a background that fades smoothly from blue to black. Moreover, it produces a beautiful gradient fill even on 256-color video adapters. The key to the high quality of its output on 256-color screens is PaletteDemo's use of a logical palette containing 64 shades of blue, ranging from almost pure black (R=0, G=0, B=3) to high-intensity blue (R=0, G=0, B=255). Brush colors are specified using palette-relative COLORREF values so that the GDI will match the brush colors to colors in the system palette after the logical palette is realized. You can judge the results for yourself by running PaletteDemo in both 8-bit and 24-bit color modes and seeing that the output is identical. Only when it is run in 16-color mode does PaletteDemo fail to produce a smooth gradient fill. But even then the results aren't bad because the GDI dithers the brush colors.
Figure 15-1. The PaletteDemo window.
Here are a few points of interest in PaletteDemo's source code, which appears in Figure 15-2. For starters, PaletteDemo's main window paints the gradient-filled background in response to WM_ERASEBKGND messages. WM_ERASEBKGND messages are sent to erase a window's background before a WM_PAINT handler paints the foreground. A WM_ERASEBKGND handler that paints a custom window background as PaletteDemo does should return a nonzero value to notify Windows that the background has been "erased." (For a cool effect, see what happens when a WM_ERASEBKGND handler paints nothing but returns TRUE anyway. What do you get? A see-through window!) Otherwise, Windows erases the background itself by filling the window's client area with the WNDCLASS's background brush.
PaletteDemo creates the logical palette that it uses to paint the window background in CMainWindow::OnCreate. The palette itself is a CPalette data member named m_palette. Before creating the palette, OnCreate checks CDC::GetDeviceCaps's return value for an RC_PALETTE bit. If the bit isn't set, OnCreate leaves m_palette uninitialized. Before selecting and realizing the palette, CMainWindow::OnEraseBkgnd checks m_palette to determine whether a palette exists:
if ((HPALETTE) m_palette != NULL) { pOldPalette = pDC->SelectPalette (&m_palette, FALSE); pDC->RealizePalette (); } |
CPalette's HPALETTE operator returns the handle of the palette attached to a CPalette object. A NULL handle means m_palette is uninitialized. OnEraseBkgnd adapts itself to the environment it's run in by selecting and realizing a logical palette if and only if the video hardware is palettized. The DoGradientFill function that draws the window background works with or without a palette because brush colors are specified with PALETTERGB macros.
One consideration that PaletteDemo doesn't address is what happens if the color depth changes while the application is running. You can account for such occurrences by processing WM_DISPLAYCHANGE messages, which are sent when the user changes the screen's resolution or color depth, and reinitializing the palette based on the new settings. There is no ON_WM_DISPLAYCHANGE macro, so you have to do the message mapping manually with ON_MESSAGE. The wParam parameter encapsulated in a WM_DISPLAYCHANGE message contains the new color depth expressed as the number of bits per pixel, and the low and high words of lParam contain the latest horizontal and vertical screen resolution in pixels.
WM_DISPLAYCHANGE isn't only for applications that use palettes. You should also use it if, for example, you initialize variables with the average width and height of a character in the system font when the application starts and later use those variables to size and position your output. If the variables aren't reinitialized when the screen resolution changes, subsequent output might be distorted.
Figure 15-2. The PaletteDemo application.
PaletteDemo.hclass CMyApp : public CWinApp { public: virtual BOOL InitInstance (); }; class CMainWindow : public CFrameWnd { protected: CPalette m_palette; void DoGradientFill (CDC* pDC, LPRECT pRect); void DoDrawText (CDC* pDC, LPRECT pRect); public: CMainWindow (); protected: afx_msg int OnCreate (LPCREATESTRUCT lpcs); afx_msg BOOL OnEraseBkgnd (CDC* pDC); afx_msg void OnPaint (); afx_msg BOOL OnQueryNewPalette (); afx_msg void OnPaletteChanged (CWnd* pFocusWnd); DECLARE_MESSAGE_MAP () }; |
PaletteDemo.cpp#include <afxwin.h> #include "PaletteDemo.h" CMyApp myApp; ///////////////////////////////////////////////////////////////////////// // CMyApp member functions BOOL CMyApp::InitInstance () { m_pMainWnd = new CMainWindow; m_pMainWnd->ShowWindow (m_nCmdShow); m_pMainWnd->UpdateWindow (); return TRUE; } ///////////////////////////////////////////////////////////////////////// // CMainWindow message map and member functions BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_CREATE () ON_WM_ERASEBKGND () ON_WM_PAINT () ON_WM_QUERYNEWPALETTE () ON_WM_PALETTECHANGED () END_MESSAGE_MAP () CMainWindow::CMainWindow () { Create (NULL, _T ("Palette Demo")); } int CMainWindow::OnCreate (LPCREATESTRUCT lpcs) { if (CFrameWnd::OnCreate (lpcs) == -1) return -1; // // Create a logical palette if running on a palettized adapter. // CClientDC dc (this); if (dc.GetDeviceCaps (RASTERCAPS) & RC_PALETTE) { struct { LOGPALETTE lp; PALETTEENTRY ape[63]; } pal; LOGPALETTE* pLP = (LOGPALETTE*) &pal; pLP->palVersion = 0x300; pLP->palNumEntries = 64; for (int i=0; i<64; i++) { pLP->palPalEntry[i].peRed = 0; pLP->palPalEntry[i].peGreen = 0; pLP->palPalEntry[i].peBlue = 255 - (i * 4); pLP->palPalEntry[i].peFlags = 0; } m_palette.CreatePalette (pLP); } return 0; } BOOL CMainWindow::OnEraseBkgnd (CDC* pDC) { CRect rect; GetClientRect (&rect); CPalette* pOldPalette; if ((HPALETTE) m_palette != NULL) { pOldPalette = pDC->SelectPalette (&m_palette, FALSE); pDC->RealizePalette (); } DoGradientFill (pDC, &rect); if ((HPALETTE) m_palette != NULL) pDC->SelectPalette (pOldPalette, FALSE); return TRUE; } void CMainWindow::OnPaint () { CRect rect; GetClientRect (&rect); CPaintDC dc (this); DoDrawText (&dc, &rect); } BOOL CMainWindow::OnQueryNewPalette () { if ((HPALETTE) m_palette == NULL) // Shouldn't happen, but return 0; // let's be sure. CClientDC dc (this); CPalette* pOldPalette = dc.SelectPalette (&m_palette, FALSE); UINT nCount; if (nCount = dc.RealizePalette ()) Invalidate (); dc.SelectPalette (pOldPalette, FALSE); return nCount; } void CMainWindow::OnPaletteChanged (CWnd* pFocusWnd) { if ((HPALETTE) m_palette == NULL) // Shouldn't happen, but return; // let's be sure. if (pFocusWnd != this) { CClientDC dc (this); CPalette* pOldPalette = dc.SelectPalette (&m_palette, FALSE); if (dc.RealizePalette ()) Invalidate (); dc.SelectPalette (pOldPalette, FALSE); } } void CMainWindow::DoGradientFill (CDC* pDC, LPRECT pRect) { CBrush* pBrush[64]; for (int i=0; i<64; i++) pBrush[i] = new CBrush (PALETTERGB (0, 0, 255 - (i * 4))); int nWidth = pRect->right - pRect->left; int nHeight = pRect->bottom - pRect->top; CRect rect; for (i=0; i<nHeight; i++) { rect.SetRect (0, i, nWidth, i + 1); pDC->FillRect (&rect, pBrush[(i * 63) / nHeight]); } for (i=0; i<64; i++) delete pBrush[i]; } void CMainWindow::DoDrawText (CDC* pDC, LPRECT pRect) { CFont font; font.CreatePointFont (720, _T ("Comic Sans MS")); pDC->SetBkMode (TRANSPARENT); pDC->SetTextColor (RGB (255, 255, 255)); CFont* pOldFont = pDC->SelectObject (&font); pDC->DrawText (_T ("Hello, MFC"), -1, pRect, DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER); pDC->SelectObject (pOldFont); } |
One of the more novel uses for a logical palette is for performing palette animation. Conventional computer animation is performed by repeatedly drawing, erasing, and redrawing images on the screen. Palette animation involves no drawing and erasing, but it can make images move just the same. A classic example of palette animation is a simulated lava flow that cycles shades of red, orange, and yellow to produce an image that resembles lava flowing down a hill. What's interesting is that the image is drawn only once. The illusion of motion is created by repeatedly reprogramming the system palette so that red becomes orange, orange becomes yellow, yellow becomes red, and so on. Palette animation is fast because it doesn't involve moving any pixels. A simple value written to a palette register on a video adapter can change the color of an entire screen full of pixels in the blink of an eye—to be precise, in the 1/60 of a second or so it takes for a monitor's electron guns to complete one screen refresh cycle.
What does it take to do palette animation in Windows? Just these three steps:
Figure 15-3. The LivePalette window.
The LivePalette application in Figure 15-3 and Figure 15-4 demonstrates how palette animation works. The window background is painted with bands of color (eight different colors in all) from PC_RESERVED entries in a logical palette. Brush colors are specified with PALETTEINDEX values. PALETTERGB values would work, too, but ordinary RGB values wouldn't because pixels whose colors will be animated must be painted with colors marked PC_RESERVED in the logical palette, not static colors. LivePalette sets a timer to fire every 500 milliseconds, and OnTimer animates the palette as follows:
PALETTEENTRY pe[8]; m_palette.GetPaletteEntries (7, 1, pe); m_palette.GetPaletteEntries (0, 7, &pe[1]); m_palette.AnimatePalette (0, 8, pe); |
The calls to CPalette::GetPaletteEntries initialize an array of PALETTEENTRY structures with values from the logical palette and simultaneously rotate every color up one position so that color 7 becomes color 0, color 0 becomes color 1, and so on. AnimatePalette then updates the colors on the screen by copying the values from the array directly to the corresponding entries in the system palette. It isn't necessary to call RealizePalette because the equivalent of a palette realization has already been performed.
The remainder of the program is very similar to the previous section's PaletteDemo program, with one notable exception: If RC_PALETTE is NULL, InitInstance displays a message box informing the user that palette animation isn't supported in the present environment and shuts down the application by returning FALSE. You'll see this message if you run LivePalette in anything other than a 256-color video mode.
Figure 15-4. The LivePalette application.
LivePalette.hclass CMyApp : public CWinApp { public: virtual BOOL InitInstance (); }; class CMainWindow : public CFrameWnd { protected: CPalette m_palette; void DoBkgndFill (CDC* pDC, LPRECT pRect); void DoDrawText (CDC* pDC, LPRECT pRect); public: CMainWindow (); protected: afx_msg int OnCreate (LPCREATESTRUCT lpcs); afx_msg BOOL OnEraseBkgnd (CDC* pDC); afx_msg void OnPaint (); afx_msg void OnTimer (UINT nTimerID); afx_msg BOOL OnQueryNewPalette (); afx_msg void OnPaletteChanged (CWnd* pFocusWnd); afx_msg void OnDestroy (); DECLARE_MESSAGE_MAP () }; |
LivePalette.cpp#include <afxwin.h> #include "LivePalette.h" CMyApp myApp; ///////////////////////////////////////////////////////////////////////// // CMyApp member functions BOOL CMyApp::InitInstance () { // // Verify that the host system is running in a palettized video mode. // CClientDC dc (NULL); if ((dc.GetDeviceCaps (RASTERCAPS) & RC_PALETTE) == 0) { AfxMessageBox (_T ("Palette animation is not supported on this " \ "device. Set the color depth to 256 colors and try again."), MB_ICONSTOP ¦ MB_OK); return FALSE; } // // Initialize the application as normal. // m_pMainWnd = new CMainWindow; m_pMainWnd->ShowWindow (m_nCmdShow); m_pMainWnd->UpdateWindow (); return TRUE; } ///////////////////////////////////////////////////////////////////////// // CMainWindow message map and member functions BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_CREATE () ON_WM_ERASEBKGND () ON_WM_PAINT () ON_WM_TIMER () ON_WM_QUERYNEWPALETTE () ON_WM_PALETTECHANGED () ON_WM_DESTROY () END_MESSAGE_MAP () CMainWindow::CMainWindow () { Create (NULL, _T ("Palette Animation Demo")); } int CMainWindow::OnCreate (LPCREATESTRUCT lpcs) { static BYTE bColorVals[8][3] = { 128, 128, 128, // Dark Gray 0, 0, 255, // Blue 0, 255, 0, // Green 0, 255, 255, // Cyan 255, 0, 0, // Red 255, 0, 255, // Magenta 255, 255, 0, // Yellow 192, 192, 192 // Light gray }; if (CFrameWnd::OnCreate (lpcs) == -1) return -1; // // Create a palette to support palette animation. // struct { LOGPALETTE lp; PALETTEENTRY ape[7]; } pal; LOGPALETTE* pLP = (LOGPALETTE*) &pal; pLP->palVersion = 0x300; pLP->palNumEntries = 8; for (int i=0; i<8; i++) { pLP->palPalEntry[i].peRed = bColorVals[i][0]; pLP->palPalEntry[i].peGreen = bColorVals[i][1]; pLP->palPalEntry[i].peBlue = bColorVals[i][2]; pLP->palPalEntry[i].peFlags = PC_RESERVED; } m_palette.CreatePalette (pLP); // // Program a timer to fire every half-second. // SetTimer (1, 500, NULL); return 0; } void CMainWindow::OnTimer (UINT nTimerID) { PALETTEENTRY pe[8]; m_palette.GetPaletteEntries (7, 1, pe); m_palette.GetPaletteEntries (0, 7, &pe[1]); m_palette.AnimatePalette (0, 8, pe); } BOOL CMainWindow::OnEraseBkgnd (CDC* pDC) { CRect rect; GetClientRect (&rect); CPalette* pOldPalette; pOldPalette = pDC->SelectPalette (&m_palette, FALSE); pDC->RealizePalette (); DoBkgndFill (pDC, &rect); pDC->SelectPalette (pOldPalette, FALSE); return TRUE; } void CMainWindow::OnPaint () { CRect rect; GetClientRect (&rect); CPaintDC dc (this); DoDrawText (&dc, &rect); } BOOL CMainWindow::OnQueryNewPalette () { CClientDC dc (this); dc.SelectPalette (&m_palette, FALSE); UINT nCount; if (nCount = dc.RealizePalette ()) Invalidate (); return nCount; } void CMainWindow::OnPaletteChanged (CWnd* pFocusWnd) { if (pFocusWnd != this) { CClientDC dc (this); dc.SelectPalette (&m_palette, FALSE); if (dc.RealizePalette ()) Invalidate (); } } void CMainWindow::OnDestroy () { KillTimer (1); } void CMainWindow::DoBkgndFill (CDC* pDC, LPRECT pRect) { CBrush* pBrush[8]; for (int i=0; i<8; i++) pBrush[i] = new CBrush (PALETTEINDEX (i)); int nWidth = pRect->right - pRect->left; int nHeight = (pRect->bottom - pRect->top) / 8; CRect rect; int y1, y2; for (i=0; i<8; i++) { y1 = i * nHeight; y2 = (i == 7) ? pRect->bottom - pRect->top : y1 + nHeight; rect.SetRect (0, y1, nWidth, y2); pDC->FillRect (&rect, pBrush[i]); } for (i=0; i<8; i++) delete pBrush[i]; } void CMainWindow::DoDrawText (CDC* pDC, LPRECT pRect) { CFont font; font.CreatePointFont (720, _T ("Comic Sans MS")); pDC->SetBkMode (TRANSPARENT); pDC->SetTextColor (RGB (255, 255, 255)); CFont* pOldFont = pDC->SelectObject (&font); pDC->DrawText (_T ("Hello, MFC"), -1, pRect, DT_SINGLELINE ¦ DT_CENTER ¦ DT_VCENTER); pDC->SelectObject (pOldFont); } |
A final word on palette usage: if your application absolutely, unequivocally has to have access to the entire system palette and not just the unused color entries that remain after the static colors are added, it can call ::SetSystemPaletteUse with a device context handle and a SYSPAL_NOSTATIC parameter to reduce the number of static colors from 20 to 2—black and white. On a 256-color video adapter, this means that 254 instead of just 236 colors can be copied from a logical palette to the system palette. The Win32 API documentation makes it pretty clear how ::SetSystemPaletteUse and its companion function ::GetSystemPaletteUse are used, so I'll say no more about them here. However, realize that replacing the static colors with colors of your own is an extremely unfriendly thing to do because it could corrupt the colors of title bars, push buttons, and other window elements throughout the entire system. Don't do it unless you have to.