Conversion Tutorial - Visual Basic 4 - 6 To PowerBASIC

Started by Frederick J. Harris, September 04, 2014, 04:54:52 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

     This tutorial series is for anyone who hasn't as of yet moved from old Visual Basic 4 - 6 to something more current, such as PowerBASIC.  In terms of my background for writing this, I spent most of my time in the middle to late 90s using these languages, and am still supporting as of 2014 an Enterprise level application I developed at that time.  I work in the forestry sector, and my applications are technical and business oriented in nature, with a lot of number crunching and network database access.  I began moving to PowerBASIC around 2000 or so, and consider PowerBASIC and C++ to be my main coding tools.  So I'll be writing this tutorial from the standpoint of someone who came to PowerBASIC pretty much lost at first.  And I'll not presuppose any particular programming knowledge of my readers (if there are still any VB6 users left who find their way here) beyond a knowledge of Visual Basic 4-6.  You'll need preferably PowerBASIC Windows 10 to follow along.  The Console Compiler will work too as will some of PowerBASIC's older compilers, but PB Win 10 would be best.  And the material I will present here will be how to get started using the Windows Api directly to create Windows programs.  As such I'll not be covering shortcut techniques to creating Windows programs using the Windows Dialog Engine, Dialog Boxes, or RAD (Rapid Application Development).

     I'll start right at the beginning.  In Visual Basic, if you choose the option to create a new executable, you can save your first project as Project1.vbp and the form as Form1.frm.  If you click the run button in the VB IDE (Integrated Development Environment)  you'll end up with a working Windows program that displays a blank Form1 Window.  If you package that into a setup and installation program you end up with about 1.3 MB of dependencies in the following files...


Asycfilt.dll    145 KB
Comcat.dll       22 KB
Msvbvm60.dll   1354 KB
Oleaut32.dll    585 KB
Olepro32.dll    161 KB
Project1.exe     16 KB
Setup1.exe      244 KB
St6unst.exe      72 KB
Stdole2.tlb      18 KB
VB6STKIT.dll    100 KB


Paste this into your PowerBASIC editor and compile and run.  It does the same thing as the above VB program...


#Compile Exe
%UNICODE = 1
#Include "Windows.inc"

Function fnWndProc(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
  If wMsg=%WM_DESTROY Then
     Call PostQuitMessage(0)
     Function=0 : Exit Function
  End If

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

Function WinMain(ByVal hIns As Long, ByVal hPrev As Long, ByVal lpCL As WStringz Ptr, ByVal iShow As Long) As Long
  Local szAppName As WStringz*16
  Local wc As WNDCLASSEX
  Local hWnd As Dword
  Local Msg As tagMsg

  szAppName="Form1"
  wc.lpszClassName=VarPtr(szAppName)              : wc.lpfnWndProc=CodePtr(fnWndProc)
  wc.cbClsExtra=0                                 : wc.cbWndExtra=0
  wc.style=%CS_HREDRAW Or %CS_VREDRAW             : wc.hInstance=hIns
  wc.cbSize=SizeOf(wc)                            : wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)  : wc.hbrBackground=%COLOR_BTNFACE+1
  wc.lpszMenuName=%NULL
  Call RegisterClassExW(wc)
  hWnd=CreateWindowExW(0,szAppName,"Form1",%WS_OVERLAPPEDWINDOW,200,100,325,300,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend

  Function=msg.wParam
End Function
                           

You'll get output from the PowerBASIC compiler something like this if you have your editor options set up like mine...


PowerBASIC 10 for Windows
Copyright (c) 1996-2011 PowerBasic Inc.
Englewood, Florida USA
All Rights Reserved

Primary source:  C:\Code\PwrBasic\PBWin10\Forms\Form1\Form1.bas   {191012 total lines}
Target compilation:  Form1.EXE
Compile time:  0.5 seconds, at 22921440 lines/minute

1088 bytes compiled code, 3680 bytes RTLibrary,
12 bytes string literals, and 2708 bytes dgroup.
Executable stack size:  1048576 bytes.
Disk image: 8704 bytes   Memory image: 6400 bytes.


     So you see the above PowerBASIC translation of the most basic Form1 type program results in about a 9 K executable with no dependencies other than that it be run on a Windows operating system.   But you more than likely don't understand any of the PowerBASIC code, whereas with Visual Basic you didn't have to write one line of code to get a working Form1.  So that isn't good.  But you did realize that in moving away from the familiar to a new programming  language that you were going to have to learn new things, right?  But I'll get you onto familiar ground just as quick as I can.  But it won't be by explaining that code, at least not anything in WinMain().  At least not yet.  But let's take a look at this...   


Function fnWndProc(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
  If wMsg=%WM_DESTROY Then
     Call PostQuitMessage(0)
     Function=0 : Exit Function
  End If

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


     In Visual Basic you are very familiar with the concept of 'Events' and 'Event Procedures'.  You probably know about what happened when you clicked the x in the title bar to close the Visual Basic version of the Form1 program.  Visual Basic would have called any code you would have written in  a Form_Unload() Event procedure such as this, right?...


Private Sub Form_Unload(Cancel As Integer)
  MsgBox ("Form_Unload()")
End Sub


     Well, we can get to that very easily in PowerBASIC. But in PowerBASIC we can't ignore it as we can in Visual Basic.  Note in our PowerBASIC fnWndProc()  function we are testing a parameter named wMsg to see if it is equal to a constant/equate named %WM_DESTROY.  If that test succeeds we call a function named PostQuitMessage().  That kind of looks something like a Visual Basic Form_Unload(), doesn't it?  Well, we can make it exact.  Here's Form2 in PowerBASIC with the Form_Unload() Event Procedure exactly as in  Visual Basic except you don't need the 'Private' keyword...


#Compile Exe
%UNICODE = 1
#Include "Windows.inc"


Sub Form_Unload(Byref blnCancel As Long)
  MsgBox("Called Form_Unload()")
End Sub


Function fnWndProc(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
  If wMsg=%WM_CLOSE Then
     Call Form_Unload(0)
     DestroyWindow(hWnd)
     Call PostQuitMessage(0)
     Function=0: Exit Function
  End If

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


Function WinMain(ByVal hIns As Long, ByVal hPrev As Long, ByVal lpCL As WStringz Ptr, ByVal iShow As Long) As Long
  Local szAppName As WStringz*16
  Local wc As WNDCLASSEX
  Local hWnd As Dword
  Local Msg As tagMsg

  szAppName="Form1"
  wc.lpszClassName=VarPtr(szAppName)              : wc.lpfnWndProc=CodePtr(fnWndProc)
  wc.cbClsExtra=0                                 : wc.cbWndExtra=0
  wc.style=%CS_HREDRAW Or %CS_VREDRAW             : wc.hInstance=hIns
  wc.cbSize=SizeOf(wc)                            : wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)  : wc.hbrBackground=%COLOR_BTNFACE+1
  wc.lpszMenuName=%NULL
  Call RegisterClassExW(wc)
  hWnd=CreateWindowExW(0,szAppName,"Form2",%WS_OVERLAPPEDWINDOW,200,100,325,300,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend

  Function=msg.wParam
End Function
         

     Please run that and verify for yourself that it is working exactly as a Visual Basic program would if you put a Message Box in the Form_Unload() Event Procedure.   Hmmmm!  Your intellectual juices should be flowing!  Are you beginning to get the picture?  Let's discuss what I just did.

     In Form1.bas in the procedure fnWndProc (pronounced Function WindProc or Window Procedure in Windows programming circles) I had tested for equality between %WM_DESTROY and wMsg, which was a parameter of fnWndProc().  In Form2, in my desire to demonstrate to you the total applicability of your Visual Basic concept of Event Procedures to PowerBASIC programming, I changed my equality test of wMsg to another constant named %WM_CLOSE.  I did that because the Form_Unload() Event Procedure of Visual Basic has a single parameter named Cancel, that if set to true, will stop the Form from being unloaded and destroyed by Windows.  But when I called my PowerBASIC version of Sub Form_Unload(), I passed in zero in the blnCancel parameter, and did nothing within that procedure to alter that value.  I simply put up a Message Box that the procedure was called.  And of course it closed the program when you clicked the x in the title bar.

     So you've seen %WM_DESTROY and %WM_CLOSE.  Are there others?  You betcha!  These constants are known as 'Windows Messages' in Window Programming nomenclature, and they are defined in various sub-includes included  by this statement in both of our programs so far ...

#Include "Windows.inc"

     And it is these messages which Visual Basic translates in the IDE into Event Procedures.   So what you need to take from this is that you are now seeing some of the hidden workings of Windows which Visual Basic was hiding from you.  The other thing about this is that these messages and the architecture of these PowerBASIC programs that I am showing you isn't something unique to PowerBASIC as for example Form_Load() or Form_Unload() is unique to Visual Basic; this is an architecture or program design existing as part of the operating system itself.  C or C++ programs would look identical to these PowerBASIC programs.  Let's try another one, i.e., Form3.bas...

...to be continued ...

Frederick J. Harris

#1
... continued


#Compile Exe
%UNICODE = 1
#Include "Windows.inc"


Function Form_Load(Byval hWnd As Long, Byval lParam As Long) As Long
  Local iResult As Long

  iResult=MsgBox("We're Now In Form_Load()  Do You Wish To Continue Loading?", %MB_YESNO, "Proceed?")
  If iResult = %IDYES Then
     Function=0
  Else
     Function=-1
  End If
End Function


Function Form_Click(Byval hWnd As Long, Byval wParam As Long, Byval lParam As Long) As Long
  MsgBox("You've Just Clicked On The Form!")
  Function=0
End Function


Function Form_Unload(Byval hWnd As Long) As Long
  Local iResult As Long

  iResult=MsgBox("Do you want to close the app?",%MB_YESNO,"End Program?")
  If iResult=%IDYES Then
     DestroyWindow(hWnd)
     Call PostQuitMessage(0)
  End If

  Function=0
End Function


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
      Function=Form_Load(hWnd,lParam)
      Exit Function
    Case %WM_LBUTTONDOWN
      Function=Form_Click(hWnd,wParam,lParam)
      Exit Function
    Case %WM_CLOSE
      Function=Form_Unload(hWnd)
      Exit Function
  End Select

  Function=DefWindowProcW(hWnd,wMsg,wParam,lParam)
End Function


Function WinMain(ByVal hIns As Long, ByVal hPrev As Long, ByVal lpCL As WStringz Ptr, ByVal iShow As Long) As Long
  Local szAppName As WStringz*16
  Local wc As WNDCLASSEX
  Local hWnd As Dword
  Local Msg As tagMsg

  szAppName="Form3"
  wc.lpszClassName=VarPtr(szAppName)              : wc.lpfnWndProc=CodePtr(fnWndProc)
  wc.cbClsExtra=0                                 : wc.cbWndExtra=0
  wc.style=%CS_HREDRAW Or %CS_VREDRAW             : wc.hInstance=hIns
  wc.cbSize=SizeOf(wc)                            : wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)  : wc.hbrBackground=%COLOR_BTNFACE+1
  wc.lpszMenuName=%NULL
  Call RegisterClassExW(wc)
  hWnd=CreateWindowExW(0,szAppName,"Form3",%WS_OVERLAPPEDWINDOW,200,100,325,300,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend

  Function=msg.wParam
End Function
   

     Run this program and see what you think.  Note you'll first see a Message Box reporting to you that we are in Form_Load(), and asking you whether you wish to proceed.  If you answer 'No' the program will terminate.  If you answer 'Yes' you'll see the Form appear on your monitor.  If you wish to learn about this, go to MSDN and do a search for ....

WM_CREATE

or I'll even make it easier for you, try this ...

http://msdn.microsoft.com/en-us/library/windows/desktop/ms632619%28v=vs.85%29.aspx

     See what I mean about this programming technique I am showing you not being provincially unique to PowerBASIC?   

     Note how I've changed fnWndProc() - our 'Window Procedure'.  Before we were just testing for one particular Message/Event.  Now we're testing for three, so I moved us to Select Case logic.  In Window terminology we would say we're handling the WM_CREATE, WM_LBUTTONDOWN, and WM_CLOSE messages/events.  Note I also improved the Form_Unload() event/message handler.  Clicking the x button throws up a Message Box requesting for confirmation for closing the program, which seems to work.  If you wish to learn about this, go to MSDN and search for ...

WM_CLOSE

     What you'll find there is that if a zero is returned from a WM_CLOSE handler, program termination is aborted.  Our WM_CLOSE handler - Form_Unload(), calls DestroyWindow() and PostQuitMessage() if the user indicates that he/she wishes to close out.  These are Windows Api functions that are called identically in other languages such as C or C++, and as such are not 'part' or the PowerBASIC language.  You can also read about them at MSDN.  However, PowerBASIC declares for these functions can be found in various sub-includes included by the master Windows Include Windows.inc.

     You'll also note I changed the parameter lists of the various message/event handlers a bit, but I'm asking you to just bear with me on that and accept my promise that I'll explain all that later.  Having about half digested all the wonders I've served up so far, let's quickly move on to child Windows controls.  How are they created in this scheme?  In Visual Basic one just drug controls such as buttons and text boxes from the Visual Basic 'Toolbox'  to the visual designer's Form and positioned them there.  What does one do in PowerBASIC?  Well, we're not using visual designers here (there are visual designers available for PowerBASIC), so we write code.  Look at how we created the main program window of all three programs I've shown so far.  And you'll have to look in the as yet not described function WinMain().  In there you've surely noticed by now a rather ominous looking function named CreateWindowEx() that has an awful lot of parameters.   Here's what's going on there.  There is a TYPE variable declared in WinMain() like so...

Local wc As WNDCLASSEX

WNDCLASSEX is a type that forms the basis of what is termed a Window Class.  You should briefly look this up at MSDN to see what some of the fields are of this type.  Note I've filled most of them in in WinMain().  In the oft repeated Object Oriented Programming analogy of a Class being a 'Cookie Cutter' and an instance of a Class being a 'cookie' patterned after the Cookie Cutter, a WNDCLASSEX is the cookie cutter and a created window based on that class is the cookie.  In WinMain() of the last program the CreateWindowEx() function created a main program window for us based on a Window Class, one of whose members is WNDCLASSEX::lpszClassName, and we specified that member as "Form3".  And if you look at the 2nd parameter of the CreateWindowEx() function, you'll see we specified it as szAppName, which was previously set as "Form3".  So The CreateWindowEx() function created an instance of the "Form3" Window Class.  I told you that to tell you this...if you wish to instantiate child window controls on your application windows, i.e., your 'Forms', then you need to do so through calls to CreateWindowEx() where you specify the Class Name of the type of window you wish to instantiate through that 2nd parameter of CreateWindowEx().  And all the controls in the Visual Basic toolbox such as buttons and list boxes and combo boxes have convenient pre-defined by Windows names that you can use such as "button", "listbox", and "combobox".  Isn't that nice!  So let's try to make a few buttons.  Let's try Form4.bas...



#Compile Exe
%UNICODE       = 1
#Include       "Windows.inc"

%IDC_COMMAND1  = 1500
%IDC_COMMAND2  = 1505

Type WindowsEventArgs
  hWnd         As Dword
  wParam       As Long
  lParam       As Long
  hInst        As Dword
End Type


Function Form_Load(Wea As WindowsEventArgs) As Long
  Local pCreateStruct As CREATESTRUCT Ptr
  Local hBtn As Dword

  pCreateStruct=wea.lParam : Wea.hInst=@pCreateStruct.hInstance
  hBtn=CreateWindow("button","Command1",%WS_CHILD Or %WS_VISIBLE,80,60, 150,30,Wea.hWnd,%IDC_COMMAND1,Wea.hInst,ByVal 0)
  hBtn=CreateWindow("button","Command2",%WS_CHILD Or %WS_VISIBLE,80,120,150,30,Wea.hWnd,%IDC_COMMAND2,Wea.hInst,ByVal 0)

  Form_Load=0
End Function


Sub Command1_Click(Wea As WindowsEventArgs)
  MsgBox ("You Clicked Button #1")
End Sub


Sub Command2_Click(Wea As WindowsEventArgs)
  MsgBox ("You Clicked Button #2")
End Sub


Function Form_Command(Wea As WindowsEventArgs) As Long
  Select Case As Long Lowrd(Wea.wParam)
    Case %IDC_COMMAND1
      Call Command1_Click(Wea)
    Case %IDC_COMMAND2
      Call Command2_Click(Wea)
  End Select

  Function=0
End Function


Function Form_Unload(Wea As WindowsEventArgs) As Long
  DestroyWindow(Wea.hWnd)
  Call PostQuitMessage(0)
  Function=0
End Function


Function fnWndProc(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
  Local Wea As WindowsEventArgs

  Select Case As Long wMsg
    Case %WM_CREATE
      Wea.hWnd=hWnd : Wea.wParam=wParam : Wea.lParam=lParam
      Function=Form_Load(Wea)    : Exit Function
    Case %WM_COMMAND
      Wea.hWnd=hWnd : Wea.wParam=wParam : Wea.lParam=lParam
      Function=Form_Command(Wea) : Exit Function
    Case %WM_CLOSE
      Wea.hWnd=hWnd : Wea.wParam=wParam : Wea.lParam=lParam
      Function=Form_Unload(Wea)  : Exit Function
  End Select

  Function=DefWindowProcW(hWnd,wMsg,wParam,lParam)
End Function


Function WinMain(ByVal hInst As Long, ByVal hPrev As Long, ByVal lpCmdLn As WStringz Ptr, ByVal iShow As Long) As Long
  Local szAppName As WStringz*16
  Local wc As WNDCLASSEX
  Local hWnd As Dword
  Local Msg As tagMsg

  szAppName="Form4"
  wc.lpszClassName=VarPtr(szAppName)              : wc.lpfnWndProc=CodePtr(fnWndProc)
  wc.cbClsExtra=0                                 : wc.cbWndExtra=0
  wc.style=0                                      : wc.hInstance=hInst
  wc.cbSize=SizeOf(wc)                            : wc.hIcon=LoadIcon(%NULL, ByVal %IDI_APPLICATION)
  wc.hCursor=LoadCursor(%NULL, ByVal %IDC_ARROW)  : wc.hbrBackground=%COLOR_BTNFACE+1
  wc.lpszMenuName=%NULL
  Call RegisterClassExW(wc)
  hWnd=CreateWindowEx(0,szAppName,"Form4",%WS_OVERLAPPEDWINDOW,400,300,330,300,0,0,hInst,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend

  Function=msg.wParam
End Function


     When you run that program you'll see you have what you would get in Visual Basic if you put two Command Buttons on a Form and left the default names Visual Basic would have given them as Command1 and Command2.  And if you double clicked on each button in Form View and edited the blank Private Sub Command1_Click() and Private Sub Command2_Click() procedures and put Message Boxes in them announcing when they were clicked, why, you would have the PowerBASIC Form4 program above!

     I made a lot of changes from Form3.bas so let me explain them now.  First note I used CreateWindow() calls up in  Form_Load() like I said previously to create the two button controls.  CreateWindow() is a simpler version of CreateWindowEx() with one less argument, i.e., the first one of CreateWindowEx(), which is a style argument containing extended styles that came about after CreateWindow() was published as part of the Windows Api.  For buttons CreateWindow() works OK. 

Near the top of the program you'll see this UDT (User Defined Type) I created...


Type WindowsEventArgs
  hWnd         As Dword
  wParam       As Long
  lParam       As Long
  hInst        As Dword
End Type


     You might have noticed (I hope you did) that the members of this type are mostly the Window Procedure parameters, i.e., here is fnWndProc's function signature...


Function fnWndProc(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long

In all the previous three programs I passed various numbers of arguments to each message/event handler.  The fact is they are oftentimes all needed, so it makes sense to me at least to always pass them all.  And rather than having lots of functions with long parameter lists, I amalgamated them all into one UDT.  This is not any kind of requirement (obviously), but its something I always do.  I'll get into why Visual Basic event procedures sometimes don't have any parameters in a bit.  Just accept that difference for now, e.g., Visual Basic's Form_Load() has an empty parameter list while mine doesn't.

     The next thing I want you to note is an easy one.  Its this ...


%IDC_COMMAND1  = 1500
%IDC_COMMAND2  = 1505


     Visual Basic names or attaches text string names to all its objects, i.e., txtFirstName for a text box to hold first names, cmdExit for an exit button.  This is something unique to Visual Basic and not part of the basic Windows Api.  Using the Windows Api numeric constants are usually used to identify child window controls, i.e., buttons, listboxes, text boxes, etc.  Those constants were fed into the ninth argument of the CreateWindow() call that created them.  That argument to CreateWindow() can hold either a Handle to a menu, or a child window control identifier.  This will take you a little bit of effort to get used to.  It is definitely different from Visual Basic. 

     Next issue is a hard one that will likely throw you for a loop.  But don't get stuck on it.  To make either a CreateWindow() or CreateWindowEx() call one needs something termed the HINSTANCE of the application.  This kind of brings up the whole subject of HANDLES in general.  In Windows Api coding you'll run into all different kinds of HANDLES, i.e., handle to a window - HWND, handle to a device context - HDC, handle to a BRUSH - HBRUSH, handle to an Instance HINSTANCE, etc.  These are simply identifiers which Windows the operating system knows how to deal with.  They are returned to us by various Windows Api functions, and we utilize them by passing them back to other Windows Api functions to accomplish various and sundry things that at the time are useful to us.  A handle to an instance identifies to Windows the actual process it is dealing with in the present function call.   Its as simple as that, or as complicated as that.  Make of it as you will. 

     There are various ways to get the handle of the instance for a program.  The function GetModuleHandle() will return it just as well as what I did above, which is this ...


Local pCreateStruct As CREATESTRUCT Ptr
pCreateStruct=wea.lParam
Wea.hInst=@pCreateStruct.hInstance

               
What's happening there is that at the point of the CreateWindowEx() call in WinMain() that created the main program window of Form4, Windows the operating system pushed all the parameters of that call on the stack.  Then it popped them back off into a struct/UDT like so...


TYPE CREATESTRUCTA DWORD
  lpCreateParams AS LONG          ' LPVOID
  hInstance      AS DWORD         ' HINSTANCE
  hMenu          AS DWORD         ' HMENU
  hwndParent     AS DWORD         ' HWND
  cy             AS LONG          ' int
  cx             AS LONG          ' int
  y              AS LONG          ' int
  x              AS LONG          ' int
  style          AS LONG          ' LONG
  lpszName       AS ASCIIZ PTR    ' LPCSTR
  lpszClass      AS ASCIIZ PTR    ' LPCSTR
  dwExStyle      AS DWORD         ' DWORD
END TYPE


     As you can see, they are in reverse order of the parameters of the CreateWindowEx() function.  And WinMain() got the handle to the instance of the process represented by Form4 in the parameter list of WinMain() itself.  That's the original source of it.  Then, before the CreateWindowEx() function call in WinMain() returned, i.e., before it completed, and while things were going on within code internal to Windows itself, Windows sent this program a WM_CREATE message.  Recall we are in the WM_CREATE handler - Form_Load().  And when it sent that message, i.e., called fnWndProc() with the wMsg parameter equal to WM_CREATE, it assigned to the lParam parameter the address of this CREATESTRUCT object containing the parameters of the CreateWindowEx() call!  Whew!

     So my code there assigns that pointer to a variable of PowerBASIC type CREATESTRUCT Ptr, i.e., pointer to a CREATESTRUCT.  And the way that is 'dereferenced' to suck out the HINSTANCE is with this pointer syntax ...

Wea.hInst=@pCreateStruct.hInstance

Read the '@' symbol with the traditional 'at' meaning, i.e., at ptrCreateStruct member hInstance.  You may need to learn how to use pointers if you aren't familiar with them.  Or use GetModuleHandle().  Or, even worse yet, define HINSTANCE as a global variable which would eliminate the whole issue! But I do things like the above to spice things up.  You don't want life to get too easy! Cuz if it does, you'll get soft, and won't be able to deal with the next adversity to befall you!  Got that?

     Next issue is this ...


Function Form_Command(Wea As WindowsEventArgs) As Long
  Select Case As Long Lowrd(Wea.wParam)
    Case %IDC_COMMAND1
      Call Command1_Click(Wea)
    Case %IDC_COMMAND2
      Call Command2_Click(Wea)
  End Select

  Function=0
End Function


     Here's the deal.  Button controls, as well as all other controls, are termed 'child window controls'.  They exist within the context of another window, and we use terms such as 'parent' and 'child' to describe the relationships.  And when one clicks or, more generally, interacts with any child window control, e.g., typing text in a text box, selecting an item in a listbox or combo box, checking a check box, selecting the state of a radio button, or simply pressing a button, these actions must be reported in some way to the parent window upon which the child window control is located.  And when child window controls are created with the CreateWindow() or CreateWindowEx() functions, the HWND (Handle of Window) of the parent is specified in the 8th argument of the CreateWindow() call and the 9th of the CreateWindowEx() call.  And the way that parent is notified of these interactions is through the WM_COMMAND message.  In our particular Form4 program above, when either of the two buttons are pressed, Windows will either post or send a WM_COMMAND message to our fnWndProc() procedure.  Here is the all important signature of that function again...


Function fnWndProc(ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long 


So to be specific, when a button is pressed, the 1st parameter hWnd will contain the Window Handle (HWND) of the main program window created down in WinMain().  The 2nd parameter wMsg will equal %WM_COMMAND - an constant equate.  The wParam parameter will contain, believe it or not, two values!  You might ask how that can be!  Well, Windows tries to be efficient so it can run fast.  The wParam is typed as a Long integer, which in 32 bit versions of Windows contains 32 bits or two 16 bit words.  So conceptually, there is a high order Word and a low order word in that 32 bit variable.  The low order word of a button click message is packed with the control Id set to the button control when it was created.  For %IDC_COMMAND1 that was 1500 and for %IDC_COMMAND2 it was 1505.  The high order Word of wParam is what is termed a 'notification message'.  Try hard to conceptualize these as different from the wMsg parameter which we just described as WM_COMMAND.  Notification messages are messages from the child window control that indicate what was actually done to them by the user, i.e., a button clicked, a keystroke in a textbox, etc.  In this case the clicking of a button results in the high order word being set to the equate BN_CLICKED.  Here is the MSDN documentation on it if you search for BN_CLICKED... 


BN_CLICKED
The BN_CLICKED notification message is sent when the user clicks a button. The parent window of the button receives this notification message through the WM_COMMAND message.
BN_CLICKED
idButton = (int) LOWORD(wParam);    // identifier of button
hwndButton = (HWND) lParam;         // handle to button

Remarks
A disabled button does not send a BN_CLICKED notification message to its parent window.
QuickInfo
  Windows NT: Requires version 3.1 or later.
  Windows: Requires Windows 95 or later.
  Windows CE: Requires version 1.0 or later.
  Header: Declared in winuser.h.
See Also
Buttons Overview, Button Messages, WM_COMMAND


     So now you should see what the last parameter or lParam of the Window Procedure fnWndProc() is.  It would be the HWND of the button that was clicked.  For what we needed to do in this simple case though, i.e., find out which button was clicked, all we really needed to determine was what number was in the low order Word of Wea.wParam in Form_Command().  That's why you see my Select Case logic there where we're using PowerBASIC's Lowrd macro to extract the 'Low Word' of Wea.wParam.  So when we make a match on either %IDC_COMMAND1 or %IDC_COMMAND2, we know which procedure to call - either Command1_Click() or Command2_Click().

continued ...

Frederick J. Harris

#2
     Good enough, and time to move on to the next Visual Basic verses PowerBASIC issue, and that would be multiple Forms.  Once one has a project open in Visual Basic one can add a new Form to the project, i.e., a 2nd, 3rd, etc., by either going to the Project main menu item and selecting Add New Form, or right clicking on the Project Name in the Project Explorer window and doing the same.   Here are the details of a new Visual Basic project I'm starting.  I named it pbMultipleForms.  Then I renamed the only Form in the project from "Form1" to "Main", and I saved it as Main.frm.  Then I added three more Forms to the project in the manner I described above, and of course Visual Basic named them Form1, Form2, and Form3.  I saved them as Form1.frm, Form2.frm, and Form3.frm.  Next, I switched back to the Main Form, which is my 'startup' object, and I put three Command Buttons on that form.  I named them btnShowForm1, btnShowForm2, and btnShowForm3.  The captions, respectively are "Show Form1", "Show Form2", and "Show Form3".   Here is the whole Main.frm file ...


VERSION 5.00
Begin VB.Form Main
   Caption         =   "Main"
   ClientHeight    =   3030
   ClientLeft      =   120
   ClientTop       =   450
   ClientWidth     =   4560
   LinkTopic       =   "Form1"
   ScaleHeight     =   3030
   ScaleWidth      =   4560
   StartUpPosition =   3  'Windows Default
   Begin VB.CommandButton btnShowForm3
      Caption         =   "Show Form3"
      Height          =   465
      Left            =   1140
      TabIndex        =   2
      Top             =   1800
      Width           =   2355
   End
   Begin VB.CommandButton btnShowForm2
      Caption         =   "Show Form2"
      Height          =   465
      Left            =   1140
      TabIndex        =   1
      Top             =   1110
      Width           =   2385
   End
   Begin VB.CommandButton btnShowForm1
      Caption         =   "Show Form1"
      Height          =   465
      Left            =   1140
      TabIndex        =   0
      Top             =   390
      Width           =   2385
   End
End
Attribute VB_Name = "Main"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Private Sub btnShowForm1_Click()
  Form1.Show
End Sub
Private Sub btnShowForm2_Click()
  Form2.Show
End Sub
Private Sub btnShowForm3_Click()
  Form3.Show
End Sub
               


As you can see by the three Private Subs at the end for the button click procedures, to show any Form, all one needed to do was call the Form::Show() method.

All three of the other files are exactly the same except for the '1', '2', and '3' numbers.  Here is Form2.frm...


VERSION 5.00
Begin VB.Form Form2
   Caption         =   "Form2"
   ClientHeight    =   3030
   ClientLeft      =   120
   ClientTop       =   450
   ClientWidth     =   4560
   LinkTopic       =   "Form2"
   ScaleHeight     =   3030
   ScaleWidth      =   4560
   StartUpPosition =   3  'Windows Default
End
Attribute VB_Name = "Form2"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Private Sub Form_Load()
  Main.Visible = False
End Sub
Private Sub Form_Unload(Cancel As Integer)
  Main.Visible = True
End Sub


So what the VB app does when started is display Main.frm with the three command buttons on it.  When you click any of the buttons that shows the Form number requested.  Each of the three Form procedures sets the visible property of the Main Form to False in its Form_Load() event, and sets it back to True in Form_Unload().   So I wrote a total of nine lines of code to make this app.  I'm not counting the Sub first and last lines because Visual Basic auto-generated them.  I think this example is a good example of an app that has several distinct 'modules' that do separate but related things.  I know I write a lot of apps like this where there is a main start up screen, where one then chooses which module of the program to start.  The project organization in the Visual Basic Project Explorer looks like so ...

- MultipleForms (MultipleForms.vbp)
    -  Forms
          Form1 (Form1.frm)
          Form2 (Form2.frm)
          Form3 (Form3.frm)
          Main (Main.frm)

     So how would one translate this into the PowerBASIC architecture I've been illustrating?  Well, it goes like this, and unfortunately we're going to have to write more than 9 lines of code!  After all, what I'm showing you here is what Visual Basic replaced in 1991.  But where has that got you now?  Visual Basic is dead and this technique still lives on.

     First thing is to recognize that each of the Form files above is an object whose purpose is going to be different from any of the others.  If that wasn't so it would be just as well to have a program with just one main module.  Thinking in terms Object Oriented Class theory, each file will contain code relating to a different class.  What that translates into then are four separate Window Classes - one for each Form in the program.   Two points to mention there.

     First, note that in filling out the members of the WNDCLASSEX struct/type there is a member there ...

                                                              WNDCLASSEX::lpfnWndProc

... I haven't discussed this yet but if you've looked at the way I've filled out previous programs I used PowerBASIC's CodePtr() function.  That you likely aren't familiar with.  It returns the address of a function.  And what WNDCLASSEX::lpfnWndProc wants is the address of the Window Procedure for the Window Class.  So what that means is that we are going to have four Window Procedures in this program because we are going to Register four Window Classes, i.e., one for the start up form, and three for the three other Forms - Form1, Form2, and Form3.  In terms of project organization, it will be exactly like the Visual Basic Project with its Form1.frm, Form2.frm, and Form3.frm.  But for PowerBASIC we'll use the *.inc extension for the three Forms and each form will contain its own Window Procedure.

     Second, where should we Register the additional Window Classes?  In all the programs up to now we only Registered one Window Class, and that was done in WinMain().  Well, I like to keep my WinMain()s simple, so if I register additional Window Classes for a program, I always do that in the WM_CREATE handler, i.e., VB's Form_Load(), for the start up Form.  Without further adieu, here is pbMultipleForms.bas...


'pbMultipleForms.bas
#Compile            Exe           "pbMultipleForms"
#Dim                All
%UNICODE            = 1           ' For wide character / ansi builds
%MyDebug            = 1           ' For Release / Debug Builds
#If %Def(%UNICODE)
    Macro ZStr      = WStringz    ' Example of a project with a main start up Form, and three separate modules that
    Macro BStr      = WString     ' perform distinctly different activities but are related in some way so as to make
    %SIZEOF_CHAR    = 2           ' incorporation into a single application logically desirable.
#Else
    Macro ZStr      = Asciiz      ' This program can be compiled as wide string or narrow by commenting in/out the
    Macro BStr      = String      ' UNICODE equate just above.  It uses BStr/ZStr as aliases for dynamic strings
    %SIZEOF_CHAR    = 1           ' generated by the OLE String Engine, or low level null terminated string buffers.
#EndIf
#If %Def(%MyDebug)
    Global          fp As Long
#EndIf
#Include            "Windows.inc"
#Include Once       "pbMultipleForms.inc"
#Include Once       "Form1.inc"
#Include Once       "Form2.inc"
#Include Once       "Form3.inc"


Function fnWndProc_OnCreate(Wea As WndEventArgs) As Long    ' This is the main start up Form, and in this WM_CREATE handler
  Local lpCreateStruct As CREATESTRUCT Ptr                  ' we'll create the three buttons, and Register Window Classes
  Local szClassName As ZStr*16                              ' for the Form1, Form2 and Form3 Window Classes.  Once these
  Local wc As WNDCLASSEX                                    ' classes are Registered, instances of them can be created from
  Local hWnd As DWORD                                       ' any point in the app.

  #If %Def(%MyDebug)
  Print #fp, "  Entering fnWndProc_OnCreate()"
  Print #fp, "    Wea.hWnd = " Wea.hWnd
  #EndIf
  lpCreateStruct=Wea.lParam : Wea.hInst=@lpCreateStruct.hInstance
  hWnd=CreateWindowEx(0,"button","Button 1",%WS_CHILD Or %WS_VISIBLE,50,25,125,25,Wea.hWnd,%IDC_BUTTON1,Wea.hInst,Byval 0)
  hWnd=CreateWindowEx(0,"button","Button 2",%WS_CHILD Or %WS_VISIBLE,50,60,125,25,Wea.hWnd,%IDC_BUTTON2,Wea.hInst,Byval 0)
  hWnd=CreateWindowEx(0,"button","Button 3",%WS_CHILD Or %WS_VISIBLE,50,95,125,25,Wea.hWnd,%IDC_BUTTON3,Wea.hInst,Byval 0)
  wc.cbSize=Sizeof(wc)
  wc.style=%CS_HREDRAW Or %CS_VREDRAW
  wc.cbClsExtra=0                                           ' We'll allocate 4 bytes of WNDCLASSEX::cbWndExtra bytes memory
  wc.cbWndExtra=4                                           ' within each instance to store the HWND of the Main Startup
  wc.hInstance=Wea.hInst                                    ' Form, i.e., pbMultipleForms.  We'll need that to make the
  wc.hIcon=LoadIcon(%NULL, Byval %IDI_APPLICATION)          ' main window visible after x'ing out of each Form module.
  wc.hCursor=LoadCursor(%NULL, Byval %IDC_ARROW)
  wc.hbrBackground=GetStockObject(%WHITE_BRUSH)
  szClassName="Form1"                                       ' Register Form1 Class
  wc.lpszClassName=Varptr(szClassName)
  wc.lpfnWndProc=Codeptr(fnForm1_WndProc)
  Call RegisterClassEx(wc)
  szClassName="Form2"                                       ' Register Form2 Class
  wc.lpszClassName=Varptr(szClassName)
  wc.lpfnWndProc=Codeptr(fnForm2_WndProc)
  Call RegisterClassEx(wc)
  szClassName="Form3"                                       ' Register Form3 Class
  wc.lpszClassName=Varptr(szClassName)
  wc.lpfnWndProc=Codeptr(fnForm3_WndProc)
  Call RegisterClassEx(wc)
  #If %Def(%MyDebug)
  Print #fp, "  Leavinging fnWndProc_OnCreate()" : Print #fp,
  #EndIf

  fnWndProc_OnCreate=0
End Function


Function fnWndProc_OnCommand(Wea As WndEventArgs) As Long
  Local hWnd As Dword

  #If %Def(%MyDebug)
  Print #fp, "  Entering fnWndProc_OnCommand()"
  Print #fp, "    Lowrd(Wea.wParam) = " Lowrd(Wea.wParam)
  Print #fp, "    Hiwrd(Wea.wParam) = " Hiwrd(Wea.wParam)
  #EndIf
  Select Case Lowrd(Wea.wParam)
    Case %IDC_BUTTON1                  'Button 1 was clicked
      hWnd=CreateWindowEx(0,"Form1","Form1",%WS_OVERLAPPEDWINDOW,50,25,300,175,Wea.hWnd,0,GetModuleHandle(Byval 0), Byval Wea.hWnd)
      Call ShowWindow(hWnd,%SW_SHOWNORMAL)
    Case %IDC_BUTTON2                  'Button 2 was clicked
      hWnd=CreateWindowEx(0,"Form2","Form2",%WS_OVERLAPPEDWINDOW,150,150,300,175,Wea.hWnd,0,GetModuleHandle(Byval 0), Byval Wea.hWnd)
      Call ShowWindow(hWnd,%SW_SHOWNORMAL)
    Case %IDC_BUTTON3                  'Button 3 was clicked
      hWnd=CreateWindowEx(0,"Form3","Form3",%WS_OVERLAPPEDWINDOW,%CW_USEDEFAULT,%CW_USEDEFAULT,300,250,Wea.hWnd,0,GetModuleHandle(Byval 0),Byval Wea.hWnd)
      #If %Def(%MyDebug)
      Print #fp, "    hWnd(Form3)  = " hWnd
      Print #fp, "    Button #3 Was Clicked!"
      Print #fp, "    %BN_CLICKED  = " %BN_CLICKED
      Print #fp, "    %IDC_BUTTON3 = " %IDC_BUTTON3
      #EndIf
      Call ShowWindow(hWnd,%SW_SHOWNORMAL)
  End Select
  #If %Def(%MyDebug)
  Print #fp, "  Leaving fnWndProc_OnCommand()" : Print #fp,
  #EndIf

  fnWndProc_OnCommand=0
End Function


Function fnWndProc_OnDestroy(Wea As WndEventArgs) As Long   ' When button #3 is clicked, the main startup form isn't made invisible
  Local hForm As Dword                                      ' like with button #2 and button #1.  So the user can keep clicking on
                                                            ' it to instantiate as many Form3 objects as he/she wishes.  They'll just
  #If %Def(%MyDebug)                                        ' keep piling up in the z order. 
  Print #fp, "  Entering fnWndProc_OnDestroy()"
  Print #fp, "    Wea.hWnd = " Wea.hWnd               
  #EndIf                                                   
  Call PostQuitMessage(0)
  #If %Def(%MyDebug)
  Print #fp, "  Leaving fnWndProc_OnDestroy()"
  #EndIf

  fnWndProc_OnDestroy=0
End Function


Function fnWndProc(Byval hWnd As Long,Byval wMsg As Long,Byval wParam As Long,Byval lParam As Long) As Long
  Local Wea As WndEventArgs

  Select Case wMsg                                          ' This is the Window Procedure for the pbMultipleForms Window Class.
    Case %WM_CREATE                                         ' It only needs to handle WM_CREATE, WM_COMMAND, and WM_CLOSE messages
      Wea.hWnd=hWnd : Wea.wParam=wParam : Wea.lParam=lParam ' In WM_CREATE, which can be considered an object Constructor call, the
      fnWndProc=fnWndProc_OnCreate(Wea)                     ' three child window control buttons are created, and the Window Classes
      Exit Function                                         ' for the three Forms are Registered.  In WM_COMMAND we use CreateWindowEx()
    Case %WM_COMMAND                                        ' to create instances of the requested class.  In WM_CLOSE we destroy any
      Wea.hWnd=hWnd : Wea.wParam=wParam : Wea.lParam=lParam ' windows hanging around, and call PostQuitMessage() to close the app.
      fnWndProc=fnWndProc_OnCommand(Wea)
      Exit Function
    Case %WM_DESTROY
      Wea.hWnd=hWnd : Wea.wParam=wParam : Wea.lParam=lParam
      fnWndProc=fnWndProc_OnDestroy(Wea)
      Exit Function
  End Select

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


Function WinMain(Byval hInstance As Long, Byval hPrevInst As Long, Byval lpCmdLn As ZStr Ptr, Byval iShow As Long) As Long
  Local szAppName As ZStr*16
  Local wc As WNDCLASSEX                                    ' This is the program entry point, and its sole job is to Register
  Local Msg As tagMsg                                       ' the application's main Window Class (the start up Form) and create
  Local hWnd As Dword                                       ' an instance of that class.  Then it enters a message loop for the
                                                            ' duration of program execution
  #If %Def(%MyDebug)
  fp=Freefile : Open "Output.txt" For Output As #fp
  Print #fp, "Entering WinMain()"
  #EndIf
  szAppName="pbMultipleForms"                      : wc.lpszClassName=Varptr(szAppName)
  wc.lpfnWndProc=Codeptr(fnWndProc)                : wc.cbSize=Sizeof(wc)
  wc.hInstance=hInstance                           : wc.hIcon=LoadIcon(%NULL, Byval %IDI_APPLICATION)
  wc.hCursor=LoadCursor(%NULL, Byval %IDC_ARROW)   : wc.hbrBackground=%COLOR_BTNFACE+1
  Call RegisterClassEx(wc)
  hWnd=CreateWindowEx(0, szAppName, "pbMultipleForms", %WS_OVERLAPPEDWINDOW, 450, 300, 240, 190, 0, 0, hInstance, Byval 0)
  #If %Def(%MyDebug)
  Print #fp, "  hWnd = " hWnd   "  <<< From WinMain() Right After CreateWindowWx() Call": Print #fp,
  #EndIf
  Call ShowWindow(hWnd, iShow)
  Call UpdateWindow(hWnd)
  While GetMessage(Msg, %NULL ,0, 0)
    Call TranslateMessage(Msg)
    Call DispatchMessage(Msg)
  Wend
  #If %Def(%MyDebug)
  Print #fp, "Leaving WinMain()"
  Close #fp
  #EndIf

  Function=msg.wParam
End Function


But that's not all!  We've still got 4 files to go!  Here is pbMultipleForms.inc...


'pbMultipleForms.inc

%IDC_BUTTON1        =  2000
%IDC_BUTTON2        =  2005
%IDC_BUTTON3        =  2010


Type WndEventArgs
  wParam            As Long      'Package parameters
  lParam            As Long      'to Window Procedure
  hWnd              As Dword     ' in TYPE
  hInst             As Dword
End Type


Now Form1.inc...


'Form1.inc

Function fnForm1_WndProc(Byval hWnd As Long,Byval wMsg As Long,Byval wParam As Long,Byval lParam As Long) As Long
  Local hMain As Dword

  Select Case As Long wMsg
    Case %WM_CREATE
       Local lpCreateStruct As CREATESTRUCT Ptr
       lpCreateStruct=lParam
       hMain=@lpCreateStruct.lpCreateParams
       Call SetWindowLong(hWnd,0,hMain)
       EnableWindow(hMain,%FALSE)
       fnForm1_WndProc=0 : Exit Function
    Case %WM_PAINT
      Local iLEN As Long, hDC As Long
      Local LpPaint As PAINTSTRUCT
      Local rc As Rect
      hDC = BeginPaint(hWnd, LpPaint)
      rc.nTop=0 : rc.nBottom=16 : rc.nLeft=0 : rc.nRight=300
      Call DrawText(hDC,"This Is Form1.  It Disables The Main",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Window, And That Makes It Modal.",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Note That A Pointer To The Main hWnd",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Was Received During %WM_CREATE ",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"And Stored In The cbWndExtra Bytes.",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"We'll Need That To Call EnableWindow()",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Upon Destruction Of This Dialog.",-1,rc,%DT_SINGLELINE)
      Call EndPaint(hWnd,LpPaint)
      fnForm1_WndProc = 0 : Exit Function
    Case %WM_CHAR
      If wParam=27 Or wParam=13 Then
         SendMessage(hWnd,%WM_CLOSE,0,0)
      End If
      Function=0 : Exit Function
    Case %WM_CLOSE
      If MsgBox("Do You Wish To Destroy This Window?",%MB_YESNO,"Destroy Window")=%IDYES Then
         hMain=GetWindowLong(hWnd,0)
         EnableWindow(hMain,%TRUE)
         fnForm1_WndProc=0
         Call DestroyWindow(hWnd)
      End If
      Function=0 : Exit Function
  End Select

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


...and Form2.inc...


'Form2.inc

Function fnForm2_WndProc(Byval hWnd As Long,Byval wMsg As Long,Byval wParam As Long,Byval lParam As Long) As Long
  Local hMain As Dword

  Select Case As Long wMsg
    Case %WM_CREATE
      Local lpCreateStruct As CREATESTRUCT Ptr
      lpCreateStruct=lParam : hMain=@lpCreateStruct.lpCreateParams
      Call SetWindowLong(hWnd,0,hMain)
      Call ShowWindow(hMain,%SW_HIDE)
      fnForm2_WndProc=0 : Exit Function
    Case %WM_PAINT
      Local hDC As Long
      Local LpPaint As PAINTSTRUCT
      Local rc As Rect
      hDC = BeginPaint(hWnd, LpPaint)
      rc.nTop=0 : rc.nBottom=16 : rc.nLeft=0 : rc.nRight=300
      Call DrawText(hDC,"This Is Form2.  It %SW_HIDEs The",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Main Window, And %SW_SHOWs It",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Upon Closing.  The Technique Can",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Be Used Similiarly To A Modal ",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Dialog If It Isn't Necessary To",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"View Simultaneously A Form Under-",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"neath The Dialog With Which You",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Can't Interact Anyway.",-1,rc,%DT_SINGLELINE)
      Call EndPaint(hWnd,LpPaint)
      fnForm2_WndProc = 0 : Exit Function
    Case %WM_CLOSE
      If MsgBox("Do You Wish To Destroy This Window?",%MB_YESNO,"Destroy Window")=%IDYES Then
         hMain=GetWindowLong(hWnd,0)
         Call ShowWindow(hMain,%TRUE)
         Call DestroyWindow(hWnd)
      End If
      fnForm2_WndProc = 0 : Exit Function
  End Select

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


...and finally Form3.inc ...


'Form3.inc

Function fnForm3_WndProc(Byval hWnd As Long,Byval wMsg As Long,Byval wParam As Long,Byval lParam As Long) As Long
  Local lpCreateStruct As CREATESTRUCT Ptr
  Local iLEN As Long, hDC As Long
  Local LpPaint As PAINTSTRUCT
  Local rc As Rect

  Select Case wMsg
    Case %WM_CREATE
       #If %Def(%MyDebug)
       Print #fp, "    Entering fnForm3_WndProc() - Case WM_CREATE"
       Print #fp, "      hWnd = " hWnd
       Print #fp, "    Leaving fnForm3_WndProc()  - Case WM_CREATE
       #EndIf
       fnForm3_WndProc=0 : Exit Function
    Case %WM_PAINT
      hDC = BeginPaint(hWnd, LpPaint)
      rc.nTop=0 : rc.nBottom=16 : rc.nLeft=0 : rc.nRight=300
      Call DrawText(hDC,"This Is Form3.  Not Only Does It",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Neither Hide Nor Disable The Main",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Window, But You'll Find That You",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Can Create As Many Of These As You",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Want By Continually Clicking Button",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Number Three!  Only Thing Is, You'll",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Have To Drag Them From On Top Of Each",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Other, As They All Appear In The Same",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Location.  You May Further Note That",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Since These Windows Are Neither",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"Disabled Nor Hidden At Any Time, You",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"May Interact With Them Irregardless Of",-1,rc,%DT_SINGLELINE)
      rc.nTop=rc.nTop+16 : rc.nBottom=rc.nTop+16
      Call DrawText(hDC,"The State Of Form1 Or Form2",-1,rc,%DT_SINGLELINE)
      Call EndPaint(hWnd,LpPaint)
      fnForm3_WndProc = 0
      Exit Function
    Case %WM_DESTROY
      #If %Def(%MyDebug)
      Print #fp, "    Entering fnForm3_WndProc() - Case WM_DESTROY"
      Print #fp, "      hWnd = " hWnd
      Print #fp, "    Leaving fnForm3_WndProc()  - Case WM_DESTROY
      #EndIf 
      fnForm3_WndProc=0 : Exit Function
  End Select

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


     Compile and run that program and let me know what you think.  You should have a main menu as the start up Form, and clicking on any of the three buttons will launch that Form associated with the button clicked.  Form1 emulates a modal dialog box, and won't allow you to proceed until you dismiss the dialog box by x'ing out, hitting the [ESC] key, or [RETURN] key. 

     Unlike Form1, Form2 renders the main menu form invisible  - just as with the VB program.  When you x out of that the main menu returns.

     Form3 behaves quite differently.  You can click Button #3 on the main menu as many times as you like to create as many instances of Form3 as you want.  And actually, you can create as many Form3s as you want and still be able to click Button #1 or Button #2 on the Main Menu.  So all three of these Forms have different behaviors.  Now I'll discuss the code, which has quite a few changes in it from what you've seen so far.

     I added the capability to easily generate Release / Debug builds of this code.  If the equate %MyDebug is defined, an Output.txt file is opened in the project directory, and voluminous debug output is generated.  Here are the results from a program run where I started the program, clicked twice on Button #3 to create two instances of Form3, then x'ed out of the whole program from the Main Menu without closing the two instances of Form3 first...


Entering WinMain()
  Entering fnWndProc_OnCreate()
    Wea.hWnd =  2163212
  Leavinging fnWndProc_OnCreate()

  hWnd =  2163212   <<< From WinMain() Right After CreateWindowWx() Call

  Entering fnWndProc_OnCommand()
    Lowrd(Wea.wParam) =  2010
    Hiwrd(Wea.wParam) =  0
    Entering fnForm3_WndProc() - Case WM_CREATE
      hWnd =  1114648
    Leaving fnForm3_WndProc()  - Case WM_CREATE
    hWnd(Form3)  =  1114648
    Button #3 Was Clicked!
    %BN_CLICKED  =  0
    %IDC_BUTTON3 =  2010
  Leaving fnWndProc_OnCommand()

  Entering fnWndProc_OnCommand()
    Lowrd(Wea.wParam) =  2010
    Hiwrd(Wea.wParam) =  0
    Entering fnForm3_WndProc() - Case WM_CREATE
      hWnd =  1508650
    Leaving fnForm3_WndProc()  - Case WM_CREATE
    hWnd(Form3)  =  1508650
    Button #3 Was Clicked!
    %BN_CLICKED  =  0
    %IDC_BUTTON3 =  2010
  Leaving fnWndProc_OnCommand()

  Entering fnWndProc_OnClose()
    hForm =  1508650
    Entering fnForm3_WndProc() - Case WM_DESTROY
      hWnd =  1508650
    Leaving fnForm3_WndProc()  - Case WM_DESTROY
    hForm =  1114648
    Entering fnForm3_WndProc() - Case WM_DESTROY
      hWnd =  1114648
    Leaving fnForm3_WndProc()  - Case WM_DESTROY
    hForm =  0
  Leaving fnWndProc_OnClose()
Leaving WinMain()
   
 
Unlike our other programs, this one has several more includes ...

#Include            "Windows.inc"
#Include Once       "pbMultipleForms.inc"
#Include Once       "Form1.inc"
#Include Once       "Form2.inc"
#Include Once       "Form3.inc"


While it would have been possible to keep everything in one file, I personally like breaking things up into logically separate and distinct pieces.  I have programs with 50,000 lines of code, and trying to navigate through that much all in one file isn't my cup of tea. 

     You might find the screen output in the various forms to be interesting, in that it shows you how to print directly to a window/form/dialog.  In VB Me.Print worked.  In direct Api coding you use either TextOut() or DrawText().  TextOut() is actually easier, so check that out.

... to be continued ...