Self-Registering Windows Classes

Home
Back To Tips Page

Every so often, there is a need to create a custom Window class. Typically, you do this via AfxRegisterWindowClass, give the window a class name of your choosing, and then use this class in a Create call. This class usually has a custom MFC subclass associated with it. The illustration to the left shows a little application with a custom control, a compass.

A typical example might be a desire to create a simple control with custom graphics. For this example, I created compass control, whose class will be CCompass, and which will show a simulated compass needle. It is a subclass of "Generic CWnd".

To create this class, go into the ClassWizard, select the "Add Class" button, and select the option "New Class". Type in the name of your class, and in the "Base Class" box, select the option "generic CWnd", which appears nearly at the bottom of the options.

When you click OK, you will get two files, Compass.cpp and Compass.h, which implement your class.

When you are back in ClassWizard, this class should be selected as the class you want. For a custom graphics class, you will typically want to add a WM_ERASEBKGND and WM_PAINT handler. To do this, select the class in the  window, selection WM_ERASEBKGND, click Add Function, select WM_PAINT, and click Add Function. You should end up with something as shown below.

At this point you can go in and fill in the two functions.

However, there is a problem with using this class in a dialog box. You must first register the "Window class" under a specific class name so the dialog editor can create it. This is necessary if you want to use the control in a CDialog-derived class, CPropertyPage-derived class, or CFormView-derived class. This means you must provide a call to register the class, and this call must be executed before you attempt to create the class that contains the control. 

This is inconvenient. Why should the programmer have to remember to do this; the consequence of not doing it is that the dialog does not come up.

I decided, in writing classes that my clients would want to use, that they should not be inconvenienced by having to remember to register the class, or understand the details of the AfxRegisterClass call. So I decided to create a mechanism that would automatically register the class.

The technique was to create a static member variable of the class and initialize it. The initialization would, as a side effect, register the class. Because the variable is a static member variable, it will be initialized during application startup. Thus, the class would be automatically registered. 

So I added the following declaration to the CCompass class:

protected:
    static BOOL hasclass;
    static BOOL RegisterMe();
#define COMPASS_CLASS_NAME _T("Compass")

 then in the CCompass.cpp file I added

BOOL CCompass::hasclass = CCompass::RegisterMe();

Note that because this is a static initializer, it will be executed at system startup. This means that the class registered by RegisterMe will be registered when the application is initialized. The class will then be available for any dialogs, property pages, or form views.

However, some approaches will not work. For example, you cannot use AfxRegisterWndClass because it returns the string for the synthesized class name, a name determined at execution time, but dialog templates require that you know the class name at the time the template is constructed. It would be the height of insanity to determine the string that AfxRegisterWndClass returned and specify that as the class name the programmer should use.

In addition, you cannot call AfxGetInstanceHandle to obtain the instance handle to register the class. This is because the variable used by AfxGetInstanceHandle is initialized after the WinMain of MFC is invoked, which is after the static member variables have been initialized. But you can use the low-level API call ::GetModuleHandle. For compatibility with 16-bit windows this returns a type HMODULE instead of HINSTANCE, although this distinction has no meaning in Win32. However, you must do the explicit cast or the compiler becomes unhappy.

I also found that it works better if you choose ::DefWindowProc as the window procedure instead of NULL (this will be eventually replaced by AfxWndProc when you subclass the window). Do not choose AfxWndProc!

In the code below I also made some arbitrary choices. For example, because this will be a child control, it does not need an icon, so the hIcon member is set to NULL. To illustrate how to choose a background brush, should you need one, I chose to use a standard background color, the dialog background, COLOR_BTNFACE, and in accordance with the completely peculiar requirements of a window class (it would have made a great deal of sense, for example, to have not allowed the integer designator of 0 for a COLOR_color, but this would have required careful design), I have to add 1 to the color. Since it is a child control it has no menu, and the lpszMenuName is therefore NULL. The critical parameter is the class name. This is the name the programmer must use in the dialog template.

BOOL CCompass::RegisterMe()
   {
    WNDCLASS wc;   
    wc.style = 0;                                                 
    wc.lpfnWndProc = ::DefWindowProc; // must be this value
    wc.cbClsExtra = 0;                         
    wc.cbWndExtra = 0;                               
    wc.hInstance = (HINSTANCE)::GetModuleHandle(NULL);        
    wc.hIcon = NULL;     // child window has no icon         
    wc.hCursor = NULL;   // we use OnSetCursor                  
    wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);                
    wc.lpszMenuName = NULL;  // no menu                             
    wc.lpszClassName = COMPASS_CLASS_NAME;                          
    return AfxRegisterClass(&wc);
   }

To put the control in a dialog, bring up the dialog editor. For step 1, select the "Custom Control" icon in the toolbox, the icon, and place the control in the desired section of the dialog box, as shown in step 2. Then bring up the Properties box. In step 3, delete the caption, and in step 4 type in the name of the class you used as COMPASS_CLASS_NAME.

Unfortunately, ClassWizard is rather primitive; it will not acknowledge the existence of this control. Why? Ask Microsoft, I have no idea why it would preclude this control from its list of controls for which you can create a member variable. But it does.

So you have to edit your dialog "by hand". The Good News is that this is easy.

For example, locate in your dialog's header file the AFX_DATA section. My dialog class is called CController, and I have already used ClassWizard to create member variables for the range, speed, and altitude of the object being tracked.

//{{AFX_DATA(CController)
	enum { IDD = IDD_CONTROLLER };
	CStatic	c_Range;
	CStatic	c_Speed;
	CStatic	c_Altitude;
	CCompass c_Compass;
	//}}AFX_DATA

What is interesting to note here is that once you add the variable, ClassWizard is perfectly happy to deal with it, it just won't let you add the variable! Weird!

Now go into the implementation file for your dialog and locate the DoDataExchange method. Inside the AFX_DATA_MAP section, add the line shown below. Note that it is identical in form to the other lines that create control variables, except that the control ID and variable name reflect the desired mapping. Again, once this is done, ClassWizard is happy to manage the control.

void CController::DoDataExchange(CDataExchange* pDX)
{
	CFormView::DoDataExchange(pDX);
	//{{AFX_DATA_MAP(CController)
	DDX_Control(pDX, IDC_RANGE, c_Range);
	DDX_Control(pDX, IDC_SPEED, c_Speed);
	DDX_Control(pDX, IDC_ALTITUDE, c_Altitude);
        DDX_Control(pDX, IDC_COMPASS, c_Compass);
	//}}AFX_DATA_MAP
}

At this point, you are free to instantiate the dialog. Note that when your application loads, the class is registered, so even if you were to use a CFormView in an SDI application, you need to take no further effort to use the class.

This control has some interesting properties from a GDI viewpoint. For example, I want a circular compass inside the control, but I would not like to constrain the designer of the dialog to choose a square dialog. I also don't want any annoying flashes within the compass as the background repaints.

To do this, I create a circular region that precludes the default WM_ERASEBKGND handler from touching the contents of the control. I then use this to limit the clip the output operations within the compass rose. This can also be used for hit-testing by using PtInRegion to see if the mouse is in the circular area.

The compass in its disabled and enabled modes is shown below.

          

CCompass::CreateClipRegion

CRect CCompass::CreateClipRegion(CRgn & rgn)
    {
     CRect r;
     GetClientRect(&r);
     int radius = min(r.Width() / 2, r.Height() / 2);
     CPoint center(r.Width() / 2, r.Height() / 2);
     rgn.CreateEllipticRgn(center.x - radius, center.y - radius,
			   center.x + radius, center.y + radius);
     return CRect(center.x - radius, center.y - radius,
		  center.x + radius, center.y + radius);
    } // CCompass::CreateClipRegion

CCompass::OnEraseBkgnd

BOOL CCompass::OnEraseBkgnd(CDC* pDC) 
   {
    CRgn rgn;
    CSaveDC sdc(pDC);
    CreateClipRegion(rgn);
    pDC->SelectClipRgn(&rgn, RGN_DIFF); // remove circle from update area
    return CWnd::OnEraseBkgnd(pDC);
   }

CCompass::MapDC

Because of the frequency with which I map the DC, I created a separate method for this purpose.

void CCompass::MapDC(CDC & dc)
    {
     dc.SetMapMode(MM_ISOTROPIC);
     CRect r;
     GetClientRect(&r);
     dc.SetWindowExt(r.Width(), r.Height());
     dc.SetViewportExt(r.Width(), -r.Height());
     CPoint center(r.left + r.Width() / 2, r.top + r.Height() / 2);
     dc.SetViewportOrg(center.x, center.y);
    } // CCompass::MapDC

CDoublePoint

This class lets me represent fractional angles. It turns out that other representations in the application were already using double precision, so it was a natural extension to use it in the compass. Note the simplistic CPoint cast which truncates instead of rounding; this is sufficient for the application.

class CDoublePoint {
    public:
       CDoublePoint(){}
       CDoublePoint(double ix, double iy) {x = ix; y = iy; }
       double x;
       double y;
       operator CPoint() { CPoint pt; pt.x = (int)x; pt.y = (int)y; return pt; }
};

CCompass::OnLButtonDown

Button detection is done by responding only if the mouse is in the compass region. Note that I send a user-defined message to the parent, as described in my companion essay.

void CCompass::OnLButtonDown(UINT nFlags, CPoint point) 
   {
    CRgn rgn;
    CreateClipRegion(rgn);
    if(rgn.PtInRegion(point))
       { /* in region */
	CClientDC dc(this);
	MapDC(dc);
	dc.DPtoLP(&point);
	GetParent()->SendMessage(CPM_CLICK, (WPARAM)point.x, (LPARAM)point.y);
	return;
       } /* in region */
    CWnd::OnLButtonDown(nFlags, point);
   }

DegreesToRadians/GeographicToGeometric

I have a utility function that converts degrees to radians, declared in a separate header file.

__inline double DegreesToRadians(double x) { return (((x)/360.0) * (2.0 * 3.1415926535)); }

The normal geometric coordinate system has the angle 0.0 going to the right of the origin, and rotates counterclockwise with increasing angle. We want to think of degrees in the geographic sense, where 0.0 is North, 90.0 is East, 180.0 is South and 270.0 is West. The following inline method is useful for doing the conversion from the natural coordinates of geography to the coordinates required for the math.h library.

__inline double GeographicToGeometric(double x) { return -(x - 90.0); }

CCompass::CCompass

The constructor loads the table of coordinate designators.

CCompass::CCompass()
{
 // Note: for optimal performance, sort monotonically by font size
 // Note: The first entry must be the largest 
 display.Add(new displayinfo(  0.0, _T("N"), 100.0, TRUE));
 display.Add(new displayinfo( 90.0, _T("E"), 90.0, FALSE));
 display.Add(new displayinfo(180.0, _T("S"), 90.0, FALSE));
 display.Add(new displayinfo(270.0, _T("W"), 90.0, FALSE));
 display.Add(new displayinfo( 45.0, _T("NE"), 80.0, FALSE));
 display.Add(new displayinfo(135.0, _T("SE"), 80.0, FALSE));
 display.Add(new displayinfo(225.0, _T("SW"), 80.0, FALSE));
 display.Add(new displayinfo(315.0, _T("NW"), 80.0, FALSE));

 RegistryString compass(IDS_COMPASS);
 compass.load();
 if(compass.value.GetLength() == 0 || !arrow.Read(compass.value))
    arrow.Read(_T("Arrow.plt")); // use default

 angle = 0.0; // initialize at North
 ArrowVisible = FALSE;
}

CCompass::OnPaint

void CCompass::OnPaint() 
   {
    CPaintDC dc(this); // device context for painting
    CBrush br(::GetSysColor(COLOR_INFOBK));
    CRgn rgn;
    CRect r;
    r = CreateClipRegion(rgn);
#define BORDER_WIDTH 2
    CPen border(PS_SOLID, BORDER_WIDTH, RGB(0,0,0));
    CBrush needle(RGB(255, 0, 0));

#define ENABLED_COLOR RGB(0,0,0)
#define DISABLED_COLOR RGB(128,128,128)

    CPen enabledPen(PS_SOLID, 0, ENABLED_COLOR);
    CPen disabledPen(PS_SOLID, 0, DISABLED_COLOR);
    //----------------------------------------------------------------
    // GDI resources must be declared above this line
    //----------------------------------------------------------------
    CSaveDC sdc(dc);
    dc.SelectClipRgn(&rgn); // clip to compass
    dc.FillRgn(&rgn, &br);
    // Convert the origin to the center of the circle
    CPoint center(r.left + r.Width() / 2, r.top + r.Height() / 2);
    // Renormalize the rectangle to the center of the circle
    r -= center;
    int radius = r.Width() / 2;
    dc.SetBkMode(TRANSPARENT);
    MapDC(dc);

    // Draw the border
    { 
     CSaveDC sdc2(dc);
     dc.SelectClipRgn(NULL);
     dc.SelectStockObject(HOLLOW_BRUSH);

     dc.SelectObject(&border);
     dc.Ellipse(-radius, -radius, radius, radius);

     r.InflateRect(-BORDER_WIDTH, -BORDER_WIDTH);
     radius = r.Width() / 2;
    }
    radius = r.Width() / 2;
    
    dc.SelectObject(IsWindowEnabled() ? &enabledPen : &disabledPen);
    // Draw N-S line
    dc.MoveTo(0, radius);
    dc.LineTo(0, -radius);
    // Draw E-W line
    dc.MoveTo(-radius, 0);
    dc.LineTo(radius, 0);

    // Draw SW-NE line
    dc.MoveTo((int)(radius * sin(DegreesToRadians(GeographicToGeometric(225.0)))), 
	      (int)(radius * cos(DegreesToRadians(GeographicToGeometric(225.0)))) );
    dc.LineTo((int)(radius * sin(DegreesToRadians(GeographicToGeometric( 45.0)))),
	      (int)(radius * cos(DegreesToRadians(GeographicToGeometric( 45.0)))) );
    // Draw NW-SE line
    dc.MoveTo((int)(radius * sin(DegreesToRadians(GeographicToGeometric(315.0)))), 
	      (int)(radius * cos(DegreesToRadians(GeographicToGeometric(315.0)))) );
    dc.LineTo((int)(radius * sin(DegreesToRadians(GeographicToGeometric(135.0)))),
	      (int)(radius * cos(DegreesToRadians(GeographicToGeometric(135.0)))) );

    // Now create the font elements
    // The symbols are placed along a circle which is inscribed
    // within the compass area
    //
    // +-----------------------------+
    //         /     N     \
    //        / NW   |   NE \
    //       /       |       \
    //       |       |       |
    //       | W-----+-----E |  
    //       |       |       |
    //       \       |       /
    //        \ SW   |   SE /
    //         \     S     /
    // +-----------------------------+

    double size = 0.15 * (double)r.Width();
    double CurrentFontSize = 0.0; // current font size
    CFont * f = NULL;
    dc.SetTextColor(IsWindowEnabled() ? ENABLED_COLOR : DISABLED_COLOR);

    for(int i = 0; i < display.GetSize(); i++)
       { /* draw points */
	CSaveDC sdc2(dc);
	dc.SetBkMode(OPAQUE);
	dc.SetBkColor(::GetSysColor(COLOR_INFOBK));
	if(display[i]->GetSize() != CurrentFontSize)
	   { /* new font */
	    if(f != NULL)
	       delete f;
	    f = display[i]->CreateFont(size, _T("Times New Roman"));
	   } /* new font */
	dc.SelectObject(f);
	CurrentFontSize = display[i]->GetSize();
	CString text = display[i]->GetText();
	//
	//      4 | 1
	//      --+--
	//      3 | 2
	//------------------------------------------------------------------
	//  Ø    qdant    x     y   x-origin     y-origin        alignment
	//  ----------------------------------------------------------------
	//  0    4.1      0    >0   x-w/2        y               TOP, LEFT
	//  <90  1       >0    >0   x            y               TOP, RIGHT
	//  90   1.2     >0    0    x            y-h/2           TOP, RIGHT
	//  <180 2       >0    <0   x            y               BOTTOM, RIGHT
	//  180  2.3     0     <0   x-w/2        y               BOTTOM, RIGHT
	//  <270 3       <0    <0   x            y               BOTTOM, LEFT
	//  270  3.4     <0    0    x            y-h/2           TOP, LEFT
	//  <360 4       <0    >0   x            y               TOP, LEFT

	int x = (int)(radius * cos(DegreesToRadians(GeographicToGeometric(display[i]->GetAngle()))));
	int y = (int)(radius * sin(DegreesToRadians(GeographicToGeometric(display[i]->GetAngle()))));
	CSize textSize = dc.GetTextExtent(text);

	double theta = display[i]->GetAngle();
	if(theta == 0.0)
	   { /* 0 */
	    dc.SetTextAlign(TA_TOP | TA_LEFT);
	    x -= textSize.cx / 2;
	   } /* 0 */
	else
	if(theta < 90.0)
	   { /* < 90 */
	    dc.SetTextAlign(TA_TOP | TA_RIGHT);
	   } /* < 90 */
	else
	if(theta == 90.0)
	   { /* 90 */
	    dc.SetTextAlign(TA_TOP | TA_RIGHT);
	    y += textSize.cy / 2;
	   } /* 90 */
	else
	if(theta < 180.0)
	   { /* < 180 */
	    dc.SetTextAlign(TA_BOTTOM | TA_RIGHT);
	   } /* < 180 */
	else
	if(theta == 180.0)
	   { /* 180 */
	    dc.SetTextAlign(TA_BOTTOM | TA_LEFT);
	    x -= textSize.cx / 2;
	   } /* 180 */
	else
	if(theta < 270.0)
	   { /* < 270 */
	    dc.SetTextAlign(TA_BOTTOM | TA_LEFT);
	   } /* < 270 */
	else
	if(theta == 270)
	   { /* 270 */
	    dc.SetTextAlign(TA_TOP | TA_LEFT);
	    y += textSize.cy / 2;
	   } /* 270 */
	else
	   { /* < 360 */
	    dc.SetTextAlign(TA_TOP | TA_LEFT);
	   } /* < 360 */
	dc.TextOut(x, y, text);
       } /* draw points */
    if(f != NULL)
       delete f;

    // Draw the arrow
    if(IsWindowEnabled() && ArrowVisible)
       { /* draw arrow */
        CRect bb = arrow.GetInputBB();
	dc.SelectObject(&needle);
	arrow.Transform(angle, (double)abs(bb.Height()) / (2.0 * (double)radius));
	arrow.Draw(dc, CDoublePoint(0.0, 0.0));
       } /* draw arrow */

    // Do not call CWnd::OnPaint() for painting messages
   }

download.gif (1234 bytes)The complete code can be downloaded from this site. This includes the polygon manger, the CSaveDC file, and necessary support header and implementation files.

[Dividing Line Image]

The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.

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