• Welcome to Powerbasic Museum 2020-B.
 

News:

Forum in repository mode. No new members allowed.

Main Menu

ProgEx41: Class Frameworks

Started by Frederick J. Harris, October 06, 2011, 08:15:00 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

Making Real C++ Objects Out Of Poor Windows Api C Objects
(Otherwise Known As Windows Class Frameworks)

     Yes, it can obviously be done.   Let's start with this bare bones Win32 Api based template we'll call Form3, and we'll wrap it all up in classes...


//Main.cpp            Compiles to 7192 bytes with Code::Blocks 10.05 using MinGW
#include <windows.h>
#include <tchar.h>

LRESULT CALLBACK fnWndProc(HWND hwnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
if(msg==WM_DESTROY)
{
    PostQuitMessage(0);
    return 0;
}

return (DefWindowProc(hwnd, msg, wParam, lParam));
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int iShow)
{
TCHAR szClassName[]=_T("Form3");
WNDCLASSEX wc;
MSG messages;
HWND hWnd;

wc.lpszClassName  =  szClassName;
wc.lpfnWndProc    =  fnWndProc;
wc.cbSize         =  sizeof (WNDCLASSEX);
wc.style          =  0;
wc.hIcon          =  LoadIcon(NULL,IDI_APPLICATION);
wc.hInstance      =  hInstance;
wc.hIconSm        =  0;
wc.hCursor        =  LoadCursor(NULL,IDC_ARROW);
wc.hbrBackground  =  (HBRUSH)COLOR_BTNSHADOW;
wc.cbWndExtra     =  0;
wc.cbClsExtra     =  0;
wc.lpszMenuName   =  NULL;
RegisterClassEx(&wc),
hWnd=CreateWindowEx(0,szClassName,szClassName,WS_OVERLAPPEDWINDOW,350,250,450,300,HWND_DESKTOP,0,hInstance,0);
ShowWindow(hWnd,iShow);
while(GetMessage(&messages,NULL,0,0))
{
    TranslateMessage(&messages);
    DispatchMessage(&messages);
}

return messages.wParam;
}


     We can start with the WNDCLASSEX declaration in WinMain() and create a class we'll call CWinClass...


//CWinClass.h
#ifndef CWinClass_h
#define CWinClass_h

class CWinClass
{
public:
CWinClass(){}
CWinClass(WNDPROC fnWndProc, LPCTSTR szClassName, int cbClsExtra,  int cbWndExtra, HBRUSH hBackColor, HINSTANCE hInst);

private:
WNDCLASSEX wc;
};
#endif


     In our constructor we'll pass in a WNDPROC typed pointer to the window procedure for the class, the class name, cbClsExtra and cbWndExtra bytes, the background color we want, and the instance handle.  Pretty basic stuff in Windows Api coding.  Our implementation file will look like this then...


//CWinClass.cpp
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <tchar.h>
#include "CWinClass.h"

CWinClass::CWinClass(WNDPROC fnWndProc, LPCTSTR szClassName, int cbClsExtra,  int cbWndExtra, HBRUSH hBackColor, HINSTANCE hInst)
{
wc.cbSize        = sizeof(WNDCLASSEX);
wc.style         = 0;
wc.lpfnWndProc   = fnWndProc;
wc.cbClsExtra    = cbClsExtra;
wc.cbWndExtra    = cbWndExtra;
wc.hInstance     = hInst;
wc.hIcon         = LoadIcon(NULL,IDI_APPLICATION);
wc.hIconSm       = 0;
wc.hCursor       = LoadCursor (0, IDC_ARROW);
wc.hbrBackground = hBackColor;
wc.lpszMenuName  = 0;
wc.lpszClassName = szClassName;
RegisterClassEx(&wc);
}


     In looking at the basic SDK skeleton Form3 above we see a CreateWindow() call, a ShowWindow() call, and the message loop.  We can wrap that too.  Let's make a CWindow class for that...


//CWindow.h
#ifndef CWindow_h
#define CWindow_h

class CWindow
{
public:
CWindow(){}
CWindow(int iShow, LPCTSTR szClassName, LPCTSTR szCaption, DWORD dwStyle, int x, int y, int iWidth, int iHeight, HWND hParent, HMENU hMenu, HINSTANCE hIns, void* lpCreateParams);
HWND Window(void)  {return this->m_hWnd;}
WPARAM Run(void);
virtual ~CWindow(void){}

protected:
HINSTANCE     m_hInst;
HWND          m_hWnd;
};
#endif


...and the implementation file...


//CWindow.cpp
#define UNICODE
#define _UNICODE
#include <windows.h>
#include <tchar.h>
#include "CWindow.h"


CWindow::CWindow(int iShow, LPCTSTR szClassName, LPCTSTR szCaption, DWORD dwStyle, int x, int y, int iWidth, int iHeight, HWND hParent, HMENU hMenu, HINSTANCE hIns, void* lpCreateParams)
{
this->m_hWnd=CreateWindowEx(0,szClassName,szCaption,dwStyle,x,y,iWidth,iHeight,hParent,(HMENU)hMenu,hIns,lpCreateParams);
this->m_hInst=hIns;
ShowWindow(this->m_hWnd,iShow);
UpdateWindow(this->m_hWnd);
}


WPARAM CWindow::Run(void)
{
MSG msg;

while(GetMessage(&msg,NULL,0,0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

return msg.wParam;
}


     Note that our CWindow constructor wraps CreateWindow(), ShowWindow(), and UpdateWindow() calls.  Our CWindow::Run() method wraps GetMessage(), TranslateMessage(), and DispatchMessage() calls.  Having done all that and wanting to exercise our new C++ class framework all that's left of our Main.cpp file to get the whole thing started is this...


//Main.cpp
#define  UNICODE
#define _UNICODE
#include <windows.h>
#include <tchar.h>
#include "CWinClass.h"
#include "CWindow.h"

LRESULT CALLBACK fnWndProc(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
if(msg==WM_DESTROY)
{
    PostQuitMessage(0);
    return 0;
}

return (DefWindowProc(hWnd, msg, wParam, lParam));
}

int WINAPI WinMain(HINSTANCE hIns, HINSTANCE hPrevIns, LPSTR lpszArgument, int iShow)
{
CWinClass wc(fnWndProc,_T("Form3"),0,4,(HBRUSH)GetStockObject(WHITE_BRUSH),hIns);
CWindow app(iShow,_T("Form3"),_T("Form3"),WS_OVERLAPPEDWINDOW,200,200,330,135,0,(HMENU)0,hIns,0);
return app.Run();
}


     So there you have it.  WinMain() now only has three lines of code in it!  Pretty neat huh!  Why don't you put together for yourself a project and try it?  Create files out of the above five code listings and name them as indicated.  Then include them in a project of whatever name you like.

     Unfortunately, there were costs in doing this though.  Instead of a 7192 byte executable I'm seeing 20,480 bytes with my MinGW compile.  And of course, the original Form3 program was all in one code file comprising about 41 lines of code, but now we've got no less than five files with a total code line count of about 107.  So we've more than doubled the line count and almost tripled the executable program size as the cost incurred of having a 24 line main program file......



Frederick J. Harris

#1
     Putting those drawbacks aside for the moment let's continue and see if we can extend this program to something that does more than nothing at all – which is about all that the above program does.  Let's add a textbox and a button.  When we type something in the textbox and click the button, we'll extract into a message box whatever we typed in the textbox.  Here is a basic SDK C style example of what we'll convert.  Please compile and run this so you get the idea (you need to compile as C++ though – not C)...


//Main.cpp           //  8192 bytes : No String Class, i.e., using C style null terminated char buffers
#define UNICODE      // 60928 bytes : C++ String Class, i.e., when using std::string or std::wstring
#define _UNICODE
#include <windows.h>
#include <tchar.h>
#include <string>
#ifdef UNICODE
   #define String std::wstring
#else
   #define String std::string
#endif
#define  IDC_TEXT     1500
#define  IDC_BUTTON   1505

LRESULT CALLBACK fnWndProc(HWND hwnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
   case WM_CREATE:
     {
        HWND hCtl;
        HINSTANCE hIns=((LPCREATESTRUCT)lParam)->hInstance;
        hCtl=CreateWindowEx(WS_EX_CLIENTEDGE,_T("edit"),_T(""),WS_CHILD|WS_VISIBLE,15,15,285,22,hwnd,(HMENU)IDC_TEXT,hIns,0);
        hCtl=CreateWindowEx(0,_T("button"),_T("Get Text"),WS_CHILD|WS_VISIBLE,115,55,80,30,hwnd,(HMENU)IDC_BUTTON,hIns,0);
        return 0L;
     }
   case WM_COMMAND:
     {
        if(LOWORD(wParam)==IDC_BUTTON)
        {
           TCHAR szBuffer[256];
           GetWindowText(GetDlgItem(hwnd,IDC_TEXT),szBuffer,256);
           String strText=szBuffer;                                                 //use if using std::string or std::wstring
           MessageBox(hwnd,strText.c_str(),_T("Here Is What You Typed..."),MB_OK);  //for text box output
           //MessageBox(hwnd,szBuffer,_T("Here Is What You Typed..."),MB_OK);       //use instead if using null terminated buffer
        }
        return 0L;
     }
   case WM_DESTROY:
     {
        PostQuitMessage(0);
        return 0;
     }
}

return (DefWindowProc(hwnd, msg, wParam, lParam));
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int iShow)
{
TCHAR szClassName[]=_T("Form4");
WNDCLASSEX wc;
MSG messages;
HWND hWnd;

wc.lpszClassName  =  szClassName;
wc.lpfnWndProc    =  fnWndProc;
wc.cbSize         =  sizeof (WNDCLASSEX);
wc.style          =  0;
wc.hIcon          =  LoadIcon(NULL,IDI_APPLICATION);
wc.hInstance      =  hInstance;
wc.hIconSm        =  0;
wc.hCursor        =  LoadCursor(NULL,IDC_ARROW);
wc.hbrBackground  =  (HBRUSH)COLOR_BTNSHADOW;
wc.cbWndExtra     =  0;
wc.cbClsExtra     =  0;
wc.lpszMenuName   =  NULL;
RegisterClassEx(&wc),
hWnd=CreateWindowEx(0,szClassName,szClassName,WS_OVERLAPPEDWINDOW,200,200,330,135,HWND_DESKTOP,0,hInstance,0);
ShowWindow(hWnd,iShow);
while(GetMessage(&messages,NULL,0,0))
{
    TranslateMessage(&messages);
    DispatchMessage(&messages);
}

return messages.wParam;
}
 

     At this point the above program shouldn't be tool challenging, but converting it to our new class framework orientation will show how to include child window controls.  The best way to do it is to use C++ class inheritance.  Let's start by wrapping a 'button' control in a class...


//CButton.h             
#ifndef CButton_h
#define CButton_h
#include <windows.h>

class CButton
{
public:
CButton(LPCTSTR lpCaption, int x, int y, int cx, int cy, HWND hParent, int iCtrlId, HINSTANCE hIns); 
~CButton(){}

private:
HWND m_hBtn;
};
#endif


     And here is the implementation...


//CButton.cpp         
#ifndef UNICODE
#define UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <tchar.h>
#include "CButton.h"

CButton::CButton(LPCTSTR lpCaption, int x, int y, int cx, int cy, HWND hParent, int iCtrlId, HINSTANCE hIns) //CButton Constructor
{
this->m_hBtn=CreateWindowEx(0,_T("button"),lpCaption,WS_CHILD|WS_VISIBLE,x,y,cx,cy,hParent,(HMENU)iCtrlId,hIns,0);
}


     The only method is the CButton constructor which wraps a CreateWindow() call for a button child window control.  The only data member is the window handle of the created control.  The CEdit Class, which wraps an edit control, is just a bit more complicated, because we'll include helper members which wrap GetWindowText() / SetWindowText() Api calls to get/set the text of the control...


//CEdit.h
#ifndef CEdit_h
#define CEdit_h

class CEdit    //CEdit
{
public:
CEdit(LPTSTR lpCaption, int x, int y, int cx, int cy, HWND hParent, int iCtrlId, HINSTANCE hIns);
void SetText(TCHAR* lpszText);
void GetText(LPTSTR lpszBuffer, int iLenBuf);
~CEdit(){}   //CEdit Destructor

private:
HWND m_hEdit;
};
#endif


And here is the implementation cpp file...


//CEdit.cpp   23 lines of code
#ifndef UNICODE
#define UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <tchar.h>
#include "CEdit.h"

CEdit::CEdit(LPTSTR lpCaption, int x, int y, int cx, int cy, HWND hParent, int iCtrlId, HINSTANCE hIns)  //CEdit Constructor
{
this->m_hEdit=CreateWindowEx(WS_EX_CLIENTEDGE,_T("edit"),lpCaption,WS_CHILD|WS_VISIBLE,x,y,cx,cy,hParent,(HMENU)iCtrlId,hIns,0);
}

void CEdit::SetText(LPTSTR lpszText)
{
SetWindowText(this->m_hEdit,lpszText);
}

void CEdit::GetText(LPTSTR lpszBuffer, int iLenBuf)
{
GetWindowText(this->m_hEdit,lpszBuffer,iLenBuf);
}
 
     
     Now comes the big change, so hang on tight!  Here is a new Class we'll name Cform5 and it will inherit all the functionality of our last CWindow class.  I'll discuss this to death, because there is a lot going on here...


//CForm5.h     
#ifndef CForm5_h
#define CForm5_h
#include "CButton.h"
#include "CEdit.h"
#define  IDC_TEXT     1500
#define  IDC_BUTTON   1505

class CForm5 : public CWindow      //It makes sense to inherit from a base class the types of
{                                  //functionality (member functions) and data members that
public:                           //will be common to all objects such as the necessity for
CForm5(){}                        //a CreateWindow() call to create a main program window,
                                   //and a HWND for the main program window.  The things that
CForm5                            //will make the application or window unique though can go
(                                 //in a derived class such as this one.  For example, in the
  int iShow,                       //CForm5 Class we'll want a CButton member and a CEdit
  TCHAR const* szClassName,        //member.  Also, we can include here the message handling
  TCHAR const* szCaption,          //functions for the class if we like.
  DWORD dwStyle,
  int x,
  int y,
  int nWidth,
  int nHeight,
  HWND hParent,
  HMENU hMenu,
  HINSTANCE hIns,
  void* lpCreateParams
);
static long OnCommand(HWND hWnd, WPARAM wParam, LPARAM lParam);
static long OnDestroy(HWND hWnd);
virtual ~CForm5(void){}

protected:
int             m_nCmdShow;
private:
CButton*        m_pbtn;            //This class member wraps a Win32 "button" object
CEdit*          m_ptxt;            //This class member wraps a Win32 "edit" object
};
#endif


     And here is the implementation file...


//CForm5.cpp               
#ifndef UNICODE
#define UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <tchar.h>
#include <cstdio>
#include "CWindow.h"
#include "CForm5.h"

CForm5::CForm5              //This is the constructor for our CForm5 class.  All the parameters
(                           //of a CreateWindow() call are passed in, plus the iShow parameter
  int iShow,                //from WinMain.  Note in this implementation file in this CForm5
  LPCTSTR szClassName,      //constructor we have a 'Constructor Initialization List' comprising
  LPCTSTR szCaption,        //of only one element and that is a call on the CWindow() constructor
  DWORD dwStyle,            //which initializes the 'base' part of a CForm2 object.  All the
  int x,                    //parameters of the CForm5 constructor are simply passed in as is.
  int y,                    //After the CWindow() constructor returns and the body of the CForm5
  int nWidth,               //constructor is entered, the most noteworthy thing that happens is
  int nHeight,              //that a pointer to the object, i.e., this, is stored at offset zero
  HWND hParent,             //in the already created .cbWndExtra bytes.
  HMENU hMenu,
  HINSTANCE hIns,
  void* lpCreateParams
):CWindow(iShow,szClassName,szCaption,dwStyle,x,y,nWidth,nHeight,hParent,hMenu,hIns,lpCreateParams)
{
  SetWindowLong(this->m_hWnd,0,(long)this);
  this->m_nCmdShow=iShow;
  this->m_pbtn=new CButton((TCHAR*)_T("Get Text"),115,55,80,30,this->m_hWnd,IDC_BUTTON,this->m_hInst);
  this->m_ptxt=new CEdit((TCHAR*)_T(""),15,15,285,22,this->m_hWnd,IDC_TEXT,this->m_hInst);
}

long CForm5::OnCommand(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
CForm5* pForm5=NULL;

pForm5=(CForm5*)GetWindowLong(hWnd,0);
switch(LOWORD(wParam))
{
  case IDC_BUTTON:
    {
       TCHAR szBuffer[256];
       pForm5->m_ptxt->GetText(szBuffer,256);
       MessageBox(hWnd,szBuffer,_T("Here Is Your Text"),MB_OK);
    }
    break;
}

return 0L;
}

long CForm5::OnDestroy(HWND hWnd)
{
CForm5* pForm5=(CForm5*)GetWindowLong(hWnd,0);
delete pForm5->m_pbtn;
delete pForm5->m_ptxt;
PostQuitMessage(0);

return 0L;
}


     Here are CWindow.h and CWindow.cpp again...     


//CWindow.h       
#ifndef CWindow_h
#define CWindow_h

class CWindow
{
public:
CWindow(){}
CWindow(int iShow, LPCTSTR szClassName, LPCTSTR szCaption, DWORD dwStyle, int x, int y, int iWidth, int iHeight, HWND hParent, HMENU hMenu, HINSTANCE hIns, void* lpCreateParams);
HWND Window(void)  {return this->m_hWnd;}
WPARAM Run(void);
virtual ~CWindow(){}

protected:
HINSTANCE     m_hInst;
HWND          m_hWnd;
};
#endif



//CWindow.cpp   
#ifndef UNICODE
#define UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <tchar.h>
#include <cstdio>
#include "CWindow.h"

CWindow::CWindow(int iShow, LPCTSTR szClassName, LPCTSTR szCaption, DWORD dwStyle, int x, int y, int iWidth, int iHeight, HWND hParent, HMENU hMenu, HINSTANCE hIns, void* lpCreateParams)
{
this->m_hWnd=CreateWindowEx(0,szClassName,szCaption,dwStyle,x,y,iWidth,iHeight,hParent,(HMENU)hMenu,hIns,lpCreateParams);
this->m_hInst=hIns;
ShowWindow(this->m_hWnd,iShow);
UpdateWindow(this->m_hWnd);
}

WPARAM CWindow::Run(void)
{
MSG msg;

while(GetMessage(&msg,NULL,0,0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

return msg.wParam;
}


Here are CWinClass.h And CWinClass.cpp


//CWinClass.h     
#ifndef CWinClass_h
#define CWinClass_h

class CWinClass
{
public:
CWinClass(){}
CWinClass(WNDPROC fnWndProc, LPCTSTR szClassName, int cbClsExtra,  int cbWndExtra, HBRUSH hBackColor, HINSTANCE hInst);

private:
WNDCLASSEX wc;
};
#endif



//CWinClass.cpp     
#ifndef UNICODE
#define UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <tchar.h>
#include "CWinClass.h"

CWinClass::CWinClass(WNDPROC fnWndProc, LPCTSTR szClassName, int cbClsExtra,  int cbWndExtra, HBRUSH hBackColor, HINSTANCE hInst)
{
wc.cbSize        = sizeof(WNDCLASSEX);
wc.style         = 0;
wc.lpfnWndProc   = fnWndProc;
wc.cbClsExtra    = cbClsExtra;
wc.cbWndExtra    = cbWndExtra;
wc.hInstance     = hInst;
wc.hIcon         = LoadIcon(NULL,IDI_APPLICATION);
wc.hIconSm       = 0;
wc.hCursor       = LoadCursor (0, IDC_ARROW);
wc.hbrBackground = hBackColor;
wc.lpszMenuName  = 0;
wc.lpszClassName = szClassName;
RegisterClassEx(&wc);
}


Finally, here is our Main.cpp file which contains our WndProc() and the WinMain() program entry point... 


//Main.cpp
#ifndef UNICODE
#define UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <tchar.h>
#include "CWinClass.h"
#include "CButton.h"
#include "CEdit.h"
#include "CWindow.h"
#include "CForm5.h"

LRESULT CALLBACK Form5_WndProc(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
   case WM_COMMAND:
     return CForm5::OnCommand(hWnd,wParam,lParam);
   case WM_DESTROY:
     return CForm5::OnDestroy(hWnd);
}

return (DefWindowProc(hWnd, msg, wParam, lParam));
}

int WINAPI WinMain(HINSTANCE hIns, HINSTANCE hPrevIns, LPSTR lpszArgument, int iShow)
{
CWinClass wc(Form5_WndProc,_T("Form5"),0,4,(HBRUSH)COLOR_BTNSHADOW,hIns);
CForm5 app(iShow,_T("Form5"),_T("Form5"),WS_OVERLAPPEDWINDOW,200,200,330,135,0,(HMENU)0,hIns,0);
return app.Run();
}


     Just so we're all on the same page, the files you'll need to amalgamate together to create this project are as follows...


CButton.h          //Header for CButton Class
CEdit.h            //Header for CEdit Class
CWinClass.h        //Header for CwinClass      - Registers A Custom Window Class
CWindow.h          //Header for CWindow Class  - Base Class Functionality For Creating A Window
CForm5.h           //Header for Main App Class – Customizations Of CWindow Class Specific To This App

CButton.cpp        //Implementation Of CButton Class
CEdit.cpp          //Implementation Of CEdit Class
CWinClass.cpp      //Implementation Of CWinClass
CWindow.cpp        //Implementation Of CWindow Class
CForm5.cpp         //Implementation Of CForm5 Class
Main.cpp           //Main Start Up Code


     I'll attach these files to this post, but the attached files will be modified somewhat from those just shown above.  Let me elaborate.  Whenever I'm developing code I always log debug statements to an output debug log file.  The attached files will contain these debug statements.  For anyone who is not a member of this forum and won't be able to access the attachments, I'll briefly describe what I do.  First I define a debug symbol such as this...

#define MYDEBUG

Then near the top of my start up file I'll declare a FILE* struct like so...

#ifdef MYDEBUG
FILE* fp=NULL;
#endif

Finally, I'll open the file in WinMain or the Window Procedure and execute output statements.  Here is WinMain() in the attached Main.cpp file...


//Main.cpp
#ifndef UNICODE
#define UNICODE
#define _UNICODE
#endif
#define MYDEBUG             // <<< define debug symbol
#include <windows.h>
#include <tchar.h>
#include <cstdio>
#include "CWinClass.h"
#include "CButton.h"
#include "CEdit.h"
#include "CWindow.h"
#include "CForm5.h"
#ifdef MYDEBUG
FILE* fp=NULL;
#endif

LRESULT CALLBACK Form5_WndProc(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
   case WM_COMMAND:
     return CForm5::OnCommand(hWnd,wParam,lParam);
   case WM_DESTROY:
     return CForm5::OnDestroy();
}

return (DefWindowProc(hWnd, msg, wParam, lParam));
}

int WINAPI WinMain(HINSTANCE hIns, HINSTANCE hPrevIns, LPSTR lpszArgument, int iShow)
{
#ifdef MYDEBUG
    fp=fopen("Output.txt","w");
    fprintf(fp,"Entering WinMain()\n");
    fprintf(fp,"  hIns = %u\n\n",(unsigned)hIns);
#endif
CWinClass wc(Form5_WndProc,_T("Form5"),0,4,(HBRUSH)COLOR_BTNSHADOW,hIns);
CForm5* pApp=new CForm5(iShow,_T("Form5"),_T("Form5"),WS_OVERLAPPEDWINDOW,200,200,330,135,0,(HMENU)0,hIns,0);
#ifdef MYDEBUG
    fprintf(fp,"  pApp = %u\n\n",(unsigned)pApp);
#endif
pApp->Run();
delete pApp;
#ifdef MYDEBUG
    fprintf(fp,"Leaving WinMain()\n");
    fclose(fp);
#endif

return 0;
}



Frederick J. Harris

continued....

    The reason I didn't post that code above is I believe the debug statements make the code ugly and harder to read.  I kind of look at it as a necessary evil though for getting the code working properly.  Just wanted to give you a 'heads up' on that because I want to continue my explanation of the code through these debug outputs.  Here is what a run of the above program produces... 

     

Entering WinMain()
  hIns = 4194304

  Entering CWindow Constructor!
    this          = 4072496
    this->m_hInst = 4194304
    this->m_hWnd  = 6161310
  Leaving CWindow Constructor!

  Entering CForm5 Constructor!
    this          = 4072496
    this->m_hInst = 4194304
    this->m_hWnd  = 6161310
  Leaving CForm5 Constructor!

  pApp = 4072496

  Executing CForm5 Destructor!
  Executed CWindow Destructor!
Leaving WinMain()


     There is one slight difference between the WinMain() of the debug version and the original version without the debug statements.  In the debug version I used a pointer to a CForm5 object instead of a locally allocated CForm5 object (app in the 1st example).  I did this so I could pick up both destructor calls in my debug log file (a call of delete triggers it).  If the app object is locally allocated in WinMain() the destructors on it won't be called until after WinMain() exits and compiler generated code cleans up the stack.  That's too late to show up in my log file!  An alternate way of handling it would be to create a 'dummy' WinMain() and pass all WinMain()'s parameters to it and call it from WinMain() so it can exit and cause a call of the destructors.  But I didn't do that here.

     OK, now I believe we're in a good position to begin explaining all this.  If you have the fundamentals of C++ Classes down pat it should be fairly simple!

     First note that the 2nd statement in WinMain() is what triggered the 'Entering CWindow Constructor!' debug output.  This...

CForm5 app(iShow,_T("Form5"),_T("Form5"),WS_OVERLAPPEDWINDOW,200,200,330,135,0,(HMENU)0,hIns,0);

...is a constructor call for a CForm5 object named app which simply 'passes in' all the needed information for the CreateWindow() call to create a main program window.  Before we see the debug output resulting from the CForm5 constructor call, which is actually generated within the body of the CForm5 constructor, we first see that the CWindow constructor is called.  CForm5 inherits everything publicly from CWindow, so the CWindow part of CForm5 must be constructed first.

     Note throughout the debug output file we see the important number 4,072,496 (your numbers will naturally be different).  That is the address of the CWindow object, the CForm5 object, and the app object declared in WinMain().  Once the CWindow constructor finishes constructing the base part of the object and program execution finally enters the body of the CForm5 constructor, the very first thing done is to store the 'this' pointer in the WNDCLASSES::cbWndExtra bytes so as to form an actual association between the C++ CForm5 object and the Microsoft Windows C / HWND based object.  Recall from ProgEx38c where we created four main program windows in WinMain().  If that were done here we would have a separate C++ CForm5 object instantiated and associated with each Windows HWND object. 

     The final job of the CForm5 constructor is to finish putting together the visual interface which needs to consist of our text box for entering data and our button object to click when we want to retrieve the data.  These sub-windows or child window controls are objects in themselves and are represented in our CForm5 class in the form of member objects m_ptxt and m_pbtn.  We can use the new operator to force constructor calls for these objects, and of course they will be for the current instance represented by 'this'...


this->m_pbtn=new CButton((TCHAR*)_T("Get Text"),115,55,80,30,this->m_hWnd,IDC_BUTTON,this->m_hInst);
this->m_ptxt=new CEdit((TCHAR*)_T(""),15,15,285,22,this->m_hWnd,IDC_TEXT,this->m_hInst);


     Finally note that in CForm5.h I included the message handling functions right in the CForm5 class in the form of static member functions.  Except for a WM_CREATE handler, it would have been possible to make them ordinary member functions, but there would have been a slight performance penalty involved in that, as it would have then been necessary to retrieve the object pointer from the .cbWndExtra bytes for every call on a message handling function.  And static member functions don't need a this pointer pushed on the stack either.  But if you wanted to do that it would look like this...
 

LRESULT CALLBACK Form5_WndProc(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
CForm5* pObj=(CForm5*)GetWindowLong(hWnd,0);      //Retrieve pointer to object from Window Class Structure
switch(msg)
{
   case WM_CREATE:
     return CForm5::OnCreate(hWnd,lParam);         //It won't be there yet for this call!!!!!
   case WM_COMMAND:
     return pObj->OnCommand(hWnd,wParam,lParam);   //But for these it will be!!!!
   case WM_DESTROY:
     return pObj->OnDestroy();                     //Ditto!
}

return (DefWindowProc(hWnd, msg, wParam, lParam));
}


      These message handling functions didn't need to be made a part of the class, but it made sense to do so – they are logically a part of a CForm5 object. 



Frederick J. Harris

#3
Pros And Cons Of Using Class Frameworks To Wrap The Windows API

     Now comes the fun part and I suspect I'm going to make some enemies here.  My intention though is to make as honest of a comparison as I am able to do, as based on my knowledge and experience.  Let's compare Form4 as presented at the top of this post and reproduced again just below, to Form5 which is our class framework adaptation of Form4 using classes, inheritance, and so forth.   As can be seen below, this simple program that just consists of a main program window with a button and a text box on it compiles to 8192 bytes using C style null terminated character buffers or 60928 bytes if the C++ Standard Library String class is used.  Those numbers would be for a wide character build.  In terms of lines of code it looks like about 78, and everything is in one code file.


//Main.cpp                       //       Form4 : Pure SDK Style Procedural Coding     
#define UNICODE                 
#define _UNICODE                 
#include <windows.h>             //  8192 bytes : No String Class, i.e., using C style null terminated char buffers
#include <tchar.h>               // 60928 bytes : C++ String Class, i.e., when using std::string or std::wstring
#include <string>                //    78 lines of code
#ifdef UNICODE
   #define String std::wstring
#else
   #define String std::string
#endif
#define  IDC_TEXT     1500
#define  IDC_BUTTON   1505

LRESULT CALLBACK fnWndProc(HWND hwnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
   case WM_CREATE:
     {
        HWND hCtl;
        HINSTANCE hIns=((LPCREATESTRUCT)lParam)->hInstance;
        hCtl=CreateWindowEx(WS_EX_CLIENTEDGE,_T("edit"),_T(""),WS_CHILD|WS_VISIBLE,15,15,285,22,hwnd,(HMENU)IDC_TEXT,hIns,0);
        hCtl=CreateWindowEx(0,_T("button"),_T("Get Text"),WS_CHILD|WS_VISIBLE,115,55,80,30,hwnd,(HMENU)IDC_BUTTON,hIns,0);
        return 0L;
     }
   case WM_COMMAND:
     {
        if(LOWORD(wParam)==IDC_BUTTON)
        {
           TCHAR szBuffer[256];
           GetWindowText(GetDlgItem(hwnd,IDC_TEXT),szBuffer,256);
           String strText=szBuffer;                                                 //use if using std::string or std::wstring
           MessageBox(hwnd,strText.c_str(),_T("Here Is What You Typed..."),MB_OK);  //for text box output
           //MessageBox(hwnd,szBuffer,_T("Here Is What You Typed..."),MB_OK);       //use instead if using null terminated buffer
        }
        return 0L;
     }
   case WM_DESTROY:
     {
        PostQuitMessage(0);
        return 0;
     }
}

return (DefWindowProc(hwnd, msg, wParam, lParam));
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int iShow)
{
TCHAR szClassName[]=_T("Form4");
WNDCLASSEX wc;
MSG messages;
HWND hWnd;

wc.lpszClassName  =  szClassName;
wc.lpfnWndProc    =  fnWndProc;
wc.cbSize         =  sizeof (WNDCLASSEX);
wc.style          =  0;
wc.hIcon          =  LoadIcon(NULL,IDI_APPLICATION);
wc.hInstance      =  hInstance;
wc.hIconSm        =  0;
wc.hCursor        =  LoadCursor(NULL,IDC_ARROW);
wc.hbrBackground  =  (HBRUSH)COLOR_BTNSHADOW;
wc.cbWndExtra     =  0;
wc.cbClsExtra     =  0;
wc.lpszMenuName   =  NULL;
RegisterClassEx(&wc),
hWnd=CreateWindowEx(0,szClassName,szClassName,WS_OVERLAPPEDWINDOW,200,200,330,135,HWND_DESKTOP,0,hInstance,0);
ShowWindow(hWnd,iShow);
while(GetMessage(&messages,NULL,0,0))
{
    TranslateMessage(&messages);
    DispatchMessage(&messages);
}

return messages.wParam;
}


     If we compare this exact program with our Form5 adaptation using C++ classes for window objects we come up with a 24576 byte executable resulting from 280 lines of code distributed over 11 source code files as follows...


File           Lines
==================
CButton.h      15 
CEdit.h        16 
CWinClass.h    14 
CWindow.h      18 
CForm5.h       39

CButton.cpp    13
CEdit.cpp      23
CWinClass.cpp  25
CWindow.cpp    29
CForm5.cpp     56
Main.cpp       32
===================
              280


Here is a table for easier comparison...


Program  Uses C++ Classes    Executable Size   Lines Of Code   Source Code Files
================================================================================
Form4          No                   8192             78                1
Form5         Yes                  24576            280               11



     For the above determinations I used the MinGW compiler as distributed with the Code::Blocks 10.05 release.  The build options were set so as to strip debugging symbols from the executable and to minimize code size.  Of course, if all one were interested in would be the numbers of lines of code in Main.cpp, then the Main.cpp source for the Form5 class framework code looks shorter and easier.  Here would be that file without any debug conditionals for easy comparison with Form4 SDK style above...


//Main.cpp 24576 bytes
#define UNICODE                 
#define _UNICODE
#include <windows.h>
#include <tchar.h>
#include "CWinClass.h"
#include "CButton.h"
#include "CEdit.h"
#include "CWindow.h"
#include "CForm5.h"

LRESULT CALLBACK Form5_WndProc(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
   case WM_COMMAND:
     return CForm5::OnCommand(hWnd,wParam,lParam);
   case WM_DESTROY:
     return CForm5::OnDestroy();
}

return (DefWindowProc(hWnd, msg, wParam, lParam));
}

int WINAPI WinMain(HINSTANCE hIns, HINSTANCE hPrevIns, LPSTR lpszArgument, int iShow)
{
CWinClass wc(Form5_WndProc,_T("Form5"),0,4,(HBRUSH)COLOR_BTNSHADOW,hIns);
CForm5 app(iShow,_T("Form5"),_T("Form5"),WS_OVERLAPPEDWINDOW,200,200,330,135,0,(HMENU)0,hIns,&app);
return app.Run();
}


     But what's actually causing a threefold increase in size?  Right off the bat let me state that any C++ program using classes is going to have quite a lot of support code produced by the compiler that is necessary to support the abstraction of classes.  For example, there is the issue of creating virtual function tables and other such structures necessary to support the incorporation of methods into structures and classes, and to support polymorphism and inheritance.  But in our case above that isn't going to constitute or explain all of it, i.e., the difference between 8192 bytes for one program and 24576 for the other.  A fair amount of the rest of it can be pretty easily explained by simple duplication of effort.  Lets take a detailed look at what happens when the button is clicked to retrieve the text out of the textbox and display it in a MessageBox.  That will pretty much tell the story.  Here is the applicable code from Form4...


case WM_COMMAND:
{
     if(LOWORD(wParam)==IDC_BUTTON)
     {
        TCHAR szBuffer[256];
        GetWindowText(GetDlgItem(hwnd,IDC_TEXT),szBuffer,256);
        MessageBox(hwnd,szBuffer,_T("Here Is What You Typed..."),MB_OK);
     }
     return 0L;
}


And here is the applicable code from Form5 for comparison.


long CForm5::OnCommand(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
CForm5* pForm5=NULL;

pForm5=(CForm5*)GetWindowLong(hWnd,0);
switch(LOWORD(wParam))
{
  case IDC_BUTTON:
    {
       TCHAR szBuffer[256];
       pForm5->m_ptxt->GetText(szBuffer,256);
       MessageBox(hWnd,szBuffer,_T("Here Is Your Text"),MB_OK);
    }
    break;
}

return 0L;
}


     First off, note that when Windows creates any child window control such as the button and textbox we used here, it internally stores for us (it needs it too, no doubt!) the window handles of the controls associated with their control ids.  In the Form4 SDK example we simply asked Windows for the handle it had stored for us through a GetDlgItem() call within the GetWindowText() function call.  We already had the parent window handle through the hwnd parameter of the Window Procedure call.  In the Form5 Class Framework example we first retrieve a pointer to the Form5 class object stored in the .cbWndExtra bytes.  The setting up of all that overhead is something we didn't even have to do in the SDK example because there was no CForm5 class or object to store.  After we obtained that we used that pointer to call a method off another class instantiated within the CForm5 class , and that would be the CEdit class and CForm5 member m_ptxt.  And of course there is no corresponding class or object in Form4.   So we have this...

pForm5->m_ptxt->GetText(szBuffer,256);

     So you can clearly see here that we're ignoring all the information Windows easily provides for us and duplicating it within classes of our own construction that do no more than provide duplicate means of accessing the same things Windows has already provided.  Stated alternately, Windows keeps track of all parent – child relationships such as the above, and provides several functions such as GetDlgItem() and GetDlgCtrlID() to retrieve this information at no code size cost to us.  In fact, Windows even provides enumerators which allow us to dump all the child window handles of any given parent we choose.  Therefore it is plain to see that our classes are simply duplicating information Windows will easily provide to us  'free of charge', so to speak.

     OK, you might be thinking, you've made your point about code size.  Its hard to argue with documen-
table facts.  But hard drives are big and cheap.  What about ease of use?  Isn't the Windows Api supposed to be hard to learn?  Aren't classes easier?  Well, all I can say to that is it only took around 70 lines of code in one file to produce our simple little Form4 project, whereas it took about 280 lines of code distributed through 11 code files to produce the same thing using classes!  And to make matters worse, not only did we need to understand the Windows Api, we had to further understand how to integrate it through a further abstraction layer we created on top of it involving class inheritance.  The actual feeling I get personally while I'm trying to think it all through is akin to my attempts years ago to think through and unravel ugly spaghetti code I had written.  To me at least it doesn't 'feel' easier.  To me it 'feels harder.  But maybe that's just me. 

     Having said all that you might be thinking I'm totally against Object Oriented Programming and C++ Classes.  But that isn't true.  In spite of all I've just written, I can think of several excellent reasons for using Class Frameworks.  Like I said, I'm really trying to be honest and objective here.

     The first reason is that they are fun to put together.  Building one's own Class Framework, or figuring out someone else's really solidifies one's understanding of class concepts.  And once one fully understands it then it can be used to good advantage.  One can tuck base classes or seldom used code away in code files and concentrate on the task at hand.  Recall above that our Main.cpp file in Form5 only had three lines of code in the WinMain() function. 

     Secondly, there may be implementations of things in Class Frameworks that would simply take one too long to master oneself.  One example of this that immediately comes to my mind is COM, i.e., Microsoft's Component Object Model.  While hand coding a 'worker' COM object isn't too hard, writing all one's code from scratch for something like an ActiveX  OCX control usable in Visual Basic is bordering on the impossible for most coders.  There are approximately 15 or so COM interfaces that would have to be coded to support such things as drag and drop within the ActiveX control container, calling event sinks within the host, persistence, so on and so forth.  It could easily take one years of effort to figure such things out.  By using a Class Framework such as MFC (Microsoft Foundation Classes) or ATL (Active Template Library), one could be up and running in a fairly short period of time (depending on one's knowledge of such things) what with the use of wizards and default implementations.  So therefore you wouldn't be re-inventing the wheel and would be leveraging your knowledge off a large base of existing code.

     Thirdly, you may wish to develop cross platform code so as to be able to provide the same app for Windows, Linux/Unix, OS X, etc.  This magic is made possible by writing code to a framework which can be compiled to various native Apis, i.e., write once, compile to multiple platforms.   If one wants to do this, it is clearly easier to learn to use one framework rather than all the Apis separately (and hope that the framework can be understood and used without knowing anything about how it is implemented or the underlying code its wrapping).

     Lastly, one may need to learn class frameworks as a matter of pure survival because one may need to understand them to get a job.  Much code has already been written using class frameworks, and one's job may simply entail maintaining it or modifying it further.  In other words, it may be a prerequisite to getting a job.

     I've discussed much about code size and efficiency here regarding pure SDK verses Class Framework code, but its clear small, tight, fast code isn't a priority anymore.  It simply doesn't seem to matter to most.  Basically, the idea has always been that the ever increasing processor speed will make up for the ever increasing software bloat.  According to Moore's Law, every 18 months there is a doubling of hardware capacity.  However, computers never seem to get faster.  The reason for this is Gates's Law, coined by Microsof's Bill Gates, who states that "The speed of software halves every 18 months."

http://catb.org/jargon/html/G/Gatess-Law.html

   It seems that coders are able nullify any gains in hardware capacities attributable to chip designers and manufacturers.    At the time of my writing this document in the fall of 2011 it is clear that Windows 8 (which is about a year from release) won't support Win Api SDK style coding for apps with the new 'Metro' user interface.  This is unfortunate because low spec low cost hardware such as Arm based chips could perform very well with fast, tight code such as that generated by Sdk style coding techniques.  I've personally seen this to be the case with development I've done on Windows CE for handheld devices running on Arm based chips.  Power consumption is reasonable, battery life good and performance excellent.   Even the PowerBASIC company, which specializes in fast, small code, added a #Bloat compiler directive to their compiler several years back to allow 'Bloatware Programmers' to artificially inflate the disk image size of their programs to be more 'in tune' with present software development.  Here is their comical and possibly tongue-in-cheek description of #Bloat from their manual...


#BLOAT metastatement

Purpose           Artificially inflate the disk image size of a compiled program.

Syntax            #BLOAT size_expression

Remarks           #BLOAT allows the creation of artificially bloated program files on
                  disk, in order to match or exceed that generated by competing
                  "BloatWare" compilers. #BLOAT does not affect the memory image size
                  (running size) of a compiled program.

size_expression   The size_expression parameter is a simple Long-integer expression
                  that specifies the total desired size of the compiled program's disk
                  image, but is ignored if it is smaller than the actual program size.
                  #BLOAT uses sections of the actual compiled code to fill and
                  obfuscate the portion added to the file.

                  While #BLOAT adds no true merit to the technical efficiency of the
                  compiled code, there are a number of reasons for its use, including:

                  1) To allow "BloatWare" programmers to feel more comfortable when using PowerBASIC.                                                                       
                  2) To impress project leaders/managers with the volume of executable code created.               
                  3) To allay the fears of uninformed customers who may mistakenly infer that "such tiny programs
                       couldn't possibly do everything that..."
                  4) To make certain versions of a program more readily identifiable simply by examining the size of the
                       file on disk.
                  5) To improve convolution of the contents of the executable disk image, because the bloat region
                       appears to contain executable code.

See Also                           #COMPILE, #OPTIMIZE

Example                              #BLOAT 1024 * 1024 * 4       ' Create a 4 MB EXE file


     In terms of the program we've been working with here, i.e., our Form4 and Form5 examples, the pure Sdk PowerBASIC 10.02 version compiles to 9216 bytes in the Unicode build, and unlike the C++ version would include advanced string handling capabilities.  Here would be that version.  If you un-comment out the #Bloat metastatement at top though, you can create the 4 megabyte file described above...


'#Bloat                     1024 * 1024 * 4   'Produce 4 MegaByte bloated executable
#Compile Exe                '9216 bytes if #Bloat not used
%UNICODE                    = 1
#If %Def(%UNICODE)
    Macro ZStr              = WStringz
    Macro BStr              = WString
#Else
    Macro ZStr              = Asciiz
    Macro BStr              = String
#EndIf
#Include "Windows.inc"
%IDC_TEXT   =  1500
%IDC_BUTTON =  1505


Function fnWndProc(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
  Select Case As Long wMsg
    Case %WM_CREATE
      Local pCreateStruct As CREATESTRUCT Ptr
      Local hIns,hCtl As Dword
      pCreateStruct=lParam
      hIns=@pCreateStruct.hInstance
      hCtl=CreateWindowEx(%WS_EX_CLIENTEDGE,"edit","",%WS_CHILD Or %WS_VISIBLE,15,15,285,22,hWnd,%IDC_TEXT,hIns,Byval 0)
      hCtl=CreateWindowEx(0,"button","Get Text",%WS_CHILD Or %WS_VISIBLE,115,55,80,30,hWnd,%IDC_BUTTON,hIns,Byval 0)
      Function=0 : Exit Function
    Case %WM_COMMAND
      If Lowrd(wParam)=%IDC_BUTTON Then
         Local szBuffer As ZStr*256
         GetWindowText(GetDlgItem(hWnd,%IDC_TEXT),szBuffer,256)
         MsgBox szBuffer, %MB_OK, "Here Is Your Text..."
      End If
      Function=0 : Exit Function
    Case %WM_DESTROY
      PostQuitMessage(0)
      Function=0 : Exit Function
  End Select

  fnWndProc=DefWindowProc(hWnd,wMsg,wParam,lParam)
End Function


Function WinMain(ByVal hIns As Long ,ByVal hPrev As Long, ByVal lpCmdLn As Asciiz Ptr, ByVal iShow As Long) As Long
  Local szClassName As ZStr*16
  Local wc As WndClassEx
  Local Msg As tagMsg
  Local hWnd As Dword

  szClassName      = "Form1"
  wc.lpszClassName = VarPtr(szClassName)
  wc.lpfnWndProc   = CodePtr(fnWndProc)
  wc.cbSize        = SizeOf(wc)
  wc.style         = 0
  wc.hIcon         = LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  wc.hInstance     = hIns
  wc.hIconSm       = 0
  wc.hCursor       = LoadCursor(%NULL, ByVal %IDC_ARROW)
  wc.hbrBackground = %COLOR_BTNSHADOW
  wc.cbClsExtra    = 0
  wc.cbWndExtra    = 0
  wc.lpszMenuName  = %NULL
  RegisterClassEx(wc)
  hWnd=CreateWindowEx(0,szClassName,szClassName,%WS_OVERLAPPEDWINDOW,200,200,330,135,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage(Msg)
    DispatchMessage(Msg)
  Wend

  Function=msg.wParam
End Function                   
 

Frederick J. Harris

I believe I promised a zip with Form5 including debugging conditionals, so attached is that.