NO

Author Topic: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1 of 2  (Read 6599 times)

Offline CFred

  • Member
  • *
  • Posts: 36
Introduction
A few years ago I wrote a turorial for using the Text Object Model (TOM) in Pure Basic and I am rewriting that turorial here for Pelles C.

The Text Object Model (TOM) underpins the rich text controls provided for Windows by Microsoft. Some of TOM's methods are available via API calls to the Editor gadget but several useful methods are unavailable that allow:

⦁ toggling the case of text selected in a rich text control
⦁ selecting whole characters, words, sentences, lines and paragraphs
⦁ underlining words with coloured lines and in a range of styles eg red wavy lines
⦁ implementing additional styles for bullet points, for example, numbers in circles
⦁ etc.

All these features can be accessed in Pelles C by setting up a TOM interface. The Microsoft website, https://learn.microsoft.com/en-us/windows/win32/controls/about-text-object-model contains information about the Text Object Model but there is little detail on how to implement and use it. This tutorial aims to provide a brief introduction to implementing and using the Text Object Model in Pelles C. A program that demonstrates how to use the Text Object Model is included in the zip file attached to this post.

Technical details
Briefly, TOM consists of a hierarchy of objects, of which the root object is defined by the ITextDocument2 interface (this is an extension of an earlier version of the TOM object, ITextDocument). This object has methods for creating objects that are lower in the hierarchy, for example, the ITextRange object that is used for basic text processing. To use rich text formatting, the objects ITextFont and ITextPara can be obtained from an ITextRange object. There is also an ITextFont object to format fonts and an ITextPara object to format paragraphs.

This brief tutorial will not explore the technical details of TOM; instead it provides a practical guide for using some of TOM's methods in Pelles C.

Conventions used in this tutorial
Functions that use the text object model will begin with TOM_

Use the latest rich text editor control
The TOM interface is only available for rich edit controls with a version number of 3.0 or greater. The header file TOMComprehensive.h needs to be included in any program that uses the TOM interface. In Windows 11 the latest rich text edit control is in the file msfted.dll. Pelles C can load this library in Windows 11 using the following function:

Code: [Select]
hRichEditLib = LoadLibrary("MsftEdit.dll");  // Load RichEdit "RichEdit50W" v4.1


Free the library after using it by calling:

Code: [Select]
FreeLibrary(hRichEditLib);

Initialising the TOM interface and releasing it in Pelles C
A few global values need to be defined in the module containing the TOM functions before setting up the TOM interface:

Code: [Select]
static IUnknown *pUnk;
static ITextDocument2 *pDoc;
static const IID IID_ITextDocument = {
    0x8CC497C0, 0xA1DF, 0x11CE,
    {0x80,0x98,0x00,0xAA,0x00,0x47,0xBE,0x5D}
};

It is also useful to include the following macros in the module containing the TOM functions:

Code: [Select]
#define IS_TOM_INIT if (!TomInit) return;
#define IS_TOM_INIT_RETURN if (!TomInit) return 0;

One of these two macros can be placed at the start of each function that uses TOM to check that TOM has been initialised correctly. If TOM has not been initialised then these functions cause the function to exit.

The following function must be called to set up the TOM interface. This interface is only available for richedit controls with a version number of 3.0 or greater. The function returns TRUE if successful, otherwise it returns FALSE.

Code: [Select]
int TOM_tomInit(HWND hRichEd){

long long int hr = 0;

if (!pUnknown){
hr = SendMessage(hRichEd, EM_GETOLEINTERFACE, 0, (LPARAM)&pUnknown);
}

if (SUCCEEDED(hr)) {
if (!pUnknown || pUnknown->lpVtbl->QueryInterface(pUnknown,&IID_ITextDocument, (void * *)&TextDocument2) != NOERROR){
pUnknown = 0;
return FALSE;
}
}
TomInit = TRUE;
return TRUE;
}

After use, the TOM interface must be released using the following function:

Code: [Select]
void TOM_releaseTOM(void){
if (pUnk) {
pUnk->lpVtbl->Release(pUnk);
pUnk= 0;
pDoc= 0;
}
}

Errors
Most of the methods for the ITextRange object (discussed later) return an error code if an error is detected. The following function uses this error code to return an error message if an error is detected.

Code: [Select]
void TOM_ShowErrorMessage(HRESULT result){

//Return if there is no error
if (result == S_OK) return;

switch (result){
case E_ABORT: DISPLAY_ERROR(E_ABORT - Operation aborted); break;
case E_ACCESSDENIED: DISPLAY_ERROR(E_ACCESSDENIED - Write access denied); break;
case E_FAIL: DISPLAY_ERROR(E_FAIL - Unspecified failure); break;
case E_HANDLE: DISPLAY_ERROR(E_HANDLE - Invalid handle); break;
case E_INVALIDARG: DISPLAY_ERROR(E_INVALIDARG - Invalid argument); break;
case E_NOINTERFACE: DISPLAY_ERROR(E_NOINTERFACE - No such interface supported); break;
case E_NOTIMPL: DISPLAY_ERROR(E_NOTIMPL - Not implemented); break;
case E_OUTOFMEMORY: DISPLAY_ERROR(E_OUTOFMEMORY - out of memory); break;
case E_POINTER: DISPLAY_ERROR(E_POINTER - Invalid pointer); break;
case E_UNEXPECTED: DISPLAY_ERROR(E_UNEXPECTED - Unexpected failure); break;
case CO_E_RELEASED: DISPLAY_ERROR(CO_E_RELEASED - the paragraph formatting object is attached to a range that has been deleted); break;
default: DISPLAY_ERROR(Some other error occurred);
}
}

DISPLAY_ERROR is a macro defined as:

Code: [Select]
#define DISPLAY_ERROR(X) MessageBox(0, #X, "ERROR", MB_ICONERROR);

This function is used liberally throughout the functions in the attached program. I could have called this function more effectivley using if… statements, but decided to merely use the line

Code: [Select]
TOM_ShowErrorMessage(hr);

to test for errors. This makes it easier to remove this function after testing the function that contains it.

Using TOM in Pelles C
Having set up the interface for TOM we can implement features that are not available with normal API functions.

Checking whether text has been selected
Some TOM methods give an error if text has not been selected. The following function checks whether text has been selected, returning TRUE if text is selected or FALSE if text has not been selected.

Code: [Select]
BOOL TOM_IsTextSelected(void){
//Check that the TOM interface has been initialised
IS_TOM_INIT_RETURN
long int start, end;
HRESULT hr;
ITextSelection *textSelection = NULL;

//Initialise the ITextSelection object with the selection
TextDocument2->lpVtbl->GetSelection(TextDocument2, &textSelection);

//Get the index for the position of the first character selected
hr = textSelection->lpVtbl->get_Start(textSelection, &start);
TOM_ShowErrorMessage(hr);

//Get the index for the position of the last character selected
hr = textSelection->lpVtbl->GetEnd(textSelection, &end);
TOM_ShowErrorMessage(hr);

//Free the ITextSelection object
textSelection->lpVtbl->Release(textSelection); //This does return a HRESULT type

if (start==end)
return FALSE;
else
return TRUE;
}

TOM_IsTextSelected() is used when any of the TOM functions in the attached zip file expects text to be selected.

Changing the case of selected text
The case of text selected in a rich text control can be amended by using the method ChangeCase provided by TOM. Although the rich text control has methods that facilitate some case changes (lower case/upper case), TOM caters for three additional case changes:

⦁ title case (to capitalise the first letter of each word in selected text)
⦁ sentence case (to capitalise the first letter of each sentence in selected text)
⦁ toggle case (to change the case of the characters in selected text).

Here is a function to change the case of selected text using the method ChangeCase:

Code: [Select]
void TOM_CaseInvert(HWND hRichEd){

//Check that the TOM interface has been initialised
        IS_TOM_INIT

//If text has not been selected exit this function
if (!TOM_IsTextSelected()) return;

long int startPos=0;
long int endPos = 0;
HRESULT hr;

    ITextRange *textRange = NULL;

//Get the position of the first and last selected characters
SendMessage(hRichEd, EM_GETSEL, (WPARAM)&startPos, (LPARAM)&endPos);

//Initialise the ITextRange object with these positions
hr = TextDocument2->lpVtbl->Range(TextDocument2, startPos, endPos, &textRange);
TOM_ShowErrorMessage(hr);

//Change the case of the selected text
hr = textRange->lpVtbl->ChangeCase(textRange, tomToggleCase);
TOM_ShowErrorMessage(hr);

//Free the ITextRange object
hr = textRange->lpVtbl->Release(textRange);
TOM_ShowErrorMessage(hr);
}

A normal Windows API function is called to get the start and end position of the selected text. Then an ITextRange object is initialised with the start and end position of the selected text. The ChangeCase() method of ITextRange is used to invert the case of the selected text. Finally, the ITextRange object is released.
Error checks are carried out at each stage in this and the other TOM functions in the attached zip file; these could be removed from the final version of applications.
Fields and methods of ITextRange are listed at https://learn.microsoft.com/en-us/windows/win32/api/tom/nn-tom-itextrange

Selecting sections of text above the caret
Individual characters, whole words, whole sentences, whole lines and whole paragraphs that contain the caret can be selected using TOM.
The following function selects the word containing the caret (this includes any spaces immediately following the end of the word).

Code: [Select]
void TOM_SelectWordContainingCaret(long int *CharsSelected){

//Check that the TOM interface has been initialised
IS_TOM_INIT

//If text has already been selected exit this function
if (TOM_IsTextSelected()) return;

HRESULT hr;

ITextSelection *textSelection = NULL;

//Get the word containing the caret
TextDocument2->lpVtbl->GetSelection(TextDocument2, &textSelection);
hr = textSelection->lpVtbl->Expand(textSelection, tomWord, CharsSelected);
TOM_ShowErrorMessage(hr);

//Free the ITextSelection object
textSelection->lpVtbl->Release(textSelection); //This does not return a HRESULT type

return;
}

CharsSelected points to a long int type. When the function exits, this will contain the number of characters selected, including the following space (if any).

The method Expand(textSelection, tomWord, CharsSelected) causes the range of the selection to be expanded to cover the word above the caret. This includes any spaces immediately following the text above the cursor, but not punctuation marks. This method returns a result code that is passed as an argument to TOM_ShowErrorMessage() and this displays an error message if an error is encountered.
This function can select other areas if required, eg the current line, by replacing tomWord with one of the following constants:

tomCharacter   Character
tomSentence    Sentence
tomParagraph   Paragraph
tomLine           Line (on screen)
tomStory         Story


Getting the start and end indices of selected text
This function obtains the start index and end index of the characters in the selected text. The index is zero based.

Code: [Select]
void TOM_GetStartAndEnd(long int *start, long int *end){

//Check that the TOM interface has been initialised
IS_TOM_INIT

HRESULT hr;

ITextSelection *textSelection = NULL;

//Initialise the ITextSelection object with the selection
TextDocument2->lpVtbl->GetSelection(TextDocument2, &textSelection);

//Get the index for the position of the first character selected
hr = textSelection->lpVtbl->get_Start(textSelection, start);
TOM_ShowErrorMessage(hr);

//Get the index for the position of the last character selected
hr = textSelection->lpVtbl->GetEnd(textSelection, end);
TOM_ShowErrorMessage(hr);

//Free the ITextSelection object
textSelection->lpVtbl->Release(textSelection); //This does return a HRESULT type

//The index of the last character is one value too many
//So reduce the end index by 1 if start and end hold different values
//(If start == end then no text has been selected)
if (*start !=*end)
*end = *end -1;

return;
}

As in the previous function, this initialises an ITextSelection object by calling the GetSelection method to get and store information about the selected range of text.
The ITextSelection object stores the start index and end index of the selected text. These indices may be retrieved using the methods get_Start() and GetEnd(). These use a parameter that is the address of the variable in which the start and end addresses are to be placed.
The index returned by GetEnd() is one value too many, so this is reduced by 1.

Retrieving the line of text containing the caret
You can retrieve the line of text containing the caret as the following function demonstrates.

Code: [Select]
#pragma comment(lib, "OleAut32.lib") // Needed for SysStringLen() and SysFreeString()

void TOM_GetTextLineContainingCaret(char *textLine, int MaxStringLen){

//Check that the TOM interface has been initialised
IS_TOM_INIT

HRESULT hr;
long int countCharsSelected = 0;
ITextSelection *textSelection = NULL;
BSTR BstrText;

//Initialise the ITextSelection object
TextDocument2->lpVtbl->GetSelection(TextDocument2, &textSelection);

//Select the line of text that contains the caret
hr = textSelection->lpVtbl->Expand(textSelection, tomLine, &countCharsSelected);
TOM_ShowErrorMessage(hr);

//Get the selected line of text
hr = textSelection->lpVtbl->GetText(textSelection, &BstrText);
TOM_ShowErrorMessage(hr);

//Convert the selected text from a BSTR type to a char type
//Set the char type string empty if thenumber of charaters read exceed MaxStringLen
int textLen = SysStringLen(BstrText);
if (textLen < MaxStringLen)
WideCharToMultiByte(CP_ACP, 0, BstrText, -1, textLine, textLen + 1, NULL, NULL);
else
strcpy(textLine, "");

//Free the memory used by BstrText
SysFreeString(BstrText);

//Free the ITextSelection object
textSelection->lpVtbl->Release(textSelection); //This does not return a HRESULT type
}

The text is retrieved using the GetText() method of the ITextSelection object. Memory is automatically allocated for the text by this object.
GetText() returns a pointer to a string data type BSTR (Basic string or binary string) that points to an area of memory that contains the required text. The pointer used by GetText() must be freed manually by calling SysFreeString().
The above function converts the selected text from a BSTR type to a char type that is returned via the *textLine argument for this function.

See https://learn.microsoft.com/en-us/previous-versions/windows/desktop/automat/bstr for more information on the string data type BSTR
See https://learn.microsoft.com/en-us/cpp/atl-mfc-shared/allocating-and-releasing-memory-for-a-bstr?view=msvc-170&viewFallbackFrom=vs-2019 for details of Allocating and Releasing Memory for a BSTR.

Disable\Enable the Undo and Redo facility in a rich text control
By default, the rich text control facilitates redo/undo actions. The TOM contains methods that can disable/enable this facility.

The following function will disable the Undo\Redo facility in a rich text control.

Code: [Select]
void TOM_DisableUndo(void){

//Check that the TOM interface has been initialised
    IS_TOM_INIT

    TextDocument2->lpVtbl->Undo(TextDocument2, tomSuspend, 0);
}

The following function will enable the undo/redo facility in a rich text control:
void TOM_EnableUndo(void){

//Check that the TOM interface has been initialised
    IS_TOM_INIT

    TextDocument2->lpVtbl->Undo(TextDocument2, tomResume, 0);
}

Temporarily freezing the rich text control
It is possible to freeze the displayed text in the rich text control so that its contents may be updated by the program without the user seeing any flickering and without loss of performance. Key presses sent to the rich text control while the display is frozen will be processed when the display is released from its frozen state.

The following function will freeze the display:

Code: [Select]
void TOM_freeze(void){

//Check that the TOM interface has been initialised
IS_TOM_INIT

HRESULT hr = TextDocument2->lpVtbl->Freeze(TextDocument2,&cnt); 

TOM_ShowErrorMessage(hr);
}

The following function will unfreeze the display:

Code: [Select]
void TOM_unfreeze(void){

//Check that the TOM interface has been initialised
    IS_TOM_INIT
   
HRESULT hr = TextDocument2->lpVtbl->Unfreeze(TextDocument2,&cnt);

TOM_ShowErrorMessage(hr);
}


Part 2 follows
« Last Edit: September 21, 2023, 05:27:02 PM by CFred »

Offline CFred

  • Member
  • *
  • Posts: 36
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 2
« Reply #1 on: August 26, 2023, 12:11:51 PM »
Paragraph formatting—Bullet points
Rich text editors provide several styles for numbering; accessing TOM provides additional styles eg numbers in circles.
The following constants set the format of the paragraph:

tomListNone - Removes paragraph bullets
tomListBullet - Inserts a circular black bullet
tomListNumberAsArabic - Normal numbering (0, 1, 2,…) followed by a round bracket
tomListNumberAsLCLetter - Lower case lettering followed by a round bracket
tomListNumberAsUCLetter - Upper case lettering followed by a round bracket
tomListNumberAsLCRoman - Lower case Roman numbering followed by a round bracket
tomListNumberAsUCRoman - Upper case Roman numbering followed by a round bracket
tomListNumberAsSequence - Seems to be the same as tomListNumberAsArabic
tomListNumberedCircle - Number placed in plain circle followed by a round bracket
tomListNumberedBlackCircleWingding - Number placed in black circle followed by a round bracket
tomListNumberedWhiteCircleWingding - Seems to be the same as tomListNumberedCircle
tomListNumberedArabicWide - Normal numbering with larger line spacing

The following constants set the decoration around the number:

tomListParentheses - Puts round brackets around the number
tomListPeriod - Puts a full stop after the number
tomListPlain - Uses plain numbering without brackets of full stops
tomListMinus - Puts a hyphen after the number

The constants for formatting the paragraph can be OR'd with the constants for the decoration around the number eg

Code: [Select]
tomListNumberedCircle | tomListPlain

Some of these constants were missing from the original Tom.h file so I have inserted them.

The following function shows how to format paragraph bullets using TOM. It will number the selected paragraph(s) or just the paragraph containing the caret, placing the number in a circle:

Code: [Select]
void TOM_SetParaBullet(HWND hEdit){

//Check that the TOM interface has been initialised
IS_TOM_INIT

long int startPos=0;
long int endPos = 0;

HRESULT hr;

ITextRange *textRange = NULL;

ITextPara *textPara = NULL;

//Get the start and end position of selected paragraph(s)
SendMessage(hEdit, EM_GETSEL, (WPARAM)&startPos, (LPARAM)&endPos);

//Initialise the ITextRange object with the start and end positions
hr = TextDocument2->lpVtbl->Range(TextDocument2, startPos, endPos, &textRange);
TOM_ShowErrorMessage(hr);

//Initialise the ITextPara object
hr = textRange->lpVtbl->GetPara(textRange, &textPara);
TOM_ShowErrorMessage(hr);

//Format the selected paragraph(s) with a number in a circle and no bracket decorations
hr = textPara->lpVtbl->SetListType(textPara, tomListNumberedCircle | tomListPlain);
TOM_ShowErrorMessage(hr);

//Free the ITextPara object
textPara->lpVtbl->Release(textPara);
//TOM_ShowErrorMessage(hr);

//Free the ITextRange object
textRange->lpVtbl->Release(textRange); //This does not return a HRESULT type

return;
}

The normal Windows API call using EM_GETSEL is used to get the selected paragraph(s). This data is used to initialise the ITextRange object and then the ITextPara object is initialised using the selected range. Next, the selected paragraphs are formatted using the SetListType method of the ITextPara object. Finally, the pointers for textPara and textRange are freed.

By default, the numbering style set by SetListType will include a round bracket after the number. This can be removed by ORing the TOM constant that sets the style with tomListPlain. In this function, the TOM constant tomListNumberedCircle is OR'd with tomListPlain to prevent a round bracket appearing after the circled number.

Experiment showed that ORing the ASCII code for '*' with tomListNumberAsArabic will provide asterisked bullet points:

Code: [Select]
textPara->lpVtbl->SetListType(textPara, tomListNumberAsArabic | 0x2A);

Possibly other symbols can be used as bullet points....

Paragraph formatting—other
The previous function can be extended to provide additional paragraph formatting eg paragraph spacing. Although this can be achieved by sending normal messages to the Editor Gadget, I am including this here as a demonstration of what can be achieved directly with TOM. Use the following TOM constants to set paragraph spacing:
tomLineSpaceSingle - Normal line spacing. The floating point argument is ignored
tomLineSpace1pt5 - Line spacing of 1.5 The floating point argument is ignored
tomLineSpaceDouble - Double line spacing. The floating point argument is ignored
tomLineSpaceAtLeast - Spacing is set by the floating point argument. If this is less than that for single spacing then the paragraph is single spaced
tomLineSpaceExactly - Spacing is set by the floating point argument and can be less than that of single spacing.
tomLineSpaceMultiple - Line spacing is set in terms of lines.
tomLineSpacePercent - Set line spacing by percent of line height.

These constants are used with the SetLineSpacing method of the ITextPara object. This method requires three arguments: the first argument is the pointer for the ITextPara object; the second argument is one of the above constants to indicate the type of line spacing to use; the third argument is a float value that sets the value of the line spacing (this does not always seems to be used but must be included when calling this method).

The following function will put a bullet point at the start of the selected paragraph(s) and sets the paragraphs to use double line spacing:

Code: [Select]
void DoubleSpacedParagraph(HWND hEdit){

//Check that the TOM interface has been initialised
IS_TOM_INIT

long int startPos=0;
long int endPos = 0;

HRESULT hr;

ITextRange *textRange = NULL;

ITextPara *textPara = NULL;

//Get the start and end positions of the selected paragraph(s)
SendMessage(hEdit, EM_GETSEL, (WPARAM)&startPos, (LPARAM)&endPos);

//Initialise the ITextRange object with the start and end positions
hr = TextDocument2->lpVtbl->Range(TextDocument2, startPos, endPos, &textRange);
TOM_ShowErrorMessage(hr);

//Initialise the ITextPara object
hr = textRange->lpVtbl->GetPara(textRange, &textPara);
TOM_ShowErrorMessage(hr);

//Set the line spacing
hr = textPara->lpVtbl->SetLineSpacing(textPara, tomLineSpaceDouble, 0);
TOM_ShowErrorMessage(hr);

//Format the selected paragraph(s) with a bullet
hr = textPara->lpVtbl->SetListType(textPara, tomListBullet);
TOM_ShowErrorMessage(hr);

//Free the ITextPara object
textPara->lpVtbl->Release(textPara);
//TOM_ShowErrorMessage(hr);

//Free the ITextRange object
textRange->lpVtbl->Release(textRange);

return;
}

Get the character, word, line, sentence or paragraph index
This may be useful, for example, if you want to display line numbers in a rich text editor where the height of each line may be different when the height of fonts change on different lines. The line numbers could be displayed in a separate rich text editor next to the one that contains the text. You could get the paragraph index and display this in a status bar as paragraph number.

To get the index for one of these items, the GetIndex() method of the ITextSelection is required. This has two parameters. The first is one of the above constants to tell the object which index is required. The second is the address of a variable that will store the index.

The following function will obtain the index of the line containing the cursor.

Code: [Select]
long int GetLineIndex(void){

//Check that the TOM interface has been initialised
IS_TOM_INIT_RETURN

long int LineIndex =0;
HRESULT hr;

ITextSelection *textSelection = NULL;

//Initialise the ITextSelection object
TextDocument2->lpVtbl->GetSelection(TextDocument2, &textSelection);

//Get the index of the line containing the caret
hr = textSelection->lpVtbl->GetIndex(textSelection, tomLine, &LineIndex);
TOM_ShowErrorMessage(hr);

//Free the ITextSelection object
textSelection->lpVtbl->Release(textSelection);

return LineIndex;
}

The index is obtained using the GetIndex()method of the ITextSelection object. This has three arguments. The first is a pointer to the ITextSelection object; the second argument is one of the TOM constants to tell the ITextrange object which index is required (character, word, sentence, etc); the third is the address of a variable that will store the index.

When indexing words, end of line markers and full stops is included in the result.

Underlining text
TOM may be used to underline words using a wide variety of different underlining styles. For example, it is possible to underline text with dotted lines, dashed lines, and wavy lines. Wavy lines are used in word processors to indicate spelling mistakes. Use the following TOM constants (listed on the Microsoft website at https://learn.microsoft.com/en-us/windows/win32/api/tom/ne-tom-tomconstants) to set the underlining style:
tomSingle - Single underline
tomWords - Underline words only
tomDouble - Double underline
tomDotted - Dotted underline
tomDash - Dash underline
tomDashDot - Dash dot underline
tomDashDotDot - Dash dot dot underline
tomWave - Wave underline
tomThick - Thick line
tomHair - Hair line
tomDoubleWave - Double wave
tomHeavyWave - Heavy wave underline
tomLongDash - Long dash underline
tomThickDash - Thick dash underline
tomThickDashDot - Thick dash dot underline
tomThickDashDotDot - Thick dash dot dot underline
tomThickDotted - Thick dotted underline
tomThickLongDash - Thick long dash underline

The following function demonstrates how to underline selected text with a dotted line.

Code: [Select]
void TOM_UnderlineText(HWND hRichEd){

//Check TOM has been initialised
    IS_TOM_INIT

long int startPos = 0;
long int endPos = 0;

HRESULT hr;

    ITextRange *textRange = NULL;
ITextFont *textFont = NULL;

//Get the start and end position of the selected text
SendMessage(hRichEd, EM_GETSEL, (WPARAM)&startPos, (LPARAM)&endPos);

//Initialise the ITextRange with start and end positions of the selected text
hr = TextDocument2->lpVtbl->Range(TextDocument2, startPos, endPos, &textRange);
TOM_ShowErrorMessage(hr);
/*
//Can use this to select whole words and (unfortunately) the following space
long int CharsSelected = 0;
hr = textRange->lpVtbl->Expand(textRange, tomWord, &CharsSelected);
TOM_ShowErrorMessage(hr);
*/
//Initialise the ITextFont object
hr = textRange->lpVtbl->GetFont(textRange, &textFont);
TOM_ShowErrorMessage(hr);

//Draw a dotted line under the selected text
hr = textFont->lpVtbl->SetUnderline(textFont, tomDotted);
TOM_ShowErrorMessage(hr);

//Free the ITextFont object
hr = textFont->lpVtbl->Release(textFont);
TOM_ShowErrorMessage(hr);

//Free the ITextRange object
hr = textRange->lpVtbl->Release(textRange);
TOM_ShowErrorMessage(hr);
}

The function gets the selected text and then the method GetFont() initialises an ITextFont object.
Calling its method, SetUnderline(), draws the line in the style set by the parameter passed to this method.
The last two lines frees the ITextFont object and the ITextRange object.

This next function demonstrates how to remove underlining from selected text.

Code: [Select]
void TOM_RemoveUnderlineText(HWND hRichEd){

//Check TOM has been initialised
    IS_TOM_INIT

long int startPos = 0;
long int endPos = 0;

HRESULT hr;

    ITextRange *textRange = NULL;
ITextFont *textFont = NULL;

//Get the start and end positions of selected text
SendMessage(hRichEd, EM_GETSEL, (WPARAM)&startPos, (LPARAM)&endPos);

//Initialise the ITextRange object with the start and end positions of selected text
hr = TextDocument2->lpVtbl->Range(TextDocument2, startPos, endPos, &textRange);
TOM_ShowErrorMessage(hr);
/*
//Can use this to select whole words and (unfortunately) the following space

long int CharsSelected = 0;
hr = textRange->lpVtbl->Expand(textRange, tomWord, &CharsSelected);
TOM_ShowErrorMessage(hr);
*/

//Initialise the ITextFont object
hr = textRange->lpVtbl->GetFont(textRange, &textFont);
TOM_ShowErrorMessage(hr);

//Remove underlining from the selected text
hr = textFont->lpVtbl->SetUnderline(textFont, tomNone);
TOM_ShowErrorMessage(hr);

//Free the ITextFont object
hr = textFont->lpVtbl->Release(textFont);
TOM_ShowErrorMessage(hr);

//Free the ITextRange object
hr = textRange->lpVtbl->Release(textRange);
TOM_ShowErrorMessage(hr);
}

The function is similar to that for underlining a word except that a parameter for SetUnderline() is changed to tomNone.

Issue with words that are already underlined
Documents may contain words that are underlined using black straight lines—the normal underlining style. If the first function is used to underline a word that already has the normal underlining style then when the second function runs it will not restore the original underlining style—it removes all underlining from the word. This could cause a problem in a word-processing program that underlines misspelt words with a red wavy line. When the word is corrected then the original underlining will disappear—it would be necessary to keep track of every underlined word so that the original underlining can be restored to these words. Even worse, if the document contains valid words or expressions that are not in the dictionary (eg program code or mathematical terms) then these will be identified as being misspelt and when the document is saved the red wavy lines will be saved with the document!

Fortunately TOM provides an alternative way of underlining words that resolves this problem; with TOM it is possible to use temporary underlining (other temporary styles can be applied to characters too, if required). When temporary underlining is removed the original underlining style (and character style) is restored; moreover, temporary underlining is not stored in the document file when the document is saved.

Implementing temporary underlining
This function shows how to implement temporary underlining. It will underline the word above the cursor with a temporary red wavy line.  It takes one argument, hRichEd, the handle of the rich text control that contains the text to be underlined.

Code: [Select]
void TOM_DrawTempWavyLine(HWND hRichEd){

//Check TOM has been initialised
    IS_TOM_INIT

long int startPos=0;
long int endPos = 0;

HRESULT hr;

    ITextRange *textRange = NULL;
ITextFont *textFont = NULL;

//Get start and end position of selected text
SendMessage(hRichEd, EM_GETSEL, (WPARAM)&startPos, (LPARAM)&endPos);

//Initialise the ITextRange object with the start and end positions of the selected characters
hr = TextDocument2->lpVtbl->Range(TextDocument2, startPos, endPos, &textRange);
TOM_ShowErrorMessage(hr);

//Initialise the ITextFont object
hr = textRange->lpVtbl->GetFont(textRange, &textFont);
TOM_ShowErrorMessage(hr);

//Make the line temporary
hr = textFont->lpVtbl->Reset(textFont, tomApplyTmp);
TOM_ShowErrorMessage(hr);

//Prepare to draw a temporary wavy line under the selected text
hr = textFont->lpVtbl->SetUnderline(textFont, tomWave);
TOM_ShowErrorMessage(hr);

//Set colour of the wavy line
// textFont->lpVtbl->SetUnderline(textFont, 0x0600+tomWave); //Draws temporary wavy line in black
// textFont->lpVtbl->SetUnderline(textFont, 0xFFFF0000); //Draws temporary wavy line in blue
hr = textFont->lpVtbl->SetUnderline(textFont, 0xFF0000FF); //Draws temporary wavy line in red
TOM_ShowErrorMessage(hr);

//Apply a wavy line to selected text
hr = textFont->lpVtbl->Reset(textFont, tomApplyNow);
TOM_ShowErrorMessage(hr);

//Free the ITextFont object
hr = textFont->lpVtbl->Release(textFont);
TOM_ShowErrorMessage(hr);

//Free the ITextRange object
hr = textRange->lpVtbl->Release(textRange);
TOM_ShowErrorMessage(hr);
}

After setting up the ITextFont object, its method Reset() is called with the argument tomApplyTmp to use temporary formatting. This method is called again to set the colour for underlining words. In this case the argument 0xFF0000FF is used. The last six digits control the colour of the line (These seem to be the colour values for RGB in reverse, so for a red line use 0xFF0000FF).
Then the method Reset() is called with the argument tomApplyNow to apply the underlining.
Finally, the ITextFont object is freed by calling its method Release()

The next function shows how to remove temporary underlining from selected text.

Code: [Select]
void TOM_RemoveTempUnderline(HWND hRichEd){

//Check TOM has been initialised
    IS_TOM_INIT

long int startPos=0;
long int endPos = 0;

HRESULT hr;

    ITextRange *textRange = NULL;
ITextFont *textFont = NULL;

//Get the start and end position of selected text
SendMessage(hRichEd, EM_GETSEL, (WPARAM)&startPos, (LPARAM)&endPos);

//Initialise the ITextRange with the start and end positions of the selected characters
hr = TextDocument2->lpVtbl->Range(TextDocument2, startPos, endPos, &textRange);
TOM_ShowErrorMessage(hr);

//Initialise the ITextFont object
hr = textRange->lpVtbl->GetFont(textRange, &textFont);
TOM_ShowErrorMessage(hr);

//Make underlining temporary
hr = textFont->lpVtbl->Reset(textFont, tomApplyTmp);
TOM_ShowErrorMessage(hr);

//Prepare to remove temporary underlining from the selected text
hr = textFont->lpVtbl->SetUnderline(textFont, tomNone);
TOM_ShowErrorMessage(hr);

//Remove underlining from the selected text
hr = textFont->lpVtbl->Reset(textFont, tomApplyNow);
TOM_ShowErrorMessage(hr);

//Free the ITextFont object
hr = textFont->lpVtbl->Release(textFont);
TOM_ShowErrorMessage(hr);

//Free the ITextRange object
hr = textRange->lpVtbl->Release(textRange);
TOM_ShowErrorMessage(hr);
}

After selecting the word above the cursor, the ITextFont object is set up by calling the method GetFont.
The Reset() method of this object is called with the argument tomApplyTmp to put the object into its temporary formatting mode.
The parameter tomNone tells the object that we want to remove temporary formatting.
As in the previous code, tomApplyNow is used to implement the removal  the temporary formatting.
Finally, the ITextFont object and the ITextRange object are released by calling the appropriate Release() methods.

Using the program in the zip file attached to the first post in this thread
The attached zip file includes a module TomFunctions.c that contains the functions described above. It also includes a module, Main.c, that produces a window containing a rich text edit control and a menu. Type (or paste) some text into this control and then use the menu to apply each of the functions on the text.

Conclusion
The text object model gives access to a number of functions that are not available using the normal Windows API functions. This tutorial could not cover all the functions available but it is hoped that it demonstrates how to use the TOM objects ITextDocument, ITextRange, ITextFont and ITextPara in Pelles C. Delving into the list of TOM constants at https://learn.microsoft.com/en-us/windows/win32/api/tom/ne-tom-tomconstants will reveal some of the other capabilities that are available using the TOM interface.

« Last Edit: September 21, 2023, 05:35:05 PM by CFred »

Offline frankie

  • Global Moderator
  • Member
  • *****
  • Posts: 2113
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1
« Reply #2 on: August 26, 2023, 12:42:31 PM »
Really nice job  8)
Maybe a downloadable PDF document and an example project could be worth.  ;)
"It is better to be hated for what you are than to be loved for what you are not." - Andre Gide

Offline CFred

  • Member
  • *
  • Posts: 36
I have attached a PDF file containing both parts of the tutorial at the end of the second tutorial.

Offline frankie

  • Global Moderator
  • Member
  • *****
  • Posts: 2113
Well done.
Thanks  :)
"It is better to be hated for what you are than to be loved for what you are not." - Andre Gide

Offline John Z

  • Member
  • *
  • Posts: 860
Hi CFred,

Really an excellent contribution!  Thanks very much!

BTW who/what is "TechFrontiers" ?

Good documentation takes a much time as good programming and often even more.  This
will be useful to many people because of the complete documentation.

John Z

Offline CFred

  • Member
  • *
  • Posts: 36
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C - Update
« Reply #6 on: September 21, 2023, 05:44:53 PM »
I found a more comprehensive include file for the Text Object Model on the internet at https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/um/TOM.h than the original include file that I used. This has additional functions such as functions for tables so I decided to update my original files to use the more comprehensive include file.

Some function names changed as follows:

Previous name         New name
get_Selection             GetSelection
get_Start                   GetStart
get_Font                    GetFont
get_End                     GetEnd
get_Text                    GetText
get_Para                    GetPara
put_Underline            SetUnderline
put_ListType              SetListType


I have updated the original files and documentation to reflect these changes.

I have not included additional functions from the new include file in the updated zip file, mainly because there is little documentation on the Text Object Model. I spent a lot of time experimenting with the functions for tables and posted a query at https://stackoverflow.com/questions/76993004/using-the-text-object-interface-to-insert-a-row-in-a-table but have not yet received a reply. I live in hope!

Offline TimoVJL

  • Global Moderator
  • Member
  • *****
  • Posts: 2115
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1 of 2
« Reply #7 on: September 22, 2023, 09:57:46 AM »
Just an old story:
https://forum.pellesc.de/index.php?topic=7435.msg28216#msg28216

Pelles C have in menu File -> Load Type Library
May the source be with you

Offline CFred

  • Member
  • *
  • Posts: 36
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1 of 2
« Reply #8 on: September 23, 2023, 01:41:05 PM »
https://forum.pellesc.de/index.php?topic=7435.msg28216#msg28216

I saw this link and that is where I obtained the original include file. However, this did not cover functions for tables such as insert table, delete row, insert column, etc.

Pelles C have in menu File -> Load Type Library

I don't understand. When I select File from the menu in Pelles C there isn't a 'Load Type Library' option.


Offline KunJohn

  • Member
  • *
  • Posts: 3
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1 of 2
« Reply #9 on: September 23, 2023, 02:12:17 PM »
Thanks for a lot of valuable and useful information, I don't know if you have noticed one issue, and that is Pelles C has in the File - Load Type Library menu. Especially the index of a character, word, line, sentence or paragraph has been very useful for me in this case.
« Last Edit: September 25, 2023, 03:20:43 PM by KunJohn »

Offline TimoVJL

  • Global Moderator
  • Member
  • *****
  • Posts: 2115
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1 of 2
« Reply #10 on: September 23, 2023, 08:26:13 PM »
'Load Type Library' option is from Add-In
typelib. dll


EDIT:
WTypeLibList
« Last Edit: September 25, 2023, 10:25:43 AM by TimoVJL »
May the source be with you

Offline frankie

  • Global Moderator
  • Member
  • *****
  • Posts: 2113
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1 of 2
« Reply #11 on: September 23, 2023, 09:00:48 PM »
Eventually you can try also this.
« Last Edit: September 23, 2023, 09:03:28 PM by frankie »
"It is better to be hated for what you are than to be loved for what you are not." - Andre Gide

Offline CFred

  • Member
  • *
  • Posts: 36
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1 of 2
« Reply #12 on: September 24, 2023, 01:20:08 PM »
Thank you for all your replies.

Now I understand - I did not realise that 'Load Type Library' was an add-in.

I haven't had time to explore the use of the available add-ins yet as I only started to use Pelles C a few months ago (after using it back in the 2000's). I have just done a quick search on this forum for information on the addins provided with Pelles C but there did not seem to be any information about these. It may be helpful for newcomers to Pelles C to have a 'sticky' topic on this forum that briefly describes each of the addins provided with Pelles C.

The COM plain 'C' header generator looks really useful - I will have to familiarise myself with that software when I get time.

Offline John Z

  • Member
  • *
  • Posts: 860
Re: Tutorial: How to use the Text Object Model (TOM) in Pelles C Part 1 of 2
« Reply #13 on: September 25, 2023, 02:50:26 PM »
Hi CFred

The COM plain 'C' header generator looks really useful - I will have to familiarise myself with that software when I get time.

Here is another com helper library:
https://forum.pellesc.de/index.php?topic=10198.0

John Z