Inside the GradientFill Explorer

Home
Back To Tips Page

Back to the GradientFill Explorer

Some of the interesting internals

This turned out to exercise some of my graphics skills. For example, the dropdown list of "canned" examples is one of my favorite toys, the Owner Draw Combo Box. However, getting it to work right involved some graphics tricks.

Enumerating resources

The itemData of each element is a VertexData value, loaded by enumerating all the VERTEX resources during OnInitDialog.

   EnumResourceNames(AfxGetInstanceHandle(), _T("VERTEX"), VertexEnum, (LONG_PTR)this);

Note the "abitrary user parameter" is a pointer to this, the CGradientFillerDlg. The callback function is a static class member and as such as no direct access to any of the instance members, but this can be remedied by using the LONG_PTR parameter

/* static */ BOOL CALLBACK CGradientFillerDlg::VertexEnum(HMODULE module, LPCTSTR type, LPTSTR name, LONG_PTR lParam)
    {
     VertexData * vd = new VertexData;
     if(vd->LoadVertexResource(module, name, type))
        { /* got it */
         CGradientFillerDlg * me = (CGradientFillerDlg *)lParam;
         me->c_Samples.AddString((LPCTSTR)vd);
        } /* got it */
     else
        { /* failed */
         delete vd;
        } /* failed */
     return TRUE;
    } // CGradientFillerDlg::VertexEnum

Drawing an arbitrary image in a limited space

In the DrawItem handler of the CComboBox-derived class, I wanted to scale the image to fit entirely in the small space of the selection area. I did not want to actually expand the combo box items to be as large as the images, but in effect show a "thumbnail". I was initially dismayed when I saw that the images were not being displayed properly; only the first image appeared, but none of the others did. Their apparent area was just plain white. Then I realized that the reason is that GradientFill would start the images at the nominal 0,0 point, and I wanted to draw them in the desired area. The trick was therefore to simply adjust the viewport origin to the rectangle being displayed.

void CGradientList::DrawItem(LPDRAWITEMSTRUCT dis)                         // [1]
    {
     CDC * dc = CDC::FromHandle(dis->hDC);                                 // [2]
     CRect rect = dis->rcItem;                                             // [3]

     if(dis->itemID != (DWORD_PTR)-1)                                      // [4]
         { /* has vertex */
          int save = dc->SaveDC();                                         // [5]

          VertexData * vd = (VertexData*)dis->itemData;                    // [6]
     
          LPTRIVERTEX vertex;                                              // [7]
          ULONG vertexCount;                                               // [7]
          LPVOID mesh;                                                     // [7]
          ULONG meshCount;                                                 // [7]
          ULONG mode;                                                      // [7]

          BOOL ok = vd->FromBinary(vertex, vertexCount, mesh, meshCount, mode); // [8]

          CRgn rgn;                                                        // [9]
          rgn.CreateRectRgnIndirect(&rect);                                // [10]
          dc->SelectClipRgn(&rgn);                                         // [11]

          //****************************************************************
          // Scale the drawing to fit
          //****************************************************************
          CRect sz = vd->GetBoundingBox(vertex, vertexCount, mesh, meshCount, mode); // [12]

          dc->SetMapMode(MM_ANISOTROPIC);                                  // [13]
          dc->SetWindowExt(sz.Width(), sz.Height());                       // [14]
          dc->SetViewportExt(rect.Width(), rect.Height());                 // [15]
          dc->SetViewportOrg(rect.left, rect.top);                         // [16]

          dc->GradientFill(vertex, vertexCount, mesh, meshCount, mode);    // [17]

          vd->Release();                                                   // [18]

          dc->RestoreDC(save);                                             // [19] 

          if(dis->itemState & ODS_SELECTED)                                // [20]
             { /* selection */
              int save = dc->SaveDC();                                     // [21]
              CPen pen(PS_DOT, 0, RGB(0,0,0));                             // [22]
              dc->SelectObject(&pen);                                      // [23]
              dc->SelectStockObject(HOLLOW_BRUSH);                         // [24]
              dc->Rectangle(&rect);                                        // [25]
              dc->RestoreDC(save);                                         // [26]
             } /* selection */
         } /* has vertex */
    }
  1. I replace the horrible name lpDrawItemStruct with something that makes sense to type frequently.
  2. I want to use MFC graphics, so I use CDC::FromHandle to get a CDC *
  3. I make a copy of the rectangle
  4. If the itemID is -1 there is no actual data in the ComboBox (no selection or combo list is empty)
  5. With an owner-draw control, you must always be sure the state of the DC when you return is the same state you received it in. Rather than save tons of state in obscure variables (and possibly forget to restore one of them) I simply use SaveDC to push a new DC context, which I will modify. Note that I do not save any intermediate state in variables at all!
  6. The itemData is a VertexData structure created by the OnInitDialog enumeration
  7. I need to create a set of variables to use VertexData::FromBinary
  8. Call the VertexData::FromBinary method to retrieve the parameters I need for GradientFill
  9. Due to what is most politely called a design blunder, most Windows controls use the CS_PARENTDC style, so the clipping region of the control is actually the clipping region of the parent. This has never made sense, and in Vista this horrible mistake was finally corrected. But for earlier versions of Windows, we have to make sure we don't overwrite the parent, and the easiest way to do this is to create a clipping region. This also keeps any drawing from "spilling into" adjacent areas of the dropdown list when it is drawn.
  10. The clipping region will be exactly the area provided for drawing in the DRAWITEMSTRUCT
  11. The clipping region is selected into the DC. Note that I have carefully arranged the code so that the clipping region will be deselected before the CRgn::~CRgn destructor is called, so at the time it is called the region is not actually selected into a DC. This is important because otherwise the graphics space will be lost, because the destructor (specifically ::DeleteObject) will not actually delete an obect which is actively selected into a DC.
  12. Because I will need to fit the image in, I need to know how big it actually is. The VertexData::GetBoundingBox method returns me a CRect which contains this information.
  13. Since I want the gradient fill to fill the ComboBox area no matter how wide or high the actual image might be, I use MM_ANISOTROPIC mapping so I can compress or stretch width and height by independent amounts.
  14. I set the "Window Extent" to be the size of the image which will be drawn
  15. I set the "Viewport Extent" to be the actual size of the rectangle we are given. In MM_ANISOTROPIC mode, this means that the image will be compressed or stretched by the ratio of the two extents so that it is scaled appropriately for the area in question.
  16. Because the gradient list has a nominal <0,0> origin, I have to offset the actual drawing by the position of the area into which it is being drawn. Then GradientFill will end up drawing the image in the area of the dropdown where I expect to see it.
  17. Finally, I do the GradientFill, using the parameters I got from the VertexData::FromBinary call.
  18. I am now done using the data I got from VertexData::FromBinary, so I call VertexData::Release to indicate that I am no longer using any pointers in the object.
  19. I restore the DC. Any changes I made in the DC, including the mapping mode, and the selection of the clipping region, are undone without my ever having had to save any internal state
  20. I need to draw a selection rectangle. I discovered that the ordinary focus rectangle was hard to see because of the complex graphics, so I wanted a much more emphatic rectangle, so I drew one explicitly.
  21. Because I am about to do additional changes, I save the DC state. Note that I want to draw this rectangle using the default coordinates of the rectangle (MM_TEXT mode), so I do this after I have restored the DC from its mapping mode and origin changes.
  22. I use a dotted pen. Implicit here is the fact that the DC is in its default SetBkMode(OPAQUE) mode, so the areas between the lines of the pen are filled with the default background color, which is the default SetBkColor(::GetSysColor(COLOR_WINDOW)), or nominally white. So the black pixels of the pen will show up on light colors of graphics, and the white color of the background will show up on dark colors of graphics.
  23. Select the pen into the DC. As with the region, I have carefully arranged that the DC will be restored before the destructor CPen::~CPen is called, so the pen will actually be deleted.
  24. Since I do not want to overdraw the actual graphics data I just created, I select a HOLLOW_BRUSH so the center of the rectangle I am about to draw will be transparent.
  25. Actually draw the highlighting rectangle.
  26. Restore DC to its original state, which will deselect the brush and pen. This must be done before the scope that defines the CPen is exited

An example showing the dropdown and the highlighting:

Owner-draw CCheckListBox

I was using an owner-draw ListBox originally for the vertex list (because I wanted to show the color), and for the GRADIENT_RECT and GRADIENT_TRIANGLE arrays (because I wanted to be able to directly edit the contents without having to delete-and-re-insert the string, which is the only way to handle this when the normal ListBox is used). However, I found a few cases where I wanted to temporarily remove a rectangle or triangle to see the effect. Deleting and re-inserting was painful, so I decided to add a check-box ListBox, the CCheckListBox class.

This pointed out a misfeature of my earlier code. When I drew the data, every item except the selected item came out correctly, but the selected item was always redrawn at the far left of the window. A bit of study showed me that I had to handle the case of ODA_FOCUS specially. The prolog of DrawItem had to be modified, so it now was:

void CGradientTriangleList::DrawItem(LPDRAWITEMSTRUCT dis) 
   {
    CDC * dc = CDC::FromHandle(dis-&hDC);
    // Make a copy of the bounding rectangle so we can modify it if we desire

    CRect r = dis->rcItem;

    // Handle the case where the control is empty. Draw an empty focus rectangle to indicate the control has focus
    if(dis->itemID == (UINT)-1)
       { /* empty list */
        if(dis->itemState & ODS_FOCUS)
           dc->DrawFocusRect(&r);
        return;
       } /* empty list */

    if(dis->itemAction & ODA_FOCUS)
       { /* draw focus */
        dc->DrawFocusRect(&r);
        return;
       } /* draw focus */

The point finder

I decided I wanted a way to see where a point was. I decided that the least obtrusive way to do this would be to "flash" a finder icon up on the place where the point was. The steps in the process of doing this were:

This whole effort was greatly simplified because clicking on a ListBox item sends a LBN_SELCHANGE notification even if the selection hasn't changed!. Otherwise, I would have had to use some sort of mouse-down detection in the subclassed ListBox class.

I added an interface to highlight a point

void CGradientImage::HighlightCoordinate(CPoint pt)
    {
     BoundingBox.left = pt.x - CIRCLE_SIZE;
     BoundingBox.right = pt.x + CIRCLE_SIZE;
     BoundingBox.top = pt.y - CIRCLE_SIZE;
     BoundingBox.bottom = pt.y + CIRCLE_SIZE;
     InvalidateRect(&BoundingBox);
     SetTimer(IDT_UNHIGHLIGHT, 350, NULL);
    } // CGradientImage::HighlightCoordinate

This is very simple code. It merely computes a "highlighting rectangle" and invalidates it, then sets a timer.

Next, I modified the OnPaint handler to add this code:

    if(result && !BoundingBox.IsRectEmpty())
       { /* has highlight */
        int save = dc.SaveDC();

        dc.SetROP2(R2_NOTXORPEN);
        dc.SelectStockObject(HOLLOW_BRUSH);
        dc.Ellipse(&BoundingBox);
        dc.MoveTo(BoundingBox.left, BoundingBox.top);
        dc.LineTo(BoundingBox.right, BoundingBox.bottom);
        dc.MoveTo(BoundingBox.right, BoundingBox.top);
        dc.LineTo(BoundingBox.left, BoundingBox.bottom);

        dc.RestoreDC(save);
       } /* has highlight */

This code only executes if the preceding GradientFill operation returned TRUE, the value in the variable result. I used the presence of anything in the BoundingBox rectangle to indicate that a highlight should be drawn.

The R2_NOTXORPEN means that the pixels will be modified not by just drawing the lines, but by complementing the bits of the pixels under the pen. This means the pen will stand out on either light or dark backgrounds.

    

Finally, I added an OnTimer handler. This invalidates the area, then clears the BoundingBox rectangle:

void CGradientImage::OnTimer(UINT nID)
    {
     switch(nID)
        { /* nID */
         case IDT_UNHIGHLIGHT:
            InvalidateRect(&BoundingBox);
            BoundingBox.SetRectEmpty();
            KillTimer(nID);
            break;
        } /* nID */
    } // CGradientImage::OnTimer

Since I no longer care about the timer at this point, I kill it.

Generalizing the highlighting

After using the above for a few hours, I found that I was having to select a GRADIENT_RECT or GRADIENT_TRIANGLE then highlight each of its vertices in turn to see where each was. This struck me as a waste of time, so I generalized the algorithm to cause the CGradientImage::HighlightCoordinate to create a set of points to be highlighted:

void CGradientImage::HighlightCoordinate(CPoint pt)
    {
     CRect r(pt.x - CIRCLE_SIZE, pt.y - CIRCLE_SIZE, pt.x + CIRCLE_SIZE, pt.y + CIRCLE_SIZE);
     Highlights.Add(r);
     InvalidateRect(&r);
     SetTimer(IDT_UNHIGHLIGHT, 350, NULL);
    } // CGradientImage::HighlightCoordinate

The OnPaint handler now used the presence of any element in the array to draw the highlights:

    if(result && !Highlights.IsEmpty())
       { /* has highlight */
        int save = dc.SaveDC();

        dc.SetROP2(R2_NOTXORPEN);
        dc.SelectStockObject(HOLLOW_BRUSH);
        for(int i = 0; i < Highlights.GetSize(); i++)
           { /* draw each */
            dc.Ellipse(&Highlights[i]);
            dc.MoveTo(Highlights[i].left, Highlights[i].top);
            dc.LineTo(Highlights[i].right, Highlights[i].bottom);
            dc.MoveTo(Highlights[i].right, Highlights[i].top);
            dc.LineTo(Highlights[i].left, Highlights[i].bottom);
           } /* draw each */

        dc.RestoreDC(save);
       } /* has highlight */

The OnTimer now had to invalidate all the rectangles before emptying the array:

         case IDT_UNHIGHLIGHT:
            { /* unhighlight */
             for(int i = 0; i < Highlights.GetSize(); i++)
                InvalidateRect(&Highlights[i]);
             Highlights.RemoveAll();
             KillTimer(nID);
            } /* unhighlight */
            break;

This screen shot shows what happens when I click the first element of the center-fill gradient, which has as its points 0, 1, 4. These represent the top left corner (0), the top middle (1), and the center (4) given the numbering I had assigned in the example.

In-place editing

If you need to be able to change values in a CListBox without having to remove and re-insert the item, an owner-draw ListBox is a good choice. This is because there is no actual string to update (this means you can't use the LBS_HASSTRINGS style). Therefore, you can simply change the structure you are working on. All that is required is to force the ListBox to actually redraw the updated information. This is very easy.

In addition, this makes actions such as "move item down" and "move item up" very straightforward. To move an item "up", you pick up its itemData and swap it with the itemData of its predecessor. To move an item "down", you swap its itemData with its successor.

To force a redraw, I created a method of the derived class, Invalidate, which takes as its argument the index of the item to be redrawn. For the CGradientTriangle list, it has the same form I use for all other similar classes:

void CGradientTriangleList::Invalidate(int sel)
    {
     CRect r;
     GetItemRect(sel, &r);
     InvalidateRect(&r);
    } // CGradientTriangleList::Invalidate

The humble About box

OK, there's nothing worse than being trapped in a hotel room for a couple days (see the Notes section) to bring out the worst excesses in a programmer. I wanted the About box to at least show some use of GradientFill. Alas, the icon is limited to the standard 256-color palette, which is quite horrible. But I could create a gradient bitmap for the Flounder image. But it wasn't as straightforward as I thought. The problem was the background color: you can't have transparent bitmaps or use LR_LOADMAP3DCOLORS or LR_LOADTRANSPARENTon 24-bit bitmaps. So I just created a Flounder-colored gradient, and edited away anything that didn't look like a Flounder. I used the same basic convention that the top left pixel (the 0,0) coordinate) was the "transparent" color. Then I just modified the bitmap to replace that color with ::GetSysColor(COLOR_3DFACE). It was only in retrospect that I realized that I could have used the standard icon and just replaced all the Flounder-colored pixels in the original bitmap with the corresponding gradient pixels in the gradient image. Then I could have simply used a solid gradient image with no editing at all.

BOOL CAboutDlg::OnInitDialog()
   {
    CDialog::OnInitDialog();

    HBITMAP bmp = (HBITMAP)::LoadImage(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_FLOUNDER), IMAGE_BITMAP, 0, 0, LR_LOADMAP3DCOLORS);
    CClientDC client(this);
    CDC dc;
    dc.CreateCompatibleDC(&client);
    dc.SelectObject(bmp);

    BITMAP info;
    ::GetObject(bmp, sizeof(BITMAP), &info);

    COLORREF transparent = dc.GetPixel(0,0);
    COLORREF bk = ::GetSysColor(COLOR_3DFACE);
    
    for(int x = 0; x < info.bmWidth; x++)
        for(int y = 0; y < info.bmHeight; y++)
            { /* change pixels */
             COLORREF c = dc.GetPixel(x, y);
             if(c == transparent)
                dc.SetPixel(x, y, bk);
            } /* change pixels */
    c_Flounder.SetBitmap(bmp);
    return TRUE;  // return TRUE unless you set the focus to a control
                  // EXCEPTION: OCX Property Pages should return FALSE
   }

Saving to the clipboard

My "screen capture" essay describes how to capture the current window contents.  However, this does not work if the window contents do not encompass all the information desired.  So if I just captured the window contents of this display. it would appear as shown here:

but that is not actually the image that was created.  It is truncated by the size of the window, and it has the background which is COLOR_3DFACE, which is not particularly useful. The actual image is

I also set the background to black.  Currently, I do not have an interface that allows changing this setting.

Producing the complete bitmap involves creating a memory DC, selecting an appropriate-sized bitmap into it, and drawing into the DC.  This is typically handled by the OnDraw member of the derived CView class.  But this is a dialog-based app with no view.  Other than having to explicitly implement the calls, however, the logic is essentially the same as the MFC document/view framework.

The common drawing routine

The common drawing logic is a subroutine shared by both the OnPaint and ToClipboard methods.

void CGradientImage::DoGradientFill(CDC & dc)
    {
     int save = dc.SaveDC();

     PVOID p;
     ULONG num;
     switch(mode)
        { /* mode */
         case GRADIENT_FILL_RECT_H:
         case GRADIENT_FILL_RECT_V:
            p = Rectangles.GetData();
            num = Rectangles.GetSize();
            break;
         case GRADIENT_FILL_TRIANGLE:
            p = Triangles.GetData();
            num = Triangles.GetSize();
            break;
         default:
            return;
        } /* mode */

     BOOL result = dc.GradientFill(Vertices.GetData(),              // [1]
                                   Vertices.GetSize(),
                                   p, num,
                                   mode);
     ImageValid = result;                                           // [2]

     if(!result)
        { /* failed */
         DWORD err = ::GetLastError();                              
         if(err != ERROR_SUCCESS)
            {/* show error */
             CRect r;
             dc.SetWindowOrg(0, 0);                                 // [3]
             GetClientRect(&r);
             dc.FillSolidRect(&r, RGB(255, 0, 0));
             CString s = ErrorString(err);                          // [4]
             dc.TextOut(0, 0, s);
            } /* show error */
        } /* failed */

    //****************************************************************
    // Highlight the point if active
    //****************************************************************
    // see discussion of the point finder for the code that goes here
 
     dc.RestoreDC(save);
    } // CGradientImage::DoGradientFill
  1. Do the GradientFill to the DC
  2. Save its state to indicate if there is a valid image
  3. Ignore the setting of the scrollbar if an error is being displayed
  4. Obtain the text representation of the error

The OnPaint handler

void CGradientImage::OnPaint() 
   {
    CPaintDC dc(this); // device context for painting

    int save = dc.SaveDC();                                     // [1]
    CRect r;
    GetClientRect(&r);
    CRgn rgn;                                                   // [2]
    rgn.CreateRectRgnIndirect(&r);                              // [3]
    dc.SelectClipRgn(&rgn);                                     // [4]
    dc.SetWindowOrg(org.x, org.y);                              // [5] 

    DoGradientFill(dc);                                         // [6]
    dc.RestoreDC(save);                                         // [7]
   }                                                            // [8]
  1. Save the state of the DC.  This is necessary so the DC can be restored before the destructor of the region is called.
  2. Because the image is larger than the window, it is necessary to clip the drawing to the window area.  This is the result of what can most politely be called a design blunder in Windows; controls have the CS_PARENTDC style, which means that the clipping region of the control is actually the clipping region of the parent window.  It is hard to figure why this could have ever been construed to make sense, but it is an unfortunate fact of Windows programming (this blunder was fixed in Vista).
  3. The clipping region is computed to be the client area
  4. The clipping region is selected into the DC.  The region must be deselected before CRgn::~CRgn is called.
  5. Set the window origin so the image is scrolled to the right place, as selected by the scrollbars, which set the CPoint org to the correct offset values.
  6. Call the DoGradientFill function passing in the clipped-and-scrolled DC
  7. Restore the DC.  This restores it to its original state, which implicitly deselects the clipping region
  8. The CRgn::~CRgn destructor is called, but because the HRGN is not selected into a DC, the region is actually deleted.  The ::DeleteObject method will not actually delete a GDI object that is actively selected into a DC.

The ToClipboard handler

void CGradientImage::ToClipboard()
    {
     CClientDC dc(this);                               // [1]
     CDC memDC;                                        // [2]

     memDC.CreateCompatibleDC(&dc);                    // [3]

     CBitmap bm;                                       // [4]
     CRect r;
     r = BoundingBox;                                  // [5]

     CSize sz(r.Width(), r.Height());                  // [6]
     bm.CreateCompatibleBitmap(&dc, sz.cx, sz.cy);     // [7]

     int save = memDC.SaveDC();                        // [8]

     memDC.SelectObject(&bm);                          // [9]
     DoGradientFill(memDC);                            // [10]

     OpenClipboard();                                  // [11]
     ::EmptyClipboard();                               // [12]
     ::SetClipboardData(CF_BITMAP, bm.m_hObject);      // [13]
     CloseClipboard();                                 // [14]

     memDC.RestoreDC(save);                            // [15]
     
     bm.Detach();                                      // [16]
    } // CGradientImage::ToClipboard                   // [17]
  1. Create a DC for the window
  2. Create a CDC object that will be the memory DC
  3. Create a memory DC in the memDC object
  4. Declare a CBitmap variable that will hold the bitmap
  5. The BoundingBox variable is a CRect which is set by the caller to indicate the actual bounding box of the image (it takes the max of the x and y values from the TRIVERTEX array)
  6. Compute the size of the bounding box
  7. Create a DDB (Device Dependent Bitmap) that will hold the image
  8. Save the state of this memory DC
  9. Select the bitmap into the memory DC
  10. Call DoGradientFill with the memory DC
  11. Open the clipboard
  12. Delete the contents of the clipboard
  13. Save the HBITMAP object into the clipboard in CF_BITMAP format.  At this point, the clipboard now owns the HBITMAP and it must not be used or deleted by this program
  14. Close the clipboard
  15. Restore the DC, which deselects the HBITMAP from the DC
  16. Detach the HBITMAP from the CBitmap object.  That way, when the variable bm leaves scope and CBitmap::~CBitmap is called, the actual bitmap will not be deleted.  See my essay on the use of Detach
  17. The scope of the bm variable is exited, and the destructor is called.  But because the bitmap is no longer attached to the CBitmap object, the object is not deleted

 

[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