Understanding GradientFill |
|
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 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 shall be first
The top left box of the interface lets you select the dwMode parameter, one of
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 next parameter to GradientFill is the count of the number of TRIVERTEX elements, and this is computed automatically by the GradientFill Explorer.
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 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);
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.
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 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
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 | |||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||
|
|
Which is exactly the parameters from the Gradient Filler Explorer
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. |
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. |
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. |
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();
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 40°F, 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.