|
Find Memo Control - Felix John COLIBRI.
|
- abstract : the e c_find_memo unit allows to place on a tForm a tMemo
with "find", "find next" and save functions. The found occurences are
highlighted and the text scrolls to this position
- key words : tMemo - Search function - Line of Caret - Scroll to Caret
- software used : Windows XP, Delphi 6
- hardware used : Pentium 1.400Mhz, 256 M memory, 140 G hard disc
- scope : Delphi 1 to 8 for Windows, Kylix
- level : Delphi developer
- plan :
1 - Introduction
As you might have noticed, we very often use tMemos in our applications, for
debug or display purposes. It sometimes happens that in huge debug logs, we
want to find some string patterns. The basic tMemo does not include a "find"
function. This is why we created the c_find_memo.
2 - Organization
2.1 - The search function
The control will be a tMemo descendent (or container) with the added search
functionality.
To search a pattern in a string, there are many possibilities:
- the Pos function
- search algorithm (Boyer More Horspool etc)
- direct search with Pascal test and loops
The Pos function only finds the first occurence. To find the next, we must
build a string with the part remaining after the current occurence, which is
somehow a waste of time (and complicated for backward searches)
The Boyer More Horspool is quite nice, and we presented an implementation for
disc "find in file" utility (see the the coliget
file expression search engine ), but is more heavy to implement. For a
simple tMemo search, we considered it overkill.
So we implemented a very simple algorithm:
- loop until the first matching character is found
- if this is the case, check the end of the search string
2.2 - tMemo functions
The tMemo function follows the classical Delphi encapsulation pattern:
- Windows can create Edit Controls
- Delphi encapsulates this Edit in a CLASS.
In addition to the display attributes, Left, Top, etc, Delphi added the
Lines and Text PROPERTIEs.
The Text PROPERTY allows to get or set the text. Like the Caption
PROPERTY, it simple reads or write the string that Windows saves along with
the position parameters, the handle etc.
The Lines PROPERTY is computed upon loading (in assembler).
However, the tMemo lacks some methods which could be useful:
- find the line (index or string) of the Caret
- scroll to a specified line
To find the index of a line, we simply added a function which compares the
running text index with SelStart.
The scrolling is done using Windows API calls (or sending Windows messages):
- em_GetFirstVisibleLine tells us the index of the current top line
- em_LineScroll performs the scrolling
2.3 - The control creation
As already presented in the 3d Graphic Designer
paper, we will not build a component, but only a CLASS whith automatic
control creation. In our case the c_find_memo CLASSe's CONSTRUCTOR uses a
tPanel reference, and nests inside this tPanel:
- the find tEdit
- the find next tButtons
- the tMemo
This automatic creation avoids the tedious clicks to place those controls when
a find memo is used in a more complex project. We simply create the
c_find_memo, and the creation is performed.
Sure, this is not as nice as having a full fledged c_find_memo component on
the Palette. But on the other hand, using a CLASS with self creating
possibilities (but no REGISTER procedure) avoids the installation on the
Palette.
3 - The Delphi Source code
4 - The c_find_memo UNIT
Here is the definition:
c_find_memo= class(c_basic_object)
m_c_parent_component_ref: tWinControl;
m_save_path: String;
// -- optional to avoid erasing the selected part
// -- (could also use a ReadOnly memo)
m_do_hilite_result: Boolean;
m_c_panel: tPanel;
m_c_text_length_label, m_c_text_line_count_label: tLabel;
m_c_find_edit: tEdit;
m_c_find_button, m_c_next_button, m_c_sort_button: tButton;
m_c_save_edit: tEdit;
m_c_memo: tMemo;
m_find_string: String;
m_find_length: Integer;
m_text: String;
m_text_index: Integer;
m_text_length: Integer;
m_did_hit_control: Boolean;
Constructor create_find_memo(p_name: String;
p_c_parent_component_ref: tWinControl;
p_save_path: String);
procedure set_panel_color(p_color: Integer);
function f_memo_line_of_index(p_text_index: Integer): Integer;
procedure set_memo_top_line_index(p_top_line_index: Integer);
procedure find_next_occurence;
procedure handle_find_edit_keypress(p_c_sender: TObject;
var pv_key: Char);
procedure handle_memo_and_find_edit_keydown(p_c_sender: TObject;
var pv_scan_code: Word; p_shift_state: TShiftState);
procedure handle_next_click(p_c_sender: tObject);
procedure handle_sort_click(p_c_sender: tObject);
procedure handle_memo_change(p_c_sender: tObject);
procedure handle_save_edit_keypress(p_c_sender: TObject;
var pv_key: Char);
Destructor Destroy; Override;
end; // c_find_memo
|
And:
- m_c_parent_component_ref: this is the Control which will contain our tMemo
- m_save_path: the default save path which will be used when saving the text
- m_do_hilite_result: the user can cancel selection highlighting, since there
is a risk of accidentally removing the highlighted found occurence when
hitting the keyboard
- m_c_panel, m_c_text_length_label, m_c_text_line_count_label,
m_c_find_edit, m_c_find_button, m_c_next_button, m_c_sort_button,
m_c_save_edit: these controls are automatically created when calling the
CONSTRUCTOR
- m_find_string, m_find_length, m_text, m_text_index, m_text_length: the
search attributes
- m_did_hit_control: used to manage Ctrl-L keypresses
The constructor builds the controls:
Constructor c_find_memo.create_find_memo(p_name: String;
p_c_parent_component_ref: tWinControl;
p_save_path: String);
procedure create_panel;
const k_button_height= 25;
var l_x: Integer;
begin
m_c_panel:= f_c_create_panel('panel', '',
m_c_parent_component_ref, m_c_parent_component_ref,
alTop, 0, k_button_height+ 2, clBtnFace);
l_x:= 15;
m_c_text_length_label:= f_c_create_label('tlength', 'length',
m_c_panel, m_c_panel, alNone, l_x, 2+ 4, -1, -1);
Inc(l_x, m_c_text_length_label.Width+ 5);
m_c_text_line_count_label:= f_c_create_label('tline', 'lines ',
m_c_panel, m_c_panel, alNone, l_x, 2+ 4, -1, -1);
Inc(l_x, m_c_text_line_count_label.Width+ 5);
Inc(l_x, 10);
m_c_find_edit:= f_c_create_edit('edit', 'find',
m_c_panel, m_c_panel, l_x, 2, 100, k_button_height);
with m_c_find_edit do
begin
OnKeyPress:= handle_find_edit_keypress;
OnKeyDown:= handle_memo_and_find_edit_keydown;
Inc(l_x, Width+ 5);
end; // with m_c_find_edit
m_c_next_button:= f_c_create_button('next', '>',
m_c_panel, m_c_panel, l_x, 2, 20, k_button_height);
Inc(l_x, m_c_next_button.Width+ 5);
m_c_next_button.OnClick:= handle_next_click;
m_c_sort_button:= f_c_create_button('sort', 'sort',
m_c_panel, m_c_panel, l_x, 2, 40, k_button_height);
Inc(l_x, m_c_sort_button.Width+ 5);
m_c_sort_button.OnClick:= handle_sort_click;
Inc(l_x, 10);
m_c_save_edit:= f_c_create_edit('save', 'save.txt',
m_c_panel, m_c_panel, l_x, 2, 100, k_button_height);
with m_c_save_edit do
begin
OnKeyPress:= handle_save_edit_keypress;
Inc(l_x, Width+ 5);
end; // with m_c_save_edit
end; // create_panel
begin // create_find_memo
Inherited create_basic_object(p_name);
m_c_parent_component_ref:= p_c_parent_component_ref;
m_save_path:= p_save_path;
create_panel;
m_c_memo:= tMemo.Create(m_c_parent_component_ref);
with m_c_memo do
begin
Parent:= m_c_parent_component_ref;
Align:= alClient;
ScrollBars:= ssVertical;
OnKeyDown:= handle_memo_and_find_edit_keydown;
OnChange:= handle_memo_change;
end; // with m_c_memo
end; // create_find_memo
|
where the f_c_create_panel, f_c_create_edit etc are very simple function
returning a tPanel, tEdit etc.
This is our very simple search routine:
procedure c_find_memo.find_next_occurence;
function f_find_rest(p_index: Integer): Boolean;
var l_word_index: Integer;
begin
l_word_index:= 2;
while (l_word_index<= m_find_length)
and (p_index+ l_word_index<= m_text_length)
and (m_find_string[l_word_index]= m_text[p_index+ l_word_index- 1]) do
inc(l_word_index);
Result:= l_word_index> Length(m_find_string);
end; // f_find_rest
var l_first_character: Char;
begin // find_next_occurence
with m_c_memo do
begin
Inc(m_text_index, m_find_length);
l_first_character:= m_find_string[1];
while m_text_index< m_text_length do
begin
if m_text[m_text_index]= l_first_character
then
if f_find_rest(m_text_index)
then begin
set_memo_top_line_index(f_memo_line_of_index(m_text_index));
SelStart:= m_text_index- 1;
if m_do_hilite_result
then SelLength:= m_find_length;
set_panel_color(clLime);
exit;
end;
inc(m_text_index);
end; // while pv_index
end; // with m_c_memo do
set_panel_color(clRed);
end; // find_next_occurence
|
And to position the Caret on the found occurence and scroll the Memo:
function c_find_memo.f_memo_line_of_index(p_text_index: Integer): Integer;
var l_line_index: Integer;
l_running_text_index: Integer;
begin
Result:= 0;
l_running_text_index:= 0;
with m_c_memo do
for l_line_index:= 0 to Lines.Count- 1 do
if l_running_text_index> p_text_index
then begin
Result:= l_line_index- 1;
Break;
end
else Inc(l_running_text_index, Length(Lines[l_line_index])+ 2);
end; // f_memo_line_of_index
procedure c_find_memo.set_memo_top_line_index(p_top_line_index: Integer);
var l_first_visible_line: Integer;
begin
with m_c_memo do
begin
l_first_visible_line:= Perform(em_GetFirstVisibleLine, 0, 0);
if l_first_visible_line<> p_top_line_index
then Perform(em_LineScroll, 0, p_top_line_index- l_first_visible_line);
end;
end; // set_memo_top_line_index
|
4.1 - Snapshot
Here is the snapshot of the tForm after creation (we loaded the main form
text):
And this is the result after hitting "Felix Enter":
4.2 - Mini HowTo
The main form created the control with:
procedure TForm1.create_Click(Sender: TObject);
begin
g_c_find_memo:= c_find_memo.create_find_memo('find', Panel3,
f_exe_path+ 'log\');
with g_c_find_memo do
begin
m_c_memo.Lines.LoadFromFile('..\p_find_memo\u_find_memo.pas');
m_do_hilite_result:= True;
end; // with g_c_find_memo
end; // create_Click
|
The search of the first occurence is done
- by hitting Control-F when focus is in the tMemo, or selecting the find_edit
- by entering the searched string, and hitting Enter
Next searches are done
- by hitting Control-L, or the ">" next button
To save the file (in the path specified during creation) is done by:
- entering the file name in the save edit
- hitting Enter
5 - Improvements
We could add the following features:
- replace the Pascal search loops with more efficient algorithms (Boyer Moore
Horspool)
- add the other find capabilities (backward, first, last, case sensitive,
words only)
- add the replace function
- add a tSaveDialog for the saving
- promote the CLASS to component status (add the PROPERTIes, the
PUBLISHED visibility definitions and the REGISTER call)
6 - Download the Sources
Here are the source code files:
Those .ZIP files contain:
- the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any
other auxiliary form
- any .TXT for parameters
- all units (.PAS) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly
mentioned).
- can be used from any folder (the pathes are RELATIVE)
- will not modify your PC in any way beyond the path where you placed the .ZIP
(no registry changes, no path creation etc).
To use the .ZIP:
- create or select any folder of your choice
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder.
As usual:
- please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs or had
some problem downloading the file. Resulting corrections will be helpful
for other readers
- we welcome any comment, criticism, enhancement, other sources or reference
suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an
answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow
developpers, add a link to your links page ou mention our articles in
your newsgroup posts when relevant. That's the way we operate: the more
traffic and Google references we get, the more articles we will write.
7 - Conclusion
The c_find_memo unit allows to place on a tForm a tMemo with find and save
functions
8 - The author
Felix John COLIBRI works at the Pascal
Institute. He programs in Pascal since 1979, and is mainly active in the area
of custom software
development and training, and is a frequent speaker at Borland
Developer Conferences. His web site features
tutorials, technical papers about programming with full downloadable source
code, and the description and calendar of forthcoming Delphi,
Interbase, Asp.Net, Ado.Net and OOP / UML training sessions.
|