2003. december 31., szerda

Published Objects in Components


Problem/Question/Abstract:

Some properties of a component can 'drop-down' to reveal other properties (such as a Font property reveals various properties within itself). These are objects within the component, and a simple demonstration of how to add you own new 'drop-down' properties is given here. (You can also get a Static Analogue Clock Component too!!!)

Answer:

Please Note: I will only include here pertinent aspects of what is being explained. I will not flesh out all the examples for the sake of clarity.

To include an object within a component is a fairly simple matter, simply declare a field, and make a property public.

interface

type
  TMyComp = class(TComponent)
  private
    fFont: TFont;
  public
    property Font: TFont read fFont write fFont;
    constructor create(AOwner: TComponent); override;
    destructor destroy; override;
  end;

implementation

constructor TMyComp.create(AOwner: TComponent);
begin
  inherited create(AOwner);
  fFont := TFont.Create; //create the Object into the field reference,
  //  so it will not raise an exception
end;

destructor TMyComp.destroy;
begin
  fFont.free; //free the field to avoid memory leaks, etc.
  inherited;
end;

NB: When creating Objects, ALWAYS remember to free them, unless a help file tells you overwise (happens very rarely, eg exception handlers). Notice that what is created in the constructor is explicitly freed in the destructor.

This creates a fairly useless component admittedly, but it is an example after all! When accessing the Font property, it can be referenced in code using:

begin
  with MyComp1.Font do
  begin
    Color := clBlue;
    Size := 10;
  end;
end;

This is all well and good, but what about the Object Inspector?
If we move the property from public to published, the Font property is now available, with the plus sign to 'drop-down' as required.

This is a step in the right direction.

However, this is not the whole story. What if we were devising a component which could logically take completely new objects as properties. For instance an analogue clock face - three similar objects would be obvious.. the hour, minute and second hands! Each is the same, save for customisable features, such as colour, thickness, etc.

So - let us construst our AnalogueHand object:

type
  TAnalogueHand = class
    Colour: TColor;
    Thickness: integer;
  end;

Here is an object, descended from TObject, which has the properties we require.

Let us put it into a Clock face component:

type
  TAnalogueClock = class(TGraphicControl)
  private
    fHourHand, fMinuteHand, FSecHand: TAnalogueHand;
  protected
    procedure SetHand(index: integer; value: TAnalogueHand);
  public
    constructor create(AOwner: TComponent); override;
    destructor destroy; override;
  published
    property HourHand: TAnalogueHand index 0 read fHourHand write SetHand;
    property MinuteHand: TAnalogueHand index 1 read fMinuteHand write SetHand;
    property SecHand: TAnalogueHand index 2 read fSecHand write SetHand;
  end;

In the constructor, each field must be created separately, and freed on destruction:

constructor TAnalogueClock.create(AOwner: TComponent);
begin
  inherited create(AOwner);
  //Set up the Hand Objects
  fHourHand := TAnalogueHand.create;
  with fHourHand do
  begin
    colour := clBlue;
    Thickness := 2;
  end;
  fMinuteHand := TAnalogueHand.create;
  with fMinuteHand do
  begin
    colour := clRed;
    Thickness := 2;
  end;
  fSecHand := TAnalogueHand.create;
  with fSecHand do
  begin
    colour := clRed;
    Thickness := 1;
  end;
end;

destructor TAnalogueClock.destroy;
begin
  fSecHand.free;
  fMinuteHand.free;
  fHourHand.free;
  inherited;
end;

procedure TAnalogueClock.SetHand(index: integer; value: TAnalogueHand);
begin
  case index of
    0: fHourHand := Value;
    1: fMinuteHand := Value;
    2: fSecHand := Value;
  end;
  invalidate;
end;

Notice that the Hands are written to all using the same procedure, SetHand, each with a different index to refer to it.

If we install this, we end up with our object, but the object inspector gives an Access Violation if we try to view the properties - not what we wanted!

The reason being that to descend our Hand Object from TObject is the wrong ancestor.. For objects which are of a temporary nature, this is fine, but to allow properties to exist abit longer, to have their properties stored in a persistent fashion (put very simply!) - we must descend from TPersistent.

So, our new hand declaration looks like:

type
  TAnalogueHand = class(TPersistent)
    Colour: TColor;
    Thickness: integer;
  end;

Rebuild, and the Access Violation has gone - hooray!! But, there are no subproperties!! An inspection of the Hand object could provide a clue.. With a standard component, for a property to appear in the object inspector, it must be published:

type
  TAnalogueHand = class(TPersistent)
  private
    fColour: TColor;
    fThickness: integer;
  published
    property Colour: TColor read fColour write fColour;
    property Thickness: integer read fThickness write fThickness;
  end;

Rebuild again - and we have subproperties within properties, droppong down without Access Violations, etc.

At runtime the new subproperties can be accessed by:

with AnalagueClock1.HourHand do
begin
  Colour := clOlive;
  Thickness := 4;
end;

AnalagueClock1.SecHand.Colour := clFuchsia;

This has been a quick and simple overview to providing subproperties in a component. More complicated user defined objects can be created, which may have further subproperties (try publishing a TCanvas Object, and see how many layers you get..).

In summary:

descend your new object from TPersistant (if it is COMPLETELY new - as in  the example);
ensure that any methods declared in the object are written (such as constructors, setting procedures, functions, etc). - I've forgotten this a few times!!
use the standard of fields and published properties (and any public as required). The published properties will appear as subproperties.
ensure that when the new object is contained within a component that it is explicitly created and freed at the appropriate times.

This worked example appears in an expanded form in the component attached to this article. I had a requirement for a Clock face, but I needed it to be static - for inputting. All the Clock faces I found were very nice, but the darn things moved!! So I created my own static analogue clock face.

I make no apology for using British English within the component! Light diffractions have a 'U' (coloUr), and the free floating state contrary to digital has a 'UE' suffix (analogUE). If you don't like it - you have the source!!


Component Download: AnalogueClock.zip

2003. december 30., kedd

Retrieve list of exported functions from a DLL


Problem/Question/Abstract:

Retrieve list of exported functions from a DLL

Answer:

To retrieve the list of exported functions from a DLL, pass the DLL name and a TStrings object to the function ListDLLFunctions() shown below.

This does not show the parameters for each export, which you can only get from the author of the DLL.


program Project1;

uses
  Forms,
  Classes,
  SysUtils,
  Dialogs,
  ImageHlp, // routines to access debug information
  Windows;


procedure ListDLLFunctions(DLLName: string; List: TStrings);
type
  chararr = array[0..$FFFFFF] of Char;
var
  H: THandle;
  I, fc: integer;
  st: string;
  arr: Pointer;
  ImageDebugInformation: PImageDebugInformation;
begin
  List.Clear;
  DLLName := ExpandFileName(DLLName);
  if FileExists(DLLName) then
  begin
    H := CreateFile(PChar(DLLName), GENERIC_READ, FILE_SHARE_READ or
      FILE_SHARE_WRITE, nil, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    if H <> INVALID_HANDLE_VALUE then
    try
      ImageDebugInformation := MapDebugInformation(H, PChar(DLLName), nil, 0);
      if ImageDebugInformation <> nil then
      try
        arr := ImageDebugInformation^.ExportedNames;
        fc := 0;
        for I := 0 to ImageDebugInformation^.ExportedNamesSize - 1 do
          if chararr(arr^)[I] = #0 then
          begin
            st := PChar(@chararr(arr^)[fc]);
            if Length(st) > 0 then
              List.Add(st);
            if (I > 0) and (chararr(arr^)[I - 1] = #0) then
              Break;
            fc := I + 1
          end
      finally
        UnmapDebugInformation(ImageDebugInformation)
      end
    finally
      CloseHandle(H)
    end
  end
end;

// the following is an example how to use the procedure
var
  List: TStrings;
  I: integer;
  S: string;

begin
  List := TStringList.Create;

  ListDLLFunctions('c:\winnt\system32\mfc42.dll', List);

  S := 'List of functions';
  for I := 0 to List.Count - 1 do
    S := S + #13#10 + List[I];
  ShowMessage(S);
  List.Free
end.

2003. december 29., hétfő

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


Problem/Question/Abstract:

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

Answer:

File, n, 1. A metal tool with numerous small cutting ridges or teeth on its surface, for smoothing or cutting metal and other substances. 2. A cabinet in which papers, etc., are arranged or classified for convenient reference. 3. Computers, a portion of a memory storage device allocated to a set of data.

If you want explanations of the first two definitions, sorry you will have to look elsewhere. This article discusses the third definition, although of course a computer file is analogous to the cabinet of the second definition.

This article discusses what a file is by looking at the origins of files; at least in so far as they have been implemented on PCs.

Outline of this article:

Disk format of a file
File contents, part I
Language conventions and ASCII
Language conventions and machine code
File contents, part II
Delphi and files
Which file types should you use?

Disk format of a file

A file is a portion of a disk (or equivalent device) allocated to a set of data and referred to by a file name. With FAT file systems, disk space is allocated in blocks of (for example) 256, 1024, or 4096 bytes, depending on the capacity of the disk. A disk with 512-byte blocks will therefore allocate 512 bytes of storage for any file up to that size. If you create a file that only requires 6 bytes, 512 bytes will be allocated. If you create a file 513 bytes long, 1024 bytes (two blocks) will be allocated. So how does the operating system know the actual size of the file? Each disk keeps a directory of files. The entries for each file include the name of the file, the date and time the file was last written to, and the size of the file. It is an enhanced version of this directory that Windows Explorer presents in the 'Files' pane. This system has been around since the very first version of MSDOS, and in fact was based on an even earlier operating system, known as CP/M.

File contents, part I

So what goes into a file? Anything, actually. Files are sequences of bytes. A byte is of course 8 bits where each bit can, by definition, have one of two values, which can be represented as on and off, zero and one, or any other representation desired. Normally bits are represented by the numeric digits 0 and 1, and eight identical bits therefore can be represented as 00000000 or 11111111.

These are binary numbers, but are not convenient for most purposes, so are often combined into groups of four bits. Because a group of four bits can have any of 16 different values, these are normally represented by the ten numeric digits and the first six letters of the alphabet. This is known as hexadecimal. Of course decimal numbers can also represent 16 different values.

The table at below shows these 16 different values represented as binary, hexadecimal, and decimal.

Binary
Hexadecimal
Decimal
0000
0
00
0001
1
01
0010
2
02
0011
3
03
0100
4
04
0101
5
05
0110
6
06
0111
7
07
1000
8
08
1001
9
09
1010
A
10
1011
B
11
1100
C
12
1101
D
13
1110
E
14
1111
F
15


As a byte is eight bits, two hexadecimal digits are used to represent the value, giving values from 00 to FF, equivalent to 00000000 to 11111111 in binary or 0 to 255 in decimal.  Delphi distinguishes hexadecimal from other numbers by the dollar sign at the start of the number, thus 40 is a decimal number whereas $40 is hexadecimal number (equivalent to 64 in decimal). But of course files don't just contain numbers, do they. They can contain text, pictures, etc., as well. How do they do this? The answer lies in what can be termed language conventions.

Language conventions and ASCII

What does the sequence of letters 'c', 'a', and 't' mean? To English-speaking people, it is a furry pet with claws. The letters themselves have no inherent meaning, but English speakers agree to apply a particular meaning to that particular sequence of letters. Similarly, the sequence 'g', 'i', 'f', and 't' mean a present. But to German-speaking people that sequence means a poison. The same sequence of letters can mean different things to different people, and in fact any sequence of letters or other symbols can mean anything at all, as long as the writers and readers all understand the meaning.

English and similar languages use 26 letters, Morse code uses two, and DNA uses four. In the 1960s a language convention was adopted for computer data, known as ASCII (American Standard Code for Information Interchange). This convention allocated meanings to the first 128 of the 256 values a byte can have. There were already other conventions in use, and others again have modified or superseded ASCII, but ASCII was adopted by personal computers when they appeared and so it became quite widespread.

Under the ASCII standard or language convention, the value 01000001/$41/65 was given the job of representing the capital letter 'A'. Thus a file that contained the bytes $43, $41, and $54 will, if loaded into WordPad, display as 'CAT'. This is not because the file contains the word 'CAT', but because WordPad understands the bytes to represent the letters 'C', 'A', and 'T'. A different application may understand the same bytes differently. So if ASCII uses 128 different values and English only has 26 letters, what are all the others for?

Well English actually uses more than just the 26 letters. It uses both capital and lower-case letters, a space to separate words, and there other symbols to help with clarity, such as commas, full stops, question marks, etc. ASCII uses 95 of the values to represent the ten numeric digits, 26 capital letters, 26 lower case letters, various punctuation marks, the space character, and miscellaneous other symbols such as the dollar sign and '@' symbol. ASCII also defines 32 control characters. These were originally designed for data transmission and similar where specified values indicate the start and end of transmission, etc. Thus value 3 was ETX (End of Text) and 4 was EOT (End of Transmission). $A (10) is LF (Linefeed) and $C (13) is CR (Carriage Return). Most of these control characters (values $0 to $19) are not used as such in PCs. IBM also decided to allocate the remaining 128 characters (values $80 to $FF) to various mathematical symbols and foreign-language characters, but these are not part of the ASCII standard and under Windows different typefaces may allocate different symbols to these values.

Language conventions and machine code

There is another important language convention used on IBM-type PCs. This is the language convention of the processor itself. To the processor, the value $41 is not the letter 'A', but the instruction inc ecx (increment the ecx register). The processor understands the byte values to be instructions to perform, and these have no connection with the ASCII code at all. Thus the same "letters" can represent two or more completely unrelated ideas, just as gift means something totally different in English and German. File contents, part II So computer files contain sequences of bytes which may represent ASCII characters or machine code or something else altogether. So how does the Operating System (OS) know what the values represent? In a sense, it doesn't. It really doesn't matter to the OS what a file contains. A file is ANY sequence of byte values. If all it is asked to do is to copy, move, or delete the file, the contents don't matter at all.

If Explorer is told to open a file, it looks up a list (based on the filename extension) to see which application to pass the file to, starts that application, and passes the file to it. It has no idea whether the file actually contains what the application expects it to contain. About the only time the file contents matter to the OS (apart from its own files) is when it is asked to run the file as a program. In this case it will check to see if the filename extension is an appropriate one (.exe, .com, etc.), but in most cases it also checks the contents of the file to see if they have certain signature values.

Early .exe programs, for example, had to start with the bytes $4D and $58. These bytes did not represent machine code, but were an indication (by yet another convention) that the file was a program. (The values $4D and $58 were arbitrarily chosen as in ASCII they represent the letters 'MZ', reputedly the initials of the programmer who designed the .exe file format!) In CP/M days, files were saved in 128-byte blocks with no record of the exact file size. The actual end of a text file was marked with a byte with a (decimal) value of 26 (also known as Ctrl-Z).

Delphi file types

Delphi provides several methods for handling files, including wrappers for Windows' own file-handling methods. I will not cover the latter here. Delphi categorises files as untyped, typed, and text. The most basic is the untyped file, with which Delphi treats the file merely as a sequence of byte values. This essentially is what is done with the following procedure, which makes a copy of a file.

procedure�CopyFile(fromName, toName: string);
var
  � infile, outfile: file;
  � buffer: pointer;
  � fs: integer;
begin
  ��assignFile(infile, �fromName);
   �reset(infile, 1);
  ��assignFile(outfile, �toName);
   �rewrite(outfile, 1);
  ��fs := FileSize(infile);
  ��getmem(buffer, fs);
  ��blockread(infile, buffer^, fs);
  ��blockwrite(outfile, buffer^, fs);
  ��CloseFile(infile);
  � CloseFile(outfile);
  ��Freemem(buffer, fs);
end;

This rather simple procedure reads the entire contents of the file into the memory allocated to buffer then writes the same data to a new file. It assumes nothing about the contents of the file. Actually, for historical reasons (probably traceable back to the CP/M file record-size), Delphi assumes that an untyped file is composed of blocks of 128 bytes unless you specify a different size in the reset and rewrite procedures. In the code above, we have specified record sizes of one byte, then told Delphi to read and write fs "records". Unless you have a special reason for not doing so, you should always specify a record size of one byte when using untyped files. With a typed file, you tell Delphi what the file contains. This may be sequences of bytes, words, booleans, or a user-defined type such as a record. This last one is often referred to as a file of record. The following procedure also copies a file, but tells Delphi that the file contains MyRecord records.

type
  MyRecord� = �packed�record
    ��Surname: string[20];
      ChristianName: string[20];
      Birthdate: TDate;
end;
��� {MyRecord}

procedure�CopyFile(FromName, ToName);
var
   InFile, �OutFile: file�of�MyRecord;
  �Rec: MyRecord;
begin
  �AssignFile(InFile, �FromName);
  �reset(InFile);
  �AssignFile(OutFile, �ToName);
  �rewrite(OutFile);
   while�not�eof(InFile)�do�begin
    ���read(InFile, �rec);
  �����write(OutFile, �rec);
  �end;
  �{while}
  �CloseFile(InFile);
  �CloseFile(OutFile);
end;

Delphi knows that a MyRecord type occupies 50 bytes (21 for each string field and eight for the Birthdate field), so reads in and writes out 50 bytes at a time. If the file is not a multiple of 50 bytes, an error will occur when the end of the file is reached in the middle of reading a record. The following code does the same thing but uses an untyped file (it uses the same MyRecord as the previous example):

procedure�CopyFile(FromName, ToName);
var
  InFile, OutFile: file; {untyped file this time}
  Rec: MyRecord;
begin
  {specify &#8220;records&#8221; of the length of MyRecord}
  AssignFile(InFile, �FromName);
  reset(InFile, sizeof(MyRecord));
  AssignFile(OutFile, �ToName);
  rewrite(OutFile, sizeof(MyRecord));
  while�not�eof(InFile)�do�begin
    BlockRead(InFile, �rec, �1);
    {read one record}
    BlockWrite(OutFile, �rec, �1); {write one record}
  end;
  {while}
  CloseFile(InFile);
  CloseFile(OutFile);
end;

The remaining file type that Delphi understands is TextFile. This indicates to Delphi that the file contains bytes conforming to the ASCII language convention, although it will accept non-ASCII characters, i.e. characters in the range $80 to $FF. Particularly, it does assume that the file contains lines of text separated by CR (Carriage Return) characters, possibly followed by LF (Line Feed) characters. The following procedure copies a text file:

procedure�CopyFile(FromName,ToName);
var
  InFile, OutFile: textfile;
  S: string;
begin
��AssignFile(InFile,�FromName);�reset(InFile);
��AssignFile(OutFile,�ToName);�rewrite(OutFile,sizeof(MyRecord));
��while�not�eof(InFile)�do�begin
����Readln(InFile,�s); {read an entire line up to a CR character.  
                                                                                        The CR (and LF) is skipped.}
����Writeln(OutFile,�s);�{write a line and append CR and LF}
��end;���{while}
��CloseFile(InFile);
��CloseFile(OutFile);
end;

A text file gives you other options. One option is to read and write partial lines (use Read and Write instead of ReadLn and WriteLn). Another is to automatically convert certain ASCII sequences to their numerical equivalents. For example, given "i" being declared as a byte, if the file contains the string '123 ', read(InFile, i) will convert the string into the numeric value $7B (123 in decimal). Delphi also defines the TiniFile object which assumes that the file is a text file conforming to the layout of a Windows .ini file, wherein most lines are of the form <keyname>=<value>. Additionally the TStrings type has methods for reading and writing text files. Then there are database files, which are beyond the scope of this article (because I haven't used them and don't know much about them!).

2003. december 28., vasárnap

How to read binary values from the registry


Problem/Question/Abstract:

I want to read out the binary value "problems" of the path HKEY_DYN_DATA\Config Manager\Enum\[add the key of a hardware component] to detect if a hardware component is troubled and not working right. But I cannot handle the ReadBinaryData-Method of TRegistry correct. Everytime I use it, it always returns "4" as content of the buffer. How do I detect if the content of the binary key "problems" is not "00 00 00 00" but something else like "16 00 00 00" or such?

Answer:

Here's an example of ReadBinaryData:


procedure TFrmReadBinary.Button1Click(Sender: TObject);
const
  CKeyName: string = 'System\Setup';
  CValName: string = 'NetcardDlls';
var
  keyGood: boolean;
  p: integer;
  regKey: TRegistry;
  tmpStr: string;
  vSize: integer;
begin
  regKey := TRegistry.Create;
  try
    regKey.RootKey := HKEY_LOCAL_MACHINE;
    keyGood := regKey.OpenKey(CKeyName, false);
    if (keyGood) then
    begin
      vSize := regKey.GetDataSize(CValName);
      if (vSize > 0) then
      begin
        SetLength(tmpStr, vSize);
        regKey.ReadBinaryData(CValName, tmpstr[1], vSize);
        repeat
          p := Pos(#0, tmpStr);
          if p <> 0 then
          begin
            Delete(tmpStr, p, 1);
            Insert(#13#10, tmpStr, p);
          end;
        until
          p = 0;
        {StringReplace(tmpStr, #0, #13#10, [rfReplaceAll]);}
        ListBox1.Items.Text := tmpStr;
      end;
    end;
  finally
    regKey.Free;
  end;
end;

2003. december 27., szombat

How to set the item index in a TRadioGroup without firing the OnClick event


Problem/Question/Abstract:

How to set the item index in a TRadioGroup without firing the OnClick event

Answer:

procedure SetRadioItem(radiogroup: TRadioGroup; index: Integer);
var
  ev: TNotifyEvent;
begin
  ev := radiogroup.OnClick;
  radiogroup.OnClick := nil;
  radiogroup.ItemIndex := index;
  radiogroup.Onclick := ev;
end;

A bit roundabout but it works. A checkbox could be treated similarly but I think you can also set its state by sending a BM_SETCHECK to it without having the OnClick event fire. This is untested:

procedure SetCheckbox(checkbox: TCheckbox; checked: Boolean);
const
  flags: array[boolean] of Integer = (BST_UNCHECKED, BST_CHECKED);
begin
  checkbox.Perform(BM_SETCHECK, flags[checked], 0);
end;

2003. december 26., péntek

Add a size grip to a TForm without using a status bar


Problem/Question/Abstract:

How to add a size grip to a TForm without using a status bar

Answer:

A size grip appears on a form in two cases: when a status bar is placed at the bottom of the form or when the form has both a horizontal and a vertical scrollbar. To place a size grip on a form without any of the above, you need to draw it yourself and handle mouse events. The following unit demonstrates drawing a size grip at the bottom right corner (including XP style, if supported):

unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    procedure FormPaint(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    FSizeGripWidth: Integer;
    FSizeGripHeight: Integer;
    FSizeGripRect: TRect;
    procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses
  Themes;

{$R *.dfm}

procedure TForm1.FormPaint(Sender: TObject);
begin
  if ThemeServices.ThemesAvailable then
  begin
    ThemeServices.DrawElement(Canvas.Handle,
      ThemeServices.GetElementDetails(tsSizeBoxRightAlign), FSizeGripRect);
  end
  else
    DrawFrameControl(Canvas.Handle, FSizeGripRect, DFC_SCROLL, DFCS_SCROLLSIZEGRIP);
end;

procedure TForm1.FormResize(Sender: TObject);
begin
  FSizeGripRect := ClientRect;
  FSizeGripRect.Left := FSizeGripRect.Right - FSizeGripWidth;
  FSizeGripRect.Top := FSizeGripRect.Bottom - FSizeGripHeight;
  Refresh;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FSizeGripWidth := GetSystemMetrics(SM_CXVSCROLL);
  FSizeGripHeight := GetSystemMetrics(SM_CYHSCROLL);
end;

procedure TForm1.WMNCHitTest(var Message: TWMNCHitTest);
begin
  inherited;
  if PtInRect(FSizeGripRect, ScreenToClient(SmallPointToPoint(Message.Pos))) then
    Message.Result := HTBOTTOMRIGHT;
end;

end.

2003. december 25., csütörtök

Get a printer settings


Problem/Question/Abstract:

How can I receive a current printer settings?

Answer:

At first, you must open the printer device:

var
  FDevice: PChar;
  FDriver: PChar;
  FPort: PChar;
  DeviceMode: THandle;
  DevMode: PDeviceMode;

procedure OpenThePrinterDevice;
var
  Driver_Info2: PDriverInfo2;
  Retrieved: dword;
  hPrinter: THandle;
begin
  Printer().GetPrinter(FDevice, FDriver, FPort, DeviceMode);
  if DeviceMode = 0 then
    Printer().GetPrinter(FDevice, FDriver, FPort, DeviceMode);
  OpenPrinter(FDevice, hPrinter, nil);
  GetMem(Driver_Info2, 255);
  GetPrinterDriver(hPrinter, nil, 2, Driver_info_2, 255, Retrieved);
  StrLCopy(FDriver, PChar(ExtractFileName(StrPas(Driver_Info2^.PDriverPath))
    + #0), 63);
  FreeMem(Driver_info_2, 255);
  DevMode := GlobalLock(DeviceMode);
end;

And now you can get the information from printer. For example, the orientation settings:

if ((DevMode^.dmFields and DM_ORIENTATION) = DM_ORIENTATION) and
  (DevMode^.dmOrientation = DMORIENT_LANDSCAPE) then
  //Landscape
else
  //Portrait

Also you can detect a paper size, paper source, print duplex and quality etc. View a const description in Windows.pas.

At last, don't forget to unlock a device:

GlobalUnlock(DeviceMode);

2003. december 24., szerda

Description of Instancing and Threading Models in COM-servers


Problem/Question/Abstract:

How to select a correct value of Unstancing/Threading Model when I develop the Com object?

Answer:

When you create a new COM object, you must define the Instancing and Threading Model, which will be used in your COM object. For example, when you use the Automation Object Wizard (select File|New from main menu, click the ActiveX tab and select Automation Object Wizard item) you must select a desired values of Instancing/Threading Model from comboboxes.

In this article I'll include a short description for each value.

1. Instancing refers to how many instances of the COM object are created for a request of client. You can select a one option from three available items: Single Instance, Multiple Instance and Internal.

a) Single Instance - allows only a single COM interface for each executable. So creating multiple single instances results in creating multiple copies of the server application. This option is commonly used for multiple document interface (MDI) applications.

b) Multiple Instance - specifies that multiple applications can connect to the object. Any time a client requests service, a separete instance of the server gets invoked. That is, there can be multiple instances in a single executable. For example, any time a user attempts to open the Windows Explorer, a separate Explorer is created

c) Internal - means the object can only be created internally. An external application cannot create an instance of the object directly. For example, a word processor application may have an internal document object that can only be created by calling a method of the application that will create the document object.

2. The Threading Model refers to how your object is advertised to the client applications via its thread support. You can select one from the next options: Single, Apartment, Free or Both.

a) Single - no thread support. Client requests are serialized by the standard calling mechanism. serialized by the calling mechanism. With this threading model the clients are handled one at a time so no threading support is needed.

b) Apartment - different objects from the same server can be called on different threads or different clients, but each object is called only from that one thread. If two clients need to use the same object, they have to take turns. With this threading model the instance data is safe, global data must be protected using critical sections or some other form of serialization. Of course, the thread's local variables are reliable across multiple calls.

c) Free - clients can call any method of object from any thread at any time. Objects can handle any number of threads at any time. That is, more than one client can share the same object. Objects must protect all instance and global data using critical sections or some other form of serialization. Thread local variables are not reliable across multiple calls. Primarily used for distributed DCOM environments.

d) Both - objects can support clients that use either Aprtment or Free threading models. This threading model give a maximum performance and flexibility.

So a correct value selection of Instancing and Threading Model can affect on performance of your COM object and depends from task which you want realize in this object.

2003. december 23., kedd

Write sorting/search methods that can be re-used


Problem/Question/Abstract:

I find that alot of developers that use sorting and search algorithms, taking the Quick Sort algorithm for an example, will reimplement it for every use.

Answer:

Sorting algorithms rarely depend on actual knowledge what they are sorting, and when we require an algorithm and implement it, why restrict the algorithm to a specific use, as the algorithm itself will never change.

They are only dependent on an index of which they then need to compare and exchange the information that resides at those indexes.

The quick sort algorithm for the example would require only 3 main factors of which could be passed to a quick sort method.

Start and End indexes
Method for Comparing points
Method for Exchanging points

This going to apply for practially all sorting/searching algorithms.

All that is required is that we specify the types that will define the Compare and Exchange methods.

type
  TIndexCompare = function(const ixA, ixB: integer): integer of object;
  TIndexExchange = procedure(const ixA, ixB: integer) of object;
  //-- Also these methods could be also reused for multiple sort algorythms.
  //-- e.g
  //-- procedure InsertionSortByIndex(ixLo, ixHi: Integer;
  //--                                IndexCompare: TIndexCompare;
  //--                                IndexExchange: TIndexExchange);
  //--  etc....

procedure QuickSortByIndex(ixLo, ixHi: Integer;
  IndexCompare: TIndexCompare;
  IndexExchange: TIndexExchange);
implementation

procedure QuickSortByIndex(ixLo, ixHi: Integer;
  IndexCompare: TIndexCompare;
  IndexExchange: TIndexExchange);

  procedure SortIndex(aLo, aHi: Integer);
  var
    I, J, P: Integer;
    tmpInt: Integer;
  begin
    repeat
      I := aLo;
      J := aHi;
      P := (aLo + aHi) shr 1;
      repeat
        while (I < aHi) and (IndexCompare(I, P) < 0) do
          Inc(I);
        while (J > aLo) and (IndexCompare(J, P) > 0) do
          Dec(J);
        if I <= J then
        begin
          IndexExchange(I, J);
          if P = I then
            P := J
          else if P = J then
            P := I;
          Inc(i);
          Dec(j);
        end;
      until I > J;
      if aLo < J then
        SortIndex(aLo, J);
      aLo := I;
    until I >= aHi;
  end;

begin
  SortIndex(ixLo, ixHi);
end;

Now to use this..lets say i want to sort a listbox for the example(rather than using  the Listbox standard sorting)

type
  TMyForm = class(TForm)
  private
    ListBox1: TListBox;
    btnSort: TButton;
    .....
    public
    function IndexCompare(const ixA, ixB: integer): integer;
    procedure IndexExchange(const ixA, ixB: integer);
  end;
  ..

implementation

function TMyForm.IndexCompare(const ixA, ixB: integer): integer;
//-- Source to compare items.
begin
  Result := AnsiCompareText(ListBox1.Items[ixA], ListBox1.items[ixB]);
end;

procedure TMyForm.IndexExchange(const ixA, ixB: integer);
// -- Source to exchange items.
var
  tmpStr: string;
begin
  tmpStr := ListBox1.Items[ixA];
  ListBox1.Items[ixA] := ListBox1.Items[ixB];
  ListBox1.Items[ixB] := tmpStr;
end;

procedure TMyForm.btnSortClick(Sender: TObject);
begin
  with ListBox1.items do
  begin
    BeginUpdate;
    try
      if UseQuickSort then
        QuickSortByIndex(0, count - 1, IndexCompare, IndexExchange)
      else
        InsertionSortByIndex(0, count - 1, IndexCompare, IndexExchange);
    finally
      EndUpdate;
    end;
  end;
end;

//----

Well hopefully that might of been some use

Later All

2003. december 22., hétfő

Convert a decimal number string to a Base36 number string


Problem/Question/Abstract:

I have a 20 digit string, all numbers, and I would like to convert this to a Base36 to take up less space. I have tried the Borland Radix() routine but this would not work on such a large number. Does anyone have an idea on how to convert the decimal number string to aBase36 number string?

Answer:

Solve 1:

Does the encoding have to result in a string having only "printable" characters (#32..#126) or is any byte value allowed? If so an easy packing method not requiring any complex calculation would be BCD: pack two digits into a byte, giving a 50% size reduction:

function NumStringToBCD(const inStr: string): string;

  function Pack(ch1, ch2: Char): Char;
  begin
    Assert((ch1 >= '0') and (ch1 <= '9'));
    Assert((ch2 >= '0') and (ch2 <= '9'));
    {Ord('0') is $30, so we can just use the low nybble of the character as value.}
    Result := Chr((Ord(ch1) and $F) or ((Ord(ch2) and $F) shl 4))
  end;

var
  i: Integer;
begin
  if Odd(Length(inStr)) then
    Result := NumStringToBCD('0' + inStr)
  else
  begin
    SetLength(Result, Length(inStr) div 2);
    for i := 1 to Length(Result) do
      Result[i] := Pack(inStr[2 * i - 1], inStr[2 * i]);
  end;
end;

function BCDToNumString(const inStr: string): string;

  procedure UnPack(ch: Char; var ch1, ch2: Char);
  begin
    ch1 := Chr((Ord(ch) and $F) + $30);
    ch2 := Chr(((Ord(ch) shr 4) and $F) + $30);
    Assert((ch1 >= '0') and (ch1 <= '9'));
    Assert((ch2 >= '0') and (ch2 <= '9'));
  end;

var
  i: Integer;
begin
  SetLength(Result, Length(inStr) * 2);
  for i := 1 to Length(inStr) do
    UnPack(inStr[i], Result[2 * i - 1], Result[2 * i]);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  S1, S2: string;
begin
  S1 := '15151515151515151515';
  S2 := NumStringToBCD(S1);
  memo1.lines.add('S1: ' + S1);
  memo1.lines.add('Length(S2): ' + IntToStr(Length(S2)));
  memo1.lines.add('S2 unpacked again: ' + BCDToNumString(S2));
end;


Solve 2:

This DecimalStrToBase36Str seems to work on smaller inputs, but I suggest that you check output on the larger inputs.

{ ... }
const
  Base36Digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';

type
  tArrayElement = Byte;
  tDoubleElement = Word;
  {tArrayElement  = Word;}
  {tDoubleElement = LongWord;}

const
  SizeOfAryElem = SizeOf(tArrayElement);
  BitsInBufElem = SizeOfAryElem * 8;

function DecimalStrToBase36Str(const Value: string): string;
var
  Man: array[0..19] of tArrayElement;
  NbrManElem, Cry, i, j, n, Tmp: integer;
  Tmp1, Tmp2: packed record
    case byte of
      0: (Wd: tDoubleElement);
      1: (Lo, Hi: tArrayElement);
  end;
begin
  n := length(Value);
  if n <> 20 then
    raise Exception.CreateFmt('Input string must be 20 decimal digits, not %d digits',
      [n]);
  NbrManElem := 0;
  for i := 1 to n do
  begin
    Cry := ord(Value[i]) - ord('0');
    if (Cry < 0) or (Cry > 9) then
      raise Exception.CreateFmt('Input string contains non-decimal digit (%s)',
        [Value[i]]);
    {Multiply accumulation by 10 and add k:}
    for j := 0 to NbrManElem - 1 do
    begin
      Tmp := Man[j] * 10 + Cry;
      Man[j] := Tmp and $FF;
      Cry := Tmp shr 8;
    end;
    if Cry <> 0 then
    begin
      Inc(NbrManElem);
      Man[NbrManElem - 1] := Cry;
    end;
  end;
  SetLength(Result, 14);
  for i := 14 downto 1 do
  begin
    {Divide by 36 and save the remainder:}
    Tmp1.Hi := 0;
    for j := NbrManElem - 1 downto 0 do
    begin
      Tmp1.Lo := Man[j];
      Tmp2.Wd := Tmp1.Wd div 36;
      Assert(Tmp2.Hi = 0);
      Man[j] := Tmp2.Lo;
      Tmp1.Hi := Tmp1.Wd mod 36;
    end;
    Result[i] := Base36Digits[Tmp1.Hi + 1];
    if (NbrManElem > 0) and (Man[NbrManElem - 1] = 0) then
    begin
      dec(NbrManElem);
    end;
  end;
end;

2003. december 21., vasárnap

Starting and stopping Windows services


Problem/Question/Abstract:

Starting and stopping Windows services

Answer:

When installing certain types of programs it maybe necessary to restart related services to avoid conflicts. Regardless of the reason why you need to control services, following functions can help you with starting and stopping services running on the local computer or on a remote machine.

uses WinSvc;

//
// start service
//
// return TRUE if successful
//
// sMachine:
//   machine name, ie: \\SERVER
//   empty = local machine
//
// sService
//   service name, ie: Alerter
//

function ServiceStart(
  sMachine,
  sService: string): boolean;
var
  //
  // service control
  // manager handle
  schm,
    //
  // service handle
  schs: SC_Handle;
  //
  // service status
  ss: TServiceStatus;
  //
  // temp char pointer
  psTemp: PChar;
  //
  // check point
  dwChkP: DWord;
begin
  ss.dwCurrentState := -1;

  // connect to the service
  // control manager
  schm := OpenSCManager(
    PChar(sMachine),
    nil,
    SC_MANAGER_CONNECT);

  // if successful...
  if (schm > 0) then
  begin
    // open a handle to
    // the specified service
    schs := OpenService(
      schm,
      PChar(sService),
      // we want to
      // start the service and
      SERVICE_START or
      // query service status
      SERVICE_QUERY_STATUS);

    // if successful...
    if (schs > 0) then
    begin
      psTemp := nil;
      if (StartService(
        schs,
        0,
        psTemp)) then
      begin
        // check status
        if (QueryServiceStatus(
          schs,
          ss)) then
        begin
          while (SERVICE_RUNNING
            <> ss.dwCurrentState) do
          begin
            //
            // dwCheckPoint contains a
            // value that the service
            // increments periodically
            // to report its progress
            // during a lengthy
            // operation.
            //
            // save current value
            //
            dwChkP := ss.dwCheckPoint;

            //
            // wait a bit before
            // checking status again
            //
            // dwWaitHint is the
            // estimated amount of time
            // the calling program
            // should wait before calling
            // QueryServiceStatus() again
            //
            // idle events should be
            // handled here...
            //
            Sleep(ss.dwWaitHint);

            if (not QueryServiceStatus(
              schs,
              ss)) then
            begin
              // couldn't check status
              // break from the loop
              break;
            end;

            if (ss.dwCheckPoint <
              dwChkP) then
            begin
              // QueryServiceStatus
              // didn't increment
              // dwCheckPoint as it
              // should have.
              // avoid an infinite
              // loop by breaking
              break;
            end;
          end;
        end;
      end;

      // close service handle
      CloseServiceHandle(schs);
    end;

    // close service control
    // manager handle
    CloseServiceHandle(schm);
  end;

  // return TRUE if
  // the service status is running
  Result :=
    SERVICE_RUNNING =
    ss.dwCurrentState;
end;

The procedure to stop a service is similar to starting a service, except for calling "ControlService()" Win32 function instead of "StartService()", checking for SERVICE_STOPPED rather than SERVICE_RUNNING and using appropriate control codes.

// stop service
//
// return TRUE if successful
//
// sMachine:
//   machine name, ie: \\SERVER
//   empty = local machine
//
// sService
//   service name, ie: Alerter
//

function ServiceStop(
  sMachine,
  sService: string): boolean;
var
  //
  // service control
  // manager handle
  schm,
    //
  // service handle
  schs: SC_Handle;
  //
  // service status
  ss: TServiceStatus;
  //
  // check point
  dwChkP: DWord;
begin
  // connect to the service
  // control manager
  schm := OpenSCManager(
    PChar(sMachine),
    nil,
    SC_MANAGER_CONNECT);

  // if successful...
  if (schm > 0) then
  begin
    // open a handle to
    // the specified service
    schs := OpenService(
      schm,
      PChar(sService),
      // we want to
      // stop the service and
      SERVICE_STOP or
      // query service status
      SERVICE_QUERY_STATUS);

    // if successful...
    if (schs > 0) then
    begin
      if (ControlService(
        schs,
        SERVICE_CONTROL_STOP,
        ss)) then
      begin
        // check status
        if (QueryServiceStatus(
          schs,
          ss)) then
        begin
          while (SERVICE_STOPPED
            <> ss.dwCurrentState) do
          begin
            //
            // dwCheckPoint contains a
            // value that the service
            // increments periodically
            // to report its progress
            // during a lengthy
            // operation.
            //
            // save current value
            //
            dwChkP := ss.dwCheckPoint;

            //
            // wait a bit before
            // checking status again
            //
            // dwWaitHint is the
            // estimated amount of time
            // the calling program
            // should wait before calling
            // QueryServiceStatus() again
            //
            // idle events should be
            // handled here...
            //
            Sleep(ss.dwWaitHint);

            if (not QueryServiceStatus(
              schs,
              ss)) then
            begin
              // couldn't check status
              // break from the loop
              break;
            end;

            if (ss.dwCheckPoint <
              dwChkP) then
            begin
              // QueryServiceStatus
              // didn't increment
              // dwCheckPoint as it
              // should have.
              // avoid an infinite
              // loop by breaking
              break;
            end;
          end;
        end;
      end;

      // close service handle
      CloseServiceHandle(schs);
    end;

    // close service control
    // manager handle
    CloseServiceHandle(schm);
  end;

  // return TRUE if
  // the service status is stopped
  Result :=
    SERVICE_STOPPED =
    ss.dwCurrentState;
end;

Example usage:

if (ServiceStart('\\ComputerName', 'alerter')) then
begin
  // "alerter" service on \\ComputerName
  // was started
  // take appropriate action here
end;

// stop "alerter" service
// running on the local
// computer
if (ServiceStop('', 'alerter')) then
begin
end;

2003. december 20., szombat

Retrieve folder size


Problem/Question/Abstract:

This function tells you how many bytes a folder, with all subfolders and contained files is taking on a HD, CD, floppy or whatever.

Answer:

function FolderSize(fld: string): dword;
var
  sr: tsearchrec;
  r: integer;
  s: dword;
begin
  fld := includetrailingbackslash(fld);
  s := 0;
  r := findfirst((fld + '*.*'), faanyfile, sr);
  while (r = 0) do
  begin
    application.processmessages;
    if ((sr.attr and fadirectory) <> 0) then
    begin
      if ((sr.name <> '.') and (sr.name <> '..')) then
        s := s + foldersize(fld + sr.name);
    end
    else
      S := S + SR.FindData.nFileSizeLow;
    r := findnext(sr);
  end;
  sysutils.findclose(sr);
  result := s;
end;

2003. december 19., péntek

Soundex function


Problem/Question/Abstract:

Soundex function

Answer:

Solve 1:

This function will scan a string, and return a 'soundex' value. Comparing soundex values will give an indication of 'how alike' two strings sound... Play with it and see!!!

function Soundex(S: string): string;
const
  CvTable: array['B'..'Z'] of char = (
    '1', '2', '3', '0', '1', {'B' .. 'F'}
    '2', '0', '0', '2', '2', {'G' .. 'K'}
    '4', '5', '5', '0', '1', {'L' .. 'P'}
    '2', '6', '2', '3', '0', {'Q' .. 'U'}
    '1', '0', '2', '0', '2'); {'V' .. 'Z'}
var
  i, j: Integer;
  aGroup, Ch: Char;

  function Group(Ch: Char): Char;
  begin
    if (Ch in ['B'..'Z']) and not (Ch in ['E', 'H', 'I', 'O', 'U', 'W', 'Y']) then
      Result := CvTable[Ch]
    else
      Result := '0';
  end;

begin
  Result := '000';
  if S = '' then
    exit;

  S := Uppercase(S);
  i := 2;
  j := 1;
  while (i <= Length(S)) and (j <= 3) do
  begin
    Ch := S[i];
    aGroup := Group(Ch);
    if (aGroup <> '0') and (Ch <> S[i - 1]) and
      ((J = 1) or (aGroup <> Result[j - 1])) and
      ((i > 2) or (aGroup <> Group(S[1]))) then
    begin
      Result[j] := aGroup;
      Inc(j);
    end;
    Inc(i);
  end; {while}

  Result := S[1] + '-' + Result;
end;


Solve 2:

function StrSoundEx(const OrgString: string): string;
var
  s: string;
  PrevCh: char;
  Ch: char;
  i: integer;
begin
  s := UpperCase(Trim(OrgString));
  if s <> '' then
  begin
    PrevCh := #0;
    result := s[1];
    for i := 2 to Length(s) do
    begin
      if Length(result) = 4 then
        break;
      Ch := s[i];
      if (Ch <> PrevCh) then
      begin
        if Ch in ['B', 'P', 'F', 'V'] then
          result := result + '1'
        else if Ch in ['C', 'S', 'K', 'G', 'J', 'Q', 'X', 'Z'] then
          result := result + '2'
        else if Ch in ['D', 'T'] then
          result := result + '3'
        else if Ch in ['L'] then
          result := result + '4'
        else if Ch in ['M', 'N'] then
          result := result + '5'
        else if Ch in ['R'] then
          result := result + '6';
        PrevCh := Ch;
      end;
    end;
  end;
  while Length(result) < 4 do
    result := result + '0';
end;

2003. december 18., csütörtök

Determine the version of the BDE


Problem/Question/Abstract:

Determine the version of the BDE

Answer:

This function determines some information about the BDE (version):

uses
  � DbiTypes, DbiProcs, DbiErrs;
...

function fDbiGetSysVersion(SysVerList: TStringList): SYSVersion;
begin
  � Check(DbiGetSysVersion(Result));
  � if SysVerList <> nil then
    �
  begin
    ��� with SysVerList do
      ���
    begin
      ����� Clear;
      ����� Add(Format('ENGINE VERSION=%d', [Result.iVersion]));
      ����� Add(Format('INTERFACE LEVEL=%d', [Result.iIntfLevel]));
      ����� Add(Format('VERSION DATE=%s', [DateToStr(Result.dateVer)]));
      ����� Add(Format('VERSION TIME=%s', [TimeToStr(Result.timeVer)]));
      ���
    end;
    �
  end;
end;
{from BDE32.hlp}

2003. december 17., szerda

Storing Vlaues to the DFM files during design Time


Problem/Question/Abstract:

How can I store values in a DFM file during design Time, so that it can be used during run time?

Answer:

Most of use a table or some kind of files to store the data for the application to pick up the data during run time. Actually we can store the data in the form file ( dfm ). In the following example I have created a component derived from the TPersistent class. It uses the TReader and TWriter class to Read and write to the respective streams. The TComponentEditor allows to define the design time editors to work with the component class. The TPropertyEditor class allows to define a property editor for a specialized property in a component class.

In the following example I have given the component's source code. The design time property editor has a source file code(pas) and source form code for the form (dfm). copy the dfm code to create a dfm file, name it as "propdlg.dfm" and assign it's Name property to "fmpropdlg" and the source file code to create a pas file, name it as "propdlg.pas". Install the component TMyComponent, include the file "propdlg.pas" of the property editor in the the package.

The component will then allow you to invoke the design time editor by clicking on the object inspector for the specified property or by right clicking on the component itself and then selecting the respective verb in the menu context. You can store the fields of the class Tmydata in the form file ( dfm ) during design time.

//**********************************************************************
//***** Component source (pas) *****************************************
//**********************************************************************
unit Test;

interface

uses
  Windows, Forms, Classes, StdCtrls, SysUtils, ComCtrls, Messages, Controls,
  {DB, DBCtrls, CommCtrl, OCIH, OCI, OCL, ExtVCs,} dsgnintf;

type
  TMyPropertyEditor = class(TPropertyEditor)
  private
    { Private declarations }
  public
    { Public declarations }
    function GetAttributes: TPropertyAttributes; override;
    procedure Edit; override;
    function GetValue: string; override;
  end;

  TMyEditorPopup = class(TComponentEditor)
  private
    { Private declarations }
  public
    { Public declarations }
    procedure Edit; override;
    procedure ExecuteVerb(Index: Integer); override;
    function GetVerb(Index: Integer): string; override;
    function GetVerbCount: Integer; override;
  end;

  TMyData = class
  private
    Fstr: string;
    FInt: Integer;
  public
    property StringValue: string read Fstr write FStr;
    property IntegerValue: Integer read FInt write FInt;
  end;

  TMyTable = class(TPersistent)
  private
    FList: TList;
    function GetCount: Integer;
    function GetItem(Index: Integer): TMyData;

    procedure SetItem(Index: Integer; vItem: TMyData);
    procedure ReadProperties(Reader: TReader);
    procedure WriteProperties(Writer: TWriter);
  protected
    procedure DefineProperties(Filer: TFiler); override;
  public
    constructor Create;
    destructor Destroy; override;
    procedure AddItem;
    procedure DeleteItem(Index: Integer);
    property ItemCount: Integer read GetCount;
    property Items[Index: Integer]: TMyData read GetItem write SetItem; default;
  end;

  TMyComponent = class(TComponent)
  private
    FMyTable: TMyTable;
    procedure SetTables(Value: TMyTable);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property MyTables: TMyTable read FMyTable write SetTables;
  end;

procedure Register;

implementation

uses PropDlg;
{ TMyTable }

constructor TMyTable.Create;
begin
  FList := TList.Create;
  FList.Clear;
end;

destructor TMyTable.Destroy;
begin
  FList.Free;
  FList := nil;
  inherited destroy;
end;

procedure TMyTable.DefineProperties(Filer: TFiler);
begin
  Filer.DefineProperty('Tables', ReadProperties, WriteProperties, True);
end;

procedure TMyTable.ReadProperties(Reader: TReader);
begin
  Reader.ReadListBegin;
  while (not Reader.EndOfList) do
  begin
    AddItem;
    with Items[itemCount - 1] do
    begin
      Fstr := Reader.ReadString;
      FInt := Reader.ReadInteger;
    end;
  end;
  Reader.ReadListEnd;
end;

procedure TMyTable.WriteProperties(Writer: TWriter);
var
  I: Integer;
begin
  Writer.WriteListBegin;
  for I := 0 to (ItemCount - 1) do
  begin
    with Items[I] do
    begin
      Writer.WriteString(Fstr);
      Writer.WriteInteger(FInt);
    end;
  end;
  Writer.WriteListEnd;
end;

procedure TMyTable.AddItem;
var
  vData: TMyData;
begin
  vData := TMyData.Create;
  FList.Add(vData);
end;

function TMyTable.GetCount: Integer;
begin
  Result := FList.Count;
end;

function TMyTable.GetItem(Index: Integer): TMyData;
begin
  Result := TMyData(FList[Index]);
end;

procedure TMyTable.SetItem(Index: Integer; vItem: TMyData);
begin
  Flist[Index] := vItem;
end;

procedure TMyTable.DeleteItem(Index: Integer);
begin
  FList.Delete(Index);
end;

{ TMyComponent }

constructor TMyComponent.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FMyTable := TMyTable.Create;
end;

destructor TMyComponent.Destroy;
begin
  FMyTable.Free;
  FMyTable := nil;
  inherited Destroy;
end;

procedure TMyComponent.SetTables(Value: TMyTable);
begin
  {}
end;

function TMyPropertyEditor.GetAttributes: TPropertyAttributes;
begin
  Result := [paDialog, paReadOnly, paRevertable];
end;

procedure TMyPropertyEditor.Edit;
var
  MyComponent: TPersistent;
  FMyComponent: TMyComponent;
  MyDialog: TfmPropDlg;
begin
  MyComponent := GetComponent(0);
  if MyComponent is TMyComponent then
  begin
    FMyComponent := TMyComponent(MyComponent);

    MyDialog := TfmPropDlg.Create(Application);
    try
      MyDialog.FMyComponent := FMyComponent;
      MyDialog.FmyPropertyEditor := Self;
      MyDialog.ShowModal;
    finally
      MyDialog.Free;
      MyDialog := nil
    end;
  end;
end;

function TMyPropertyEditor.GetValue: string;
begin
  FmtStr(Result, '(%s)', [GetPropType^.Name]);
end;

procedure Register;
begin
  RegisterComponents('YOGI', [TMyComponent]);
  RegisterPropertyEditor(TypeInfo(TMyTable), TMyComponent, 'MyTables',
    TMyPropertyEditor);
  RegisterComponentEditor(TMyComponent, TMyEditorPopup);
end;

{ TMyEditorPopup }

procedure TMyEditorPopup.Edit;
var
  //  MyComponent : TPersistent;
  FMyComponent: TMyComponent;
  MyDialog: TfmPropDlg;
begin
  if Component is TMyComponent then
  begin
    FMyComponent := TMyComponent(Component);
    MyDialog := TfmPropDlg.Create(Application);
    try
      MyDialog.FMyComponent := FMyComponent;
      MyDialog.FMyEditorPopup := Self;
      MyDialog.ShowModal;
    finally
      MyDialog.Free;
      MyDialog := nil;
    end;
  end;
end;

procedure TMyEditorPopup.ExecuteVerb(Index: Integer);
begin
  if (Index = 0) then
    Edit;
end;

function TMyEditorPopup.GetVerb(Index: Integer): string;
begin
  if Index = 0 then
    Result := 'Yoganand''s Editor';
end;

function TMyEditorPopup.GetVerbCount: Integer;
begin
  Result := 1;
end;

{ TMyTest }

end.

//**********************************************************************
//***** Property Editor's source file (pas) code *******************
//**********************************************************************

unit propDlg;

interface

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

type
  TfmPropDlg = class(TForm)
    Label1: TLabel;
    Label2: TLabel;
    edtStr: TEdit;
    edtInt: TEdit;
    sbAdd: TSpeedButton;
    sbDelete: TSpeedButton;
    sbOk: TSpeedButton;
    sbCancel: TSpeedButton;
    sbup: TSpeedButton;
    sbDown: TSpeedButton;
    procedure sbAddClick(Sender: TObject);
    procedure sbOkClick(Sender: TObject);
    procedure sbupClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure sbDownClick(Sender: TObject);
    procedure sbDeleteClick(Sender: TObject);
  private
    { Private declarations }
    FCurrentIndex: Integer;
  public
    { Public declarations }
    FMyComponent: TMyComponent;
    FMyPropertyEditor: TPropertyEditor;
    FMyEditorPopup: TComponentEditor;
    FPageIndex: Integer;
  end;

var
  fmPropDlg: TfmPropDlg;

implementation

{$R *.DFM}

procedure TfmPropDlg.sbAddClick(Sender: TObject);
begin
  FMyComponent.MyTables.AddItem;
  edtStr.Text := '';
  edtInt.Text := '';
  edtStr.SetFocus;
end;

procedure TfmPropDlg.sbOkClick(Sender: TObject);
begin
  FMyComponent.MyTables[FMyComponent.MyTables.ItemCount - 1].StringValue :=
    edtStr.Text;
  FMyComponent.MyTables[FMyComponent.MyTables.ItemCount - 1].IntegerValue :=
    StrtoInt(edtInt.Text);
end;

procedure TfmPropDlg.sbupClick(Sender: TObject);
var
  I: Integer;
begin
  if (FCurrentIndex > 0) then
  begin
    Dec(FCurrentIndex);
    edtStr.Text := FMyComponent.MyTables[FCurrentIndex].StringValue;
    edtInt.Text := Inttostr(FMyComponent.MyTables[FCurrentIndex].IntegerValue);
  end;
end;

procedure TfmPropDlg.FormCreate(Sender: TObject);
begin
  FCurrentIndex := 0;
end;

procedure TfmPropDlg.FormShow(Sender: TObject);
begin
  if (FMyComponent.MyTables.ItemCount > 0) then
  begin
    FCurrentIndex := 0;
    edtStr.Text := FMyComponent.MyTables[FCurrentIndex].StringValue;
    edtInt.Text := Inttostr(FMyComponent.MyTables[FCurrentIndex].IntegerValue);
  end;
end;

procedure TfmPropDlg.sbDownClick(Sender: TObject);
begin
  if (FCurrentIndex < (FMyComponent.MyTables.ItemCount - 1)) then
  begin
    Inc(FCurrentIndex);
    edtStr.Text := FMyComponent.MyTables[FCurrentIndex].StringValue;
    edtInt.Text := Inttostr(FMyComponent.MyTables[FCurrentIndex].IntegerValue);
  end;
end;

procedure TfmPropDlg.sbDeleteClick(Sender: TObject);
begin
  if (FMyComponent.MyTables.ItemCount > 0) then
  begin
    FMyComponent.MyTables.DeleteItem(FCurrentIndex);
    FCurrentIndex := 0;
    edtStr.Text := FMyComponent.MyTables[FCurrentIndex].StringValue;
    edtInt.Text := Inttostr(FMyComponent.MyTables[FCurrentIndex].IntegerValue);
  end;
end;

end.

//**********************************************************************
//****** Property Editor's form file (dfm)  Code **************************
//**********************************************************************

object fmPropDlg: TfmPropDlg
  Left = 263
    Top = 371
    BorderStyle = bsDialog
    Caption = 'Editor Dlg'
    ClientHeight = 103
    ClientWidth = 218
    Color = clBtnFace
    Font.Charset = DEFAULT_CHARSET
    Font.Color = clWindowText
    Font.Height = -11
    Font.Name = 'MS Sans Serif'
    Font.Style = []
    OldCreateOrder = False
    Position = poScreenCenter
    OnCreate = FormCreate
    OnShow = FormShow
    PixelsPerInch = 96
    TextHeight = 13
    object Label1: TLabel
    Left = 16
      Top = 24
      Width = 36
      Height = 13
      Caption = 'Value 1'
  end
  object Label2: TLabel
    Left = 17
      Top = 51
      Width = 33
      Height = 13
      Caption = 'Value2'
  end
  object sbAdd: TSpeedButton
    Left = 26
      Top = 77
      Width = 23
      Height = 22
      Glyph.Data = {
    76010000424D7601000000000000760000002800000020000000100000000100
    04000000000000010000130B0000130B00001000000000000000000000000000
    800000800000008080008000000080008000808000007F7F7F00BFBFBF000000
    FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00333333333333
    33333333FF33333333FF333993333333300033377F3333333777333993333333
    300033F77FFF3333377739999993333333333777777F3333333F399999933333
    33003777777333333377333993333333330033377F3333333377333993333333
    3333333773333333333F333333333333330033333333F33333773333333C3333
    330033333337FF3333773333333CC333333333FFFFF77FFF3FF33CCCCCCCCCC3
    993337777777777F77F33CCCCCCCCCC3993337777777777377333333333CC333
    333333333337733333FF3333333C333330003333333733333777333333333333
    3000333333333333377733333333333333333333333333333333}
    NumGlyphs = 2
      OnClick = sbAddClick
  end
  object sbDelete: TSpeedButton
    Left = 62
      Top = 76
      Width = 23
      Height = 22
      Glyph.Data = {
    76010000424D7601000000000000760000002800000020000000100000000100
    04000000000000010000130B0000130B00001000000000000000000000000000
    800000800000008080008000000080008000808000007F7F7F00BFBFBF000000
    FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00333333333333
    333333333333333333FF33333333333330003333333333333777333333333333
    300033FFFFFF3333377739999993333333333777777F3333333F399999933333
    3300377777733333337733333333333333003333333333333377333333333333
    3333333333333333333F333333333333330033333F33333333773333C3333333
    330033337F3333333377333CC3333333333333F77FFFFFFF3FF33CCCCCCCCCC3
    993337777777777F77F33CCCCCCCCCC399333777777777737733333CC3333333
    333333377F33333333FF3333C333333330003333733333333777333333333333
    3000333333333333377733333333333333333333333333333333}
    NumGlyphs = 2
      OnClick = sbDeleteClick
  end
  object sbOk: TSpeedButton
    Left = 100
      Top = 76
      Width = 23
      Height = 22
      Glyph.Data = {
    76010000424D7601000000000000760000002800000020000000100000000100
    04000000000000010000120B0000120B00001000000000000000000000000000
    800000800000008080008000000080008000808000007F7F7F00BFBFBF000000
    FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00555555555555
    555555555555555555555555555555555555555555FF55555555555559055555
    55555555577FF5555555555599905555555555557777F5555555555599905555
    555555557777FF5555555559999905555555555777777F555555559999990555
    5555557777777FF5555557990599905555555777757777F55555790555599055
    55557775555777FF5555555555599905555555555557777F5555555555559905
    555555555555777FF5555555555559905555555555555777FF55555555555579
    05555555555555777FF5555555555557905555555555555777FF555555555555
    5990555555555555577755555555555555555555555555555555}
    NumGlyphs = 2
      OnClick = sbOkClick
  end
  object sbCancel: TSpeedButton
    Left = 144
      Top = 76
      Width = 23
      Height = 22
      Glyph.Data = {
    76010000424D7601000000000000760000002800000020000000100000000100
    04000000000000010000130B0000130B00001000000000000000000000000000
    800000800000008080008000000080008000808000007F7F7F00BFBFBF000000
    FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00333333333333
    333333333333333333333333333333333333333FFF33FF333FFF339993370733
    999333777FF37FF377733339993000399933333777F777F77733333399970799
    93333333777F7377733333333999399933333333377737773333333333990993
    3333333333737F73333333333331013333333333333777FF3333333333910193
    333333333337773FF3333333399000993333333337377737FF33333399900099
    93333333773777377FF333399930003999333337773777F777FF339993370733
    9993337773337333777333333333333333333333333333333333333333333333
    3333333333333333333333333333333333333333333333333333}
    NumGlyphs = 2
  end
  object sbup: TSpeedButton
    Left = 192
      Top = 16
      Width = 23
      Height = 22
      Glyph.Data = {
    76010000424D7601000000000000760000002800000020000000100000000100
    04000000000000010000120B0000120B00001000000000000000000000000000
    800000800000008080008000000080008000808000007F7F7F00BFBFBF000000
    FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00333333000333
    3333333333777F33333333333309033333333333337F7F333333333333090333
    33333333337F7F33333333333309033333333333337F7F333333333333090333
    33333333337F7F33333333333309033333333333FF7F7FFFF333333000090000
    3333333777737777F333333099999990333333373F3333373333333309999903
    333333337F33337F33333333099999033333333373F333733333333330999033
    3333333337F337F3333333333099903333333333373F37333333333333090333
    33333333337F7F33333333333309033333333333337373333333333333303333
    333333333337F333333333333330333333333333333733333333}
    NumGlyphs = 2
      OnClick = sbupClick
  end
  object sbDown: TSpeedButton
    Left = 192
      Top = 64
      Width = 23
      Height = 22
      Glyph.Data = {
    76010000424D7601000000000000760000002800000020000000100000000100
    04000000000000010000120B0000120B00001000000000000000000000000000
    800000800000008080008000000080008000808000007F7F7F00BFBFBF000000
    FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00333333303333
    333333333337F33333333333333033333333333333373F333333333333090333
    33333333337F7F33333333333309033333333333337373F33333333330999033
    3333333337F337F33333333330999033333333333733373F3333333309999903
    333333337F33337F33333333099999033333333373333373F333333099999990
    33333337FFFF3FF7F33333300009000033333337777F77773333333333090333
    33333333337F7F33333333333309033333333333337F7F333333333333090333
    33333333337F7F33333333333309033333333333337F7F333333333333090333
    33333333337F7F33333333333300033333333333337773333333}
    NumGlyphs = 2
      OnClick = sbDownClick
  end
  object edtStr: TEdit
    Left = 56
      Top = 21
      Width = 121
      Height = 21
      TabOrder = 0
  end
  object edtInt: TEdit
    Left = 56
      Top = 48
      Width = 121
      Height = 21
      TabOrder = 1
  end
end

//******************************************************

2003. december 16., kedd

Read infos in MP3 files


Problem/Question/Abstract:

How to read infos in MP3 files

Answer:

{
   Description:
   Component MP3Info extracts/saves any ID3 Tag into/from it's
   properties from/to MP3 file.
   Keywords: MP3 ID3 TAG INFO

   Author: Rok Krulec (Rok@fpp.edu) (http://Rok.fpp.edu)
   Primary site: ftp://ftp.fpp.edu/pub/people/Rok_Krulec/mp3i-v12.zip

If you use this component, I would like to get 1 American dollar to Rok Krulec, Zagrad 6, 3000 Celje, Slovenia, Europe.

Description of Variables, Properties, Methods and Events:
    Genres: TStrings;                         - List of Genres
    constructor Create(AOwner: TComponent);   - Creates an instance
    destructor Destroy; override;             - Destroys an instance
    method Save;                              - Saves ID3 Tag to file
    method RemoveID3;                         - Removes ID3 Tag form file
    property Filename: TFilename;             - Filename of MP3 file, when changed it opens a new MP3 file
    property Artist: String;                  - Artist   (30 Chars)
    property Title: String;                   - Title    (30 Chars)
    property Album: String;                   - Album    (30 Chars)
    property Year: String;                    - Year     ( 4 chars)
    property Comment: String;                 - Comment  (30 Chars)
    property Genre: String;                   - Genre               [Read Only]
    Property GenreID: Byte;                   - Genre ID
    property Valid: Boolean;                  - Is ID3 valid        [Read Only]
    property Saved: Boolean;                  - Save success        [Read Only]
    property Error: String;                   - Error Message       [Read Only]
    property onChangeFile:TNotifyEvent;       - Triggers when other file is openned
    property onChange:TNotifyEvent;           - Triggers when one of propertis is changed (Artist, Title, Album, Year, Comment, GenreID)
    property onError:TNotifyEvent;            - Triggers when errors ocure (Wrong filename, Frong fileformat)
}

unit MP3Info;

interface

uses
  SysUtils, Classes;

const
  TAGLEN = 127;

type
  TMP3Info = class(TComponent)
  private
    { Private declarations }
    vFilename: TFilename;
    vMP3Tag, vArtist, vTitle, vAlbum, vComment, vYear, vGenre, vError: string;
    vGenreID: Byte;
    vValid: Boolean;
    vSaved: Boolean;
    vChangeFileEvent, vChangeEvent, vErrorEvent: TNotifyEvent;
    procedure SetFilename(Filename: TFilename);
    procedure SetArtist(Artist: string);
    procedure SetTitle(Title: string);
    procedure SetAlbum(Album: string);
    procedure SetYear(Year: string);
    procedure SetComment(Comment: string);
    procedure SetGenreID(ID: Byte);
    procedure Open;
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
    Genres: TStrings;
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Save;
    procedure RemoveID3;
    property Filename: TFilename read vFilename write SetFilename;
    property Artist: string read vArtist write SetArtist;
    property Title: string read vTitle write SetTitle;
    property Album: string read vAlbum write SetAlbum;
    property Year: string read vYear write SetYear;
    property Comment: string read vComment write SetComment;
    property Genre: string read vGenre;
    property GenreID: Byte read vGenreID write SetGenreID;
    property Valid: Boolean read vValid;
    property Saved: Boolean read vSaved;
    property Error: string read vError;
    property onChangeFile: TNotifyEvent read vChangeFileEvent write vChangeFileEvent;
    property onChange: TNotifyEvent read vChangeEvent write vChangeEvent;
    property onError: TNotifyEvent read vErrorEvent write vErrorEvent;
  end;

procedure Register;

implementation

constructor TMP3Info.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Genres := TStringList.Create;
  vGenreID := 12;
  vValid := false;
  vSaved := false;

  { Fill the Genres String List so one can use it combo boxes e.t.c. Example: ComboBox.Items.Assign(MP3Info.Genres) }
  Genres.CommaText :=
    '"Blues","Classic Rock","Country","Dance","Disco","Funk","Grunge","Hip-Hop","Jazz","Metal","New Age","Oldies",'
    + '"Other","Pop","R&B","Rap","Reggae","Rock","Techno","Industrial","Alternative","Ska","Death Metal","Pranks",'
    + '"Soundtrack","Euro-Techno","Ambient","Trip-Hop","Vocal","Jazz+Funk","Fusion","Trance","Classical","Instrumental",'
    + '"Acid","House","Game","Sound Clip","Gospel","Noise","AlternRock","Bass","Soul","Punk","Space","Meditative",'
    + '"Instrumental Pop","Instrumental Rock","Ethnic","Gothic","Darkwave","Techno-Industrial","Electronic","Pop-Folk",'
    + '"Eurodance","Dream","Southern Rock","Comedy","Cult","Gangsta","Top 40","Christian Rap","Pop/Funk","Jungle",'
    + '"Native American","Cabaret","New Wave","Psychedelic","Rave","Showtunes","Trailer","Lo-Fi","Tribal","Acid Punk",'
    + '"Acid Jazz","Polka","Retro","Musical","Rock & Roll","Hard Rock","Folk","Folk/Rock","National Folk","Swing","Bebob",'
    + '"Latin","Revival","Celtic","Bluegrass","Avantgarde","Gothic Rock","Progressive Rock","Psychedelic Rock","Symphonic Rock",'
    + '"Slow Rock","Big Band","Chorus","Easy Listening","Acoustic","Humour","Speech","Chanson","Opera","Chamber Music","Sonata",'
    + '"Symphony","Booty Bass","Primus","Porn Groove","Satire","Slow Jam","Club","Tango","Samba","Folklore"'

end;

destructor TMP3Info.Destroy;
begin
  inherited Destroy;
end;

{ Procedure to run when Filename property is changed }

procedure TMP3Info.SetFilename(Filename: TFilename);
begin
  vFilename := Filename;
  Open;
end;

procedure TMP3Info.SetArtist(Artist: string);
begin
  vArtist := Copy(Artist, 0, 30);
  if Assigned(onChange) then
    onChange(Self);
end;

procedure TMP3Info.SetTitle(Title: string);
begin
  vTitle := Copy(Title, 0, 30);
  if Assigned(onChange) then
    onChange(Self);
end;

procedure TMP3Info.SetAlbum(Album: string);
begin
  vAlbum := Copy(Album, 0, 30);
  if Assigned(onChange) then
    onChange(Self);
end;

procedure TMP3Info.SetYear(Year: string);
begin
  vYear := Copy(Year, 0, 4);
  if Assigned(onChange) then
    onChange(Self);
end;

procedure TMP3Info.SetComment(Comment: string);
begin
  vComment := Copy(Comment, 0, 30);
  if Assigned(onChange) then
    onChange(Self);
end;

procedure TMP3Info.SetGenreID(ID: Byte);
begin
  if ((ID > 255) or (ID > Genres.Count - 1)) then
    ID := 12;
  vGenreID := ID;
  vGenre := Genres[vGenreID]; //this line is important because after changing
  //vGenreID whitout it vGenre will be the same like before !!!
  if Assigned(onChange) then
    onChange(Self);
end;

{ Opens file with Filename property, reads ID3 Tag and sets properties }

procedure TMP3Info.Open;

{ Strips empty spaces at the end of word }
  function Strip(WordToStrip: string; CharToStripAway: Char): string;
  var
    i: Integer;
  begin
    for i := length(WordToStrip) downto 1 do
    begin
      if WordToStrip[i] <> ' ' then
      begin
        Strip := Copy(WordToStrip, 0, i);
        exit;
      end;
    end;
    Strip := '';
  end;

var
  dat: file of char;
  id3: array[0..TAGLEN] of char;
begin
  vSaved := false;
  vValid := True;
  if FileExists(vFilename) then
  begin
    assignfile(dat, vFilename);
    reset(dat);
    seek(dat, FileSize(dat) - 128);
    blockread(dat, id3, 128);
    closefile(dat);
    vMP3tag := copy(id3, 1, 3);
    if vMP3Tag = 'TAG' then
    begin
      vTitle := strip(copy(id3, 4, 30), ' ');
      vArtist := strip(copy(id3, 34, 30), ' ');
      vAlbum := strip(copy(id3, 64, 30), ' ');
      vComment := strip(copy(id3, 98, 30), ' ');
      vYear := strip(copy(id3, 94, 4), ' ');
      vGenreID := ord(id3[127]);
      if vGenreID > Genres.Count then
        vGenreID := 12;
      vGenre := Genres[vGenreID];
      { Trigger OnChange Event }
      if Assigned(onChangeFile) then
        onChangeFile(Self);
    end
    else
    begin
      vValid := False;
      vTitle := '';
      vArtist := '';
      vAlbum := '';
      vComment := '';
      vYear := '';
      vGenreID := 12;
      vError := 'Wrong file format or no ID3 Tag !';
      if Assigned(onError) then
        onError(Self);
    end;
  end
  else
  begin
    vValid := False;
    vError := 'File doesn`t exist !';
    if Assigned(onError) then
      onError(Self);
  end;
end;

{ Removes the ID3-tag from currently open file }

procedure TMP3Info.RemoveID3;
var
  dat: file of char;
begin
  // does the file exist ?
  if not FileExists(vFilename) then
  begin
    vError := 'File doesn`t exist !';
    if Assigned(onError) then
      onError(Self);
    exit;
  end;
  // is the file already untagged ?
  if (vValid = false) then
  begin
    vError := 'File is already untagged !';
    if Assigned(onError) then
      onError(Self);
    exit;
  end;
  // remove readonly-attribute
  if (FileGetAttr(vFilename) and faReadOnly > 0) then
    FileSetAttr(vFileName, FileGetAttr(vFilename) - faReadOnly);
  // if readonly attr. already exists it cannot be removed to cut ID3 Tag
  if (FileGetAttr(vFilename) and faReadOnly > 0) then
  begin
    vError := 'Can�t write ID3 tag information !';
    if Assigned(onError) then
      onError(Self);
    exit;
  end;
  // open current mp3 file if ID3 tag exists
  if (vValid = true) then
  begin
    {I-}
    assignfile(dat, vFilename);
    reset(dat);
    {I+}
    if IOResult <> 0 then
    begin
      vError := 'Could not open file !';
      if Assigned(onError) then
        onError(Self);
      exit;
    end;
    seek(dat, FileSize(dat) - 128);
    truncate(dat); // cut all 128 bytes of file
    closefile(dat);
    vValid := false; // set vValid to false because the tag has been removed
  end;
end;

{ Saves ID3 Tag to currently opened file }

procedure TMP3Info.Save;

{ Empties 128 character array }{ Don't tell me that there is a function for this in Pascal }
  procedure EmptyArray(var Destination: array of char);
  var
    i: Integer;
  begin
    for i := 0 to TAGLEN do
    begin
      Destination[i] := ' ';
    end;
  end;

  { Insert a substring into character array at index position of array }
  procedure InsertToArray(Source: string; var Destination: array of char; Index:
    Integer);
  var
    i: Integer;
  begin
    for i := 0 to length(Source) - 1 do
    begin
      Destination[Index + i] := Source[i + 1];
    end;
  end;

var
  dat: file of char;
  id3: array[0..TAGLEN] of char;
begin
  vSaved := true;
  // does the filename exist ?
  if FileExists(vFilename) then
  begin
    // fill 128 bytes long array with ID3 Tag information
    EmptyArray(id3);
    InsertToArray('TAG', id3, 0);
    InsertToArray(vTitle, id3, 3);
    InsertToArray(vArtist, id3, 33);
    InsertToArray(vAlbum, id3, 63);
    InsertToArray(vComment, id3, 97);
    InsertToArray(vYear, id3, 93);
    id3[127] := chr(vGenreID);
    // remove readonly-attribute
    if (FileGetAttr(vFilename) and faReadOnly > 0) then
      FileSetAttr(vFileName, FileGetAttr(vFilename) - faReadOnly);
    // if readonly attr. already exists it cannot be removed to write ID3
    if (FileGetAttr(vFilename) and faReadOnly > 0) then
    begin
      vSaved := False;
      vError := 'Can�t write ID3 tag information !';
      if Assigned(onError) then
        onError(Self);
      exit;
    end;
    // if valid then overwrite existing ID3 Tag, else append to file
    if (vValid = True) then
    begin
      {I-}
      assignfile(dat, vFilename);
      reset(dat);
      seek(dat, FileSize(dat) - 128);
      blockwrite(dat, id3, 128);
      closefile(dat);
      {I+}
      if IOResult <> 0 then
        vSaved := false;
    end
    else
    begin
      {I-}
      assignfile(dat, vFilename);
      reset(dat);
      seek(dat, FileSize(dat));
      blockwrite(dat, id3, 128);
      closefile(dat);
      {I+}
      if IOResult <> 0 then
        vSaved := false;
    end
  end
  else
  begin
    vValid := False;
    vSaved := False;
    vError := 'File doesn`t exist or is not valid !';
    if Assigned(onError) then
      onError(Self);
  end;
end;

procedure Register;
begin
  RegisterComponents('Samples', [TMP3Info]);
end;

end.

{
   The ID3 Information is stored in the last 128 bytes of an MP3 file.
   The ID3 has the following fields, and the offsets given here, are from 0-127

   Field       Length            offsets
   -------------------------------------
   Tag           3                0-2
   Songname     30                3-32
   Artist       30                33-62
   Album        30                63-92
   Year          4                93-96
   Comment      30                97-126
   Genre         1                127
}

2003. december 15., hétfő

Putting a TDBLookupComboBox in a Grid


Problem/Question/Abstract:

How do I display a DBLookupComboBox in a Grid?

Answer:

The TDBGrid is an interesting component in that it's not really a "grid;" rather, it's more or less a collection of rectangles that are dynamically drawn to display data. The operative word here is "dynamic." If you take a look at the events of a TDBGrid, you'll see an event handler called OnDrawDataCell. Without going into a lot of technical mumbo-jumbo, this event is responsible for drawing data (or whatever) in the "cell" of a grid. The default action, obviously, is to display the underlying data of the grid, but since it's visible, we have the opportunity of adding some enhanced functionality. And that's exactly what we do to display a drop-down edit box. Now some of you might be thinking at this point that if we're adding our own functionality to the OnDrawDataCell, are we actually manipulating the grid itself? The answer to that is no. What we're actually doing in this case is drawing OVER the cell to make it look like the cell is a drop-down. Okay, let's get to specifics...

Setting Up Your Application

The sample application that we'll be building is going to be a simple order entry screen. For simplicity's sake, we'll be using the the Orders.db and Customer.db tables from the DBDEMOS database that gets installed with Delphi, though you easily transfer what you do here to any other application where you need a lookup. For our application, we'll be using the Orders table as the data entry table, and the Customer table as the lookup to retrieve customer identifications. Okay, here we go...

The first thing you need to do is to create a new application in Delphi. On the main form of the application, drop the following components:

Two (2) TTable Components
Two (2) TDatasource Components
One (1) TDBGrid
One (1) TDBLookupComboBox (You can drop this anywhere, we'll be positioning it at runtime)

To make things easier, set both TTables' DatabaseName properties to "DBDEMOS." Point the first table (Table1) to ORDERS.DB, and the second table (Table2) to CUSTOMER.DB (this will be our lookup table). Point DataSource1 to Table1 and DataSource2 to Table2. In plain english, you're setting DataSource1 and Table1 to point to the data entry table, while DataSource2 and Table2 point to the lookup data table. From there it's a matter of setting DBGrid1 to point to DataSource1.

Now with the TDBLookupComboBox, you've got to set a few properties, which is why I separated its setup from the other components. Besides, setting the properties of a TDBLookupComboBox has caused more than enough consternation among developers over time. From my point of view, or at least from what I remember when I wanted to just use this component by itself, one of the most confusing things about it was the way the properties were listed in the object inspector. But I guess that's neither here nor there. In any case here's what you do:

Set the DataSource property to DataSource1 (the same one that the DBGrid points to).
Set the DataField property to the CustNo field (this is the field that you're going to put lookup information into).
Now, set the ListSource property to DataSource2
Set the ListField property to the CustNo field.
This one's important: Drop down the KeyField property field and select CustNo from the list (It's the only field available). This will form the link between the two tables.
Finally, set the Visible property of the component to False - I'll explain that in a bit.

Once you're done with the steps above, set the Active properties of both tables to True. If you've done everything right, data should be displaying in the grid and you should see a value appear in the DBLookupComboBox. Now on to coding...

Making It Work

As I mentioned above, in order to make it appear that the DBGrid has a drop-down lookup, we use the OnDrawDataCell to draw the lookup combo box over the cell in which we want to get lookup information. In order to make this totally seamless to the user, we have to fulfill a few criteria:

Move and size the DBLookupComboBox over the cell in which we want to look up information.
Handle the lookup's visibility as the user scrolls from cell to cell in grid.
Handle focus control when the user enters the lookup cell.
Handle movement out of the DBLookupComboBox

The first and second criteria are easily met by writing code for OnDrawDataCell and OnColExit event handlers on the grid:

procedure TForm1.DBGrid1DrawDataCell(Sender: TObject; const Rect: TRect;
  Field: TField; State: TGridDrawState);
begin
  //Regardless of cell, do we have input focus? Also,
  //is the field we're on the same as the data field
  //pointed to by the DBLookupComboBox? If so, then
  //Move the component over the cell.
  if ((gdFocused in State) and
    (Field.FieldName = DBLookupComboBox1.DataField)) then
    with DBLookupComboBox1 do
    begin
      Left := Rect.Left + DBGrid1.Left;
      Top := Rect.Top + DBGrid1.Top;
      Width := Rect.Right - Rect.Left;
      if ((Rect.Bottom - Rect.Top) > Height) then
        Height := Rect.Bottom - Rect.Top;
      Visible := True;
    end;
end;

procedure TForm1.DBGrid1ColExit(Sender: TObject);
begin
  //Are we leaving the field in the grid that
  //is also the data field for our lookup?
  with DBGrid1, DBLookupComboBox1 do
    if (SelectedField.FieldName = DataField) then
      Visible := False;
end;

As you can see above, the OnDrawDataCell event handles the movement and sizing of the DBLookupComboBox and sets its visibility to True, while the OnColExit sets its visibility to False. In both cases, the conditional statement includes a comparison between the grid's field and the data field pointed to by the combo box. If they're the same, then they act. In the case of the OnDrawDataCell event though, the conditional also includes an evaluation of the State parameter. This is incredibly important because we only want to perform the drawing if a cell has input focus. If we were to remove this conditional, the component would be continuously drawn, causing an irritating strobe. Not good.

The third criteria exists because the DBLookupComboBox is not really part of the grid; it merely floats above it. Furthermore, since we're controlling the combo's behavior from the grid, it really doesn't ever receive input focus. The net result is that keystrokes don't get sent to the combo box, they get sent to the grid, even if the combo is displaying above the cell and is highlighted! If you tried typing a new customer number into the DBLookupComboBox at this point, nothing would appear to be happening. The combo box would remain highlighted. Actually, there is something happening - the grid's cell is actually getting updated. But you can't see it. In that case, what we have to do is make the grid give focus to the combo box as keys are pressed, and the place you do this is in the OnKeyPress event of the grid:

//If you edit the value in the lookup field, the grid actually
//has focus, so unless the keystroke is a Tab, then we need to
//send keystrokes to the LookupCombo

procedure TForm1.DBGrid1KeyPress(Sender: TObject; var Key: Char);
begin
  if (Key <> Chr(9)) then
    with DBGrid1, DBLookupComboBox1 do
      if (SelectedField.FieldName = DataField) then
      begin
        SetFocus;
        SendMessage(Handle, WM_CHAR, Word(Key), 0);
      end;
end;

The code above first checks the keypress to see if it isn't a Tab. If it was, it's ignored, and the user can move to an adjacent cell. But for any other key, we do our conditional to see if the field in the cell is the same as the data field for the combo. In that case, focus is set to the DBLookupComboBox and we send the keystroke message to it using the Win API SendMessage function. As much as possible, you want to avoid going to the Win API, but in this case, it's the only way to send a message.

Building on the third criteria, once you give focus control to the DBLookupCombo, it keeps focus. That's not bad in and of itself, but there's a catch. When you Tab out of the box, what happens is that focus is returned to the grid, but focus is also returned to the underlying cell. This means that in order to move to the next field, the user is forced to press Tab twice! There's no way to get around this phenomenon. However, there is a bit of trickery you can perform that will programmatically send another Tab to the grid. You do this in the OnKeyUp event of the DBGrid:

//If you choose an item from the lookup, you give focus
//control to it. The net result is that it takes two
//Tabs to move to the next cell. In that case, we need
//to send another Tab keystroke to the grid so that only
//one keystroke is needed to move to the next cell.

procedure TForm1.DBGrid1KeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if (Key in [VK_TAB]) and InBox then
  begin
    SendMessage(DBGrid1.Handle, WM_KEYDOWN, Key, 0);
    InBox := False;
  end;
end;

Notice the variable that's being set here: InBox. This is an implemenation-level variable that is used to determine whether or not the user has entered the CustNo cell. It's set to True in the OnEnter event of the combo box. Then in the OnKey up, if InBox is true and the keypress was a Tab, then we send the keystroke again. Otherwise, it's ignored. Here's the OnEnter of the DBLookupComboBox:

procedure TForm1.DBLookupComboBox1Enter(Sender: TObject);
begin
  InBox := True;
end;

Pretty straight forward....

But there is just one more tidbit that I have to throw at you to make this work problem-free.

One Last Tidbit

There's an option in the options property of the TDBGrid called dgCancelOnExit. This option is defined as follows in the online help:

When the user exits the grid from an inserted record to which the user made no modifications, the inserted record is not posted to the dataset. This prevents the inadvertent posting of empty records.

What does this have to do with what we're doing here? Well, let's say you insert a new record into the grid. If you immediately click on the CustNo lookup combo, your new record will disappear. Why? Well, based upon the definition above and based upon the code presented here, if you went to the CustNo field immediately following an insert, the grid would lose input focus! When dgCancelOnExit is set to True, if the grid loses focus before the record has been posted, the new row is deleted. Luckily, setting this option to False alleviates the problem.

Putting It All Together

To make the job of performing this technique easier, here's the full code listing of the form I used for the sample application:

unit main;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Grids, DBGrids, DBCtrls, Db, DBTables;

type
  TForm1 = class(TForm)
    Table1: TTable;
    DataSource1: TDataSource;
    DataSource2: TDataSource;
    Table2: TTable;
    Table1OrderNo: TFloatField;
    Table1CustNo: TFloatField;
    Table1SaleDate: TDateTimeField;
    Table1ShipDate: TDateTimeField;
    Table1EmpNo: TIntegerField;
    Table1AmountPaid: TCurrencyField;
    Table2CustNo: TFloatField;
    DBLookupComboBox1: TDBLookupComboBox;
    DBGrid1: TDBGrid;
    procedure DBGrid1DrawDataCell(Sender: TObject; const Rect: TRect;
      Field: TField; State: TGridDrawState);
    procedure DBGrid1ColExit(Sender: TObject);
    procedure DBGrid1KeyPress(Sender: TObject; var Key: Char);
    procedure DBGrid1KeyUp(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure DBLookupComboBox1Enter(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation
var
  InBox: Boolean;
{$R *.DFM}

procedure TForm1.DBGrid1DrawDataCell(Sender: TObject; const Rect: TRect;
  Field: TField; State: TGridDrawState);
begin
  //Regardless of cell, do we have input focus? Also,
  //is the field we're on the same as the data field
  //pointed to by the DBLookupComboBox? If so, then
  //Move the component over the cell.
  if (gdFocused in State) and
    (Field.FieldName = DBLookupComboBox1.DataField) then
    with DBLookupComboBox1 do
    begin
      Left := Rect.Left + DBGrid1.Left;
      Top := Rect.Top + DBGrid1.Top;
      Width := Rect.Right - Rect.Left;
      if ((Rect.Bottom - Rect.Top) > Height) then
        Height := Rect.Bottom - Rect.Top;
      Visible := True;
    end;
end;

procedure TForm1.DBGrid1ColExit(Sender: TObject);
begin
  //Are we leaving the field in the grid that
  //is also the data field for our lookup?
  with DBGrid1, DBLookupComboBox1 do
    if (SelectedField.FieldName = DataField) then
      Visible := False;
end;

//If you edit the value in the lookup field, the grid actually
//has focus, so unless the keystroke is a Tab, then we need to
//send keystrokes to the LookupCombo

procedure TForm1.DBGrid1KeyPress(Sender: TObject; var Key: Char);
begin
  if (Key <> Chr(9)) then
    with DBGrid1, DBLookupComboBox1 do
      if (SelectedField.FieldName = DataField) then
      begin
        SetFocus;
        SendMessage(Handle, WM_CHAR, Word(Key), 0);
      end;
end;

//If you choose an item from the lookup, you give focus
//control to it. The net result is that it takes two
//Tabs to move to the next cell. In that case, we need
//to send another Tab keystroke to the grid so that only
//one keystroke is needed to move to the next cell.

procedure TForm1.DBGrid1KeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if (Key in [VK_TAB]) and InBox then
  begin
    SendMessage(DBGrid1.Handle, WM_KEYDOWN, Key, 0);
    InBox := False;
  end;
end;

procedure TForm1.DBLookupComboBox1Enter(Sender: TObject);
begin
  InBox := True;
end;

end.

So now, you have everything you need to "drop" a TDBLookupComboBox onto a grid. By the way, you can use this technique for ANY windowed component; that is, any component that has a Handle property. This includes forms, panels, memos, etc.. Try it out!

Note: Some of you old hats at Delphi might immediately exclaim, "What's the use of this article? In Delphi 3 and above, we have  the capability of specifying a cell in a DBGrid to be a drop-down edit." Well, that's the thing, isn't it? You have to fill in the values of the Items property yourself. What I'm suggesting here is adding a TDBLookupComboBox that will enable you to look up information from another data source. This isn't available in ANY version of Delphi.

By the way, this isn't my original idea, and in fact, the technique has been around since Delphi 1. But it's valid and applicable to later versions of Delphi.