Color, Well...

by Lee White

Published 1998-08-01    Printer-friendly version

Download the code here

Do you or your end users ever get tired of using the ColorDialog? Give them a color well instead, and dive into the Windows API for your own benefit, as well. Did someone say well?

This project delves into some of the uses of the Windows API to provide an easy-to-use color control. Some call this a well. Here it is called a pool!

The project is a simple color pool that could be used on a toolbar or as a control wherever preferred. The example provides all that's needed to build a color pool or to provide a means to select a color from an image using the commonly accepted "eye-dropper" cursor. All bitmaps and cursors used in the example are provided within the source download.

Image1.gif

The screen captured above shows the color pool, selection cursor, sample box and readouts of the component colors (red, green and blue) displayed on the toolbar. This pool is created from three controls (bitmap, region and panel) although only bitmap and region are required. The panel is used solely to provide a 3D effect.

The pool is also sensitive to the color depth of the video in use so it can be used on 16, 256 and higher color depths using the same code but three different pool images.

Listed here are the Windows APIs used in this project. (Prototypes provided are for 16-bit only.)

MODULE('')
  GetDC(UNSIGNED),UNSIGNED,PASCAL,NAME('GetDC')
  ReleaseDC(UNSIGNED,UNSIGNED),SIGNED,PASCAL,NAME('ReleaseDC'),PROC
  SelectObject(UNSIGNED,UNSIGNED),UNSIGNED,PASCAL,NAME('SelectObject'),PROC
  GetStockObject(SIGNED),UNSIGNED,PASCAL,NAME('GetStockObject')
  DeleteObject(UNSIGNED),BOOL,PASCAL,NAME('DeleteObject'),PROC
  CreateSolidBrush(ULONG),UNSIGNED,PASCAL,NAME('CreateSolidBrush')
  Rectangle(UNSIGNED,SIGNED,SIGNED,SIGNED,SIGNED),BOOL,PASCAL,NAME('Rectangle'),PROC
  GetCursorPos(LONG),PASCAL,NAME('GetCursorPos')
  GetPixel(UNSIGNED,UNSIGNED,UNSIGNED),ULONG,PASCAL,NAME('GetPixel')
  GetWindowRect(UNSIGNED,LONG),PASCAL,PROC,NAME('GetWindowRect')
  GetDeviceCaps(UNSIGNED,SIGNED),SIGNED,PASCAL,NAME('GetDeviceCaps')
END

The MODULE('') section encompasses the Windows API prototypes that provide the color pool its functionality. Notice that some of these prototypes use the PROC argument so they can be called as procedures as well as functions.

GetDC: Returns the handle of a device context(DC)

ReleaseDC: Releases the device context returned by GetDC

A device context is a Windows data structure that contains information about the attributes of a device such as a display or a printer.

To retrieve information about a window or to draw to a window requires its Device Context. To get the DC of a window you pass GetDC the handle to that window. For use in this project a NULL is passed to GetDC to return a DC for the display screen. The handle returned by GetDC can be used with any API call that requires a DC such as those used in this project.

Once completed with a device context remember to release it using ReleaseDC. Failure to do so will result in memory loss and eventual application failure. Kind of like a pool with a leak... eventually it runs out of water!

The following API functions are used to draw to the window.

SelectObject: Selects an object, such as a bitmap or brush, into a device context(DC)

GetStockObject: Retrieves a handle to a predefined stock pen, brush or font

DeleteObject: Deletes a created object when no longer needed

CreateSolidBrush: Creates a brush object of the specified color which can then be selected

Rectangle: Draws a rectangle with the current pen filled with the current brush

To "do something" within a DC, the objects required must be selected into that DC before they can be used. Outlines use pens. Fills use brushes. Text uses fonts. You get the idea.

SelectObject provides a means to select the brushes, pens, etc. needed to perform other API functions within a device context. The objects selected determine the final effect produced by the functions that use them.

All device contexts have common stock objects such as black & white pens, several different brushes like black, white, gray, dark gray and several different fonts, among others. GetStockObject is used to select from these common objects.

If a non-stock object is needed, such as a specific fill color (a brush) or specific outline color (a pen), there are other API calls that can be used to create these objects. This project uses a specific brush to fill the sample BOX() by using CreateSolidBrush.

Whenever an object is created, remember that it must be deleted using DeleteObject to recover the memory used. Also remember never to delete an object until it has been selected back out of the DC in which it was used.

When SelectObject is used to select an object into a device context, it returns the handle of the current object. The original object's handle should be retained so that object can be selected back into the DC before it is released. If the DC is not reset to its original state; i.e., all original objects selected back into the DC, odd behavior (and memory loss from any created objects) will result.

Some simple steps to remember when using a DC:

  1. Get a device context (DC)
  2. Create any specific objects required
  3. Select objects to be used (retain handles to original objects within DC)
  4. Perform action desired, such as drawing a rectangle (pen is outline, brush is fill)
  5. Select original objects BACK into DC (this selects your objects out of the DC)
  6. Delete any created objects
  7. Release the DC

A metaphor for using a DC.

Painting a wall...

GetDC - Rope off the area and determine surface type: stucco, plaster, wood, etc

-create objects- - Rent scaffolding

SelectObject - (IN) Gather brushes and paints and set up scaffolding

Perform the task, ie: paint the damn thing!

SelectObject - (OUT) Clean brushes, close paint cans and disassemble scaffolding

DeleteObject - Return scaffolding

ReleaseDC - Take the rope down, step back and admire your work

In addition to the DC and drawing functions the following are used to retrieve information about the window and cursor.

GetCursorPos: Returns the current cursor position in a POINT structure

GetPixel: Returns the composite color attributes for a given pixel by DC and x, y

GetWindowRect: Fills a RECT structure with screen coordinates for a window or control

GetDeviceCaps: Retrieves information about a specific device and its capabilities

GetCursorPos fills a POINT structure with the current position of the cursor in pixels from the top left corner of the display. Don't confuse this with the results of MouseX() and MouseY(). More often than not these will be VERY different measurements.

A POINT structure holds x and y coordinates...

POINT    GROUP
x          SIGNED ! X in pixels
y          SIGNED ! Y in pixels
         END

GetPixel is a simple API call that returns the composite color of the indicated pixel. This is the most important API call used within this project. The LONG that is returned by this call is a composite of the three primary colors; red, green and blue, of the selected pixel. Each component may have a value of 0 to 255 determining the saturation of that color. If all three components are 0 the color is black. If all three are 255 the color is white. This results in a maximum of 16,777,216 (256*256*256) unique colors.

GetWindowRect fills a RECT structure with the bounding coordinates of the window specified by the passed handle. The shape of the "window" referenced by the handle passed to GetWindowRect is of no importance. In other words, if the handle to a Clarion Ellipse() is passed the coordinates of a rectangle which fits the outside of the ellipse will be returned. (some restrictions apply, such as the width of the outline used in the control referenced)

A RECT structure is used with API calls to define the bounding (outside) coordinates of a rectangle...

RECT      GROUP
LeftX       SIGNED ! Upper Left X in pixels
TopY        SIGNED ! Upper Left Y in pixels
RightX      SIGNED ! Lower Right X in pixels
BottomY     SIGNED ! Lower Right Y in pixels
          END

GetDeviceCaps has far too many uses to list here. Suffice it to say that GetDeviceCaps can be of much use within any Windows application depending on the needs. In this project it is used to determine the color depth of the display, ie: the number of colors supported. This is further used in the selection of an appropriate bitmap for the color pool.

The example...

This example is a simple window with a toolbar and a handful of controls. The region behind PoolImage is used to determine mouse movement and, when accepted, to fill the larger BOX() control with the selected color.

The Windows API GetDeviceCaps is used in this procedure to determine the color depth of the display.

When mouse movement is detected within the PoolRegion the cursor position is read and passed to Get_Color. Get_Color reads the current pixel properties and returns the composite color value that is then compared to the previous color. If the colors are different then a call is made to Color_Rect. This procedure creates a solid brush in the selected color and then draws a rectangle filled with this color.

When this region is ACCEPTED, the last color selected by mouse movement is used to fill the larger BOX() control.

Couldn't get much simpler, could it? Now don't make up your mind until you read the source that follows. You should agree that there isn't that much involved.

ColorPool     PROCEDURE
NULL          EQUATE(0) !An equate for NULL parameters
PLANES        EQUATE(14) !An equate for GetDeviceCaps API
BITSPIXEL     EQUATE(12) !An equate for GetDeviceCaps API
hDC           UNSIGNED !Handle to a Device Context
sbHnd         UNSIGNED !Handle to the sample BOX()
MaxColors     ULONG !Maximum color depth of display
NewColor      ULONG !New color selected in pool
LastColor     ULONG !Previous color selected
ColorRef      GROUP,PRE(),OVER(LastColor) !Group to dissect color components
vRed            BYTE ! RED color component
vGreen          BYTE ! GREEN color component
vBlue           BYTE ! BLUE color component
              END
POINT         GROUP !POINT structure group (current)
x               SIGNED
y               SIGNED
              END
LastPOINT     GROUP !POINT structure group (previous)
x               SIGNED
y               SIGNED
              END

Window  WINDOW('Color Pool'),AT(,,244,120)|
                ,FONT('MS Sans Serif',8,,FONT:regular)|
                ,CENTER,SYSTEM,GRAY,DOUBLE
          TOOLBAR,AT(0,0,244,34)
            BUTTON('Close'),AT(2,2,41,30),USE(?CloseButton)
            STRING('Red'),AT(112,2),USE(?rString)
            STRING('Green'),AT(112,12),USE(?gString)
            STRING('Blue'),AT(112,22),USE(?bString)
            STRING(@n3),AT(142,2),USE(vRed),TRN
            STRING(@n3),AT(142,12),USE(vGreen),TRN
            STRING(@n3),AT(142,22),USE(vBlue),TRN
            REGION,AT(162,2,65,29),USE(?PoolRegion),IMM,CURSOR('POOL.CUR')
            IMAGE('POOL16.BMP'),AT(161,1,67,31),USE(?PoolImage)
            PANEL,AT(161,1,67,31),USE(?PoolBevel),BEVEL(-1)
            BOX,AT(230,1,13,31),USE(?SampleBox),COLOR(00H),FILL(08000H)
          END
          BOX,AT(43,15,157,57),USE(?Shadow),FILL(00H)
          BOX,AT(41,13,157,57),USE(?BigSample),FILL(0FFFFFFH)
          STRING(@N_8),AT(104,74),USE(NewColor),CENTER
        END
  CODE
  LastColor = COLOR:White !Initialize color for sample BOX()
  NewColor = 0 !Initialize value for comparison
  OPEN(Window)
  !
  ! Calculate maximum colors supported and select the appropriate pool bitmap
  !
  hDC = GetDC(NULL) !Get device context for screen
  MaxColors = 2^(GetDeviceCaps(hDC,PLANES) * GetDeviceCaps(hDC,BITSPIXEL))
  IF MaxColors > 256 !More than 256 colors supported
    ?PoolImage{PROP:Text} = '~Pool16.bmp' ! then use the 16-bit bitmap
  ELSIF MaxColors > 16 !256 colors supported
    0{PROP:Palette} = 165 ! set palette to image colors
    ?PoolImage{PROP:Text} = '~Pool8.bmp' ! and use the 8-bit bitmap
  ELSE !16 colors supported
    ?PoolImage{PROP:Text} = '~Pool4.bmp' ! then use the 4-bit bitmap
  END
  ReleaseDC(NULL,hDC) !Release the DC selected by GetDC
  sbHnd = ?SampleBox{PROP:Handle} !Get a handle to the sample BOX()
  ACCEPT
    CASE EVENT()
    OF EVENT:OpenWindow
      Color_Rect(sbHnd,LastColor) !Initialize sample color rectangle
    OF EVENT:Accepted
      CASE FIELD()
      OF ?CloseButton
        POST(EVENT:CloseWindow) !Close window and exit ACCEPT LOOP
      OF ?PoolRegion
        ?BigSample{PROP:Fill} = NewColor !Display newly selected color
      END
    OF EVENT:MouseMove !Mouse movement within pool REGION()
      IF FIELD() = ?PoolRegion
        GetCursorPos(ADDRESS(POINT)) !Get cursor position
        IF LastPOINT <> POINT !If cursor position has changed
          LastPOINT = POINT ! remember the new position
          NewColor = Get_Color(POINT.x,POINT.y) ! and read the pixel color...
          IF NewColor <> LastColor AND NewColor > -1 !If the color is different
            LastColor = NewColor ! remember the new color
            Color_Rect(sbHnd,LastColor) ! and update the sample BOX()
          END
        END
      END
    END
  END
  CLOSE(Window)
  RETURN

The two procedures used for this project:

Get_Color is a CW function used to retrieve the color components of an individual screen pixel. This is the heart of the color pool and provides easy color selection using bitmaps as opposed to seriously verbose math and logic... something most of us prefer to stay away from!

This function is passed the X and Y coordinates of the cursor. These values are used to perform the Windows GetPixel API and return the composite color in a LONG.

Using the customary "eye-dropper" cursor this function could be used to return the color for any use desired simply by reading the cursor position.

Get_Color   FUNCTION(SIGNED AtX, SIGNED AtY)
NULL        EQUATE(0) !An equate for NULL parameters
hDC         UNSIGNED !Handle to a Device Context
NewColor    LONG !Function return variable
CODE
hDC = GetDC(NULL) !Get device context for screen
NewColor = GetPixel(hDC,AtX,AtY) !Read the color of the pixel
ReleaseDC(NULL,hDC) !Release the DC selected by GetDC
RETURN(NewColor) !Return the composite color

Color_Rect is a CW procedure that uses the Windows API to fill a rectangular area with a selected color. The Windows RECTANGLE function is used to reduce the flashing which occurs when using PROP:Fill in a BOX() control. Using these API calls provides a flash free alternative for rectangles that are not overlapped.

This procedure is passed the handle of the sample BOX() and the composite color to be used as fill. By default this procedure uses a black pen for the box outline.

Color_Rect   PROCEDURE(UNSIGNED SmplHndl, ULONG ThisColor)
NULLEQUATE(0) !An equate for NULL parameters
BLACK_PENEQUATE(7) !An equate for a black pen
hDCUNSIGNED !Handle to a Device Context
hPen         UNSIGNED !Handle to a pen object
hBr          UNSIGNED !Handle to a brush object
hPenOLD      UNSIGNED !Handle to original DC pen
hBrOLD       UNSIGNED !Handle to original DC brush
RECT         GROUP !RECT structure group
LeftX          SIGNED
TopY           SIGNED
RightX         SIGNED
BottomY        SIGNED
END
CODE
hDC = GetDC(NULL) !Get device context for screen
hPen = GetStockObject(BLACK_PEN) !Select the stock black pen object
hBr = CreateSolidBrush(ThisColor) !Create a solid color brush
hPenOLD = SelectObject(hDC,hPen) !Select the black pen into the DC
hBrOLD = SelectObject(hDC,hBr) !Select the solid brush into the DC
GetWindowRect(SmplHndl,ADDRESS(RECT)) !Load the RECT of the sample BOX()
Rectangle(hDC,RECT.LeftX,RECT.TopY,RECT.RightX,RECT.BottomY)
  !DRAW A NEW RECTANGLE
SelectObject(hDC,hBrOLD) !Select the old brush into the DC
SelectObject(hDC,hPenOLD) !Select the old pen into the DC
DeleteObject(hBr) !Delete the brush object
ReleaseDC(NULL,hDC) !Release the DC selected by GetDC
RETURN

Since a black pen and a solid brush have been selected into the device context the call to RECTANGLE will draw a box to the dimensions provided in the RECT structure with a black outline of 1 pixel, filled with the color passed in "ThisColor".

hPenOLD and hBrOLD are used to retain the original pen and brush within the device context. Any time a new pen, brush, font, bitmap or other object is selected into a device context the original object should be selected back into the device context before releasing the DC. Failure to do so can have some rather odd results, to say the least.

Any objects created should be deleted once they have been selected out of the device context to release the memory they used.

Once all objects have been selected out, and all created objects have been deleted, the device context must be released.

Summary

That's it. Two rather simple procedures that provide some special functionality and can add a touch of class to your applications if you have the need for color selection.

With some effort you could easily produce a LIB, LIB/DLL or CLASS and template to include this tool as a group of controls in future applications. The beauty of CW, reusable code!

The example source includes the CLW and PRJ files as well as these images for your use.

Image2.gif

Printer-friendly version

Reader Comments

To add a comment to this article you must log in.