KControls/KMemo notes

From Lazarus wiki
Revision as of 10:24, 9 October 2017 by Dbannon (talk | contribs) (clean up)
Jump to navigationJump to search

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.

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 6 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.

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 : longin t;
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. A little strange, the source says parameter is an integer, not a TKMemoBlockIndex so you may miss it when looking for something to delete a block. But thats 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.

Links

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. Check it can span blocks.

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.


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 -

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.