2004. január 29., csütörtök

Object Inspector Shortcuts


Problem/Question/Abstract:

Object Inspector Shortcuts

Answer:

To display the Object Inspector's component pop-up menu, press [Ctrl][DownArrow]. This is a convenient way to select a component that's behind another component. To quickly select a specific component from the menu, press the key that corresponds to the first letter of the component's name.

If the names of several components start with the same letter, pressing the letter key again will move the focus to the next component in the menu that starts with that letter. (In other words, typing the full name doesn't help.)

To expand or collapse a nested property (such as Font, which defines subproperties such as Color or Height), select the property and press [Alt][F10], and then choose Expand or Collapse from the Object Inspector speed menu.

When the Object Inspector is active, you can toggle between the Properties and Events pages by pressing [Ctrl][Tab]. If you set your editor to IDE classic, as I prefer, you may use F6 for this as well.

To select a specific property or event, obviously you can use the arrow keys or the [PageUp] and [PageDown] keys. However, you can also select a property or event by name by pressing [Tab] to move the focus to the names and values, and then typing the first letter of the property or event name.

If you mistype a name and need to start again, press [Esc] once to return the focus to the beginning of the names. When you've selected the correct property or event, press [Tab] to move the focus from the name to the value.

2004. január 28., szerda

Generate the SELECT-statement in run-time


Problem/Question/Abstract:

Generate the script for SELECT-statement

Answer:

I want to publish a small procedure that generate a SELECT-statement for data of table. This code I uses in DIM: Database Information Manager (http://www.scalabium.com/download/dbinfo.zip):

function GetSelectTable(Dataset: TTable): TStrings;
var
  i: Integer;
  str: string;
begin
  Result := TStringList.Create;
  try
    for i := 0 to DataSet.FieldCount - 1 do
    begin
      if i = 0 then
        str := 'SELECT'
      else
        str := ',';
      str := str + ' ' + DataSet.Fields[i].FieldName;
      Result.Add(str);
    end;
    Result.Add('FROM ' + DataSet.TableName)
  except
    Result.Free;
    Result := nil;
  end;
end;

Of course, you can add the ORDER BY-clause (just iterate by index fields)...

2004. január 27., kedd

Windows detection routines


Problem/Question/Abstract:

Here is how to find out almost everything of windows versions.

Answer:

function IsWin31: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  Result := (Os.dwPlatformId = VER_PLATFORM_WIN32s);
end;

function IsWin95: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  result := (OS.dwMajorVersion >= 4) and (OS.dwMinorVersion = 0) and (OS.dwPlatformId
    = VER_PLATFORM_WIN32_WINDOWS);
end;

function IsWin95OSR2: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  result := (OS.dwMajorVersion >= 4) and (OS.dwMinorVersion = 0) and
    (lo(OS.dwBuildNumber) > 1000) and (OS.dwPlatformId = VER_PLATFORM_WIN32_WINDOWS);
end;

function IsWinNT: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  result := OS.dwPlatformId = VER_PLATFORM_WIN32_NT;
end;

function IsWin98: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  result := (OS.dwMajorVersion >= 4) and (OS.dwMinorVersion > 0) and (OS.dwPlatformId
    = VER_PLATFORM_WIN32_WINDOWS);
end;

function IsWin98se: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  result := (OS.dwMajorVersion >= 4) and (OS.dwMinorVersion > 0) and
    (lo(OS.dwBuildNumber) > 2000) and (OS.dwPlatformId = VER_PLATFORM_WIN32_WINDOWS);
end;

function IsWin2000: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  result := (OS.dwMajorVersion >= 5) and (OS.dwPlatformId = VER_PLATFORM_WIN32_NT);
end;

function IsWinXP: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  result := (OS.dwMajorVersion >= 5) and (OS.dwMinorVersion >= 1) and (OS.dwPlatformId
    = VER_PLATFORM_WIN32_NT);
end;

function IsWinMe: Boolean;
var
  OS: TOSVersionInfo;
begin
  ZeroMemory(@OS, SizeOf(OS));
  OS.dwOSVersionInfoSize := SizeOf(OS);
  GetVersionEx(OS);
  result := (OS.dwMajorVersion >= 4) and (OS.dwMinorVersion >= 90) and (OS.dwPlatformId
    = VER_PLATFORM_WIN32_WINDOWS);
end;

function GetNTType: string;
var
  r: TRegistry;
  ts: string;
begin

  Result := '[UNKNOWN]';

  if IsWinNT then
  begin
    r := TRegistry.Create;
    r.RootKey := HKEY_LOCAL_MACHINE;
    r.OpenKey('SYSTEM\CurrentControlSet\Control\ProductOptions', False);
    ts := AnsiUpperCase(R.ReadString('ProductType'));
    r.Free;
    if (ts = 'WINNT') then
    begin
      result := 'Workstation';
      if IsWin2000 then
        result := 'Professional';
    end
    else if (ts = 'SERVERNT') then
    begin
      result := 'Server';
    end
    else if (ts = 'LANMANNT') then
    begin
      result := 'Advanced Server';
    end;
  end;

end;

2004. január 26., hétfő

How to draw a TRadioGroup without a frame


Problem/Question/Abstract:

How to draw a TRadioGroup without a frame

Answer:

unit GSRadioGroup;

interface

uses
  Windows, SysUtils, Classes, Forms, ExtCtrls;

type
  TGSRadioGroup = class(TRadioGroup)
  private
    FBorderStyle: TBorderStyle;
    FValues: TStrings;
  protected
    procedure SetBorderStyle(Value: TBorderStyle);
    procedure Paint; override;
    function GetValues: TStrings;
    procedure SetValues(Value: TStrings);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property BorderStyle: TBorderStyle read FBorderStyle write SetBorderStyle default bsNone;
    property Values: TStrings read GetValues write SetValues;
  end;

procedure Register;

implementation

constructor TGSRadioGroup.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FBorderStyle := bsNone;
  FValues := TStringList.Create;
end;

destructor TGSRadioGroup.Destroy;
begin
  FValues.Free;
  inherited Destroy;
end;

function TGSRadioGroup.GetValues;
begin
  Result := FValues;
end;

procedure TGSRadioGroup.SetValues(Value: TStrings);
begin
  if Value <> FValues then
  begin
    FValues.Assign(Value);
  end;
end;

procedure TGSRadioGroup.SetBorderStyle(Value: TBorderStyle);
begin
  if FBorderStyle <> Value then
  begin
    FBorderStyle := Value;
    RecreateWnd;
  end;
end;

procedure TGSRadioGroup.Paint;
var
  c: Integer;
  diff: Integer;
  H: Integer;
  R: TRect;
begin
  if FBorderStyle = bsSingle then
    inherited Paint
  else
  begin
    with Canvas do
    begin
      if Text <> EmptyStr then
      begin
        Font := Self.Font;
        H := TextHeight('0');
        R := Rect(8, 0, 0, H);
        DrawText(Handle, PChar(Text), Length(Text), R, DT_LEFT or DT_SINGLELINE or DT_CALCRECT);
        Brush.Color := Color;
        DrawText(Handle, PChar(Text), Length(Text), R, DT_LEFT or DT_SINGLELINE);
      end
      else
      begin
        if ControlCount > 0 then
        begin
          diff := Controls[0].Top;
          for c := 0 to ControlCount - 1 do
          begin
            Controls[c].Top := Controls[c].Top - diff;
          end;
          {You may want to adjust the height here}
        end;
      end;
    end;
  end;
end;

procedure Register;
begin
  RegisterComponents('Garlin', [TGSRadioGroup]);
end;

end.

2004. január 25., vasárnap

How to reverse a string


Problem/Question/Abstract:

How to reverse a string

Answer:

Here are three examples how to reverse a string:


#1, While easy to understand suffers from a lot of memory reallocation. Each time the next letter is added to s2, it's added to the beginning of the string causing a reallocation of the entire string.


function ReverseString(s: string): string;
var
  i: integer;
  s2: string;
begin
  s2 := '';
  for i := 1 to Length(s) do
    s2 := s[i] + s2;
  Result := s2;
end;


#2, Taking advantage of the fact that we can work at both ends of the string at once AND the fact that IF there is a middle character, ie. an odd number of characters in the string, it doesn't change position at all and we can eliminate all the memory allocations, work completely within the source string swapping from end to end working toward the middle and only having to make 1/2 of a loop through the string.



procedure ReverseStr(var Src: string);
var
  i, j: integer;
  C1: char;
begin
  j := Length(Src);
  for i := 1 to (Length(Src) div 2) do
  begin
    C1 := Src[i];
    Src[i] := Src[j];
    Src[j] := C1;
    Dec(j);
  end;
end;


#3, One disadvantage of #2 can be seen when trying to fill one control with the contents of another.  For example, two TEdits.  Since TEdit.Text can't be sent as a var parameter you'll need to first make use of a temporary string and then set the second TEdit:


var
  tStr: string;
begin
  tStr := Edit1.Text;
  ReverseStr(tStr);
  Edit2.Text := tStr;


However, using #3 this code turns into,


Edit2.Text := ReverseStr(Edit1.Text);

In addition, we lost 1 local var and the loop body was reduced since we could use Result directly swapping as we go!


function ReverseStr(const Src: string): string;
var
  i, j: integer;
begin
  j := Length(Src);
  SetLength(Result, j);
  for i := 1 to (Length(Src) div 2) do
  begin
    Result[i] := Src[j];
    Result[j] := Src[i];
    Dec(j);
  end;
end;

2004. január 24., szombat

How to really make a resource file


Problem/Question/Abstract:

Creating a sort of uncompressed Zip file to store all the files required for a game or any other program that requires additional files.

Answer:

Instead of having loads of files for your games distributed all over the place, you can stick all your files into a single package, you find these used in almost every game out.

To make these files requires a header which can be a set length then all the files, followed by each files information in equal segments:

[HEADER]
[FILE1]
[FILE2]
...
[FILEN]
[FILE1INFO]
[FILE2INFO]
...
[FILENINFO]

this can easily be achieved and you can have lots of other 'addins' such as putting files in sub directories, special properties being set for each file etc....

first off you need a header, this usually consists of 4 things

type
  header = record
    Signature: array[1..4] of char;
    Version: LongInt;
    fileoffset: LongInt;
    fileentries: LongInt;
  end;

The signiture could be anything that you wish, but it is used for checking if the file is a valid package file for your program. next is the version, you may wish to improve the package file over time or have an increment system for your application so that a file in an newer package, determined by the version number would overide that of a file in an older package.

Next is the fileoffset, this points to the begining of the file info section after the last file in the package.

FILEENTRIES is used for counting how many file info entries there are, so u can have a loop running reading off the entries if you so wished.

so create ur file then write in the header

wfile.Write(head, SizeOf(head));

next comes the adding of the actual files this can be done by using TFileStream then

rfile.create('filename', fmopenread);

wfile.copyfrom(rfile, rfile.size);

continue writing the files next comes writing the file info, this must be done either after adding all the files or while adding one file to the final file you create a temporary file and add the file info to the file then, then when finished 'stick' the temp file onto the end of the final file. There is other options available, but they are upto you to discover.

The File Info entries MUST be all of the same size for this example i have used 44 bytes but you could use anything aslong as it is the same, having large file info entries will dramatically increase your file size so i would sujest someting around 44 bytes.

type
  tfilenametype = array[0..29] of char;
  direntry = record
    offset: longint;
    size: longint;
    filename: tfilenametype;
    timestamp: longint;
  end;

offset = the position from the begining of the package file. and the size value = the size of that file it refrences. so that you can seek and read the file out of the package. Filename is obvious and the timestamp would be

fileage('filename');

add this all into a file and then u have your package, reading it is just of case of reading instead of writing the file, but using this as a guide you could take this far.

Check this GDC article if this is not enought for you.

2004. január 23., péntek

Registering an ActiveX for its class


Problem/Question/Abstract:

How getting the IUnknown reference on a specific COM object's instance created by an application ?

Answer:

The RegisterActiveObject function -from the Win32 API- can register an object by passing its IUnknown reference and its CLSID to make it the active object for its CLSID.
Registration causes the object to be listed in OLE's running object table, a globally accessible lookup table that keeps track of the objects that are currently running on your computer.
An application can then create an OLE automation object for example, register it as the active object at startup.
Other application can have access to this particular instance by getting a IDispatch reference with the Delphi's GetActiveOleObject using its progID.

I've placed the registration mecanism in the TActiveObject class showed bellow and you can download the demo applications.

unit ActiveObject;

// Written by Bertrand Goetzmann (http://www.object-everywhere.com)
// Keywords : RegisterActiveObject, CoLockObjectExternal, RevokeActiveObject, CoDisconnectObject, GetActiveOleObject, GetActiveObject

interface

type
  TActiveObject = class
  private
    FUnk: IInterface;
    FRegister: Integer;
  public
    constructor Create(Unk: IInterface; const clsid: TGUID); overload;
    constructor Create(Unk: IInterface; const ProgId: string); overload;
    destructor Destroy; override;
  end;

implementation

uses ActiveX, ComObj;

{ TActiveObject }

constructor TActiveObject.Create(Unk: IInterface; const clsid: TGUID);
begin
  inherited Create;
  FUnk := Unk;
  OleCheck(RegisterActiveObject(FUnk, clsid, ACTIVEOBJECT_WEAK, FRegister));
  OleCheck(CoLockObjectExternal(FUnk, True, True));
end;

constructor TActiveObject.Create(Unk: IInterface; const ProgId: string);
begin
  Create(Unk, ProgIDToClassID(ProgId));
end;

destructor TActiveObject.Destroy;
begin
  OleCheck(CoLockObjectExternal(FUnk, False, True));
  OleCheck(RevokeActiveObject(FRegister, nil));
  OleCheck(CoDisconnectObject(FUnk, 0));
  inherited;
end;

end.

In the demo applications, OleObject.dll is the implementation of the OLE automation object with "OleObject.Test" as progId and supporting the ITest interface. This interface has a single property named Message : you can read or write a simple string of characters.
The AppTest.exe creates an instance of this OLE automation object and register it with an instance of TActiveObject. When the applicatino shut down, the registration of the active objet is revoked.
Start several instances of ClientTest. ClientTest gets the IDispatch reference, via a Variant variable, on the active object by using a call of GetActiveOleObject('OleObject.Test'), to set or get the Message property value.

I think it is a powerful way to make applications more collaborative.


Component Download: http://perso.worldonline.fr/objecteverywhere/ActiveObject.zip

2004. január 22., csütörtök

Revert all controls on a TForm to design-time values when clicking on a button at runtime


Problem/Question/Abstract:

Is it possible to reset the state of controls like TEdit.text, TCheckBox.Checked, etc. at runtime to their original design-time values without assigning the property values for each control again?

Answer:

If I understand you correctly you want all controls on the form to revert to the design-time values when the user clicks the a cancel button, for example. The generic way would be to reload the controls from the form resource. The main problem is that you have to delete all components on the form first or you get a load of errors since the component loading code really creates new instances of all components on the form.

unit Unit1;

interface

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

const
  UM_RELOADFORM = WM_USER + 321;

type
  TForm1 = class(TForm)
    Button1: TButton;
    CheckBox1: TCheckBox;
    CheckBox2: TCheckBox;
    CheckBox3: TCheckBox;
    RadioGroup1: TRadioGroup;
    RadioGroup2: TRadioGroup;
    CheckBox4: TCheckBox;
    CheckBox5: TCheckBox;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    procedure UMReloadForm(var msg: TMessage); message UM_RELOADFORM;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.Button1Click(Sender: TObject);
begin
  {Delay action until button click code has finished executing}
  PostMessage(handle, UM_RELOADFORM, 0, 0);
end;

procedure TForm1.UMReloadForm(var msg: TMessage);
var
  i: Integer;
  rs: TResourceStream;
begin
  {Block form redrawing}
  Perform(WM_SETREDRAW, 0, 0);
  try
    {Delete all components on the form}
    for i := ComponentCount - 1 downto 0 do
      Components[i].Free;
    {Find the forms resource}
    rs := TResourceStream.Create(FindClassHInstance(TForm1), Classname, RT_RCDATA);
    try
      {Recreate components from the form resource}
      rs.ReadComponent(self);
    finally
      rs.free
    end;
  finally
    {Redisplay form}
    Perform(WM_SETREDRAW, 1, 0);
    Invalidate;
  end;
end;

end.

2004. január 21., szerda

Queue up message forms in a TStringList


Problem/Question/Abstract:

I use a timer to check some conditions. If something special happens, I display a message form (using MyMessage.ShowModal, because I need an answer from the user). The timer goes on, so several of this Messages could be displayed simultaneously. What I want to do: Queue this messages and just display one. If the message is done, the next one is to be displayed.

Answer:

Simple idea: Instead of immediately popping up each message, queue them up into a stringlist. With a second timer (set to an appropriate interval) , process the message list, and delete/ take action. See example below (variations with enabling/ disabling timer2 are possible)

procedure TForm1.FormCreate(Sender: TObject);
begin
  MsgList := TStringList.Create;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  MsgList.Free;
end;

{check for abnormal conditions}

procedure TForm1.Timer1Timer(Sender: TObject);
const
  msgNumber: integer = 0;
begin
  if Random > 0.5 then
  begin
    MsgList.Add('message ' + IntToStr(msgNumber) + ' @ ' + TimeToStr(Now));
    inc(MsgNumber);
  end;
end;

{process messages}

procedure TForm1.Timer2Timer(Sender: TObject);
begin
  Timer2.Enabled := false; {extremely important !}
  while MsgList.Count > 0 do
  begin
    {show oldest messages first}
    ShowMessage(MsgList[0] + ' viewed @' + TimeToStr(Now));
    MsgList.Delete(0);
    {your specific actions ...}
  end;
  Timer2.Enabled := true;
end;

2004. január 20., kedd

How to save multiple records and an integer into one file


Problem/Question/Abstract:

I am writing an adventure game and need to store information in a save game. The game requires data from 3 different records and one variable

record1 = hotspot scene information(50 recs),
record2 = conversation information (60 recs),
record3 = hypertext information(50 recs)
variable = integer - # of scene currently on.

My problem is that I need to seek for a particular record of particular type in the file (I do not want to have to keep huge arrays of records in memory). I know how to do this with a file containing records of only one record type but have no clue how to combine all three records and one integer into a single random access file.

Answer:

I generally use a file with a header, then just keep the header in memory and use it to seek to the records I need.

type
  TSaveHeader = record
    scene: Integer;
    hotspots: LongInt;
    talk: LongInt;
    hype: LongInt;
  end;

var
  SaveHeader: TSaveHeader;

procedure OpenSaveFile(fname: string);
var
  f: file;
  i: Integer;
begin
  AssignFile(f, fname);
  Reset(f, 1);
  BlockRead(f, SaveHeader, Sizeof(TSaveHeader));
  { get one set of records }
  Seek(f, SaveHeader.hotspots);
  for i := 1 to 50 do
    BlockRead(f, somevar, sizeof_hotspotrec);
  { and so on }
  CloseFile(f);
end;

{ assuming the file is open }

procedure GetHotspotRec(index: LongInt; var hotspotrec: THotspot);
var
  offset: LongInt;
begin
  offset := SaveHeader.hotspots + index * Sizeof(THotSpot);
  Seek(f, offset);
  BlockRead(f, hotspotrec, Sizeof(THotspot));
end;

2004. január 19., hétfő

Searching Strings by the way they sound (2)


Problem/Question/Abstract:

How to match strings based on the way they sound & not on their spellings.

Answer:

This article is in continuation of my previous article "Searching Strings by the way they sound" and represents an attempt at making the SoundEx() more versatile so as to theoratically accomodate languages other than English - the only restriction being that the language should use the ASCII character set. Another advantage is that the function can be "tuned" to peculiarities of a language e.g. "Knife" is pronounced as "Nife" in English. There is theoratically no limit to this "tunability" - of course with corresponding decrease in performance. But you can get amazing results which are better than what SoundEx() gives.

I have chosen to post a new article rather than update the original one since the original function has been modified quite significantly (in concept) thus making it different from the industry standard SoundEx() function - which was implemented in the original article.

Since the function now supports language "tuning", it can give different results than the industry standard SoundEx(). I have thus renamed the function to "Sound()". This also gives me the freedom to implement it differently.

Sound() returns the same value (M240) for each of Micael/Maical/Michael/Maichael. Additionally, since it has been (partially) tuned for English, it will give the same result (F500) for "Phone"/"Fone".

I guess the "Ultimate" Sound Matching logic will be based on phonemes - of which I currently know very little. If you help me by providing me details of phonemes that you may have, then I will make yet another attempt at improving "Sound()" even further...

I thank Toninho Nunes and Joe Meyer for providing me ideas & inputs respectively.

Please save the code below in a file called "Sounds.pas". You will need to include the file in your source (Uses Sounds) and then you will have access to the Sound() function.

{********************************************************************}
{* Description: Modified Soundex function in which it is attempted to include *}
{* language pecularities which theoratically makes it adaptable to languages  *}
{* other than English - the only restriction being that the language in       *}
{* question should use ASCII character set                                    *}
{********************************************************************}
{* Date Created  : 15-Nov-2000                                                *}
{* Last Modified : 16-Nov-2000                                                *}
{* Version       : 0.10                                                       *}
{* Author        : Paramjeet Reen                                             *}
{* eMail         : Paramjeet.Reen@EudoraMail.com                              *}
{******************************************************************************}
{* This program is based on an algorithm that I had found in a magazine,      *}
{* merged with an algorithm of a program posted by Joe Meyer. I do not        *}
{* gurantee the fitness of this program in any way. Use it at your own risk.  *}
{********************************************************************}
{* Category: Freeware.                                                        *}
{********************************************************************}

unit Sounds;

interface

//Returns a code for InpStr depending upon how it sounds.
function Sound(const InpStr: ShortString): ShortString;

implementation

type
  TReplacePos = (pStart, pMid, pEnd);
  TReplacePosSet = set of TReplacePos;

const
  {********************************************************************}
  {* The following are selected letters of the alphabet which are divided     *}
  {* into their corresponding code (1-6). You might need to modify these for  *}
  {* different languages depending upon whether the language requires         *}
  {* alphabets other than the ones specified below                            *}
  {********************************************************************}
  Chars1 = ['B', 'P', 'F', 'V'];
  Chars2 = ['C', 'S', 'K', 'G', 'J', 'Q', 'X', 'Z'];
  Chars3 = ['D', 'T'];
  Chars4 = ['L'];
  Chars5 = ['M', 'N'];
  Chars6 = ['R'];

procedure ReplaceStr(var InpStr: ShortString; const SubStr, WithStr: ShortString;
  const ReplacePositions: TReplacePosSet);
var
  i: Integer;
begin
  if (pStart in ReplacePositions) then
  begin
    i := Pos(SubStr, InpStr);

    if (i = 1) then
    begin
      Delete(InpStr, i, Length(SubStr));
      Insert(WithStr, InpStr, i);
    end;
  end;

  if (pMid in ReplacePositions) then
  begin
    i := Pos(SubStr, InpStr);

    while (i > 1) and (i <= (Length(InpStr) - Length(SubStr))) do
    begin
      Delete(InpStr, i, Length(SubStr));
      Insert(WithStr, InpStr, i);
      i := Pos(SubStr, InpStr);
    end;
  end;

  if (pEnd in ReplacePositions) then
  begin
    i := Pos(SubStr, InpStr);

    if (i > 1) and (i > (Length(InpStr) - Length(SubStr))) then
    begin
      Delete(InpStr, i, Length(SubStr));
      Insert(WithStr, InpStr, i);
    end;
  end;
end;

function Sound(const InpStr: ShortString): ShortString;
var
  vStr: ShortString;
  PrevCh: Char;
  CurrCh: Char;
  i: Word;
begin
  {********************************************************************}
  {* Uppercase & remove invalid characters from given string                  *}
  {********************************************************************}
  {* Please have a long & hard look at this code if you have modified any of  *}
  {* the constants Chars1,Chars2 ... Chars6 by increasing the overall range   *}
  {* of alphabets                                                             *}
  {********************************************************************}
  vStr := '';
  for i := 1 to Length(InpStr) do
    case InpStr[i] of
      'a'..'z': vStr := vStr + UpCase(InpStr[i]);
      'A'..'Z': vStr := vStr + InpStr[i];
    end; {case}

  if (vStr <> '') then
  begin
    {**************************************************************************}
    {* Language Tweaking Section                                              *}
    {********************************************************************}
    {* Tweak for language peculiarities e.g. "CAt"="KAt", "KNIfe"="NIfe"      *}
    {* "PHone"="Fone", "PSYchology"="SIchology", "EXcel"="Xcel" etc...        *}
    {* You will need to modify these for different languages. Optionally, you *}
    {* may choose not to have this section at all, in which case, the output  *}
    {* of Sound() will correspond to that of SoundEx(). Please note however   *}
    {* the importance of what you replace & the order in which you replace.   *}
    {********************************************************************}
    {* Also, please note that the following replacements are targeted for the *}
    {* English language & that too is subject to improvements                 *}
    {********************************************************************}
    ReplaceStr(vStr, 'CA', 'KA', [pStart, pMid, pEnd]); //arCAde = arKAde
    ReplaceStr(vStr, 'CL', 'KL', [pStart, pMid, pEnd]); //CLass  = Klass
    ReplaceStr(vStr, 'CK', 'K', [pStart, pMid, pEnd]); //baCK   = baK
    ReplaceStr(vStr, 'EX', 'X', [pStart, pMid, pEnd]); //EXcel  = Xcel
    ReplaceStr(vStr, 'X', 'Z', [pStart]); //Xylene = Zylene
    ReplaceStr(vStr, 'PH', 'F', [pStart, pMid, pEnd]); //PHone  = Fone
    ReplaceStr(vStr, 'KN', 'N', [pStart]); //KNife  = Nife
    ReplaceStr(vStr, 'PSY', 'SI', [pStart]); //PSYche = SIche
    ReplaceStr(vStr, 'SCE', 'CE', [pStart, pMid, pEnd]); //SCEne  = CEne

    {********************************************************************}
    {* String Assembly Section                                                *}
    {********************************************************************}
    PrevCh := #0;
    Result := vStr[1];
    for i := 2 to Length(vStr) do
    begin
      if Length(Result) = 4 then
        break;

      CurrCh := vStr[i];
      if (CurrCh <> PrevCh) then
      begin
        if CurrCh in Chars1 then
          Result := Result + '1'
        else if CurrCh in Chars2 then
          Result := Result + '2'
        else if CurrCh in Chars3 then
          Result := Result + '3'
        else if CurrCh in Chars4 then
          Result := Result + '4'
        else if CurrCh in Chars5 then
          Result := Result + '5'
        else if CurrCh in Chars6 then
          Result := Result + '6';

        PrevCh := CurrCh;
      end;
    end;
  end
  else
    Result := '';

  while (Length(Result) < 4) do
    Result := Result + '0';
end;

end.

2004. január 18., vasárnap

Saving List Box Data at Runtime (TFileStream)


Problem/Question/Abstract:

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

Answer:

Note: A sample program is available. Even though this article focuses on saving a list box at runtime, it really presents a general overview of using the TFileStream class for streaming components to and from disk. This is an important distinction to make because while I use the TListBox as an example, it is possible to apply the concepts to almost all components.

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

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

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

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

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

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

A simple example

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

unit main;

interface

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

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

var
  Form1: TForm1;

implementation

{$R *.DFM}

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

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

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

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

end.

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

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

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

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

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

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

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

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

2004. január 17., szombat

How to hook into Windows' built-in screenshot function


Problem/Question/Abstract:

How to hook into Windows' built-in screenshot function

Answer:

{ ... }
if not fullScreen then
  Keybd_Event(VK_MENU, 0, 0, 0);
Keybd_Event(VK_SNAPSHOT, 0, 0, 0);
Keybd_Event(VK_SNAPSHOT, 0, KEYEVENTF_KEYUP, 0);
if not fullScreen then
  Keybd_Event(VK_MENU, 0, KEYEVENTF_KEYUP, 0);
{ ... }

The fullScreen value tells, if you wish to get a windowed printscreen or the full screen.

2004. január 16., péntek

How to remove white-spaces from a string


Problem/Question/Abstract:

I need to be able to search through a list of strings and remove the ones that only contain what I call "white space" - spaces, tabs, control chars, etc.. Is there a function (either Delphi or WinAPI) that will do this?

Answer:

Solve 1:

procedure RemoveBlanks(sl: TStringList);
var
  i, j: Integer;
  blank: Boolean;
  c: Char;
  chars: array[Char] of Boolean;
begin
  { Set all significant chars to false }
  FillChar(chars, SizeOf(chars), True);
  for c := 'A' to 'Z' do
    chars[c] := False;
  for c := 'a' to 'z' do
    chars[c] := False;
  for c := '0' to '9' do
    chars[c] := False;
  i := Pred(sl.Count);
  while (i >= 0) do
  begin
    blank := True;
    j := Length(sl[i]);
    while (blank and (j >= 0)) do
    begin
      blank := blank and chars[sl[i][j]];
      Dec(j);
    end;
    if blank then
      sl.Delete(i);
    Dec(i);
  end;
end;


Solve 2:

procedure DeleteWhiteLines(Strings: TStrings);
var
  I: Integer;
begin
  for I := Strings.Count - 1 downto 0 do
    if TrimLeft(Strings[I]) = '' then
      Strings.Delete(I);
end;


Solve 3:

function KeepStr(sSource: string; ValidChars: TCharSet): string;
var
  iCurPos: Integer;
begin
  Result := Trim(sSource);
  iCurPos := 1;
  if Length(Result) > 0 then
  begin
    repeat
      if Result[iCurPos] in ValidChars then
        Inc(iCurPos)
      else
        Delete(Result, iCurPos, 1);
      if length(Result) = 0 then
        break;
    until (iCurPos = Length(Result) + 1);
  end;
end;

You use KeepStr like this:

type
  TCharSet = set of char;

var
  i: integer;
  s: string;
begin
  {AList is a TStringList declared somewhere}
  {have to work from the end of the list}
  for i := pred(AList) downto 0 do
  begin
    s := AList[i];
    s := KeepStr(s, ['A'..'Z'] + ['a'..'z'] + ['0'..'9']);
    if s = '' then
      AList.Delete(i);
  end;
end;