Click here to Skip to main content
Click here to Skip to main content

Self-Registering Windows Classes

, 24 Oct 2001
Rate this:
Please Sign up or sign in to vote.
A class that automatically registers its own Window class.

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, select 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::<A name=CreateClipRegion>CreateClipRegion</A>(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::<A name=OnEraseBkgnd>OnEraseBkgnd</A>(CDC* pDC) 
   {
    CRgn rgn;
    CSaveDC sdc(pDC);
    <A href="#CreateClipRegion">CreateClipRegion</A>(rgn);
    pDC->

CCompass::MapDC

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

void CCompass::<A name=MapDC>MapDC</A>(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 <A name=CDoublePoint>CDoublePoint</A> {
    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 mycompanion essay.

void CCompass::OnLButtonDown(UINT nFlags, CPoint point) 
   {
    CRgn rgn;
    <A href="#CreateClipRegion">CreateClipRegion</A>(rgn);
    if(rgn.<A name=PtInRegion>PtInRegion</A>(point))
       { /* in region */
    CClientDC dc(this);
    <A href="#MapDC">MapDC</A>(dc);
    dc.DPtoLP(&point);
    GetParent()->

DegreesToRadians/GeographicToGeometric

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

__inline double <A name=DegreesToRadians>DegreesToRadians</A>(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 <A name=GeographicToGeometric>GeographicToGeometric</A>(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 = <A href="#CreateClipRegion">CreateClipRegion</A>(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);
    <A href="#MapDC">MapDC</A>(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(<A href="#DegreesToRadians">DegreesToRadians</A>(<A href="#GeographicToGeometric">GeographicToGeometric</A>(225.0)))), 
      (int)(radius * cos(<A href="#DegreesToRadians">DegreesToRadians</A>(<A href="#GeographicToGeometric">GeographicToGeometric</A>(225.0)))) );
    dc.LineTo((int)(radius * sin(<A href="#DegreesToRadians">DegreesToRadians</A>(<A href="#GeographicToGeometric">GeographicToGeometric</A>( 45.0)))),
      (int)(radius * cos(<A href="#DegreesToRadians">DegreesToRadians</A>(<A href="#GeographicToGeometric">GeographicToGeometric</A>( 45.0)))) );
    // Draw NW-SE line
    dc.MoveTo((int)(radius * sin(<A href="#DegreesToRadians">DegreesToRadians</A>(<A href="#GeographicToGeometric">GeographicToGeometric</A>(315.0)))), 
      (int)(radius * cos(<A href="#DegreesToRadians">DegreesToRadians</A>(<A href="#GeographicToGeometric">GeographicToGeometric</A>(315.0)))) );
    dc.LineTo((int)(radius * sin(<A href="#DegreesToRadians">DegreesToRadians</A>(<A href="#GeographicToGeometric">GeographicToGeometric</A>(135.0)))),
      (int)(radius * cos(<A href="#DegreesToRadians">DegreesToRadians</A>(<A href="#GeographicToGeometric">GeographicToGeometric</A>(135.0)))) );

    // Now create the font elements
    // The symbols are placed along a circle which is inscribed
    // within the compass area
    //
    // +-----------------------------+
    //         /     N     \<IMG height=120 src="selfregister/compassdisabled.gif" width=120 align=right border=0>
    //        / 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]->

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Joseph M. Newcomer

United States United States
No Biography provided

Comments and Discussions

 
GeneralWhy won't this display in a property page PinmemberMark Cariddi30-Aug-05 3:46 
QuestionHow to create a WS_POPUP CMyListBox (already derived from CListBox) using CWnd::CreateEx ? PinmemberMojo Jojo17-May-03 18:53 
I wanted to use that MyListBox (already designed in my MFC Project) as a pop-up control, creating it as a result of a mouse-click on a radio button like this:
 
void CDlgBarBand1::OnMyRadioBtn()
{
if(mp_myListBox)
return; // don't want to create it twice
 
CRect rcListBox;
...
mp_myListBox = new CMyListBox;
 
// there is a native CListBox::Create member function:
mp_myListBox->Create(WS_POPUP | LBS_STANDARD | LBS_HASSTRINGS, rcListBox, this, IDC_MYLISTBOX);
...
mp_myListBox->ModifyStyle(WS_CHILD | WS_CAPTION, WS_DLGFRAME);
...
mp_myListBox->ShowWindow(SW_SHOWNA);
}

where we have that:
- CDlgBarBand1 is derived from CDialogBar and is the control "owned" by one of the bands of a CRebarCtrl in my App.
 

Actually, it "worked fine", but only because of using the "Release" project's configuration for a while. And when I returned to "Debug" configuration again, surprise ! I got an ASSERT:
- in a Wincore.cpp file:
BOOL CWnd::Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID, CCreateContext* pContext)
{
// can't use for desktop or pop-up windows (use CreateEx instead)
ASSERT(pParentWnd != NULL);
ASSERT((dwStyle & WS_POPUP) == 0); // <<< HERE IS MY ASSERT LINE !!! (my comment)
 
return CreateEx(0, lpszClassName, lpszWindowName,
dwStyle | WS_CHILD,
rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
pParentWnd->GetSafeHwnd(), (HMENU)nID, (LPVOID)pContext);
}

So, CListBox::Create member function calls CWnd::CreateEx(...) with WS_CHILD style, which (they say) cannot stay together with the desired WS_POPUP style.
They say: "use CreateEx instead", but this function requires a LPCTSTR lpszClassName.
What could be the lpszClassName for MyListBox ? I thought it could be "CMyListBox", but I seemed to be wrong.
They also say (documentation):
"The class name can be any name registered with the global AfxRegisterWndClass function or any of the predefined control-class names."
I did not find out how to use AfxRegisterWndClass with MyListBox in order to get the lpszClassName required by CreateEx.
 
Could you help ?
 
- I must also say I was unsuccesfully trying like this:
void CDlgBarBand1::OnMyRadioBtn()
{
...
mp_myListBox = new CMyListBox;
 
mp_myListBox->CreateEx(NULL, "LISTBOX", "LBWndName", WS_POPUP | WS_DLGFRAME | LBS_STANDARD | LBS_HASSTRINGS, rcListBox, this, IDC_MYLISTBOX); // it returned NULL = unsuccesfully
...
}

Unsure | :~ Where is my mistake ?
 
(Actually, I solved it using the Desktop window as parent for the ListBox in CListBox::Create member function, but the "CreateEx alternative" has made me very curious ...)
 

- sorry for I could't find out how to indent and my TABs didn't work.
AnswerRe: How to create a WS_POPUP CMyListBox (already derived from CListBox) using CWnd::CreateEx ? PinmemberJoseph M. Newcomer18-May-03 16:57 
GeneralSelf-Registering Window Class: See "Creating Custom Controls" from Chris Maunder PinmemberMartin Holzherr25-Oct-01 23:17 
GeneralRe: Self-Registering Window Class: See "Creating Custom Controls" from Chris Maunder PinmemberJoseph M. Newcomer26-Oct-01 6:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140827.1 | Last Updated 25 Oct 2001
Article Copyright 2001 by Joseph M. Newcomer
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid