Wrapping a Memo in a BrowseBox

by Mike Hanson

Published 1998-10-01    Printer-friendly version

Download the code here

Publisher's Note
At several points in this article, the template code is much too long to fit in a normal width document. We've used continuation characters ( the rule character | ) to show where the code is continued. This code will not work as shown in this article. You should use the enclosed templates instead. We hope this message helps eliminate the confusion that could be caused.

Clarion has included a "memo" entry field in its products for as long as I can remember. In the DOS products, it wasn’t a true word wrapping experience, because there was no support for a true line break (i.e. CR+LF). But since and release of Clarion for Windows (and now retrofitted into Clarion for DOS), this support is now available.

This is great for entering memos in forms and printing them in reports, but we are still stuck without a way to wrap our long text fields in a BrowseBox. Hence this article. Although it’s still a bit of a kludge, this hybrid of a Class and a Template allows us to split the contents of a memo into multiple fields in a browse box.

To use it, you create multiple local STRING variables (as many as you like), and place them on a BrowseBox in a single column. (This is done by grouping the fields, and specifying that all but the last field are "Last on line".) The class will automatically adjust each line to match each row’s corresponding field, so you don’t even need to make them the same size or indentation. This will enable you to create wrapping effects like hanging indents.

There are numerous elements of the Clarion system demonstrated in this article: Templates, Classes, ABC (with templates and classes), Windows API, and String Slicing, to name a few. I’ll try to describe each of them in turn.

There are a couple of limitations that I should mention:

  1. The browse is not automatically refilled during a window resize, column resize, or horizontal column scroll. This is a design issue: I just didn’t have the time to add the support.
  2. There’s a problem with 16-bit support (due to strangeness with the CreateFont WINAPI function). As soon as I get a fix, I’ll release a new version. For now, the wrapping doesn’t work perfectly under 16-bit. (You should all be using 32-bit by now anyway <grin>.)
  3. Clarion 4 has a bug that occasionally omits the class parent call if only generated code exists in the overridden method embed. In this case, we are placing the call to Wrap in the SetQueueRecord method code embed. If you are using Clarion 4, you must add additional embed code yourself to trick the system into generating the parent call. All you need to do is go into Source view, and search for ".Wrap". Add an exclamation point in the nearest blank embed.

Class Overview

Any good helper shouldn’t need to be told about something twice. In the same way, our class should take as many details up front, then require minimal additional instructions for the repetitive calls. Our class will have four methods:

Init – This is the main initialization procedure. It initializes most of the class data.

AddRow – This informs the class about the various destination variables and their associated list box columns.

Wrap – This method does the actual wrapping.

Kill – You need to call this at the end to do clean up.

Our class include file looks like this:

  !ABCIncludeFile
  OMIT('_EndOfInclude_', _MhAbResizePresent_)
_MhAbResizePresent_    EQUATE(1)
MH::WrapRowQueue       QUEUE,TYPE
Column                   SHORT
Indent                   SHORT
Field                    &STRING
                       END!QUEUE
MH::WrapBrowse         CLASS,TYPE,MODULE('MhAbWrpB')
ListControl              LONG,PROTECTED
Row                      &MH::WrapRowQueue,PROTECTED
MeasureControl           LONG,PRIVATE
Init                     PROCEDURE(LONG ListControl)
Kill                     PROCEDURE
AddRow                   PROCEDURE(SHORT Column,|
                                   *STRING Field)
Wrap                     PROCEDURE(STRING Value)
                       END!CLASS
_EndOfInclude_

Let’s examine each section. The !ABCIncludeFile is necessary to tell Clarion that this class is "ABC compliant". There are a whole bunch of benefits that come with this. For example, you can extend the class in your APPs by using the regular embed editor. All ABC compliant classes must have their header file (in this case MHABWRPB.INC) in the C:\CLARIONx\LIBSRC directory. The next two lines and the last ensure that the file is not doubly included at any point.

Our class maintains a queue of "Wrap Rows". Each element in the queue represents one "row" in our output. We remember the browse column number, indent setting, and a reference to the destination field. For us to use a queue in our class, we must first define it as a TYPE.

Now comes the class definition itself. Notice that there are three class variables. ListControl is the LIST control used by the BrowseBox. Row is a reference to the WrapQueue. Both of these variables are protected, which means that you can access them if you inherit the class, but not from the outside world. Finally, MeasureControl is used internally (notice the PRIVATE attribute).

The Init method takes only one parameter, the BrowseBox’s list control. Kill doesn’t have any parameters. AddRow takes the column number and a pointer to the destination string. And Wrap takes the value to be wrapped.

Referencing the Windows API

Before we get into the meat of the class member definitions, we’ll look at how the class will achieve its goals. If we were still in the DOS world, word wrapping would be simple. We could walk through the characters until they filled the width of our entry field, knowing that all text is the same size. However, with the myriad font options in Windows, our lives are a little tougher.

There is a WINAPI function called GetTextExtentPoint. It tells you how much space a string needs when outputted using the "current font". You set the current font using SelectObject, with a font handle as a parameter. This handle must first be produced using CreateFont. This function takes more parameters than any other windows API function. However, we can get all of the required information using the PROP:Font attributes of our list control.

In addition to these functions, we have a number of others, including GetDC, ReleaseDC, DeleteObject, GetDeviceCaps, etc. I’m not going to explain the purpose of all of these, because you can always get that information from a standard Windows programming reference (Two good ones are "Programming Windows" by Charles Petzold, and "Windows API Bible" by James Conger.)

Whenever you call WINAPI functions from Clarion, you must first define them in your MAP structure. Once you know which function to call, you can use a handy example program that Clarion provides called WINAPI.EXE You’ll find this in C:\CLARION4\EXAMPLES\RESOURCE\WINAPI. This utility contains the Clarion equivalents for most of the useful Windows constant equates, data structures, and function prototypes. You can select a bunch of these and output them to a file, or you can just cut and paste from the displayed memo field into the Clarion editor.

Most of the time these definitions are sufficient. However, I have found occasional mistakes, and sometimes "style" issues come into play. For example, some developers like to pass all addresses to the WINAPI functions as LONGs, and to use ADDRESS(Variable) to produce the value for that parameter. I don’t like this method myself, as it doesn’t involve any parameter type checking. If you need to pass a bunch of NULL addresses, though, then this can be handy.

My preference is to define all functions, parameter types, data structures, and constants with the same names as those in C, so that I can consistently compare my declarations with those of the standard WINAPI reference manuals. Sometimes this is impossible, though. For example, there is a WINAPI structure called SIZE. I can’t use this name, because SIZE is a reserved word in Clarion. Therefore, I changed this name to SIZETYPE. Similarly, C compilers are case sensitive, so "HDC" and "hdc" are two different things. In Clarion, I normally use the C name for my "type name", then use a more description name for my actual variable (e.g. "DeviceContext").

Class Definition

Now that we defined what we want our class to do on the high level and how to call the Windows API, lets look at how it’s done. The top of our member module looks like this:

  MEMBER

  INCLUDE('MhAbWrpB.INC')
  INCLUDE('EQUATES.CLW')

CLIP_DEFAULT_PRECIS EQUATE(00h)
DEFAULT_CHARSET     EQUATE(1)
DEFAULT_PITCH       EQUATE(00h)
FF_DONTCARE         EQUATE(00h)
FW_NORMAL           EQUATE(400)
LF_FACESIZE         EQUATE(32)
LOGPIXELSY          EQUATE(90)
OUT_DEFAULT_PRECIS  EQUATE(0)
PROOF_QUALITY       EQUATE(2)
DWORD               EQUATE(ULONG)
HANDLE              EQUATE(UNSIGNED)
HDC                 EQUATE(HANDLE)
HFONT               EQUATE(HANDLE)
HGDIOBJ             EQUATE(HANDLE)
HWND                EQUATE(HANDLE)
LPCSTR              EQUATE(CSTRING)

SIZETYPE            GROUP,TYPE
CX                    SIGNED
CY                    SIGNED
                    END!GROUP
  MAP
    MODULE('Windows API Library')
      GetDC(HWND),HDC,RAW,PASCAL
      GetDeviceCaps(HDC, SIGNED),|
                    SIGNED,RAW,PASCAL
      ReleaseDC(HWND, HDC),|
                SHORT,PROC,RAW,PASCAL
      SelectObject(HDC, HGDIOBJ),|
                   HGDIOBJ,PROC,PASCAL,RAW
      DeleteObject(HGDIOBJ),BOOL,PROC,PASCAL
      MulDiv(SIGNED, SIGNED, SIGNED),SIGNED,PASCAL
      OMIT('***',_WIDTH32_)
      CreateFont(SIGNED, SIGNED, SIGNED, SIGNED, |
                 SIGNED, DWORD, DWORD, DWORD, DWORD,|
                 DWORD, DWORD, DWORD, DWORD,|
                 *LPCSTR), HFONT, PASCAL, RAW
      GetTextExtentPoint(HDC, *?, SIGNED, *SIZETYPE),|
                         BOOL, PROC, RAW, PASCAL
      ***
      COMPILE('***',_WIDTH32_)
      CreateFont(SIGNED, SIGNED, SIGNED, SIGNED, |
                 SIGNED, DWORD, DWORD, DWORD, DWORD,|
                 DWORD, DWORD, DWORD, DWORD, *LPCSTR),|
                 HFONT,PASCAL, RAW, NAME('CreateFontA')
      GetTextExtentPoint(HDC, *?, SIGNED, *SIZETYPE), |
                         BOOL, PROC, RAW, PASCAL, |
                         NAME('GetTextExtentPointA')
      ***
    END!MODULE
  END!MAP

The MEMBER statement indicates that this module is not a program itself, but the lack of an associated program name indicates that it can be attached as a module to any PROGRAM (or to another MEMBER).

The next line INCLUDEs our class header file. This is followed by the inclusion of Clarion’s basic EQUATES.CLW. Next we have a number of equates and type definitions needed for declaration of the WINAPI functions and data structures.

Now we come to the MAP structure. Notice that the module name isn’t a filename. This tells the compiler that these functions are out there somewhere, but that it shouldn’t worry about compiling the corresponding module. Because these are from the Windows API, Clarion automatically adds the API library to the project for us. (It’s invisible.)

Notice that we use the WINAPI keywords for parameter types and return value types, rather than Clarion’s equivalents. Again, this makes it easier when cross-referencing with WINAPI documentation. Also, note that some of these functions have different names for 16 and 32-bit, which is why there are OMIT and COMPILE sections in the MAP. In this case, only the names were different. There will be situations where parameters are also different. In that case, you would likely have OMIT and COMPILE sections in your code too.

Now we come to the class methods. First is Init:

MH::WrapBrowse.Init PROCEDURE(LONG ListControl)
  CODE
  SELF.Row &= NEW MH::WrapRowQueue
  SELF.ListControl = ListControl
  SELF.MeasureControl = CREATE(0, CREATE:Entry)

This is quite simple. It creates a new WrapRowQueue for use with this instance, it remembers the ListControl that’s associated with the browse, and it creates a new control to be used for measuring the columns. This control creation dictates that our Init method must be called after the window has been opened. Otherwise, the created control will be associated with the window in the calling procedure.

Next comes the AddRow method:

MH::WrapBrowse.AddRow PROCEDURE(SHORT Column, *STRING Field)
  CODE
  SELF.Row.Column = Column
  0{PROP:Pixels} = True
  IF SELF.ListControl{PROPLIST:Left, Column}
    SELF.Row.Indent = SELF.ListControl|
                      {PROPLIST:LeftOffset, Column}
  ELSIF SELF.ListControl{PROPLIST:Right, SELF.Row.Column}
    SELF.Row.Indent = SELF.ListControl|
                      {PROPLIST:RightOffset, Column}
  ELSE
    SELF.Row.Indent = 0
  END!IF
  0{PROP:Pixels} = False
  SELF.Row.Field &= Field
  ADD(SELF.Row)
  ASSERT(~ERRORCODE())

This method is a little stranger. First it remembers the column for the row. Then it sets the measurement standard to Pixels (instead of dialog units). This makes it easier to work with the WINAPI functions. The list box column is then checked for its indent setting. (The indent setting normally will not change for the duration of the procedure, so we can remember this value at the start.) After storing the indent setting, the measurement standard is returned to using dialog units. The only attribute left to initialize is a reference to the destination string field. Finally, we add a new element to the Row queue.

Next comes the Kill method:

MH::WrapBrowse.Kill PROCEDURE
  CODE
  DESTROY(SELF.MeasureControl)
  FREE(SELF.Row)
  DISPOSE(SELF.Row)

Notice that we reverse most of the stuff that was done in the Init method. To be consistent, the Kill method must be called before the Window is closed. (You actually don’t need to worry about remembering this, because you will be using a template to implement the class in your procedure.)

Now we get into the really strange stuff. The Wrap method is broken into a main procedure and a few support routines. We’ll take each piece at a time. First comes the procedure itself:

MH::WrapBrowse.Wrap   PROCEDURE(STRING Value)
DeviceContext         HDC,AUTO
Font                  HFONT,AUTO
SaveFont              HFONT,AUTO
Length                SHORT,AUTO
StartPos              SHORT(1)
Finished              BOOL(False)
Row                   SHORT,AUTO
  CODE
  Length = LEN(CLIP(Value))
  LOOP WHILE Value[StartPos] = ' '
    StartPos += 1
  END!LOOP
  0{PROP:Pixels} = True
  DeviceContext = GetDC(SELF.ListControl{PROP:Handle})
  DO MH::SetFont
  LOOP Row = 1 TO RECORDS(SELF.Row)
    GET(SELF.Row, Row)
    ASSERT(~ERRORCODE())
    IF Finished
      CLEAR(SELF.Row.Field)
    ELSE
      DO MH::SetRow
    END!IF
  END!LOOP
  DO MH::ResetFont
  ReleaseDC(SELF.ListControl{PROP:Handle},|
            DeviceContext)
  0{PROP:Pixels} = False

This method is responsible for splitting the Value into the various rows defined in the calls to AddRow. The first thing it does is count the number of characters in the Value. Next it skips spaces at the start of the string (i.e., It left justifies the value). Then the measurement standard is set to pixels (like in AddRow).

Most of the WINAPI functions that we’re using here require a "DeviceContext". This is a special type of handle to a device. We cannot get the device context directly from Clarion, but we can use the PROP:Handle of a Window or Control, along with GetDC(), to allocate a new device context for our purposes.

Our next step is to set the font to match the list box’s font settings. This is a rather complicated process, so it is handled in a separate SetFont routine.

Now we’ve come to the stage where we can start splitting the Value into its various Rows. The LOOP processes each of the Rows that were created earlier in the calls to AddRow. If we’ve already finished splitting the entire value, then the row’s destination variable is cleared. Otherwise it calls the SetRow routine to handle the wrapping logic.

After all of the rows have been filled, the font is reset, the device context is released, and the measurement standard is returned to dialog units.

Now we’ll look at the SetFont routine:

MH::SetFont ROUTINE
  DATA
Height      SIGNED,AUTO
Weight      SIGNED,AUTO
Italic      DWORD,AUTO
Underline   DWORD,AUTO
Strikeout   DWORD,AUTO
Typeface    CSTRING(LF_FACESIZE),AUTO
  CODE
  Height = -MulDiv(SELF.ListControl{PROP:Font,2},|
           GetDeviceCaps(DeviceContext, LOGPIXELSY), 72)
  Weight = BAND(SELF.ListControl{PROP:Font,4}, 0FFFh)
  Italic = CHOOSE(BAND(SELF.ListControl{PROP:Font,4},|
           FONT:Italic))
  Underline = CHOOSE(BAND(SELF.ListControl{PROP:Font,4},|
              FONT:Underline))
  Strikeout = CHOOSE(BAND(SELF.ListControl{PROP:Font,4},|
              FONT:Strikeout))
  Typeface = CLIP(SELF.ListControl{PROP:Font,1})
  Font = CreateFont(Height, 0, 0, 0, Weight, Italic, |
         Underline, Strikeout, DEFAULT_CHARSET, |
         OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, |
         PROOF_QUALITY, BOR(DEFAULT_PITCH, FF_DONTCARE),|
         Typeface)
  SaveFont = SelectObject(DeviceContext, Font)

This routine is responsible for copying the font settings from the list control so that we can set the "current font" for the WINAPI. (This is necessary so that the GetTextExtentPoint measurement function will work.)

The Height is determined by a special calculation involving the point size, number of displayable pixels, and points per inch. I want the width to correspond to the height, so I just pass zero as the second parameter to CreateFont. The same applies to the Escapement and Orientation parameters.

The Weight, Italic,Underline, and Strikeout parameters are all derived from {PROP:Font,4}, which is the "font style" property in Clarion. In Clarion 5 you are allowed to have additional font style settings for each column, so we would have to handle this here. For now, this should be sufficient for a basic word wrap.

The Typeface comes directly from {PROP:Font,1}. There are various additional attributes that are passed using equates to represent default values.

CreateFont is responsible for finding a "closest match" to the desired font. We are merely responsible for passing in as much information as we have available to us, then Windows does its best to find a matching font. Because this is a font that is already being used on the Screen, there’s a very good chance that Windows will find exactly the same font with this function call. As I mentioned earlier, I’ve discovered a problem with CreateFont in 16-bit programs. I don’t know where the mistake lies, but as soon as I find a fix I’ll release a new version of the public domain templates.

The next routine is ResetFont:

MH::ResetFont ROUTINE
  SelectObject(DeviceContext, SaveFont)
  DeleteObject(Font)

This is quite simple. Notice, though, that when SelectObject was originally called in the SetFont routine, we saved the previous font setting. At this point we restore that setting. Then we delete the font that was created.

Finally, we come to the SetRow routine:

MH::SetRow ROUTINE
  DATA
ColWidth LONG,AUTO
EndPos SHORT,AUTO
NextPos SHORT,AUTO
TextSize LIKE(SIZETYPE)
  CODE
  SELF.ListControl{PROP:Edit, SELF.Row.Column}|
                  = SELF.MeasureControl
  ColWidth = SELF.MeasureControl{PROP:Width}|
             - SELF.Row.Indent
  SELF.ListControl{PROP:Edit, SELF.Row.Column} = 0
  EndPos = StartPos
  NextPos = 0
  LOOP
    IF EndPos > Length
      EndPos = Length
      Finished = True
      BREAK
    ELSIF Value[EndPos] = '<13>'
      NextPos = EndPos|
              + CHOOSE(Value[EndPos+1] ='<10>', 2, 1)
      EndPos -= 1
      BREAK
    ELSIF Value[EndPos] = ' '
      IF EndPos > 1 AND Value[EndPos-1] <> ' '
        NextPos = EndPos - 1
      END!IF
    ELSE
      GetTextExtentPoint(DeviceContext,|
                         Value[StartPos],|
                         EndPos-StartPos+1,|
                         TextSize)
      IF TextSize.CX > ColWidth
        IF NextPos
          EndPos = NextPos
          LOOP
            NextPos += 1
          UNTIL Value[NextPos] <> ' '
        ELSE
          NextPos = EndPos
          EndPos -= 1
        END!IF
        BREAK
      END!IF
    END!IF
    EndPos += 1
  END!LOOP
  SELF.Row.Field = Value[StartPos : EndPos]
  StartPos = NextPos

This is the actual word wrapping code. The first thing it does is measure the width of the column for the current row. There is a PROPLIST:Width property that I originally attempted to use. The problem was that it wouldn’t report the proper value if the column were the last one in the list box or the last in a group.

The work-around is to temporarily assign an edit-in-place control for the column. As you may have noticed, the EIP controls always fill the entire column width. After measuring the size of the control, I cancel the EIP status for the column. The code never hits an ACCEPT loop during this process, so the edit control never appears on the window. If you were wondering why I create and destroy MeasureControl in Init and Kill, well now you know. (Thanks to Arnor Baldvinsson for the idea.)

The logic of the wrapping code uses three variables: StartPos (the start of the current row), EndPos (the walking end of the current row), and NextPos (the end of the most recent word in the current row, or the start of the next row).

While it walks through the input value, it watches for four things:

  1. Has it processed all the characters in the value?
  2. Has it reached a line break character (CR or CR+LF)?
  3. Has it reached the end of a word?
  4. Has it got too many characters to fit in the column?

Of these, only the last is hard to understand. It uses the GetTextExtentPoint WINAPI function to measure the potential size of the current sub-string. If it’s too big to fit in the column, then it attempts to go back to the prior word break. If there have been no spaces up to this point, it just breaks the line after the previous character.

There is another function called GetTextExtentExPoint that can measure the entire string in one fell swoop. However, it made the rest of the wrapping logic rather confusing, so I elected to use GetTextExtentPoint instead.

Template Support

Whenever we create a class, it’s always nice if we can produce a companion template to simplify usage. This is no exception. Our template must ask for the source variable and destination fields within the list box. In addition, our template will be ABC compatible. That makes it possible to inherit the class locally using the regular IDE facilities (e.g. Classes tab and Embed window).

Let’s look at the top of the template module:

#EXTENSION(mhWrapBrowse,'Wrap Browse Field'),|
  DESCRIPTION('[MikeH] Wrap Browse Field: ' & %SourceField),|
  PROCEDURE, REQ(BrowseBox(ABC)), MULTI

The extension template name is mhWrapBrowse. When you are adding the template to your procedure, the description you will see is ‘Wrap Browse Field’, while the description in your procedure’s extensions window will be ‘[MikeH] Wrap Browse Field: SourceField’. This template must be populated in a procedure with an ABC BrowseBox. You can also have more than one for each browse box.

NOTE: You must highlight the existing BrowseBox template in your extensions window before hitting the Insert button for the mhWrapBrowse template to be available.

Writing an ABC compatible template requires a bunch of extra code. The best way to learn how to write a template is to look at an example. However, many of Clarion’s ABC templates are quite complex, making them not the best of samples for learning. You’ll find that this template encapsulates the most basic elements necessary to produce an ABC template. The first required section is:

#PREPARE
#CALL(%ReadABCFiles(ABC))
#CALL(%SetClassItem(ABC),'mhWrapBrowse'&|
      %ActiveTemplateInstance)
#CALL(%SetOOPDefaults(ABC),'WrapBrowse'&|
      %ActiveTemplateInstance, 'MH::WrapBrowse')
#ENDPREPARE

This ensures that the ABC header files have been read, and that the proper %ClassItem is selected. Finally, it sets the defaults for the Class variables. %ClassItem and its associated tokens are used to generate the class data and overridden methods. It’s our responsibility to ensure that these variables contain the proper values for generation to occur.

Now we need our prompts:

#SHEET
  #TAB('&General')
    #PROMPT('Source Field:', FIELD),%SourceField,REQ
    #DISPLAY
    #BOXED('Destination Row Fields')
      #BUTTON('Destination Row Field'), |
              MULTI(%RowFields,%RowField),|
              INLINE
        #DISPLAY('The destination field must be placed'&|
                 ' in your list box before you can select '&|
                 'it here. Usually you will use local '&|
                 'variables for this task, and they must '&|
                 'be STRINGs.'),AT(10,,180,32)
        #PROMPT('List Box Field:',FROM(%ControlField)),|
                %RowField,REQ
        #PREPARE
          #FIND(%ControlInstance,|
                %ActiveTemplateParentInstance,|
                %Control)
        #ENDPREPARE
      #ENDBUTTON
    #ENDBOXED
  #ENDTAB
  #TAB('Description')
    #DISPLAY('This template displays a memo field on '&|
             'multiple "rows" in a BrowseBox.'),AT(10,,180,20)
    #DISPLAY('It takes the value from the source field, '&|
             'and splits it into the various result fields.'&|
             ' Each result field receives one "Line" of '&|
             'ouput.'),AT(10,,180,28)
    #DISPLAY('The text is automatically word-wrapped at '&|
             'spaces, and breaks the line at a CR+LF.')|
             ,AT(10,,180,16)
  #ENDTAB
  #TAB('&Classes')
    #WITH(%ClassItem, 'mhWrapBrowse'& %ActiveTemplateInstance)
      #INSERT(%ClassPrompts(ABC))
    #ENDWITH
  #ENDTAB
#ENDSHEET

There are a couple of interesting things here. Notice the #DISPLAY(…),AT(10,,180,32). If you specify the left margin (10), the width (180), and the height (32), then the display string will automatically word wrap within this region. Each wrapped line will be 8 units high, and you’ll have to experiment to determine the required height of each of your wrapped display strings. Take the number of lines after wrapping, multiply by 8, and you have the height. If you want multiple paragraphs, place each one in its own #DISPLAY statement, then add 4 to the height for an attractive space between paragraphs. By the way, #DISPLAY without any parameters creates a blank line 10 units high.

Another thing to notice is the #PREPARE section after the %RowField prompt. The %RowField must be selected from the values in the %ControlField multi-valued symbol. However, %ControlField is attached to %Control, so you must first set the %Control to match the BrowseBox’s list control. The easiest way to do this is with a #FIND statement. It finds a %ControlInstance matching the %ActiveTemplateParentInstance. (Our direct parent is the BrowseBox.) If found, it automatically anchors any parent symbols of %ControlInstance. I’ve explicitly told it to stop the anchoring at %Control (which it would have stopped at anyway, because %Control is the top level).

To make your template ABC compatible, you need additional prompts associated with your template. Some of these are visible (see #TAB(‘Classes’) above). Notice the #WITH statement. This ensures that the %ClassItem is concurrent with this particular object. Some of the ABC prompts are hidden:

#BOXED('Hidden Prompts'),AT(0,0,0,0),WHERE(%False),HIDE
#INSERT(%OOPHiddenPrompts(ABC))
#ENDBOXED

Now we can get into the code generation. This is done by inserting code into embeds using the #AT structure. Sometimes only processing is done, and no code is generated. This is an example of that:

#ATSTART
#CALL(%ReadABCFiles(ABC))
#CALL(%SetClassItem(ABC),'mhWrapBrowse'&|
      %ActiveTemplateInstance)
#CALL(%SetOOPDefaults(ABC),'WrapBrowse'&|
      %ActiveTemplateInstance, 'MH::WrapBrowse')
#ENDAT

The #ATSTART section is always executed before any other #AT structure for the current template. (You can’t really predict in what order different templates will generate their code.) If you want to do something to another template’s symbols (especially your parent template), then you use the %GatherSymbols embed:

#AT(%GatherSymbols)
#FIX(%QueueField, %ManagerName &'.Q.'& %SourceField)
#IF(NOT %QueueField)
#ADD(%QueueField, %ManagerName &'.Q.'& %SourceField)
#SET(%QueueFieldAssignment, %SourceField)
#SET(%QueueFieldComment, %ActiveTemplate)
#ENDIF
#ENDAT

All file fields that are displayed in the browse are automatically added to the VIEW structure and browse QUEUE. However, our source variable is not directly placed in the list box, so the parent BrowseBox template doesn’t know about our field. We could insist that the developer always remember to specify that the source field is a hot field for the browse … Or we could execute the above code automatically. Remember the Clarion motto: If you always have to do it, then you should never have to do it!

Now we have some more code required for ABC compatibility:

#AT(%GatherObjects)
  #CALL(%SetClassItem(ABC),'mhWrapBrowse'&|
        %ActiveTemplateInstance)
  #ADD(%ObjectList,%ThisObjectName)
  #SET(%ObjectListType,%GetBaseClassType())
#ENDAT
#!-----
#AT(%LocalDataClasses)
  #CALL(%SetClassItem(ABC), 'mhWrapBrowse'&|
        %ActiveTemplateInstance)
  #INSERT(%GenerateClassDefinition(ABC),%ClassLines)
#ENDAT
#!-----
#AT(%LocalDataClasses)
  #CALL(%SetClassItem(ABC), 'mhWrapBrowse'&|
        %ActiveTemplateInstance)
  #INSERT(%GenerateClassDefinition(ABC), %ClassLines)
#ENDAT
#!-----
#AT(%LocalProcedures)
  #CALL(%SetClassItem(ABC),'mhWrapBrowse'&|
        %ActiveTemplateInstance)
  #IF(%BaseClassToUse())
    #CALL(%FixClassName,%BaseClassToUse())
    #FOR(%pClassMethod)
      #FOR(%pClassMethodPrototype),|
       WHERE(%MethodEmbedPointValid())
        #CALL(%SetupMethodCheck(ABC))
#EMBED(%mhWrapBrowseMethodDataSection,|
       'WrapBrowse Method Data Section'),|
        %ActiveTemplateInstance,|
        %pClassMethod,|
        %pClassMethodPrototype,|
        MAP(%ActiveTemplateInstance,|
            %ActiveTemplateInstanceDescription&' using '&|
            %BaseClassToUse()), |
        LABEL, DATA, WHERE(%MethodEmbedPointValid()),|
        PREPARE(,%FixClassName(|
                %FixBaseClassToUse(|
               'mhWrapBrowse'& %ActiveTemplateInstance)))
#?%NULL
#?  CODE
  #EMBED(%mhWrapBrowseMethodCodeSection,|
         'WrapBrowse Method Executable Code Section'),|
         %ActiveTemplateInstance,|
         %pClassMethod,|
         %pClassMethodPrototype,|
         MAP(%ActiveTemplateInstance, |
             %ActiveTemplateInstanceDescription&' using '&|
             %BaseClassToUse()),|
         WHERE(%MethodEmbedPointValid()), |
         PREPARE(,%FixClassName(|
                 %FixBaseClassToUse(|
                 'mhWrapBrowse'& %ActiveTemplateInstance)))
  #CALL(%CheckAddMethodPrototype(ABC),%ClassLines)
      #ENDFOR
    #ENDFOR
    #CALL(%GenerateNewLocalMethods(ABC))
  #ENDIF
#ENDAT
#!-----
#AT(%mhWrapBrowseMethodCodeSection, |
    %ActiveTemplateInstance), PRIORITY(5000)
  #CALL(%GenerateParentCall(ABC))
#ENDAT

This code probably looks rather strange, and it is. Generally, you can copy this and make a few small changes to match your own code. (You would think with all of the ABC templates needing this, that you could create a reusable component. To the best of my knowledge, this is not yet possible.)

If you are using Clarion 5, you’ll notice that these class embeds appear the way they did in Clarion 4. This is because TopSpeed has changed from using MAP to using TREE to control how the various embeds appear in the Embeds window. For compatibility with both versions, though, I’ve left it this way for now. Similarly, you can add DESCRIPTION attributes to the #AT statements in Clarion 5, which will appear in your embed window if you tell it to "Show Priority Labels". Again, if I were to add these descriptions here, the template would not work in Clarion 4.

Now it’s time to generate the code to call our class methods. First we’ll initialize our class:

#AT(%WindowManagerMethodCodeSection,|
    'Init','(),BYTE'),PRIORITY(8050)
#FIX(%ClassItem, 'mhWrapBrowse'& %ActiveTemplateInstance)
%ThisObjectName.Init(%ListControl)
#FIX(%Control, %ListControl)
#FOR(%RowFields)
#FIX(%ControlField, %RowField)
%ThisObjectName.AddRow(%(INSTANCE(%ControlField)),|
                       %RowField)
#ENDFOR
#ENDAT

This code will be generated into the WindowManager’s Init method (the one that has a prototype of "(),BYTE"). Notice the PRIORITY(8050). This occurs after the window is opened. First we call our Init method with the %ListControl parameter, which is one our parent BrowseBox’s template variables. Then for each row, it calls AddRow with the appropriate parameters. Note that we must determine the column number with INSTANCE(%ControlField).

Now for the corresponding call to our Kill method:

#AT(%WindowManagerMethodCodeSection,'Kill',|
    '(),BYTE'),PRIORITY(2500)
%ThisObjectName.Kill
#ENDAT

Again, the priority places the code before the window is closed.

Finally, we have to ensure that our Wrap method is called before the BrowseClass fills its queue fields:

#AT(%BrowserMethodCodeSection,|
    %ActiveTemplateParentInstance,|
    'SetQueueRecord', '()'), PRIORITY(4900)
%ThisObjectName.Wrap
#ENDAT

Notice that the priority of 4900 places the code before the parent call (which always occurs at a priority of 5000). As I mentioned earlier, If you are using Clarion 4 the parent call will not be generated. You must add a manual embed alongside; a blank comment line will do fine.

Conclusion

Well, that’s all there is to it (as if that weren’t enough <grin>). Now you should have a better understanding of classes, templates, WINAPI, string slicing, and ABC compatibility. For the full source code of this and the rest of my public domain templates, go towww.BoxsoftDevelopment.com. Remember that I’m always adding more stuff and fixing quirks, so you should occasionally check in to see what’s new. Catch you later!

Printer-friendly version

 
 

Search

 

Advanced Search
Topical Index

Related Articles

Subscribe to
ClarionMag

One year: $184

(includes all back issues since '99)

Renewals from $134

Two years: $274

Renewals from $224

More Info

Subscribe Now!

ClarionMag Blog

RSS Feeds

Updates via Email

Enter your Email


Powered by FeedBlitz

Quick Links