![]() |
|
Published 1998-10-01 Printer-friendly version
| 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 wasnt 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 its 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 rows corresponding field, so you dont 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. Ill try to describe each of them in turn.
There are a couple of limitations that I should mention:
Any good helper shouldnt 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_
Lets 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 BrowseBoxs list control. Kill doesnt have any parameters. AddRow takes the column number and a pointer to the destination string. And Wrap takes the value to be wrapped.
Before we get into the meat of the class member definitions, well 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. Im 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 Youll 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 dont like this method myself, as it doesnt 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 cant 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").
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 its 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 Clarions 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 isnt a filename. This tells the compiler that these functions are out there somewhere, but that it shouldnt worry about compiling the corresponding module. Because these are from the Windows API, Clarion automatically adds the API library to the project for us. (Its invisible.)
Notice that we use the WINAPI keywords for parameter types and return value types, rather than Clarions 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 thats 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 dont 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. Well 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 were 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 boxs font settings. This is a rather complicated process, so it is handled in a separate SetFont routine.
Now weve 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 weve already finished splitting the entire value, then the rows 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 well 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, theres a very good chance that Windows will find exactly the same font with this function call. As I mentioned earlier, Ive discovered a problem with CreateFont in 16-bit programs. I dont know where the mistake lies, but as soon as I find a fix Ill 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 wouldnt 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:
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 its 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.
Whenever we create a class, its 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).
Lets 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 procedures 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 Clarions ABC templates are quite complex, making them not the best of samples for learning. Youll 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. Its 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 youll 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 BrowseBoxs 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. Ive 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 cant really predict in what order different templates will generate their code.) If you want to do something to another templates 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 doesnt 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, youll 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, Ive 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 its time to generate the code to call our class methods. First well 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 WindowManagers 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 BrowseBoxs 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.
Well, thats all there is to it (as if that werent 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 Im always adding more stuff and fixing quirks, so you should occasionally check in to see whats new. Catch you later!
Copyright © 1999-2008 by CoveComm Inc. All Rights Reserved. Reproduction in any form without the express written consent of CoveComm Inc., except as described in the subscription agreement, is prohibited.
Clarion Magazine ISSN 1718-9942
One year: $184
(includes all back issues since '99)
Renewals from $134
Two years: $274
Renewals from $224