Link to home
Start Free TrialLog in
Avatar of deedge
deedge

asked on

How to create a user-defined control?

I am using 1.52, creating a 16-bit app. I have a CProgressBar class defined as a sub-class of CWnd. When I want to place it in a "Work In Progress" dialog, I have to create it from within the OnCreate of the dialog.

I would like to make it a "user-defined" control, so that I can place it in the dialog in App Studio. Then the Create would be called automatically (not sure how, exactly).  

Here is a snipit of code to help explain what I am doing already:

"progress.h"

class CProgressBar : public CWnd
{
protected:
   COLORREF m_ProgressColor;
   COLORREF m_BackColor;
   COLORREF m_TextColor;
   UINT     m_Current;
   UINT     m_Total;

// Construction
public:
   BOOL PreCreateWindow(CREATESTRUCT& cs);

// Implementation
public:
   BOOL Create (const RECT& rect,
                CWnd* pParentWnd,
                UINT nID,
                COLORREF Background,
                COLORREF Foreground,
                COLORREF Text);
   void UpdateProgress (UINT Current,
                        UINT Total);
   
protected:
   // Generated message map functions
   //{{AFX_MSG(CProgressBar)
   afx_msg void OnPaint();
   //}}AFX_MSG
   DECLARE_MESSAGE_MAP()  
};

------------

"progress.cpp"

BEGIN_MESSAGE_MAP(CProgressBar, CWnd)
   //{{AFX_MSG_MAP(CProgressBar)
   ON_WM_PAINT()
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()

BOOL CProgressBar::PreCreateWindow(CREATESTRUCT& cs)
{
   cs.lpszClass = AfxRegisterWndClass (WS_CHILD |                                        WS_VISIBLE);

   return TRUE;
}

///////////////////////////////////////////////////////////

BOOL CProgressBar::Create(const RECT& rect,
                          CWnd* pParentWnd,
                          UINT nID,
                          COLORREF Background,
                          COLORREF Foreground,
                          COLORREF Text)
{
   if (CWnd::Create ("CProgressBar",
                     "ProgressBar",
                     WS_CHILD | WS_VISIBLE,
                     rect,
                     pParentWnd,
                     nID) == 0)
   {
      return FALSE;
   }


   return TRUE;
}

///////////////////////////////////////////////////////////

void CProgressBar::UpdateProgress (UINT Current,
                                   UINT Total)
{
   // Set the percentage
}

////////////////////////////////////////////////////////

void CProgressBar::OnPaint()
{

 // Draw stuff...

}

--------------

Okay, then if I want to use it:

BOOL CMyDialog::Create(void)
{
   if (!CDialog::Create(CMyDialog::IDD))
   {
      return FALSE;
   }
   
   CRect ProgressRect;
   GetClientRect (&ProgressRect);
   CPoint BottomRight = ProgressRect.BottomRight();
   ProgressRect.SetRect (7, BottomRight.y - 33, BottomRight.x - 7, BottomRight.y - 7);

   if (!m_ProgressBar.Create (ProgressRect,
                              this,
                              IDC_PROGRESS_BAR_MY,
                              RGB(64, 128, 128),
                              RGB(0, 64, 128),
                              RGB(255,128,128)))
   {
      return FALSE;
   }

   return TRUE;
}                    

--------

The key is the PreCreateWindow function which registers the class to windows.  However, I am not sure how to register the class so that a "user-defined" control can use it. When I tried placing "CProgressBar" in for the class of the user defined control, I received a GPF, and the TRACE said that it couldn't find the window... or something like that.

Any help is appreciated.
Avatar of deedge
deedge

ASKER

Edited text of question
If you want to make the control something that you can just drop-in you need to do a little work.  You need to go back to the Windows SDK days a little.  1st you need to register the window giving it a real name, not a pseudo name like MFC generates.
Once that is done you ca go into your dialog editor and add a user control.  The properties of the control have a Class Name entry which you should populate with the same name you used in the window registration.  The style value is a bitmask of the WS flags for the window, you have to manually calculate this.  Remember that if you create a DDX variable for the window you MUST use the class that you used when registering the window class, and not generic CWND or any other class.
Avatar of deedge

ASKER

Sorry Kargo, I think you tried answering my original question, rather than the edited version... could you (or someone) re-answer the question based on the code snipits above.
CProgressBar::CProgressBar()
{
    WNDCLASS    wc;
   
    wc.hCursor          = ::LoadCursor(NULL, IDC_ARROW);
    wc.hIcon            = NULL;
    wc.lpszMenuName     = NULL;
    wc.lpszClassName    = "ProgressBar";
    wc.hbrBackground    = (HBRUSH)(COLOR_WINDOW + 1);
    wc.hInstance        = ::AfxGetInstanceHandle();
    wc.style            = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc      = ::DefWindowProc;
    wc.cbClsExtra       = 0;
    wc.cbWndExtra       = 0;

    ::RegisterClass(&wc);
}


In the dialog editor, create a custom control and specify the class name as "ProgressBar".

IDD_PROGDLG DIALOG PRELOAD FIXED PURE  144, 90, 241, 94
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Progress"
FONT 8, "Helv"
BEGIN
    PUSHBUTTON      "Cancel",IDCANCEL,101,75,41,14
    CONTROL         "",IDC_PROGBAR,"ProgressBar",0x7,8,51,225,14
    LTEXT           "",IDC_STATUS1,8,6,224,8
    LTEXT           "",IDC_STATUS2,8,19,224,8
    LTEXT           "",IDC_STATUS3,8,31,224,8
END


CMyDialog

CProgressBar m_progressbar;  // member of CMyDialog

Remove CProgressBar::PreCreateWindow and CProgressBar::Create. Remove the code for creating CProgressBar in CMyDialog::Create.

You may write a member function of CProgressBar to specify COLORREF Background,  COLORREF Foreground, COLORREF Text.
Avatar of deedge

ASKER

Again, sorry... it doesn't quite work correctly... I am able to register the class and place it with App Studio, and everything seems to work fine, except that my CProgressBar (defined as a subclass of a CWnd) doesn't receive any WM_PAINT messages... which is really wierd, because I ran Spy on the CProgressBar window in the dialog, and Spy reported that it was receiving WM_ERASEBKGRND and WM_PAINT messages all of the time (like I expected).  However, nothing happens (ie: OnPaint never gets called).  So, I tried calling GetClientRect() in the UpdateProgress routine, just to see what it was, and I received an Invalid Window Handle message everytime... I thought that was pretty strange, since the object itself is a subclass of a window, so I am not sure why it doesn't thing it has a handle. On top of that, Spy did think it had a handle, and it reported all of the stats like one would expect.  

It seems like I am not locking down something quite right.  Could you (or someone) give me that final push (or shove, by now).  Here is what I have:

-----------

class CProgressBar : public CWnd
{
protected:
   COLORREF m_ProgressColor;
   COLORREF m_BackColor;
   COLORREF m_TextColor;
   UINT     m_Current;
   UINT     m_Total;

// Construction
public:
   CProgressBar();

// Implementation
public:
   virtual ~CProgressBar();
   void SetColors (COLORREF Background,
                   COLORREF Foreground,
                   COLORREF Text);
   void UpdateProgress (UINT Current,
                        UINT Total);
   
protected:
   // Generated message map functions
   //{{AFX_MSG(CProgressBar)
   afx_msg void OnPaint();
   //}}AFX_MSG
   DECLARE_MESSAGE_MAP()  
};

-------------

// progress.cpp : implementation file
//

CProgressBar::CProgressBar()
{
   m_Current = 0;
   m_Total = 1;

   WNDCLASS wc;
   
   wc.hCursor = ::LoadCursor(NULL, IDC_ARROW);
   wc.hIcon = NULL;
   wc.lpszMenuName = NULL;
   wc.lpszClassName = "ProgressBar";
   wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
   wc.hInstance = ::AfxGetInstanceHandle();
   wc.style = CS_HREDRAW | CS_VREDRAW;
   wc.lpfnWndProc = ::DefWindowProc;
   wc.cbClsExtra = 0;
   wc.cbWndExtra = 0;
   
   ::RegisterClass(&wc);
}

BEGIN_MESSAGE_MAP(CProgressBar, CWnd)
   //{{AFX_MSG_MAP(CProgressBar)
   ON_WM_PAINT()
   //}}AFX_MSG_MAP
END_MESSAGE_MAP()

void CProgressBar::UpdateProgress (UINT Current,
                                   UINT Total)
{
 // update the prgress members...

   Invalidate(FALSE); // This call used to work, but now it
   // causes the whole screen to flicker (like I am invalidating
   // the desktop window or something.
}

void CProgressBar::OnPaint()
{
// This routine used to be called in response to the Invalidate
// or for any other WM_PAINT messages... now it is never called,
// even though 'Spy' says that the window is receiving the
// WM_PAINT messages.

   CPaintDC dc(this); // device context for painting
.
.
.

}

Okay, then the dialog that contains the CProgressBar object doesn't do anything really special.  I declare a member of m_ProgressBar, so the class gets registered on creation, and then I setcolors and updateprogress every once in a while... the .rc snipit is as follows:

IDD_DIALOG_WORK_IN_PROGRESS DIALOG DISCARDABLE  0, 0, 185, 66
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Title"
FONT 8, "MS Sans Serif"
BEGIN
    CTEXT           "Work In Progress... Please Wait",IDC_STATIC_MESSAGE,5,
                    15,175,8,NOT WS_GROUP
    CONTROL         "",IDC_PROGRESS_BAR_WIP,"ProgressBar",0x0,5,45,175,14
END

----------

I have tried changing the various values to match exactly what chensu had put down, but that didn't seem to help at all (not sure what the hex value is after the class name...)

anyway, Spy reported that I had a WS_CHILD window and the coordinates and everything...

Again, any help is much appreciated.
Avatar of deedge

ASKER

Something else I noticed was that if I have multiple dialog objects, I get a runtime trace error indicating that the class has already been registered.  Aside from making two different constructors, is there some way to avoid this problem... the only way I see is :

#ifndef __PBAR__
#define __PBAR__
...constructor1 with the registration...
#else
..constrictor1 without registration...
#endif

Perhaps there is some other "official windows" way to handle this problem.
Try replacing
wndcls.lpfnWndProc = ::DefWindowProc;
with
wndcls.lpfnWndProc = ::AfxWndProc;
Avatar of deedge

ASKER

Nope, after making that change, I received a failed assertion in WinCore (line 210):

LRESULT CALLBACK AFX_EXPORT
AfxWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
   CWnd* pWnd;

   pWnd = CWnd::FromHandlePermanent(hWnd);
   ASSERT(pWnd != NULL);  // <- This is Line 210
   ASSERT(pWnd->m_hWnd == hWnd);

   LRESULT lResult = _AfxCallWndProc(pWnd, hWnd, message, wParam, lParam);

   return lResult;
}

While looking around in Wincore, I tried cut-n-pasting the body of AfxRegisterWndClass, because that is pretty much the same thing, but met with the same result. Any other ideas???  
Let MFC subclass the new control:
void CMyDialog::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CMyDialog)
DDX_Control(pDX, IDC_PROGRESS_BAR_WIP, m_ProgressBar);
//}}AFX_DATA_MAP
}


ASKER CERTIFIED SOLUTION
Avatar of agopal
agopal

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of deedge

ASKER

Yeah, I already saw that sample code... it may work, but I finally gave up on trying to do this, because (for what I am trying to do) it is much easier (and less code) to do what I was doing to begin with.  However, for anyone else who wants the answer to this question, I think that looking at the sample code mentioned is the correct answer.