2008. január 14., hétfő

How to port the output of EnumWindows to a TStringList


Problem/Question/Abstract:

How to port the output of EnumWindows to a TStringList

Answer:

Untested:

procedure TForm1.Button1Click(Sender: TObject);
begin
  StrList.clear;
  EnumWindows(@EnumWindowsProc, integer(StrList));
end;

function EnumWindowsProc(Wnd: HWND; lst: TStringList): BOOL; stdcall;
var
  capttxt: array[0..128] of Char;
begin
  Result := True;
  if IsWindowVisible(Wnd) { skip invisible windows } and
  ((GetWindowLong(Wnd, GWL_HWNDPARENT) = 0) { only top-level windows}
    or (HWND(GetWindowLong(Wnd, GWL_HWNDPARENT)) = GetDesktopWindow))
    and ((GetWindowLong(Wnd, GWL_EXSTYLE) and
    WS_EX_TOOLWINDOW) = 0) {skip Tool windows } then
  begin
    SendMessage(Wnd, WM_GETTEXT, Sizeof(capttxt), integer(@capttxt));
    List.Items.Add(capttxt);
  end;
end;

2008. január 13., vasárnap

Loading an exe in a memo field


Problem/Question/Abstract:

How can I read a binary file?
How can I show a binary file in a memo field?

Answer:

Solve 1:

Why?

This article has been written in answer to an old request by ismael u, asking how an executable can be loaded in a memo or rich memo field.

First a remark, executables should usually not be stored in a tmemo field, but rather in some blob field. However, there are some occasions on which one would like to view an executable. Studying (differences between) compiled  executables comes to mind.

I assume that Ismael means executable when he says exec, and the solution is rather simple.

How?

Loading a an executable in a memo field basically comes down to 2 steps. The first step is reading the file from disk and loading the file into memory, the second step is showing the loaded contents in the tmemo field.

The first step, reading the file from disk and loading it into memory, is rather easy. Perhaps TFileStream could be used, but I prefer the rather low level FileOpen function because of its performance. Also, when working with binary files, we must keep in mind that these files may contain #0 and many pointer based operations regard this as an end/of/string character.

Basically, here is the code, mostly a copy of the delphi5 help after fixing some minor bugs. Just create a form, add a button and a fileopendialog,
      
procedure TForm1.Button1Click(Sender: TObject);
var
  iFileHandle: Integer;
  iFileLength: Integer;
  iBytesRead: Integer;
  Buffer: PChar;
begin
  opendialog1.filter := 'executables|*.exe';
  if opendialog1.Execute then
  begin
    try
      iFileHandle := FileOpen(OpenDialog1.FileName, fmOpenRead or fmShareDenyNone);
      if iFileHandle > 0 then
      begin
        iFileLength := FileSeek(iFileHandle, 0, 2);
        FileSeek(iFileHandle, 0, 0);
        Buffer := PChar(AllocMem(iFileLength + 2));
        iBytesRead := FileRead(iFileHandle, Buffer^, iFileLength);
          // note that ^ is missing in D5 help.
        FileClose(iFileHandle);
      end;
    finally
      FreeMem(Buffer);
    end;
  end;
end;
    
The second step again poses us some questions. As the contents of the binary file will contain #0, how will we show them?

The first way is to convert the entire Buffer read above into a string and add this string to the memo. Doing this causes no technical problem, but the memo shows just a few characters. That's probably now what we want. The cause are the aforementioned #0 characters.

The second way is to go through the Buffer bit by bit, and switch to a new line whenever we encounter a #0. Doing so is easy, and reveals that an ordinary executable contains lots of #0 characters.

The third and probably best way is to show all characters in a hexagonal notation.
  
procedure TForm1.Button1Click(Sender: TObject);
var
  iFileHandle: Integer;
  iFileLength: Integer;
  iBytesRead: Integer;
  Buffer: PChar;
  i, linelength: integer;
  s: string;
  line: string;
  c: char;
  ordval, ordval1, ordval2: integer;
begin
  opendialog1.filter := 'executables|*.exe';
  if opendialog1.Execute then
  begin
    try
      iFileHandle := FileOpen(OpenDialog1.FileName, fmOpenRead or fmShareDenyNone);
      if iFileHandle > 0 then
      begin
        iFileLength := FileSeek(iFileHandle, 0, 2);
        FileSeek(iFileHandle, 0, 0);
        Buffer := PChar(AllocMem(iFileLength + 2));
        iBytesRead := FileRead(iFileHandle, Buffer^, iFileLength);
        // note that ^ is missing in D5 help.
        // 3 ways of conversion and show:
        // way 1: exe will contain \0 so this code shows only part of exe
        memo1.lines.add('way 1*********************************************');
        s := string(Buffer);
        memo1.lines.add(s);
        // way 2: use \0 as newline for purpose of displaying in memo1.
        memo1.lines.add('way 2*********************************************');
        LineLength := 0;
        Line := '';
        for i := 0 to iFileLength - 1 do
        begin
          if Buffer[i] = #0 then
          begin
            memo1.lines.add(Line);
            LineLength := 0;
            Line := '';
          end
          else
          begin
            inc(LineLength);
              // perhaps provision should be added for LineLength > max
                                                        delphi stringlength
            Line := Line + Buffer[i]; // memo1 will handle normal new line chars
          end;
        end;
        // way 3: display every char as ord
        memo1.lines.add('way 3*********************************************');
        Line := '';
        for i := 0 to iFileLength - 1 do
        begin
          c := Buffer[i];
          ordval := ord(c);
          ordval1 := ordval div 16;
          ordval2 := ordval mod 16;
          Line := Line + '0123456789ABCDEF'[ordval1 + 1] + '0123456789ABCDEF'[ordval2             + 1];
          if Length(Line) = 80 then
          begin
            memo1.lines.add(line);
            line := '';
          end;
        end;
        FileClose(iFileHandle);
      end;
    finally
      FreeMem(Buffer);
    end;
  end;
end;


Solve 2:

There is an inbuild delphi function (which I think appears in pre delphi 5)

BinToHex(Buffer, Text: PChar; BufSize: Integer);

which would create an output buffer in hex format.

2008. január 12., szombat

Keep a dataset in dsInsert/dsEdit mode after validation fails


Problem/Question/Abstract:

Keep a dataset in dsInsert/dsEdit mode after validation fails

Answer:

If you want to keep a dataset in dsInsert/dsEdit mode after a validation fails, but do not want to loose your input, use Abort in the BeforePost() event.

(If you would use Dataset.Cancel, you'd loose the input and return to browse mode.)

procedure TForm1.Table1BeforePost(DataSet: TDataSet);
begin
  if Table1ID.Value <= 0 then
  begin
    // the data is invalid!!
    Showmessage('Error! Invalid value!');
    Abort
  end
  else
    Table1.Post;
end;

2008. január 11., péntek

Create a flat TDBGrid


Problem/Question/Abstract:

How to create a flat TDBGrid

Answer:

This is an approach for creating a flat TDBGrid. It has some problems with its scrollbar tuning, but you can take it at least as an example.

{ ... }
type
  TMyGBGrid = class(TDBGrid)
  protected
    FFlat: boolean;
    procedure CreateWnd; override;
    procedure DrawCell(ACol, ARow: Longint; ARect: TRect; AState: TGridDrawState);
      override;
    procedure InitScrollBars;
    procedure SetFlat(AValue: boolean);
  public
    constructor Create(AOwner: TComponent); override;
  published
    property Flat: boolean read FFlat write SetFlat;
  end;

  { .... }

constructor TMyGBGrid.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FFlat := true;
end;

procedure TMyGBGrid.CreateWnd;
begin
  inherited CreateWnd;
  InitScrollBars;
end;

procedure TMyGBGrid.DrawCell(ACol, ARow: Longint; ARect: TRect; AState:
  TGridDrawState);

  procedure xXDrawBorder(var ABorderRect: TRect);
  begin
    InflateRect(ABorderRect, 1, 1);
    Frame3D(Canvas, ABorderRect, clBtnShadow, clBtnShadow, 1);
    Frame3D(Canvas, ABorderRect, clBtnHighlight, clBtnFace, 1);
  end;

begin
  inherited DrawCell(ACol, ARow, ARect, AState);
  if Flat and ((ACol = 0) or (ARow = 0)) then
  begin
    if (ARow = 0) and (dgTitles in Options) then
      xXDrawBorder(ARect)
    else if (ACol = 0) and (dgIndicator in Options) then
      xXDrawBorder(ARect);
  end
end;

procedure TMyGBGrid.InitScrollBars;
var
  XVerScrollInfo, XHorScrollInfo: TScrollInfo;
begin
  if FFlat then
  begin
    GetScrollInfo(Handle, SB_VERT, XVerScrollInfo);
    GetScrollInfo(Handle, SB_HORZ, XHorScrollInfo);
    InitializeFlatSB(Handle);
    FlatSB_SetScrollInfo(Handle, SB_VERT, XVerScrollInfo, true);
    FlatSB_SetScrollInfo(Handle, SB_HORZ, XHorScrollInfo, true);
    FlatSB_SetScrollProp(Handle, WSB_PROP_VSTYLE, FSB_ENCARTA_MODE, true);
    FlatSB_SetScrollProp(Handle, WSB_PROP_HSTYLE, FSB_ENCARTA_MODE, true);
    FlatSB_SetScrollProp(Handle, WSB_PROP_VBKGCOLOR, clGreen, true);
    FlatSB_SetScrollProp(Handle, WSB_PROP_HBKGCOLOR, clBlue, true);
  end;
end;

procedure TMyGBGrid.SetFlat(AValue: boolean);
begin
  if AValue <> FFlat then
  begin
    FFlat := AValue;
    RecreateWnd;
  end;
end;

2008. január 10., csütörtök

How to delete multiple files from a directory


Problem/Question/Abstract:

How to delete multiple files from a directory

Answer:

procedure DeleteFiles(sMask, sPath: string);
var
  SearchRec: TSearchRec;
  Found: Integer;
begin
  sPath := IncludeTrailingPathDelimiter(sPath);
  Found := SysUtils.FindFirst(sPath + sMask, faAnyFile, SearchRec);
  try
    while (Found = 0) do
    begin
      if not (SearchRec.Attr and faDirectory > 0) then
        SysUtils.DeleteFile(sPath + SearchRec.Name);
      Found := SysUtils.FindNext(SearchRec);
    end;
  finally
    SysUtils.FindClose(SearchRec);
  end;
end;

Usage:

DeleteFiles('*.txt', 'c:\myfiles');

2008. január 9., szerda

Easiest Way Drawing Transparent Image


Problem/Question/Abstract:

How to draw an image transparently ?

Answer:

Here is anohter way to draw a transparent image. Only using Delphi properties and method (Image).

Here is the example code:

// make draw proc to draw transparently

procedure MyTransparentDraw(src, dest: TBitmap; x, y: integer; warna: TColor);
begin
  src.Transparent := true;
  src.TransparentMode := tmFixed;
  src.TransparentColor := warna;
  dest.Canvas.Draw(x, y, src);
end;

procedure TForm1.Button1Click(Sender: TObject);
const
  TRANS_COLOR = clYellow; // change with transparent color you want
var
  bmp, bmp2: TBitmap;
begin
  if (OpenPictureDialog1.Execute) then
  begin
    Image1.Picture.LoadFromFile(OpenPictureDialog1.FileName);
    bmp := TBitmap.Create;
    try
      bmp.Width := Image1.Width;
      bmp.Height := Image1.Height;
      bmp.Assign(Image1.Picture.Bitmap);
      bmp2 := TBitmap.Create;
      try
        bmp2.Width := bmp.Width;
        bmp2.Height := bmp.Height;
        MyTransparentDraw(bmp, bmp2, 0, 0, TRANS_COLOR);
        Image1.Canvas.Draw(0, 0, bmp2);
      finally
        bmp2.Free;
      end;
    finally
      bmp.Free;
    end;
  end;
end;

To try above code, just copy and paste those code, then click on button to choose an image to be drawn transparently. You can change the value of "TRANS_COLOR" above with any other color that you want to be the transparent color of you image.

2008. január 8., kedd

Conserving Windows resources when using TPageControl


Problem/Question/Abstract:

Complex forms can use up a lot of the Windows resources. This article describes a method for reducing the drain on the Windows resources.

Answer:

In my application there is a form with lots of components on it; 1047 components to be exact.  In my development environment, WindowsNT, this did not cause any problems but under Windows 98, my application would crash if a user tried to create a second instance of this form.  The problem was that in Windows 98, there are a limited number of Windows resources and one instance of my form used 42% of the Windows resources.  With the operating system already using 20% of the resources, there weren't enough resources left to create a second instance of the form.

In researching this problem I came across a thread in alt.comp.lang.borland-delphi by Ken Phipps, Bruce Roberts, and M.H. Avegaart that gave most of a solution to the problem.  The thread extended from Jan. 13, 2001 to Jan. 16, 2001.  There is also an article on the Borland web site that gives some related information (TI1375D.txt; http://community.borland.com/article/0,1410,16375,00.html).

The following is the solution I implemented in my program.  It solved my problem.

First, I created the ConserveResourcesUnit shown below.  I called FreeFormResources in the OnCreate event handler of each form that had a TPageControl.  I called FreePageControlResources in the OnChange event handler of each TPageControl.  I could have gone further and made a component that did the same thing after the page control was created and when the ActivePage was changed but it didn't seem worth the effort.  I could also have called FreePageControlResources when the ActivePage of a TPageControl was changed programatically but I haven't gotten around to that yet.  Another option would have been to redesign the form to use fewer TWinControls but that would have been a lot of effort and would have created other problems.

There is one tricky bit.  If you have a TPageControl on a tab of another TPageControl and you want to set the TabVisible property of one of the tabs on the former TPageControl you may need to call HandleNeeded for all the tabs on the former TPageControl before setting the TabVisible property.  You can call FreePageControlResources afterwards to free-up resources.  If you don't do this, an error can occur in the VCL when you try to set the TabVisible property.

If you use a TTabbedNotebook rather than a TPageControl, see the article on the Borland web site cited above.

unit ConserveResourcesUnit;

interface

uses Windows, Classes, Controls, Forms, comctrls;

procedure FreePageControlResources(const APageControl: TPageControl;
  const FormHandle: HWND);

procedure FreeFormResources(const AForm: TForm);

implementation

type
  TMyWinControl = class(TWinControl);

procedure FreePageControlResources(const APageControl: TPageControl;
  const FormHandle: HWND);
var
  Index: integer;
begin
  //  LockWindowUpdate prevents any drawing in a given window}
  LockWindowUpdate(FormHandle);
  with APageControl do
  begin
    for Index := 0 to PageCount - 1 do
    begin
      // DestroyHandle is protected so a typecast is required
      // to expose it.
      // Usually, the handles will be automatically recreated when needed.
      // However, in setting the TabVisible property, they may not be recreated
      // without a direct call to HandleNeeded.
      if Pages[Index] <> ActivePage then
        TMyWinControl(Pages[Index]).DestroyHandle;
    end;
  end;
  {Release the Lock on the Form so any Form drawing can work}
  LockWindowUpdate(0);
end;

procedure FreeFormResources(const AForm: TForm);
var
  AComponent: TComponent;
  Index: integer;
begin
  for Index := 0 to AForm.ComponentCount - 1 do
  begin
    AComponent := AForm.Components[Index];
    if AComponent is TPageControl then
    begin
      FreePageControlResources(TPageControl(AComponent), AForm.Handle);
    end;
  end;
end;

end.

2008. január 7., hétfő

How to save and restore font selections to a text file


Problem/Question/Abstract:

I need to save and restore Font selections to a text file. I was able to convert all the font attributes except for style to and from strings using one line expressions.

Answer:

Solve 1:

Here's one way of doing it:

function StyleToStr(Style: TFontStyles): string;
begin
  SetLength(Result, 4);
  {T = true, S = false 83 is ordinal value of S, if true then S + 1 (84) = T}
  Result[1] := Char(Integer(fsBold in Style) + 83);
  Result[2] := Char(Integer(fsItalic in Style) + 83);
  Result[3] := Char(Integer(fsUnderline in Style) + 83);
  Result[4] := Char(Integer(fsStrikeOut in Style) + 83);
  {replace all S to F's if you like}
  Result := StringReplace(Result, 'S', 'F', [rfReplaceAll]);
end;

function StrToStyle(Str: string): TFontStyles;
begin
  Result := [];
  {T = true, S = false}
  if Str[1] = 'T' then
    Include(Result, fsBold);
  if Str[2] = 'T' then
    Include(Result, fsItalic);
  if Str[3] = 'T' then
    Include(Result, fsUnderLine);
  if Str[4] = 'T' then
    Include(Result, fsStrikeOut);
end;


Solve 2:

I'd suggest this:

function StyleToStr(Style: TFontStyles): string;
const
  Chars: array[Boolean] of Char = ('F', 'T');
begin
  SetLength(Result, 4);
  Result[1] := Chars[fsBold in Style];
  Result[2] := Chars[fsItalic in Style];
  Result[3] := Chars[fsUnderline in Style];
  Result[4] := Chars[fsStrikeOut in Style];
end;


Solve 3:

A more algorithmic approach:

function FontStylesToStr(Style: TFontStyles): string;
var
  I: TFontStyle;
begin
  SetLength(Result, High(TFontStyle) + 1);
  for I := Low(TFontStyle) to High(TFontStyle) do
    if I in Style then
      Result[Ord(I) + 1] := 'F'
    else
      Result[Ord(I) + 1] := 'T';
end;

function StrToFontStyles(Str: string): TFontStyles;
var
  I: TFontStyle;
begin
  Result := [];
  for I := Low(TFontStyle) to High(TFontStyle) do
    if Str[Ord(I) + 1] = 'T' then
      Include(Result, I);
end;


Solve 4:

May I propose that you save the font style using a numeric representation of the bit pattern. One special consideration during the conversion would be the size of the enumeration. That is, make sure you use an integer type that has the same boundary as the set type. For example, there are four possible font styles in TFontStyles, it would be stored as a byte.

function FontStylesToInt(Styles: TFontStyles): Integer;
begin
  Result := byte(Styles)
end;

function IntToFontStyles(Value: integer): TFontStyles;
begin
  Result := TFontStyles(byte(Value))
end;

If you are a purist, replace 'integer's with 'byte's

2008. január 6., vasárnap

How to use ScanLine


Problem/Question/Abstract:

Can someone explain to me how to use the ScanLine function? I tried the example in the Help, only got a black box.

Answer:

type
  PRGBs = ^TRGBs;
  TRGBs = array[0..1000000] of TRGBTriple;

var
  r, g, b: Byte2DArray; {customer implemented}

procedure VirtualImageRGB.fromimage(t: TImage);
var
  x, y: Integer;
  P: PRGBs;
begin
  for y := 0 to t.picture.Bitmap.Height - 1 do
  begin
    P := PRGBs(t.picture.Bitmap.Scanline[y]);
    for x := 0 to t.picture.Bitmap.Width - 1 do
      with P^[x] do
      begin
        r[x, y] := rgbtRed;
        g[x, y] := rgbtGreen;
        b[x, y] := rgbtBlue;
      end;
  end;
end;

procedure VirtualImageRGB.toimage(t: TImage);
var
  x, y: Integer;
  P: PRGBs;
begin
  for y := 0 to t.picture.Bitmap.Height - 1 do
  begin
    P := PRGBs(t.picture.Bitmap.Scanline[y]);
    for x := 0 to t.picture.Bitmap.Width - 1 do
      with P^[x] do
      begin
        rgbtRed := r[x, y];
        rgbtGreen := g[x, y];
        rgbtBlue := b[x, y];
      end;
  end;
  t.canvas.draw(0, 0, t.picture.Bitmap);
end;

2008. január 5., szombat

Display different splash screens anytime during program execution


Problem/Question/Abstract:

How to display different splash screens anytime during program execution

Answer:

Solve 1:

I wanted to be able to display a splash screen anytime during the program run with different durations and different images each time. The problem with using the Release method from within a form is that it doesn't set the variable referencing the splash form to NIL. When the splash form is released, I need to reset the reference so that I can test it when displaying the form to make sure it's NIL (that there isn't one already up). I could easily have made the reference public and set it to NIL after calling Release from within the timer event, but that's not very reusable.

What I ended up doing is creating a class (TFormWithSplash) derived from TForm that has all mechanisms necessary to handle the splash screen itself. Then, any form I want to be able to display splashscreens, I simply derive from this class instead of TForm. The unit that defines TFormWithSplash has a simple form within it that contains an image and timer control. The ShowSplash method of TFormWithSplash creates an instance of this form and displays it. This form then loads the image file and starts the timer. When the timer elapses, the form closes itself and sends a user defined message its parent form which frees the reference and resets it to nil. Everything but the PicFileName, SplashDuration, StayOnTop properties and the ShowSplash function calls are invisible to the programmer. Here is the code; it still needs a few features, but seems to work well.

unit SplashFrm;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls;

const
  WM_SPLASHCLOSED = WM_USER + 113;

type
  TFormWithSplash = class;
  TSplashForm = class(TForm)
    Image1: TImage;
    Timer1: TTimer;
    procedure FormShow(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure Timer1Timer(Sender: TObject);
    procedure FormDeactivate(Sender: TObject);
  private
    CloseOnDeactivate: Boolean;
    procedure LoadAndDisplay(PicFile: string);
  public
    constructor Create(Owner: TFormWithSplash; DisplayFor: Cardinal;
      CloseOnLostFocus: Boolean; TopMost: Boolean);
  end;

  TFormWithSplash = class(TForm)
    PicFile: string;
    SplashForm: TSplashForm;
    procedure SetPicFile(FileName: string);
    procedure OnSplashClosed(var msg: TMessage); message WM_SPLASHCLOSED;
  public
    SplashDuration: Cardinal;
    StayOnTop, CloseOnLostFocus: Boolean;
    constructor Create(Owner: TComponent); override;
    procedure ShowSplash;
    property SplashPicFile: string read PicFile write SetPicFile;
  end;

implementation

{$R *.DFM}

constructor TFormWithSplash.Create(Owner: TComponent);
begin
  SplashForm := nil;
  PicFile := '';
  StayOnTop := True;
  CloseOnLostFocus := False;
  inherited Create(Owner);
end;

procedure TFormWithSplash.OnSplashClosed(var msg: TMessage);
begin
  SplashForm.Free;
  SplashForm := nil;
end;

procedure TFormWithSplash.SetPicFile(FileName: string);
begin
  if not FileExists(FileName) then
    raise EInOutError.Create('Couldn''t load image file: ' + FileName)
  else
    PicFile := FileName;
end;

procedure TFormWithSplash.ShowSplash;
begin
  if PicFile = '' then
    Exit;
  while Assigned(SplashForm) do
    Application.ProcessMessages;
  SplashForm := TSplashForm.Create(self, SplashDuration, CloseOnLostFocus, StayOnTop);
  SplashForm.LoadAndDisplay(PicFile);
end;

constructor TSplashForm.Create(Owner: TFormWithSplash; DisplayFor: Cardinal;
  CloseOnLostFocus: Boolean; TopMost: Boolean);
begin
  inherited Create(Owner);
  CloseOnDeactivate := CloseOnLostFocus;
  if TopMost then
    FormStyle := fsStayOnTop;
  Image1.AutoSize := True;
  Timer1.Interval := DisplayFor * 1000;
end;

procedure TSplashForm.LoadAndDisplay(PicFile: string);
begin
  Image1.Picture.LoadFromFile(PicFile);
  ClientHeight := Image1.Picture.Height + 1;
  ClientWidth := Image1.Picture.Width + 1;
  Left := Screen.Width div 2 - Width div 2;
  Top := Screen.Height div 2 - Height div 2;
  Show;
end;

procedure TSplashForm.FormShow(Sender: TObject);
begin
  Application.RestoreTopmosts;
  Timer1.Enabled := True;
end;

procedure TSplashForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  PostMessage(TFormWithSplash(Owner).Handle, WM_SPLASHCLOSED, 0, 0);
end;

procedure TSplashForm.Timer1Timer(Sender: TObject);
begin
  Close;
end;

procedure TSplashForm.FormDeactivate(Sender: TObject);
begin
  if CloseOnDeactivate then
  begin
    Timer1.Enabled := False;
    Close;
  end;
end;

end.


Solve 2:

This information is found in the View Menu and then view Project Source.

begin
  SplashScreen := TSplashScreen.Create(Application); //These 3 lines
  SplashScreen.Show; //Added Manually
  SplashScreen.Update; //to load SplashScreen
  Application.Initialize;
  Application.Title := 'Application Title';
  Application.CreateForm(TForm1, Form1);
  SplashScreen.Hide; //These 2 added
  SplashScreen.Free; //manually to close Splash
  Application.Run;
end.

In Project Options, set SplashScreen Form to be an available form, not Autocreate.

2008. január 4., péntek

Building an Easy-to-Use Parser/Parsing Framework (Part I)


Problem/Question/Abstract:

How to create a simple parsing framework to parse any kind of data?

Answer:

Note:

The full sourcecodes for all components & examples descripted in the following article are available as an open-source project under SourceForge:

  Parser Framework

A second article was released on 29.01.2002 with a more detailed example:

Building an Easy-to-Use Parser/Parsing Framework (Part II)

Today, we wonna speak about "how to create a simple parser framework" in Delphi. Our goal will be a class solutions which helps up to parse any kind of data and store all relevant informations in an easy-to- access object model.

At the end of this article, we've developed a small utility, which generates a simple object model of a .dtd file and output it's xml pendant from a given starting node. In other words, we're using the parsing framework to create a parser, which is able to parse a .dtd file, extract all neccessary tags, store them in the object model and generates the xml output. Note: This utility uses a simply dtd- parser model, which don't include all routines to parse all kinds of dtd datas - it's up to you to include those features.

Our claims to the framework and object model are:

easy to use.
save/loadable object trees.
integrated error reporting.
expandable.

Okay, now let's start to develope the main parsing engine. Delphi comes with a unit called CopyPrsr which includes  the simple stream parser object TCopyParser. Try to take a look into that file to understand how it works - it's located under $(DELPHI)\Source\Internet\CopyPrsr.pas. Our framework parser is derived from that idea, but uses a simple string instead of the stream and includes some additional functions:

The boiler plate:

unit StringParser;

interface

uses
  Classes;

const
  { Additional Parser special tokens }

  toEOL = char(6);
  toBOF = char(7);

type
  { TSysCharSet }

  TSysCharSet = set of Char;

  { TStringParser }

  TStringParser = class
  private
    { Private declarations }
    FParseString: string;
    FLineTokens: Integer;
    FSourceLine: Integer;
    FSourcePos: Integer;
    FTokenPos: Integer;
    FToken: Char;
    procedure SkipBlanks;
    function GetParseString: string;
    function GetSourcePos: Integer;
    function GetTokenString: string;
  protected
    { Protected declarations }
  public
    { Public declarations }
    constructor Create;
    function LoadFromFile(const FileName: string): Boolean;
    function LoadFromStream(const Stream: TStream): Boolean;
    function SkipToEOF: string;
    function SkipToEOL: string;
    function SkipToken: Char;
    function SkipTokenString: string;
    function SkipToToken(const AToken: Char): string; overload;
    function SkipToToken(const AToken: TSysCharSet): string; overload;
    function SkipToTokenString(const ATokenString: string): string;
    property ParseString: string read GetParseString;
    property SourceLine: Integer read FSourceLine;
    property SourcePos: Integer read GetSourcePos;
    property Token: Char read FToken;
    property TokenString: string read GetTokenString;
  end;

As you can see, there are many public helper functions which you can use to parse the data. The main functions are LoadFromFile and LoadFromStream, which needs the name of the file to be parsed as the only parameter. Both functions loads the content of the file and store it to the string FParseString which can be accessed through the denominator property:

LoadFromFile/LoadFromStream:

function TStringParser.LoadFromFile(const FileName: string): Boolean;
var
  Stream: TMemoryStream;
begin
  Result := False;
  if not FileExists(FileName) then
    Exit;
  Stream := TMemoryStream.Create;
  try
    Stream.LoadFromFile(FileName);
    Result := LoadFromStream(Stream);
  finally
    Stream.Free;
  end;
end;

function TStringParser.LoadFromStream(const Stream: TStream): Boolean;
var
  MemStream: TMemoryStream;
begin
  Result := False;
  if not (assigned(Stream)) then
    Exit;
  MemStream := TMemoryStream.Create;
  try
    Stream.Seek(0, soFromBeginning);
    MemStream.CopyFrom(Stream, Stream.Size);
    FParseString := StrPas(MemStream.Memory);
    SetLength(FParseString, MemStream.Size);
    FParseString := Concat(FParseString, toEOF);
    FToken := toBOF;
    Result := True;
  finally
    MemStream.Free;
  end;
end;

The main functionality of the parsing engine is the extraction of so- called tokens. A token can be a seperator (like CR, LF or EOF) or a symbol, which can be a keyword if you plan to parse a programing language. The following functions are used to skip blank characters (which are used to seperate symbols and aren't relevant) and to extract/skip the next token symbol:

Token related functions (pullout only):

procedure TStringParser.SkipBlanks;
begin
  while True do
  begin
    FToken := FParseString[FTokenPos];
    case FToken of
      #10:
        begin
          Inc(FSourceLine);
          FLineTokens := FTokenPos;
        end;
      toEOF, #33..#255:
        Exit;
    end;
    Inc(FTokenPos);
  end;
end;

function TStringParser.SkipToken: Char;
const
  KeySet = ['A'..'Z', 'a'..'z', '0'..'9', '_'];
begin
  SkipBlanks;
  FSourcePos := FTokenPos;
  if FParseString[FTokenPos] = toEOF then
    FToken := toEOF
  else if FParseString[FTokenPos] in KeySet then
  begin
    while FParseString[FTokenPos] in KeySet do
      Inc(FTokenPos);
    FToken := toSymbol;
  end
  else
  begin
    FToken := FParseString[FTokenPos];
    Inc(FTokenPos);
  end;
  Result := FToken;
end;

function TStringParser.SkipToToken(const AToken: TSysCharSet): string;
begin
  FSourcePos := FTokenPos;
  while not ((FParseString[FTokenPos] = toEOF) or (FParseString[FTokenPos] in AToken))
    do
  begin
    if FParseString[FTokenPos] = #10 then
    begin
      Inc(FSourceLine);
      FLineTokens := FTokenPos;
    end;
    Inc(FTokenPos);
  end;
  if FParseString[FTokenPos] = toEOF then
    FToken := toEOF
  else
    FToken := FParseString[FTokenPos];
  Result := GetTokenString;
  if FToken <> toEOF then
    SkipToken;
end;

The absent functions includes alternativ possibilities to extract or skip the tokens, like SkipToTokenString or SkipToEof. Well, the next step is to create the object model, which holds all our parsed informations. As I mentioned earlier, the goal of this article it to create a simple dtd parser, so we'll create an object model to store dtd
informations.

A dtd file is used to descripe the syntax rules of a xml file, like:

DTD example:

key CDATA #REQUIRED
value CDATA #REQUIRED
>

This example demonstrated the simplest way of a dtd structure. It's not the purpose of this article to develope a highly flexible dtd parser which spots all dtd grammas, so we only include the weightly ones. Root of each object model is the document, which holds all other objects as collections:

The Root Document:

{ TDTDDocument }

TDTDDocument = class(TPersistent)
private
  { Private declarations }
  FEntities: TDTDEntities;
  FElements: TDTDElements;
  procedure SetEntities(Value: TDTDEntities);
  procedure SetElements(Value: TDTDElements);
public
  { Public declarations }
  constructor Create;
  destructor Destroy; override;
  procedure Assign(Source: TPersistent); override;
published
  { Published declarations }
  property Entities: TDTDEntities read FEntities write SetEntities;
  property Elements: TDTDElements read FElements write SetElements;
end;

As you can see, our document gives us the access of some other types of data: Entities and Elements. Entities are very hard to parse, so it's a good lesson for you to include that feature. Parsing elements is quite easier, so this type of data is better to explain here. Look at the dtd example some rows above this. You can see, that a dtd element is descripted as followed:

Our object model needs some extra fields to store such element informations. If you are not familiar with dtd or xml, look at W3CSchools - it's a good starting point to learn more about that topic. So, take a look at the following  object structure:

TDTDDocument
  |
  o--TDTDEntities
  |
  o--TDTElements
    |
    o--TDTDElementTyp
    |
    o--TDTDAttributes
      |
      o--TDTDAttributeTyp
      o--TDTDAttributeStatus
      o--Default: string
      o--TDTDEnums
    o--TDTDChild
      |
      o--TDTDTyp
      o--TDTDChilds

I've tried to "pack" the dtd grammars into an easy object model as you can see above:

Each document contains a collection of elements. Each element is descripted by an elementtype and containes in turn a collection of attributes and childs. Each attribute again is descripted by an attributetype and contains a collection of enum(erations) and so forth. Followed a code cantle from the element implementation, it's not suggestive to show you the whole code here - it's quit long and a little bit more complex:

TDTDElement:

unit DTD_Document;

interface

uses
  Classes;

type

  ...

  { TDTDElementTyp }

  TDTDElementTyp =
    (etAny, etEmpty, etData, etContainer);

  { TDTDElementStatus }

  TDTDElementStatus =
    (esRequired, esRequiredSeq, esOptional, esOptionalSeq);

  { TDTDItem }

  TDTDItem = class(TCollectionItem)
  private
    { Private declarations }
    FName: string;
  public
    { Public declarations }
    procedure Assign(Source: TPersistent); override;
  published
    { Published declarations }
    property Name: string read FName write FName;
  end;

  { TDTDItems }

  TDTDItems = class(TCollection)
  private
    { Private declarations }
    function GetItem(Index: Integer): TDTDItem;
    procedure SetItem(Index: Integer; Value: TDTDItem);
  public
    { Public declarations }
    function Add: TDTDItem;
    function Find(const Name: string): TDTDItem;
    property Items[Index: Integer]: TDTDItem read GetItem write SetItem; default;
  end;

  ...

  { TDTDElement }

  TDTDElement = class(TDTDProperty)
  private
    { Private declarations }
    FTyp: TDTDElementTyp;
    FAttributes: TDTDAttributes;
    FChilds: TDTDChilds;
    procedure SetAttributes(Value: TDTDAttributes);
    procedure SetChilds(Value: TDTDChilds);
  public
    { Public declarations }
    constructor Create(Collection: TCollection); override;
    destructor Destroy; override;
    procedure Assign(Source: TPersistent); override;
  published
    { Published declarations }
    property Typ: TDTDElementTyp read FTyp write FTyp;
    property Attributes: TDTDAttributes read FAttributes write SetAttributes;
    property Childs: TDTDChilds read FChilds write SetChilds;
  end;

  { TDTDElements }

  TDTDElements = class(TDTDProperties)
  private
    { Private declarations }
    function GetItem(Index: Integer): TDTDElement;
    procedure SetItem(Index: Integer; Value: TDTDElement);
  public
    { Public declarations }
    function Add: TDTDElement;
    function Find(const Name: string): TDTDElement;
    property Items[Index: Integer]: TDTDElement read GetItem write SetItem; default;
  end;

  ...

implementation

...

{ TDTDItem }

procedure TDTDItem.Assign(Source: TPersistent);
begin
  if Source is TDTDItem then
  begin
    Name := TDTDItem(Source).Name;
    Exit;
  end;
  inherited Assign(Source);
end;

{ TDTDItems }

function TDTDItems.Add: TDTDItem;
begin
  Result := TDTDItem(inherited Add);
end;

function TDTDItems.Find(const Name: string): TDTDItem;
var
  i: Integer;
begin
  Result := nil;
  for i := 0 to Count - 1 do
    if CompareStr(Items[i].Name, Name) = 0 then
    begin
      Result := Items[i];
      Break;
    end;
end;

function TDTDItems.GetItem(Index: Integer): TDTDItem;
begin
  Result := TDTDItem(inherited GetItem(Index));
end;

procedure TDTDItems.SetItem(Index: Integer; Value: TDTDItem);
begin
  inherited SetItem(Index, Value);
end;

...

{ TDTDElement }

constructor TDTDElement.Create(Collection: TCollection);
begin
  inherited Create(Collection);
  FAttributes := TDTDAttributes.Create(TDTDAttribute);
  FChilds := TDTDChilds.Create(TDTDChild);
end;

destructor TDTDElement.Destroy;
begin
  FAttributes.Free;
  FChilds.Free;
  inherited Destroy;
end;

procedure TDTDElement.Assign(Source: TPersistent);
begin
  if Source is TDTDElement then
  begin
    Typ := TDTDElement(Source).Typ;
    Attributes.Assign(TDTDElement(Source).Attributes);
    Childs.Assign(TDTDElement(Source).Childs);
  end;
  inherited Assign(Source);
end;

procedure TDTDElement.SetAttributes(Value: TDTDAttributes);
begin
  FAttributes.Assign(Value);
end;

procedure TDTDElement.SetChilds(Value: TDTDChilds);
begin
  FChilds.Assign(Value);
end;

{ TDTDElements }

function TDTDElements.Add: TDTDElement;
begin
  Result := TDTDElement(inherited Add);
end;

function TDTDElements.Find(const Name: string): TDTDElement;
begin
  Result := TDTDElement(inherited Find(Name));
end;

function TDTDElements.GetItem(Index: Integer): TDTDElement;
begin
  Result := TDTDElement(inherited GetItem(Index));
end;

procedure TDTDElements.SetItem(Index: Integer; Value: TDTDElement);
begin
  inherited SetItem(Index, Value);
end;

The advantage of this object model is, that you're able to easily add the document to a standard component and use Delphi's internal streaming technology to load and save the object contents of a parsed file.

The next step will be the developing of the real dtd parser. Do you remember the TStringParser descripted at the top of this article? We'll using this class to build up our parser. But, we don't want to create a parser from scratch each time we're about to parse a new kind of data - it's not mind of a framework. So first, we'll develope a small parser class from which we will inherit our dtd parser. This parent class should include the error reporting and accessable fields to some other informations:

The Private Parser class:

unit PrivateParser;

interface

uses
  Classes, SysUtils, StringParser;

type
  { TParserError }

  TParserError = class(TCollectionItem)
  private
    { Private declarations }
    FFileName: string;
    FLine: Integer;
    FMessage: string;
    FPosition: Integer;
  public
    { Public declarations }
    procedure Assign(Source: TPersistent); override;
  published
    { Published declarations }
    property FileName: string read FFileName write FFileName;
    property Line: Integer read FLine write FLine;
    property Message: string read FMessage write FMessage;
    property Position: Integer read FPosition write FPosition;
  end;

  { TParserErrors }

  TParserErrors = class(TCollection)
  private
    { Private declarations }
    function GetItem(Index: Integer): TParserError;
    procedure SetItem(Index: Integer; Value: TParserError);
  public
    { Public declarations }
    function Add: TParserError;
    property Items[Index: Integer]: TParserError read GetItem write SetItem; default;
  end;

  { TValidationParser }

  TValidationParser = class
  private
    { Private declarations }
    FErrors: TParserErrors;
    procedure SetErrors(const Value: TParserErrors);
  public
    { Public declarations }
    constructor Create;
    destructor Destroy; override;
    procedure AddError(const AMessage: string; Parser: TStringParser; const AFileName:
      string = '');
    procedure AddErrorFmt(const AMessage: string; Params: array of const; Parser:
      TStringParser; const AFileName: string = '');
    property Errors: TParserErrors read FErrors write SetErrors;
  end;

implementation

{ TParserError }

procedure TParserError.Assign(Source: TPersistent);
begin
  if Source is TParserError then
  begin
    Line := TParserError(Source).Line;
    Message := TParserError(Source).Message;
    Position := TParserError(Source).Position;

    Exit;
  end;

  inherited Assign(Source);
end;

{ TParserErrors }

function TParserErrors.Add: TParserError;
begin
  Result := TParserError(inherited Add);
end;

function TParserErrors.GetItem(Index: Integer): TParserError;
begin
  Result := TParserError(inherited GetItem(Index));
end;

procedure TParserErrors.SetItem(Index: Integer; Value: TParserError);
begin
  inherited SetItem(Index, Value);
end;

{ TValidationParser }

constructor TValidationParser.Create;
begin
  inherited Create;
  FErrors := TParserErrors.Create(TParserError);
end;

destructor TValidationParser.Destroy;
begin
  FErrors.Free;
  inherited Destroy;
end;

procedure TValidationParser.SetErrors(const Value: TParserErrors);
begin
  FErrors.Assign(Value);
end;

procedure TValidationParser.AddErrorFmt(const AMessage: string; Params: array of
  const; Parser: TStringParser; const AFileName: string = '');
begin
  with FErrors.Add do
  begin
    FileName := AFileName;
    Line := Parser.SourceLine;
    Message := Format(AMessage, Params);
    Position := Parser.SourcePos;
  end;
end;

procedure TValidationParser.AddError(const AMessage: string; Parser: TStringParser;
  const AFileName: string = '');
begin
  AddErrorFmt(AMessage, [], Parser, AFileName);
end;

end.

Now we can start developing the real parser by inheriting it from the TValidationParser. Again, I don't want to show you the whole sourcecode here, so I pick up only the sapid one. Our dtd parser is a so- called two-way parser, i.e. it uses the first pass to parse the elements and the second pass to parse the attributes. This is useful, because an attibute can refer to an element which is placed below it and otherwise we'll get some unneeded errors. The main method of our parse is Parse, which  needs the name of the file to be parsed as the first parameter, and a pre-initialized TDTDDocument as the second parameter. A sample call should looks like:

Sample Call:

// Create DTDDocument.
DTDDocument := TDTDDocument.Create;
try
  // Create DTDParser.
  DTDParser := TDTDParser.Create;
  try
    // Parse File.
    DTDParser.Parse(FileName, DTDDocument);

    // Display possible Errors.
    if DTDParser.Errors.Count > 0 then
    begin
      for i := 0 to DTDParser.Errors.Count - 1 do
        with DTDParser.Errors[i] do
          WriteLn(Format('Error in Line %d, Pos %d: %s...', [Line, Position,
            Message]));
      Exit;
    end;

    ...

    // Free DTDParser.
  finally
    DTDParser.Free;
  end;

  // Free DTDDocument.
finally
  DTDDocument.Free;
end;

But now, let's take a look at some sourcecode lines of the parser implementation. The first think we had to do is to inherited our parser from the parent class:

Parser Implementation (Snippet):

type
  { EDTDParser }

  EDTDParser = class(Exception);

  { TDTDParser }

  TDTDParser = class(TValidationParser)
  private
    { Private declarations }
    procedure ParseElement(Parser: TStringParser; Document: TDTDDocument; const Pass:
      Integer);
    procedure ParseAttlist(Parser: TStringParser; Document: TDTDDocument);
    procedure ParseFile(const FileName: string; Document: TDTDDocument; const Pass:
      Integer = 0);
  public
    { Public declarations }
    procedure Parse(const FileName: string; var Document: TDTDDocument);
  end;

Afterwards we implement the Parse method which calls the internal method ParseFile on her part:

Method "Parse":

procedure TDTDParser.Parse(const FileName: string; var Document: TDTDDocument);
var
  TmpDocument: TDTDDocument;
begin
  if not assigned(Document) then
    raise EDTDParser.Create('Document not assigned!');
  TmpDocument := TDTDDocument.Create;
  try
    ParseFile(FileName, TmpDocument);
    if Errors.Count = 0 then
      Document.Assign(TmpDocument);
  finally
    TmpDocument.Free;
  end;
end;

As you can see, we create a special temporar document to store the parsed objects in. I've done this because I don't want to return the document if it is full of errors - I assign a exact copy of the objects only, if no errors occured. The method ParseFile implements the proper parsing calls to the StringParser and creates the real objects. Followed a code snippet of the method body:

Method "ParseFile":

procedure TDTDParser.ParseFile(const FileName: string;
  Document: TDTDDocument; const Pass: Integer = 0);
var
  Parser: TStringParser;
begin
  Parser := TStringParser.Create;
  try
    if not Parser.LoadFromFile(FileName) then
    begin
      AddErrorFmt('File "%s" not found', [FileName], Parser);
      Exit;
    end;
    while True do
    begin
      while not (Parser.Token in [toEOF, '<']) do
        Parser.SkipToken;
      if Parser.Token = toEOF then
        Break;
      Parser.SkipToken;
      if Parser.Token <> '!' then
      begin
        if not (Parser.Token in ['?']) and (Pass = 1) then
          AddError('InvalidToken', Parser);
        Continue;
      end;
      if Parser.SkipToken <> toSymbol then
      begin
        if (Parser.Token <> '-') and (Pass = 1) then
          AddError('InvalidToken', Parser);
        Continue;
      end;
      if UpperCase(Parser.TokenString) = 'ENTITY' then
        Continue;
      if UpperCase(Parser.TokenString) = 'ELEMENT' then
        ParseElement(Parser, Document, Pass)
      else if UpperCase(Parser.TokenString) = 'ATTLIST' then
      begin
        if Pass = 1 then
          ParseAttlist(Parser, Document);
      end
      else if Pass = 1 then
        AddErrorFmt('Invalid Symbol "%s"', [Parser.TokenString], Parser);
    end;
    if Pass = 0 then
      ParseFile(FileName, Document, 1);
  finally
    Parser.Free;
  end;
end;

This method calls some other functions (ParseElement and ParseAttlist) which parses the internal structures of an element or an attribute. Look at the whole sourceode to understand.

What's next??

Well, this article has shown you how easy it is to write a customizeable parser which can parse any kind of data - it's up to you, how complex it should be. The main benefit in using this kind of parsing is, that you don't need to incorporate in complex systems like LexParser.

Continue reading my second article:

Building an Easy-to-Use Parser/Parsing Framework (Part II)

2008. január 3., csütörtök

Resize all controls when the parent form is resized


Problem/Question/Abstract:

How to resize all controls when the parent form is resized

Answer:

I would like to describe a technique I used in order to resize (with recursion) all the child controls of a TForm when the user resizes the form itself. I think it works relatively well, at least for my requirements. The key is the implementation of the OnResize event of the TForm. Every time the TForm is resized we chnage from an initial height PH to a final height HH and from an initial width PW to a final width WW.

At the very beginning (for example in the OnCreate event) we register the value of PW and PH:

Form1.FormCreate(Sender: TObject);
begin
  { ... }
  PW := Form1.ClientWidth;
  PH := Form1.ClientHeight;
  { ... }
end;

Then in the implementation of the OnResize event we have:

Form1.FormResize(Sender: TObject);
begin
  WW := Form1.ClientWidth; {new value of width after the resizing}
  HH := Form1.ClientHeight; {new value of height after the resizing}
  {put your cyclically resizing code here}
  {actual dimension: they will be the old dimensions in the next resizing }
  PW := WW;
  PH := HH;
end;

Now let's go deep into details with the cyclically resizing code. This is the code we place inside the implementation of the OnResize event of the TForm:

{ ... }
for I := 0 to (Form1.ControlCount - 1) do
begin
  ChangeDims(Form1.Controls[i], WW, PW, HH, PH);
end;

"ChangeDims" is the procedure which resizes (with recursion) the child controls.

procedure ChangeDims(Control: TControl; WW, PW, HH, PH: Integer);
var
  L, T, W, H: integer;
begin
  if Control is TWinControl then
    (Control as TWinControl).DisableAlign;
  try
    if Control is TWinControl then
      ScaleCtrls(Control as TWinControl, WW, PW, HH, PH);
    L := MulDiv(Control.Left, WW, PW);
    T := MulDiv(Control.Top, HH, PH);
    W := MulDiv(Control.Left + Control.Width, WW, PW) - L;
    H := MulDiv(Control.Top + Control.Height, HH, PH) - T;
    Control.SetBounds(L, T, W, H);
  finally
    if Control is TWinControl then
      (Control as TWinControl).DisableAlign;
  end;
end;

procedure ScaleCtrls(Control: TWinControl; WW, PW, HH, PH: Integer);
var
  I: Integer;
begin
  for I := 0 to Control.ControlCount - 1 do
    ChangeDims(Control.Controls[i], WW, PW, HH, PH);
end;

By making the above mentioned changes to your Form you can achieve the autoresizing of all child controls on a form. After verifing the validity of the code I encapsulated its functionality in a component. I didn't want to fill every time with extra code my form's implementation: I wanted to drop a component to my form and stop. This component assign the implementation of the OnResize event of the form after having assigned the aventually already present OnResize implementation. As a consequence the OnResize event of the Form will consist in the call to the user defined implementation (if any) followed by the above mentioned implementation which is responsible for the resizing. In this way you can assign your implementation to the OnResize event of the Form avoiding its hiding because of the presence of the cyclically resizing code.

Here is the complete code for the component:

unit Resize;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  { THackControl = class(TControl); }

  TResize = class(TComponent)
  private
    { Private declarations }
    FForm1: TForm;
    PW, PH: integer;
    FOnResizeEvent: TNotifyEvent;
    procedure ScaleCtrls(Control: TWinControl; WW, PW, HH, PH: Integer);
    procedure ChangeDims(Control: TControl; WW, PW, HH, PH: Integer);
    procedure OnResizeEvent(Sender: TObject);
    procedure Resize();
  protected
    { Protected declarations }
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy(); override;
  published
    { Published declarations }
  end;

procedure Register;

implementation

constructor TResize.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  if csDesigning in ComponentState then
    Exit;
  FForm1 := (Owner as TForm);
  PW := FForm1.ClientWidth;
  PH := FForm1.ClientHeight;
  if Assigned(FForm1.OnResize) then
  begin
    FOnResizeEvent := FForm1.OnResize;
  end;
  FForm1.OnResize := OnResizeEvent;
end;

destructor TResize.Destroy();
begin
  if not (csDesigning in ComponentState) then
    FForm1.OnResize := FOnResizeEvent;
  inherited Destroy();
end;

procedure TResize.ScaleCtrls(Control: TWinControl; WW, PW, HH, PH: Integer);
var
  I: Integer;
begin
  for I := 0 to Control.ControlCount - 1 do
    ChangeDims(Control.Controls[i], WW, PW, HH, PH);
end;

procedure TResize.ChangeDims(Control: TControl; WW, PW, HH, PH: Integer);
var
  L, T, W, H: integer;
begin
  if Control is TWinControl then
    (Control as TWinControl).DisableAlign;
  try
    if Control is TWinControl then
      ScaleCtrls(Control as TWinControl, WW, PW, HH, PH);
    L := MulDiv(Control.Left, WW, PW);
    T := MulDiv(Control.Top, HH, PH);
    W := MulDiv(Control.Left + Control.Width, WW, PW) - L;
    H := MulDiv(Control.Top + Control.Height, HH, PH) - T;
    { THackControl(Control).Font.Size := (PW div WW) * THackControl(Control).Font.Size;}
    Control.SetBounds(L, T, W, H);
  finally
    if Control is TWinControl then
      (Control as TWinControl).DisableAlign;
  end;
end;

procedure TResize.OnResizeEvent(Sender: TObject);
begin
  if Assigned(FOnResizeEvent) then
  begin
    FOnResizeEvent(Sender);
  end;
  Resize();
end;

procedure TResize.Resize();
var
  L, T, R, B, W, H, WW, HH, I: Integer;
begin
  WW := FForm1.ClientWidth;
  HH := FForm1.ClientHeight;
  for I := 0 to (FForm1.ControlCount - 1) do
  begin
    ChangeDims(FForm1.Controls[i], WW, PW, HH, PH);
  end;
  PW := WW;
  PH := HH;
end;

procedure Register;
begin
  RegisterComponents('Carlo Pasolini', [TResize]);
end;

end.

2008. január 2., szerda

Implementing object persistence using streams


Problem/Question/Abstract:

How do I save data entered in a list box at run time without resorting to a text file or having to deal with the overhead of a table?

Answer:

This question actually asks much more than just saving a list box at runtime. It brings to the surface some of the internal workings of Delphi itself. But I should clarify that what I present here does not deal with the workings of Delphi's Runtime Type Information (RTTI). That's probably best left for another article. But what I'm going to discuss is a practical way of implementing object persistence in your programs by using the saving the of a listbox at runtime as an example. Okay, here we go...

Any OOP class library worth its salt supports what is called streamable persistent objects. Simply put, this means that an instance of a class (or at least its data) can be saved to a disk file and restored later. When a program reloads the object, it is restored in its last state, just prior to being written. The cool thing about this is that the program doesn't have to have any advance knowledge of the state of the object; the object itself contains all the information it needs to recreate itself when it's restored.

For example, let's say you've created a program that has a list box in which people append various bits of information at run time. For many folks, saving the information to disk means iterating through all the items in the list and writing them to a text file or even a table. The program must reload the data from the external file and add the data, line by line. This is not so bad, but it can be a bit of a chore to write the code.

On the other hand, using object persistence, the same program mentioned above instructs the list box to write its data to a disk file of some sort. When it wants to reload the object, all it has to do is stream it back into memory and specify the base class to write to. Remember, since all the data of the object was saved with it when it was written to disk, the object comes back to life in its original form. That's the whole idea behind object persistence.

Delphi itself makes heavy use of object persistence. Every time you save a project, it streams out to disk the data contained in your objects' properties so that everything you set during your session is saved. When you reload a project, Delphi streams the object data back into your form(s) to restore everything you previously set. In fact, a form file itself is streamed to and from disk. I should note here that Delphi uses a couple of specialized stream classes, TWriter and TReader which are derived from a superclass called TFiler. I won't go into the details of these classes here, since I'm providing a much simpler demonstration of employing object persistence in your programs. I'll leave it up to you to research this topic further.

Moving on, you might ask, "Where does employing streamable persistent objects come in handy?" The most useful cases I've found for employing them are when I've written programs that provide parameter or input criteria for processes, where the range of possible values to search on remain fairly constant from one run of the program to the next.

For instance, in my line of work, almost all of my programs are typically front-ends to very complex query operations. However, the range of domains and their values don't change very often, and from client to client, the same questions are typically asked. So in these cases, I've found that simply streaming my criteria objects (these are all list objects) out to disk when I close the forms and streaming them back in when I open the forms provides a much cleaner solution to saving my criteria sets from session to session. Besides, this is very low overhead programming, since once the programs are finished with the streams, they're immediately destroyed. Not only that, I don't have to use DB.PAS or DBTables.PAS for data operations.

A simple example

The example I've provided here is by no means a full-fledged search program of the type I normally write. I've merely taken the parts pertinent to this article for your use. Feel free to include or modify this code to your heart's content. In any case, here's the code listing for the main form of the program. We'll discuss particulars below.

unit main;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics,
  Controls, Forms, Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Edit1: TEdit;
    Memo1: TMemo;
    procedure Edit1KeyPress(Sender: TObject; var Key: Char);
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure ListBox1DblClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #13 then
  begin
    Key := #0;
    ListBox1.Items.Add(Edit1.Text);
    Edit1.Text := '';
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  strm: TFileStream;
begin
  if FileExists('MyList.DAT') then
  begin
    strm := TFileStream.Create('MyList.DAT', fmOpenRead);
    strm.ReadComponent(ListBox1);
    strm.Free;
  end;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
  strm: TFileStream;
begin
  strm := TFileStream.Create('MyList.DAT', fmCreate);
  strm.WriteComponent(ListBox1);
  strm.Free;
end;

procedure TForm1.ListBox1DblClick(Sender: TObject);
begin
  ListBox1.Items.Delete(ListBox1.ItemIndex);
end;

end.

You were expecting some complex code, weren't you? In actuality, this stuff is incredibly simple. So why isn't it documented very well? I'd say it's because this is one of the more uncommon things done in Delphi. But for those of you who wish to really get into the innards of the environment, this stuff is a must to understand and master. Let's look a little deeper into the code.

The program consists of a form with a TEdit and a TListBox dropped onto it. It has just two meaningful methods: FormCreate and FormClose. In the FormCreate method,

procedure TForm1.FormCreate(Sender: TObject);
var
  strm: TFileStream;
begin
  if FileExists('MyList.DAT') then
  begin
    strm := TFileStream.Create('MyList.DAT', fmOpenRead);
    strm.ReadComponent(ListBox1);
    strm.Free;
  end;
end;

the program checks for the existence of MyList.DAT with a call to FileExists, which is the stream file that holds the list box information. If it exists, the file is streamed into ListBox1; otherwise, it does nothing. With the FormClose method,

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
var
  strm: TFileStream;
begin
  strm := TFileStream.Create('MyList.DAT', fmCreate);
  strm.WriteComponent(ListBox1);
  strm.Free;
end;

the program writes ListBox1 out to MyList.DAT, overwriting any previous versions of the file.

That's all there is to this program. Surprisingly, this is one of the more simple things to do in Delphi, but paradoxically it's one of the most difficult things to find good information about in the manuals or help file. Granted, as I mentioned above, doing this type of stuff is fairly uncommon, but think of the implication: simple, low overhead, persistent storage without the need for tables. What was accomplished above was done in fewer than 10 lines of code &mdash that's absolutely incredible!

I urge you to play around with this technique and apply it to other things. I think you'll get a lot of mileage out of it.

A demonstration program is available.

2008. január 1., kedd

Color all controls on a form at runtime


Problem/Question/Abstract:

I want to color form controls at runtime without knowing beforehand how many and what controls there are.

Answer:

type
  TControlCracker = class(TControl);

procedure TForm1.Button1Click(Sender: TObject);
var
  I: Integer;
begin
  for I := 0 to ComponentCount - 1 do
    if Components[I] is TControl then
      TControlCracker(Components[I]).Color := clLime;
end;