VC++ 6.0 ebook chapter index
homepage aranna.altervista.org
Free counters!

Using Palettes

Traditionally, a palette is the board that a painter uses to mix colors. The word can also refer to the entire range of colors that an artist uses in creating a painting. In computer graphics, the palette is the range of colors available on a graphics output device such as a video display. The word can also refer to a lookup table on video boards that support 256-color modes.

Video Hardware

The palette lookup table on video boards works something like this:

click here to view full size

In 8-bit video modes, each pixel has 8 bits. The pixel value addresses a lookup table that contains 256 RGB values. These RGB values can be a full 24 bits wide or can be smaller, typically 18 bits wide (that is, 6 bits for each red, green, and blue primary). The values for each color are input to digital-to-analog converters for the three analog red, green, and blue signals that go to the monitor.

The palette lookup table can generally be loaded with arbitrary values through software, but there are some obstacles for a device-independent windowing interface such as that in Microsoft Windows. First, Windows must provide a software interface so that applications can access the Palette Manager without directly fooling around with the hardware. The second problem is more serious: because all applications are sharing the same video display and running side by side, one application's use of the palette lookup table might interfere with another's.

This is where the Windows Palette Manager (introduced in Windows 3.0) comes into play. Windows reserves 20 of the 256 colors for itself and lets applications change the other 236. (In certain cases, an application can change up to 254 of the 256 colors—all except black and white—but this is a bit of a chore.) The 20 colors that Windows reserves for system use, sometimes called the 20 static colors) are shown in Figure 16-1.

Pixel Bits RGB Value Color Name Pixel Bits RGB Value Color Name
00000000 00 00 00 Black 11111111 FF FF FF White
00000001 80 00 00 Dark Red 11111110 00 FF FF Cyan
00000010 00 80 00 Dark Green 11111101 FF 00 FF Magenta
00000011 80 80 00 Dark Yellow 11111100 00 00 FF Blue
00000100 00 00 80 Dark Blue 11111011 FF FF 00 Yellow
00000101 80 00 80 Dark Magenta 11111010 00 FF 00 Green
00000110 00 80 80 Dark Cyan 11111001 FF 00 00 Red
00000111 C0 C0 C0 Light Gray 11111000 80 80 80 Dark Gray
00001000 C0 DC C0 Money Green 11110111 A0 A0 A4 Medium Gray
00001001 A6 CA F0 Sky Blue 11110110 FF FB F0 Cream

Figure 16-1. The 20 reserved colors in 256-color video modes.

When running in 256-color video modes, Windows maintains a "system palette," which is the same as the hardware palette lookup table on the video board. The default system palette is shown in Figure 16-1. Applications can change the other 236 colors by specifying "logical palettes." If more than one application is using logical palettes, Windows gives highest priority to the active window. (As you know, the active window is the window that has the highlighted title bar and appears to the foreground of all the other windows.) We'll examine how this works in the context of a simple sample program.

For running the programs shown in the remainder of this chapter, you may want to switch your video board into a 256-color mode. Right click the mouse on the desktop, pick Properties from the menu, and select the Settings tab.

Displaying Gray Shades

The GRAYS1 program shown in Figure 16-2 does not use the Windows Palette Manager but instead tries to normally display 65 shades of gray as a "fountain" of color ranging black to white.

Figure 16-2. The GRAYS1 program.

GRAYS1.C

/*---------------------------------------
   GRAYS1.C -- Gray Shades
               (c) Charles Petzold, 1998
  ---------------------------------------*/

#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("Grays1") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"), 
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Shades of Gray #1"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,             
                          NULL, NULL, hInstance, NULL) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static int  cxClient, cyClient ;
     HBRUSH      hBrush ;
     HDC         hdc ;
     int         i ;
     PAINTSTRUCT ps ;
     RECT        rect ;
     
     switch (message)
     {
     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;
          
     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;

               // Draw the fountain of grays

          for (i = 0 ; i < 65 ; i++)
          {
               rect.left   = i * cxClient / 65 ;
               rect.top    = 0 ;
               rect.right  = (i + 1) * cxClient / 65 ;
               rect.bottom = cyClient ;

               hBrush = CreateSolidBrush (RGB (min (255, 4 * i), 
                                               min (255, 4 * i), 
                                               min (255, 4 * i))) ;
               FillRect (hdc, &rect, hBrush) ;
               DeleteObject (hBrush) ;
          }
          EndPaint (hwnd, &ps) ;
          return 0 ;

     case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

During the WM_PAINT message, the program makes 65 calls to the FillRect function, each time with a brush created using a different gray shade. The gray shades are the RGB values (0, 0, 0), (4, 4, 4), (8, 8, 8), and so forth, until the last one, which is (255, 255, 255). That last one is the reason for the min macro in the CreateSolidBrush function.

If you run this program in a 256-color video mode, you'll see 65 shades of gray from black to white, but almost all of them are rendered using dithering. The only pure colors are black, dark gray (128, 128, 128), light gray (192, 192, 192), and white. The other colors are various bit patterns combining these pure colors. If we were displaying lines or text rather than filled areas using these 65 gray shades, Windows would not use dithering and would use only the four pure colors. If we were displaying a bitmap, the image would be approximated using the 20 standard Windows colors, as you can see for yourself by running one of the programs from the last chapter and loading in a color or gray-shade DIB. Windows normally does not use dithering for bitmaps.

The GRAYS2 program shown in Figure 16-3 demonstrates the most important Palette Manager functions and messages with little extraneous code.

Figure 16-3. The GRAYS2 program.

GRAYS2.C

/*-----------------------------------------------
   GRAYS2.C -- Gray Shades Using Palette Manager
               (c) Charles Petzold, 1998
  -----------------------------------------------*/

#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("Grays2") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"), 
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Shades of Gray #2"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,             
                          NULL, NULL, hInstance, NULL) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static HPALETTE hPalette ;
     static int      cxClient, cyClient ;
     HBRUSH          hBrush ;
     HDC             hdc ;
     int             i ;
     LOGPALETTE    * plp ;
     PAINTSTRUCT     ps ;
     RECT            rect ;
     
     switch (message)
     {
     case WM_CREATE:
               // Set up a LOGPALETTE structure and create a palette

          plp = malloc (sizeof (LOGPALETTE) + 64 * sizeof (PALETTEENTRY)) ;

          plp->palVersion    = 0x0300 ;
          plp->palNumEntries = 65 ;

          for (i = 0 ; i < 65 ; i++)
          {
               plp->palPalEntry[i].peRed   = (BYTE) min (255, 4 * i) ;
               plp->palPalEntry[i].peGreen = (BYTE) min (255, 4 * i) ;
               plp->palPalEntry[i].peBlue  = (BYTE) min (255, 4 * i) ;
               plp->palPalEntry[i].peFlags = 0 ;
          }
          hPalette = CreatePalette (plp) ;
          free (plp) ;
          return 0 ;

     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;

               // Select and realize the palette in the device context

          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;

               // Draw the fountain of grays

          for (i = 0 ; i < 65 ; i++)
          {
               rect.left   = i * cxClient / 64 ;
               rect.top    = 0 ;
               rect.right  = (i + 1) * cxClient / 64 ;
               rect.bottom = cyClient ;

               hBrush = CreateSolidBrush (PALETTERGB (min (255, 4 * i), 
                                                      min (255, 4 * i), 
                                                      min (255, 4 * i))) ;
               FillRect (hdc, &rect, hBrush) ;
               DeleteObject (hBrush) ;
          }
          EndPaint (hwnd, &ps) ;
          return 0 ;

     case WM_QUERYNEWPALETTE:
          if (!hPalette)
               return FALSE ;

          hdc = GetDC (hwnd) ;
          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;
          InvalidateRect (hwnd, NULL, TRUE) ;

          ReleaseDC (hwnd, hdc) ;
          return TRUE ;

     case WM_PALETTECHANGED:
          if (!hPalette || (HWND) wParam == hwnd)
               break ;

          hdc = GetDC (hwnd) ;
          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;
          UpdateColors (hdc) ;
  
          ReleaseDC (hwnd, hdc) ;
          break ;

     case WM_DESTROY:
          DeleteObject (hPalette) ;
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

Generally the first step in using the Palette Manager is to create a logical palette by calling the CreatePalette function. The logical palette contains all the colors—or rather, as many as 236 colors—that the program needs. The GRAYS1 program handles this job during the WM_CREATE message. It initializes the fields of a LOGPALETTE ("logical palette") structure and passes a pointer to this structure to the CreatePalette function. CreatePalette returns a handle to the logical palette, which is stored in the static variable hPalette.

The LOGPALETTE structure is defined like so:

typedef struct
{
     WORD         palVersion ;
     WORD         palNumEntries ;
     PALETTEENTRY palPalEntry[1] ;
}
LOGPALETTE, * PLOGPALETTE ;

The first field is always set to 0x0300, indicating Windows 3.0 compatibility, and the second field is set to the number of entries in the palette table. The third field in the LOGPALETTE structure is an array of PALETTEENTRY structures, one for each of the palette entries. The PALETTEENTRY structure is defined like this:

typedef struct
{
     BYTE peRed ;
     BYTE peGreen ;
     BYTE peBlue ;
     BYTE peFlags ;
}
PALETTEENTRY, * PPALETTEENTRY ;

Each of the PALETTEENTRY structures defines an RGB color value that we want in the palette.

Notice that LOGPALETTE is defined for an array of only one PALETTEENTRY structure. You need to allocate some memory large enough for one LOGPALETTE structure and additional PALETTEENTRY structures. GRAYS2 wants 65 gray shades, so it allocates enough memory for a LOGPALETTE structure and 64 additional PALETTEENTRY structures. It sets the palNumEntries field to 65. GRAYS2 then goes through a loop from 0 through 64, calculates a gray level (which is simply 4 times the loop index, but not greater than 255), and sets the peRed, peGreen, and peBlue fields of the structure to this gray level. The peFlags field is set to 0. The program passes the pointer to this block of memory to CreatePalette, saves the palette handle in a static variable, and then frees the memory.

A logical palette is a GDI object. Programs should delete any logical palettes they create. WndProc takes care of deleting the logical palette during the WM_DESTROY message by calling DeleteObject.

Notice that the logical palette is independent of a device context. Before you can actually make use of it, it must be selected into a device context and "realized." During the WM_PAINT message, the SelectPalette function selects the logical palette into the device context. This is similar to the SelectObject function except that a third argument is included. Normally this third argument is set to FALSE. If the third argument to SelectPalette is set to TRUE, the palette is always a "background palette," which means that it gets whatever unused entries still exist in the system palette after all other programs have realized their palettes.

Only one logical palette can be selected into the device context at any time. The function returns the handle of the logical palette previously selected in the device context. You can save this for selecting back into the device context if you wish to.

The RealizePalette function causes Windows to "realize" the logical palette in the device context by mapping the colors to the system palette, which in turn corresponds to the actual physical palette of the video board. The real work goes on during this function call. Windows must determine whether the window calling the function is active or inactive and perhaps notify other windows that the system palette is changing. (We'll see how this notification works shortly.)

You'll recall that GRAYS1 used the RGB macro to specify the color of the solid brush. The RGB macro constructs a 32-bit long integer (known as a COLORREF value) where the upper byte is 0 and the lower 3 bytes are the intensities of red, green, and blue.

A program that uses the Windows Palette Manager can continue to use RGB color values to specify color. However, these RGB color values will not give access to the additional colors in the logical palette. They will have the same effect as if the Palette Manager were not used. To make use of the additional colors in the logical palette, you use the PALETTERGB macro. A "Palette RGB" color is very much like an RGB color except that the high byte of the COLORREF value is set to 2 rather than 0.

Here are the important rules:

For example, during WM_PAINT processing in GRAYS2, after you select and realize the logical palette, if you try to display something in red, it will come out as a shade of gray. You need to use RGB color values to select colors not in the logical palette.

Notice that GRAYS2 never checks to see whether the video display driver actually supports palette management. When running GRAYS2 under video modes that do not support palette management (that is, all video modes that are not 256 colors), GRAYS2 is functionally equivalent to GRAYS1.

The Palette Messages

If multiple Windows programs are using the Palette Manager, the active window gets priority over the palette. The most recently active window gets second priority, and so forth. Whenever a new program becomes active, the Windows Palette Manager usually must reorganize the system palette table.

If a program specifies a color in its logical palette that is identical to one of the 20 reserved colors, Windows will map that logical palette entry to that color. Also, if two or more applications specify the same color in their logical palettes, these applications will share the system palette entry. A program can override this default behavior by specifying the constant PC_NOCOLLAPSE as the peFlags field of the PALETTEENTRY structure. (The other two possible flags are PC_EXPLICIT, which is used to display the system palette, and PC_RESERVED, which is used in palette animation. I'll demonstrate both of these flags later in this chapter.)

To help in organizing the system palette, the Windows Palette Manager includes two messages sent to main windows.

The first is QM_QUERYNEWPALETTE. This message is sent to a main window when it is about to become active. If your program uses the Palette Manager when drawing on your window, it must process this message. GRAYS2 demonstrates how to do so. The program obtains a device context handle, selects the palette into it, calls RealizePalette, and then invalidates the window to generate a WM_PAINT message. The window procedure returns TRUE from this message if it realizes its logical palette and FALSE otherwise.

Whenever the system palette changes as a result of a WM_QUERYNEWPALETTE message, Windows sends the WM_PALETTECHANGED message to all main windows starting with the most active window and proceeding down the window chain. This allows the foreground window to have priority. The wParam value passed to the window procedure is the handle of the active window. A program using the Palette Manager should process this message only if wParam is not equal to the program's window handle.

Generally, any program that uses a customized palette calls SelectPalette and RealizePalette while processing WM_PALETTECHANGED. When subsequent windows call RealizePalette during the message, Windows first checks for matches of RGB colors in the logical palette with RGB colors already loaded in the system palette. If two programs need the same color, the same system palette entry is used for both. Next Windows checks for unused system palette entries. If none exist, the color in the logical palette is mapped to the closest color from the 20 reserved entries.

If you don't care about how the client area looks when your program is inactive, you do not need to process the WM_PALETTECHANGED message. Otherwise, you have two choices. GRAYS2 shows one of them: As when processing the WM_QUERYNEWPALETTE message, it gets a device context, selects the palette into it, and then calls RealizePalette. At this point, it could call InvalidateRect as when processing WM_QUERYNEWPALETTE. Instead, GRAYS2 calls UpdateColors. This function is usually more efficient than repainting the window, and it changes the values of pixels in your window to help preserve the previous colors.

Most programs that use the Palette Manager will have WM_QUERYNEWPALETTE and WM_PALETTECHANGED message processing identical to that shown in GRAYS2.

The Palette Index Approach

The GRAYS3 program shown in Figure 16-4 is very similar to GRAYS2 but uses a macro called PALETTEINDEX instead of PALETTERGB during WM_PAINT processing.

Figure 16-4. The GRAYS3 program.

GRAYS3.C

/*-----------------------------------------------
   GRAYS3.C -- Gray Shades Using Palette Manager
               (c) Charles Petzold, 1998
  -----------------------------------------------*/

#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     static TCHAR szAppName[] = TEXT ("Grays3") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"), 
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("Shades of Gray #3"),
                          WS_OVERLAPPEDWINDOW,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,             
                          NULL, NULL, hInstance, NULL) ;
     
     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;
     
     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static HPALETTE hPalette ;
     static int      cxClient, cyClient ;
     HBRUSH          hBrush ;
     HDC             hdc ;
     int             i ;
     LOGPALETTE    * plp ;
     PAINTSTRUCT     ps ;
     RECT            rect ;
     switch (message)
     {
     case WM_CREATE:
               // Set up a LOGPALETTE structure and create a palette

          plp = malloc (sizeof (LOGPALETTE) + 64 * sizeof (PALETTEENTRY)) ;

          plp->palVersion    = 0x0300 ;
          plp->palNumEntries = 65 ;

          for (i = 0 ; i < 65 ; i++)
          {
               plp->palPalEntry[i].peRed   = (BYTE) min (255, 4 * i) ;
               plp->palPalEntry[i].peGreen = (BYTE) min (255, 4 * i) ;
               plp->palPalEntry[i].peBlue  = (BYTE) min (255, 4 * i) ;
               plp->palPalEntry[i].peFlags = 0 ;
          }
          hPalette = CreatePalette (plp) ;
          free (plp) ;
          return 0 ;

     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;
          
     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;

               // Select and realize the palette in the device context

          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;

               // Draw the fountain of grays

          for (i = 0 ; i < 65 ; i++)
          {
               rect.left   = i * cxClient / 64 ;
               rect.top    = 0 ;
               rect.right  = (i + 1) * cxClient / 64 ;
               rect.bottom = cyClient ;

               hBrush = CreateSolidBrush (PALETTEINDEX (i)) ;
               FillRect (hdc, &rect, hBrush) ;
               DeleteObject (hBrush) ;
          }

          EndPaint (hwnd, &ps) ;
          return 0 ;

     case WM_QUERYNEWPALETTE:
          if (!hPalette)
               return FALSE ;

          hdc = GetDC (hwnd) ;
          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;
          InvalidateRect (hwnd, NULL, FALSE) ;
     
          ReleaseDC (hwnd, hdc) ;
          return TRUE ;

     case WM_PALETTECHANGED:
          if (!hPalette || (HWND) wParam == hwnd)
               break ;

          hdc = GetDC (hwnd) ;
          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;
          UpdateColors (hdc) ;

          ReleaseDC (hwnd, hdc) ;
          break ;

     case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

A "Palette Index" color is quite different from the Palette RGB color. The high byte is 1, and the value in the low byte is an index into the logical palette currently selected in the device context. In GRAYS3, the logical palette has 65 entries. The indices for these entries are thus 0 through 64. The value

PALETTEINDEX (0)

refers to black,

PALETTEINDEX (32)

refers to medium gray, and

PALETTEINDEX (64)

refers to white.

Using palette indices is more efficient than using RGB values because Windows does not need to perform a nearest-color search.

Querying the Palette Support

As you can easily verify, the GRAYS2 and GRAYS3 programs run fine when Windows is running under a 16-bit or 24-bit video mode. But in some cases, a Windows application that wishes to use the Palette Manager might want to first determine whether the device driver supports it. You can do this by calling GetDeviceCaps using a device context handle for the video display and the RASTERCAPS parameter. The function returns an integer composed of a series of flags. You can test palette support by performing a bitwise AND between the return value and the constant RC_PALETTE:

RC_PALETTE & GetDeviceCaps (hdc, RASTERCAPS)

If this value is nonzero, the device driver for the video display supports palette manipulation. In that case, three other important items are also available from GetDeviceCaps. The function call

GetDeviceCaps (hdc, SIZEPALETTE)

returns the total size of the palette table on the video board. This is the same as the total number of colors that can be simultaneously displayed. Because the Palette Manager is invoked only for video display modes with 8 bits per pixel, this value will be 256.

The function call

GetDeviceCaps (hdc, NUMRESERVED)

returns the number of colors in the palette table that the device driver reserves for system purposes. This value will be 20. Without invoking the Palette Manager, these are the only pure colors a Windows application can use in a 256-color video mode. To use the other 236 colors, a program must use the Palette Manager functions.

One additional item is also available:

GetDeviceCaps (hdc, COLORRES)

This value tells you the resolution (in bits) of the RGB color values loaded into the hardware palette table. These are the bits going into the digital-to-analog converters. Some video display adapters use only 6-bit ADCs, so this value would be 18. Others use 8-bit ADCs, so this value would be 24.

It is useful for a Windows program to take a look at this color resolution value and behave accordingly. For example, if the color resolution is 18, it makes no sense for a program to attempt to request 128 shades of gray because only 64 discrete shades of gray are possible. Requesting 128 shades of gray will unnecessarily fill the hardware palette table with redundant entries.

The System Palette

As I've mentioned, the system palette in Windows corresponds directly to the hardware palette lookup table on the video adapter board. (However, the hardware palette lookup table may have a lower color resolution than the system palette.) A program can obtain any or all of the RGB entries in the system palette by calling this function:

GetSystemPaletteEntries (hdc, uStart, uNum, &pe) ;

This function works only if the video adapter mode supports palette manipulation. The second and third arguments are unsigned integer values that indicate the index of the first palette entry you want and the number of palette entries you want. The last argument is a pointer to a structure of type PALETTEENTRY.

You can use this function in several ways. A program can define one PALETTEENTRY structure like this,

PALETTEENTRY pe ;

and then call GetSystemPaletteEntries multiple times like so,

GetSystemPaletteEntries (hdc, i, 1, &pe) ;

with i being from 0 to one less than the value returned from GetDeviceCaps with the SIZEPALETTE index, which will be 255. Or a program can obtain all the system palette entries by defining a pointer to a PALETTEENTRY structure and then allocating a block of memory sufficient to hold as many PALETTEENTRY structures as indicated by the size of the palette.

The GetSystemPaletteEntries function really lets you examine the hardware palette table. The entries in the system palette are in the order of increasing values of pixel bits that are used to denote color in the video display buffer. I'll demonstrate how to do this shortly.

Other Palette Functions

As we saw earlier, a Windows program can change the system palette but only indirectly. The first step is creating a logical palette, which is basically an array of RGB color values that the program wants to use. The CreatePalette function does not cause any change to the system palette or the palette table on the video board. The logical palette must be selected into a device context and realized before anything happens.

A program can query the RGB color values in a logical palette by calling

GetPaletteEntries (hPalette, uStart, uNum, &pe) ;

You can use this function in the same way you use GetSystemPaletteEntries. But note that the first parameter is a handle to the logical palette rather than a handle to a device context.

A corresponding function lets you change values in the logical palette after it has been created:

SetPaletteEntries (hPalette, uStart, uNum, &pe) ;

Again, keep in mind that calling this function does not cause any change to the system palette—even if the palette is currently selected in a device context. This function also cannot change the size of the logical palette. For that, use ResizePalette.

The following function accepts an RGB color reference value as the last argument and returns an index into the logical palette that corresponds to the RGB color value that most closely approximates it:

iIndex = GetNearestPaletteIndex (hPalette, cr) ;

The second argument is a COLORREF value. If you wish, you can then obtain the actual RGB color value in the logical palette by calling GetPaletteEntries.

Programs that need more than 236 custom colors in 8-bit video modes can call GetSystemPaletteUse. This lets a program set 254 custom colors; the system reserves only black and white. However, the program should do this only when it is maximized to fill the screen, and it should set some system colors to black and white so that title bars and menus and such are still visible.

The Raster-Op Problem

As you know from Chapter 5, GDI allows you to draw lines and fill areas by using various "drawing modes" or "raster operations." You set the drawing mode using SetROP2. The "2" indicates a binary raster operation between two objects; tertiary raster operations are used with BitBlt and similar functions. These raster operations determine how the pixels of the object you're drawing combine with the pixels of the surface. For example, you can draw a line so that the pixels of the line are combined with the pixels of the display using a bitwise exclusive-OR operation.

The raster operations work by performing bitwise operations on pixel bits. Changing the palette can affect how these raster operations work. The raster operations manipulate pixel bits, and these pixel bits might have no relationship to actual colors.

You can see this for yourself by running the GRAYS2 or GRAYS3 program. Drag the top or bottom sizing border across the window. Windows displays the dragged sizing border by using a raster operation that inverts the background pixel bits. The intent is to make the dragged sizing border always visible. But with the GRAYS2 and GRAYS3 programs, you'll probably see various random colors instead. These colors happen to correspond to unused entries in the palette table that result from inverting the pixel bits of the display. The visible color is not being inverted—only the pixel bits.

As you can see in Figure 16-1, the 20 standard reserved colors are placed at the top and bottom of the system palette so that the results of raster operations are still normal. However, once you begin changing the palette—and particularly if you take over the reserved colors—then raster operations of color objects can become meaningless.

The only guarantee you have is that the raster operations will work with black and white. Black is the first entry in the system palette (all pixel bits set to 0), and white is the last entry (all pixel bits set to 1). These entries cannot be changed. If you need to predict the results of raster operations on color objects, you can do so by getting the system palette table and looking at the RGB color values for the various pixel-bit values.

Looking at the System Palette

Programs running under Windows deal with logical palettes; Windows sets up the colors in the system palette to best service all programs using logical palettes. The system palette mirrors the hardware lookup table of the video board. Thus, taking a look at the system palette can help in debugging palette applications.

I'm going to show you three programs that display the contents of the system palette because there are three quite different approaches to this problem.

The SYSPAL1 program is shown in Figure 16-5. This program uses the GetSystemPaletteEntries function that I described above.

Figure 16-5. The SYSPAL1 program.

SYSPAL1.C

/*----------------------------------------
   SYSPAL1.C -- Displays system palette
                (c) Charles Petzold, 1998
  ----------------------------------------*/

#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

TCHAR szAppName [] = TEXT ("SysPal1") ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("System Palette #1"), 
                          WS_OVERLAPPEDWINDOW, 
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     if (!hwnd)
          return 0 ;

     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

BOOL CheckDisplay (HWND hwnd)
{
     HDC hdc ;
     int iPalSize ;

     hdc = GetDC (hwnd) ;
     iPalSize = GetDeviceCaps (hdc, SIZEPALETTE) ;
     ReleaseDC (hwnd, hdc) ;
     if (iPalSize != 256)
     {
          MessageBox (hwnd, TEXT ("This program requires that the video ")
                            TEXT ("display mode have a 256-color palette."),
                      szAppName, MB_ICONERROR) ;
          return FALSE ;
     }
     return TRUE ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static int   cxClient, cyClient ;
     static SIZE  sizeChar ;
     HDC          hdc ;
     HPALETTE     hPalette ;
     int          i, x, y ;
     PAINTSTRUCT  ps ;
     PALETTEENTRY pe [256] ;
     TCHAR        szBuffer [16] ;

     switch (message)
     {
     case WM_CREATE:
          if (!CheckDisplay (hwnd))
               return -1 ;

          hdc = GetDC (hwnd) ;
          SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
          GetTextExtentPoint32 (hdc, TEXT ("FF-FF-FF"), 10, &sizeChar) ;
          ReleaseDC (hwnd, hdc) ;
          return 0 ;
     
     case WM_DISPLAYCHANGE:
          if (!CheckDisplay (hwnd))
               DestroyWindow (hwnd) ;

          return 0 ;

     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;
          SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;

          GetSystemPaletteEntries (hdc, 0, 256, pe) ;

          for (i = 0, x = 0, y = 0 ; i < 256 ; i++)
          {
               wsprintf (szBuffer, TEXT ("%02X-%02X-%02X"),
                         pe[i].peRed, pe[i].peGreen, pe[i].peBlue) ;

               TextOut (hdc, x, y, szBuffer, lstrlen (szBuffer)) ;

               if ((x += sizeChar.cx) + sizeChar.cx > cxClient)
               {
                    x = 0 ;
          
                    if ((y += sizeChar.cy) > cyClient)
                         break ;
               }
          }
          EndPaint (hwnd, &ps) ;
          return 0 ;

     case WM_PALETTECHANGED:
          InvalidateRect (hwnd, NULL, FALSE) ;
          return 0 ;

     case WM_DESTROY:
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

As with the other programs in the SYSPAL series, SYSPAL1 does not run unless GetDeviceCaps with the SIZEPALETTE argument returns 256.

Notice that SYSPAL1's client area is invalidated whenever it receives a WM_PALETTECHANGED message. During the resultant WM_PAINT message, SYSPAL1 calls GetSystemPaletteEntries with an array of 256 PALETTEENTRY structures. The RGB values are displayed as text strings in the client area. When you run the program, note that the 20 reserved colors are the first 10 and last 10 in the list of RGB values, as indicated by Figure 16-1.

While SYSPAL1 is certainly displaying useful information, it's not quite the same as actually seeing the 256 colors. That's a job for SYSPAL2, shown in Figure 16-6.

Figure 16-6. The SYSPAL2 program.

SYSPAL2.C

/*----------------------------------------
   SYSPAL2.C -- Displays system palette
                (c) Charles Petzold, 1998
  ----------------------------------------*/

#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

TCHAR szAppName [] = TEXT ("SysPal2") ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, TEXT ("System Palette #2"), 
                          WS_OVERLAPPEDWINDOW, 
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     if (!hwnd)
          return 0 ;

     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

BOOL CheckDisplay (HWND hwnd)
{
     HDC hdc ;
     int iPalSize ;

     hdc = GetDC (hwnd) ;
     iPalSize = GetDeviceCaps (hdc, SIZEPALETTE) ;
     ReleaseDC (hwnd, hdc) ;

     if (iPalSize != 256)
     {
          MessageBox (hwnd, TEXT ("This program requires that the video ")
                            TEXT ("display mode have a 256-color palette."),
                      szAppName, MB_ICONERROR) ;
          return FALSE ;
     }
     return TRUE ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static HPALETTE hPalette ;
     static int      cxClient, cyClient ;
     HBRUSH          hBrush ;
     HDC             hdc ;
     int             i, x, y ;
     LOGPALETTE    * plp ;
     PAINTSTRUCT     ps ;
     RECT            rect ;
     switch (message)
     {
     case WM_CREATE:
          if (!CheckDisplay (hwnd))
               return -1 ;

          plp = malloc (sizeof (LOGPALETTE) + 255 * sizeof (PALETTEENTRY)) ;

          plp->palVersion    = 0x0300 ;
          plp->palNumEntries = 256 ;

          for (i = 0 ; i < 256 ; i++)
          {
               plp->palPalEntry[i].peRed   = i ;
               plp->palPalEntry[i].peGreen = 0 ;
               plp->palPalEntry[i].peBlue  = 0 ;
               plp->palPalEntry[i].peFlags = PC_EXPLICIT ;
          }
          
          hPalette = CreatePalette (plp) ;
          free (plp) ;          
          return 0 ;
     
     case WM_DISPLAYCHANGE:
          if (!CheckDisplay (hwnd))
               DestroyWindow (hwnd) ;

          return 0 ;

     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;

          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;

          for (y = 0 ; y < 16 ; y++)
          for (x = 0 ; x < 16 ; x++)
          {
               hBrush = CreateSolidBrush (PALETTEINDEX (16 * y + x)) ;
               SetRect (&rect, x      * cxClient / 16,  y      * cyClient / 16,
                              (x + 1) * cxClient / 16, (y + 1) * cyClient / 16);
               FillRect (hdc, &rect, hBrush) ;
               DeleteObject (hBrush) ;
          }
          EndPaint (hwnd, &ps) ;
          return 0 ;

     case WM_PALETTECHANGED:
          if ((HWND) wParam != hwnd)
               InvalidateRect (hwnd, NULL, FALSE) ;

          return 0 ;

     case WM_DESTROY:
          DeleteObject (hPalette) ;
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

SYSPAL2 creates a logical palette during the WM_CREATE message. But notice that all 256 values in the logical palette are palette indices ranging from 0 to 255 and that the peFlags field is PC_EXPLICIT. The definition of this flag is this: "Specifies that the low-order word of the logical palette entry designates a hardware palette index. This flag allows the application to show the contents of the display device palette." Thus, this flag is specifically intended for doing what we're trying to do.

During the WM_PAINT message, SYSPAL2 selects this palette into its device context and realizes it. This does not cause any reorganization of the system palette but instead allows the program to specify colors in the system palette by using the PALETTEINDEX macro. SYSPAL2 does this to display 256 rectangles. Again, when you run this program, notice that the first 10 and last 10 colors of the top row and bottom row are the 20 reserved colors shown in Figure 16-1. As you run programs that use their own logical palettes, the display changes.

If you like seeing the colors in SYSPAL2 but would like RGB values as well, run the program in conjunction with the WHATCLR program from Chapter 8.

The third version in the SYSPAL series uses a technique that occurred to me only recently—some seven years after I first started exploring the Windows Palette Manager.

Virtually all the GDI functions specify color—either directly or indirectly—as an RGB value. Somewhere deep in GDI this is converted into certain pixel bits that correspond to that color. In some video modes (for example, in 16-bit or 24-bit color mode), this conversion is rather straightforward. In other video modes (4-bit or 8-bit color), this can involve a nearest-color search.

However, there are two GDI functions that let you specify color directly in pixel bits. These two functions used in this way are, of course, highly device-dependent. They are so device-dependent that they can directly display the actual palette lookup table on the video display adapter. These two functions are BitBlt and StretchBlt.

The SYSPAL3 program in Figure 16-7 shows how to use StretchBlt to display the colors in the system palette.

Figure 16-7. The SYSPAL3 program.

SYSPAL3.C

/*----------------------------------------
   SYSPAL3.C -- Displays system palette
                (c) Charles Petzold, 1998
  ----------------------------------------*/

#include <windows.h>

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

TCHAR szAppName [] = TEXT ("SysPal3") ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;

     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;

      if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }

    hwnd = CreateWindow (szAppName, TEXT ("System Palette #3"), 
                          WS_OVERLAPPEDWINDOW, 
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     if (!hwnd)
          return 0 ;

     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

BOOL CheckDisplay (HWND hwnd)
{
     HDC hdc ;
     int iPalSize ;

     hdc = GetDC (hwnd) ;

     iPalSize = GetDeviceCaps (hdc, SIZEPALETTE) ;
     ReleaseDC (hwnd, hdc) ;

     if (iPalSize != 256)
     {
          MessageBox (hwnd, TEXT ("This program requires that the video ")
                            TEXT ("display mode have a 256-color palette."),
                      szAppName, MB_ICONERROR) ;
          return FALSE ;
     }
     return TRUE ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static HBITMAP hBitmap ;
     static int     cxClient, cyClient ;
     BYTE           bits [256] ;
     HDC            hdc, hdcMem ;
     int            i ;
     PAINTSTRUCT    ps ;

     switch (message)
     {
     case WM_CREATE:
          if (!CheckDisplay (hwnd))
               return -1 ;

          for (i = 0 ; i < 256 ; i++)
               bits [i] = i ;
          
          hBitmap = CreateBitmap (16, 16, 1, 8, &bits) ;
          return 0 ;
                                                        
     case WM_DISPLAYCHANGE:
          if (!CheckDisplay (hwnd))
               DestroyWindow (hwnd) ;

          return 0 ;

     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;

          hdcMem = CreateCompatibleDC (hdc) ;
          SelectObject (hdcMem, hBitmap) ;

          StretchBlt (hdc,    0, 0, cxClient, cyClient,
                      hdcMem, 0, 0, 16, 16, SRCCOPY) ;

          DeleteDC (hdcMem) ;
          EndPaint (hwnd, &ps) ;
          return 0 ;

     case WM_DESTROY:
          DeleteObject (hBitmap) ;
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

During the WM_CREATE message, SYSPAL3 uses CreateBitmap to create a 16-by-16 bitmap with 8 bits per pixel. The last argument to the function is an array of 256 bytes containing the numbers 0 through 255. These are the 256 possible pixel-bit values. During the WM_PAINT message, the program selects this bitmap into a memory device context and uses StretchBlt to display it to fill the client area. Windows simply transfers the pixel bits in the bitmap to the hardware of the video display, thus allowing these pixel bits to access the 256 entries in the palette lookup table. The program's client area doesn't even need to be invalidated on receipt of the WM_PALETTECHANGED message—any change to the lookup table is immediately reflected in SYSPAL3's display.