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

How to save components to a file or stream (3)


Problem/Question/Abstract:

In D6, what's the best or usual way to stream an object to text so that it shows up just as it does when you copy an object to the clipboard or when you display a form as text?

Answer:

This example shows how to use the built-in VCL component streaming support to convert any component into a string and convert that string back into a component.

function ComponentToString(Component: TComponent): string;
var
  BinStream: TMemoryStream;
  StrStream: TStringStream;
  s: string;
begin
  BinStream := TMemoryStream.Create;
  try
    StrStream := TStringStream.Create(s);
    try
      BinStream.WriteComponent(Component);
      BinStream.Seek(0, soFromBeginning);
      ObjectBinaryToText(BinStream, StrStream);
      StrStream.Seek(0, soFromBeginning);
      Result := StrStream.DataString;
    finally
      StrStream.Free;
    end;
  finally
    BinStream.Free
  end;
end;

function StringToComponent(Value: string): TComponent;
var
  StrStream: TStringStream;
  BinStream: TMemoryStream;
begin
  StrStream := TStringStream.Create(Value);
  try
    BinStream := TMemoryStream.Create;
    try
      ObjectTextToBinary(StrStream, BinStream);
      BinStream.Seek(0, soFromBeginning);
      Result := BinStream.ReadComponent(nil);
    finally
      BinStream.Free;
    end;
  finally
    StrStream.Free;
  end;
end;

2008. január 30., szerda

How to mix or separate three color channels


Problem/Question/Abstract:

I need to send a picture to 3 separate monochrome monitors in an embedded application I am writing. I could simply allow only one monitor to work at a time but thought it might be possible to drive the RGB outputs separately. This could be achieved by creating 3 pictures, one with red shades, one with green shades and one with blue shades and blending them together. I could do this by mixing the pictures pixel by pixel but suspect this would be extremely slow.

Answer:

But it's the only way (all other ways I can think of go back to the same). Use scanline and it is not that "extremely slow":

{ ... }
var
  Ptr1, Ptr2, Ptr3: ^Byte;
  PtrMix: ^Byte;
  X, Y: Integer;
begin
  for Y := 0 to Height - 1 do
  begin
    Ptr1 := RedBitmap.ScanLine[Y];
    Ptr2 := GreenBitmap.ScanLine[Y];
    Ptr3 := BlueBitmap.ScanLine[Y];
    PtrMix := MixBitmap.ScanLine[Y];
    for X := 0 to Width - 1 do
    begin
      PtrMix^ := Ptr1^;
      Inc(Ptr1);
      Inc(PtrMix);
      PtrMix^ := Ptr2^;
      Inc(Ptr2);
      Inc(PtrMix);
      PtrMix^ := Ptr3^;
      Inc(Ptr3);
      Inc(PtrMix);
    end;
  end;
end;

Make sure MixBitmap has 24 bit format, Red-, Green- and BlueBitmap have 8 bit format (or change the code for other formats). All bitmaps should have the same size (or use the smallest size for Width and Height).

Separating 3 color channels (instead of mixing) is very similar. Simply change PtrMix^ := Ptr1^; to Ptr1^ := PtrMix^; (and for the other channels, too).

2008. január 29., kedd

Force a selection of cells in a TStringGrid


Problem/Question/Abstract:

I'm attempting to force a selection of cells based on the user clicking in the grid. Here is my code:

procedure TSizeSelectFrm.SizeSelectStrngGrdSelectCell(Sender: TObject;
  ACol, ARow: Integer; var CanSelect: Boolean);
var
  Select: TGridRect;
begin
  CanSelect := False;
  Select.Left := 0;
  Select.Right := 2;
  if ARow < 10 then
  begin
    Select.Top := ARow;
    Select.Bottom := SizeSelectStrngGrd.Selection.Bottom;
  end;
  if ARow > 10 then
  begin
    Select.Top := SizeSelectStrngGrd.Selection.Top;
    Select.Bottom := ARow;
  end;
  SizeSelectStrngGrd.Selection := Select;
end;

What happens is sometimes the selected cells don't always select properly. In the debugger, the value for select seems to be always set properly. Is there something wrong with the way I'm "applying" the selection?

Answer:

Your selection is overwritten when the mouse crosses a cell boundary or when the mouse button is released. SizeSelectStrngGrd.Selection. Right is made to correspond to the most recent cursor position. You can put it back like so:

procedure TForm1.SizeSelectStrngGrdMouseUp(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  Select: TGridRect;
begin
  {When the mouse button is released set the right of the selected range}
  Select := SizeSelectStrngGrd.Selection;
  Select.Right := 2;
  SizeSelectStrngGrd.Selection := Select;
  {Record that the mouse button is up}
  MouseDown := false;
end;

MouseDown is a class-scope Boolean that is used later. The mouse up handler above gives the right selection, but the grid looks naff as the selected area spreads out to the right then contracts when the mouse button is released. You can improve on that by recording whether the left mouse button is up or down:

procedure TForm1.SizeSelectStrngGrdMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  {Record that the mouse button is down}
  MouseDown := mbLeft = Button;
end;

and controlling how the grid is drawn so that cells you don't want to be selected are not drawn as though they were selected:

procedure TForm1.SizeSelectStrngGrdDrawCell(Sender: TObject;
  ACol, ARow: Integer; Rect: TRect; State: TGridDrawState);
begin
  {When the mouse is down only allow cells in column two to be drawn as though they are selected}
  if ((ACol <> 2) and (gdSelected in State) and MouseDown) then
    State := State - [gdSelected];
  with SizeSelectStrngGrd.Canvas do
  begin
    {Choose a brush colour to suit the cell}
    if gdFixed in State then
      Brush.Color := clBtnFace
    else if gdSelected in State then
      Brush.Color := clHighlight
    else
      Brush.Color := clWindow;
    {Colour the cell background}
    FillRect(Rect);
    {Display the text}
    TextOut(Rect.Left + 2, Rect.Top + 2, SizeSelectStrngGrd.Cells[ACol, ARow]);
  end;
end;

This will likely need some polishing before it's ready for use, but it does let you control what is
selected.







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

How to change the caption of a MessageDlg


Problem/Question/Abstract:

How to change the caption of a MessageDlg

Answer:

Build your own:

function MyMessageDlg(const Msg: string; DlgType: TMsgDlgType; Buttons:
  TMsgDlgButtons;
  const ACaption: string = 'Hi'; DefaultButtonIndex: Integer = -1; HelpCtx: Longint =
    0): Integer;
var
  Index: Integer;
  ButtonIndex: Integer;
begin
  with CreateMessageDialog(Msg, DlgType, Buttons) do
  try
    HelpContext := HelpCtx;
    if ACaption <> '' then
      Caption := ACaption;
    if DefaultButtonIndex >= 0 then
    begin
      ButtonIndex := -1;
      for Index := 0 to ControlCount - 1 do
      begin
        if Controls[Index] is TButton then
        begin
          Inc(ButtonIndex);
          TButton(Controls[Index]).Default := ButtonIndex = DefaultButtonIndex;
          if ButtonIndex = DefaultButtonIndex then
            ActiveControl := TButton(Controls[Index]);
        end;
      end;
    end;
    Result := ShowModal;
  finally
    free;
  end;
end;

2008. január 27., vasárnap

Refresh the Windows Desktop


Problem/Question/Abstract:

How can I refresh the Windows desktop after I have set a new background image in the registry?

Answer:

Solve 1:

I want to post this article because I sure that this question can ask a lot of developers but to find an answer is not easy task.

So if you need refresh a desktop in run-time, you can execute a next procedure:

uses ShlObj;
{...}
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);

This code will refresh a desktop image and re-read the icons for files with registered extentions.

PS: you can do a same if you press a right mouse button on desktop and select Update item.

Solve 2:

procedure TForm1.Button1Click(Sender: TObject);
begin
  SendMessage(FindWindow('Progman', 'Program Manager'), WM_COMMAND, 106597, 0);
end;

This has the same effect as pressing F5 in Explorer.

2008. január 26., szombat

Understanding what files are and choosing a Delphi file type - part 3


Problem/Question/Abstract:

What is a File? How are they stored? What format is best for my project? - The final part of a series by Philip Rayment

Answer:

The final example uses a simple text file for storage:

Example 3: Text File

procedure�WriteFile(filename:string);
var
��fil:�text;
��i:�integer;
begin���{WriteFile}
��AssignFile(fil,filename);�rewrite(fil);{Create the file}
  {Write the file version and number of records}
��Writeln(fil,LatestFileVersion,'�',length(People));[6]
��for�i:=0�to�high(people)�do
����with�people[i]�do�begin{Write the data}
������Writeln(fil,ChristianName);[6,10]
������Writeln(fil,Surname);[7,14]
������Writeln(fil,Address1);[21,9]
������Writeln(fil,Address2);[2,24]
������Writeln(fil,Town);[13,19]
������Writeln(fil,Postcode,'�',YearsService,'�',ID,'�',DateToStr(Birthdate)); [24,23]
����end;���{with}
��CloseFile(fil);
end;���{WriteFile}

procedure�ReadFile(filename:string);
var
��ver:�byte;
��i,�num:�integer;
��d:�string;
begin���{ReadFile}
��AssignFile(fil,filename);�reset(fil);��{Open the file}
��readln(fil,ver,num);{Read the version number and number of records}
��SetLength(people,num);
��for�i:=0�to�pred(num)�do
������with�people[i]�do�begin��{Read the data}
��������Readln(fil,ChristianName);
��������Readln(fil,Surname);
��������Readln(fil,Address1);
��������Readln(fil,Address2);
��������Readln(fil,Town);
��������Readln(fil,Postcode,YearsService,ID,d);
��������Birthdate:=StrToDate(d);
������end;���{with}
��CloseFile(fil);
end;���{ReadFile}

Analysis

The total file size is 178 bytes, not much more than the untyped file. Delphi automatically converts the numbers to text, but we have to use the DateToStr function to convert the date to text.

This table provides comparative statistics on the three examples:


Untyped files
Typed Files
Text Files
File size
143 bytes
342 bytes
178 bytes
Bytes in "header"
3
114
6
Average bytes per record
70
114
86
Lines of code in WriteFile()
28
12
17
Estimate of speed
Probably fairly fast, as no conversions were required, but there were many different calls to BlockWrite and several calls to a subroutine.
Probably fastest, as just three separate writes to the disk were involved.
Probably the slowest, due to all the conversions to text required.


The Advantages and disadvantages of the various file types are as follows:

Untyped Files

Advantages

You can store anything you wish in Untyped Files. There are no restrictions.
Untyped Files will normally be the most compact

Disadvantages

You have to keep track of the data yourself, which will normally require a fair bit of coding.

Typed Files

Advantages

Typed Files are easier to use than Untyped files.
Typed Files are probably the fastest for most purposes as the data can be loaded straight into the record. The other types usually involve more conversions and/or data shuffling.

Disadvantages

You are limited to one type of data per file.
Records have to be designed to hold the largest data (e.g. the longest possible name) and all records thus take up this space, so a Typed File is generally the most space hungry.
It is pointless writing pointers to the file (if you did, you would simply write and read the memory address, not the data itself), so you cannot have file of pointers, objects, or strings (longstrings) or records containing any of these. In any case the compiler will not allow a file of string (longstring).

Text Files

Advantages

Delphi has special facilities for handling Text Files, such as conversion of numerical data to text and vice versa, making Text Files easy to use and fairly compact.
Text Files can be viewed in a text editor or even dumped to the screen (or printer) at the command prompt with the TYPE command.
Corrupted files can be edited with a text editor.

Disadvantages

Non-textual data (other than numbers) cannot be included, unless somehow converted into a textual form.
Not efficient storage of non-textual data.

Appendix A

The following table compares how to code selected actions for various file types.


Untyped files
Typed Files
Text Files
Declaring
var
  f: file;
  v: type;
  i: integer;
var
  f: file of type;
  r: type;
  i: integer;
var
  f: TextFile;
  i: integer;
  s: string;
  ch: char;
Assigning
AssignFile(f, filename);
AssignFile(f, filename);
AssignFile(f, filename);
Opening for reading
FileMode := 0;
Reset(f, 1);
FileMode := 0;
Reset(f);
Reset(f);
Opening for reading and writing
Reset(f, 1);
Reset(f);
Not available
Opening for appending
Reset(f, 1);
Seek(f, filesize(f));
Reset(f);
Seek(f, filesize(f));
Append(f);
Creating
write(f, 1);
Rewrite(f);
Rewrite(f);
Reading
BlockRead(f, v, sizeof(v));
Read(f, r);
Read(f, i);
Read(f, s);
Readln(f, i, ch, s);(1)
Skip a record/line while reading
Seek(f, filepos(f) + sizeof(v));
Seek(f, succ(filepos(f)));
Readln(f);(2)
Writing
BlockWrite(f, v, sizeof(v));
Write(f, r);
Write(f, i);
Write(f, s);
Writeln(f, i, &#8216; &#8216;, s);(1)
Get the current file position
i := filepos(f);
i := filepos(f);
Not available
Jump to a position in the file
Seek(f, i);
Seek(f, i);
Not available
Get the file size
i := filesize(f);
i := filesize(f);
Not available (3)
Closing
CloseFile(f);
CloseFile(f);
CloseFile(f);


The Read, ReadLn, Write, and WriteLn procedures can take multiple arguments.
If the ReadLn procedure has no parameters (other than the file variable), the file pointer merely moves to the end of the line. If the WriteLn procedure has no parameters (other than the file variable), a blank line is output.
See the Tip "How do I get the size of a Text File in Delphi"?

2008. január 25., péntek

Address Sort Order Index


Problem/Question/Abstract:

Address Sort Order Index

Answer:

The custom sort order is used to deal with the fact that the house and flat numbers are sorted as strings. They are stored as strings to allow things like '150-175' as a house number, or '3a', or perhaps even simply a flat 'A'.
The need for a custom sort order is caused by the fact that with an ordinary ASCII sort order '4' will appear after '30'. This is not desirable behaviour.

This approach to fix this problem is to look for the first number  in the string (if there is one) and then use this as some kind of  primary sort order. The rest of the sorting will then be done on  the remaining characters (with preceding and trailing spaces stripped out), based on the ASCII value of their upper - case varients. Potential problems caused by this approach include (but are not limited to) the use of accented characters will
possibly cause strange orderings and furthermore, if there is a block of flats with three floors A, B, C for example then supposing the flats on those floors are A1, A2, A3, B1, B2, B3 then the ordering of records will not be ideal - this approach will sort them as A1, B1, A2, B2, A3, B3. This behaviour is regrettable, but acceptable - we cannot tell that it is not flat A on floor 1 for example. It's unlikely that we will be able to find a sort order that always produces ideal results.

Some examples of sorted lists (not all ideal):

EXAMPLE 1
EXAMPLE 2
EXAMPLE 3






Flat 1
1
A
Flat 2
-2
B
3
2-4
C
3B
3a
1
Flat 3A
5
2


                
                            
                          
unit AddrSortOrder;

interface

uses SysUtils;

function CalcSortIndex(NumStr: string): double;

implementation

function CalcSortIndex(NumStr: string): double;
var
  strlength, i, j, tmp: integer;
  found: boolean;
  numpart, strpart, divisor: double;
  choppedstr: string;
begin
  //This function will return the sort index value for the string passed

  strlength := length(NumStr);
  if strlength = 0 then
  begin
    result := 0;
    exit;
  end;

  found := false;

  //split the string into a 'number' and a 'string' part..

  //initialise
  choppedstr := numstr;
  numpart := 0;

  //Locate the first digit (if there)
  for i := 1 to strlength do
  begin
    if numstr[i] in ['0'..'9'] then
    begin
      found := true; //First digit found!!
      break;
    end;
  end; //for i..

  if found then
  begin
    //now get the to the end of the digits..
    found := false;
    for j := i to strlength do
    begin
      if not (numstr[j] in ['0'..'9']) then
      begin
        found := true; //end of digits found
        break;
      end;
    end; //for j..

    //Separate out the string parts
    if found then
    begin
      //Number was embedded..
      val(copy(numstr, i, j - i), numpart, tmp);
      Delete(choppedstr, i, j - i);
    end
    else
    begin
      //Number went to the end of the string
      val(copy(numstr, i, strlength), numpart, tmp);
      Delete(choppedstr, i, strlength);
    end;
  end;

  choppedstr := Uppercase(trim(choppedstr));
  strlength := length(choppedstr);

  //evaluate a number for the remaining part of the string
  strpart := 0;
  divisor := 1;

  for i := 1 to strlength do
  begin
    divisor := divisor / 256;
    //convert from Char to single using a variant conversion
    strpart := strpart + (ord(choppedstr[i]) * divisor);
  end;

  //All done, return the value
  result := numpart + strpart;
end;

end.

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

Basic functions of the listbox explained


Problem/Question/Abstract:

I'm a beginner, how do i use the listbox??

Answer:

Ok heres some basic info on the listbox.
Their are many different functions to do with the listbox and it is not possible to display them all. So i am going to try and explain as many as possible.

Number 1, Lets start from the beginning.

OK so once you have placed the listbox you can add items at design time but to add them at runtime is a little more trick

make a form with one listbox, 1 button and 1 edit box

then add the following

in the button1 onclick method plave

listbox1.items.add(edit1.text);

this then adds the current text in the edit box onto the bottom of the list you can add as many items as you like until you exit the program.

Number 2, Taking it one step further.

Ok so you have a listbox now with a few items in it, but what if you want to add one of the items in at say line 3 instead of the bottom line. This can easily be achieved by amending the above code, by changing the 'add' to 'insert' and then adding an index refrence you can place it in at the desired line.

for example

listbox1.items.insert(3, edit1.text);

this will always then add the text from the edit box to the third line

Number 3, The rest.

Ok now you can get the total amount of items currently in a listbox by inserting the count procedure

showmessage('Current Number of lines := ' + inttostr(listbox1.items.count));

this will then display the message 'Current Number of Lines := ??' where ?? is a number.

Finally to get the currently selected item in the listbox you can add the following to the onclick method of your listbox

edit1.text := listbox1.items[listbox1.itemindex];

2008. január 23., szerda

Position The Start-Button Anywhere On Your Taskbar


Problem/Question/Abstract:

Hiding/displaying/enabling and disabling the start button seems an old trick, try this new one
Start moving the start button on the taskbar !!

Answer:

Well, Start A New Project, And Give The Form The Next Properties

Align = top;
width = screen.width;

and then, OnMouseMove exent place the following code ....

procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  p: tpoint;
begin
  getcursorpos(p);
  movewindow(FindWindowEx(FindWindow('Shell_TrayWnd', nil), 0, 'Button', nil), x, y,
    25, 25, true);
end;

Run your application, when moving your mouse on your form, you will notice that the start button is minimized and is moving along with your mouse, with some more work, you can make this invisible to make the statr button move on a pre-schedueled event !!


Some More Information....

If you do the following, you will start to smile of what will happen on your desktop

procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
var
  p: tpoint;
begin
  getcursorpos(p);
  movewindow(FindWindowEx(FindWindow('Shell_TrayWnd', nil), 0, 'ReBarWindow32', nil),
    x, y, 25, 25, true);
end;


Have Fun !!

2008. január 22., kedd

Get the handle to the icon of a registered extension


Problem/Question/Abstract:

How can I get the icon of a registered extension (in the windows registry)?

Answer:

Solve 1:

First get the pointer to the operating systems image list and assign it to your own.

{Image List From System}
ImageListHandle := SHGetFileInfo('C:\', 0, FileInfo, SizeOf(FileInfo), SHGFI_SYSICONINDEX  or SHGFI_SMALLICON);
SendMessage(ListView1.Handle, LVM_SETIMAGELIST, LVSIL_SMALL, LParam(ImageListHandle));

Then in your listview you can extract out the icon by doing this:

function GetShellIcon(FileName: string; Folder: Boolean): Integer;
var
  FileInfo: TSHFileInfo;
  ImageHandle: THandle;
  Flag: Integer;
begin
  if Folder then
    Flag := FILE_ATTRIBUTE_DIRECTORY
  else
    Flag := FILE_ATTRIBUTE_NORMAL;
  SHGetFileInfo(PChar(FileName), Flag, FileInfo, SizeOf(FileInfo), SHGFI_SYSICONINDEX or SHGFI_SMALLICON or SHGFI_UseFileAttributes);
  Result := FileInfo.IIcon;
end;

Notice: At the bottom I assign the icon index from the IICON field of the data returned by SHGetFileInfo. So my function gets passed a filename and returns an image index. You could change the IICON field to the HICON field and then I believe you would be getting the handle to an icon.


Solve 2:

The SHGFI_SYSICONINDEX in the above code snippet is going to place the index of the icon in the system image list (a handle to which is being returned as the result of the SHGetFileInfo function call) in the FileInfo.iIcon member. Use the SHGFI_ICON flag instead to get an icon handle in HIcon.

function GetShellIcon(FileName: string; Folder: Boolean): Integer;
var
  FileInfo: TSHFileInfo;
  ImageHandle: THandle;
  Flag: Integer;
  IconHandle: THandle;
begin
  if Folder then
    Flag := FILE_ATTRIBUTE_DIRECTORY
  else
    Flag := FILE_ATTRIBUTE_NORMAL;
  SHGetFileInfo(PChar(FileName), Flag, FileInfo, SizeOf(FileInfo),
    SHGFI_ICON or SHGFI_SMALLICON or SHGFI_UseFileAttributes);
  Result := FileInfo.IIcon;
  IconHandle := FileInfo.HIcon;
end;


Solve 3:

One method of getting the associated icon with a particular file is to first populate an imagelist with registered system icons (this example is for small icons, you could do the same for large). In this example, iIndex will be the ImageList item for the particular file.

var
  dwSmallIcon: DWord;
  pFileInfo: TshFileInfo;
  iIndex: Integer;
begin
  dwSmallIcon := SHGetFileInfo('', 0, FileInfo, SizeOf(TshFileInfo), (SHGFI_ICON or
    SHGFI_SMALLICON or SHGFI_SYSICONINDEX));
  ImageList1.Handle := dwSmallIcon; {imagelist}
  ImageList1.ShareImages := True;
  FillChar(pFileInfo, SizeOf(TshFileInfo), 0);
  shGetFileInfo(pName, 0, pFileInfo, SizeOf(TshFileInfo), SHGFI_SYSICONINDEX or
    SHGFI_SMALLICON or SHGFI_OPENICON);
  iIndex := pFileInfo.iIcon;
end;

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

How to load, create and use (animated) cursors?


Problem/Question/Abstract:

Sometimes you have the need to use an animated cursor, for example when your applications are executing long operations.
This article explains how to load, create and use cursors (animated included).

Answer:

To use an animated cursor you have several options: load it from a file (using LoadImage or LoadCursorFromFile), load it from a resource (using LoadCursor) or even creating the cursor at runtime (using CreateCursor).

Note:
You should implement custom cursors as resources. Rather than create the cursors at run time, use the LoadCursor, LoadCursorFromFile, or LoadImage function to avoid device dependence, to simplify localization, and to enable applications to share cursor designs.

Loading a cursor from a file
  
The easiest way to load a cursor from a file is by using LoadCursorFromFile.
This functions returns a handle to the loaded cursor that you should assign to your application Cursors array.

var
  hCur: HCURSOR;

begin
  // Load the cursor from file
  hCur := LoadCursorFromFile(PChar('path_to_my_cursor'));
  // Assign the loaded cursor to application Cursors array. (This cursor will ave the
  // number 1 assigned to it
  // Remember that predefined cursors start at a negative index, and user defined
  // custom cursors are assigned positive indexes.
  Screen.Cursors[1] := hCur;

  // Use the cursor as you would use a built-in cursor.
  Screen.Cursor := 1;
end;

You can also use LoadImage instead of LoadCursorFromFile like this:  

  hCur := LoadImage(0, PChar(PChar('path_to_my_cursor')), IMAGE_CURSOR, 0, 0,
  LR_DEFAULTSIZE or LR_LOADFROMFILE);
    
Loading a cursor from a resource

Before loading a cursor from a resource it's necessary to create the resource file with the cursor to be loaded.

To do this create a file myResources.rc where you'll put the following

#define ANICURSOR 21
myCursor ANICURSOR "path_to_my_cursor"

Because Borland's resource compiler does not understand the ANICURSOR resource type, so you have to use the numeric id (21).
Compile your resource file using "brcc32 myResources.rc" and include in the unit where you'll be loading the cursor, using {$R myResources.res}

Now, you just have to load the cursor from the resource instead of loading it from a file, using:

hCur := LoadCursor(HInstance, PChar('myCursor'));
  
Remember that HInstance contains the instance handle of the application or library as provided by Windows. This variable it's very importante because it's the one used with many Windows API that work with current application resources.
  
Creating a cursor at runtime
  
Another way to use a cursor it's creating one at runtime. Why would you do that?
I don't know, it's your choice. I doubt you ever will create your cursors at runtime, anyway here it's way how to do it.

Define the cursor map

const
  // Yin cursor AND bitmask
  ANDmaskCursor: array[0..127] of byte = (
    $FF, $FC, $3F, $FF, $FF, $C0, $1F, $FF,
    $FF, $00, $3F, $FF, $FE, $00, $FF, $FF,
    $F7, $01, $FF, $FF, $F0, $03, $FF, $FF,
    $F0, $03, $FF, $FF, $E0, $07, $FF, $FF,
    $C0, $07, $FF, $FF, $C0, $0F, $FF, $FF,
    $80, $0F, $FF, $FF, $80, $0F, $FF, $FF,
    $80, $07, $FF, $FF, $00, $07, $FF, $FF,
    $00, $03, $FF, $FF, $00, $00, $FF, $FF,
    $00, $00, $7F, $FF, $00, $00, $1F, $FF,
    $00, $00, $0F, $FF, $80, $00, $0F, $FF,
    $80, $00, $07, $FF, $80, $00, $07, $FF,
    $C0, $00, $07, $FF, $C0, $00, $0F, $FF,
    $E0, $00, $0F, $FF, $F0, $00, $1F, $FF,
    $F0, $00, $1F, $FF, $F8, $00, $3F, $FF,
    $FE, $00, $7F, $FF, $FF, $00, $FF, $FF,
    $FF, $C3, $FF, $FF, $FF, $FF, $FF, $FF
    );

  // Yin cursor XOR bitmask
  XORmaskCursor: array[0..127] of byte = (
    $00, $00, $00, $00, $00, $03, $C0, $00,
    $00, $3F, $00, $00, $00, $FE, $00, $00,
    $0E, $FC, $00, $00, $07, $F8, $00, $00,
    $07, $F8, $00, $00, $0F, $F0, $00, $00,
    $1F, $F0, $00, $00, $1F, $E0, $00, $00,
    $3F, $E0, $00, $00, $3F, $E0, $00, $00,
    $3F, $F0, $00, $00, $7F, $F0, $00, $00,
    $7F, $F8, $00, $00, $7F, $FC, $00, $00,
    $7F, $FF, $00, $00, $7F, $FF, $80, $00,
    $7F, $FF, $E0, $00, $3F, $FF, $E0, $00,
    $3F, $C7, $F0, $00, $3F, $83, $F0, $00,
    $1F, $83, $F0, $00, $1F, $83, $E0, $00,
    $0F, $C7, $E0, $00, $07, $FF, $C0, $00,
    $07, $FF, $C0, $00, $01, $FF, $80, $00,
    $00, $FF, $00, $00, $00, $3C, $00, $00,
    $00, $00, $00, $00, $00, $00, $00, $00
    );    

then create the cursor

hCur := CreateCursor(HInstance, 19, 2, 32, 32, @ANDmaskCursor, @XORmaskCursor);
  
For an example see the attached sample.

2008. január 20., vasárnap

Accessing a TForm from a package


Problem/Question/Abstract:

I have a package which contains a form and some units. I want to be able to load that form into a main application but I have no idea how to do it does anyone have some example code I can look at?

Answer:

There are two ways to use packages: statically bound or dynamically loaded. This is similar to DLLs. To bind a package statically to your project you name it in the projects package list and build the project with run-time packages.
To use something from the package you simply add the unit that contains the something to a uses clause as if it were part of the project. The linker takes care of making sure the unit is not compiled into the project but linked in from the package at run-time. In this scenario the code using the package is completely unaware that the stuff it uses comes from a package.

Dynamically loaded packages are somewhat more complex to use. The following is an excerpt from an older reply on how to put MDI children into packages. Note that you need to build the host app with run-time packages, you just use only the standard set of packages, do not name the packages you want to load dynamically in the package list.

Quote:

If you load them dynamically via LoadPackage the main program typically gets a list of available child form packages from somewhere, e.g. from an INI file, a registry key, or simply by scanning a folder at a known location for BPL files. Each package is required to export a set of functions with known names (perhaps only one). You export functions from a package the same way you do it from a DLL, via an exports clause. And you get these entry points from your main program also the same way you do it for DLLs, via GetProcAddress. So each child form package would export a function

function CreateChild: TForm;

The main app can now create child forms just by calling this function for each childform package. Whether it uses the returned reference is up to the program, it can get references to the child forms from mainform.MDIChildren if it needs them. Each child form in turn is required to implement a specific interface which the main form can use to communicate with it. This interface can be message-based, or it can be an actual Interface (non-COM) which the main form can obtain by sending a specific user message to the child form. This way the main form needs to know no details about the child forms, so has no need to Use the child forms unit.

If you don't load the packages dynamically you can still remove the dependence on the child form units completely. In this case the CreateChild method would be something the package exports in a unit (one unit per package, it may contain only this one function) and the main form would Use this unit from each of the packages. This is of course not very maintenance-friendly, there are other ways to manage this, e.g. by using a central registration mechanism (implemented in a common package) into which each of the child packages can register its CreateChild function together with the package name. The main form could then ask this registry to create a child for a given package. If the main form reads the list of packages from some file this would make your package set extensible without requiring any change to the main form when you add a new one.

2008. január 19., szombat

Change the project default directory


Problem/Question/Abstract:

Change the project default directory

Answer:

The project default directory is

\Borland\Delphi 3.0\Bin

which annoys me regularly.

This setting can be changed via right-click on the Delphi program icon:
"Properties" -> page "Shortcut", field "Start in:"

Here you may enter your default - project directory.

2008. január 18., péntek

How to get the control identifier of Windows dialogs


Problem/Question/Abstract:

How can I get the identifier of a given control, which is part of a predefined windows dialog such as open/ save dialogs? I need to alter text in the edit control and/ or alter captions on buttons before the dialog closes. Can this be accomplished with API calls and/ or messages to the dialog in question?

Answer:

With WinSight for example. Anyway, there are some of them (Open/ Save dialog controls ID's):

"Look in" - ID 1137 , classname "ComboBox"
"Files of type" - ID 1136 , classname "ComboBox"
"File Name" - ID 1152 , classname "Edit"
"Help" - ID 1038 , classname "Button"
"Open" - ID 1 , classname "Button"
"Cancel" - ID 2 , classname "Button"
"Open as Read only" - ID 1040 , classname "Button"

The usage - for example, to alert the text of "File name" Edit - is like this:

{ ... }
var
  MyText: string;
begin
  MyText := 'Hello!';
  SendMessage(Windows.GetParent(Self.Handle), CDM_SETCONTROLTEXT,
    1152, Integer(PChar(MyText)));
  { ... }

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

How to execute a console application from a Delphi program


Problem/Question/Abstract:

How to execute a console application from a Delphi program

Answer:

This executes a console application, but if your application is not a console, just remove CREATE_NEW_CONSOLE.

function RunApp(const aCmd: string; aWait: boolean; aShowMode: integer): DWORD;
var
  StartUpInfo: TStartUpInfo;
  ProcessInfo: TProcessInformation;
  WaitCode: DWORD;
begin
  Result := 0;
  ZeroMemory(@StartupInfo, SizeOf(TStartupInfo));
  StartUpInfo.cb := SizeOf(StartUpInfo);
  StartUpInfo.wShowWindow := aShowMode;
  StartUpInfo.dwFlags := STARTF_USESHOWWINDOW;
  ZeroMemory(@ProcessInfo, SizeOf(TProcessInformation));
  Win32Check((CreateProcess(nil, PChar(aCmd), nil, nil, False, CREATE_NEW_CONSOLE +
    NORMAL_PRIORITY_CLASS, nil, nil, StartUpInfo, ProcessInfo)));
  try
    if aWait then
    begin
      repeat
        WaitCode := WaitForSingleObject(ProcessInfo.hProcess, 10000);
        Win32Check(WaitCode < > WAIT_FAILED);
        if WaitCode = WAIT_TIMEOUT then
        begin
          if MessageDlg('This is a test', mtWarning, [mbYes, mbNo], 0) < > mrYes then
            Break;
        end
        else
          Break;
      until
        False;
      Win32Check(GetExitCodeProcess(ProcessInfo.hProcess, Result));
    end;
  finally
    CloseHandle(ProcessInfo.hThread);
    CloseHandle(ProcessInfo.hProcess);
  end;
end;

2008. január 16., szerda

How to create user-defined messages in a TThread


Problem/Question/Abstract:

I try to build a thread that I can send a message to order to stop. I know that messages are normally used for screen object but the thread is also having a handle. I 'd like to be able to send a message to this thread and having the sender waiting until the stop is confirmed. (or something that's equivalent)

Answer:

A thread has a handle, but it is not a window handle, so you cannot send a message to it with SendMessage. There is a PostThreadMessage API function that can be used to send a message to the thread itself. But to receive it the thread needs a message loop, which threads normally don't have.

If your thread is permanently slaving away in a work loop and you want to stop it just set a boolean field declared in the thread object to true (this is what Thread.Terminate does, for example). The work code inside the thread has to check this field regularly to detect that it has been set, and then exit the loop.

If the thread is waiting on something and you want to wake it up you have to modify the wait code so that it uses WaitforMultipleObjects, one of which is an event object you can signal from outside to wake the thread up.

Here is an example for this technique:


{Writing an interruptible timer thread}

unit Unit1;

interface

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

type
  TTimerThread = class;
  TWakeupKind = (wkTimerExpired, wkEventTriggered);
  TWaitState = (wsIdle, wsWaiting);
  TWakeupEvent = procedure(sender: TTimerThread; reason: TWakeupKind) of object;
  TTimerThread = class(TThread)
  private
    FInterval: DWORD;
    FReason: TWakeupKind;
    FEvent: THandle;
    FState: TwaitState;
    FWakeupEvent: TWakeupEvent;
    FNoWakeupEvent: Boolean;
    procedure SyncWakeup;
  protected
    procedure DoWakeup;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    procedure Execute; override;
    procedure Sleep(forInterval: DWORD);
    procedure Wakeup;
    procedure Terminate;
    property OnWakeup: TWakeupEvent read FWakeupEvent write FWakeupEvent;
    property Interval: DWORD read FInterval write FInterval;
    property State: TWaitState read FState;
  end; {TTimerThread}

  TForm1 = class(TForm)
    StatusBar: TStatusBar;
    WaitButton: TButton;
    OpenDialog1: TOpenDialog;
    Label1: TLabel;
    WaitIntervalEdit: TEdit;
    WakeupButton: TButton;
    Memo1: TMemo;
    procedure WaitIntervalEditKeyPress(Sender: TObject; var Key: Char);
    procedure WaitButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure WakeupButtonClick(Sender: TObject);
  private
    { Private declarations }
    FTimerthread: TTimerThread;
    procedure TimerWakeup(sender: TTimerThread; reason: TWakeupKind);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses typinfo;

{$R *.DFM}

procedure TForm1.WaitIntervalEditKeyPress(Sender: TObject; var Key: Char);
begin
  if not (key in ['0'..'9', #8]) then
    Key := #0;
end;

procedure TForm1.WaitButtonClick(Sender: TObject);
begin
  FTimerThread.Sleep(StrToInt(WaitIntervalEdit.Text));
  memo1.lines.add('Timer started');
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FTimerthread := TTimerThread.Create;
  FTimerthread.FreeOnTerminate := true;
  FTimerthread.OnWakeup := TimerWakeup;
end;

procedure TForm1.TimerWakeup(sender: TTimerThread; reason: TWakeupKind);
begin
  memo1.lines.add('Timer woke up, reason: ' + GetEnumName(Typeinfo(TWakeupKind),
    Ord(reason)));
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  if Assigned(FTimerthread) then
    FTimerThread.Terminate;
end;

procedure TForm1.WakeupButtonClick(Sender: TObject);
begin
  FTimerthread.Wakeup;
end;

{ TTimerThread }

constructor TTimerThread.Create;
begin
  {create thread suspended}
  inherited Create(true);
  {create event object}
  FEvent := CreateEvent(
    nil, {use default security}
    true, {event will be manually reset}
    false, {event starts out not signaled}
    nil); {event has no name}
  if FEvent = 0 then
    raise Exception.CreateFmt('TTimerThread.Create: could not create API event
      handle. '#13#10' %s', [ Syserrormessage( GetLastError ) ] );
      {thread will stay suspended until started by a Sleep or Resume call}
      FState := wsIdle;
      FNoWakeupEvent := False;
end;

destructor TTimerThread.Destroy;
begin
  inherited;
  if FEvent <> 0 then
    CloseHandle(FEvent);
end;

procedure TTimerThread.DoWakeup;
begin
  {called in threads context to fire OnWakeup event}
  if Assigned(FWakeupEvent) and not FNoWakeupEvent then
    Synchronize(SyncWakeup);
end;

procedure TTimerThread.Execute;
var
  res: DWORD;
begin
  {Executes inside threads context}
  repeat
    Fstate := wsWaiting;
    res := WaitForSingleObject(FEvent, FInterval);
    if res = WAIT_OBJECT_0 then
    begin
      FReason := wkEventTriggered;
      ResetEvent(FEvent);
    end
    else
      FReason := wkTimerExpired;
    DoWakeup;
    if not Terminated then
    begin
      Fstate := wsIdle;
      Suspend;
    end;
  until
    Terminated;
end;

procedure TTimerThread.Sleep(forInterval: DWORD);
begin
  {called from outside threads context to start thread sleeping}
  Interval := forInterval;
  if State <> wsIdle then
  begin
    {thread is already waiting. Wake it up but disable wakeup event}
    FNoWakeupEvent := true;
    try
      Wakeup;
      while State = wsWaiting do
        Windows.Sleep(10);
    finally
      FNoWakeupEvent := false;
    end;
  end;
  Resume;
end;

procedure TTimerThread.SyncWakeup;
begin
  {executes in main threads context}
  {Note: FWakeupevent has already been checked to be <> nil in DoWakeup}
  FWakeupEvent(self, FReason);
end;

procedure TTimerThread.Terminate;
begin
  inherited Terminate;
  {in case thread is waiting, don't fire Wakeup event on wakeup}
  FNoWakeupEvent := true;
  Wakeup;
end;

procedure TTimerThread.Wakeup;
begin
  {executes in callers thread context}
  if State = wsWaiting then
    SetEvent(FEvent);
end;

end.

2008. január 15., kedd

How to blend two TBitmap's (3)


Problem/Question/Abstract:

I was wondering how I could overlay/ transpose an image over another while taking account the transparent background. Basically, what I want to do is to allow my client to place a transparent logo (an overlay image) unto their other image. Each time I tried to do that, I keep getting the rectangular background.

Answer:

Here is some code to merge two bitmaps. It is not exactly what you are looking for, but it is a place to start. You would need to check for the transparency color and skip those pixels and only operate on the rect of the smaller bitmap.

{ ... }
MaxPixelCount = 65536;

type
  TRGBArray = array[0..MaxPixelCount - 1] of TRGBTriple;
  pRGBArray = ^TRGBArray;

function MergeBitmap(const BitmapA: TBitmap; const WeightA: Cardinal;
  const BitmapB: TBitmap; const WeightB: Cardinal): TBitmap;
var
  i, j: Integer;
  RowA: pRGBArray;
  RowB: pRGBArray;
  RowTween: pRGBArray;
  SumWeights: Cardinal;

  function WeightPixels(const pixelA, pixelB: Cardinal): Byte;
  begin
    Result := Byte((WeightA * pixelA + WeightB * pixelB) div SumWeights)
  end;

begin
  SumWeights := WeightA + WeightB;
  Result := TBitmap.Create;
  Result.Width := BitmapA.Width;
  Result.Height := BitmapA.Height;
  Result.PixelFormat := pf24bit;
  if WeightA = 0 then
    Result.Canvas.Draw(0, 0, BitmapB)
  else if WeightB = 0 then
    Result.Canvas.Draw(0, 0, BitmapA)
  else if SumWeights > 0 then
  begin
    for j := 0 to Result.Height - 1 do
    begin
      RowA := BitmapA.Scanline[j];
      RowB := BitmapB.Scanline[j];
      RowTween := Result.Scanline[j];
      for i := 0 to Result.Width - 1 do
      begin
        with RowTween[i] do
        begin
          rgbtRed := WeightPixels(rowA[i].rgbtRed, rowB[i].rgbtRed);
          rgbtGreen := WeightPixels(rowA[i].rgbtGreen, rowB[i].rgbtGreen);
          rgbtBlue := WeightPixels(rowA[i].rgbtBlue, rowB[i].rgbtBlue)
        end;
      end;
    end;
  end;
end;

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)