The following code appears to do the trick:

[pascal][background=#FFFFFF][normal=#000000][number=#FF0000][string=#0000FF][comment=#8080FF][reserved=#000000]function WordCount(const Text: String): Integer;
const
Whitespace: set of char = [#0..#32]; // chars to skip
var
i: Integer;
TextLength: Integer;
DoingWord: Boolean;
begin
Result := 0;

TextLength := Length(Text);
DoingWord := False;

for i := 1 to TextLength do
begin
// if not in whitespace, we've found a word. Next,
// we check if we weren't parsing a word - if we weren't
// then this is the start of another word, so we add
// one more to the result
if not (Text[i ] in Whitespace) then
begin
if not DoingWord then
begin
DoingWord := True;
Inc(Result);
end;
end
else // not doing a word
begin
DoingWord := False;
end;
end;
end;

// and an example use:
procedure TForm1.Button1Click(Sender: TObject);
begin
ShowMessage(IntToStr(WordCount(Memo1.Text)));
end;[/pascal]

Note that you can use "SomeMemo.Text" to get the memo's contents as a string.

The above works by going through each char and checking whether it's whitespace (or a control character). If so, it means no word is there - so the next time a non-control-char, non-space is found it's considered the start of the word.

You can bung more values into the set depending on your needs - for example:

Whitespace: set of char = [#0..#32, '.', ',', '[', ']', '(', ')']; // chars to skip

...which would also include periods, commas and parentheses there. Put whatever ones you need. You could also take the opposite approach and use a set to represent allowed characters, and say "if not (text[i] in allowedChars) then... deal with whitespace else deal with allowed letter".

There's probably a Windows message for this, though I don't know of it (anyone?).

The above function could be optimised using PChars (pointer-to-chars) and using some other tricks. That's left as an exercise

[EDIT: Oops! Off-by-one error there - the loop goes from 1 to length, not 0 to length-1!]