Tips about Delphi's TRichEdit
This is a list of tips and hints on Delphi database issues.
Most are in the Q&A format. Some are just e-mail messages.
This is a ragtag comglomeration of info from many sources.
Most of the info came from Compuserve, Delphi Talk, and various
newsgroups.
I can't vouch for the accuracy of every item - but I do make
corrections to this document as I learn of problems.
Please send me corrections so I can share them with others.
I also like to get additional examples to include here.
Eric Engler, October 21, 1998
englere@swcp.com
http://www.geocities.com/SiliconValley/Network/2114/
---------------------------------------------------------------------
Delphi 4 Note: I do not have Delphi 4 yet, but most of the information
here should work fine with Delphi 4.
---------------------------------------------------------------------
The original release of Delphi 2 had a bug in RichEdit that prevented
it from printing under NT 4. It was fixed in one of the minor updates.
Delphi 3 never had that bug.
-----------------------------
Delphi 1 had no RTF control.
Delphi 2 introduced TRichEdit.
Delphi 3 added TDBRichEdit.
Delphi 3's TDBRichEdit doesn't give you any way to access the raw
RTF codes, so I normally use TRichEdit with Delphi 3, and I do my own
loads/save to tables. Code for this is provided here.
---------------------------------------------------------------------
General Note: The RichEdit control sometimes needs to have the focus
before some options will work. So, if any option in this document is
not working for you, try to do this first: RichEdit1.SetFocus;
=====================================================================
Q: Which Microsoft DLL must be present on the user's hard drive to
support TRichEdit?
A: The RTF DLL needed by deployed Delphi 2 and 3 programs that use
TRichEdit is: RICHED32.DLL. Mine is still from the original
release of Win95. Most Win32 users have this already.
Some RTF enhanced controls (add-ons) for Delphi 3 require RICHED20.DLL.
This is an updated RTF control. It comes with NT 4, and it came with
IE 3 and IE 4 for Win95. It also comes with MS Office 97 and probably
comes with lots of other software. It has lots of enhancements over the
older RICHED32.DLL, but you need an enhanced Delphi VCL in order to make
use of the new features.
The best freeware enhanced richedit control that I know of is called
TRichEdit98/TDBRichEdit98, which I found in file RICHED98.ZIP on
the Delphi Super Page. The new features include support for embedding
OLE objects (used to embed pictures), and automatic recognition of
Internet URLs. There are also handy properties to tell you the current
row and column. It also supports multiple languages and Unicode. It
supports multiple levels of UNDO/REDO. It has many enhanced formatting
options such as: kerning, animation, shadow, emboss. It is by Alexander
Obukhov, E-mail alex@niiomr.belpak.minsk.by
This document will only discuss features available with the Delphi
native VCL, which uses the older RICHED32.DLL.
==============
Q: How do I set the background color?
A: SendMessage(RichEdit1.Handle, EM_SETBKGNDCOLOR, 0, clYellow);
or:
RichEdit1.Color:=clYellow;
============
Q: Many times I see programmers use "SendMessage" to send messages that
control a RichEdit, but other times I see them use "RichEdit1.Perform".
Why are there 2 ways of doing the same thing?
A: The generic Windows solution is to use "SendMessage", with the
control's handle as the first argument. This is what you normally see in
the Microsoft RichEdit documentation.
Delphi has a special "shortcut" method of sending a message to
a VCL, by using the VCL's own "Perform" method.
Both of these work fine, but the "Perform" is more efficient. I use
them both in my code.
SendMessage(RichEdit1.Handle, EM_SETBKGNDCOLOR, 0, clYellow);
is the same as:
RichEdit1.Perform(EM_SETBKGNDCOLOR, 0, clYellow);
============
Q: I want to find a specific word and color it blue.
A: This is a good and simple demo for you:
with RichEdit1 do
begin
SelStart :=FindText('word',0,GetTextLen,[stWholeWord]);
SelLength := 4; // select the word
SelAttributes.color:=clBlue;
SelLength:=0; // de-select the word
end;
==================
Q: I can't seem to figure out how to make a certain word in a
specific line of a TRichEdit component become BOLD or ITALIC?
A: Try something like the following:
procedure TForm1.Button1Click(Sender: TObject);
var
start: integer;
begin
start := RichEdit1.FindText(Edit1.Text, 0, -1, [stMatchCase]);
if start > -1 then
with RichEdit1 do begin
SelStart := start;
SelLength := Length(Edit1.Text);
SelAttributes.Style := [fsItalic];
SelLength := 0;
end;
RichEdit1.SetFocus;
end;
============
Q: How do I position the cursor at the beginning of the text?
A: RichEdit1.SelLength := 0;
RichEdit1.SelStart := 0; // here we go
RichEdit1.Perform( EM_SCROLLCARET, 0, 0 ); // ensure viewport is right
============
Q: How do I position the cursor at the end of the text?
A: RichEdit1.SelLength := 0;
RichEdit1.SelStart:=RichEdit1.GetTextLen; // position caret at end
RichEdit1.Perform( EM_SCROLLCARET, 0, 0 ); // ensure viewport is right
============
Q: How do I find out where the last character is?
A: Length(RichEdit1.Text)
or
RichEdit1.GetTextLen
or
(I think this last one works?)
LastChar := SendMessage(self.handle, EM_FORMATRANGE, 1, Longint(@Range));
=================
Q: I want to programatically insert text into a RichEdit box.
A:
procedure TForm1.Button1Click(Sender: TObject);
begin
with RichEdit1 do
begin
Lines.Clear;
SelStart := 0; // position to top of box
SelLength := 0; // nothing is selected
// Set the desired style for the first text
SelAttributes.Style := SelAttributes.Style - [fsBold];
// insert this text at the beginning of the box:
SelText := 'The dog';
// Position to point of insertion, but don't select anything.
// Set the SelAttrib's and insert the text at the point of the caret.
SelStart := 3; // point after "The"
SelLength := 0;
SelAttributes.Style := SelAttributes.Style + [fsBold];
SelText := ' big'; // it was a big dog!
end;
end;
===============
Q: How do I indent text in from the left?
A: RichEdit1.Paragraph.LeftIndent := 20;
RichEdit1.Paragraph.FirstIndent := 22;
Make sure your text is wrapped in a paragraph (CR at the end).
====================================
Q: How do I Print a page or range of pages?
A: Take a look at TGWRich in the Compuserve Delphi File Libraries. It's
supposed to handle this. But, the std TRichEdit has no concept of
"printed page boundaries".
NOTE: I found this VCL here:
http://www.delphiexchange.com/files/comvisual.html
There's a new freeware Richedit VCL that offers page-print support.
I haven't checked it out, but it's called RICHPR.ZIP and it's by
Gerrit Wolsink. It is available at the Delphi Super Page:
http://SunSITE.ICM.edu.pl/delphi/ftp/d20free/richpr.zip
================
Q: How can I assign the contents of 1 RichEdit box to another?
A: There is no way to directly assign them over. As you may have found,
if you simply assign the lines over you will lose the formatting info.
But, you can do it this way:
var
ms: TMemoryStream;
begin
ms:= TMemoryStream.Create;
RichEdit1.Lines.SaveToStream(ms);
ms.Position:=0;
RichEdit2.Lines.LoadFromStream(ms);
ms.Free;
end;
Note: Most people forget to set the position back to 0 before
loading from the stream.
================
Q: How could I merge 2 RichEdits? I want to append RichEdit1's
data to RichEdit2.
A: Use this:
RichEdit2.Lines.AddStrings(RichEdit1.Lines);
Follow-up Question:
Thank you very much for your answer.
It should work. It compiles fine, but when I run it, it seems
like I enter an infinite loop when executing the "AddStrings".
Here is exactly the code I tried:
RichEdit1.Lines.LoadFromFile('C:/Temp/Test1.RTF');
RichEdit2.Lines.LoadFromFile('C:/Temp/Test2.RTF');
RichEdit2.Lines.AddStrings(RichEdit1.Lines); // Infinite Loop ?
RichEdit2.Lines.SaveToFile('C:/Temp/Test.RTF');
I tried with different files RTF, but the result is the same). In
this precise case, I use the RTF file shipped with Delphi, in
"Delphi 2.0/Demos/RICHEDIT". Is there a bug around there, or did
I goof up ?
Answer:
I haven't tried it with those files, but when I wrote an example the
other day to test it I had no problems.
==================
Q: How do I Page forward/backward.
A: I assume you want to scroll up/down one logical screen at a time.
var
ScrollMessage:TWMVScroll;
...
{scroll the edit box all the way down}
ScrollMessage.Msg:=WM_VScroll;
ScrollMessage.ScrollCode:=sb_Bottom;
ScrollMessage.Pos:=0;
RichEdit.Dispatch(ScrollMessage);
{scroll the edit box all the way up}
ScrollMessage.Msg:=WM_VScroll;
ScrollMessage.ScrollCode:=sb_Top;
ScrollMessage.Pos:=0;
RichEdit.Dispatch(ScrollMessage);
{one page-up}
ScrollMessage.Msg:=WM_VScroll;
ScrollMessage.ScrollCode:=sb_PageUp;
ScrollMessage.Pos:=0;
RichEdit.Dispatch(ScrollMessage);
{one page-down}
ScrollMessage.Msg:=WM_VScroll;
ScrollMessage.ScrollCode:=sb_PageDown;
ScrollMessage.Pos:=0;
RichEdit.Dispatch(ScrollMessage);
==================
Q: How do I Move to specific line by index number?
A: For those who don't know, an index number is an embedded bookmark
in a RTF document. I don't think TRichEdit supports index numbers.
==================
Q: How do I change the font attributes for a single line, such as color?
A: Select a section of text and use SelAttributes to change it.
Set SelStart to the start, set SelLength to the length.
and SelAttributes.Color:=your_color.
Don't forget to de-select the text after by setting SelLength to 0.
======================
Q: How do I read a text file into a RichEdit box and give the new text
a specific set of attributes?
A: NOTE: This example also shows how to tell if a textfile is already in
an RTF format, so it can read it either way.
With RichEdit1 do
begin
Lines.Clear;
PlainText := not IsRtfFile(FileName);
// If PlainText is true, then we will NOT look for RTF tags in the input
DefAttributes.Name:='Fontname';
DefAttributes.size:=fontsize;
DefAttributes.color:=clRed;
DefAttributes.style:=DefAttributes.style + [fsBold];
Lines.LoadFromFile(Filename);
end;
Note: The ONLY time you should mess with "DefAttributes" is when
you know the RichEdit box is empty.
If it may already have text, you must use "SelAttributes" instead.
But if you do this, you'll have to select the last char before
setting the attributes. After setting them, de-select the last
character. Those attrib's will remain in effect as you read in
the text file.
===========
Q: Does anybody know a way to get the current cursor position (in columns
and rows) in a TRichEdit component ?
A: The following should work with TEdits, TMemos, and TRichEdits...
procedure GetEditColRow( CustEdit: TCustomEdit; var Col, Row: Integer );
begin
Row := SendMessage(CustEdit.Handle, EM_LINEFROMCHAR, CustEdit.SelStart, 0);
Col := Edit.SelStart - SendMessage(CustEdit.Handle, EM_LINEINDEX, -1, 0);
end;
===========
Q: How do I position the cursor to a certain row and column?
A: RichEdit1.SelStart := RichEdit1.Perform(EM_LINEINDEX, Row, 0) + Column;
RichEdit1.Perform(EM_SCROLLCARET, 0, 0);
===============
Q: Have you got any ideas how to activate a RichEdit component when it isn't
activated?
A:
if Screen.ActiveControl is TRichEdit then
TRichEdit(Screen.ActiveControl).SetFocus;
======================
Q: How can I add some text, and change it's attrib's later?
A: After a Lines.Add the selection start is on the start of the next line.
To find the position of the start of a specific line, use the EM_LINEINDEX
message. Example:
procedure TForm1.Button1Click(Sender: TObject);
Begin
With RichEdit1 Do Begin
// insert some text now
Lines.Add('normal bold');
// later, change it's attributes
SelStart := SelStart-6; { position to the b of bold }
SelLength := 4; { select the word }
SelAttributes.Style := [fsBold]; { set boldface style for it }
SelStart := Perform(EM_LINEINDEX, Pred(Lines.Count), 0);
{ position to start of added line }
SelLength := 6; { select "normal" }
SelAttributes.Color := clRed; { color it red }
SelLength := 0; { remove selection }
SetFocus; { activate control }
End;
End;
===============
Q: In the above example, you show a EM_LINEINDEX message. Is it a
valid Edit Control message, since it is not listed under the
standard RICH Edit Control messages?
A: EM_LINEINDEX is a valid message (and a very common one) that can
be sent to a RICHEDIT control. It is used to find your current line
number. Delphi's RichEdit documentation is very weak.
=============
Q: I am using the standard edit control messaging capabilities to find out
where I am in the RichEdit text at a particular time, based on the current
line number. To do this, I use the following syntax:
Offset := SendMessage(Handle, EM_LINEINDEX, LineNumber, 0);
This returns the current Offset within the control so that I can insert
text at that point. This code gets executed several times in a loop, and
it works perfectly until either the Control loses focus or the Application
loses focus. When this happens, I get spurious results back from this
procedure. Can you help?
A: Sorry, as I noted above, RichEdit often needs to have the focus before
it can do it's job.
======================
I've found that code in the Help files:
procedure TMainForm1.FindDialog1Find(Sender: TObject);
var
I, J, PosReturn, SkipChars: Integer;
begin
For I := 0 to RichEdit1.Lines.Count do
begin
PosReturn := Pos(FindDialog1.FindText,RichEdit1.Lines);
if PosReturn <> 0 then {found!}
begin
Skipchars := 0;
for J := 0 to I - 1 do
Skipchars := Skipchars + Length(RichEdit1.Lines[J]);
SkipChars := SkipChars + (I*2);
SkipChars := SkipChars + PosReturn - 1;
RichEdit1.SetFocus;
RichEdit1.SelStart := SkipChars;
RichEdit1.SelLength := Length(FindDialog1.FindText);
end;
end;
end;
But when I use this code it scrolls to the end of the text without
waiting when it finds the text.
BTW. This code is only working with frHideMatchCase,
frHideWholeWord, frHideUpDown on. Have you got any idea how I can
make my finddialog working with these functions off?
A: No
===============
Q: How do I change the alignment for all the text in the control?
A: use the following code:
LockWindowUpdate(RichEdit1.handle); //turn off updating of the RichEdit;
RichEdit1.SelectAll;
RichEdit1.Paragraph.Alignment:=taLeftJustify; // switch for other alignments
RichEdit1.SelLength:=0;
LockWindowUpdate(0); //turn on the updating of the RichEdit.
if you need to do it for the current paragraph, use the same code as
above, but take out the following lines:
RichEdit1.SelectAll;
RichEdit1.SelLength:=0;
The reason for this is because each paragraph in the RichEdit control has
it's own alignment setting and when you set the general one, it does not
change the paragraph that you are working on.
=========================
Q: What does EM_CharFromPos do?
A:Check out the win32.hlp file and look for the EM_CharFromPos message for the
RichEdit control. When sent, it returns the character index and line index of
the nearest character to the position that you send it.
=========================
Q: When I use the following EM_CHARFROMPOS sendmessage code for a RICH-EDIT
under Windows 95, the app raises an access violation:
res := sendmessage(RichEdit.Handle,EM_CHARFROMPOS,0,MAKELPARAM(50,50));
but the same code together with a normal TMemo (Memo1.Handle instead of
RichEdit1.Handle) works fine !! Is there a bug with this message type
and the Rich-Edit-Control ?
A: Yes, there is a bug in EM_CharFromPos, but I don't know the details
about the bug. See the following Q and A.
=========================
Q: I want the cursor to change when is slides over some text with unique
attributes like an HTML link or a help link). I have been trying to use
EM_CharFromPos, but it doesn't seem to work right.
A: It appears there is an error in the Win32 SDK documentation regarding
the EM_CharFromPos message. This error got into the Windows.pas file.
Secondly, there are two messages needed which won't work in NT 3.51 or before.
They are EM_EXSETSEL and EM_EXGETSEL. Now, I'm sure there is a more elegant
solution but this works very nice indeed:
procedure TMainForm.RichEdit1MouseMove(Sender: TObject; Shift: TShiftState;
X, Y: Integer);
var
mpt: TPoint;
cr,ccr: TCharRange;
i: Integer;
begin
i := SendMessage(RichEdit1.Handle,EM_EXGETSEL,0,longInt(@ccr));
mpt.x := x;
mpt.y := y;
i := SendMessage(RichEdit1.Handle,EM_CHARFROMPOS,0,LongInt(@mpt));
cr.cpmin := i;
cr.cpMax := i;
i := SendMessage(RichEdit1.Handle,EM_EXSETSEL,0,longInt(@cr));
if (fsBold in Richedit1.SelAttributes.Style) and
(fsUnderline in RichEdit1.SelAttributes.Style) then
Screen.Cursor := crHand
else
Screen.Cursor := crDefault;
i := SendMessage(RichEdit1.Handle,EM_EXSETSEL,0,longInt(@ccr));
end;
The declaration for TCharRange is contained in the RichEdit.Pas file. However,
if you include it in your uses clause it causes havoc with the Windows.pas file
and things just don't work. Again Richedit.pas is translated directly from
Richedit.H (part of the win32 SDK) and MS indicates that error pointed out
above spills into this header file also. I'll leave it to the reader to hunt
this down. To solve the declaration problems just declare the needed type and
const declarations in the mainform unit.
The next step is to be able to select all of the specially formated text and
use that as an index to a jump to some other topic or traditional help.
================
Q: Does anybody know how to correct the vertical scrolling of the RichEdit
control when the RichEdit control is resized. If the RichEdit height is say
100, and then the enduser sizes the form so the RichEdit' height now appears
to be 300.... when the enduser scrolls through the RichEdit's text, the text
is still displayed in the upper third of the RichEdit control instead of the
entire RichEdit control. This bug is happening in Delphi's RichEdit demo
project.
A: This is caused by a bug in NT 3.51. As a work-around, you can use
EM_SETRECT to forcibly resize the editing rectangle. This problem
doesn't exist on NT 4+ or Win95.
==============
Q: I need to ensure that the text I add to the RichEdit component is
visible. Currently, I'm doing this by scrolling it all the way to the
bottom, then executing one page up.
Like this:
var
ScrollMessage:TWMVScroll;
...
{scroll the edit box all the way down}
ScrollMessage.Msg:=WM_VScroll;
ScrollMessage.ScrollCode:=sb_Bottom;
ScrollMessage.Pos:=0;
RichEdit.Dispatch(ScrollMessage);
{one page-up}
ScrollMessage.Msg:=WM_VScroll;
ScrollMessage.ScrollCode:=sb_PageUp;
ScrollMessage.Pos:=0;
RichEdit.Dispatch(ScrollMessage);
Do you know of an easier way to do this? And while we are on the subject of
messages, where can I find out what the parts of TWMVScroll *mean*? Delphi's
help says what the are, but not what you can do with them. For example, what
does pos do? I've played with all the values but haven't had much luck
understanding them...
A:
After you have added text or moved the SelStart, send a EM_SCROLLCARET message
to the RichEdit to force it to scroll the caret into view if it's not visible:
RichEdit1.Perform(EM_SCROLLCARET, 0 ,0);
The parameters for windows messages are described under the corresponding
message identifiers, e.g. WM_VSCROLL. The only problem is that these (API)
descriptions constantly refer to the wparam and lparam message parameters,
which is exactly what the Delphi message records try to hide.
===============
Q: I want to use TRichEdit to build an RTF file for use in making a
help file...
A: Sorry, you can't. It doesn't support most of the features needed to
make a help file (footnotes, etc). You must use MS Word, or some other
truly good RTF editor.
==============
Q: I'm having problems setting the tabstops in a TRichEdit control, I
tried the same code on a TMemo and there it works properly...
procedure SetTabstops(..)
var
TabStops: array[0..3] of word;
begin
TabStops[0]:=10;
TabStops[1]:=20;
TabStops[2]:=30;
TabStops[3]:=40;
TS:=SendMessage(RichEdit.Handle, EM_SETTABSTOPS, 4, LPARAM(@TabStops));
end;
Why this doesn't work ? or what am I doing wrong?
A: Tab settings are part of the Paragraph formatting property. Any
changes you do to that property apply to selected paragraphs only.
I suggest you do a "RichEdit1.SelectAll;" first, and after setting
the tabs do a "RichEdit1.SelLength:=0;".
By the way, I'm not sure of the syntax of your code. I have never
done this by sending an API message, but I think it will work.
==============
Q: Give me another example of setting tabs.
A: Use the tab sub-property of the paragraph property.
You should do this in the OnCreate event of the form,
so it will affect all the paragraphs you create.
WARNING: It will not apply to paragraphs that are
pasted in.
const
TabStops := 4;
var
i: Integer;
begin
RichEdit1.SelectAll;
With RichEdit1.Paragraph do
begin
TabCount := 40; // number of tab stops
for i:=0 to TabCount do
Tab := (i+1)*TabStops;
End;
RichEdit1.SelLength := 0;
end;
==============
Q: How do I read a BIG FILE into RichEdit?
A: Is the Bible big enough?
procedure TForm1.LoadFile1Click(Sender: TObject);
var
data: string;
linecount: integer;
infile: textfile;
begin
assignfile(infile,'c:/text/Bible/kjv.txt');
reset(input);
if (IOresult<>0) then exit;
RichEdit1.lines.clear;
linecount:=0;
RichEdit1.visible:=false;
RichEdit1.MaxLength := High (Integer) - 1024;
while not eof(infile) do
begin
readln(infile,data);
inc(linecount);
if linecount mod 50 = 0 then
Application.ProcessMessages; // This is a friendly thing to do
RichEdit1.lines.add(inttostr(linecount)+' '+data);
end;
closefile(infile);
RichEdit1.visible:=true;
end;
OR, more simply (but Windows "locks up" while the file is being read):
RichEdit1.lines.clear;
RichEdit1.MaxLength := High (Integer) - 1024;
RichEdit1.PlainText := True;
RichEdit1.Lines.LoadFromFile('c:/text/Bible/kjv.txt');
==============
Q: Does anyone know how to carry out a word count for the delphi
richedit component?
A:
function GetWord: boolean;
var
s: string; {presume no word > 255 chars}
c: char;
begin
result:= false;
s:= ' ';
while not eof(f) do
begin
read(f, c);
if not (c in ['a'..'z','A'..'Z'{,... etcetera}]) then
break;
s:=s+c;
end;
result:= (s<>' ');
end;
procedure GetWordCount(TextFile: string);
begin
Count:= 0;
assignfile(f, TextFile);
reset(f);
while not eof(f) do
if GetWord then inc(Count);
closefile(f);
end;
================
Q: How do I capture the complete contents of an RTF memo to a bitmap?
A: Well, I surprised myself. I figured out how to do what you want. I
looked at the source for the TRichEdit.Print method and found the appropriate
way to copy a portion of the rich edit control to another canvas. This isn't
very well tested, but should at least get you headed in the right direction.
procedure TForm1.Button1Click(Sender: TObject);
var
Range: TFormatRange;
LastChar, MaxLen, LogX, LogY: Integer;
begin
FillChar(Range, SizeOf(TFormatRange), 0);
with Image1, Range do
begin
LogX := GetDeviceCaps(Canvas.Handle, LOGPIXELSX);
LogY := GetDeviceCaps(Canvas.Handle, LOGPIXELSY);
hdc := Canvas.Handle;
hdcTarget := hdc;
rc.right := Image1.ClientWidth * 1440 div LogX;
rc.bottom := Image1.ClientHeight * 1440 div LogY;
rcPage := rc;
LastChar := 0;
MaxLen := GetTextLen;
chrg.cpMax := -1;
repeat
chrg.cpMin := LastChar;
LastChar := SendMessage(RichEdit1.Handle, EM_FORMATRANGE, 1,
Longint(@Range));
until (LastChar >= MaxLen) or (LastChar = -1);
end;
SendMessage(RichEdit1.Handle, EM_FORMATRANGE, 0, 0);
end;
I don't know what you are going to do if your bitmap is not large enough to
accommodate the contents of the rich edit. I think the repeat..until in the
code above will at least things from blowing up if you run out of room on the
bitmap.
Be carefull... There are limits to the size of a bitmap under different
version of Windows. If I recall correctly, its somewhere in the range of 2.75
to 3.75 megs. The Video driver limitations will also some into play as well.
Joe C. Hecht (Borland)
================
Q: Is it possible to dragover a RichEditBox or MemoBox with the mouse and in
the meantime moving the caret with the position of the mouse?
A:
You could try sending a WM_LBUTTONDOWN message to the rich edit to simulate the
user clicking the mouse, then WM_MOUSEMOVE messages as the mouse moves and
finally a WM_LBUTTONUP when the mouse is released.
Two things to note: first, that you'll need to use Windows.GetFocus to note the
focus window before sending the WM_LBUTTONDOWN message. Then, use
Windows.SetFocus to set the focus back to the control that had it immediately
afterwards; secondly, you'll need to convert the mouse coordinates to local
coordinates for the richedit.
==========
Q: I need to print a huge RTF file. How can I do this without first
waiting for TRichEdit to read the entire file?
A: You can shell out to WordPad with the "/p" commandline switch:
ShellExecute(mainForm.handle,
nil,
'write.exe',
'myfile.rtf /p',
nil,
SW_HIDE);
(I found that using the WRITE.EXE stub is a bit more universal
because WORDPAD.EXE isn't always on the path.)
The "/p" parameter is the undocumented feature. It will launch
WordPad, print the file, then close WordPad. And with SW_HIDE, the only
thing you see is the Printing status box.
WordPad probably loads as much as it can into memory before
printing, but it should be able to handle any size file by
segmentation. And WordPad has a pretty small footprint, so it
loads and prints fairly quickly. It's also generally on every
Win95 system.
==========
Q: I need to know when a TRichEdit's scroll bars are visible or not. The
following code, adopted from something similar in the VCL, is not reliable for
TRichEdits (with HideScrollBars set to true and ScrollBars set to ssBoth):
function TMyForm.ScrollBarVisible(code: word): boolean;
var
min, max: Integer;
begin
result := False;
if (MyRichEdit.ScrollBars = ssBoth) or
((code = SB_HORZ) and (MyRichEdit.ScrollBars = ssHorizontal)) or
((code = SB_VERT) and (MyRichEdit.ScrollBars = ssVertical)) then
begin
GetScrollRange(MyRichEdit..Handle, code, min, max);
{ !!! Sometimes GetScrollRange returns 0 for min and 100 for max, even
though no scroll bar is visible. !!! }
result := (min <> max);
end; { if }
end;
Are there any other ways to tell if a TRichEdit's scroll bars are visible?
A: Not that I know of.
============
Q: Why can't I feed in filenames with spaces into my program that
uses LoadFromFile?
A: Your problem is in the registry. You told me before that this
problem occurs when double-clicking on .RTF files from Explorer.
When you changed the registered .RTF extension in the registry to point to
your program you had to specify the Shell/Open/Command keys and for the Command
to the path+program to execute plus a %1 to tell the Explorer to pass the
path+filename.ext that you double clicked as parameter string 1. You need to
modify this entry to enclosed the %1 in double quotes as follows "%1" this way
the embedded spaces will not act as delimiters.
Example:
HKEY_CLASSES_ROOT
.dmp Passmore file
Passmore file Dennis Passmore file
DefaultIcon C:/DELPHI32/SYSMANW/SYSMANW.EXE
Shell
Open
Command C:/DELPHI32/SYSMANW/SYSMANW.EXE "%1"
================
Q: How do I read an RTF Blob from a TTable? I want to read from a Memo
Blobfield and store the data into RichEdit1.
A: This is my preferred way to do it. I assume that Table1 is already open and
positioned on the record you want to update, and that 'RTF' is the fieldname
of the Blob in the table:
procedure TForm1.Button1Click(Sender: TObject);
var
theBStream: TBlobStream;
begin
RichEdit1.Clear; // in case there is no RTF data in this record
if Table1.State <> dsBrowse then
Showmessage('Error: table not in browse mode to read RTF');
if (not Table1.FieldByName('RTF').IsNull) then
begin
BlobStream:=nil;
try
BlobStream := Table1.CreateBlobStream(
Table1.FieldByName('RTF') as TBlobField, bmRead);
//ShowMessage('rtf blob size=' + IntToStr(BlobStream.Size));
if BlobStream.Size > 0 then
RichEdit1.Lines.LoadFromStream (BlobStream);
finally
if BlobStream <> nil then
BlobStream.Free;
end;
end;
RichEdit1.Modified:=False;
SetModifiedIndicator(False);
end;
=============
Q: How do I save the contents of my RichEdit1 into a Memo Blobfield of
a table?
A: This is my preferred way to do it. I assume that Table1 is already open and
positioned on the record you want to update, and that 'RTF' is the fieldname
of the Blob in the table:
var
BlobStream : TStream;
begin
if (not Richedit1.Modified) then
Exit; // exit if the data in the RichEdit1 VCL was not changed
BlobStream:=nil;
try
Table1.Edit;
BlobStream := Table1.CreateBlobStream(
Table1.FieldByName('RTF') as TBlobField, bmWrite);
RichEdit1.Lines.SaveToStream(BlobStream);
RichEdit1.Modified := False; // we saved the data, so start out fresh
finally
if BlobStream <> nil then
begin
BlobStream.Free; // IMPORTANT! Free the stream before doing the Post.
// By free'ing first, it prevents possible trouble if you have an
// OnDataChanged event handler that may change the current record
// immediately after the post takes effect.
try
Table1.Post;
dbiSaveChanges(Table1.Handle); // flush cache - in case of power failure
except
showmessage('Error while saving text');
end;
end;
if Table1.State = dsEdit then
Table1.Cancel;
end;
end;
=============
Q: How do I limit the amount of text that can go into RichEdit?
A: Send the TRichEdit component a EM_EXLIMITTEXT message.
============
Q: I want to show the user's current line number on a status bar on the
bottom of a form, How do I tell which line I'm on in a file opened in a
RichEdit component?
A: The following unit demonstrates:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ComCtrls;
type
TForm1 = class(TForm)
RichEdit1: TRichEdit;
Label1: TLabel;
procedure RichEdit1KeyUp(Sender: TObject; var Key: Word;
Shift: TShiftState);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
type
TMemoInfo = record
Line: integer;
Col: integer;
end;
procedure GetMemoPos(AMemo: TCustomMemo; var AMemoInfo:TMemoInfo);
begin
with AMemo, AMemoInfo do begin
{ Line number of line containing cursor. }
Line := SendMessage(Handle, EM_LINEFROMCHAR, SelStart, 0);
{ Offset of cursor in line. }
Col := SelStart - SendMessage(Handle, EM_LINEINDEX, Line, 0);
end;
end;
procedure TForm1.RichEdit1KeyUp(Sender: TObject; var Key: Word;
Shift: TShiftState);
var
MemoInfo: TMemoInfo;
begin
GetMemoPos(RichEdit1, MemoInfo);
with MemoInfo do
Label1.Caption := IntToStr(Succ(Line)) + ': ' + IntToStr(Succ(Col));
end;
end.
=============
Q: I've got a form with a TListView and a TRichEdit component. I already can
drag and drop from the TListView to the TRichEdit component but I inserts the
text at the place of the cursor. I'd like to insert this text at the position
of the mouse cursor, not the textcursor position.
Has anyone got an answer?
A: You could change the text cursor position so that it corresponds to the
mouse position and then insert the text. Send a ButtonDown message to
"lock in" the current mouse location as the new cursor location.
procedure TForm1.RichEdit1DragDrop( Sender, Source: TObject;
X, Y: Integer );
begin
if Source is TListView then
begin
SendMessage( RichEdit1.Handle, WM_LButtonDown, MK_LBUTTON,
MakeLParam( X, Y ) );
SendMessage( RichEdit1.Handle, EM_ReplaceSel, 1,
LongInt( PChar( ListView1.Selected.Caption ) ) );
end;
end;
==========
Q: I want to do something special when the user enters a certain word...
A: Try this. In the form turn KeyPreview to True then respond to the OnKeyPress
event. In the event check for the characters you want and send to the specific
objects you want by using the Perform method. For example:
if Key = 'A' then begin
ObjectThatGetsAs.Perform(WM_CHAR, ...);
Key := char(0);
end;
This is off the top of my head and it almost 12:00am, but it looks correct.
Give it a try.
=========
Q: I want to dynamically create an RTF control onto a tab sheet.
A:
procedure TMainForm1.Button12Click(Sender: TObject);
var
RichEdit: TRichEdit;
begin
NoteBook1.Pages.Add('Untitled');
TabSet1.Tabs.Assign(Notebook1.Pages);
TabSet1.TabIndex := NoteBook1.Pages.Count - 1;
RichEdit := TRichEdit.Create(self); // the form should be the Owner.
// The Notebook page should be the Parent.
RichEdit.Parent := Notebook1.Pages.Objects[Notebook1.PageIndex] as TPage;
end;
================
Q: I've loaded a large text (over 100K) into a RichEdit and now it won't let me
add more text. I have MaxLength = 0. But if I set Maxlength to (MaxInt-2),
it works. Is this a known bug?
A: This may be a Delphi 2 limit, since Borland used the non-extended GETSEL
and SETSEL messages in Delphi 2's TRichEdit control.
Although you can expand the maximum text by using EM_EXLIMITTEXT, you may still
run into 64K limits. The "SelStart" property of a TRichEdit is the same as
that for a TMemo - it uses the messages EM_GETSEL and EM_SETSEL, which are
limited to values between 0 and 65,535. If the selection is in the first 64K,
you're fine. Otherwise, these messages return -1. To get around this, you
need to use EM_EXGETSEL and EM_EXSETSEL, which don't have the 64K limit. To
me, this is a bug in TRichEdit - Borland should've used these new messages
instead of the 16-bit ones.
In addition, the message EM_LINEFROMCHAR also has a 64K limit. You should use
EM_EXLINEFROMCHAR instead.
It took me a while to track these down, so I figured I could save someone else
some time by posting this information here. What's odd about this is that I
never ran into the limit myself - it was users of my application who found it.
I've been able to edit files in excess of 800K without a problem, but several
users reported problems with files >64K. I was never able to reproduce the
problems under either Win95 or NT4, and I used the same files that users
reported problems with. Does anyone know why I would never experience the
problem while others would?
===============================
Q: How many bytes can I store in RichEdit?
A: In theory, it seems to be about 2 Gigabytes. It depends on the property
"MaxLength". For some obscure reasons, setting this to 0 limits the plain text
size to 64 KByte. I use a value of:
Editor.MaxLength := High (Integer) - 1024;
OR, you can do it with a message:
RichEdit.Perform(EM_EXLIMITTEXT, 0, NewSize);
But, some of Delphi's RTF methods don't use the extended Win32 messages
(as of Delphi 2; not sure about Delphi 3), so some features may fail to
work when you have lots of text!
===============
Q: If you could help me, I would be most gracious. I am having trouble
reading a TRichEdit from a stream. (I want to read the whole component
and it's contents)?
A:
Note: your problem has nothing whatsoever to do with streams. You just happened
to notice it here!
Actually, what you are missing is related to how Windows works, not Delphi. And
believe me, to extend the tree analogy, Delphi is a walk in the park while
Windows is the deepest darkest part of the Black Forest.
Here's the deal. For Delphi components which are windowed controls two things
actually get created: 1) the Delphi component itself, and 2) the Windows
windowed control. The first occurs in the constructor, like all Delphi objects.
The second occurs *after the constructor*. This is the key to your problem.
You showed us your streaming code:
S := TFileStream.Create (OpenDialog1.FileName,fmOpenRead);
S.Seek(0,0);
...
But you didn't show WHERE you were calling it. This is a crucial bit of
information. I would guess you are calling it from a constructor (or OnCreate
event). The problem with that is the Windows control hasn't been created yet.
To understand why the Lines property is causing the problem, you must
understand how Delphi components interface to Windows.
The write method for TRichEdit.Lines looks like this:
procedure TRichEditStrings.Put(Index: Integer; const S: string);
var
Selection: TSelection;
begin
if Index >= 0 then
begin
Selection.StartPos := SendMessage(RichEdit.Handle, EM_LINEINDEX, Index, 0);
if Selection.StartPos <> -1 then
begin
Selection.EndPos := Selection.StartPos +
SendMessage(RichEdit.Handle, EM_LINELENGTH, Selection.StartPos, 0);
SendMessage(RichEdit.Handle, EM_SETSEL, Selection.StartPos,
Selection.EndPos);
SendMessage(RichEdit.Handle, EM_REPLACESEL, 0, Longint(PChar(S)));
end;
end;
end;
TRichEditStrings is a private class (descended from TStrings) which Delphi uses
to manage the strings, but you don't really need to know this (because it is an
implementation detail).
The key here is the SendMessage statements. Instead of managing the strings in
memory owned by the component, TRichEdit uses the *standard Windows control
itself to manage the strings*. It does so by sending *standard Windows messages
to and from the control*.
This is a beautiful encapsulation of the Windows interface, and is one of the
great aspects of Delphi.
The problem is that SendMessage requires a valid window handle as its first
parameter. The window handle *is not valid when the component is first
created*. It only becomes valid at some point after the component's constructor
has completed, and the Windows control has been created.
So what you are doing is changing a property which requires a window handle
(via SendMessage), but doing so before the window handle has been created. This
is why you get the error message, and, by the way, the "Control '' has no
parent window" comes from Windows, not Delphi. It is Windows way of saying
"Hey, you just sent a message to a window handle but we cannot figure out what
the heck you were referring to".
The solution is to defer modifying the Lines property (or any other property
which requires a window handle) until you are sure the entire Windows control
(and the window handle) have been created. You have two options for this:
1) Use an overridden CreateWnd method. Calling inherited first will ensure the
handle is properly created. This is formally the correct place for this, and is
what I prefer.
2) Use an overridden Loaded method. Some people like this better because it is
easier to understand. The Loaded method gets called as the tail end of the
process, so you'll know the handle is created by that point.
Note: this entire discussion relates to many other properties besides the
TStrings you mentioned (TMemo.Lines, TRichEdit.Lines, etc.). These all have in
common the fact that they use the Windows API for storage (via SendMessage) but
there are other properties like this as well.
Rick's Rule: if you see the "Control '' has no parent window" message, move the
property assignment to the CreateWnd or Loaded methods. It is that simple.
- Rick Rogers, Fenestra Technologies
UPDATE:
My previous suggestion to set the properties in the CreateWnd or Loaded methods
only pertains to component development. If you had tried to set the RichEdit
lines property in the component's constructor, you would have gotten the
"Control '' has no parent window" message, and moving this assignment to
CreateWnd or Loaded would have solved your problem.
Your problem is that ReadComponent reads in a component and its properties, but
you haven't set the component's Parent yet, so ReadComponent itself is
assigning a value to Lines when the control doesn't have a Parent.
The solution is to use the TReader and TWriter classes, as this unit
demonstrates:
>>>>
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls, ComCtrls;
type
TForm1 = class(TForm)
RichEdit1: TRichEdit;
Button1: TButton;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.Button1Click(Sender: TObject);
var
Stream : TFileStream;
Filer : TWriter;
begin
Stream := nil;
Filer := nil;
try
Stream := TFileStream.Create('c:/test.dat', fmCreate);
Filer := TWriter.Create(Stream, 4096);
Filer.WriteRootComponent(RichEdit1);
finally
Filer.Free;
Stream.Free;
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
Stream : TFileStream;
Filer : TReader;
begin
RichEdit1.Free;
Stream := nil;
Filer := nil;
try
RichEdit1.Free;
Stream := TFileStream.Create('c:/test.dat', fmOpenRead);
Filer := TReader.Create(Stream, 4096);
Filer.Parent := Self;
Filer.Owner := Self;
Filer.ReadRootComponent(RichEdit1);
finally
Filer.Free;
end;
end;
initialization
begin
RegisterClass(TRichEdit);
end;
end.
<<<<
Setting the TReader Parent and Owner properties means any objects dynamically
created from the stream will have their Parent and Owner properties set before
other properties are assigned. Cheers.
- Rick Rogers, Fenestra Technologies
============
Q: I need a way to have the Found Text shown at the top of RichEdit1
(not at the last line)
A:
procedure TForm1.TabSet1Change(Sender: TObject; NewTab: Integer;
var AllowChange: Boolean);
var
S: String;
CurrentCursorPos, FoundTextPos : longint;
begin
RichEdit1.SelStart := 0;
S := TabSet1.Tabs.Strings[NewTab];
CurrentCursorPos:=RichEdit1.SelStart;
FoundTextPos:=RichEdit1.FindText('CHAPTER'+' '+S,CurrentCursorPos+1,
RichEdit1.GetTextLen,[]);
if FoundTextPos=-1 then begin
MessageDlg('Text Not Found',mtError,[mbOK],0);
RichEdit1.SelStart:=CurrentCursorPos;
end
else
begin
RichEdit1.SelStart:=FoundTextPos;
SendMessage(RichEdit1.Handle,EM_SCROLLCARET,0,0); <=== REPOSITION MEMO
end;
end;
=====================
Q: I can't get wingdings to show up even in design mode!
A: Windings is a "symbol style" true type font. The MS DLL (RICHED32.DLL) that
TRichedit wraps doesn't support the use of symbol style fonts.
UPDATE:
The RTF-Control displays these fonts: format a text with WordPad, save it with
a .rtf extension and load it into your TRichEdit. Borland simply forgot the
CFM_CHARSET flag (it's still undocumented in the Delphi 3 Win32 help file).
Accordingly the Delphi 3 TextAttributes class got a new (documented) CharSet
property.
Try something like this :
procedure TForm1.FormCreate(Sender: TObject);
begin
RichEdit1.Text:='Hello World!'#13#10'Hello World!';
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
with RichEdit1 do
begin
SelStart:=0;
SelLength:=12;
SelAttributes.Name:='Wingdings';
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
cf:TCharFormat;
begin
RichEdit1.SelStart:=14;
RichEdit1.SelLength:=12;
fillchar(cf,SizeOf(cf),#0);
with cf do
begin
cbSize:=SizeOf(cf);
dwMask:=CFM_FACE or CFM_CHARSET;
bCharSet:=DEFAULT_CHARSET;
bPitchAndFamily:=VARIABLE_PITCH;
szFacename:='Wingdings';
end;
SendMessage(RichEdit1.Handle, EM_SETCHARFORMAT, SCF_SELECTION, LPARAM(@cf));
end;
=====================
Q: How can I convert an RTF file to a std text file?
A:
Use the tools you've got. Put a TRichEdit on a form, set Visible := false,
adjust Width to avoid word-wrap and execute code similar to the following:
with RichEdit1 do begin
Clear;
PlainText := false;
Lines.LoadFromFile('QandD.Rtf');
PlainText := true;
Lines.SaveToFile('QandD.Txt');
end;
=======================
Q: How can I automatically scroll the text?
RichEdit.SelLength := 0;
currentline := Richedit.Perform(EM_LINEFROMCHAR, Richedit.SelStart, 0 );
Richedit.SelStart := richEdit.perform( EM_LINEINDEX, currentline+1, 0 );
RichEdit.Perform( EM_SCROLLCARET, 0, 0 );
This should move the caret one line down and scroll the control when the caret
moves beyond the bottom edge.
Also, there is this way:
RichEdit1.perform (WM_VSCROLL, SB_BOTTOM, 0)
RichEdit1.perform (WM_VSCROLL, SB_PAGEUP, 0)
====================
q: How do I implement context-sensitive help on words in
a RichEdit box?
A:
well, the main task is to convert the caret position (i assume you are
working in a TMemo or TRichedit) into a line/column postion, get the lineit
is in and isolate the word by searching back- and forwards from the caret
position to find where the word starts and end. The rest is just a call to
Application.helpcommand( HELP_KEY ...);
The key is the first task:
caretRow := Memo1.PerForm(EM_LINEFROMCHAR,Memo1.SelStart,0);
caretCol := Memo1.SelStart-Memo1.Perform(EM_LINEINDEX,caretRow,0);
line := memo1.lines[caretRow];
line[caretCol+1] is the starting position for the search for where the
word starts and ends.
Some tips to help you isolate the word:
in Pascal you can access the individual characters in a string like the
elements of an array (in fact a string is an array, of characters). So you
go about it this way:
Const
wordchars = ['a'..'z','A'..'Z']; { a set of char }
{add other chars that are allowed in a word}
Var
line, theWord: String;
n, wordstart, wordend: Integer;
Begin
line := RichEdit.Lines[currentlineindex];
n := currentcolumnindex+1;
{ column indices are zero based, string indices are 1-based }
if line[n] In wordchars Then Begin
wordstart := n-1;
While (wordstart >0) and (line[wordstart] In wordchars) Do
Dec(wordstart);
{ we moved one position to far back, wordstart is now on the
character before the word starts. Fix that. }
Inc(wordstart);
wordend := n+1;
While( wordend <= Length(line)) and (line[wordend] In wordchars) Do
Inc(wordend);
{ wordend is now one character beyond the end of the word }
theWord := copy(line, wordstart, wordend-wordstart);
End {if}
Else
theWord := EmptyStr;
{ caret was not on a word }
==============
Q: I need to select a bunch of text to send it to the clipboard, but I
don't want to distract the user. How can I do it?
A:
move the focus to another control (see SetFocus) before you select the text in
the RichEdit. The selection is only visible (unless you set the HideSelection
property to false) if the RichEdit has the focus.
Just remember that you need to send the focus to a visual control, not
a form.
==============
Q: I want to copy the richedit codes to a memo box so I can study them.
How can I see these codes, which are normally hidden?
A:
var
MemStream1: TMemoryStream;
begin
{ copy data from RTFEdit control to a std memo }
MemStream1 := TMemoryStream.Create;
RichEdit1.PlainText:=False; { we want the rtf formatting stuff }
RichEdit1.Lines.SaveToStream(MemStream1);
MemStream1.Position := 0;
RichEdit1.PlainText:=False; { we want the rtf formatting stuff }
Memo1.Lines.LoadFromStream(MemStream1);
MemStream1.Free;
end;
WARNING! Although this should work the same way for DBRichEdit, it
doesn't work on Delphi 3's implementation. I haven't tested other
Delphi versions for this bug. On Delphi 3, DBRichEdit can only
write the RTF formatting codes to a data table, not to a stream or
a file. It seems to ignore the PlainText property, and it is always
treated as true.
==============