views:

277

answers:

3

I had some code before I moved to Unicode and Delphi 2009 that appended some text to a log file a line at a time:

procedure AppendToLogFile(S: string);
// this function adds our log line to our shared log file
// Doing it this way allows Wordpad to open it at the same time.
var F, C1 : dword;
begin
  if LogFileName <> '' then begin
    F := CreateFileA(Pchar(LogFileName), GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_ALWAYS, 0, 0);
    if F <> 0 then begin
      SetFilePointer(F, 0, nil, FILE_END);
      S := S + #13#10;
      WriteFile(F, Pchar(S)^, Length(S), C1, nil);
      CloseHandle(F);
    end;
  end;
end;

But CreateFileA and WriteFile are binary file handlers and are not appropriate for Unicode.

I need to get something to do the equivalent under Delphi 2009 and be able to handle Unicode.

The reason why I'm opening and writing and then closing the file for each line is simply so that other programs (such as WordPad) can open the file and read it while the log is being written.

I have been experimenting with TFileStream and TextWriter but there is very little documentation on them and few examples.

Specifically, I'm not sure if they're appropriate for this constant opening and closing of the file. Also I'm not sure if they can make the file available for reading while they have it opened for writing.

Does anyone know of a how I can do this in Delphi 2009 or later?


Conclusion:

Ryan's answer was the simplest and the one that led me to my solution. With his solution, you also have to write the BOM and convert the string to UTF8 (as in my comment to his answer) and then that worked just fine.

But then I went one step further and investigated TStreamWriter. That is the equivalent of the .NET function of the same name. It understands Unicode and provides very clean code.

My final code is:

procedure AppendToLogFile(S: string);
// this function adds our log line to our shared log file
// Doing it this way allows Wordpad to open it at the same time.
var F: TStreamWriter;
begin
  if LogFileName <> '' then begin
    F := TStreamWriter.Create(LogFileName, true, TEncoding.UTF8);
    try
      F.WriteLine(S);
    finally
      F.Free;
  end;
end;

Finally, the other aspect I discovered is if you are appending a lot of lines (e.g. 1000 or more), then the appending to the file takes longer and longer and it becomes quite inefficient.

So I ended up not recreating and freeing the LogFile each time. Instead I keep it open and then it is very fast. The only thing I can't seem to do is allow viewing of the file with notepad while it is being created.

+2  A: 

Char and string are Wide since D2009. Thus you should use CreateFile instead of CreateFileA!

If you werite the string you shoudl use Length( s ) * sizeof( Char ) as the byte length and not only Length( s ). because of the widechar issue. If you want to write ansi chars, you should define s as AnsiString or UTF8String and use sizeof( AnsiChar ) as a multiplier.

Why are you using the Windows API function instead of TFileStream defined in classes.pas?

Ritsaert Hornstra
I'm not sure if TFileStream was there when I coded this between Delphi 2 and Delphi 4. I must have found some code that used the Windows API routines - and since they worked, I went with it.
lkessler
He probably does not want to write UTF16 to the log file.
Warren P
@Warren: My new program is now Unicode, so I need to be able to write Unicode characters to the log file. Whether this is done in UTF-16 or UTF-8 is a different matter, but it can no longer be Ansi.
lkessler
AFAIK, UTF8 is the way to go. Most text editors accept ANSI and UTF8, and not all text editors accept UTF16, although notepad and wordpad do, UTF16 is a "rare choice" these days, like pictures in PCX format.
Warren P
@Warren: You might add saving an UTF8 BOM if the file did not exist already (eg after opening when the file size = 0). This way editors will automagically understand that it is UTF8
Ritsaert Hornstra
+1  A: 

Try this little function I whipped up just for you.

procedure AppendToLog(filename,line:String);
var
  fs:TFileStream;
  ansiline:AnsiString;
  amode:Integer;
begin
  if not FileExists(filename) then
      amode := fmCreate
  else
      amode := fmOpenReadWrite;
fs := TFileStream.Create(filename,{mode}amode);
try
if (amode<>fmCreate) then
   fs.Seek(fs.Size,0); {go to the end, append}

 ansiline := AnsiString(line)+AnsiChar(#13)+AnsiChar(#10);
 fs.WriteBuffer(PAnsiChar(ansiline)^,Length(ansiline));
finally
   fs.Free;
end;

Also, try this UTF8 version:

procedure AppendToLogUTF8(filename, line: UnicodeString);
var
    fs: TFileStream;
    preamble:TBytes;
    outpututf8: RawByteString;
    amode: Integer;
  begin
    if not FileExists(filename) then
      amode := fmCreate
    else
      amode := fmOpenReadWrite;
    fs := TFileStream.Create(filename, { mode } amode, fmShareDenyWrite);
    { sharing mode allows read during our writes }
    try

      {internal Char (UTF16) codepoint, to UTF8 encoding conversion:}
      outpututf8 := Utf8Encode(line); // this converts UnicodeString to WideString, sadly.

      if (amode = fmCreate) then
      begin
          preamble := TEncoding.UTF8.GetPreamble;
          fs.WriteBuffer( PAnsiChar(preamble)^, Length(preamble));
      end
      else
      begin
        fs.Seek(fs.Size, 0); { go to the end, append }
      end;

      outpututf8 := outpututf8 + AnsiChar(#13) + AnsiChar(#10);
      fs.WriteBuffer(PAnsiChar(outpututf8)^, Length(outpututf8));
    finally
      fs.Free;
    end;
end;
Warren P
Incidentally, I tend to shy away from using win32 file apis like CreateFile directly. Your stated goal in the comments can be easily achieved using a file stream instead of CreateFile.
Warren P
+2  A: 

For logging purposes why use Streams at all?

Why not use TextFiles? Here is a very simple example of one of my logging routines.

procedure LogToFile(Data:string);
var
  wLogFile: TextFile;
begin
  AssignFile(wLogFile, 'C:\MyTextFile.Log');
  {$I-}
  if FileExists('C:\MyTextFile.Log') then
    Append(wLogFile)
  else     
    ReWrite(wLogFile); 
  WriteLn(wLogfile, S);
  CloseFile(wLogFile);
  {$I+}
  IOResult; //Used to clear any possible remaining I/O errors 
end;

I actually have a fairly extensive logging unit that uses critical sections for thread safety, can optionally be used for internal logging via the OutputDebugString command as well as logging specified sections of code through the use of sectional identifiers.

If anyone is interested I'll gladly share the code unit here.

Ryan J. Mills
I have had problems with TextFile on Vista and windows 7. Something about permissions. For example, how do you set the share mode with a text file? I think there might be a global for that.
Warren P
@Warren P: FileMode is the variable that sets the ability to read/write/readwrite and if I understand it correctly can also be used for specifying sharing compatibility you set it before calling the Append/Reset/Rewrite file commands. With Vista and 7 you are limited to where you can write files. Windows will block you from writing under the Program Files folder for example and that is a permissions thing. It is not necessarily related to the use of TextFiles but is a function of the OS. Are you sure the problem is with TextFiles.
Ryan J. Mills
This is a good answer. I tried it, but I found it won't write Unicode as it is. I had to use what Dr. Bob said on Unicode Text File Output and write the BOM and then change S to UTFString(S). See: http://www.bobswart.nl/weblog/Blog.aspx?RootId=5:2975
lkessler
@lkessler: Thanks for the update. I haven't moved that unit to unicode yet. I'll take a look at Dr. Bob's website and update my unit accordingly.
Ryan J. Mills