Finished C++ Conversion Of PowerBASIC Grid Code. I'm Happy!

Started by Frederick J. Harris, January 03, 2014, 05:46:50 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

Thanks Patrice. 

No.  The issue you mentioned is one I considered heavily before I started.  My PowerBASIC code of course was procedural, as it had to be.  Using such simple low level techniques produces very small tight code, as you know. 

What decided me to use C++ 'isms', i.e., classes, was the old and now familiar string class issue.  The way the user passes column information into the grid is a comma delimited monstrosity like so ...

strSetup  = SysAllocString(L"110:Column 1:^:edit,110:Column 2:^:edit,110:Column 3:^:edit,110:Column 4:^:edit,110:Column 5:^:combo");

So you see there are commas delimiting the data for each column, i.e.,

110:Column 1:^:edit

And that string further contains ':' symbols to delimit further characteristics of the field/column.  Now that sort of parsing is unbelievably miserable with the C string primitives.  It can be done but its miserable.  So I figured I'd be nuts to handicap myself with that stuff on an already difficult endeavor such as this.  Also considering I had spent so much effort developing my String Class and the parsing works so well.

Having said that, I may still do it just for kicks and giggles.  I'm like that.  Its my guess I could knock off another 10 to 20 K if I'd eliminate all uses of C++ isms such as the class keyword.  If I'm right there, that could bring the packed binary down close to the PowerBASIC one of 22K.  Maybe 30-35 K or so.

Frederick J. Harris

     It turned out to be a good thing I recreated the grid in C++, because over the course of the past couple years I found two problems with the PowerBASIC grid which I managed to easily fix in the C++ code, and when I did that I went back and fixed the PowerBASIC grid code too.  So now they are both working as I want them to.  I see some forty of fifty folks downloaded my zip file of the PowerBASIC grid, so I'll get to fixing it there and I'll add a new zip shortly.

     The first problem with the PowerBASIC grid code was I had forgotten to add %SIF_DISABLENOSCROLL in the grid's fnGridProc_OnSize() WM_SIZE event handler.  What happened there to my dismay, was that if the grid was instantiated on a client where the column setup was such that a horizontal scroll bar wasn't needed, and the user then used the header control to widen a column to the point where the scrollbar became necessary, it wasn't showing up.  Somehow I had missed that in my extensive testing.  When I discovered the problem I was able to work around it by always specifying in the initial grid setup column widths that initially required a horizontal scroll bar.  If you did that and the user adjusted the columns smaller, everything worked OK. 

     The second problem was that my grid doesn't show partial rows and has logic within it to find the maximum number of full rows that can be seen, given however the user set the desired row height as compared to the client rectangle size of the grid.  So unless the usable client rectangle integer divided by the specified row height came out with no remainder, an unsightly strip in between the horizontal scrollbar and the bottom row would appear. I fixed that by adjusting the original size of the grid as specified by the user either up or down a few pixels so the division would come out perfect with no remainders.  This latter problem didn't affect the functionality of the grid like the first problem, but it looked bad.  So its fixed now.

     Now I'd like to pass on some of my reflections on converting my PowerBASIC grid code to C++.  It actually went really smooth.  Here, however, was something of a comical surprise.  Let me explain.  When I need a little dynamic memory (or a lot), I'll frequently allocate one more memory slots than I'll need, and store the present count of whatever in the zeroeth memory location.  For example, let's say I need room for 10 Dwords, and just for exposition purposes, let's say I need to store the numbers 1 through 5 in the first five slots like so ...


Local pDwords As Dword Ptr
Register i As Long

pDwords=GlobalAlloc(%GPTR, 11 * sizeof(Dword))
If pDwords Then
   For i=1 To 5
     @pDwords[i]=i
   Next i
   @pDwords[0] = 5
End If


     What I just did there is allocated memory for 11 Dwords but I only used 5 of the slots to store the numbers I needed (1 through 5).  I stored the present count of the 5 Dwords I stored at @pDwords[0].  That notation allows me to use 1 based indexing instead of zero based indexing, and to keep some kind of total in the zeroeth slot at offset zero.  I don't know if others do this but I fell into this 'design pattern' many years ago and use it all the time. It saves storing counts in some seperate variable.

     Anyway, my PowerBASIC grid code has a 'GridData' struct/type for instance data where I store Window HANDLEs of piles of things the grid needs such as grid cell handles, and HANDLEs of lots of other things a grid needs.  One of those types of 'instance data' were HBRUSHs for the background of each cell, and also for when the user 'selects' a grid row.  One of the grid interface members is IGrid::SetCellAttributes(...).  In that code I first run logic to see if the grid previously created and stored an HBRUSH for the color the user specified through the parameters of the call, and since we usually use Dwords for such things in PowerBASIC, being as an HBRUSH is just an integral value not really fundamentally different from a count or integer, one of the members of my GridData type was a GridData::pCreatedBrushes, and I had it 'typed' as a Dword Ptr.  In that procedure, in the event an HBRUSH was requested which the grid hadn't yet created, I'd first do this to increment the count of HBRUSHs presently stored at @pGridData.@pCreatedBrushes[0].  Lets assume this is the first HBRUSH the grid is creating, and the number presently stored at ...

@pGridData.@pCreatedBrushes[0]

... is zero.  So to increment that one would just do this ...

Incr @pGridData.@pCreatedBrushes[0]

After that operation in PowerBASIC one could expect to find the value '1' stored in that slot, because we are incrementing a Dword stored at that memory location.  If zero was stored there and its been incremented there should be a '1' there now.  Now lets switch over to C++ and we'll see the plot thicken quite a bit!

     When I converted my GridData type to C/C++ syntax I dutifully typed the various members to what they actually were in terms of the strong typing of that language.  In other words, my buffer to hold HBRUSHs was an HBRUSH* (HBRUSH Pointer) instead of a Dword Pointer.  Just for comparison purposes, here are the GridData types, 1st in PowerBASIC, then in C++ ...


Type GridData                                           'This object is used to maintain 'state' in the grid control.
  iCtrlID                             As Long           'Each instantiation of a grid object will cause one of these
  hParent                             As Dword          'to be dynamically allocated, and the pointer to the allocated
  hGrid                               As Dword          'storage will be stored at offset zero in the grid object's
  hBase                               As Dword          'WNDCLASSEX::cbWndExtra bytes.  The IGrid Interface has a method
  hPane                               As Dword          'named CreateGrid() implemented in IGrid_CreateGrid(), and it is
  hCtrlInCell                         As Dword          'there where the CreateWindowEx() call is made that starts
  cx                                  As Dword          'construction of the grid.  The actual grid construction code
  cy                                  As Dword          'is largely contained in function fnGridProc_OnCreate(), which
  hHeader                             As Dword          'is the WM_CREATE handler for objects of class "Grid".
  iCols                               As Dword
  iRows                               As Dword          'GridData::pComObj is particularly noteworthy.  GridData, which
  iVisibleRows                        As Dword          'holds mostly GUI specific data, holds this pointer to the COM
  iRowHeight                          As Dword          'specific data of the IGrid Interface.  Likewise, the CGrid object,
  iPaneHeight                         As Dword          'which holds COM specific data relating to the COM plumbing of the
  iEditedCellRow                      As Long           'grid, i.e., the addresses of the various VTables, also stores the
  iEditedRow                          As Long           'hWnd of the grid there in CGrid::hWndCtrl.  In a sense, this is
  iEditedCol                          As Long           'the interface or conduit between these two types of objects created
  pComObj                             As Dword Ptr      'by an instantiation of a grid in a host object.
  pColWidths                          As Dword Ptr
  pCellCtrlTypes                      As Dword Ptr      'iEditedRow refers to the row in the larger grid buffer.
  pCellHandles                        As Dword Ptr      'iEditedCellRow refers only to rows counting from 1 up to iVisibleRows.
  pGridMemory                         As Dword Ptr      'Will be storing ZStr Ptrs here
  pTextColor                          As Dword Ptr      'Will be storing RGB values here
  pBackColor                          As Dword Ptr      'Will be storing HBRUSHs here.  May be zero for default brush.
  pCreatedColors                      As Dword Ptr      'Colors so far asked for by user per grid instance
  pCreatedBrushes                     As Dword Ptr      'Will be storing created HBRUSHs here.  Accumulate them.
  pVButtons                           As Dword Ptr
  pCtrlHdls                           As Dword Ptr      'Another really important variable in GridData is hCtrlInCell.  See
  iSelectionBackColor                 As Long           'my comments in fnCellProc() for that.  But in summary, this member
  iSelectionTextColor                 As Long           'holds a handle to the edit control superimposed over a cell when
  blnRowSelected                      As Long           'the user WM_LBUTTONDOWNs over a cell.  When no cell is active with
  iSelectedRow                        As Long           'an edit control in it, this member is set to zero.  So in that sense
  iFontSize                           As Long           'it acts both as the handle to the edit control, and as a boolean to
  iFontWeight                         As Long           'indicate that a cell is active and being edited.
  hFont                               As Dword
  szFontName                          As ZStr * 28
End Type




struct GridData
{
int         iCtrlId;
HWND        hParent;
HWND        hGrid;
HWND        hBase;
HWND        hPane;
HWND        hCtrlInCell;
int         cx;
int         cy;
HWND        hHeader;
int         iCols;
int         iRows;
int         iVisibleRows;
int         iRowHeight;
int         iPaneHeight;
int         iEditedCellRow;
int         iEditedRow;
int         iEditedCol;
FHGrid*     pComObj;
DWORD*      pColWidths;
DWORD*      pCellCtrlTypes;
HWND*       pCellHandles;
TCHAR**     pGridMemory;
COLORREF*   pTextColor;
HBRUSH*     pBackColor;
COLORREF*   pCreatedColors;
HBRUSH*     pCreatedBrushes;
HWND*       pVButtons;
HWND*       pCtrlHdls;
COLORREF    iSelectionBackColor;
COLORREF    iSelectionTextColor;
int         blnRowSelected;
int         iSelectedRow;
int         iFontSize;
int         iFontWeight;
HFONT       hFont;
TCHAR       szFontName[28];
};


     So what happened then when I performed this operation to increment the value stored at pGridData->pCreatedBrushes[0] = 0 wasn't what I initially expected.  If there was a 0 there I expected to see a 1 like with the PowerBASIC code.  But alas my 0 became an 8! My initial reaction when I was looking at my debug output file to trace the creation of my first HBRUSH was 'where in the world did those 7 other HBRUSHs come from?'  Then it soon dawned on me that any kind of HANDLE in Windows, whether a HANDLE to a Window (HWND), a HANDLE to a device context, or a HANDLE to a brush, is really typed somewhere in the obscure Windows headers as a void* or void** (not sure which?), and so the compiler is seeing a pointer where I'm seeing a simple integral value (a count), and hence it is incrementing by the amount it needs to point to the next pointer, which is 4 for x86 and 8 for x64!

     Of course, one could argue that I shouldn't be using that buffer position at offset zero to store a count, if the buffer is supposed to hold virtual memory pointers, but I can be easily forgiven such misbehavior because after all I'm only forester – not a computer science graduate type IT professional!  So what did I do?  All I'll say is it got done.  Where there was a 0 that was supposed to become a 1 there is a 0 that became a 1.  Lot of ways to 'skin a cat'.

     But I'm mentioning this because one of the big conceptual hurdles you have to overcome in moving to x64 is that the equality between the size of an integral value and the size of an integral value pointer may no longer hold.  And of course there is the issue of strong typing as described above.

     The other significant bug I ran into in making the conversion actually cost me more time than what I just described above, which only cost me a few minutes.  In my grid I have a call to EnumChildWindows(), and that function requires the address of a user written callback function where Windows returns the HWNDs of child Windows of the parent specified.  Here is my PowerBASIC Callback ...


Function EnumGridProc _                                        ' The purpose of this code here is to solve a typical grid problem of
( _                                                            ' having an edit control stuck in a grid cell where the text in the
  Byval hWnd As Long, _                                        ' edit control hasn't yet been written to the underlying grid storage,
  Byval lParam As Dword _                                      ' and the user clicks a button or something, and processing occurs
) As Long                                                      ' without the user's new edit being used.  In other words, some way
  If GetClassLong(hWnd,%GCL_WNDPROC)=lParam Then               ' had to be found to guarantee that user edits are immediately written...
     #If %Def(%DEBUG)
     Print #fp, "    Called EnumGridProc() - ", hWnd, lParam
     #EndIf
     Local pGridData As GridData Ptr                           ' ..to the grid's storage when the user clicks elsewhere in the same grid
     pGridData=GetWindowLong(hWnd,0)                           ' as an edit was made, or any other grids on the user's Form.  So this
     Call IGrid_FlushData(@pGridData.pComObj)                  ' code here runs in response to a user's WM_LBUTTONDOWS in any grid on
  End If                                                       ' the Form.  The WM_LBUTTONDOWN will occur in fnCellProc() just below.
  Function=%True                                               ' See Call EnumChildWindows(). The way it works is that the address of
End Function                                                   ' the grid's WndProc is passed into this procedure through the lParam ...


     At the time I converted this to C++ I was working with the 64 bit C++ compiler, and here was my translation at the time, which worked perfectly ...


BOOL EnumGridProc(HWND hWnd, LPARAM lParam)
{
#ifdef MYDEBUG
_ftprintf(fp,_T("  Called EnumGridProc() - %u\t%u\n"),hWnd,lParam);
#endif
if(GetClassLongPtr(hWnd,GCLP_WNDPROC)==lParam)
{
    #ifdef MYDEBUG
    _ftprintf(fp,_T("\n  Made A Match! - %u\t%u\n\n"),hWnd,lParam);
    #endif
    GridData* pGridData=(GridData*)GetWindowLongPtr(hWnd,0);
    pGridData->pComObj->FlushData();
}

return TRUE;
}


However, when I then tested that construction for 32 bit x86 C++ guess what?  Crashes! GPFs!  So isn't that strange?  Why would what I did work flawlessly in PowerBASIC, which is 32 bit, and 64 bit C++, but crash hard in 32 bit C++? I lost a couple hours on that one.  The answer, like in so many cases with computer bugs, was staring me in the face, and I just wasn't seeing it.  I don't know how many times I looked at the documentation for EnumChildProc in the MSDN documentation ...


BOOL CALLBACK EnumChildProc
(
  HWND hwnd,      // handle to child window
  LPARAM lParam   // application-defined value
);


... but I just wasn't seeing the answer.  I thought I was doing all that.  But why the crashes in x86 but not in x64?  And why not in PowerBASIC?  Well, I eventually searched for a C example of EnumChildWindows and for some reason I noted the 'CALLBACK' specifier there where I simply wasn't seeing it in the MSDN documentation right above where I posted it for EnumChildProc.  CALLBACK is a typedef or #define of __stdcall, and of course in PowerBASIC that is the default stack setup protocol.  In x64 CALLBACK is defined as nothing, I believe, because the x64 stack setup protocol isn't quite as involved as x86 with all the permissible stack setup choices, i.e., __stdcall, __cdecl, all the various synonyms for those, etc.  So my 64 bit code didn't need the CALLBACK specifier.  Even if I had put it there it would have had no positive or negative effect.  So no problem there.  Not so with 32 bit C++.  My EnumGridProc() callback for my EnumChildindows() call would have defaulted to __cdecl not __stdcall in 32 bit, and the call is a __stdcall function call.  That pretty quickly trashes memory and GPFs.  I thought that little problem interesting enough that I wanted to share it, as it did a good job of highlighting an interesting and noteworthy feature of x64 involving calling convention and calling convention specifiers.

     One other thing I might mention, even though I love coding in PowerBASIC, occasionally I find something that works better in C++.  One thing that jumped out at me there is the syntax for calling interface functions low level using standard C++ pointer syntax instead of Call Dword.  For example, here are some code snippets from the edit control subclass procedure that shows how keypress events are sent back to the client's sink from the edit control capturing them.  First, here is the PowerBASIC code ...


Function fnEditSubClass(ByVal hEdit As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
  Local hCell,hPane,hBase,hGrid As Dword
  Local pGridData As GridData Ptr
  Local Vtbl,dwPtr As Dword Ptr
  Local hr,blnCancel As Long
  Local pGrid As CGrid Ptr
  Register i As Long

  hCell=GetParent(hEdit) : hPane=GetParent(hCell)
  hBase=GetParent(hPane) : hGrid=GetParent(hBase)
  pGridData=GetWindowLong(hPane,0)
  pGrid=@pGridData.pComObj
  Select Case As Long wMsg
    Case %WM_CHAR
      For i=0 To %MAX_CONNECTIONS-1
        If @pGrid.@pISink[i] Then
           dwPtr=@pGrid.@pISink[i]
           VTbl=@dwPtr
           Call Dword @Vtbl[3] Using ptrKeyPress(dwPtr, wParam, lParam, @pGridData.iEditedRow, @pGridData.iEditedCol, blnCancel) To hr
           If blnCancel Then
              Exit Function
           End If
        End If
      Next i
      ...
      ...


And here is the C++ code doing the same thing ...


LRESULT CALLBACK fnEditSubClass(HWND hEdit, UINT msg, WPARAM wParam, LPARAM lParam)
{
int blnCancel=0;
HWND hCell=GetParent(hEdit),   HWND hPane=GetParent(hCell);
HWND hBase=GetParent(hPane),   HWND hGrid=GetParent(hBase);
GridData* pGridData=(GridData*)GetWindowLongPtr(hGrid,0);
switch(msg)
{
   case WM_CHAR:
     {
        for(int i=0; i<MAX_CONNECTIONS; i++)
        {
            if(pGridData->pComObj->pISink[i])
            {
               pGridData->pComObj->pISink[i]->Grid_OnKeyPress((int)wParam, (int)lParam, pGridData->iEditedRow, pGridData->iEditedCol, &blnCancel);
               if(blnCancel)
                  return 0;
            }
        }
        ...
        ...


     Point being, in C/C++ one doesn't have to perform these sorts of mentally difficult circumlocutions to get the call executed at the correct address ..


dwPtr=@pGrid.@pISink[i]
VTbl=@dwPtr


     So make of that what you will.

     Time I guess to post the Client code.  I'll attach the debug and release code for the grid, as well as the 64 bit test client, i.e. Client5.cpp and Client5.h.  Since everyone here likely can compile the PowerBASIC client, I'll just post the source code for that.  Before I go and leave everyone with piles of code they may or may not be interested in, and may or may not know how to use, let me say a few words about setting things up and getting it to work in C++.

     You'll note a few #defines in the clients.  Here is what the top few lines of Client5.cpp looks like ...


// cl Client5.cpp Strings.cpp Kernel32.lib User32.lib Gdi32.lib UUID.lib Ole32.lib OleAut32.lib /O1 /Os /MT /GA
// g++ Client5.cpp Strings.cpp -lole32 -loleaut32 -luuid -oClient5_MinGW64.exe -mwindows -m64 -s -Os
// CD C:\Code\VStudio\VC++9\64_Bit\FHGrid
// CD C:\Code\VStudio\VC++9\FHGrid
#define     UNICODE
#define     _UNICODE
#define     MyDebug
#define     VStudio
#include    <windows.h> 
#include    <objbase.h>
#include    <tchar.h>
#include    <fcntl.h>
#include    <io.h>
#include    <stdio.h>
#include    <ocidl.h>
#include    "Client5.h"
#include    "Strings.h"
const CLSID CLSID_FHGrid       =   {0x30000000, 0x0000, 0x0000, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}};
const IID   IID_IFHGrid        =   {0x30000000, 0x0000, 0x0000, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}};
const IID   IID_IFHGridEvents  =   {0x30000000, 0x0000, 0x0000, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02}};
const IID   LIBID_FHGrid       =   {0x30000000, 0x0000, 0x0000, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03}};
 

     You'll note the #define MyDebug.  That symbol is in both the client and the dll based control.  If it isn't commented out in the client, i.e., //#define MyDebug, the client will do an AllocConsole() in the WM_CREATE message handler.  That will cause voluminous printf calls in the client, and also in the dll if a version of the dll is running that was also compiled with MyDebug not commented out.  Note it needs to be commented in or out in four different server files.  I also created another #define, i.e., VStudio.  I needed to do that because Microsoft changed some rather obscure code in the C runtime related to low level i/o handles.  The printf function I used to write to the console needs some special handling to get it to work in GUI apps.  You'll see that near the top of fnWndProc_OnCreate().  If MinGW is used instead of Microsoft's later compilers you need to comment out the VStudio define.  If by some slim chance you are using ancient VC6 it also needs commented out for that.  My guess is that any post 2000 Microsoft compiler will need VStudio defined to compile/work, and any GNU compiler will need it commented out.

     The other #define to watch, is #define X64_GRID.  That is found in Server.cpp of the dll grid code.  I've previously discussed this, but that equate was necessitated by a changed enumeration used by UnRegisterTypeLib() in DllUnregisterServer() – one of the exported COM functions. Better for me to just show how that's used ...


#if defined X64_GRID
    hr=UnRegisterTypeLib(LIBID_FHGrid,1,0,LANG_NEUTRAL,SYS_WIN64);
#else
    hr=UnRegisterTypeLib(LIBID_FHGrid,1,0,LANG_NEUTRAL,SYS_WIN32);
#endif


     So depending on whether you are running a 32 bit or 64 bit build, it needs to be right if you want the thing to have un-registration capability.  The grid itself will register and work if you get it wrong -  its just that unregistration will fail.  I guess that won't cause the world to end or your crops to fail, but if you are interested in this code you likely will want to get it right.

     All in all I don't think that's a whole lot of bother for getting a bunch of code to work across both x86 and x64 for the exact same code, and even further considering that the exact same GUIDs are used throughout.  And in terms of Client5.cpp, it's the same exact code across both Microsoft's and GNU compilers for both 32 and 64 bit. 

     Also, there aren't any project files.  I did everything with command line compiling.  I've come full circle.  Started out that way, used bigger and bigger and bigger IDEs for many years, and am now back to command line compiling.  I have Visual Studio 2008 Pro but can't stand the editor.  My XP laptop almost can't tolerate it.  There is some kind of component it uses called devenv or something that causes the processor to max out at 100% and if I'm running on batteries it'll drain a battery in 20 minutes!  I've a really powerful behemoth Dell M6500 laptop but won't run it on that anymore just on general principals.  Actually, I used the Code::Blocks editor for a lot of the coding even though I couldn't get it to work with Microsoft's 64 bit compiler toolchain.  But anyway, the command lines for compiling are at the top of each client source code file, and in the server (for the grid in the dll) are at the top of Server.cpp.  The only other #define to watch is the previously mentioned JUST_NEED_PARSE in Strings.cpp and Strings.h.  For the dll you'll want to have that in, and for the clients comment it out.  In terms of UNICODE and _UNICODE – leave them in.  COM works natively with wide character strings.  I tested the registry code for both ansi and wide character, but COM code really needs to see wide character OLE strings.

     I guess I could say a few words about where necessary compiler files and SDKs are stored now that the world is getting harder to use.  I spent a good many days just trying to find things.  And I'm not a shell script wizard like some here no doubt are.  With the Microsoft compilers I finally gave up trying to set up everything on my own, and I used Microsoft's command line shortcuts put on my Start menu for setting up the environment for either x86 or x64 builds.  While mine were installed with VStudio, I imagine if you just download and install the Windows 7 or 8 SDKs you'll get the necessary batch files or shell scripts to properly invoke the compiler toolchain and find libraries and includes.  This is what executes from my shortcut to do command line compiling for 32 bit builds ...

%comspec% /k ""c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat"" x86
 
And this for 64 bit ...
     
%comspec% /k ""c:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat"" amd64
   
In the above you can see the x86 or amd64 command line parameter is being passed into vcvarsall.bat.
 

Frederick J. Harris

Here's the Client5 code that can be compiled x64/x86, MS or GNU.

Frederick J. Harris

#18
Last but not least here is a PowerBASIC client that will work with a registered FHGrid.dll in x86 flavor.  Of course, one could do a registry free load too.  This is the one that came in 25K.  Note there is a Combo Box in Column 5.  To make the combo box column easier toi use pull cols 1, 2 or 3 left to make them smaller.  This will cause the horizontal scrollbar to disappear.  Then widen column 5.  If you want, stretch the other columns back.  Just thought I'd mention that because it might not be immediately obvious.


#Compile                  Exe  "PBClient32.exe"
#Dim                      All
%UNICODE                  = 1
'%Debug                    = 1  ' Uncomment this to produce a console window for Debug Output.
#If %Def(%UNICODE)
    Macro ZStr            = WStringz
    Macro BStr            = WString
    %SIZEOF_CHAR          = 2
#Else
    Macro ZStr            = Asciiz
    Macro BStr            = String
    %SIZEOF_CHAR          = 1
#EndIf
$CLSID_FHGrid             = GUID$("{30000000-0000-0000-0000-000000000000}")
$IID_IFHGrid              = GUID$("{30000000-0000-0000-0000-000000000001}")
$IID_IGridEvents          = GUID$("{30000000-0000-0000-0000-000000000002}")
%IDC_RETRIEVE             = 1500
%IDC_GET_SELECTED_ROW     = 1505
%IDC_GET_ROW_COUNT        = 1510
%IDC_SET_ROW_COUNT        = 1515
%IDC_GET_HCELL            = 1520
%IDC_COLOR_SOME_ROWS      = 1525
%IDC_UNLOAD_GRID          = 1530


%IDC_TEST_LBUTTONDOWN     = 1525
%NUMBER_ROWS              = 12
%NUMBER_COLUMNS           = 5

#Include                  "Win32Api.inc"

Type GridInterfaces
  pGrid1                  As Dword Ptr
  dwCookie1               As Dword
End Type

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

Declare Function FnPtr(wea As WndEventArgs) As Long

Type MessageHandler
  wMessage                As Long
  dwFnPtr                 As Dword
End Type

Macro  CObj(pUnk, dwAddr) = Poke Dword, Varptr(pUnk), dwAddr

#If %Def(%Debug)
Sub Prnt(strLn As BStr)
  Local iLen, iWritten As Long
  Local hStdOutput As Dword
  Local strNew As BStr
  hStdOutput=GetStdHandle(%STD_OUTPUT_HANDLE)
  strNew=strLn + $CrLf
  iLen = Len(strNew)
  WriteConsole(hStdOutput, Byval Strptr(strNew), iLen, iWritten, Byval 0)
End Sub
#EndIf


Interface IGrid $IID_IFHGrid : Inherit IAutomation
  Method CreateGrid _
  ( _
    Byval hParent             As Long, _
    Byval strSetup            As BStr, _
    Byval x                   As Long, _
    Byval y                   As Long, _
    Byval cx                  As Long, _
    Byval cy                  As Long, _
    Byval iRows               As Long, _
    Byval iCols               As Long, _
    Byval iRowHt              As Long, _
    Byval iSelectionBackColor As Long, _
    Byval iSelectionTextColor As Long, _
    Byval strFontName         As BStr, _
    Byval iFontSize           As Long, _
    Byval iFontWeight         As Long _
  )
  Method SetRowCount(Byval iRowCount As Long, Byval blnForce As Long)
  Method GetRowCount() As Long
  Method SetData(Byval iRow As Long, Byval iCol As Long, Byval strData As WString)
  Method GetData(Byval iRow As Long, Byval iCol As Long) As WString
  Method FlushData()
  Method Refresh()
  Method GetVisibleRows() As Long
  Method GethGrid() As Long
  Method GethCell(Byval iRow As Long, Byval iCol As Long) As Long
  Method GethComboBox(Byval iCol As Long) As Long
  Method SetCellAttributes(Byval iRow As Long, Byval iCol As Long, Byval iBackColor As Long, Byval iTextColor As Long) As Long
  Method DeleteRow(Byval iRow As Long)
End Interface


Class CGridEvents  As Event
  Instance hMain As Dword

  Class Method Create()
    #If %Def(%Debug)
    Prnt "  Called Class Method Create()!"
    #EndIf
    hMain=FindWindow("Grid Test","Grid Test")
    #If %Def(%Debug)
    Prnt "    hMain = " & Str$(hMain)
    Prnt "  Leaving Class Method Create()
    #EndIf
  End Method

  Interface IGridEvents $IID_IGridEvents : Inherit IAutomation
    Method Grid_OnKeyPress(Byval iKeyCode As Long, Byval iKeyData As Long, Byval iRow As Long, Byval iCol As Long, Byref blnCancel As Long)
      #If %Def(%Debug)
      Prnt "Got KeyPress From CGridEvents1!" & Str$(iKeyCode) & "=" & Chr$(iKeyCode)
      #EndIf
    End Method

    Method Grid_OnKeyDown(Byval iKeyCode As Long, Byval iKeyData As Long, Byval iCellRow As Long, Byval iGridRow As Long, Byval iCol As Long, Byref blnCancel As Long)
      #If %Def(%Debug)
      Prnt "Got KeyDown From CGridEvents!" & Str$(iKeyCode) & "=" & Chr$(iKeyCode)
      #EndIf
    End Method

    Method Grid_OnLButtonDown(Byval iCellRow As Long, Byval iGridRow As Long, Byval iCol As Long)
      #If %Def(%Debug)
      Prnt "Got WM_LBUTTONDOWN In Grid Cell From CGridEvents1" & "(" & Trim$(Str$(iGridRow)) & "," & Trim$(Str$(iCol)) & ")"
      #EndIf
    End Method

    Method Grid_OnLButtonDblClk(Byval iCellRow As Long, Byval iGridRow As Long, Byval iCol As Long)
      ' Insert your code here
    End Method

    Method Grid_OnPaste(Byval iCellRow As Long, Byval iGridRow As Long, Byval iCol As Long)
      ' Insert your code here
    End Method

    Method Grid_OnRowSelection(Byval iRow As Long, Byval iAction As Long)
      #If %Def(%Debug)
      Prnt "  Entering Grid_OnRowSelection(GridEvents)"
      Prnt "    iRow    = " & Str$(iRow)
      Prnt "    iAction = " & Str$(iAction)
      #EndIf
      If iAction Then
         Call SetWindowLong(hMain,4,iRow)
      Else
         Call SetWindowLong(hMain,4,0)
      End If
      #If %Def(%Debug)
      Prnt "  Leaving Grid_OnRowSelection(GridEvents)"
      #EndIf
    End Method

    Method Grid_OnDelete(Byval iRow As Long)
      Local pGridInterfaces As GridInterfaces Ptr
      Local pGrid As IGrid

      #If %Def(%Debug)
      Prnt "  Entering Grid_OnDelete()"
      Prnt "    iRow = " & Str$(iRow)
      #EndIf
      pGridInterfaces=GetWindowLong(hMain,0)
      If pGridInterfaces Then
         If @pGridInterfaces.pGrid1 Then
            CObj(pGrid,@pGridInterfaces.pGrid1)
            Call pGrid.AddRef()
            Call pGrid.DeleteRow(iRow)
            Call pGrid.Refresh()
         End If
      End If
      #If %Def(%Debug)
      Prnt "  Leaving Grid_OnDelete()"
      #EndIf
    End Method
  End Interface
End Class


Function fnWndProc_OnCreate(Wea As WndEventArgs) As Long         'Offset      Item in WNDCLASSEX::cbWndExtraBytes
  Local pConnectionPointContainer As IConnectionPointContainer   '================================================
  Local pGridInterfaces As GridInterfaces Ptr                    '0  -  3     GridInterfaces Ptr - pGridInterfaces
  Local pConnectionPoint As IConnectionPoint                     '4  -  7     Row Selected
  Local pCreateStruct As CREATESTRUCT Ptr                        '8  - 11     hGrid
  Local strSetup,strCoordinate As BStr
  Local pSink As IGridEvents
  Local EventGuid As Guid
  Local dwCookie As Dword
  Local szName As ZStr*16
  Local pGrid As IGrid
  Local hCtl As Dword
  Register i As Long
  Register j As Long
  Local hr As Long

  #If %Def(%Debug)
  Call AllocConsole()
  Prnt "Entering fnWndProc_OnCreate()"
  #EndIf
  pCreateStruct=wea.lParam : wea.hInst=@pCreateStruct.hInstance
  pGridInterfaces=GlobalAlloc(%GPTR,sizeof(GridInterfaces))
  If pGridInterfaces=0 Then
     MsgBox("Memory Allocation Failure")
     Function=-1 : Exit Function
  End If
  Call SetWindowLong(Wea.hWnd,0,pGridInterfaces)
  hCtl=CreateWindow("button","Get Cell (3,2) Data",%WS_CHILD Or %WS_VISIBLE,10,10,200,30,Wea.hWnd,%IDC_RETRIEVE,Wea.hInst,ByVal 0)
  hCtl=CreateWindow("button","Get Selected Row",%WS_CHILD Or %WS_VISIBLE,10,50,200,30,Wea.hWnd,%IDC_GET_SELECTED_ROW,Wea.hInst,ByVal 0)
  hCtl=CreateWindow("button","Get Row Count",%WS_CHILD Or %WS_VISIBLE,10,90,200,30,Wea.hWnd,%IDC_GET_ROW_COUNT,Wea.hInst,ByVal 0)
  hCtl=CreateWindow("button","Set Row Count",%WS_CHILD Or %WS_VISIBLE,10,130,200,30,Wea.hWnd,%IDC_SET_ROW_COUNT,Wea.hInst,ByVal 0)
  hCtl=CreateWindow("button","Get hCell",%WS_CHILD Or %WS_VISIBLE,10,170,200,30,Wea.hWnd,%IDC_GET_HCELL,Wea.hInst,ByVal 0)
  hCtl=CreateWindow("button","Color Some Cells",%WS_CHILD Or %WS_VISIBLE,10,210,200,30,Wea.hWnd,%IDC_COLOR_SOME_ROWS,Wea.hInst,ByVal 0)
  hCtl=CreateWindow("button","Destroy Grid",%WS_CHILD Or %WS_VISIBLE,10,250,200,30,Wea.hWnd,%IDC_UNLOAD_GRID,Wea.hInst,ByVal 0)
  Let pGrid = NewCom "FHGrid.Grid"
  #If %Def(%Debug)
  Prnt "  Objptr(pGrid) = " & Str$(Objptr(pGrid))
  #EndIf
  If IsObject(pGrid) Then
     @pGridInterfaces.pGrid1=Objptr(pGrid)
     pGrid.AddRef()
     strSetup="110:Column 1:^:edit,110:Column 2:^:edit,110:Column 3:^:edit,110:Column 4:^:edit,110:Column 5:^:combo"
     pGrid.CreateGrid(Wea.hWnd,strSetup,250,10,570,273,%NUMBER_ROWS,%NUMBER_COLUMNS,28,0,0,"Times New Roman",18,%FW_DONTCARE)
     If ObjResult=%S_OK Then
        pConnectionPointContainer = pGrid
        If IsObject(pConnectionPointContainer) Then
           EventGuid=$IID_IGridEvents
           pConnectionPointContainer.FindConnectionPoint(Byval Varptr(EventGuid), Byval Varptr(pConnectionPoint))
           If ObjResult=%S_OK Then
              Let pSink = Class  "CGridEvents"
              #If %Def(%Debug)
              Prnt "  Objptr(pSink) = " & Str$(Objptr(pSink))
              #EndIf
              If IsObject(pSink) Then
                 pConnectionPoint.Advise(Byval Objptr(pSink), dwCookie)
                 If ObjResult=%S_OK Then
                    @pGridInterfaces.dwCookie1=dwCookie
                    #If %Def(%Debug)
                    Prnt "  dwCookie      = " & Str$(dwCookie)
                    #EndIf
                    hCtl=pGrid.GethGrid()
                    #If %Def(%Debug)
                    Prnt "  hGrid = " & Str$(hCtl)
                    #EndIf
                    Call SetWindowLong(Wea.hWnd,8,hCtl)
                    For i=1 To %NUMBER_ROWS
                      For j=1 To %NUMBER_COLUMNS
                        strCoordinate="(" & Trim$(Str$(i)) & "," & Trim$(Str$(j)) & ")"
                        pGrid.SetData(i, j, strCoordinate)
                      Next j
                    Next i
                    pGrid.Refresh()
                    hCtl=pGrid.GethComboBox(5)
                    szName="Frederick" : Call SendMessage(hCtl,%CB_INSERTSTRING,-1,Varptr(szName))
                    szName="Elsie"     : Call SendMessage(hCtl,%CB_INSERTSTRING,-1,Varptr(szName))
                    szName="Scott"     : Call SendMessage(hCtl,%CB_INSERTSTRING,-1,Varptr(szName))
                    szName="Lorrie"    : Call SendMessage(hCtl,%CB_INSERTSTRING,-1,Varptr(szName))
                    szName="Joseph"    : Call SendMessage(hCtl,%CB_INSERTSTRING,-1,Varptr(szName))
                    szName="Frank"     : Call SendMessage(hCtl,%CB_INSERTSTRING,-1,Varptr(szName))
                 End If
              End If
           End If
        End If
     End If
  End If
  #If %Def(%Debug)
  Prnt "Leaving fnWndProc_OnCreate()"
  #EndIf

  fnWndProc_OnCreate=0
End Function


Sub DestroyGrid(Wea As WndEventArgs)
  Local pConnectionPointContainer As IConnectionPointContainer
  Local pGridInterfaces As GridInterfaces Ptr
  Local pConnectionPoint As IConnectionPoint
  Local dwCookie As Dword
  Local EventGuid As Guid
  Local pGrid As IGrid
  Local hr As Long

  #If %Def(%Debug)
  Prnt "  Entering DestroyGrid()"
  #EndIf
  pGridInterfaces=GetWindowLong(Wea.hWnd,0)
  If pGridInterfaces Then
     If @pGridInterfaces.pGrid1 Then
        CObj(pGrid,@pGridInterfaces.pGrid1)
        @pGridInterfaces.pGrid1=0
        If IsObject(pGrid) Then
           pConnectionPointContainer=pGrid
           If IsObject(pConnectionPointContainer) Then
              EventGuid=$IID_IGridEvents
              hr=pConnectionPointContainer.FindConnectionPoint(Byval Varptr(EventGuid), Byval Varptr(pConnectionPoint))
              If SUCCEEDED(hr) Then
                 dwCookie=@pGridInterfaces.dwCookie1
                 @pGridInterfaces.dwCookie1=0
                 hr=pConnectionPoint.Unadvise(dwCookie)
                 If SUCCEEDED(hr) Then
                    #If %Def(%DEBUG)
                    Prnt "  pConnectionPoint.Unadvise(dwCookie)   Succeeded!"
                    #EndIf
                 End If
              End If
           End If
        End If
     Else
        #If %Def(%Debug)
        Prnt "    Must Have Already Released pGrid!"
        #EndIf
     End If
  End If
  #If %Def(%Debug)
  Prnt "  Leaving DestroyGrid()"
  #EndIf
End Sub


Function fnWndProc_OnCommand(Wea As WndEventArgs) As Long
  Local pGridInterfaces As GridInterfaces Ptr
  Register i As Long,j As Long
  Local strData As BStr
  Local iCnt,hr As Long
  Local pGrid As IGrid

  pGridInterfaces=GetWindowLong(Wea.hWnd,0)
  If pGridInterfaces Then
     Select Case As Long Lowrd(Wea.wParam)
       Case %IDC_RETRIEVE
         #If %Def(%Debug)
         Prnt "Entering fnWndProc_OnCommand()"
         Prnt "  Case %IDC_RETRIEVE"
         #EndIf
         CObj(pGrid,@pGridInterfaces.pGrid1)
         Call pGrid.AddRef()
         pGrid.FlushData()
         strData=pGrid.GetData(3,2)
         #If %Def(%Debug)
         Prnt "  Cell 3,2 Contains " & strData
         Prnt "Leaving fnWndProc_OnCommand()"
         #Else
         MsgBox("Cell (3, 2) Contains " & strData & ".")
         #EndIf
       Case %IDC_GET_SELECTED_ROW
         If GetWindowLong(Wea.hWnd,4) Then
            MsgBox("Selected Row = " & Str$(GetWindowLong(Wea.hWnd,4)))
         Else
            MsgBox("No Row Selected!")
         End If
       Case %IDC_GET_ROW_COUNT
         CObj(pGrid,@pGridInterfaces.pGrid1)
         Call pGrid.AddRef()
         MsgBox("pGrid.GetRowCount() = " & Str$(pGrid.GetRowCount()) & ".")
       Case %IDC_SET_ROW_COUNT
         CObj(pGrid,@pGridInterfaces.pGrid1)
         Call pGrid.AddRef()
         Call pGrid.SetRowCount(25,%True)
         If ObjResult=%S_OK Then
            For i=1 To 25
              For j=1 To 5
                strData="(" & Trim$(Str$(i)) & "," & Trim$(Str$(j)) & ")"
                pGrid.SetData(i,j,strData)
              Next j
            Next i
            pGrid.Refresh()
            MsgBox "pGrid->SetRowCount() Succeeded!",%MB_OK,"Report"
         End If
       Case %IDC_COLOR_SOME_ROWS
         If Hiwrd(Wea.wParam)=%BN_CLICKED Then
            #If %Def(%Debug)
            Prnt "Entering fnWndProc_OnCommand()"
            Prnt "  Case %IDC_COLOR
            #EndIf
            CObj(pGrid,@pGridInterfaces.pGrid1)
            pGrid.AddRef()
            pGrid.FlushData()
            For i=1 To 5                                                 ' back, text
              pGrid.SetCellAttributes(3,i,&H000000FF,&H00FFFFFF)         ' red, white
            Next i
            For i=1 To 5
              pGrid.SetCellAttributes(4,i,&H0000FF00,&H00FFFFFF)         ' green, white
            Next i
            For i=1 To 5
              pGrid.SetCellAttributes(5,i,&H00FF0000,&H00FFFFFF)         ' blue, white
            Next i
            For i=1 To 5
              pGrid.SetCellAttributes(6,i,RGB(255,255,0),&H00000001)     ' yellow, black
            Next i
            For i=1 To 5
              pGrid.SetCellAttributes(7,i,RGB(0,255,255),&H00000001)     ' light bluish, black
            Next i
            pGrid.Refresh()
            #If %Def(%Debug)
            Prnt "Leaving fnWndProc_OnCommand()"
            #EndIf
         End If
       Case %IDC_GET_HCELL
         Local hCell As Dword
         CObj(pGrid,@pGridInterfaces.pGrid1)
         Call pGrid.AddRef()
         hCell=pGrid.GethCell(3,2)
         If ObjResult=%S_OK Then
            MsgBox "pGrid.GethCell(3,2) = " & Str$(hCell) & ".", %MB_OK, "Report"
         End If   
       Case %IDC_UNLOAD_GRID
         #If %Def(%Debug)
         Prnt "Entering fnWndProc_OnCommand()"
         Prnt "  Case %IDC_UNLOAD_GRID"
         #EndIf
         Call DestroyGrid(Wea)
         Call EnableWindow(GetDlgItem(Wea.hWnd,%IDC_RETRIEVE),%False)
         Call EnableWindow(GetDlgItem(Wea.hWnd,%IDC_GET_SELECTED_ROW),%False)
         Call EnableWindow(GetDlgItem(Wea.hWnd,%IDC_GET_ROW_COUNT),%False)
         Call EnableWindow(GetDlgItem(Wea.hWnd,%IDC_SET_ROW_COUNT),%False)
         Call EnableWindow(GetDlgItem(Wea.hWnd,%IDC_GET_HCELL),%False)
         Call EnableWindow(GetDlgItem(Wea.hWnd,%IDC_COLOR_SOME_ROWS),%False)
         Call EnableWindow(GetDlgItem(Wea.hWnd,%IDC_UNLOAD_GRID),%False)
         Call InvalidateRect(Wea.hWnd,Byval %Null, %True)
         #If %Def(%Debug)
         Prnt "Leaving fnWndProc_OnCommand()"
         #EndIf
     End Select
  End If

  fnWndProc_OnCommand=0
End Function


Function fnWndProc_OnDestroy(Wea As WndEventArgs) As Long
  Local pGridInterfaces As GridInterfaces Ptr
  Local blnFree As Long

  #If %Def(%Debug)
  Prnt "Entering fnWndProc_OnDestroy()"
  #EndIf
  Call DestroyGrid(Wea)
  pGridInterfaces=GetWindowLong(Wea.hWnd,0)
  #If %Def(%Debug)
  Prnt "  pGridInterfaces          = " & Str$(pGridInterfaces)
  #EndIf
  blnFree=GlobalFree(pGridInterfaces)
  #If %Def(%Debug)
  Prnt "  blnFree(pGridInterfaces) = " & Str$(blnFree)
  #EndIf
  Call CoFreeUnusedLibraries()
  Call PostQuitMessage(0)
  #If %Def(%Debug)
  Prnt "Leaving fnWndProc_OnDestroy()"
  #EndIf

  Function=0
End Function


Sub AttachMessageHandlers()
  Dim MsgHdlr(2) As Global MessageHandler  'Associate Windows Message With Message Handlers
  MsgHdlr(0).wMessage=%WM_CREATE           :   MsgHdlr(0).dwFnPtr=CodePtr(fnWndProc_OnCreate)
  MsgHdlr(1).wMessage=%WM_COMMAND          :   MsgHdlr(1).dwFnPtr=CodePtr(fnWndProc_OnCommand)
  MsgHdlr(2).wMessage=%WM_DESTROY          :   MsgHdlr(2).dwFnPtr=CodePtr(fnWndProc_OnDestroy)
End Sub


Function fnWndProc(ByVal hWnd As Long,ByVal wMsg As Long,ByVal wParam As Long,ByVal lParam As Long) As Long
  Local wea As WndEventArgs
  Register iReturn As Long
  Register i As Long

  For i=0 To 2
    If wMsg=MsgHdlr(i).wMessage Then
       wea.hWnd=hWnd: wea.wParam=wParam: wea.lParam=lParam
       Call Dword MsgHdlr(i).dwFnPtr Using FnPtr(wea) To iReturn
       fnWndProc=iReturn
       Exit Function
    End If
  Next i

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


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

  szAppName="Grid Test"                           : Call AttachMessageHandlers()
  wc.lpszClassName=VarPtr(szAppName)              : wc.lpfnWndProc=CodePtr(fnWndProc)
  wc.cbClsExtra=0                                 : wc.cbWndExtra=12
  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 RegisterClassEx(wc)
  hWnd=CreateWindowEx(0,szAppName,szAppName,%WS_OVERLAPPEDWINDOW,200,100,840,340,0,0,hIns,ByVal 0)
  Call ShowWindow(hWnd,iShow)
  While GetMessage(Msg,%NULL,0,0)
    TranslateMessage Msg
    DispatchMessage Msg
  Wend
  #If %Def(%Debug)
  MsgBox("Last Chance To Get What You Can!")
  #EndIf

  Function=msg.wParam
End Function


Frederick J. Harris