Programming Windows with MFC, Second Edition

[Previous] [Next]

MFC's CRect class represents rectangles—simple regions of space enclosed by four boundaries aligned at right angles. More complex regions of space can be represented with the CRgn class, which encapsulates GDI objects called, appropriately enough, regions. The most common use for regions is to create complex patterns that serve as clipping boundaries for GDI drawing functions. But you can use CRgn in other ways, too. Here's a brief look at regions and some of the interesting things that you can do with them.

Regions and the CRgn Class

CRgn provides functions for creating geometrically shaped regions, combining existing regions to create more complex regions, and performing certain operations such as hit-testing a region or retrieving a region's bounding rectangle. The CDC class provides tools for drawing with a region once it's created—for example, filling a region with a brush color or using it to clip other drawing operations. Let's see first how regions are created. Then we'll look at the CDC functions that act on regions and finish up by building a sample program that uses regions to generate some rather unusual output.

Creating Regions

After a CRgn object is constructed, a region is created and attached to it by calling one of several member functions the CRgn class provides for region creation. The pertinent CRgn functions are summarized in the following table.

CRgn Region-Creation Functions

Function Description
CreateRectRgn Creates a rectangular region from a set of coordinates
CreateRectRgnIndirect Creates a rectangular region from a RECT structure or a CRect object
CreateEllipticRgn Creates an elliptical region from a set of coordinates
CreateEllipticRgnIndirect Creates an elliptical region from a RECT structure or a CRect object
CreateRoundRectRgn Creates a rectangular region with rounded corners
CreatePolygonRgn Creates a polygonal region from a set of points
CreatePolyPolygonRgn Creates a region composed of multiple polygons from a set of points
CreateFromPath Creates a region from a path
CreateFromData Creates a region by applying two-dimensional coordinate transformations to an existing region
CopyRgn Creates a region that is a copy of an existing region

The use of most of these functions is straightforward. For example, to create an elliptical region from a CRect object named rect that defines a bounding box, you can write

CRgn rgn; rgn.CreateEllipticRgnIndirect(&rect);

To create a rectangular region with rounded corners, you'd do it this way instead:

CRgn rgn; rgn.CreateRoundRectRgn (rect.left, rect.top, rect.right, rect.bottom, nCornerWidth, nCornerHeight);

nCornerWidth and nCornerHeight represent the horizontal and vertical dimensions, respectively, of the ellipses used to round the corners. All coordinates passed to functions that create regions are logical coordinates. Like other GDI objects, a region must be deleted when it's no longer needed. Creating a CRgn on the stack makes destruction automatic because when a CRgn goes out of scope it destroys the GDI region it's attached to.

One of the most powerful region-creation functions is CRgn::CreateFromPath, which converts the device context's current path into a region. A path is an outline generated by bracketing calls to other GDI drawing functions between calls to CDC::BeginPath and CDC::EndPath. The following statements generate a simple elliptical path and convert it into a region:

dc.BeginPath (); // Define a path dc.Ellipse (0, 0, 400, 200); dc.EndPath (); CRgn rgn; // Convert the path into a region. rgn.CreateFromPath (&dc);

There's nothing remarkable about this code because you could do the same thing by simply calling CRgn::CreateEllipticRgn. But what's cool about CreateFromPath is that you can create paths from more complex objects such as Bézier curves and text outlines. The following statements create a region from the characters in the text string "Hello, MFC":

dc.BeginPath (); dc.TextOut (0, 0, CString (_T ("Hello, MFC"))); dc.EndPath ();

Once created, the path can be converted into a region with CRgn::CreateFromPath. Ellipse and TextOut are but two of several CDC drawing functions that work with BeginPath and EndPath; for a complete list, refer to the MFC documentation for the API function ::BeginPath. (The subset of GDI drawing functions that can be used to generate paths varies slightly between Windows 95 and Windows 98 and Windows NT and Windows 2000, so watch out.) You can also use paths in ways unrelated to regions. To learn about the drawing operations you can perform with paths, see the MFC documentation for the CDC functions FillPath, StrokePath, StrokeAndFillPath, and WidenPath.

Another way to create complex regions is to combine existing regions with CRgn::CombineRgn. CombineRgn accepts three parameters: CRgn pointers to the two regions to be combined (region 1 and region 2) and an integer value specifying the combine mode. The combine mode can be any one of the five values listed here:

Mode Description
RGN_COPY Sets the region equal to region 1
RGN_AND Sets the region equal to the intersection of regions 1 and 2
RGN_OR Sets the region equal to the union of regions 1 and 2
RGN_DIFF Sets the region equal to the area bounded by region 1 minus the area bounded by region 2
RGN_XOR Sets the region equal to the nonoverlapping areas of regions 1 and 2

The combine mode tells the GDI what Boolean operations to use to combine the regions. The statements

CRgn rgn1, rgn2, rgn3; rgn1.CreateEllipticRgn (0, 0, 100, 100); rgn2.CreateEllipticRgn (40, 40, 60, 60); rgn3.CreateRectRgn (0, 0, 1, 1); rgn3.CombineRgn (&rgn1, &rgn2, RGN_DIFF);

create a donut-shaped region consisting of a circle with a hole in the middle. Note that CombineRgn can't be called until the region it's called for is created by some other means (that is, until there's an HRGN to go with the CRgn). That's why this example calls CreateRectRgn to create a trivial rectangular region for rgn3 before calling CombineRgn.

Using Regions

Just what can you do with a region after it's created? To start with, the following CDC drawing functions use regions:

You can also invalidate a region with CWnd::InvalidateRgn. Internally, Windows uses regions rather than rectangles to track the invalid areas of a window. When you call CDC::GetClipBox, what you get back is a rectangle that bounds the window's invalid region. That region could be a simple rectangle, or it could be something much more complex.

You can perform hit-testing in regions with CRgn::PtInRegion. Let's say you create an elliptical region that's centered in a window's client area. You used PaintRgn or FillRgn to paint the region a different color from the window background color, and now you want to know when the user clicks the left mouse button inside the ellipse. If m_rgn is the CRgn object, here's what the OnLButtonDown handler might look like:

void CMyWindow::OnLButtonDown (UINT nFlags, CPoint point) { CClientDC dc (this); dc.DPtoLP (&point); // Convert to logical coordinates. if (m_rgn.PtInRegion (point)) { // The point falls within the region. } }

MFC's CRect class provides an analogous function for rectangles: PtInRect. In fact, there are many parallels in the API (and in MFC member functions) between regions and rectangles: InvalidateRect and InvalidateRgn, FillRect and FillRgn, and so on. Rectangle functions are faster, so when possible you should avoid using region functions to operate on simple rectangles and use the equivalent rectangle functions instead.

Regions really pay off when you use them as clipping boundaries for complex graphic images. A region can be selected into a device context with CDC::SelectObject or CDC::SelectClipRgn. Once selected, the region serves as a clipping boundary for all subsequent output to the device context. The RegionDemo application in the next section uses a clipping region to create an image that would be murderously difficult to draw by other means. But with a region acting as a virtual stencil for graphics output, the image is relatively easy to render. The drawback to complex clipping regions is that they're slow. But sometimes using a clipping region is the only practical way to get the output you're looking for. If you want to use a path as a clipping region, you don't have to convert it into a region and then select it into a device context. You can select the path directly into the device context with CDC::SelectClipPath.

One of the more imaginative uses for a region is to pass it to the CWnd::SetWindowRgn function so that it becomes a window region. A window region is a clipping region for an entire window. Windows doesn't allow anything outside the window region to be painted, including title bars and other nonclient-area window elements. Create an elliptical region and pass its handle to SetWindowRgn, and you'll get an elliptical window. If the window is a top-level window and its title bar is hidden from view, use an OnNcHitTest handler to convert HTCLIENT hit-test codes into HTCAPTION codes so that the window can be dragged by its client area. A more practical use for nonrectangular window regions is to create stylized text bubbles that are actually windows and that receive messages just as other windows do. With SetWindowRgn to assist you, it's not terribly difficult to create a popup window class that displays help text in a window shaped like a thought balloon and that automatically destroys itself when it's clicked.

The RegionDemo Application

Figure 15-10 shows the output from an application named RegionDemo, which uses a clipping region to draw a radial array of lines forming the words "Hello, MFC." The clipping region is generated from a path, and the path, in turn, is generated by calling CDC::TextOut between calls to CDC::BeginPath and CDC::EndPath. All the work is done in OnPaint. Look over the source code in Figure 15-11; it should be pretty apparent what's going on in each phase of the output, with the possible exception of the code that uses two different CRgn objects and various calls to CRgn member functions to generate the final clipping region (rgn1) that is selected into the device context with CDC::SelectClipRgn.

Figure 15-10. The RegionDemo window.

Figure 15-11. The RegionDemo application.

RegionDemo.h

class CMyApp : public CWinApp { public: virtual BOOL InitInstance (); }; class CMainWindow : public CFrameWnd { public: CMainWindow (); protected: afx_msg void OnPaint (); DECLARE_MESSAGE_MAP () };

RegionDemo.cpp

#include <afxwin.h> #include <math.h> #include "RegionDemo.h" CMyApp myApp; ///////////////////////////////////////////////////////////////////////// // CMyApp member functions BOOL CMyApp::InitInstance () { m_pMainWnd = new CMainWindow; m_pMainWnd->ShowWindow (m_nCmdShow); m_pMainWnd->UpdateWindow (); return TRUE; } ///////////////////////////////////////////////////////////////////////// // CMainWindow message map and member functions BEGIN_MESSAGE_MAP (CMainWindow, CFrameWnd) ON_WM_PAINT () END_MESSAGE_MAP () CMainWindow::CMainWindow () { Create (NULL, _T ("Region Demo")); } void CMainWindow::OnPaint () { CPaintDC dc (this); // // Create a 72-point Times New Roman font. // CFont font; font.CreatePointFont (720, _T ("Times New Roman")); // // Create a clipping region from the text string "Hello, MFC." // CRect rect; GetClientRect (&rect); CString string ("Hello, MFC"); CFont* pOldFont = dc.SelectObject (&font); CSize size = dc.GetTextExtent (string); int x = (rect.Width () - size.cx) / 2; TEXTMETRIC tm; dc.GetTextMetrics (&tm); int y = (rect.Height () - tm.tmHeight) / 2; dc.BeginPath (); dc.TextOut (x, y, string); dc.EndPath (); dc.SelectObject (pOldFont); CRect rcText; CRgn rgn1, rgn2; rgn1.CreateFromPath (&dc); rgn1.GetRgnBox (&rcText); rgn2.CreateRectRgnIndirect (&rcText); rgn1.CombineRgn (&rgn2, &rgn1, RGN_DIFF); dc.SelectClipRgn (&rgn1); // // Draw a radial array of lines. // dc.SetViewportOrg (rect.Width () / 2, rect.Height () / 2); double fRadius = hypot (rect.Width () / 2, rect.Height () / 2); for (double fAngle = 0.0; fAngle < 6.283; fAngle += 0.01745) { dc.MoveTo (0, 0); dc.LineTo ((int) ((fRadius * cos (fAngle)) + 0.5), (int) ((fRadius * sin (fAngle)) + 0.5)); } }

Here's a blow-by-blow analysis of the code that creates the clipping region after the path outlining the characters in the text string is created. The statement

rgn1.CreateFromPath (&dc);

initializes rgn1 with a region that matches the path. Figure 15-12 shows what this first region looks like. The interior of the region is a rectangle with the outline of the letters "Hello, MFC" stamped out in the middle. (Some graphics systems—notably PostScript—handle paths generated from character outlines differently by making the interiors of the regions the interiors of the characters themselves. The GDI does essentially the opposite, creating a region from the characters' bounding box and then subtracting the areas enclosed by the character outlines.) Next, the statements

rgn1.GetRgnBox (&rcText); rgn2.CreateRectRgnIndirect (&rcText);

copy rgn1's bounding box to a CRect object named rcText and create a region (rgn2) from it. The final statement effectively inverts rgn1 by subtracting rgn1 from rgn2:

rgn1.CombineRgn (&rgn2, &rgn1, RGN_DIFF);

Figure 15-12. The path generated from the text string "Hello, MFC."

The resulting region is one whose interior exactly matches the insides of the characters drawn by TextOut. After the region is selected into the device context, a radial array of lines is drawn outward at 1-degree increments from the center of the window's client area. Because the lines are clipped to the boundaries of the region, nothing is drawn outside the character outlines.

You could make RegionDemo slightly more efficient by moving the code that generates the region out of OnPaint and into OnCreate. The region would no longer have to be generated anew each time the window is repainted, but it would need to be repositioned with CRgn::OffsetRgn to keep it centered. Eliminating redundant calls to CRgn functions improves the speed of the output somewhat, but the biggest performance hit still comes when the lines drawn on the screen are clipped to the region's boundaries. That complex clipping regions exact a price in performance is a fact of life in computer graphics, so it's wise to avoid using nonrectangular clipping regions except in cases in which there's no reasonable alternative.

Категории