Understanding GradientFill

Home
Back To Tips Page
How it works inside!

The GradientFill function is one of the more obscure GDI functions, yet one which has much to recommend it for aesthetic purposes. But it is hard to use, and its documentation is profoundly obscure. The inadequate documentation discourages the use of this rather cool API.

I wrote the GradientFill Explorer because I got tired of fiddling with parameters and recompiling, or fiddling in the debugger, trying to get the effect I wanted.

The GradientFill Explorer allows you to play with the parameters to GradientFill and it generates code you can paste directly into your application. You will typically need to edit the x,y parameters to conform to your application (usually replacing the static integers with dynamically-computed values suitable for your application), and you may wish to change the rather simple variable names to names that are more suited for your environment.

 

The Gradient Filler interface

The specification of GradientFill is

BOOL GradientFill(HDC hdc, PTRIVERTEX pVertex, ULONG dwNumVertex, PVOID pMesh, ULONG dwNumMesh, ULONG dwMode)
BOOL CDC::GradientFill(TRIVERTEX * pVertex, ULONG nVertices, void * pMesh, ULONG nMeshelements, DWORD dwMode

(It is worth observing here that the people who wrote these specs appear to be functionally illiterate in several ways: the parameters are gratuitously changed in type and the use of Hungarian Notation is inconsistent within each call and between calls. This is an example of how HN demonstrates that it should not be used, because people who use it are most often like the authors of these two specifications: demonstrably incompetent to use it! I spot at least seven inconsistencies in two lines of code!)

The last parameter: The gradient fill mode

The last shall be first

The top left box of the interface lets you select the dwMode parameter, one of

Parameter: The TRIVERTEX array

The second argument to ::GradientFill or the first argument to CDC::GradientFill is a pointer to an array of TRIVERTEX values. Each color is represented by a pair of TRIVERTEX values, where the first element of the pair has an x,y coordinate that represents the top left corner of the region, and the second element of the pair has an x,y coordinate that represents the bottom right corner of the region. The first TRIVERTEX element represents the first color to be used, and the second TRIVERTEX element represents the second color to be used. The colors will flow smoothly from the left edge of the region to the right edge (for GRADIENT_FILL_RECT_H) or from the top edge of the region to the bottom edge (for GRADIENT_FILL_RECT_V). A simple gradient fill of black to white is illustrated here.

The list box in the TRIVERTEX section holds the elements. The edit controls and spin controls can be used to set the values. The x and y spin controls will not exceed whatever the limits of the sample control are, which will vary depending on your screen resolution. The expectation is that you will replace these values by something appropriate for your app.

You can select a color by using the R, G, and B edit controls, or the button ... to bring up a standard color selection dialog.

Note that the colors appear to use a "decimal" notation. They don't actually have a decimal representation, but the TRIVERTEXT fields are

typedef struct _TRIVERTEX {
LONG        x;
Long        y;
COLOR16     Red;
COLOR16     Green;
COLOR16     Blue;
COLOR16     Alpha;
} TRIVERTEX, *PTRIVERTEX;
A COLOR16 color is a color whose high-order 8 bits are the familiar RGB values in the range of 0..255, but which allows a finer gradation by allowing up to 255 divisions between each normal RGB value. I express this "sub-value" as if it were a decimal, but the actual values would be n.0 to n.255, where n represents the values in the range 0..255. The GradientFill Explorer does not support setting these "fine" values of color. You are free to specify these by hand-editing the output, for example, MAKEWORD(128, 128) is a color halfway between RGB 128 and RGB 129. It is not clear if GDI actually supports these fine values, or even if any devices exist which can utilize them, but they may be there to support future generality.

According to the GradientFill documentation, the Alpha channel A is not actually used.

Having selected the parameters x, y, R, G, B and (the usused) A, the Add button will add this selection to the ListBox. The selection is always added to the end. If you do not want it there, the Up and Down buttons will move the selected item up or down, and the Remove button will remove it.

If you select an item, and click the Edit check box, the parameters for the item are set to the controls. Changing the value in the controls will immediately change the value in the ListBox, and if you are seeing the gradient displayed, you will see it change immediately in response to these edits. As shown in the screen shot below, the editing of the color from white to magenta is displayed, both in terms of changing the contents of the ListBox and in changing the actual image displayed

The point finder

Whenever an item is selected in the TRIVERTEX list, a little "finder" image pops up. Note that this image can be clipped by the edges of the window, so it was designed to maximize its visibility. This screen snapshot was taken during the brief time it was up. The combination of the circle and the diagonal lines makes it fairly easy to spot. The image of the finder is combined with the image (using a technique that complements the bits) so that it is visible on both light and dark colors.

Parameter: Number of vertices

The next parameter to GradientFill is the count of the number of TRIVERTEX elements, and this is computed automatically by the GradientFill Explorer.

Parameter: Mesh elements

The "mesh elements" explain to the API how to interpret the values.

For a GRADIENT_FILL_RECT_*, each mesh element has the indices of two elements of the TRIVERTEX array. One element represents the top left corner of the starting color (in this case, element 0) and one element represents the bottom right corner of the ending color (in this case, element 1). So a GRADIENT_RECT value represents a pair of TRIVERTEX elements.

To add a new value, select the UpperLeft and LowerRight values, which represent the indexes into the TRIVERTEX array, and click the Add button. The element will be added to the end of the ListBox. To change its position, select it and use the Up and Down buttons to shift its position.

To remove an element, select the element and click the Remove button.

To change the contents of an element, select the element, and click the Edit check box. The current values of the element will be copied to the controls, and changes in the controls will immediately change the value of the selected element. If a sample is being displayed, the sample will immediately change to correspond to the new settings.

Each element has a check box beside it. To see the effects of removing that element from the list, simply uncheck it. Note that the check boxes reference only the visual display in the GradientFill Explorer. If the data is saved to a .vtx file, or the code sample is used, the elements will be present. If it is desirable to actually remove them, then the Remove button must be used. Note there is no "undo" capability.

The sample dropdown

There are a number of "pre-canned" samples that are available. These samples are all stored as .vtx-format resources in the resource segment of the executable, and this set is not extensible. However, you can use the arrow button to transfer an experiment to the list of samples. This information is not saved across program runs. Whatever samples you add will disappear when you close the program. You have to use the File > Save command to save your data to vertex (.vtx) files. However, if you load a vertex file, you can transfer it to the dropdown by using the arrow button, and quickly retrieve it. Thus, the dropdown is a handy "scratchpad" for experiments.

The code window

The code window shows the sample code you would write to create the gradient you draw. Using the Edit > Copy all code option this code can be placed in the Clipboard, and you can paste it into your program. Alternatively, you can take the binary vertex file, add it to your resource segement, and copy the VertexData source files into your project, and use the binary representation. The techniques and code for this are shown later in this article.

Normally the values in the code window are displayed in decimal. Unfortunately this makes the values hard to read, because the COLOR16 values end up being the familiar values multiplied by 256. The use of the pseudo-decimal notation in the comment shows this; color values range from 0.000 to 0.255.

TRIVERTEX tv[2] = {
{   0,     0,        0,        0,        0,    65280}, // [ 0] RGBA(  0.000,     0.000,     0.000,   255.000)
{ 120,    96,    65280,    65280,    65280,    65280}, // [ 1] RGBA(255.000,   255.000,   255.000,   255.000)
};

GRADIENT_RECT gr[1] = {
{0, 1},
};
BOOL result = dc.GradientFill(tv, 2, gr, 1, GRADIENT_FILL_RECT_H);

By using the Show colors in hex check box, the color values will be displayed in hexadecimal.

TRIVERTEX tv[2] = {
{   0,     0,   0x0000,   0x0000,   0x0000,   0xff00}, // [ 0] RGBA(  0.000,     0.000,     0.000,   255.000)
{ 120,    96,   0xff00,   0xff00,   0xff00,   0xff00}, // [ 1] RGBA(255.000,   255.000,   255.000,   255.000)
};

GRADIENT_RECT gr[1] = {
{0, 1},
};
BOOL result = dc.GradientFill(tv, 2, gr, 1, GRADIENT_FILL_RECT_H);

When things go bad

If you supply invalid parameters to GradientFill, and the operation returns FALSE, instead of seeing a blank image, the entire image background changes color and the error message for GetLastError is displayed:

Note that it is possible to provide a set of parameters to GradientFill that will produce no output, but will also produce no error, because the GradientFill has worked correctly but your parameters do not result in any actual display.

The Menus

File New Clears out all the vertices and resets to allow you to create a new gradient
Open... Opens a previously-saved Vertex (.vtx) file
Save... Saves the current gradient information in a Vertex (.vtx) file. If no previous File > Save has been done, acts as File > Save As...
Save As... Opens a file save dialog and lets you save the current workspace as a Vertex (.vtx) file.
Exit Exits the program
Edit Copy Code Selection Copies the highlighted selection in the code box to the clipboard.
Copy All Code Copies all the code in the code box to the clipboard. Any selection is ignored.
Copy Bitmap The contents of the sample bitmap are copied to the clipboard in CF_BITMAP format.
Transpose Applies a transformation to the image to create a vertical fill from a horizontal fill. Does not work for triangle fills. This changes the coordinates, but you must explicitly select the correct graident fill technique to match them
Help About... The usual About box (but with a gradient filled image!)

Triangular Gradients

Triangular gradients are the Really Fun Part of GradientFill, but they are largely a deep mystery. My hope is that the GradientFill Explorer will help demystify some of the features of triangular gradients.

When you select GRADIENT_FILL_TRIANGLE, a slightly different interface appears. In this interface, the GRADIENT_RECT controls are replaced by the GRADIENT_TRIANGLE controls, as shown here.

Triangular gradients allow you to use two, three-color fills with a triangular fill pattern, and with multiple triangles you can easily get very fancy, such as the four-color example shown later, which is just two three-color triangular fills. The example from the GradientFill documentation is shown here. First, the TRIVERTEX values are set up:

Then the mesh parameters are selected:

This produces the 3-color filled area

A Triangular Gradient Tutorial

Consider the humble center-gradient image:

How do we create this with a single operation?

To give some concrete numbers here, we are going to make a 96  96 image. We need to identify the key points in the image, and there turn out to be 9. A key point is represented by its coordinates <x, y, color> so schematically we get the following

0,0      48,0      96,0
              
0,48      48,48      96,48
              
0,96      48,96      96,96

Next, I will assign vertex numbers to each of these points. How you assign them is up to you, but they should be a dense set of integers starting at 0. I chose to assign them left-to-right and top-to-bottom, but I could equally have assigned the integers 0..8 randomly to the points. But that would make it harder to understand and explain.

0      1      2
              
3      4      5
              
6      7      8

These numbers will tell me where I want to place the point in the TRIVERTEX array I'm about to create.

Index x y color
0 0 0  
1 48 0  
2 96 0  
3 0 48  
4 48 48  
5 96 48  
6 0 96  
7 48 96  
8 96 96  

Next, I identify where the triangles are formed. For example I can see that there is one triangle as shown here:

0      1      2
              
3      4      5
              
6      7      8

From this information I can deduce that one of the entries in the GRADIENT_TRIANGLE array should be 0, 1 4, which produces the triangle

Image GRADIENT_TRIANGLE Cumulative    Image GRADIENT_TRIANGLE Cumulative
0 1 2
3 4 5
6 7 8
0, 1, 4
  
0 1 2
3 4 5
6 7 8
4, 7, 8
0 1 2
3 4 5
6 7 8
1, 2, 4
  
0 1 2
3 4 5
6 7 8
4, 7, 6
0 1 2
3 4 5
6 7 8
2, 5, 4
  
0 1 2
3 4 5
6 7 8
3, 4, 6
0 1 2
3 4 5
6 7 8
4, 5, 8
  
0 1 2
3 4 5
6 7 8
0, 4, 3

Which is exactly the parameters from the Gradient Filler Explorer

.vtx files

Vertex data is saved in a binary format called a .vtx file. The format of this file is described below. A vertex file can be read in and displayed in any program using methods of the VertexData class. In addition, a vertex file can be saved as a resource and loaded using methods of the VertexData class. Typically, you would save the resource as a "prototype" and then modify its x,y coordinates to match your actual application.
offset value type notes
000000 0x00000002 DWORD This is the current version format supported
000004 0x00008003 DWORD
VertexData::GMODE
The next element will be the graphics mode
000008 mode DWORD The graphics mode. One of GRADIENT_FILL_RECT_H, GRADIENT_FILL_RECT_V, or GRADIENT_FILL_TRIANGLE
000012 0x00008000 DWORD
VertexData::GVERT
The data which follows will be a set of TRIVERTEX values
000016 tvcount DWORD Tne number of TRIVERTEX elements which follow
000020 TRIVERTEX0 TRIVERTEX The first TRIVERTEX element
... ... TRIVERTEX The remaining TRIVERTEX elements
... 0x00008001 DWORD
VertexData::GRECT
Indicates a set of GRADIENT_RECT structures will follow. This is used only if the mode is GRADIENT_FILL_RECT_H or GRADIENT_FILL_RECT_V. This record type will not be present if the the mode is GRADIENT_FILL_TRIANGLE
... grcount DWORD Tne number of GRADIENT_RECT elements which follow
... GRADIENT_RECT0 GRADIENT_RECT The first GRADIENT_RECT structure
... ... GRADIENT_RECT The remaining GRADIENT_RECT structures
... 0x00008002 DWORD
VertexData::GTRI
Indicates a set of GRADIENT_TRIANGLE structures will follow. This is used only if the mode is GRADIENT_FILL_TRIANGLE. This record type will not be present if the the mode is GRADIENT_FILL_RECT_H or GRADIENT_FILL_RECT_V
... gtcount DWORD Tne number of GRADIENT_TRIANGLE elements which follow
... GRADIENT_TRIANGLE0 GRADIENT_TRIANGLE The first GRADIENT_TRIANGLE structure
... ... GRADIENT_TRIANGLE The remaining GRADIENT_TRIANGLE structures
... 0x00008004 DWORD
VectorData::GEOF
Indicates the end of the data.

class VertexData

The VertexData class handles transformations to and from the packed binary representation of GradientFill data. It knows how to format the data for output and it knows how to read and parse the data from a file or resource. It is the primary interface to .vtx files, both as files and as resource data.

VertexData::VertexData()
Constructor of a VertexData object.
BOOL VertexData::LoadVertexResource(HMODULE module, LPCTSTR id, LPCTSTR type)

Parameters

module is a module handle to the instance that contains the resource
id is the string ID or MAKEINTRESOURCE integer ID of the resource
type is the string name or MAKEINTRESOURCE integer name of the type of the resource

Returns

TRUE if successful, FALSE if error, use GetLastError to discover details of the error

Description

Loads the resource, which is assumed to be the contents of the .vtx file.

Note that calling VertexData::LoadVertexResource is illegal if VertexData::FromBinary has been called and there has been no intervening call on VertexData::Release.

BOOL VertexData::LoadFileData(CFile & file)

Parameters

file is a reference to a CFile object which has opened a .vtx file.

Returns

TRUE if successful, FALSE if error, use GetLastError to discover details of the error

Description

Reads the contents of the .vtx file.

Note that calling VertexData::LoadFileData is illegal if VertexData::FromBinary has been called and there has been no intervening call on VertexData::Release.

BOOL VertexData::WriteFile(CFile & file)

Parameters

file is a reference to a CFile object which has opened a .vtx file for output.

Returns

TRUE if successful, FALSE if error, use GetLastError to discover details of the error

Description

Writes the VertexData of the .vtx file
BOOL VertexData::FromBinary(PTRIVERTEX & vertex, ULONG & vertexCount, PVOID & mesh, ULONG & meshCount, ULONG & mode)

Parameters

vertex is a reference to a pointer to an array of TRIVERTEX objects which represent the vertices
vertexCount is a reference to a variable which will receive the count of the number of vertices in the array
mesh is a reference to a pointer to an array of GRADIENT_RECT or GRADIENT_TRIANGLE objects which represent the vertex order
meshCount is a reference to a variable which will receive the count of the number of GRADIENT_RECT or GRADIENT_TRIANGLE objects in the array
mode is a reference to a variable which will receive the mode for the fill, GRADIENT_FILL_RECT_H, GRADIENT_FILL_RECT_V or GRADIENT_FILL_TRIANGLE

Returns

TRUE if successful, FALSE if error, use GetLastError to discover details of the error

Description

Makes the internal binary format of data (the .vtx representation) available to an application in the expected format for GradientFill. The vertex parameter will be set to the TRIVERTEX array in memory, and the vertexCount parameter will be set to indicate how many elements are in the array. The mesh parameter will be set to point to the array of GRADIENT_RECT or GRADIENT_TRIANGLE elements in the array, and the meshCount value will be set to indicate the number of elements in the array. The mode parameter will be set to indicate the type of fill, GRADIENT_FILL_RECT_H, GRADIENT_FILL_RECT_V or GRADIENT_FILL_TRIANGLE

However, these pointers are pointing to structures which are internal to the VertexData structure, and if it is changed, these pointers are rendered meaningless. Therefore, this function sets a "data in use" flag, and any subsequent operation that would cause a reallocation of the internal data will fail, with GetLastError returning the error code XACT_E_WRONGSTATE. To avoid this, the programmer must explicitly call VertexData::Release to indicate that the pointers returned from VertexData::FromBinary are not going to be used in the future.

BOOL VertexData::ToBinary(PTRIVERTEX vertex, ULONG vertexCount, PVOID mesh, ULONG meshCount, ULONG mode)

Parameters

vertex is a pointer to an array of TRIVERTEX objects which represent the vertices
vertexCount is the count of the number of vertices in the array
mesh is a pointer to an array of GRADIENT_RECT or GRADIENT_TRIANGLE objects which represent the vertex order
meshCount is the count of the number of GRADIENT_RECT or GRADIENT_TRIANGLE objects in the array
mode is the mode for the fill, GRADIENT_FILL_RECT_H, GRADIENT_FILL_RECT_V or GRADIENT_FILL_TRIANGLE

Returns

TRUE if successful, FALSE if error, use GetLastError to discover details of the error

Description

Transfers the vertex data from the external representation to the internal .vtx format, suitable for writing with VertexData::WriteFile

Note that calling VertexData::ToBinary is illegal if VertexData::FromBinary has been called and there has been no intervening call on VertexData::Release.

void VertexData::Release()

Description

Marks the VertexData object as being "not currently in use", allowing operations that can change its contents to take place. The "currently in use" flag is set if a VertexData::FromBinary operation has been performed, since this returns pointers to internal data. The data may not be modified until a VertexData::Release call is made.

This call clears the flag; there is no "reference count" of outstanding pointers maintained. If the VertexData object is newly-created, or a VertexData::FromBinary operation has not been performed, a Release operation has no effect.

CRect VertexData::GetBoundingBox(PTRIVERTEX vertex, ULONG vertexCount, PVOID mesh, ULONG meshCount, ULONG mode)

Parameters

vertex is a pointer to an array of TRIVERTEX objects which represent the vertices
vertexCount is the count of the number of vertices in the array
mesh is a pointer to an array of GRADIENT_RECT or GRADIENT_TRIANGLE objects which represent the vertex order
meshCount is the count of the number of GRADIENT_RECT or GRADIENT_TRIANGLE objects in the array
mode is the mode for the fill, GRADIENT_FILL_RECT_H, GRADIENT_FILL_RECT_V or GRADIENT_FILL_TRIANGLE
CRect VertexData::GetRawVertexData(CByteArray & data)

Parameters

data is a reference to a CByteArray that will be filled with the contents of the internal binary representation (.vtx-file format data).

Remarks

The intent is that the programmer will do something to this data as a single entity (e.g., send it across a network connection to a recipient explecting data in this format). The data should not be modified except with great care.
CRect VertexData::SetRawVertexData(CByteArray & data)

Parameters

data is a reference to a CByteArray that will replace the contents of the internal binary representation (.vtx-file format data).

Remarks

The intent is that the programmer will have obtained this from some source (such as a .vtx file). The data should not be modified except with great care. However, if the data is not in a valid format, the operation VertexData::FromBinary will most likely give an error return.

Summary of error codes for VertexData

These are the specific errors issued by the VertexData class. However, operations such as using CFile, LoadResource, and so on which are support APIs used inside the class can have their own errors, which would be added to this list.
MSSIPOTF_E_BADVERSION The version number of the vertex data is not supported
ERROR_INVALID_DATA The data values found are not consistent with valid syntax of a .vtx file
ERROR_INVALID_PARAMETER A parameter, or combination of parameters, are invalid.
ERROR_HANDLE_EOF The GEOF value was not found where it was expected to be, and an attempt was made to read beyond the end of the .vtx data
XACT_E_WRONGSTATE You are about to do an operation that will reallocate or delete the contents of the VertexData object. However, this data might be in use and this reallocation would be fatal. Before performing an operation that can delete or reallocate the data, be certain there are no other uses of pointers to the data (as would be obtained from VertexData::FromBinary and call VertexData::Release to avoid this error.

Using .vtx files as resources

I wanted to have some "pre-canned" examples in the GradientFill Explorer. How to get them there? Well, I'd already added the ability to save work (so it could be edited later) in the .vtx file format. All I had to do was moving the parsing into a reusable class, and there it was! To use a GradientFill resource, you will need to import it into Visual Studio. To do this,
  1. Create a .vtx file containing your gradient information
  2. Select your .rc file, and right-click on it, selecting Add Resource...
  3. When the dialog comes up, select the Import... option
  4. When the file open dialog comes up, go to the right directory, and select *.vtx
  5. After you open the file, you will be presented with an option to identify the type of the resource. Note that since I have been adding resources to this project already, my name (you can choose any name you like), "VERTEX", is already known as a possibility. Either type the name of your resource into the edit control, or click on one of the listed types.
  6. The resource will be added as shown. Note that if you double-click on the resource it will show the resource in binary, and it can be edited (not recommended unless you've really studied the file format), but if you want to you can study the bytes and decode them in the file format specified. Note that I took the default symbol assigned, IDB_VERTEX7. You can change the symbol or its value using the Properties editor.
  7. To use this, you could write

    VertexData vd;
    BOOL ok = vd.LoadVertexResource(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_VERTEX7), _T("VERTEX"));
    PTRIVERTEX tv;
    ULONG vertexCount;
    PVOID mesh;
    ULONG meshCount;
    ULONG mode;
    ok = vd.FromBinary(tv, vertexCount, mesh, meshCount, mode);
    dc.GradientFill(tv, vertexCount, mesh, meshCount, mode);
    vd.Release();

The decode of the .vtx file

download.gif (1234 bytes)

Notes on the development of the GradientFill Explorer

I was sitting here in Bristol England in January of 2008. I'm sure Bristol is a lovely city in the summer, but right now it is 40F, pouring down rain, and with winds gusting to 20mph. Not a day to be outdoors in an electric cart. Yesterday was the same, and the next two days promise to be the same (afternote: they were). So what to do? I started browsing my laptop, and found this project which I started in August of 2005 and apparently forgot about. I'd used it just to create some rectangular fills I needed for one project, and I'd not bothered to add the triangular fill code. There was nothing to do but finish it. I alternated this with reading Simon Singh's book Fermat's Last Theorem, a fascinating history of the conquest of the most famous problem in mathematical history.

As the rain hits the window and the wind whistles around the corner of the hotel, I'm creating the images and tables of this Web page. I do not have FrontPage; I'm using Epsilon and editing raw HTML text (the header says it was created by FrontPage, but that's because I started with one of my existing pages which I downloaded and stripped). I'll have to convert the .bmp files to .gif or .jpg before I put this site up, because my laptop doesn't have any decent image processing software. But everything else is written in raw HTML. Those fancy tables-in-tables were all hand-done. I'm dangerous when I'm bored.

I added the multiple-point highlighting and scrollbar support at some 35,000 feet over the Atlantic Ocean, about an hour from Bristol.

 

 

[Dividing Line Image]

Send mail to newcomer@flounder.com with questions or comments about this web site.
Copyright 2008, The Joseph M. Newcomer Co./FlounderCraft Ltd., All Rights Reserved
Last modified: May 14, 2011