2009. október 31., szombat

How to draw a rotated ellipse at a specific angle (2)


Problem/Question/Abstract:

I created an object based off of TGraphicControl and I used the TCanvas.Ellipse method to create an ellipse. I would like to give the user the ability to rotate this ellipse. I would also like to give the user the ability after rotating the ellipse to still adjust the size and shape.

Answer:

You can use Win32GDI Routines. It works like this:


procedure RotatedEllipse(aCanvas: TCanvas; X1, Y1, X2, Y2: Integer);
var
  T, O: TXForm; {in unit Windows}
begin
  { ... }
  SetGraphicsMode(aCanvas.Handle, GM_Advanced);
  GetWorldTransform(aCanvas.Handle, O);
  {Angle in degree}
  T.eM11 := 1 * Cos(w / 360 * Pi * 2);
  T.eM22 := 1 * Cos(w / 360 * Pi * 2);
  T.eM12 := 1 * Sin(w / 360 * Pi * 2);
  T.eM21 := 1 * -Sin(w / 360 * Pi * 2);
  T.eDX := Round((X1 + X2) / 2);
  T.eDY := Round((Y1 + Y2) / 2);
  ModifyWorldTransform(aCanvas.Handle, T, MWT_LEFTMULTIPLY);
  Canvas.Ellipse(X1, Y1, X2, Y2);
  SetWorldTransform(TheDraw.Handle, O);
end;

2009. október 30., péntek

Define variables in a Word document and set their values programmatically


Problem/Question/Abstract:

I need to define some variables in a Word document and be able to set their values from my Delphi program. How can I do that?

Answer:

You can do that using custom document properties:

uses
  Office97; {or Office2000, OfficeXP, Office_TLB}

var
  VDoc, PropName, DocName: OleVariant;
  VDoc := Word.ActiveDocument;
  { ... }

{ Set a document property }
PropName := 'MyOpinionOfThisDocument';
VDoc.CustomDocumentProperties.Add(PropName, False, msoPropertyTypeString,
  'Utter drivel', EmptyParam);
{ Read a document property }
Caption := VDoc.CustomDocumentProperties[PropName].Value;
{ ... }

2009. október 29., csütörtök

Faster recordcount for sqlserver clientserver applications


Problem/Question/Abstract:

When using the standard dataset.recordcount in my client-server (win nt against sqlserver7 db, targettable has 500.000 records) i can go for lunch and stil be waiting (:-

Answer:

For those of you who don't know why u should not use the standard dataset.recordcount when developing client server database applications.
This article is especialy for those cs db apps against a sqlserver 7 db.

since the standard dataset.recordcount iterates from begin of the table through the end of the table to result in the recordcount. This is a crime when developing cs db apps (against sqlserver7).

simply use another way of obtaining the number of records. I use a sql for obtaining the number of records in a sqlserver table.

drop a tquery on the form

provide this tquery with the follow SQL:

SQL:

select distinct max(itbl.rows)
from sysindexes as itbl
inner join sysobjects as otbl on (itbl.id = otbl.id)
where (otbl.type = 'U') and (otbl.name = :parTableName)

notice the parameter: parTableName type string

use this tquery to find out how many rows in the table

TIP: try to make your own tYourSqlServerCountQuery and thus override the recordcount property.
ByTheWay: use this only for sqlserver

for other cs db apps simply use a count sql (coming upnext time...)

2009. október 28., szerda

Set system date and time


Problem/Question/Abstract:

Set system date and time

Answer:

With the procedure SetDateTime you can set the date and time of the operating system, from within your Delphi application.

In the interface-section you define the procedure:

procedure SetDateTime(Year, Month, Day, Hour, Minu, Sec, MSec: Word);

In the 'implementation' you write...:

{ SetDateTime sets the date and time of the operating system }

procedure SetDateTime(Year, Month, Day, Hour, Minu, Sec, MSec: Word);
var
  NewDateTime: TSystemTime;
begin
  FillChar(NewDateTime, sizeof(NewDateTime), #0);
  NewDateTime.wYear := Year;
  NewDateTime.wMonth := Month;
  NewDateTime.wDay := Day;
  NewDateTime.wHour := Hour;
  NewDateTime.wMinute := Minu;
  NewDateTime.wSecond := Sec;
  NewDateTime.wMilliseconds := MSec;
  SetLocalTime(NewDateTime);
end;

2009. október 27., kedd

Determining the associated application


Problem/Question/Abstract:

How can I get the application associated with a document?

Answer:

Where is that information?

The applications associated with the file extensions are stored in the Windows Registry. To get this information first we should retrieve the "class" that a file extensions belongs to. This information can be found at:

  HKEY_CLASSES_ROOT\.ext\(default)

where ".ext" is the file extension you want (like ".txt", ".bmp", etc.). Then we get the command line used to open that kind of files. To do that, we retrieve the data under

  HKEY_CLASSES_ROOT\class\Shell\Open\Command\(default)

where "class" is the file class an extension belongs to. That string usually has the form

  "D:\PATH\APPNAME.EXT" "%1" -OPTIONS

where %1 is a placeholder for the document file to open with the application, so we should find its position within the string and replace it with the filename we want to open.

Example

The following function returns the command line of the associated application to open a documente file:

function GetAssociation(const DocFileName: string): string;
var
  FileClass: string;
  Reg: TRegistry;
begin
  Result := '';
  Reg := TRegistry.Create(KEY_EXECUTE);
  Reg.RootKey := HKEY_CLASSES_ROOT;
  FileClass := '';
  if Reg.OpenKeyReadOnly(ExtractFileExt(DocFileName)) then
  begin
    FileClass := Reg.ReadString('');
    Reg.CloseKey;
  end;
  if FileClass <> '' then
  begin
    if Reg.OpenKeyReadOnly(FileClass + '\Shell\Open\Command') then
    begin
      Result := Reg.ReadString('');
      Reg.CloseKey;
    end;
  end;
  Reg.Free;
end;

Copyright (c) 2001 Ernesto De Spirito
Visit: http://www.latiumsoftware.com/delphi-newsletter.php

2009. október 26., hétfő

Clear all edit controls on your form


Problem/Question/Abstract:

Clear all edit controls on your form

Answer:

The shortest way to do this:

procedure TForm1.ClearAll;
var
  i: integer;
begin
  for i := 0 to ComponentCount - 1 do
    if (Components[i] is TEdit) then
      (Components[i] as TEdit).Text := '';
end;

2009. október 25., vasárnap

BDE alias info


Problem/Question/Abstract:

BDE alias info

Answer:

The following function uses the GetAliasParams method of TSession to get the directory mapping for an alias:

uses DbiProcs, DBiTypes;

function GetDataBaseDir(const Alias: string): string;
{* Will return the directory of the database given the alias
  (without trailing backslash) *}
var
  sp: PChar;
  Res: pDBDesc;
begin
  try
    New(Res);
    sp := StrAlloc(length(Alias) + 1);
    StrPCopy(sp, Alias);
    if DbiGetDatabaseDesc(sp, Res) = 0 then
      Result := StrPas(Res^.szPhyName)
    else
      Result := '';
  finally
    StrDispose(sp);
    Dispose(Res);
  end;
end;

2009. október 24., szombat

DBGrid Component that show deleted and updated and inserted new records in diffrent colors


Problem/Question/Abstract:

DBGrid Show all state of the related DataSet

Answer:

This component show state of the related data set of DBGrid and used for data base controlers. It's appreciate to send any note or comment or suggestion to Vafaeija@yahoo.com

unit atcDBGrid;
{*
  (c) Aveen Tech
  2001 - 2002

  FileName: atcDBGrid.pas

  Version       Date            Author             Comment
  1.0           13/06/2000      Majid Vafai Jahan  Create.

OVERVIEW
  - This grid is inherited from DBGrid and add some required functionality to it.

Functionality:
  - Record type are all records that may be modified, unmodified, inserted, deleted.
  - Coloring according to Record type.
  - show selected Record Type.

*}

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Grids, DBGrids, dbTables, db;
const
  AlignFlags: array[TAlignment] of Integer =
  (DT_LEFT or DT_WORDBREAK or DT_EXPANDTABS or DT_NOPREFIX,
    DT_RIGHT or DT_WORDBREAK or DT_EXPANDTABS or DT_NOPREFIX,
    DT_CENTER or DT_WORDBREAK or DT_EXPANDTABS or DT_NOPREFIX);
  RTL: array[Boolean] of Integer = (0, DT_RTLREADING);
type
  TCachedShow = (csModify, csUnModify, csRemoved, csInserted, csAll, csNormal);
  TatcDBGrid = class(TDBGrid)
  private
    FCachedShow: TCachedShow;
    FModifiedColor: TColor;
    FInsertedColor: TColor;
    FDeletedColor: TColor;
    procedure SetCachedShow(const Value: TCachedShow);
  protected
    procedure DrawDataCell(const Rect: TRect; Field: TField;
      State: TGridDrawState); override;
    procedure DrawColumnCell(const Rect: TRect; DataCol: Integer;
      Column: TColumn; State: TGridDrawState); override;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property atcCachedShow: TCachedShow read FCachedShow write SetCachedShow;
    property atcDeletedColor: TColor read FDeletedColor write FDeletedColor;
    property atcInsertedColor: TColor read FInsertedColor write FInsertedColor;
    property atcModifiedColor: TColor read FModifiedColor write FModifiedColor;
  end;

procedure Register;

implementation



procedure Register;
begin
  RegisterComponents('ATC DB Compo', [TatcDBGrid]);
end;



constructor TatcDBGrid.Create(AOwner: TComponent);
{*
  Description: Record Type Showing is All except Deletes.
*}

begin
  inherited;
  FCachedShow := csNormal;
  FDeletedColor := clGray;
  FInsertedColor := clAqua;
  FModifiedColor := clRed;
end;



procedure TatcDBGrid.DrawColumnCell(const Rect: TRect; DataCol: Integer;
  Column: TColumn; State: TGridDrawState);
{*
  Description: On Drawing Column Color Updated Records.
*}
var
  ARect: TRect;
begin
  inherited;
  if not Assigned(Column.Field) then
    exit;
  // Copy Rect into Variable.
  CopyRect(ARect, Rect);
  if Assigned(DataLink) and (DataLink.Active) and (DataLink.DataSet <> nil) then
  begin
    // if current record is modified
    if DataLink.DataSet.UpdateStatus = usModified then
    begin
      Canvas.Brush.Color := atcModifiedColor;
      Canvas.Font.Color := clBlack;
      Canvas.FillRect(Rect);
      DrawText(Canvas.Handle, PChar(Column.Field.Text), Length(Column.Field.Text),
        ARect,
        AlignFlags[Column.Alignment] or
          RTL[UseRightToLeftAlignmentForField(Column.Field, Column.Alignment)]);
    end
      // if current record is inserted.
    else if DataLink.DataSet.UpdateStatus = usInserted then
    begin
      Canvas.Brush.Color := atcInsertedColor;
      Canvas.Font.Color := clBlack;
      Canvas.FillRect(Rect);
      DrawText(Canvas.Handle, PChar(Column.Field.Text), Length(Column.Field.Text),
        ARect,
        AlignFlags[Column.Alignment] or
          RTL[UseRightToLeftAlignmentForField(Column.Field, Column.Alignment)]);
    end
      // if current record is deleted.
    else if DataLink.DataSet.UpdateStatus = usDeleted then
    begin
      Canvas.Brush.Color := atcDeletedColor;
      Canvas.Font.Color := clWhite;
      Canvas.FillRect(Rect);
      DrawText(Canvas.Handle, PChar(Column.Field.Text), Length(Column.Field.Text),
        ARect,
        AlignFlags[Column.Alignment] or
          RTL[UseRightToLeftAlignmentForField(Column.Field, Column.Alignment)]);
    end;
  end;
end;



procedure TatcDBGrid.DrawDataCell(const Rect: TRect; Field: TField;
  State: TGridDrawState);
{*
  Description: Draw Cell
*}
var
  ARect: TRect;
begin
  inherited;
  CopyRect(ARect, Rect);

  if Assigned(DataLink) and (DataLink.Active) and (DataLink.DataSet <> nil) then
  begin
    // if current record is modified.
    if DataLink.DataSet.UpdateStatus = usModified then
    begin
      Canvas.Brush.Color := clRed;
      Canvas.Font.Color := clBlack;
      Canvas.FillRect(Rect);
      DrawText(Canvas.Handle, PChar(Field.Text), Length(Field.Text), ARect,
        AlignFlags[Field.Alignment] or RTL[UseRightToLeftAlignmentForField(Field,
          Field.Alignment)]);
    end
      // if current record is inserted.
    else if DataLink.DataSet.UpdateStatus = usInserted then
    begin
      Canvas.Brush.Color := clAqua;
      Canvas.Font.Color := clBlack;
      Canvas.FillRect(Rect);
      DrawText(Canvas.Handle, PChar(Field.Text), Length(Field.Text), ARect,
        AlignFlags[Field.Alignment] or RTL[UseRightToLeftAlignmentForField(Field,
          Field.Alignment)]);
    end
      // if current record is deleted.
    else if DataLink.DataSet.UpdateStatus = usDeleted then
    begin
      Canvas.Brush.Color := clGray;
      Canvas.Font.Color := clWhite;
      Canvas.FillRect(Rect);
      DrawText(Canvas.Handle, PChar(Field.Text), Length(Field.Text), ARect,
        AlignFlags[Field.Alignment] or RTL[UseRightToLeftAlignmentForField(Field,
          Field.Alignment)]);
    end;
  end;
end;



procedure TatcDBGrid.SetCachedShow(const Value: TCachedShow);
{*
  Description: Record type for showing in grid.
  Parameters: Value cached record show.
*}

begin
  FCachedShow := Value;
  if ComponentState = [csDesigning] then
    exit;
  if not Assigned(DataSource) or not Assigned(DataSource.DataSet) then
    exit;
  // for showing selected record type only.
  if Assigned(DataLink) and Assigned(DataLink.DataSet) and (DataLink.Active) then
  begin
    case FCachedShow of
      csAll:
        TBDEDataSet(DataSource.DataSet).UpdateRecordTypes := [rtModified, rtInserted,
          rtDeleted, rtUnmodified];
      csModify:
        TBDEDataSet(DataSource.DataSet).UpdateRecordTypes := [rtModified];
      csUnModify:
        TBDEDataSet(DataSource.DataSet).UpdateRecordTypes := [rtUnmodified];
      csInserted:
        TBDEDataSet(DataSource.DataSet).UpdateRecordTypes := [rtInserted];
      csRemoved:
        TBDEDataSet(DataSource.DataSet).UpdateRecordTypes := [rtDeleted];
      csNormal:
        TBDEDataSet(DataSource.DataSet).UpdateRecordTypes := [rtModified, rtInserted,
          rtUnmodified];
    end;
  end;
end;


end.

2009. október 23., péntek

Copy a Paradox table with all its family members from one place to another on my system


Problem/Question/Abstract:

How can I copy a Paradox table with all its family members from one place to another on my system?

Answer:

Introduction

When I discovered the TBatchMove component, it was a real god-send because I now had a means to copy and append data from one Paradox table to another, which was my desktop database of choice. But the one thing that irked me about it was that if I wanted to perform a physical copy of a table with all its indexes and other family members, TBatchMove wouldn't copy them. It would only copy the table itself.

An obvious workaround to this dilemma would be to do an operating system level copy of all files in the directory that have the same name as the table. But the problem with this is that a system-level copy is indescriminant of the file types that are being copied. What does this mean? Well, one thing is that if a subdirectory residing in the source directory happens to have the same name as the table, its entire contents will be copied to the destination. Also, the potential for copying other stray files with the same name but having no association with the table exists. So what happens is that you have to write a lot of logic just to deal with those two situations.

So, what do you do when there doesn't seem to be anything available in the VCL that will let you copy a table and all its associates at once. Even the TTable's BatchMove method won't do it. Well, when all else fails, go to the BDE itself.

When I was programming in Paradox and Paradox for Windows, I took it for granted that I could issue a Paradox copy command and all my tables and their family members would be copied all together. All that changed in Delphi, but that doesn't mean it's not available. It's just a matter of doing some programming. And surprisingly enough, it's not that hard to do.

Copying the File

Doing BDE stuff is rarely a one step operation. Usually, you have to instantiate or initialize a few things before you actually make the call you want to make. The BDE call that we want to perform the copy, DBICopyTable, is no exception. But thankfully, it only requires a single prerequisite object to be created before making the call. That object is simply a TDatabase object. DBICopyTable uses the TDatabase object's Handle property internally to get information about the file being copied. Specifically, once it has the handle to the object, it uses the Locale property information get the language driver information it needs so it knows what files to copy. Once we create the database object, we're ready to make the call. Let's look at it in detail.

DBICopyTable takes four (5) parameters. They're explained below:

Parameter Name
Type
Description
hDB
hDBIDb
In English, this is read Database Handle, the Handle property of a TDatabase object.
bOverwrite
Boolean
True = Overwrite the destination file if it exists
False = Don't overwrite. If the file exists, an exception will occur. This is trapped by the Check function which will also pop up a message describing the error.
pszSrcTableName
PChar
The fully qualified (path and file name) name of the source table to copy
pszSrcDriverType
PChar
The driver type of the source table. If you supply the file extension, you can set this to nil. Otherwise, you have to specify a valid driver type (i.e. 'STANDARD', SQLServer, etc.).
pszDestName
PChar
The fully qualified name of the destination table.


Here's some code that encapsulates the call (we'll discuss it below):

{========================================================================
This will copy a Paradox or dBase table from one directory to another.
Note that this does not use BDE aliases. It would be possible to do that
by declaring parameters for the source and destination databases,
respectively.}
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
  = = = = = = = = = = = = = = = = = = = = = = = = = = = =
procedure CopyPdoxTable(SrcTbl, DstTbl: string; Overwrite: Boolean);
var
  DB: TDatabase;
  STbl, DTbl: string;
begin
  {Since we're using path names and not BDE aliases, we have to do
   some checking of the paths to see if they're blank; that is, the
   user passed just the file name to the procedure, and not the
   FULLY QUALIFIED file name. In that case, we merely set the source
   and destination to the application's EXEName directory}
  if (ExtractFilePath(SrcTbl) = '') then
    STbl := ExtractFilePath(Application.EXEName) + SrcTbl
  else
    STbl := SrcTbl;

  if (ExtractFilePath(DstTbl) = '') then
    DTbl := ExtractFilePath(Application.EXEName) + DstTbl
  else
    DTbl := DstTbl;

  {First, check to see if the source file actually exists. If it does
  create a TDatabase that points to the source file's directory.
  This can actually point anywhere using the method we're using because
  we're specifying fully qualified file names as entries as opposed to.
        The important thing though, is to set it to a valid directory}
  if FileExists(STbl) then
  begin
    DB := TDatabase.Create(nil);
    with DB do
    begin
      Connected := False;
      DatabaseName := ExtractFilePath(SrcTbl);
      DriverName := 'STANDARD';
      Connected := True;
    end;

    {Do the table copy from source to dest. Notice the PChar typecast of STbl
     and DTbl. The BDE function actually calls for a DBITBLNAME type. But this
     is just a null-terminated string - a PChar - so we can save ourselves a
     lot of time by just typecasting the strings.}
    Check(DBICopyTable(DB.Handle, Overwrite, PChar(STbl), nil, PChar(DTbl)));

    //Get rid of the database component.
    DB.Free;
  end;
else
  ShowMessage('Could not copy the table. It is not in the location specified.');

end;

Note that the boldface type is the code you actually write. I did it this way because I added a lot of comments and they got in the way of the code.

So, what's going on in the code?

Well, the first thing that happens is a little sanity check. If only the file name of the table is passed to the procedure, it assumes that the file resides in the same directory as the application. Then the copying operation is enclosed in a conditional statement and only executes depending upon the existence of the file itself.

Once that's done, we create a TDatabase object in memory, set its DatabaseName property to the directory location of the file by calling ExtractFilePath, specify the STANDARD driver (Paradox and dBase files) as our table language driver, then connect. Pretty simple.

Then, it's a simple matter of calling DbiCopyTable, inputting the parameters described above. Notice that I enclose the call in the Check function. This is a special BDE call which checks the error constant returned from a BDE call. While the BDE is fairly complex, it's error tracking is fairly robust. In the old days of the BDE, you had to trap all the constants yourself, then display error messages depending upon the value returned. Check handles all that for you.

Well, that's it. Try it out and see how it works for you.

2009. október 22., csütörtök

How to colorize an image


Problem/Question/Abstract:

How to colorize an image

Answer:

Assumes 8 bit R, G, Bs packed into RGB. Luma is 0 - 255


function Colorize(RGB, Luma: Cardinal);
var
  l, r, g, b: Single;
begin
  Result := Luma;
  if Luma = 0 then { it's all black anyway}
    Exit;
  l := Luma / 255;
  r := RGB and $FF * l;
  g := RGB shr 8 and $FF * l;
  b := RGB shr 16 and $FF * l;
  Result := Round(b) shl 16 or Round(g) shl 8 or Round(r);
end;

2009. október 21., szerda

System menu in tray-icon mode


Problem/Question/Abstract:

I always wanted to show the Main window's system menu also when the only thing the user could click on the screen of my application, was the tray icon. Like Total Commander is doing...

Answer:

With the most components provinding very-easy-to-use icon tray support, you can specify to show up a popupmenu, or you can catch the click event.

But you can't easily show the same menu as if you right-clicked on application's taskbar icon.

Just catch the WM_CLICK event over the icon, or simply use the OnClick event as shown:

procedure TfrmMain.tiIconClick(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
var
  I: HMenu;
begin
  I := GetSystemMenu(Handle, False);
  TrackPopupMenuEx(I, TPM_HORIZONTAL, X, Y, Handle, nil);
end;

Be warned: you must specify X and Y as screen coordinates, not relative to the icon, like some components do.

2009. október 20., kedd

A Simple Property Editor


Problem/Question/Abstract:

How can I create a simple property editor?

Answer:

This is an introductory level article about creating a simple property editor that I hope will get you started. I'll provide enough information about the DSGNINTF.PAS (Design interface that holds the TPropertyEditor class) file so you can finish the article with a sense of that you are able to create a property editor.

TPropertyEditor

The following is a cut and paste of the TPropertyEditor class declaration found in DSGNINTF.PAS:

  TPropertyEditor = class
private
  FDesigner: TFormDesigner;
  FPropList: PInstPropList;
  FPropCount: Integer;
  constructor Create(ADesigner: TFormDesigner; APropCount: Integer);
  function GetPrivateDirectory: string;
  procedure SetPropEntry(Index: Integer; AInstance: TComponent;
    APropInfo: PPropInfo);
protected
  function GetPropInfo: PPropInfo;
  function GetFloatValue: Extended;
  function GetFloatValueAt(Index: Integer): Extended;
  function GetMethodValue: TMethod;
  function GetMethodValueAt(Index: Integer): TMethod;
  function GetOrdValue: Longint;
  function GetOrdValueAt(Index: Integer): Longint;
  function GetStrValue: string;
  function GetStrValueAt(Index: Integer): string;
  function GetVarValue: Variant;
  function GetVarValueAt(Index: Integer): Variant;
  procedure Modified;
  procedure SetFloatValue(Value: Extended);
  procedure SetMethodValue(const Value: TMethod);
  procedure SetOrdValue(Value: Longint);
  procedure SetStrValue(const Value: string);
  procedure SetVarValue(const Value: Variant);
public
  destructor Destroy; override;
  procedure Activate; virtual;
  function AllEqual: Boolean; virtual;
  procedure Edit; virtual;
  function GetAttributes: TPropertyAttributes; virtual;
  function GetComponent(Index: Integer): TComponent;
  function GetEditLimit: Integer; virtual;
  function GetName: string; virtual;
  procedure GetProperties(Proc: TGetPropEditProc); virtual;
  function GetPropType: PTypeInfo;
  function GetValue: string; virtual;
  procedure GetValues(Proc: TGetStrProc); virtual;
  procedure Initialize; virtual;
  procedure Revert;
  procedure SetValue(const Value: string); virtual;
  function ValueAvailable: Boolean;
  property Designer: TFormDesigner read FDesigner;
  property PrivateDirectory: string read GetPrivateDirectory;
  property PropCount: Integer read FPropCount;
  property Value: string read GetValue write SetValue;
end;

Whew! that's a lot of stuff, isn't it? Add to the fact that the Tools API is poorly documented, and you've got a lot confusion to deal with. There's a little relief in the Delphi 2.0 help file, but the way it's organized can leave you with the sinking feeling that you're in way over your head. After you've done a few property editors, it's really not that hard.

The Trick to Writing Property Editors

One of the biggest problems with technical documentation is that it's technical. Not much conceptual material is ever covered in tech specs or tech manuals. This leaves it up to the programmer to extrapolate the underlying concepts. I'm of the opinion that if something has the possibility of being a widely used feature, you should cover not only the technical specifications, but the conceptual points as well. Gaining conceptual understanding is the real trick to creating property.

The trick to writing property editors is understanding the virtual methods and what they do. Writing your own custom property editors is all about overriding the proper methods of the TPropertyEditor class to get the functionality out of property editor that you require. Granted, there are a lot of very complex property editors out there. But whether simple or complex, they're all built in a similar fashion: they override default functionality of TPropertyEditor.

Once you let this concept sink in, and as you gain more experience in building components, writing a property editor merely becomes the task of overriding the appropriate methods to get your job done.

Furthermore, the DSGNINFT.PAS is thoroughly commented. When constructing components that will have property editors, make sure that this file is open in the editor so you can refer to the documentation covering the virtual methods you will be overriding. As an aside, if you do BDE programming, having the BDE.INT (Delphi 2.0) or DBIPROCS.INT, DBITYPES.INT, DBIERRS.INT (Delphi 1.0) is essential to successful BDE programming

The Value List: the Simplest Type of Property Editor

Properties that display value lists are common to components. In fact, you see them all the time. For instance, a value list property that everyone has used is the Align property.

Value lists are simple enumerated types, which are merely a collection of sequentially ordered elements in a list. The first item has an ordinal value of 0, the second 1, and so forth. Enumerated types are useful in communicating with the user using a set of named choices rather than ordinal or numeric choices. For instance, the Align element alBottom is much easier to understand than '0,' which is its ordinal value in the list. In this case, the ordinal value has no clear conceptual context.

To create a property editor that presents a value list to a user in the object inspector is very simple and requires only a few steps. Here is a brief synopsis of what you have to do before we go into detail:

First, define and declare your enumerated type under a new type section.
Under the enumerated type declaration, declare your class, including the functions you will be overriding in your code.
Write your code in the implementation section of the unit.

Sounds pretty simple, right? It is. So let's go and create one now, then we'll discuss it in detail below.

...other code

interface

type
  TEnumMonth = (emJan, emFeb, emMar,
    emApr, emMay, emJun,
    emJul, emAug, emSep,
    emOct, emNov, emDec);

  TEnumMonths = class(TEnumProperty)
  public
    function GetAttributes: TPropertyAttributes; override;
    function AllEqual: Boolean; override;
  end;

implementation

...other code

function TEnumMonths.AllEqual: Boolean;
begin
  Result := True;
end;

function TEnumMonths.GetAttributes: TPropertyAttributes;
begin
  Result := [paMultiSelect, paValueList];
end;

procedure Register;
begin
  RegisterPropertyEditor(TypeInfo(TEnumMonth), TPSIBaseExt, 'RangeBegin',
    TEnumMonths);
  RegisterPropertyEditor(TypeInfo(TEnumMonth), TPSIBaseExt, 'RangeEnd', TEnumMonths);
end;

The property editor listed above was created to serve a singular purpose: Allow the user to select a specific month from a list of months, rather than typing in a month value code himself (which is more work than the user needs and is also prone to spelling errors).

This component modernizes a cumbersome style of interaction in existing applications. In these applications users were required to enter month ranges as a six-digit string beginning with current two-digit year plus the four digit month/day combination (eg., YYMMDD). Past experience said that runtime errors or empty result sets from queries that used the range values were usually the result of mistyping. So the property editor was created to let the user pick a month for both the starting month and ending month of the range of values they wanted to extract. This explains why in the code above I registered the property editor for both the RangeBegin and RangeEnd properties.

Elsewhere in the component, I have created an array type of type String and created two arrays representing the starting month and ending month code values, respectively.

Here's the array type declaration:

type
  TMonthRng = array[0..11] of string;
  ....

Here are the declarations and initializations of the arrays themselves:

var
  stmonArr,
    enmonArr: TMonthRng;
begin
  stmonArr[0] := '0101';
  stmonArr[1] := '0201';
  stmonArr[2] := '0301';
  stmonArr[3] := '0401';
  stmonArr[4] := '0501';
  stmonArr[5] := '0601';
  stmonArr[6] := '0701';
  stmonArr[7] := '0801';
  stmonArr[8] := '0901';
  stmonArr[9] := '1001';
  stmonArr[10] := '1101';
  stmonArr[11] := '1201';
  enmonArr[0] := '0131';
  enmonArr[1] := '0229';
  enmonArr[2] := '0331';
  enmonArr[3] := '0430';
  enmonArr[4] := '0531';
  enmonArr[5] := '0630';
  enmonArr[6] := '0731';
  enmonArr[7] := '0831';
  enmonArr[8] := '0930';
  enmonArr[9] := '1031';
  enmonArr[10] := '1130';
  enmonArr[11] := '1231';

  ...

By doing things in this manner, one can easily get the appropriate value needed by passing the ordinal value of the appropriate enumerated type as an index of an element in the array. For example, let's say the user chose emApr as his/her starting month. The ordinal value of emApr is 3. Referencing that value in the stmonArr array would produce the string '0401.' What I've essentially done here is eliminate the need for the user to do anything more than choose an appropriate month to start with. The proper code is handled by the program. Here's some sample code that demonstrates how it's done:

procedure ReturnMonthCode(Index: Integer; StartMonth: Boolean): string;
var
  stmonArr,
    enmonArr: TMonthRng;
begin
  stmonArr[0] := '0101';
  stmonArr[1] := '0201';
  stmonArr[2] := '0301';
  stmonArr[3] := '0401';
  stmonArr[4] := '0501';
  stmonArr[5] := '0601';
  stmonArr[6] := '0701';
  stmonArr[7] := '0801';
  stmonArr[8] := '0901';
  stmonArr[9] := '1001';
  stmonArr[10] := '1101';
  stmonArr[11] := '1201';

  enmonArr[0] := '0131';
  enmonArr[1] := '0229';
  enmonArr[2] := '0331';
  enmonArr[3] := '0430';
  enmonArr[4] := '0531';
  enmonArr[5] := '0630';
  enmonArr[6] := '0731';
  enmonArr[7] := '0831';
  enmonArr[8] := '0930';
  enmonArr[9] := '1031';
  enmonArr[10] := '1130';
  enmonArr[11] := '1231';

  if StartMonth then
    Result := stMonArr[Index]
  else
    Result := enMonArr[Index];
end;

To actually use ReturnMonthCode all we do is the following:

var
  S: string;
begin

  S := ReturnMonthCode(Ord(RangeBegin), True);

Remember, RangeBegin is a property of type TEnumArray. Therefore, to access its ordinal value, all we need do is apply the Ord function to it.

Based on the information above, you should be able to create at the very least a simple property editor like the example above. For more complex property editors, you will have to override more of the methods; but remember, don't be daunted by the code. The trick is overriding the default methods with your own.

2009. október 19., hétfő

How to detect if DCOM is installed


Problem/Question/Abstract:

How to detect if DCOM is installed

Answer:

function IsDCOMInstalled: Boolean;
var
  OLE32: HModule;
begin
  Result := not (IsWin95 or IsWin95OSR2);
  if not Result then
  begin
    OLE32 := LoadLibrary(COLE32DLL);
    if OLE32 > 0 then
    try
      Result := GetProcAddress(OLE32, PChar('CoCreateInstanceEx')) <> nil;
    finally
      FreeLibrary(OLE32);
    end;
  end;
end;

2009. október 18., vasárnap

How to read the file header of a wave file


Problem/Question/Abstract:

I want to open a wave file in my application, but how do I know that it is really a wave file and not just a file with *.wav extension?

Answer:

First you have to know what the structure of a wave file is. I'd create a record which represent this structure:


type
  TWaveHeader = record
    ident1: array[0..3] of Char;      // Must be "RIFF"
    len: DWORD;                       // Remaining length after this header
    ident2: array[0..3] of Char;      // Must be "WAVE"
    ident3: array[0..3] of Char;      // Must be "fmt "
    reserv: DWORD;                    // Reserved 4 bytes
    wFormatTag: Word;                 // format type
    nChannels: Word;                  // number of channels (i.e. mono, stereo, etc.)
    nSamplesPerSec: DWORD;            //sample rate
    nAvgBytesPerSec: DWORD;           //for buffer estimation
    nBlockAlign: Word;                //block size of data
    wBitsPerSample: Word;             //number of bits per sample of mono data
    cbSize: Word;                     //the count in bytes of the size of
    ident4: array[0..3] of Char;      //Must be "data"
end;


With this structure you can get all the information's about a wave file you want to.
After this header following the wave data which contains the data for playing the wave file.

Now we trying to get the information's from a wave file. To be sure it's really a wave file, we test the information's:


function GetWaveHeader(FileName: TFilename): TWaveHeader;
const
  riff = 'RIFF';
  wave = 'WAVE';
var
  f: TFileStream;
  w: TWaveHeader;
begin
  if not FileExists(Filename) then
    exit; //exit the function if the file does not exists

  try
    f := TFileStream.create(Filename, fmOpenRead);
    f.Read(w, Sizeof(w)); //Reading the file header

    if w.ident1 <> riff then
    begin //Test if it is a RIFF file, otherwise exit
      Showmessage('This is not a RIFF File');
      exit;
    end;

    if w.ident2 <> wave then
    begin //Test if it is a wave file, otherwise exit
      Showmessage('This is not a valid wave file');
      exit;
    end;

  finally
    f.free;
  end;

  Result := w;
end;



I hope this example will help you to work with wave files in your application.


2009. október 17., szombat

Delete the row in TStringGrid component


Problem/Question/Abstract:

How can I delete the row in TStringGrid component?

Answer:

If you worked with TStringGrid component, then you saw that in this component the Borland developers not provided the method for row deleting.
In this tip I describe the few ways for it:

1. navigate by rows and copy the row contains to the prev row:

procedure DeleteRow(yourStringGrid: TStringGrid; ARow: Integer);
var
  i, j: Integer;
begin
  with yourStringGrid do
  begin
    for i := ARow to RowCount - 2 do
      for j := 0 to ColCount - 1 do
        Cells[j, i] := Cells[j, i + 1];
    RowCount := RowCount - 1
  end;
end;

2. the modificated #1:

procedure DeleteRow(yourStringGrid: TStringGrid; ARow: Integer);
var
  i: Integer;
begin
  with yourStringGrid do
  begin
    for i := ARow to RowCount - 2 do
      Rows[i].Assign(Rows[i + 1]);
    RowCount := RowCount - 1
  end;
end;

3. the "hacked" way. The TCustomGrid type (the TStringGrid is TCustomGrid's successor) have the DeleteRow method. But this method allocated not in public section but in protected section. So the all successors can "see" this DeleteRow method.

type
  THackStringGrid = class(TStringGrid);

procedure DeleteRow(yourStringGrid: TStringGrid; ARow: Integer);
begin
  with THackStringGrid(yourStringGrid) do
    DeleteRow(ARow);
end;

Personally I use the third method but the first and second are more visual.

Also you should clear the Row after moving the data to the row above.  If not you will get the old date back when you add a Row.

if StringGrid.RowCount > 2 then
begin
  if StringGrid.Selection.Top <> (StringGrid.RowCount - 1) then
  begin
    for iRow := StringGrid.Selection.Top to (StringGrid.RowCount - 2) do
    begin
      for iCol := 0 to (StringGrid.ColCount - 1) do
      begin
        StringGrid.Cells[iCol, iRow] := StringGrid.Cells[iCol, iRow + 1];
      end;
    end;
  end;
  StringGrid.Rows[StringGrid.RowCount - 1].Clear;
  StringGrid.RowCount := StringGrid.RowCount - 1;

2009. október 16., péntek

Extract string property values from DFM files


Problem/Question/Abstract:

Does anybody know the name of the routine used in the IDE that enables control characters embedded into a string to be recognised. For example the characters 'Line 1'#13#10'Line 2' are recognised by the compiler as a single string literal. I'd like to build a similar facility into an application. Is a single routine used for this or is it embedded somewhere in the parser of the compiler?

Answer:

I'm pretty sure that the compiler uses an internal routine for this which is not accessible to us mere mortals. Perhaps you can extract something useful from the unit below. I wrote it to extract string property values from DFM files. It is a work in progress, so if you want to use it for the same purpose be aware that you may have DFMs it will not be able to digest without modifications.

unit DFMParser;

interface

uses
  classes, sysutils;

type
  TBaseParser = class
  private
    FText: string;
    FCurrent, FAnchor: Integer;
    FToken: string;
  protected
    procedure Error(const S: string); overload;
    procedure Error(const fmt: string; const A: array of const); overload;
    procedure DropAnchor;
    procedure NextToken;
    procedure NextChar;
    procedure SkipWhitespace;
    procedure SkipToEol;
    procedure SkipTo(ch: Char);
    procedure SkipToString(const S: string);
    function EndOfText: Boolean;
    function IsTokenChar: Boolean;
    function IsWhiteSpace: Boolean;
    function CurrentChar: Char;
    function LastWord: string;
    function ParseEncodedChar: Char;
    function ParseQuotedString: string;
    function ParseStringValue: string;
  public
    procedure Parse; virtual; abstract;
    constructor Create(const S: string); virtual;
    property Token: string read FToken;
  end;

  TParsePropertyEvent = procedure(const aComponentName, aPropertyName,
    aPropertyValue: string) of object;

  TDFMParser = class(TBaseParser)
  private
    FParsePropertyEvent: TParsePropertyEvent;
  protected
    procedure ParseComponent;
    procedure ParseProperty(const componentName: string);
    procedure ParsePropertyString(const componentName, propertyName: string);
    function TokenIsObject: Boolean;
    function IsEndToken: Boolean;
    procedure DoPropertyEvent(const componentName, propertyname, propvalue: string);
  public
    procedure Parse; override;
    property OnParseProperty: TParsePropertyEvent read FParsePropertyEvent
      write FParsePropertyEvent;
  end;

  EDFMParserError = class(Exception);

  TTranslationItemEvent = procedure(const name, value: string) of object;

  TTranslationParser = class(TBaseParser)
  private
    FTranslationItemEvent: TTranslationItemEvent;
    procedure ParseStringConstant;
    procedure SkipWhitespaceAndComments;
    procedure DoTranslationItem(const name, value: string);
  public
    constructor Create(const S: string); override;
    procedure Parse; override;
    property OnTranslationItem: TTranslationItemEvent read FTranslationItemEvent
      write FTranslationItemEvent;
  end;

implementation

uses
  charsets;

const
  quote = '''';

constructor TBaseParser.Create(const S: string);
begin
  FText := S;
  FCurrent := 1;
end;

function TBaseParser.CurrentChar: Char;
begin
  result := FText[FCurrent];
end;

procedure TBaseParser.DropAnchor;
begin
  FAnchor := FCurrent;
end;

function TBaseParser.EndOfText: Boolean;
begin
  result := FCurrent > Length(FText);
end;

procedure TBaseParser.Error(const S: string);
begin
  raise EPArserError.Create(S);
end;

procedure TBaseParser.Error(const fmt: string; const A: array of const);
begin
  Error(Format(fmt, A));
end;

function TBaseParser.IsTokenChar: Boolean;
begin
  result := (Currentchar in Charsets.IdentifierChars) or (CurrentChar = '.');
end;

function TBaseParser.IsWhiteSpace: Boolean;
begin
  result := Currentchar in [#1..#32];
end;

function TBaseParser.LastWord: string;
begin
  Assert(FAnchor <= FCurrent);
  result := Copy(FText, FAnchor, FCurrent - FAnchor);
end;

procedure TBaseParser.NextChar;
begin
  Inc(FCurrent);
  if EndOfText then
    Error('Unexpected end of text');
end;

procedure TBaseParser.NextToken;
begin
  SkipWhitespace;
  DropAnchor;
  while not EndOfText and IsTokenChar do
    Inc(FCurrent);
  FToken := LastWord;
end;

procedure TBaseParser.SkipTo(ch: Char);
begin
  while not EndOfText and (Currentchar <> ch) do
    NextChar;
  Inc(FCurrent);
end;

procedure TBaseParser.SkipToString(const S: string);
var
  P: PChar;
begin
  p := StrPos(@FText[FCurrent], Pchar(S));
  if Assigned(p) then
    FCurrent := p - PChar(FText) + 1 + Length(S)
  else
    Error('Expected string "%s" not found', [s]);
end;

procedure TBaseParser.SkipToEol;
begin
  while not EndOfText and (FText[FCurrent] <> #10) do
    Inc(FCurrent);
end;

procedure TBaseParser.SkipWhitespace;
begin
  while not EndOfText and IsWhiteSpace do
    Inc(FCurrent);
end;

function TBaseParser.ParseQuotedString: string;
begin
  Assert(CurrentChar = quote);
  Result := '';
  repeat
    NextChar; {skip leading quote}
    DropAnchor;
    while CurrentChar <> quote do
      NextChar;
    Result := Result + LastWord;
    NextChar;
    if CurrentChar = quote then
      Result := Result + quote; {literal quote}
  until
    CurrentChar <> quote;
  SkipWhitespace;
end;

function TBaseParser.ParseEncodedChar: Char;
var
  allowed: Charsets.TCharset;
  n: Integer;
begin
  Assert(CurrentChar = '#');
  NextChar;
  DropAnchor;
  if CurrentChar = '$' then
  begin
    allowed := CHarsets.HexNumerals;
    NextChar;
  end
  else
    allowed := Charsets.IntegerChars;
  while CurrentChar in allowed do
    NextChar;
  n := StrToInt(LastWord);
  if n > High(Byte) then
    Error('Encountered UNICODE character in string, cannot handle that.');
  Result := Char(n);
end;

function TBaseParser.ParseStringValue: string;
begin
  Result := '';
  while True do
    case CurrentChar of
      quote:
        Result := Result + ParseQuotedString;
      '#':
        Result := Result + ParseEncodedChar;
      '+':
        begin
          NextChar;
          SkipWhitespace;
        end;
    else
      Break;
    end;
end;

{ TDFMParser }

procedure TDFMParser.DoPropertyEvent(const componentName, propertyname, propvalue:
  string);
begin
  if Assigned(FParsePropertyEvent) then
    FParsePropertyEvent(componentName, propertyname, propvalue);
end;

function TDFMParser.IsEndToken: Boolean;
begin
  result := Token = 'end';
end;

procedure TDFMParser.Parse;
begin
  while not EndOfText do
  begin
    ParseComponent;
    SkipWhitespace;
  end;
end;

procedure TDFMParser.ParseComponent;
var
  componentName: string;
begin
  if FToken = '' then
    NextToken;
  if not TokenIsObject then
    Error('Expected: inherited or object, found : %s', [Token]);
  NextToken;
  componentName := Token;
  SkipToEol;
  repeat
    NextToken;
    if TokenIsObject then
      ParseComponent
    else if not IsEndToken then
      ParseProperty(componentName);
  until
    IsEndToken or EndOfText;
  if IsEndToken then
    FToken := '';
end;

procedure TDFMParser.ParseProperty(const componentName: string);
var
  propname: string;
begin
  propname := Token;
  SkipWhitespace;
  if CurrentChar <> '=' then
    Error('Expected: =, found %s', [Currentchar]);
  NextChar;
  SkipWhitespace;
  case CurrentChar of
    '{':
      SkipTo('}');
    '(':
      SkipTo(')');
    '[':
      SkipTo(']');
    quote, '#':
      ParsePropertyString(componentName, propname);
  else
    SkipToEol
  end;
end;

procedure TDFMParser.ParsePropertyString(const componentName, propertyName: string);
var
  propvalue: string;
begin
  propvalue := ParseStringValue;
  if propvalue <> '' then
    DoPropertyEvent(componentName, propertyname, propvalue);
end;

function TDFMParser.TokenIsObject: Boolean;
begin
  Result := (Token = 'inherited') or (Token = 'object')
end;

{ TTranslationParser }

constructor TTranslationParser.Create(const S: string);
const
  resStr = 'resourcestring';
var
  lS: string;
  resourceStringPos: Integer;
  n1, n2: Integer;
begin
  {Isolate the resourcestring section. We expect only one}
  lS := LowerCase(S);
  resourceStringPos := Pos(resStr, lS);
  if resourceStringPos = 0 then
    inherited Create('')
  else
  begin
    {look for an $ifdef german}
    n1 := Pos('{$ifdef german', lS);
    if n1 > 0 then
    begin
      {look for the following $else}
      Delete(lS, 1, n1 - 1);
      n2 := Pos('{$else}', lS);
      if n2 = 0 then
        Error('Malformed $IFDEF...$ELSE encountered, $ELSE not found');
      Delete(lS, 1, n2 - 1);
      Inc(n1, n2 - 1);
      {look for the $ENDIF}
      n2 := Pos('{$endif}', lS);
      if n2 = 0 then
        Error('Malformed $IFDEF...$ENDIF encountered, $ENDIF not found');
      inherited Create(Copy(S, n1, n2 - 1));
    end
    else
    begin
      {look for an $ifndef german}
      n1 := Pos('{$ifndef german', lS);
      if n1 = 0 then
        inherited Create('')
      else
      begin
        {in the $ifndef german construct the resourcestring keyword often comes after the $ifndef.}
        if n1 < resourceStringPos then
          n1 := resourceStringPos + Length(resstr);
        Delete(lS, 1, n1 - 1);
        {look for the $ENDIF}
        n2 := Pos('{$endif}', lS);
        if n2 = 0 then
          Error('Malformed $IFDEF...$ENDIF encountered, $ENDIF not found');
        inherited Create(Copy(S, n1, n2 - 1));
      end;
    end;
  end;
end;

procedure TTranslationParser.DoTranslationItem(const name, value: string);
begin
  if Assigned(FTranslationItemEvent) then
    FTranslationItemEvent(name, value);
end;

procedure TTranslationParser.Parse;
begin
  while not EndOfText do
  begin
    ParseStringConstant;
    SkipWhitespace;
  end;
end;

procedure TTranslationParser.ParseStringConstant;
var
  name, value: string;
begin
  SkipWhitespaceAndComments;
  if EndOfText then
    Exit;
  NextToken;
  name := Token;
  SkipWhitespaceAndComments;
  if EndOfText then
    Exit;
  if CurrentChar <> '=' then
    Error('Expected: =, found "%s"', [CurrentChar]);
  NextChar;
  SkipWhitespaceAndComments;
  if EndOfText then
    Exit;
  value := ParseStringValue;
  SkipWhiteSpace;
  if not EndOfText and (CurrentChar = ';') then
    NextChar;
  DoTranslationItem(name, value);
end;

procedure TTranslationParser.SkipWhitespaceAndComments;
begin
  while True do
  begin
    SkipWhitespace;
    if not EndOfText then
    begin
      case CurrentChar of
        '/':
          SkipToEol; { single line comment }
        '{':
          SkipTo('}'); { comment }
        '(':
          begin
            NextChar;
            if CurrentChar = '*' then
              SkipToString('*)')
            else
              Error('Expected: comment or indentifier, found: "(%s"', [CurrentChar]);
          end;
      else
        Break
      end;
    end
    else
      Break;
  end;
end;

end.

unit Charsets;

interface

type
  TCharSet = set of AnsiChar;
const
  Signs: TCharset = ['-', '+'];
  Numerals: TCharset = ['0'..'9'];
  HexNumerals: TCharset = ['A'..'F', 'a'..'f', '0'..'9'];
  IntegerChars: TCharset = ['0'..'9', '-', '+'];
  IdentifierChars: TCharset = ['a'..'z', 'A'..'Z', '0'..'9', '_'];
var
  Digits, Letters, LowerCaseLetters, UpperCaseLetters: TCharSet;
  FloatChars, SciFloatChars: TCharset;
  AlphaNum, NonAlphaNum: TCharset;

  { Need to call this again when locale changes.  }
procedure SetupCharsets;

implementation

uses
  Windows, Sysutils;

var
  locale: DWORD = 0;

procedure SetupCharsets;
var
  ch: AnsiChar;
begin
  if locale = GetThreadLocale then
    Exit
  else
    Locale := GetThreadLocale;
  LowerCaseLetters := [];
  UpperCaseLetters := [];
  AlphaNum := [];
  NonAlphaNum := [];
  Digits := Numerals;
  for ch := Low(ch) to High(ch) do
  begin
    if IsCharAlpha(ch) then
      if IsCharUpper(ch) then
        Include(UpperCaseLetters, ch)
      else
        Include(LowerCaseLetters, ch);
    if IsCharAlphanumeric(ch) then
      Include(AlphaNum, ch)
    else
      Include(NonAlphaNum, ch);
  end;
  Letters := LowerCaseLetters + UpperCaseLetters;
  FloatChars := IntegerChars;
  Include(FloatChars, DecimalSeparator);
  SciFloatChars := FloatChars + ['e', 'E'];
end;

initialization
  SetupCharsets;
end.

2009. október 15., csütörtök

A Simple example of Artificial Intelligence using Delphi Array


Problem/Question/Abstract:

A Simple example of Artificial Intelligence using Delphi Array (Computer simulates learning process of human, learning by correcting mistakes !)

Answer:

Artificial Intelligence (AI) is an advance branch of science that studies the process of human thinking and attempts to apply the knowledge to simulate the same process in machines.  As computers are far ahead in the marathon of processing machines, AI is considered to be the branch of Computer Science than that of General Science.
There have been many research and development in the field of Artificial Intelligence. The area of research include speech and pattern recognition, natural language processing, learning from previous experiences (learning by making and correcting mistakes!), reasoning under the situations providing limited or incomplete information etc. AI is practically applied in the field of computer games, expert systems, neural networks, robotics and many other fields of science and technology.

In this article we will try to demonstrate a very simple practical example of artificial Intelligence programming in Delphi using Delphi arrays. I have chosen a Nepali game named "GATTA TIPNE KHEL" (meaning pebble picking game) for this purpose. We can see small children playing this game in the playground.

(By the way, Nepal, my nation, is a small Asian country between India and China. Recently Nepal was in the main highlight of media for the notorious Royal Massacre in which the whole family of the ruling king were cruelly killed.)

In this pebble picking game a pile of some pebbles is kept in the ground. One of the two players picks one, two or three pebbles at a time in his turn, leaving the pile for the other player to pick for his alternate turn. In this alternate picking process, the player who picks the last pebble(s) will be the loser and called to be a DOOM in Nepali.  
The main logic of the game is to leave the pile of pebbles with 13, 9, 5 or 1 pebble(s) for the opponent to pick.
In the program the starting number of pebbles are set to 17, 21, 25, 29 &#8230; etc. so that computer could win always if it does not make a mistake. But in the real play computer seems to be gradually learning by correcting mistakes of the previously played games. At last it finds all its mistakes and corrects them to become an unbeatable champion. It seems computer simulates the psychological learning process of animal, learning by correcting and not repeating the mistakes.
A multidimensional array of elements (1..4,1..3) is chosen as the instruction book for the computer to pick the pebbles. The instruction book contains four pages with three lines of instructions to pick pebbles. The first line instructs to pick a single pebble, the second line instructs to pick 2 and the third line instructs to pick 3 pebbles. At the beginning, computer chooses a random page and a random line of instruction to pick the pebble. When the game finishes, if computer looses the game, the last instruction is red-marked (erased) and the instruction will not be read in the future. After playing many games, all the instructions leading to a lost game will be red marked and there will be left only the instructions those lead to a win.  

Well, it is enough for the description of the game.
Let us jump directly to the code below:

unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Panel1: TPanel;
    LabelPeb1: TLabel;
    LabelPeb2: TLabel;
    Panel2: TPanel;
    Panel3: TPanel;
    Panel4: TPanel;
    Label1: TLabel;
    ComboBox1: TComboBox;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Label6: TLabel;
    LabelIWon: TLabel;
    LabelYouWon: TLabel;
    LabelTotPlayed: TLabel;
    ListBox1: TListBox;
    GroupBox1: TGroupBox;
    BitBtn1: TBitBtn;
    BitBtn2: TBitBtn;
    BitBtn3: TBitBtn;
    BitBtn5: TBitBtn;
    Panel5: TPanel;
    Labelbtn: TLabel;
    BitBtnResign: TBitBtn;
    Label7: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure ComboBox1Change(Sender: TObject);
    procedure BitBtnResignClick(Sender: TObject);
    procedure LabelbtnClick(Sender: TObject);
    procedure BitBtn1Click(Sender: TObject);
  private
    { Private declarations }

    //Procedure to display remaining Pebbles
    procedure DispPebbles(const nPebs: integer);
    //Procedure to reset Variables and Labels after a game is finished
    procedure Finish;
    //Procedure to enable or Disable some controls;
    procedure ManageEnabling(lValue: Boolean);
    //Procedure to display winning or loosing messages
    procedure Messaging(const lost: Boolean; const resigning: Boolean);
    //Real Procedure for playing the game
    procedure LearnByMistakes;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

var
  PlayedGames, ComputerWon: Integer;
  totPebs, RemainPebs, nTurn: Integer;
  OldPageNum, OldLineNum: Integer;

  //18 possible permutations of 3 digits: 1,2 and 3
  //to determine the number of pebbles to take for the computer
  aPermutations: array[1..6, 1..3] of integer;
  // A book containing the instruction pages(4) and lines(3 in each page) to draw the pebbles for the computer
  aPages: array[1..4, 1..3] of integer;

procedure TForm1.FormCreate(Sender: TObject);
// Permutations of 1,2 and 3  in the group of 3s to fill the permutation Array
const
  cPermutations = '123132213231312321';
var
  x, y: Integer;
begin
  //Filling  Permutation Array
  for x := 1 to 6 do
    for y := 1 to 3 do
      aPermutations[x, y] := strtoInt(copy(cPermutations, (x - 1) * 3 + y, 1));
  //Filling the lines of all pages of the instruction Book (array aPages[1..4,1..3]).
  for x := 1 to 4 do
    for y := 1 to 3 do
      aPages[x, y] := y;
  ComboBox1.ItemIndex := 1;
  PlayedGames := 0;
  ComputerWon := 0;
  totPebs := 17; {Default to 17 pebbles}
  RemainPebs := 17; {All are intact up to Now}
end;

procedure TForm1.DispPebbles(const nPebs: integer);
begin
  LabelPeb1.Caption := intToStr(nPebs);
  LabelPeb2.Caption := LabelPeb1.Caption;
end;

procedure TForm1.ManageEnabling(lValue: Boolean);
begin
  BitBtn1.Enabled := lValue;
  BitBtn2.Enabled := lValue;
  BitBtn3.Enabled := lValue;
  Labelbtn.Enabled := lValue;
  BitBtnResign.Enabled := lValue;
  GroupBox1.Enabled := lValue;
  if (RemainPebs < 3) and (RemainPebs > 0) and lValue then
  begin
    if RemainPebs < 3 then
      BitBtn3.Enabled := False;
    if RemainPebs < 2 then
      BitBtn2.Enabled := False;
  end

end;

procedure TForm1.Finish;
begin
  LabelTotPlayed.caption := intToStr(PlayedGames);
  LabelIWon.caption := intToStr(ComputerWon);
  LabelYouWon.caption := intToStr(PlayedGames - ComputerWon);
  DispPebbles(strToInt(ComboBox1.Items[ComboBox1.ItemIndex]));
  totPebs := strtoint(LabelPeb1.Caption);
  RemainPebs := totPebs;
  ManageEnabling(True);
  ComboBox1.Enabled := True;
  Labelbtn.Enabled := False;
  BitBtnResign.Enabled := False;
end;

procedure TForm1.ComboBox1Change(Sender: TObject);
begin
  DispPebbles(strToInt(ComboBox1.Items[ComboBox1.ItemIndex]));
  totPebs := strtoint(LabelPeb1.Caption);
  RemainPebs := totPebs;
end;

procedure TForm1.Messaging(const lost: Boolean; const resigning: Boolean);
begin
  inc(PlayedGames);
  MESSAGEBEEP(0);
  if lost then
  begin
    if resigning then
      showmessage('I Resign ! You won the game again !!')
    else
      showmessage('Congratulations ! You won !! I acknowledges defeat !');
  end
  else
  begin
    inc(ComputerWon);
    showmessage('Hi !  You lost ! I WON THIS GAME !!');
  end;
  Finish;
end;

procedure TForm1.LearnByMistakes;
var
  x, PageNum, LineNum, nTemp, nTakes: integer;
begin
  if RemainPebs <= 0 then
  begin
    //Openent drew the last pebble(s) ! Computer won !!
    //DispPebbles(0);
    Messaging(False, False);
    exit;
  end;
  nTemp := random(6) + 1;
  PageNum := RemainPebs mod 4;
  if PageNum = 0 then
    PageNum := 4;
  for x := 1 to 3 do
  begin
    LineNum := aPermutations[nTemp, x];
    if (aPages[PageNum, LineNum] > 0) then
      break;
  end;
  if x > 3 then {No unmarked instructions remained ! All are redmarked !!}
  begin
    // The effect of this move was unknown previously. But it proved to be fatal this time !
    // RedMark This oldLineNum of this oldPageNum !
    aPages[OldPageNum, OldLineNum] := -99;
    // Lost with Resigning Message !
    ListBox1.Items.add(intTostr(nTurn) + '. I resigned ');
    DispPebbles(RemainPebs);
    Messaging(True, True);
    exit;
  end;
  nTakes := aPages[PageNum, LineNum];
  if nTakes >= RemainPebs then
  begin
    ListBox1.Items.add(intTostr(nTurn) + '. I (Computer): ' + intTostr(RemainPebs));
    //I am the last drawer and I lost !!
    // RedMark This LineNum of this PageNum !
    aPages[PageNum, LineNum] := -99;
    DispPebbles(0);
    Messaging(True, False);
    exit;
  end;
  ListBox1.Items.add(intTostr(nTurn) + '. I (Computer): ' + intTostr(nTakes));
  showmessage('I (Computer) take: ' + IntTostr(nTakes) +
    ' Pebble(s) for this turn !');
  OldPageNum := PageNum;
  oldLineNum := LineNum;
  RemainPebs := RemainPebs - nTakes;
  ManageEnabling(True);
  DispPebbles(RemainPebs);
end;

procedure TForm1.BitBtnResignClick(Sender: TObject);
begin
  inc(PlayedGames);
  inc(ComputerWon);
  inc(nTurn);
  ListBox1.Items.add(intTostr(nTurn) + '. You resigned');
  Finish;
end;

procedure TForm1.LabelbtnClick(Sender: TObject);
begin
  BitBtnResign.SetFocus;
  BitBtnResign.Click;
end;

procedure TForm1.BitBtn1Click(Sender: TObject);
var
  nTaken: Integer;
begin
  with Sender as TBitBtn do
    nTaken := Tag;
  ManageEnabling(False);
  if (RemainPebs = totPebs) then
  begin
    ComboBox1.Enabled := False;
    ListBox1.Clear;
    nTurn := 1;
  end
  else
    inc(nTurn);
  ListBox1.Items.add(intTostr(nTurn) + '. You: ' + intTostr(nTaken));
  RemainPebs := RemainPebs - nTaken;
  DispPebbles(RemainPebs);
  LearnByMistakes;
end;

end.

A zipped file game.zip contains all stuffs (forms, units and project file) of the running project of this game. This project was compiled and run with Delphi - 3.

At last for the interested friends:
I have programmed a game of strategic thinking using the principles of Artificial Intelligence.  The game is somehow like Chess.
The game was programmed using PowerBasic and Assembly Language. This game has won the First Prize in The First All Nepal Software Competition, SOFTWARE MEET 2000. The game can be downloaded at: http//:www.viewnepal.com/gamezip.exe (a self extracting pkzip file). I will be glad to provide a copy through e-mail if the download does not work.

For users, who are interessted in Artificical intelligence: You can find a Delphi component for ArtificialNeuronalNetworks under http://www.logiware.de/ann

Component Download: game.zip

2009. október 14., szerda

Bytes to file size


Problem/Question/Abstract:

How I can convert bytes to file size in KB/MB like in windows explorer

Answer:

function FileSizeStr(Size: LongInt): string;
begin
  if Size < 1000 then
    Result := IntToStr(Size) + ' B'
  else if Size < 102400 then
    Result := IntToStr((Size + 1023) shr 10) + ' KB'
  else
    Result := IntToStr((Size + 1048575) shr 20) + ' MB';
end;

2009. október 13., kedd

How to calculate the elapsed time between 2 DateTime fields


Problem/Question/Abstract:

Using Delphi 5, I need to calculate the elapsed time between 2 DateTime fields with the result returned in Days, Hours and Minutes format.

Answer:

Subtract the values AsDateTime and then use DecodeDate and DecodeTime on the result:


procedure TMyForm.Button1Click(Sender: TObject);
var
  StartT, EndT, DeltaT: TDateTime;
  Days, Hour, Min, Sec, MSec: Word;
begin
  StartT := StrToDateTime('27.02.2000 13:45');
  EndT := StrToDateTime('02.03.2000 17:30');
  DeltaT := EndT - StartT;
  Days := trunc(DeltaT);
  DecodeTime(DeltaT, Hour, Min, Sec, MSec);
  ShowMessage(' Time elapsed: ' + IntToStr(Days) + ' days, ' + IntToStr(Hour) +
    ' hours, ' + IntToStr(Min) + ' minutes ');
end;


When trying to actually run the above sample, you have to change the string constants used to initialize the datetime vars according to your settings in ShortDateFormat and LongTimeFormat.

2009. október 12., hétfő

How to change the default button in a MessageDlg


Problem/Question/Abstract:

How to change the default button in a MessageDlg

Answer:

function DefMessageDlg(const aCaption: string; const Msg: string; DlgType: TMsgDlgType;
  Buttons: TMsgDlgButtons; DefButton: Integer; HelpCtx: Longint): Integer;
var
  i: Integer;
  btn: TButton;
begin
  with CreateMessageDialog(Msg, DlgType, Buttons) do
  try
    Caption := aCaption;
    HelpContext := HelpCtx;
    for i := 0 to ComponentCount - 1 do
    begin
      if Components[i] is TButton then
      begin
        btn := TButton(Components[i]);
        btn.Default := btn.ModalResult = DefButton;
        if btn.Default then
          ActiveControl := Btn;
      end;
    end;
    Result := ShowModal;
  finally
    Free;
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  if DefMessageDlg('Please confirm', 'Do you want to format your harddisk now?', mtConfirmation, mbYesNoCancel, mrno, 0) = mrYes then
    ShowMessage('Formatting disk...');
end;

2009. október 11., vasárnap

How to hide the caret in a TEdit


Problem/Question/Abstract:

Does anyone know how I might be able to suppress the text cursor in a TEdit, so it's not visible?

Answer:

For that you need to call the HideCaret API function after the control has processed the WM_SETFOCUS message. The OnEnter event is a tad too early for that, so if you do not want to create a TEdit descendent with an overriden message handler for WM_SETFOCUS you need to delay the action by posting a message to the form and have the handler for that message do the HideCaret.

The three edits on this example form share the same OnEnter handler (AllEditEnter), have ReadOnly = true and AutoSelect = false.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, stdctrls, jpeg;

const
  UM_HIDECARET = WM_USER + 101;
type
  TUMHideCaret = packed record
    msg: Cardinal;
    control: TWinControl;
  end;
type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    Edit1: TEdit;
    Edit2: TEdit;
    Edit3: TEdit;
    procedure AllEditEnter(Sender: TObject);
  private
    { Private declarations }
    procedure UMHideCaret(var msg: TUMHideCaret); message UM_HIDECARET;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.AllEditEnter(Sender: TObject);
begin
  PostMessage(handle, UM_HIDECARET, wparam(sender), 0);
end;

procedure TForm1.UMHideCaret(var msg: TUMHideCaret);
begin
  HideCaret(msg.control.Handle);
end;

end.

Of course this is an excellent way to confuse the user, since there will be no indication where the focus is anymore when the user tabs into one of the edits...

2009. október 10., szombat

How to implement delayed autotyping in a TDBEdit or TEdit


Problem/Question/Abstract:

Using a TDBEdit, I would like to achieve something like this on user input:
S ... (wait a few milliseconds) ... O ... (wait) ... M ... (wait) ... E

Answer:

procedure SendtextToControl(control: TWinControl; const S: string; interval: Integer {milliseconds});
var
  i: Integer;
begin
  for i := 1 to Length(S) do
  begin
    control.perform(WM_CHAR, Ord(S[i]), 0);
    control.Update;
    if i < Length(S) then
      Sleep(interval);
  end;
end;

2009. október 9., péntek

How to assign a system variable to a Session.PrivateDir


Problem/Question/Abstract:

On a terminal server I have a login script, which creates a temporary folder (c:\Temp\OfficeTemp\%username%) for each user that logs in. The system variable name is "OfficeTemp". So how do I assign this system variable to Session.PrivateDir? I tried Session.PrivateDir := 'OfficeTemp'; but this did not work. If I explicitly hardcode to := ''C:\temp\OfficeTemp\darshak' it works, but then I am back to my problem of share violation with multiple database, because all the temp files (all users) get created on that folder.

Answer:

function GetPESEnvironmentValue(pesVarName: string): string;
var
  lth: integer;
begin
  lth := GetEnvironmentVariable(@pesVarName[1], nil, 0);
  if lth > 0 then
  begin
    SetLength(result, lth);
    GetEnvironmentVariable(@pesVarName[1], @result[1], lth);
  end
  else
    result := '';
end;

Session.PrivateDir := GetPESEnvironmentValue('OfficeTemp');

2009. október 8., csütörtök

Drag and Drop from FileManager


Problem/Question/Abstract:

Drag and Drop from FileManager

Answer:

You need to use these 3 functions from the ShellApi:

DragAcceptFiles - registers whether a window accepts dropped files
DragQueryFile - retrieves the filename of a dropped file
DragFinish - releases memory allocated for dropping files


uses
  ShellApi;

..

procedure TForm1.FormCreate(Sender: TObject);
begin
  DragAcceptFiles(Form1.Handle, true);
  Application.OnMessage := AppMessage;
end;

{ message handler procedure }
// Delphi 1: type DWord = longint; .. FileIndex := -1;

procedure TForm1.AppMessage(var Msg: Tmsg; var Handled: Boolean);
const
  BufferLength: DWORD = 511;
var
  DroppedFilename: string;
  FileIndex: DWORD;
  NumDroppedFiles: DWORD;
  pDroppedFilename: array[0..511] of Char;
  DroppedFileLength: DWORD;
begin
  if Msg.message = WM_DROPFILES then
  begin
    FileIndex := $FFFFFFFF;
    NumDroppedFiles := DragQueryFile(Msg.WParam, FileIndex,
      pDroppedFilename, BufferLength);

    for FileIndex := 0 to (NumDroppedFiles - 1) do
    begin
      DroppedFileLength := DragQueryFile(Msg.WParam, FileIndex,
        pDroppedFilename, BufferLength);
      DroppedFilename := StrPas(pDroppedFilename);

      { process the file name you just received }
    end;
    DragFinish(Msg.WParam); { important to free memory }
    Handled := true;
  end;
end;

Notes:

When dropping files, the DroppedFilename is the complete path, not just the filename.ext
It is possible to drag and drop just a directory. So if you are expecting filenames, you have to check for existence yourself.
The filenames come in uppercased.

2009. október 7., szerda

Make the cell of a TStringGrid flash when its value changes


Problem/Question/Abstract:

I want the cell in a TStringGrid to flash for a few seconds when its value changes (as a result of some outside monitored process for example). How would I do that?

Answer:

One way would be to check the contents of the cell against the previous value in the OnDrawCell event. When it has changed, start a timer which invalidates the grid on a set interval. Below you'll find an example for just one cell.

{Somewhere in the private section of the form}
var
  FToggleCount: integer;
  FCheckstring: string;

implementation

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Inc(FToggleCount);
  if FToggleCount >= 10 then
    Timer1.Enabled := False;
  StringGrid1.Invalidate;
end;

procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  s: string;
begin
  if (ACol = 1) and (ARow = 1) then
    with Sender as TStringGrid do
    begin
      s := Cells[ACol, ARow];
      if s <> FCheckstring then
      begin
        FCheckstring := s;
        FToggleCount := 0;
        Timer1.Enabled := True;
      end;
      if Timer1.Enabled and ((FToggleCount mod 2) = 0) then
      begin
        Canvas.Brush.Color := clRed;
        Canvas.FillRect(Rect);
        Canvas.TextOut(Rect.Left + 2, Rect.Top + 2, s);
      end;
    end;
end;

2009. október 6., kedd

How to split and join a file using TFileStream


Problem/Question/Abstract:

How to split and join a file using TFileStream

Answer:

{Procedure Splitfile
Parameters:
sourcefilename: name of file to split
blocksize: maximum size of the parts to create

Call method:
static

Description:

Will split the passed file into partial files with a size of blocksize bytes (the last one may be smaller). The names of the files are created from the sourcefilename by replacing the extension with a number, .000, .001 etc. The files can be concatenated using the DOS Copy command with the /b switch to regenerate the original file.

Error Conditions:
I/O errors will raise exceptions.

Created: 03.03.98 by P. Below}

procedure Splitfile(const sourcefilename: string; blocksize: Longint);
var
  targetfilename: string;
  filecounter: Integer;
  bytesRemaining, bytesToWrite: LongInt;
  source, target: TFileStream;
begin
  if not FileExists(sourcefilename) or (blocksize < = 0) then
    Exit;
  source := TFileStream.Create(sourcefilename, fmOpenRead or fmShareDenyNone);
  try
    filecounter := 0;
    bytesRemaining := source.Size;
    while bytesRemaining > 0 do
    begin
      targetfilename := ChangeFileExt(sourcefilename, Format('.%.3d', [filecounter]));
      if blocksize < bytesRemaining then
        bytesToWrite := blocksize
      else
        bytesToWrite := bytesRemaining;
      target := TFileStream.Create(targetfilename, fmCreate);
      try
        target.CopyFrom(source, bytesToWrite);
        bytesRemaining := bytesRemaining - bytesToWrite;
        Inc(filecounter);
      finally
        target.Free;
      end;
    end;
  finally
    source.Free;
  end;
end;

2009. október 5., hétfő

Highlight the current cell in a TStringGrid


Problem/Question/Abstract:

How to highlight the current cell in a TStringGrid

Answer:

You solve your problem by using a handler for the grids OnDrawCell event. There you draw both cell background and text when the cell is in the column or row of the selected cell. You also need a handler for OnSelectCell. Here you invalidate the row and column for the old selected cell (still indicated by the grids Col and Row property) and then you invalidate the row and column for the newly selected cell (indicated by the handlers aCol and arow parameters). The TStringGrid class inherits some protected methods from its ancestors which you need for this task. These are InvalidateRow and InvalidateCol. You get at them using a cracker class.

type
  TGridCracker = class(TStringgrid);

procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  grid: TStringGrid;
begin
  grid := Sender as TStringGrid;
  if not (gdfixed in State) and ((grid.row = arow) or (grid.col = acol)) then
  begin
    with grid.canvas do
    begin
      brush.color := clYellow;
      if gdSelected in State then
        Font.color := clBlue
      else
        Font.color := clBlack;
      Fillrect(rect);
      TextRect(Rect, Rect.left + 2, rect.top + 2, grid.Cells[acol, arow]);
    end;
  end;
end;

procedure TForm1.StringGrid1SelectCell(Sender: TObject; ACol,
  ARow: Integer; var CanSelect: Boolean);
begin
  with TGridCracker(Sender as TStringGrid) do
  begin
    {redraw column and row if currently selected cell}
    Invalidaterow(row);
    Invalidatecol(col);
    {redraw column and row of new selected cell}
    Invalidaterow(arow);
    Invalidatecol(acol);
  end;
end;

2009. október 4., vasárnap

Access components by their name property


Problem/Question/Abstract:

How to access components by their name property

Answer:

The following example uses the FindComponent method of Form1 to disable the first 10 SpeedButtons by name:

for i := 1 to 10 do
  with Form1.FindComponent('SpeedButton' + IntToStr(i)) as TSpeedButton do
    Enabled := False;

2009. október 3., szombat

How to play an AVI file from a resource DLL


Problem/Question/Abstract:

I have an AVI as a resource that I want to play using TAnimate. In a standalone executable this works fine, but fails with a "Cannot Open AVI." error message. According to MSDN I should set the TAnimate ResHand to the instance handle of the DLL. How do I find this?

Answer:

It appears that trying to load an AVI from a resource causes an exception on the first attempt, so I gave it another go and it worked. Here's the code:


procedure TSplash.ShowSplash;
var
  ResHandle: Integer;
begin
  try
    ResHandle := LoadAviAsResource(Animate1, 'MyDll.dll', 'SPLASHAVI');
    { causes exception }
  except
    ResHandle := LoadAviAsResource(Animate1, 'MyDll.dll', 'SPLASHAVI');
    { this time it works }
  end;
  if ResHandle > 0 then
  begin
    Animate1.Visible := True;
    Animate1.Repetitions := -1;
    Animate1.Active := True
  end
  else
  begin
    Animate1.Visible := False;
    Animate1.Active := False;
    { Show a static bitmap or something if AVI cannot be displayed }
  end;
  Show;
end;

function TSplash.LoadAviAsResource(const AviName: TObject; DllName,
  ResourceName: string): Integer;
var
  ResourceHandle: THandle;
begin
  ResourceHandle := LoadLibrary(Pchar(DllName));
  TAnimate(AviName).ResName := ResourceName;
  TAnimate(AviName).ResHandle := ResourceHandle;
  FreeLibrary(ResourceHandle);
  result := ResourceHandle;
end;

2009. október 2., péntek

Interbase Object for executing all the Interbase commands at Run time


Problem/Question/Abstract:

How can I create Interbase database at run time? How can I change Interbase database password/user without using Interbase utilities?

Answer:

If an application runs on an interbase database, the database and all the required objects such as functions, stored procedures etc. has to be created before running the application.

And some of the commands such as changing the Administrators name and password has to be done either using Server manager of Interbase or by the command line utilities supplied by Interbase.

By including this unit in the project, You can execute all the required commands such as creating a database, changing the administrators password, creating shadows, functions, procedures etc.

Make sure that this object is created first in your application. In your project source file the unit "Object_Interbase" must be the first unit to follow after the standard units used by the application.

Include the Object_Interbase unit in your unit's uses cluase from which you are going to use the object. You will be able to get the variable named "ThisDataBase" of Class TMyIbDataBase which we will be using to executing the Interbase commands.

//*** THE UNIT STARTS HERE

unit Object_Interbase;

interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
  Dialogs, Registry, IBDataBase, IBQuery, FileCtrl;

type
  TShadow = (stManual, stAuto, stConditional);

  TMyIbDataBase = class(TObject)
  private
    //User Defined Type Variable

    FShdType: TShadow;

    //Components Variables
    FDataBase: TIBDatabase;
    FTransaction: TIBTransaction;
    FQuery: TIBQuery;

    //Boolean Variables
    FUseDefaultFiles: Boolean;
    FConnected: Boolean;
    FShadow: Boolean;

    //String Variables
    FIBServerPath: string;
    FDataBasePath, FShadowPath: string;
    FUser, FPassword: string;
    FDatabaseName: string;

    //Procedures
    procedure CheckDirPath(var Value: string);
    procedure ChangetoIBDir;
    procedure CreateComponents;
    procedure InitilizeVariables;
    procedure IBLoadpathfromRegistry;
    procedure SetDataBasePath(Value: string);
    procedure SetShadowPath(Value: string);
    procedure SetAdminName(Value: string);
    procedure SetAdminPassword(Value: string);
    procedure SetDatabaseName(Value: string);
    procedure SetShadow(Value: Boolean);
    procedure SetShadowType(Value: TShadow);
  protected
  public
    constructor Create;

    //Procedures
    procedure IBCreateDatabase;
    procedure IBConnectToDatabase;
    procedure IBDisConnecFromDatabase;
    procedure IBCreateShadow;
    procedure IBQueryAssisnSQL(Value: string; CloseAfterExecution: Boolean);
    procedure IBChangeAdminPassword(Value: string);

    //Component Properties
    property IBAppDatabase: TIBDatabase read FDataBase;
    property IBAppTransaction: TIBTransaction read FTransaction;
    property IBAppQuery: TIBQuery read FQuery;

    //User Defined Type Properties
    property IBShadowType: TShadow read FShdType write SetShadowType;

    //Boolean value Properties
    property IBConnected: Boolean read FConnected default False;
    property IBExists: Boolean read FUseDefaultFiles default False;
    property IBShadow: Boolean read FShadow write SetShadow default False;

    //String Value Properties
    property IBServerPath: string read FIBServerPath;
    property IBUserName: string read FUser write SetAdminName;
    property IBPassword: string read FPassword write SetAdminPassword;
    property IBDatabasePath: string read FDataBasePath write SetDataBasePath;
    property IBShadowPath: string read FShadowPath write SetShadowPath;
    property IBDatabaseName: string read FDatabaseName write SetDatabaseName;
  end;

var
  ThisDataBase: TMyIbDataBase;

implementation

{ TIbDataBase }

procedure TMyIbDataBase.CheckDirPath(var Value: string);
begin
  if Value[Length(Value)] <> '\' then
    Value := Value + '\';
  if not DirectoryExists(Value) then
  begin
    CreateDir(Value);
  end;
end;

procedure TMyIbDataBase.IBChangeAdminPassword(Value: string);
var
  I: Integer;
begin
  ThisDataBase.ChangetoIBDir;
  I := WinExec(pchar('gsec -user ' + ThisDataBase.IBUserName +
    ' -password ' + ThisDataBase.IBPassword +
    ' -mo ' + ThisDataBase.IBUserName + ' -pw ' +
    Value), 0);
  ThisDataBase.IBPassword := Value;
end;

procedure TMyIbDataBase.ChangetoIBDir;
begin
  if ThisDataBase.IBExists then
    ChDir(ThisDataBase.IBServerPath);
end;

procedure TMyIbDataBase.IBConnectToDatabase;
begin
  if not ThisDataBase.IBConnected then
  begin
    FDataBase.Close;
    FDataBase.SQLDialect := 1;
    FTransaction.Active := False;
    FQuery.Close;
    FDataBase.LoginPrompt := False;
    FDataBase.Params.Clear;
    FDataBase.Params.Add('USER_NAME=' + ThisDataBase.IBUserName);
    FDataBase.Params.Add('PASSWORD=' + ThisDataBase.IBPassword);
    FDataBase.DatabaseName := ThisDataBase.IBDatabasePath + IBDatabaseName;
    try
      FDataBase.Connected := True;
      FTransaction.DefaultDatabase := FDataBase;
    except
    end;
    FConnected := FDataBase.Connected;
    FQuery.Transaction := FTransaction;
    FQuery.Database := FDataBase;
    FDataBase.DefaultTransaction := FTransaction;
    if FConnected then
    begin
      FTransaction.Active := True;
    end;
  end;
end;

constructor TMyIbDataBase.Create;
begin
  CreateComponents;
  InitilizeVariables;
  IBLoadpathfromRegistry;
end;

procedure TMyIbDataBase.CreateComponents;
begin
  FDataBase := TIBDatabase.Create(Application);
  FTransaction := TIBTransaction.Create(Application);
  FDataBase.DefaultTransaction := FTransaction;
  FTransaction.DefaultDatabase := FDataBase;
  FQuery := TIBQuery.Create(Application);
  FQuery.Database := FDataBase;
  FQuery.Transaction := FTransaction;
  FQuery.ParamCheck := False;
end;

procedure TMyIbDataBase.IBCreateDatabase;
var
  vmem: TStringList;
  S: string;
begin
  S := ExtractFilePath(Application.ExeName);
  vmem := TStringList.Create;
  vmem.Add('Create database "' + ThisDataBase.IBDatabasePath +
    ThisDataBase.IBDatabaseName +
    '" user "' + ThisDataBase.IBUserName + '" password "' +
    ThisDataBase.IBPassword + '" page_size=2048 Length=50;');
  vmem.Add('Commit work;');
  vmem.Add('gfix -w "sync" -user "' + ThisDataBase.IBUserName + '" -pa ' +
    ThisDataBase.IBPassword + '" "' + ThisDataBase.IBDatabasePath +
    ThisDataBase.IBDatabaseName + '"');
  S := S + 'Sql03EASY05.Sql';
  vmem.SaveToFile(S);
  vmem.Free;
  ThisDataBase.ChangetoIBDir;
  S := 'isql -input ' + S;
  winexec(pchar(S), 0);
  DeleteFile(S);
  S := ThisDataBase.IBDatabasePath + ThisDataBase.IBDatabaseName;
  while not FileExists(S) do
    ;
  ThisDataBase.IBConnectToDatabase;
  FConnected := FDataBase.Connected;
end;

procedure TMyIbDataBase.IBCreateShadow;
var
  S, vFname: string;
begin
  if ThisDataBase.IBConnected then
  begin
    case FShdType of
      stAuto: S := 'Auto';
      stManual: S := 'Manual';
      stConditional: S := 'Conditional';
    end;
    vFname := Copy(FDatabaseName, 1, pos('.', FDatabaseName)) + 'Shd';
    FQuery.Close;
    FQuery.SQL.Clear;
    FQuery.SQL.Text := 'Create Shadow 1 ' + S + ' "' + FShadowPath +
      vFname + '" Length = 10000';
    FQuery.ExecSQL;
    Application.ProcessMessages;
  end;
end;

procedure TMyIbDataBase.InitilizeVariables;
begin
  FDataBasePath := '';
  FShadowPath := '';
  FIBServerPath := '';
  FUser := '';
  FPassword := '';
  FDatabaseName := '';
  FShdType := stConditional;
  FConnected := False;
end;

procedure TMyIbDataBase.IBLoadpathfromRegistry;
var
  vReg: TRegistry;
begin
  vReg := TRegistry.Create;
  vReg.RootKey := HKEY_LOCAL_MACHINE;
  if vReg.OpenKey('\Software\InterBase Corp\InterBase\CurrentVersion', False) then
  begin
    FIBServerPath := vreg.ReadString('ServerDirectory');
    FUseDefaultFiles := True;
  end
  else
  begin
    FIBServerPath := ExtractFilePath(Application.ExeName);
    FUseDefaultFiles := False;
  end;
  vReg.CloseKey;
  vReg.Free;
end;

procedure TMyIbDataBase.SetAdminName(Value: string);
begin
  if (Value <> FUser) then
    FUser := Value;
end;

procedure TMyIbDataBase.SetAdminPassword(Value: string);
begin
  if (Value <> FPassword) then
    FPassword := Value;
end;

procedure TMyIbDataBase.SetDatabaseName(Value: string);
begin
  if (Value <> FDatabaseName) then
    FDatabaseName := Value;
end;

procedure TMyIbDataBase.SetDataBasePath(Value: string);
begin
  if (Value <> FDataBasePath) then
  begin
    FDataBasePath := Value;
    CheckDirPath(FDataBasePath);
  end;
end;

procedure TMyIbDataBase.SetShadow(Value: Boolean);
begin
  if (Value <> FShadow) then
    FShadow := Value;
end;

procedure TMyIbDataBase.SetShadowPath(Value: string);
begin
  if (Value <> FShadowPath) then
  begin
    FShadowPath := Value;
    CheckDirPath(FShadowPath);
  end;
end;

procedure TMyIbDataBase.SetShadowType(Value: TShadow);
begin
  if (Value <> FShdType) then
    FShdType := Value;
end;

procedure TMyIbDataBase.IBQueryAssisnSQL(Value: string; CloseAfterExecution: Boolean);
begin

  FQuery.Close;
  FQuery.SQL.Clear;
  FQuery.SQL.Text := Value;
  try
    FQuery.ExecSQL;
  except
  end;
  if CloseAfterExecution then
  begin
    FQuery.Close;
    FQuery.SQL.Clear;
    FQuery.SQL.Text := 'Commit';
    FQuery.ExecSQL;
    FQuery.Close;
  end;
end;

procedure TMyIbDataBase.IBDisConnecFromDatabase;
begin
  FDataBase.CloseDataSets;
  FDataBase.ForceClose;
  FConnected := FDataBase.Connected;
end;

initialization
  if (ThisDataBase = nil) then
    ThisDataBase := TMyIbDataBase.Create;
finalization
  if (ThisDataBase <> nil) then
  begin
    ThisDataBase.Free;
    ThisDataBase := nil;
  end;
end.

//** THE UNIT ENDS HERE

Examples:

1. Creating a Database

If you want to create a database called "Sample.Gdb" in the directory called "c:\test\" with the administrator named
"LION" with the password "king". Just by using the properties and methods of this simple object we can create the database.

ThisDataBase.IBUserName := 'LION';
ThisDataBase.IBPassword := 'king';
ThisDataBase.IBDatabasePath := 'c:\test\';
ThisDataBase.IBDatabaseName := 'Sample.Gdb';
ThisDataBase.IBCreateDatabase;

The properties IBUserName, IBPassword, IBDatabaseName has to be assigned only once.

2. Creating Shadow

ThisDataBase.IBConnectToDatabase;
ThisDatabse.IBShadowType := stAuto;
ThisDatabse.IBCreateShadow;

3. Changing Database Password

ThisDataBase.IBChangeAdminPassword('NewPassword');

4. Creating a Table

ThisDatabase.IBQueryAssisnSQL('CREATE TABLE USERS(                    ' +
  'USERCODE      VARCHAR(6)  NOT NULL   , ' +
  'USERNAME      VARCHAR(20) NOT NULL   , ' +
  'USERACTIVE    VARCHAR(1)  DEFAULT "Y", ', True);

In the same way you can assign the scripts for creating the stored procedures, function and the scripts for creating all the objects using this method.

5. The properties  IBAppDatabase, IBAppTransaction and IBAppQuery can be used to assign to the properties of the IBTable component if you are go to work with the IBTable component.

If in case you are not going to work with the Interbase components this object can be modifed to work with the simple database components.

2009. október 1., csütörtök

Disabling a TMemo without getting gray text


Problem/Question/Abstract:

I want to set Enabled=False in my TMemo, but I don't want the text to turn gray.

Answer:

OPTION 1:

Ok, this is a dirty trick. But is works fine for me.

Set the Enabled to True (yes realy).
In the OnEnter event of your memo put:

procedure TForm1.MyMemoEnter(Sender: TObject);
begin
  SomeOtherControl.SetFocus;
end;

Now, focus will be forced to some other control every time your user tries to fucus on your memo. To the user it seems that the memo is disabled.

OPTION 2:

This one is much simpler:
Put the memo on a panel. Then set Panel.Enabled=False.
Ok, this works if you can put the memo on a panel. Just with inherited forms you can get into some trouble. But is should work for most people.