KControls/KmemoNotes

From Lazarus wiki
Revision as of 06:49, 10 October 2017 by Paskal (talk | contribs) (Hyperlinks)

DRAFT Introduction DRAFT


This page is about the KMemo Component, a part of KControls. KMemo provides a Cross Platform (Linux, Windows, Mac OSX) memo capable of a range of text font styles, colours and similar. Its also capable of showing images and a range of other things but that may need to be dealt with at a later stage. And, importantly, it really does work pretty much the same way across Linux, Windows and Mac. Possibly other platforms as well but I cannot speak authoritatively there.

The content presented here is in addition to the manual distributed with the KControls package. Its written by a KMemo user, not the author and could contain errors, omissions and possibly outright lies ! Someone else will undoubtedly use a different set of methods so that person is urged to document what they find too. And correct any errors they find here.

Underlying structure

Its important, when using KMemo, to understand how the memo content and its meta data is stored. Everything is in a Block, and KMemo itself has a Blocks property. A Block has a Text property that contains the text being displayed, a Font property and a number of others controlling how the text is displayed. Every change to font style, colour, size and so on requires a new block. Block classes discussed here include TKMemoTextBlock, TKMemoParagraph, TKMemoHyperlink. A TKMemoParagraph appears between two paragraphs.

This line of text is an example.

It will contain 5 blocks.

  • Block 0 "This " - normal font.
  • Block 1 "line of " - bold
  • Block 2 "text is an " - bold and italic
  • Block 3 "example." - italic
  • Block 4 - a paragraph marker.

The KMemo, Block and Blocks classes have a large number of other Properties and Methods, far too many to possibly document here. And there are also many associated classes. Lazarus will prompt you with them and the names are reasonably intuitive.

Inserting Text

Easy. KMemo1.Blocks.AddTextBlock(AString). That will append the block at the end of any existing blocks. You can add a number after the string parameter and it will be inserted at that BlockIndex. Its a function, returning the TKMemoBlock so, once created you can alter how that text is presented.

procedure Form1.AddText();
var
    TB: TKMemoTextBlock;
begin
  TB := KM.Blocks.AddTextBlock(InStr);
  TB.TextStyle.Font.Size := 16;
  TB.TextStyle.Font.Style := TB.TextStyle.Font.Style + [fsBold];
end;

Now, presumably, AddTextBlock() has allocated some memory for that TextBlock, we assume it will Free it when appropriate.

NOTE: By default upon creation of a table, a text block containing a new line is created witin it. Usually it has to be removed by:

procedure Form1.AddText();
   KM.Blocks.Clear;
end;

Playing With Blocks

You will get used to working with the two indexes, one character based and the other Block based. Both start at zero.

The Blocks class has a property, Items that can let you address an individual block, so KMemo1.Blocks.Items[BlockNo] is a particular block. And it has a whole bunch of properties and methods.

There are KMemo1.Blocks.Count blocks in a KMemo. And there is length(KMemo1.Blocks.Text) characters but only in a Unix based system such as Linux or OSX. All systems allow one character for a Paragraph marker in the KMemo itself but Windows, being Windows puts a CR/LF, (#13#10) at the end of each paragraph in KMemo1.Blocks.Text. So you need allow for that, for an example, see below about Searching.

How to convert between the two ? Like this

var 
    BlockNo, CharNo, LocalIndex : longint;
begin
    CharNo := SomeValue;
    BlockNo := Kmemo1.Blocks.IndexToBlockIndex(CharNo, LocalIndex);

BlockNo now has what block number CharNo points to and LocalIndex tells us how far in the block we are. Useful information, if LocalIndex is 0, we are at the start of a block, if its length(KMemo1.Blocks.Items[BlockNo].Text)-1 we must be at the end of a block.

The reverse -

CharNo := KMemo1.Blocks.BlockToIndex(KMemo1.Blocks.Items[BlockNo]);

will convert a blocknumber to SelectionIndex type of number.

When iterating over blocks, important to know what each block is -

if KMemo1.Blocks.Items[BlockNo].ClassNameIs('TKMemoHyperlink') then
	URL := TKMemoHyperlink(KMemo1.Blocks.Items[BlockNo].URL);

Here we have found out that the Block at BlockNo is a Hyperlink block, we can therefore cast it to TKMemoHyperlink and get the URL stored there previously.

Kmemo1.Blocks.Delete(BlockNo);

To delete a block. The source says parameter is an integer, not a TKMemoBlockIndex so you may miss it when looking for something to delete a block. But that appears to be the one to use !

KMemo1.Blocks.DeleteChar(Index);

can also be a bit confusing. You would expect it to delete the character pointed to by Index, and yes, it might. But if there is another area of Text selected when you call it, instead, it will delete that other area of text. And that can be quite a surprise ! So, something like -

KMemo1.Select(Index, 0);
while Cnt < Len do begin			
  	KMemo1.Blocks.DeleteChar(Index);    
  	inc(Cnt);
end;

And, you may need to restore the selection points afterwards.

Hyperlinks

TKMemo supports text-only hyperlinks. As of October 2017 Hyperlinks containing images are not supported.

OK, lets suppose you have a KMemo full of text and you want to make a word of it a hyperlink. It will be blue, have an underline and when clicked, do something. The hyperlink will be one block (remember, everything is a block), so if the text we want to convert is already a discrete block, easy, delete it and then insert a new hyperlink block with the same text. However, its more likely we need to split a block. The process is to delete the characters that we want to become the link, split the block at that spot, create a TKMemoHyperlink block, configure it and insert it between the split blocks.

This procedure will make a hyperlink at the character position, Index and it will be Len long. It exits doing nothing if that spot is already a hyperlink.

procedure TForm1.MakeDemoLink(Index, Len : longint);
var
    Hyperlink: TKMemoHyperlink;
    Cnt : integer = 0;
    BlockNo, Offset : longint;
    DontSplit : Boolean = false;
    Link : ANSIString;
begin
	// Is it already a Hyperlink ?
    BlockNo := KMemo1.Blocks.IndexToBlockIndex(Index, Offset);
    if KMemo1.Blocks.Items[BlockNo].ClassNameIs('TKHyperlink') then exit();

    Link := copy(KMemo1.Blocks.Items[BlockNo].Text, Offset+1, Len);
    if length(Kmemo1.Blocks.Items[BlockNo].Text) = Len then DontSplit := True;
    KMemo1.Select(Index, 0);	// cos having a selection confuses DeleteChar()
    while Cnt < Len do begin
        KMemo1.DeleteChar(Index);
        inc(Cnt);
    end;
    if not DontSplit then BlockNo := KMemo1.SplitAt(Index);
    Hyperlink := TKMemoHyperlink.Create;
    Hyperlink.Text := Link;
    Hyperlink.OnClick := @OnUserClickLink;
    KMemo1.Blocks.AddHyperlink(Hyperlink, BlockNo);
end;

Now, danger Will Robertson ! This function will fail if you pass it parameters of text that are not within one block. But only because of the "Link := copy(..." line. Easy to fix with a few extra lines of code.

Oh, and whats this "@OnUserClickLink" ? Its the address of a procedure "OnUserClickLink()" that might be defined like this -

procedure TEditBoxForm.OnUserClickLink(sender : TObject);
begin
	showmessage('Wow, someone clicked ' + TKMemoHyperlink(Sender).Text);
end;

Search

Despite all those methods, I could not find a Search function. But its not that difficult to search KMemo1.Blocks.Text, treat it as an ANSIString. On Unix like systems, the position in KMemo1.Blocks.Text can be used directly as a TKMemoSelectionIndex. Sadly, in Windows, KMemo1.Blocks.Text has Window's two character line endings, but in KMemo itself, only one char is reserved for line endings. So, count the #13 characters from the start to the position of interest and subtract that from what becomes our TKMemoSelection index.

This procedure will search for the character '5' in the KMemo. It tells us what it found in a separate TMemo, I find that a good way to log whats happening during the experimental phase.

procedure TForm1.SimpleSearch();
var
    Index : longint = 1;		// ~.text starts at one, not zero !
    WinError : longint = 0;
begin
    while Index < length(KMemo1.Blocks.Text) do begin
        if #13 = KMemo1.Blocks.Text[Index] then
             inc(WinError);
    	if '5' = KMemo1.Blocks.Text[Index] then
             memo1.Append('Found it at ' + inttostr(Index - WinError));
        inc(Index);
	end;
end;

Now, be warned, that would be a seriously slow way to search even a medium sized file, especially if you expand it out to search for multi character strings as you almost certainly will need to. Copying the ~.Text to a PChar and working from was a lot faster in my app, your mileage may vary. But anyway, you get the idea here.

Locking

As your app grows, you may find that some processes start to take a significant amount of time. Here, Locking is not refering to where we lock a process to avoid interruptions, it actually speeds up the process, and quite substantially. If you have a series of (write) operations to perform on the contents of a KMemo, apply the lock before you start, release it when finished. This sort of thing -

try
	KMemo1.LockUpdate;
	while BlockNo < Kmemo1.Blocks.Count do begin
		SomeCrazyProcedure(KMemo.Blocks.Items[BlockNo]);
		inc(BlockNo);
	end
finally
	KMemo1.UnlockUpdate;
end;

Locking does not seem to make much difference where you are not writing to the contents nor for on-off procedures. It makes a big difference when you are doing a lot of stuff all at once.

Bugs

Yep, it has some bugs and some not yet implemented features. But not many ! Please report bugs at the link below.

Canceling Bullets

Late 2017 its been noted that there is a problem canceling Bullets on linux. Its fixed in the 2017-09-24 bitbucket version.

Direction of Selection

You select text (with a mouse) either from left to right or from right to left. You may notice that in KMemo, this direction is important if you use KMemo1.SelStart etc. This is NOT a bug, in fact, if it matters, you should be using KMemo1.RealSelStart (etc) instead.

However, in October, 2017, I noticed a similar effect, after selecting text as above, press Enter, this may either delete the text and replace it with a newline OR leave the text there and push it down a line. This is accepted as a bug, stay tuned.

Installation

KMemo must be installed as part of the KControls Package. As is usual, its easy. You should be using a recent version of FPC and Lazarus. Download the Bitbucket vesion of KControls, unzip and put it somewhere suitable. Fire up Lazarus, click Packages, click "Open Pack File (.lpk)" and find the kcontrolslaz.lpk file in the kit you downloaded.

Click Compile. As of late 2017, there is an issue that will cause the compile to fail telling you There is no method in an ancestor class to be overridden : "DoAutoAdjustLayout(const TLayoutAdjustmentPolicy;const Double;const Double;const Boolean);" in line (2945,15). just comment out the offending prototype and implementation, save and try again. Assuming all works, click Use, Install.

Lazarus will do its install thing, shortly will ask you about rebuilding, answer yes, it will then shutdown. On startup, you will find an extra row of components at the far right, KMemo amongst them.

If you find this as valuable a component as I did, I suggest you hit the link below and thank TK, it is one hell of an effort !

See Also