Page 1 of 1

question on RichView vs. RichEdit

Posted: Wed Nov 30, 2005 3:50 am
by toolwiz
I've got a Delphi 6 app where I'm currently using a component named TRichEdit98, which is a wrapper for Windows Richedt2.dll.

Basically, I have two richedit controls. Plain old ASCII text is loaded into one, some words are "selected", and as the text is copied into the other richedit control, the selected words are replaced with something else.

The problem I'm having is that the re.Text vs. re.Lines.Text properties produce different SELSTART values for the same words in the display, so it's really complicated to keep them synchronized based on SELSTART values.

So when I copy the text over to the second richedit control, some input texts replace the selected words properly, but some of them are skewed. Sometimes they're off by one letter per line counting from the start of the file, and sometimes they're off by one letter per line relative to the first line in the paragraph.

This behavior varies for indeterminate reasons; some files loaded via LoadFromFile work fine, others get skewed. Some text pasted from the clipboard works fine, some gets skewed.

The problem relates to differences in how "newlines" are counted -- sometimes <CRLF> is counted as one char, sometimes as two.

It's driving me absolutely NUTS... :evil:

The question I have is this: has anybody else run into this problem with standard TRichEdit controls and tell me for sure that switching to TRichView will solve the problem?

Posted: Wed Nov 30, 2005 2:29 pm
by Sergey Tkachenko
Functions from RVLinar unit provide one-to-one correspondence between text and (SelStart,SelLength):

Code: Select all

function RVGetLinearCaretPos(rve: TCustomRichViewEdit): Integer;
procedure RVSetLinearCaretPos(rve: TCustomRichViewEdit; LinearPos: Integer);
procedure RVGetSelection(rv: TCustomRichView; var SelStart, SelLength: Integer);
procedure RVSetSelection(rv: TCustomRichView; SelStart, SelLength: Integer);
function RVGetTextRange(rv: TCustomRichView; RangeStart, RangeLength: Integer): String;
function RVGetTextLength(rv: TCustomRichView): Integer;
Besides, it may be more convenient to use other methods (different from SelStart+SelLength) to define position of the marked words, but it depends on what do you want to implement.

Posted: Thu Dec 01, 2005 12:42 am
by shmp
Hi Toolwiz,

It depends how you look at TRichView as and how you understand the component. I have been using the this component for quite sometime and I had none of the problem you have mentioned. The skewing is most probably caused by Unicode characters or the draw method in TRichEdit.

There are some moments that I did experienced skewing but it is because of errors in my own coding especially while dealing with Unicode. As far as plain text is concerned, I did not face much difficulty.

My reference above is based of experience only.

I hope this could give you some insight to the component.

Henry.

Posted: Thu Dec 01, 2005 1:05 am
by toolwiz
As far as I can tell, the files I'm working with are just plain old ASCII text files. No Unicode. Some might be *nix format with just single-character <LF>'s instead of <CRLF> pairs.

The problem is that the re.Text property is a TCaption, while the re.Lines.Text property is a string. In particular,

Length(re.Text) <> Length(re.Lines.Text)

This is apparently because the re.Text property returns text based on whether wordwrap is enabled and in force. It monkeys with the newlines. Unfortunately, it seems that when you click the mouse, it reports its position relative to THIS, not relative to the position you'd expect in Lines. It's really subtle and quite annoying.

Posted: Thu Dec 01, 2005 1:11 am
by toolwiz
Sergey Tkachenko wrote:Functions from RVLinar unit provide one-to-one correspondence between text and (SelStart,SelLength):

Code: Select all

function RVGetLinearCaretPos(rve: TCustomRichViewEdit): Integer;
procedure RVSetLinearCaretPos(rve: TCustomRichViewEdit; LinearPos: Integer);
procedure RVGetSelection(rv: TCustomRichView; var SelStart, SelLength: Integer);
procedure RVSetSelection(rv: TCustomRichView; SelStart, SelLength: Integer);
function RVGetTextRange(rv: TCustomRichView; RangeStart, RangeLength: Integer): String;
function RVGetTextLength(rv: TCustomRichView): Integer;
Besides, it may be more convenient to use other methods (different from SelStart+SelLength) to define position of the marked words, but it depends on what do you want to implement.
Ok, that's a start. Please answer me this.

How would you identify the word under a mouse click?

And, given a list of words, how would you highlight or enable a hyperlink for the n'th word in the list? (Right now I'm keeping track of their individual SelStart and SelLength values. I also have to keep track of an adjusted_SelStart, which is adjusted for the number of <CR>s preceeding the word in the text.)

Thanks
-David

Posted: Fri Dec 02, 2005 12:19 am
by shmp
This is where re and rv differs slightly. rv returns the offset of an item (a sentence of texts is also considered an item) and not the position of the mouse click, which I found it convenient. The position of the mouse click has to be calculated by the user of rv himself.

There is already a function in rv that returns the (single) word clicked.

Posted: Sat Dec 03, 2005 10:45 am
by Sergey Tkachenko
1) TRichView can load text files where lines separated by CR+LF, LF+CR, only CR, only LF. The resulting document will be absolutely the same.
For saving, CR+LF is used.

2) The most universal method for detecting where the user clicked is using OnRVMouseUpEvent. In this event you can find the clicked position using GetItemAt method. If the user clicked on a text item, you can get its text string and the position in it (index of the clicked character). Using this information, you can extract the clicked word. See the example in the help file topic on OnRVMouseDown.

There is a simpler method - using OnRVDblClick event. This event occurs on double click, but can be called on a single click instead, if you include rvoSingleClick in RichView.Options. This event has two important parameters: (1) Style. If Style>=0, a text item is clicked. (2) ClickedWord.
The problem with this event - you cannot get the position of the clicked word.

There is also a special event for clicking hyperlinks.

3) As far as I understand, you know SelStart and SelLength, and you want to make a text defined by them a hyperlink. How should it look visually (blue, underlined, etc?). Should it store additional information about the link target?
Do I understand right - this document is read-only?
In TRichView/TRichViewEdit, you do not need adjusted SelStart/SelLength.

Posted: Sat Dec 03, 2005 12:50 pm
by toolwiz
Thanks, Sergey. This info is quite helpful.

Yes, both versions of the document are read-only in terms of the text contained in them. The hyperlinks are also fixed, but the background color attributes will change.

I don't particularly care how the words are indexed. The richedit control forces me to use SelStart, but then confounds things with the different ways it handles LFs and CRs.

My requirements are fairly simple. I start with a list of words. For every one of the words in the list that occur in the text, I want to create a hyperlink at that word and associate it with an object that's saved in a TList. When you click a word with a hyperlink, it needs to be able to look up its associated object in the TList and extract some data that's specific to that instance of the word. Five different instances of the same word will have five different objects in the TList, and they can contain different data for each word.

Conversely, I need to be able to iterate through the TList objects and change the background colors on corresponding words, depending on various conditions.

I've got a routine that maps SelStart to an item in the TList. But if RV provides a simpler mechanism, that's fine by me. (Eg., maybe it lets me access the words in the text as a big array of words?)

Thanks
-David

Posted: Sat Dec 03, 2005 2:46 pm
by Sergey Tkachenko
Below is the code.

It assumes:

1) The document is loaded in rve: TRichViewEdit. rve.ReadOnly=True. This code temporary sets it to False and then restores it back to True.
2) List of words is stored in WordList: TStringList. It is filled in before calling this code.
3) TList with information about hyperlinks has name HypObjects. Type of item in it is THypObject. This list is initially empty. When this code creates a new hyperlink, a new object is added to this list, and the hyperlink is linked to this object.
4) You said that your documents are plain text. I assume that text styles (RVStyle1.TextStyles) are defined as following:
TextStyles[0] - normal text;
TextStyles[1] - style for hyperlinks (RVStyle1.TextStyles[1].Jump=True, blue and underlined).
Other text styles are not used. You can define text styles at designtime, in the Object Inspector.
5) rvoTagsArePChars is not included in rve.Options (this is by default)
6) This document is formatted before calling this code.

For example, document can be loaded as

Code: Select all

rve.Clear;
rve.LoadText(FileName, 0, 0, False);
rve.Format;
Then the code below is called:

Code: Select all

var 
    Obj: THypObject;
    i, index: Integer;
begin
  SendMessage(rve.Handle, WM_SETREDRAW, 0, 0); // no repaint
  rve.ReadOnly := False;
  try
    for i := 0 to WordList.Count-1 do
    begin
      // moving caret to the beginning
      rve.SetSelectionBounds(0, rve.GetOffsBeforeItem(0), 0, rve.GetOffsBeforeItem(0));
      while rve.SearchText(WordList[i], [rvseoDown]) do begin
        Obj := THypObject.Create;
        Obj.Word := WordList[i]; // assigning properties of Obj
        HypObjects.Add(Obj);
        index := HypObjects.Count-1; // index of Obj in HypObjects
        // the word is selected in rve; applying hypertext style to it
        rve.ApplyTextStyle(1);
        // assigning the hyperlink's tag
        rve.SetCurrentTag(index);
      end;
    end;
  finally
    rve.ReadOnly := True;
    SendMessage(rve.Handle, WM_SETREDRAW, 1, 0);
    rve.Invalidate;
  end;
end;
When user click the hyperlink, rve.OnJump occurs:

Code: Select all

uses CRVFData;
procedure TForm1.rveJump(Sender: TObject; id: Integer);
var RVData: TCustomRVFormattedData;
    ItemNo, Index: Integer;
    ClickedWord: String;
begin
  rve.GetJumpPointLocation(id, RVData, ItemNo);
  Index := RVData.GetItemTag(ItemNo);
  ClickedWord := RVData.GetItemTextA(ItemNo);
  // now you can use HypObjects[Index]
end;
As you can see, SelStart and SelLength were not used.

Posted: Sat Dec 03, 2005 7:19 pm
by toolwiz
Wow, this is terrific Sergey! The code clearly is MUCH SIMPLER than the code for use with the RichEdit control.

Thanks!
-David

Posted: Thu Dec 08, 2005 11:50 am
by toolwiz
I've got this code:

Code: Select all

    // set the selected_word as hotlinked
    selected_kwd.hotlinked := true;
    // . . .
    SendMessage(TextInput_rvedt.Handle, WM_SETREDRAW, 0, 0); // no repaint
    iter := tagged_wds.NewIterator; // used by count_hotlinks!
    n_hotlinks := count_hotlinks( selected_kwd._word );  // the # of instances of this word that are hotlinked
    try
        iter.First;
        while (iter.KwdInfo <> nil) do begin
            if iter.MatchesWord( selected_kwd._word ) then
                HighlightKwd( iter.KwdInfo, (n_hotlinks = 0) );
            iter.Next;
        end;
    finally
        SendMessage(TextInput_rvedt.Handle, WM_SETREDRAW, 1, 0); // allow repaint
        TextInput_rvedt.Invalidate;
        iter.Free;
    end;
and

Code: Select all

procedure Tmain_form.HighlightKwd( pki : PKwdInfo; use_dflt_color : boolean );
var wd : string;
begin
    with TextInput_rvedt do begin
wd := GetSelText;  // ***
        SetSelectionBounds( pki.item_num, pki.i_offs, pki.item_num, pki.i_offs+Length(pki._word) );
wd := GetSelText;  // ***
        if use_dflt_color then
            ApplyTextStyle(YELLOW_HYPERLINK)
        else if pki.hotlinked
            then ApplyTextStyle(GREEN_HYPERLINK)
            else ApplyTextStyle(PLAIN_HYPERLINK);
    end;
end;
Why is it that the value returned by GetSelText is ALWAYS empty, even after calling SetSelectionBounds? Am I not calling SetSelectionBounds correctly?

pki.item_num is the value returned by rve.CurItemNo
pki.i_offs is the value returned by rve.OffsetInCurItem

The idea here is that I've got a bunch of words that appear initially as style YELLOW_HYPERLINK. When one is selected and 'flagged', (ie., pki.hotlinked=true) then the other similar words are set to style PLAIN_HYPERLINK and the 'flagged' word is set to "GREEN_HYPERLINK".

The rule is: if there are no words of a particular instance 'flagged', then all such words will appear YELLOW. If any are 'flagged', then only the 'flagged' words will appear as GREEN; the others will appear as PLAIN.

Posted: Thu Dec 08, 2005 8:52 pm
by Sergey Tkachenko
Applying text style may add or delete items. For example, if there is an item with text 'a b c', selecting 'b' and making it a hyperlinks creates 3 items instead of one:
'a ', 'b', ' c'.
I guess you code does not work because the stored pki.item_num, pki.i_offs are not valid, because making a new hyperlink can make old stored values incorrect.

Solution: store positions in richedit-like coordinates (SelStart, SelLength) using functions from RVLinear.pas

Posted: Thu Dec 08, 2005 9:11 pm
by toolwiz
It sounds like Heisenburg's principle is at work here! You're saying that the act of changing a style, but not content, actually alters the internal structure of a document?

When I went through the Help file, it suggests that each distinct word is an "Item". But when I went through the code and looked at values at run-time, it appears that, at least initially, each input line is an Item.

Actually, I think it would be much easier for me to deal with words having a hyperlink style as individual Items. The text itself is read-only, and some words are set as hyperlinks and left that way.

Later on, I need to go through the document and replace each of the hyperlinked words that have their "pki.hotlinked" value = true with some other text, and copy the transformed text into another RVEdit. (I don't need to change the values in the original text -- they're still the same text -- just in the second RVE.)

So, the first text has something like this (where "[xxx]" is a hyperlink containing word "xxx")

:: This is the [input] text with a few [hyperlinked] words.

If the first hyperlinked word, "input" has its .hotlinked = true, but not the other, then the transformed text written to the second RVE might look like this:

:: This is the <a href="someurl">input</a> text with a few hyperlinked words.

The contents of the original RVE remain unchanged.

I think that iterating through the list of Items would make it very easy to copy the ones with no hyperlinks and the hyperlinks with .hotlinked = false, and replace the others with whatever output text is desired.

Does this make sense to you? If so, how can I get the ItemNum/Offs for items AFTER they've been designated as hyperlinks? If I set the hyperlinks from start to end (rather than backwards), I should end up with the correct values, no?

If individual words are standalone items, wouldn't their OffsetInItem always be = 0?

Tks

Posted: Thu Dec 08, 2005 9:22 pm
by Sergey Tkachenko
No, words are not necessary items.
Normally, one item is a text of the same font, without line breaks and tab characters (line breaks are not items at all, tabs are special items).
In your special case, text of each hyperlink contain one word (because you make links from words), but it's not necessary for surrounding text.

Yes, changing style changes structure: it splits one item into several items, or, vice versa, joins adjacent items having the same font and other properties. This operation does not affect a number of characters, so all stored SelStart values remain valid.

Posted: Thu Dec 08, 2005 9:32 pm
by toolwiz
Ok, thanks for the clarification.


How does one go about creating a "clone" of one RVE into another RVE, including item tag values?

Also, if I'm iterating over rve.RVData.Items (so to speak), how do I detect the presence of newlines?