ObjReader Community

Frederick Harris section => TCLib => Topic started by: Frederick Harris on October 14, 2021, 09:24:40 pm

Title: Win32 SDK Tutorial - Form3 - Modularizing Code And Debugging With Log Files
Post by: Frederick Harris on October 14, 2021, 09:24:40 pm
/*
   Form3.cpp -- Modularizing Code And Debugging With Log Files
   
   In the last program in this series - Form2, we introduced child window controls and the switch
   logic structure in the Window Procedure to map incomming Windows messages to the code which
   handles each message.  There can be a bit of a problem with this switch construct that is not
   immediately apparent in the Form2 code.  Let me elaborate.  The Form2 example didn't really do
   anything significant.  If the reader is in engineering, science, business, finance, agriculture,
   accounting, or myriads of other technical specialities, the algorithms to accomplish anything
   significant can run into untold thousands of lines of code - almost no limit really.  But if
   you try to put all this code into case blocks in a switch construct you'll have a real mess.
   Taken to the point of absurdity, it's possible, for example, to end up with a five thousand line
   program with only two procedures, i.e., about 25 lines of code in WinMain() and 4,975 lines of
   code in the Window Procedure!  This isn't good.  I'm sad to admit that my first Windows Api SDK
   style programs were like that.
   
   As a first cut at fixing it one might redesign the Window Procedure so that a seperate message
   handling function is provided for each message the Window Procedure handles, and in that way
   keep the actual message handling code out of the Window Procedure's switch construct.  For
   example, when the Window Procedure receives a WM_CREATE message, under the case WM_CREATE,
   call....
   
   fnWndProc_OnCreate(hWnd, msg, wParam, lParam);
   
   ...and simply pass the four Window Procedure's parameters through to the new message handling
   function.  Likewise for all the other messages.  See the code below to get the idea.  I believe
   you'll find this works out better.
   
   Next thing - this app violates my mantra of 'no global variables', at least if #Debug is defined.
   See the code below.  Logging the values variables are taking on in a 'trace file' along with
   output statements indicating where programm execution is taking place can be a valuable learning
   and debugging tool.  If #Debug isn't commented out, an Output.txt log file is created in the
   app's directory.  Here is that output from a program run where I entered something or other in
   the edit control, clicked the button, then ended the program run...   

   Entering WinMain()
     Right Before CreateWindow() Call In WinMain() - << Text From WinMain()...

     Entering fnWndProc_OnCreate()
       hWnd = 0x00000000000904E6
     Leaving fnWndProc_OnCreate()

     hWnd = 0x00000000000904E6 - << Text From WinMain()
     Right After CreateWindow() Call In WinMain() - << Text From WinMain()

     Entering fnWndProc_OnCommand()
       LOWORD(wParam) = 1500
     Leaving fnWndProc_OnCreate()

     Entering fnWndProc_OnClose()
       hWnd = 0x00000000000904E6
       Entering fnWndProc_OnDestroy()
         hWnd = 0x00000000000904E6
       Leaving fnWndProc_OnDestroy()
     Leaving fnWndProc_OnClose()
   Leaving WinMain()

  Note carefully that at the point of the CreateWindowEx() call in WinMain() to create the app's
  main program window, we get the debug output that code execution is in fnWndProc_OnCreate(). 
  That function call is actually occurring 'inside' the CreateWindowEx() call!  What you are seeing
  here is a C based object Constructor call.  In fnWndProc_OnCreate() we get the main app's HWND
  before it is revealed to us by the #Debug output statement in WinMain().  With this logic we see
  the symmetric C based object Destructor call in fnWndProc_OnDestroy().
 
  Next, note I added a WM_CLOSE handler to what had been our Form2 program.  In fnWndProc_OnClose()
  I call Api function DestroyWindow(), which causes Windows to send a WM_DESTROY message.  The
  debug output shows entry into the WM_CLOSE handler, then entry into the WM_DESTROY handler and
  exit from this latter handler, followed finally by exit from the WM_CLOSE handler.  Without a log
  or trace file this behavior might not be obvious.  And it is interesting.
 
  Finally, you are seeing a truely elegant code design here in terms of the Windows Api.  One
  function - CreateWindowEx(), is used to create every type of window Windows supports.  In
  WinMain() we Register a 'custom' Window Class named "Form3" with which we base our main program
  window.  We feed that name into the CreateWindowEx() call in its second parameter - szClassName.
  In fnWndProc_OnCreate() we see two more CreateWindowEx() calls, but neither of those calls
  contain in the second parameter a Class Name we Registered in the app.  The 1st CreateWindowEx()
  call contains in its 2nd parameter the string "edit".  The 2nd CreateWindowEx() call asks for
  the instantiation of an object of class "button".  We never Registered an "edit" or "button"
  object in the "Form2" app.  Think about it.  What's going on here?

  What's going on here is that Windows the operating system has predefined classes of specialized
  'window' objects that can be instantiated in application programs by CreateWindow() or
  CreateWindowEx() function calls.  There are several categories of these objects or 'controls'.
  Edit Controls and Button Controls are in the category known as the 'Standard Controls'.  Then
  there are the 'Common Controls'.  These are a bit more complex to use and they are quite
  sophisticated.  Then there are ActiveX Controls.  With those the CreateWindow() calls are actually
  encapsulated within the control and the user creates them indirectly through the COM (Component
  Object Model) subsystem.  Finally, one can create controls oneself and these would be 'Custom
  Controls' - a somewhat advanced topic, as are ActiveX Controls. But the truly elegant part of
  this code design of the Windows Api is that one function call - CreateWindow(), is used to create
  all these very different types of windows.  Below is Form3.cpp....   
*/

Code: [Select]
// cl Form3.cpp /O1 /Os /link Kernel32.lib User32.lib                       // 88,064 Bytes
// g++ Form3.cpp -luser32 -lkernel32 -oForm3_gcc.exe -mwindows -m64 -s -Os  // 17,920 Bytes
// cl Form3.cpp /O1 /Os /GS- /Gy /link TCLib.lib kernel32.lib user32.lib    //  4,608 Bytes
//#define TCLib
//#define Debug
#ifndef UNICODE
   #define UNICODE
#endif
#ifndef _UNICODE
   #define _UNICODE
#endif   
#include <windows.h>
#ifdef TCLib
  #include "stdio.h"
#else
  #include <cstdio>
#endif
#define  IDC_BUTTON 1500
#define  IDC_EDIT   1505
#ifdef Debug
  FILE* fp=NULL;
#endif


LRESULT CALLBACK fnWndProc_OnCreate(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
 HINSTANCE hIns = NULL;    // This is a C - based Constructor Function whose code execution will be occurring 
                           // 'inside' the CreateWindowEx() call down in WinMain().  Unlike a C++ language
 #ifdef Debug              // Constructor, failure is a possibility.  If -1 is returned by this function, the
 fprintf(fp,"  Entering fnWndProc_OnCreate()\n");  // CreateWindowEx() function in WinMain() will fail and return
 fprintf(fp,"    hWnd = 0x%p\n",hWnd);             // a NULL for its HWND.  Just below we'll make two CreateWindowEx()
 #endif                                            // calls for two 'Child Window Controls'.  The code just at left
 hIns=((LPCREATESTRUCT)lParam)->hInstance;         // retrieves the HINSTANCE from the CREATESTRUCT pointed to by LPARAM.
 CreateWindowEx(WS_EX_CLIENTEDGE,L"edit",L"",WS_CHILD|WS_VISIBLE,50,40,200,25,hWnd,(HMENU)IDC_EDIT,hIns,0);
 CreateWindowEx(0,L"button",L"Retrieve Text",WS_CHILD|WS_VISIBLE,95,90,120,30,hWnd,(HMENU)IDC_BUTTON,hIns,0);
 #ifdef Debug
 fprintf(fp,"  Leaving fnWndProc_OnCreate()\n\n");
 #endif

 return 0;
}


LRESULT CALLBACK fnWndProc_OnCommand(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
 wchar_t szBuffer[256];                                        // Child Window Controls communicate with their parent
                                                               // by sending WM_COMMAND messages.  Packed tightly within
 if(LOWORD(wParam)==IDC_BUTTON && HIWORD(wParam)==BN_CLICKED)  // the WPARAM and LPAREM parameters is information the
 {                                                             // parent (our main app window) can use to determine which
    #ifdef Debug                                               // child window control is sending the message, and what
    fprintf(fp,"  Entering fnWndProc_OnCommand()\n");          // the nature of the user's interaction with the child
    fprintf(fp,"    LOWORD(wParam) = %u\n",LOWORD(wParam));    // window control was that triggered the sending of the
    #endif                                                     // message, e.g., the user clicked a button or typed a
    GetWindowText(GetDlgItem(hWnd,IDC_EDIT),szBuffer,256);     // character in an edit control.  Note that the GetDlgItem()
    MessageBox(hWnd,szBuffer,L"Button Click",MB_OK);        // function returns the HWND of a child window given the
    #ifdef Debug                                               // HWND of the parent and the Control ID of the child window
    fprintf(fp,"  Leaving fnWndProc_OnCreate()\n\n");          // control as specified in the HMENU parameter of the
    #endif                                                     // CreateWindow() call that created the control.  Its never
 }                                                             // necessary to use global variables in Windows programs to
                                                               // persist HWNDs because Windows will always be able to
 return 0;                                                    // return these to us through a GetDlgItem() function call.
}                                                              // Windows keeps track of all sorts of stuff like this.


LRESULT CALLBACK fnWndProc_OnClose(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
 #ifdef Debug                                                  // The WM_CLOSE message provides the application the opportunity
 fprintf(fp,"  Entering fnWndProc_OnClose()\n");               // of querying the user for confirmation that the user indeed
 fprintf(fp,"    hWnd = 0x%p\n",hWnd);                         // wants to exit the application.  In this app all I did with the
 #endif                                                        // message was call DestroyWindow().  Note the Window Procedure
 DestroyWindow(hWnd);                                          // is 'reentrant'.  fnWndProc_OnDestroy() will execute within
 #ifdef Debug                                                  // fnWndProc_OnClose().  This is clearly seen in the Output.txt
 fprintf(fp,"  Leaving fnWndProc_OnClose()\n");                // log file if #Debug is active/defined, i.e., not commented out.
 #endif 
 
 return 0;   
}


LRESULT CALLBACK fnWndProc_OnDestroy(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
 #ifdef Debug                                                  // This is a C - Based Destructor Call for our main program
 fprintf(fp,"    Entering fnWndProc_OnDestroy()\n");           // window.  In more complex programs than this, it is a good
 fprintf(fp,"      hWnd = 0x%p\n",hWnd);                       // place to make sure memory allocations are released, databases
 #endif                                                        // or files are closed, etc.  Here, all we need to do is call
 PostQuitMessage(0);                                           // PostQuitMessage() to stop the 'message pump' in WinMain()
 #ifdef Debug                                                  // from running so that the program can end.
 fprintf(fp,"    Leaving fnWndProc_OnDestroy()\n");
 #endif 
 
 return 0;   
}


LRESULT CALLBACK fnWndProc(HWND hWnd, unsigned int msg, WPARAM wParam, LPARAM lParam)
{
 switch(msg)                                                // I don't personally much care for placing a lot of code
 {                                                          // in 'case' constructs under a switch statement.  So at
   case WM_CREATE:                                          // left you see I created seperate 'message handling functions'
     return fnWndProc_OnCreate(hWnd, msg, wParam, lParam);  // to put actual code to be executed for each message to be
   case WM_COMMAND:                                         // handled.  The idea illustrated here is pretty simple; in  my
     return fnWndProc_OnCommand(hWnd, msg, wParam, lParam); // actual production code I don't use switch logic but rather
   case WM_CLOSE:                                           // for loop logic to iterate through a struct array containing
     return fnWndProc_OnClose(hWnd, msg, wParam, lParam);   // the message and a function pointer to the message handling
   case WM_DESTROY:                                         // function that handles that specific message. 
     return fnWndProc_OnDestroy(hWnd, msg, wParam, lParam);
 }                                                         

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


int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevIns, LPWSTR lpszArgument, int iShow)
{
 wchar_t szClassName[] =L"Form3";               // Program Entry Point for Windows GUI app.  I personally like to keep my
 HWND hWnd = NULL;                              // WinMain() functions short.  The only code I put in them is code to
 WNDCLASSEX wc;                                 // Register my main app window, the CreateWindowEx() call to create that window,
 MSG messages;                                  // and the 'Message Pump' that keeps the program alive and functioning.  If
                                                // the app instantiates multiple top level windows, I put the code to register
 #ifdef Debug                                   // those additional classes in the WM_CREATE handler of the main app's window.
 fp=fopen("Output.txt","w");                    // After all, that is the 'Constructor' for the entire app. 
 fprintf(fp,"Entering WinMain()\n");
 #endif
 memset(&wc,0,sizeof(wc));                      // Zero out WNDCLASSEX object                   
 wc.lpszClassName = szClassName;                // Start filling out WNDCLASSEX object....
 wc.lpfnWndProc   = fnWndProc;                  // Function Pointer to Window Procedure
 wc.cbSize        = sizeof(WNDCLASSEX);         // Size of WNDCLASSEX object
 wc.hbrBackground = (HBRUSH)COLOR_BTNSHADOW;    // Color for background of main app window
 wc.hInstance     = hInstance;                  // Virtual Memory Address Thing
 RegisterClassEx(&wc);                          // Register Custom Window Class
 #ifdef Debug
 fprintf(fp,"  Right Before CreateWindow() Call In WinMain() - << Text From WinMain()...\n\n");
 #endif
 hWnd=CreateWindowEx(0,szClassName,szClassName,WS_OVERLAPPEDWINDOW,200,175,320,200,HWND_DESKTOP,0,hInstance,0);  // C Based Object Constructor
 #ifdef Debug
 fprintf(fp,"  hWnd = 0x%p - << Text From WinMain()\n",hWnd);
 fprintf(fp,"  Right After CreateWindow() Call In WinMain() - << Text From WinMain()\n\n");
 #endif
 ShowWindow(hWnd,iShow);                        // Show Window
 while(GetMessage(&messages,NULL,0,0))          // Message Pump
 {                                       
    TranslateMessage(&messages);         
    DispatchMessage(&messages);         
 }
 #ifdef Debug
 fprintf(fp,"Leaving WinMain()\n");
 fclose(fp);
 #endif
                                         
 return (int)messages.wParam;
}