For another project, I needed a data-aware editor based the TDBRichEdit component provided with Delphi but with built-in editing and formatting capabilities. Additionally, I needed to use it in several location in my projects. Ideally, it would be a component comprised of several components but I backed up facing the difficulties of developing such component and I decided to use the TFrame class introduced by Borland in Delphi 5.
The difficulties which one encounters when one wants to conceive and develop composite component comprised of other components from the VCL (Visual Component Library) and from other sources are enormous. This is due to the fact that during the process, the visual programming environment of Delphi is no longer available.
A few years ago, I developed a personal information manager based on a Paradox database in which the user could write text, modify it and format it. I then used the data-aware DBRichEdit component which is part of the Delphi component palette.
If I had had to use it only once, I would have probably put the component on the form and I would have surrounded it with the controls necessary for its management. The problem is that I wanted to re-use it at least twice in this applications and also re-use it elsewhere. I had only one choice then because I worked then with Delphi 3 programming environment: I had to design a complex component, a task really not easy without the visual environment of Delphi. I waited a little and Delphi 5 saved me because it allowed an important extension of the concept of the visual form inheritance with a new class which then made my work easy: the frame.
The objective of this work was to equip an application on which I worked at the time with the built-in capacity to write, edit and format and save the content of a TDBRichEdit components in a portion of a window. Additionally, I wanted to be able to re-use the code at several places in the aplication. Essentially, I wanted to have a sort of component based on a TDBRichEdit component that would also be comprised of controls allowing to edit (cut, copy, paste, add or delete text), to format (to put in bold, in italic, to underline, justify and add bullets to the paragraphs), to search for words, to spell-check and to print the contents of it. the frame was the perfect tool for such a project.
This editor is comprised of a TDBRichEdit component surrounded by various buttons that enable Copy, Cut, Paste, Bold, Italic, Underline, Color, Bullet, Print and Check spell and grammar that are wrapped in a TFrame component.
The frame, a descendent of the TFrame class, is a new class that was introduced with Delphi 5. On a frame, one can put components, write event handlers for each component and then, put the frame on a form or on another frame. In other words, a frame looks like a form since it is a component container. What differentiate a frame from a container is that it defines a part of a window rather than a complete window.
What makes frames very interesting is that they behave almost like components: one can create various instances of them at design time while allowing the designer to conceive them visually. I will not go any further in the detail of the frames here because this exceeds the intent of this page. I thus recommend the reader the book "Mastering Delphi 5" by Macro Cantu where one will find all that it is necessary to know in order to work with this class.
Rich Text Format (RTF)
Text is considered rich if it can display various characters or paragraphs in different styles and features that make it more attractive than a regular ASCII text. Such a text can have some of its sections in different colors. Its paragraphs can have customized attributes or arranged independent of each other. Although you can create a complete rich but static text, the common use of a rich text is to let the user process most of the formatting [Windows Control: The Rich Text Box].
In 1992, Microsoft introduced the Rich Text Format (RTF) for specifying simple formatted text with embedded graphics. Initially intended to transfer such data between different applications on different Operating Systems (MS-DOS, Windows, OS/2, and Apple Macintosh), today this format is commonly used in Windows for enhanced editing capabilities. For a more complete discussion of this format, see "Rich Text Format (RTF) Version 1.5 Specification"
Delphi has two components that display RTF: the TRichEdit component and its data-aware counterpart, the TDBRichEdit component.
This frame needed the following components::
- a TDBRichEdit component that displays rich-text from a blob contained in the table to which it is connected;
- a TActionList component which provides access to edit (EditUndo, EditCut, EditCopy, EditPaste), format (RichEditBold, RichEditItalic, RichEditUnderline, RichEditStrikeOut, RichEditBullet, RichEditAlignLeft, RichEditAlignRight et RichEditCenter) from the Delphi environment as well as to a genuine category of actions that I created specifically for the TDBRichEdit component (GtroRichEditBold, GtroRichEditItalic, GtroRichEditUnderline, GtroRichEditLeftJustify, GtroRichEditCenter, GtroRichEditRightJustify, GtroRichEditBullet et GtroRichEditColor)
- a TControlBar component to hold the following tool bars:
- la barre d'outils ToolBarEdit d'édition avec les boutons:
- a TToolButton button to undo le formatage fait précédemment. This button is connected to the EditUndo action;
- a TToolButton button to cut the selected text. This button is connected to the EditCut action;
- a TToolButton button to copy the selected text. This button is connected to the EditCopy action;
- la barre d'outil ToolBarStyle de formatage des sélections qui comptrend
- a TToolButton button to put the selected text in bold. This button is connected to the GtroRichEditBold action;
- a TToolButton button to mettre en italique du texte. This button is connected to the EditUndo action;Ce bouton est connecté à l'action GtroRichEditItalic;
- a TToolButton button to underline the selected text. This button is connected to the EditUndo action;Ce bouton est connecté à l'action GtroRichEditUnderline;
- a TToolButton button to color the selected text. Ce bouton est connecté à l'action GtroRichEditColor;
- la barre d'outil ToolBarPara de formatage des paragraphes qui comprend
- a TToolButton button to left-justify the text. This button is connected to the EditUndo action;Ce bouton est connecté à l'action GtroRichEditLeftJustify;
- an TToolButton button to center the text. This button is connected to the EditUndo action;Ce bouton est connecté à l'action GtroRichEditCenter;
- a TToolButton button to right-justify the text. This button is connected to the EditUndo action;Ce bouton est connecté à l'action GtroRichEditRightJustify;
- a TToolButton button to add bullets. This button is connected to the EditUndo action;Ce bouton est connecté à l'action GtroRichEditBullet;
- la barre d'outil ToolBarOthers pour l'impressione et la recherche qui comprend
- a TToolButton button to print the text displayed on the TDBRichEdit component. This action must be performed by the host application as it is not implemented by the frame;
- a TToolButton to search for words in the component or in the in the memo fields of the table;
- a TToolButton to spell-check the text of the memo or to spell-check selected words in the text.
- la barre d'outils ToolBarEdit d'édition avec les boutons:
- a TStatusBar component that appears at the bottom of the frame;
- a TImageList component containing the images displayed on the buttons
- a TPopupMenu component which displays all the edit, format and other functions provided by the tool buttons. These menu items use the same actions as the tool buttons;
- a TFindDialog component displayed when the search button is clicked;
- a TColorDialog component which is displayed when the color button is clicked; and
- a TSpeller component downloaded from Luzius Schneider's Web site.
Figure I displays the frame as it appears in the host application. From top to bottom, there is the command bar with its four tool bars and their buttons, the TDBRichEdit component and the status bar that indicates the position of the cursor in the editor.
Once the components were put on the frame, what was left to do was to integrate them with the appropriate code. The forthcoming sections describe this process.
Rather than proramming the editing and formatting actions, I have used the TAction and TActionList components introduced in Delphi 4 by Borland. These components greatly simplify programming the actions that occur when clicking the buttons or when activating one of the items of the context menu. Thee components also allow detemining the state of all the elements and components connected to the actions. For more details on these components, the eader is referred to Chapter 5 of Reference 1. It also discussed at some length in the Annex entitled "Actions and Action lists".
For the implementation of the edit functions, i.e., for the buttons that perform the editing (undo, cut, copy, paste), I have used the Delphi's preprogrammed actions that inherit from the TEditAction class. For the formatting functions (bold, italic, underline, ...) I thought I could use the actions derived from the TRichEditAction class of Delphi but I soon noticed that these actions, if they perform the required formatting actions, did not put the underlying dataset in edit mode thus causing an exception when posting.
As a consequence, I had to develop genuine actions that, after the formatting of the selected text, did put the underlying data set in edit mode (dsEdit): there were derived from the TGtroRichEditAction class. The code of these action is the same as that of the descendant of the TRichEditAction class with the added feature that they put the underlying data set in edit mode.
As the change of mode of the dataset did not preserve the selection, it was also necessary to wrap the transition to the edit mode with code that preserve the selection through the change of mode. This code is executed ih the ExecuteTarget method of the TGtroRichEditAction class, a method that is called when the method of the same name in its derived classes is executed. This code follows:
procedure TGtroRichEditAction.ExecuteTarget(Target: TObject) var Start, Len: integer; begin if GetControl(Target) is TDBRichEdit then begin Start:= TDBRichEdit(Target).SelStart; Len:= TDBRichEdit(Target).SelLength; // saves the selection TDBRichEdit(Target).DataSource.Edit; // puts the data set in edit mode TDBRichEdit(Target).SelStart:= Start; // recuperation of the selection TDBRichEdit(Target).SelLength:= Len; end;end;
An undesirable behavior of the TDBRichEdit component of the editor caused me some concern. When editing or posting the content of the editor, the display of the content of the TDBRichEdit component always returned to the beginning of the text irrespective of what was displayed before any of these actions. This was caused by the transition of the state of the data set from dsBrowse to dsEdit when editing or from dsEdit to dsBrowse when posting.
To correct this behavior, I have added event handlers for the BeforeEdit, BeforePost, AfterEdit and AfterPost events of the TDBRichEdit component. The first two call the BeforeEvent method whereas the other two call the AfterEvent method. The details of these changes are as follows.
This method is called by the BeforeEdit and BeforePost event handlers and it memorizes the selection and the position of the text displayed in the editor by memorizing the beginning of the selection in the RemStart private variable, the length of the selection in the RemSel private variable and the index of the first visible line of the display in the RemLast private variable . This operation is performed by sending the EM_GETFIRSTLINEVISIBLE message to the TDBRichEdit component as shown below:
procedure TFrameEdit.BeforeEvent; begin RemStart:= DBRichEdit.SelStart; // memorizes the beginning of the selection RemSel:= DBRichEdit.SelLength; // memorizes the length of the selection RemLast:= DBRichEdit.Perform(EM_GETFIRSTVISIBLELINE, 0, 0); // memorizes the index of the first line end;
This method is called by the AfterEdit and AfterPost event handlers and it preserves the position of the display of the editor through the edit and post actions.
procedure TFrameEdit.AfterEvent; begin DBRichEdit.Perform(EM_LINESCROLL, 0, RemLast); DBRichEdit.SelStart:= RemStart;DBRichEdit.SelLength:= RemSel; end;
As one can see from the preceding code, after editing or saving, the view of the data is returned to its original position through the EM_LINESCROLL message. The text that was selected previously remains selected because the selection was memorized in the variables called RemStart and RemSel.
This framed editor, as well as all the applications that it was part of, had an occasional weakness: the editor stocks its data in a Paradox database. As the record is a BLOB that is kept in wo files, a .db file that contains a fixed length part of the test whereas the rest of the txt is stocked in a .mb file. Here is the prblem: it may happen that the BDE is closed in an irregular fashion (for instance, if the system crashes before the post is complete) causin a "BLOB has ben modified" error which occurs when one of the two files has been updated while the other has not.
This weakness has been corrected using the BDE function DBiSavhanges() in the AfterPost event handler of the data set to which the editor is connected. The code of this event handler is as follows:
procedure TFrameEdit.AfterPost(DataSet: TDataSet); var Table: TTable; begin AfterEvent; Table:= DBRichEdit.DataSource.DataSet as TTable; Check(DBiSaveChanges(Table.Handle)); // forces all updated records to disk if Assigned(FAfterPost) then FAfterPost(DataSet) end; // TFrameEdit.AfterPost
where the highlighted statement has been added and forces all updated records to disk.
Events of the underlying data set
In the preceding sections, I have described how the data-aware framed editor reacted to the events of the data set to which it is connected. As it is necessary that the editor be connected to a data source (TDataSource) which is itself connected to a data set (TDataSet), it is therefore necessary that the event handlers associated with this data set in the host application be executed when edit and post events occur in the editor. The Initialize() method performs this task for the BeforePost, BeforeEdit, AfterPost and AfterEdit events of the data set. This code is listed hereunder:
procedure TFrameEdit.Initialize; begin if Assigned(DBRichEdit.DataSource) then begin if Assigned(DBRichEdit.DataSource.DataSet) then begin // the editor is connected to a data set if Assigned(DBRichEdit.DataSource.DataSet.BeforePost) then FBeforePost:= DBRichEdit.DataSource.DataSet.BeforePost; DBRichEdit.DataSource.DataSet.BeforePost:= BeforePost; if Assigned(DBRichEdit.DataSource.DataSet.BeforeEdit) then FBeforeEdit:= DBRichEdit.DataSource.DataSet.BeforeEdit; DBRichEdit.DataSource.DataSet.BeforeEdit:= BeforeEdit; if Assigned(DBRichEdit.DataSource.DataSet.AfterPost) then FAfterPost:= DBRichEdit.DataSource.DataSet.AfterPost; DBRichEdit.DataSource.DataSet.AfterPost:= AfterPost; if Assigned(DBRichEdit.DataSource.DataSet.AfterEdit) then FAfterEdit:= DBRichEdit.DataSource.DataSet.AfterEdit; DBRichEdit.DataSource.DataSet.AfterEdit:= AfterEdit; end; // ...if end; // if Assigned(DBRichEdit.DataSource) end;
I would have likes this frame to be completely encapsulated by putting this code in the OnCreate event handler of the frame but the TFrame class does not have such an event. As a consequence, I has to declare Initialize() as a public method to be called by the host application once it is certain that the frame is completely constructed. I therefore recommend the user to call this method from the OnCreate event handler of the host form as I did.
Additionally, this editor can search for words using the FindText method of the TDBRichEdit class. This capacity is implemented in the FindDialogFind procedure, the code of which follows hereunder. It is the OnFind event handler of the FindDialog component of the editor and it is triggered when the user, after having clicked on the Search button, clicks on Next in the non-modal dialog. It uses the FindText method in order to search the content of the TDBRichEdit component.
procedure TFrameEdit.FindDialogFind(Sender: TObject); // published var FoundAt: LongInt; L: Integer; StartPos, ToEnd, FirstLine: Integer; Options: TSearchTypes; begin FStopSearch:= False; repeat // until the end of file Options:= ; // options for the DBRichEdit.FindText dialog if frMatchCase in FindDialog.Options then Options:= Options + [stMatchCase]; if frWholeWord in FindDialog.Options then Options:= Options + [stWholeWord]; with DBRichEdit do begin if SelLength <> 0 then // any text selected? StartPos := SelStart + SelLength // positionne le curseur après la sélection else StartPos := 0; ToEnd := Length(Text) - StartPos; FoundAt := FindText(FindDialog.FindText, StartPos, ToEnd, Options) if FoundAt <> -1 then // found ... begin Show; // show the content of the control FirstLine:= Perform(EM_GETFIRSTVISIBLELINE, 0, 0); SelStart := FoundAt; // Beginning of the selection L := Length(FindDialog.FindText); SelLength := L; // length of the selection Perform(EM_LINESCROLL, 0, CaretPos.Y - FirstLine); // ajouté 13 mai 05 SetFocus; Inc(NumFound); StatusBar.Panels.Text:= 'Found = ' + IntToStr(NumFound); if Assigned(FOnFind) then OnFind(Self, FoundAt, L); Exit; end // if FoundAt else begin // pas trouvé ... Hide; // hide the content of the control Application.ProcessMessages; if FRecurseSearch then if frDown in FindDialog.Options then DataSource.DataSet.Prior else DataSource.DataSet.Next; end; // else FoundAt end; until DBRichEdit.DataSource.DataSet.Eof or DBRichEdit.DataSource.DataSet.Bof or not FRecurseSearch; FindDialog.CloseDialog; DBRichEdit.Visible:= true; end;
The RecurseSearch property determines if the search will be performed in the whole table or only on the text displayed in the editor. The FStopSearch variable which prompts the end of the serach becomes "true" in the OnClose event handler of the search dialog box when the user clicks on "Cancel".
FirstLine:= Perform(EM_FIRSTLINEVISIBLE, 0, 0); ... Perform(EM_LINESCROLL, 0, CaretPos.Y - FirstLine);
are used to maintain the display of the selection visible in the control. These statements replace
FirstLine:= Perform(EM_SCROLLCARET, 0, 0);
that shoul have done the job but did not.
The frame shows a button that allows printing but the printing mechanism is not implemented in the editor. However, the OnClick event handler of that button is available for implementation by the client application.
A spell check capability was first implemented using ISpell but it was abandonned early. Recently (June 2013), I decided to use MS Word grammar/spell check as discussed in "A spell and grammar checker using Word".
Correcting a weakness
This framed editor, and the project that it was a part of, showed an occasional weakness: the editor keeps its data in a Paradox table. As the record containing the data in the table is a formatted BLOB of text stoked in two files (a .db file containing a fixed-length of the text and a .mb file containing the rest of the blob), it could happen (and it happened) that the blob data became corrupted when the BDE was closes inadvertantly (for instance, when the operating system crashed). In such instance, the records was not updated properly and the error "BLOB has been modified" showed up.
This weakness has been corrected using the BDE function DBiSaveChanges() in the AfterPost event handler of the data set. The code of this event handler was modified as follows:
procedure TFrameEdit.AfterPost(DataSet: TDataSet) var Table: TTable; begin AfterEvent; Table:= DBRichEdit.DataSource.DataSet as TTable; Check(DBiSaveChanges(Table.Handle)); // stockage sur disque immédiat if Assigned(FAfterPost) then FAfterPost(DataSet); end; // TFrameEdit.AfterPost
where the highlighted statement was added and causes the immediate updating of the BLOB in the dataset.
The source code can be downloaded here.
The TDBRichEdit component of the Delphi component palette can display formatted text but does not provide the user interface elements needed to perform such formatting. Our editor provides such capability through the inclusion of buttons that allow various actions such as cut, copy, paste and undo, or formatting of the text (bold, italic, underline, centering, left or right justifying, and other functions). This tool is a fairly complete editor but there is no way to change the fonts locally: this capability was not added as I did not need it for the project that I developed the component for.
It would have been possible to add more capabilities. Of course, I have used a TDBRichEdit component (a descendant of TCustomRichEdit) that encapsulated the access to the Microsoft Rich Edit 1.0 format implemented in riched.dll. I could have used the TRxRichEdit from RXLib that encapsulated the RichEdit 2.0 format and introduces several new methods and properties. For more details on this, you are referred to "Using Rich Edit 2.0 With BCB".
- "Mastering Delphi 5" par Marco Cantu, Copyright © 1999 Sybex, Inc, ISBN: 0-7821-2565-4