2009. augusztus 31., hétfő

Working with KeyBoard Binding


Problem/Question/Abstract:

How to set your own shortcut keys while working with delphi editor? If you want your own piece of shortcut key to perform a certain action for you then this code will help

Answer:

Include this unit in a delphi package, and install the package. Now, if you press ctrl + d you will get the 'This was written by Subha Narayanan' in your editor window.
The actual process is very simple. We use the interface TNotifier Object and IOTAkeyboardbinding to create our own interface.
Our main key to perform this action is the procedure 'Dupline'

unit DupLineBinding;

interface

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

type
  TDupLineBinding = class(TNotifierObject, IOTAKeyboardBinding)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
    procedure DupLine(const Context: IOTAKeyContext; KeyCode: TShortCut;
      var BindingResult: TKeyBindingResult);
    {IOTAKeyBoardBinding}
    function GetBindingType: TBindingType;
    function GetDisplayName: string;
    function GetName: string;
    procedure BindKeyboard(const BindingServices: IOTAKeyBindingServices);

  published
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  (BorlandIDEServices as
    IOTAKeyBoardServices).AddKeyBoardBinding(TDupLineBinding.Create);
end;

function TDupLineBinding.GetBindingType: TBindingType;
begin
  Result := btPartial;
end;

function TDupLineBinding.GetDisplayName;
begin
  Result := 'Subha Line Binding';
    {The way it shoudl appear in the delphi ide editor window}
end;

function TDupLineBinding.GetName;
begin
  Result := 'sn.dupline'; {Should be unique}
end;

procedure TDupLineBinding.DupLine(const Context: IOTAKeyContext; KeyCode: TShortCut;
  var BindingResult: TKeyBindingResult);
var
  ep: IOTAEditPosition;
  eb: IOTAEditBlock;
  r, c: Integer;
begin
  {Actual place where the writting into editor takes place}
  try
    ep := Context.EditBuffer.EditPosition;
    ep.Save;
    r := ep.Row;
    c := ep.Column;
    eb := Context.EditBuffer.EditBlock;
    ep.MoveBOL;
    eb.Reset;
    eb.BeginBlock;
    eb.Extend(Ep.Row + 1, 1);
    eb.EndBlock;
    eb.Copy(False);
    ep.MoveBOL;
    ep.Paste;
    ep.Move(r, c);
  finally
    ep.Restore;
  end;
  BindingResult := krHandled;
end;

procedure TDupLineBinding.BindKeyboard(const BindingServices: IOTAKeyBindingServices);
{Here we specify the shortcut key which should do the action}
begin
  BindingServices.AddKeyBinding([Shortcut(Ord('D'), [ssCtrl])], DupLine, nil);
end;

end.

2009. augusztus 30., vasárnap

Create a multiple line heading in a TStringGrid


Problem/Question/Abstract:

How to create a multiple line heading in a TStringGrid

Answer:

Here is an example for a TStringGrid that has a multiple line heading with centered and bold text:

procedure TForm1.grid1DrawCell(Sender: TObject; Col, Row: Longint;
  Rect: TRect; State: TGridDrawState);
var
  l_oldalign: word;
  l_YPos, l_XPos, i: integer;
  s, s1: string;
  l_col, l_row: longint;
begin
  l_col := col;
  l_row := row;
  with sender as TStringGrid do
  begin
    if (l_row = 0) then
      canvas.font.style := canvas.font.style + [fsbold];
    if l_row = 0 then
    begin
      l_oldalign := settextalign(canvas.handle, ta_center);
      l_XPos := rect.left + (rect.right - rect.left) div 2;
      s := cells[l_col, l_row];
      while s <> '' do
      begin
        if pos(#13, s) <> 0 then
        begin
          if pos(#13, s) = 1 then
            s1 := ''
          else
          begin
            s1 := trim(copy(s, 1, pred(pos(#13, s))));
            delete(s, 1, pred(pos(#13, s)));
          end;
          delete(s, 1, 2);
        end
        else
        begin
          s1 := trim(s);
          s := '';
        end;
        l_YPos := rect.top + 2;
        canvas.textrect(rect, l_Xpos, l_YPos, s1);
        inc(rect.top, rowheights[l_row] div 3);
      end;
      settextalign(canvas.handle, l_oldalign);
    end
    else
    begin
      canvas.textrect(rect, rect.left + 2, rect.top + 2, cells[l_col, l_row]);
    end;
    canvas.font.style := canvas.font.style - [fsbold];
  end;
end;

2009. augusztus 29., szombat

How to check when a screen saver has been invoked


Problem/Question/Abstract:

I want my application to go into 'sleep mode' when the screen saver has been started.

Answer:

unit U1;

interface

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

type
  TForm1 = class(TForm)
    Label1: TLabel;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    procedure AppMessage(var AMessage: TMsg; var Handled: Boolean);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

{Get screen saver messages}

procedure TForm1.AppMessage(var AMessage: TMsg; var Handled: Boolean);
begin
  if ((WM_SYSCOMMAND = AMessage.Message) and (SC_SCREENSAVE = AMessage.wParam)) then
  begin
    Handled := True;
    Label1.Caption := 'Warning! Screen saver detected. Top clearance required.';
    Handled := False;
  end;
end;

{On form create}

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnMessage := AppMessage;
end;

end.

2009. augusztus 28., péntek

How to store the content of the clipboard in a file


Problem/Question/Abstract:

How to store the content of the clipboard in a file

Answer:

{ ... }
if Clipboard.HasFormat(CF_BITMAP) then
begin
  bmp := TBitmap.Create;
  try
    Clipboard.AssignTo(bmp);
    bmp.SaveToFile(filename);
  finally
    bmp.free
  end;
end;
{ ... }

2009. augusztus 27., csütörtök

How to rotate a TBitmap smoothly


Problem/Question/Abstract:

I would like to rotate a TBitmap using a smooth algorithm but without clipping the corner of the bitmap. The size of the rotated bitmap change according to the angle.

Answer:

cx, cy represent the center of rotation.

function IntToByte(i: integer): byte;
begin
  if (i > 255) then
    Result := 255
  else if (i < 0) then
    Result := 0
  else
    Result := i;
end;

function TrimInt(i, Min, Max: integer): integer;
begin
  if (i > Max) then
    Result := Max
  else if (i < Min) then
    Result := Min
  else
    Result := i;
end;

procedure SmoothRotate(Src, Dst: TBitmap; cx, cy: integer; Angle: Extended);

type
  TFColor = record
    b, g, r: Byte
  end;

var
  Top, Bottom, Left, Right, eww, nsw, fx, fy, wx, wy: Extended;
  cAngle, sAngle: Double;
  xDiff, yDiff, ifx, ify, px, py, ix, iy, x, y: Integer;
  nw, ne, sw, se: TFColor;
  P1, P2, P3: PByteArray;
begin
  Src.PixelFormat := pf24Bit;
  Dst.PixelFormat := pf24Bit;
  Angle := -Angle * Pi / 180;
  sAngle := Sin(Angle);
  cAngle := Cos(Angle);
  xDiff := (Dst.Width - Src.Width) div 2;
  yDiff := (Dst.Height - Src.Height) div 2;
  for y := 0 to Dst.Height - 1 do
  begin
    P3 := Dst.scanline[y];
    py := 2 * (y - cy) + 1;
    for x := 0 to Dst.Width - 1 do
    begin
      px := 2 * (x - cx) + 1;
      fx := (((px * cAngle - py * sAngle) - 1) / 2 + cx) - xDiff;
      fy := (((px * sAngle + py * cAngle) - 1) / 2 + cy) - yDiff;
      ifx := Round(fx);
      ify := Round(fy);
      if (ifx > -1) and (ifx < Src.Width) and (ify > -1) and (ify < Src.Height) then
      begin
        eww := fx - ifx;
        nsw := fy - ify;
        iy := TrimInt(ify + 1, 0, Src.Height - 1);
        ix := TrimInt(ifx + 1, 0, Src.Width - 1);
        P1 := Src.scanline[ify];
        P2 := Src.scanline[iy];
        nw.r := P1[ifx * 3];
        nw.g := P1[ifx * 3 + 1];
        nw.b := P1[ifx * 3 + 2];
        ne.r := P1[ix * 3];
        ne.g := P1[ix * 3 + 1];
        ne.b := P1[ix * 3 + 2];
        sw.r := P2[ifx * 3];
        sw.g := P2[ifx * 3 + 1];
        sw.b := P2[ifx * 3 + 2];
        se.r := P2[ix * 3];
        se.g := P2[ix * 3 + 1];
        se.b := P2[ix * 3 + 2];
        Top := nw.b + eww * (ne.b - nw.b);
        Bottom := sw.b + eww * (se.b - sw.b);
        P3[x * 3 + 2] := IntToByte(Round(Top + nsw * (Bottom - Top)));
        Top := nw.g + eww * (ne.g - nw.g);
        Bottom := sw.g + eww * (se.g - sw.g);
        P3[x * 3 + 1] := IntToByte(Round(Top + nsw * (Bottom - Top)));
        Top := nw.r + eww * (ne.r - nw.r);
        Bottom := sw.r + eww * (se.r - sw.r);
        P3[x * 3] := IntToByte(Round(Top + nsw * (Bottom - Top)));
      end;
    end;
  end;
end;

2009. augusztus 26., szerda

How to remove white-spaces from a TMemo


Problem/Question/Abstract:

How to remove white-spaces from a TMemo

Answer:

This would trim a TMemo (multi-line string), discarding any white-space from end-of-lines and from end-of-text. The end result is exactly similar to the original, without the useless extras, usually left-overs from bad typing habits.

function MemoTrimTrail(const aMemo: string): string;
var
  iRead, iWrite, vLastNonWhite, vLastNonSpace: Integer;
  vChr: Char;
  vIsSpace, vIsReturn: Boolean;
begin
  if aMemo = '' then
  begin {empty string}
    Result := ''; {nothing to do}
    exit;
  end;
  SetLength(Result, Length(aMemo)); {initially, empty string of same length}
  UniqueString(Result); {make sure we have a separate copy}
  iWrite := 0; {where characters will be written out}
  vLastNonWhite := 0; {last non-space, non-return}
  vLastNonSpace := 0; {last non-space, but could be return}
  for iRead := 1 to Length(aMemo) do
  begin
    vChr := aMemo[iRead]; {pick next char in source}
    vIsReturn := vChr in [#13, #10]; {CR or LF}
    vIsSpace := vChr in [#32, #09]; {space or tab}
    if vIsReturn then
      iWrite := vLastNonSpace + 1 {skip empty end-of-lines}
    else
      Inc(iWrite);
    Result[iWrite] := vChr; {write char in result-string}
    if not vIsSpace then
    begin
      vLastNonSpace := iWrite; {last non-space, returns are Ok}
      if not vIsReturn then
      begin
        vLastNonWhite := iWrite; {last black-ink character}
      end;
    end;
  end;
  SetLength(Result, vLastNonWhite); {truncate at last black-ink character}
end;

2009. augusztus 25., kedd

Detecting database changes


Problem/Question/Abstract:

Detecting database changes

Answer:

Do you need to determine if a user has changed any of the fields of a displayed record, but hasn't yet posted the record? If so, you can check the current state of the database via the datasource associated with it. To do so, simply write the following code:

if (Datasource1.State in dsEditModes) then
  {...code you want to execute...}

If the datasource's State property is dsEdit, dsInsert or dsSetKey, this statement will evaluate as true.

2009. augusztus 24., hétfő

ADO data state


Problem/Question/Abstract:

ADO data state

Answer:

This little code snip is how to get the database's state as it changes.

procedure TForm1.DataSource1StateChange(Sender: TObject);
var
  ds: string;
begin
  case ADOTable1.State of
    dsInactive: ds := 'Closed';
    dsBrowse: ds := 'Browsing';
    dsEdit: ds := 'Editing';
    dsInsert: ds := 'New record inserting';
  else
    ds := 'Other states'
  end;
  Caption := 'ADOTable1 state: ' + ds;
end;

2009. augusztus 23., vasárnap

How to enable/disable individual buttons on a TDBNavigator


Problem/Question/Abstract:

How to enable/disable individual buttons on a TDBNavigator

Answer:

{Append allowed/ not allowed}
DBNavigator1.Controls[4].Enabled := not DBNavigator1.Controls[4].Enabled

2009. augusztus 22., szombat

Abstraction of Runtime Queries from Code


Problem/Question/Abstract:

Programming methodology books such as 'The Pragmatic Programmer' by Andrew Hunt and David Thomas teach the principles of decoupling, abstraction and non-repetition. This article shows how to achieve some of these goals when codeing queries whose SQL statements are set at runtime.

Answer:

In article "Auxiliary TQuery used with queries built at run time", Fernando Martins suggested a way to avoid code replication when using TQuery objects with SQL statements set at runtime. Here I show how to take this a step further and remove the SQL from your code so that the queries can be changed without recompiling your program.

My queries are stored within an inifile in the program's directory. This provides us with a simple file format which is easy to use both in and out of Delphi. My inifile has the following syntax:

[QUERIES]
1=SELECT CAPITAL FROM COUNTRY WHERE NAME = 'Argentina'
2=SELECT NAME FROM COUNTRY WHERE CONTINENT = 'South America'

To perform the query call the ExecuteQuery procedure which in turn will call the GetQuery function. ExecuteQuery must be passed the following paramaters:

myQuery : TQuery  - TQuery component used to perform query
queryID : integer - ID number in inifile for the SQL satement
myDB : string     - optional Database Name

ExecuteQuery(qryRuntime, 1, );
ExecuteQuery(qryRuntime, 2, 'DBDEMOS');

Now for the code:

uses IniFiles;

const
  queryFileName = 'queries.ini';

procedure ExecuteQuery(myQuery: TQuery; const queryID: integer; const myDB: string =
  '');
{performs query getting SQL statement at runtime from inifile}
begin
  if not (myDB = '') then
    myQuery.DatabaseName := myDB;
  try
    myQuery.Close;
    myQuery.SQL.Clear;
    myQuery.SQL.Add(GetQuery(queryID));
    myQuery.Open;
  except
    on E: Exception do
      MessageDlg(E.message, mtError, [mbYes], 0);
  end; {try..except}
end; {procedure ExecuteQuery}

function GetQuery(const qID: integer): string;
{reads SQL statement from inifile}
var
  DirPath: string;
  queryIni: TIniFile;
begin
  DirPath := ExtractFilePath(ParamStr(0));
  queryIni := TIniFile.Create(DirPath + queryFileName);
  try
    if not (queryIni.ValueExists('QUERIES', IntToStr(qID))) then
      raise Exception.Create('ERROR: Query ID not found in file!')
    else
      result := queryIni.ReadString('QUERIES', IntToStr(qID), '');
  finally
    queryIni.Free;
  end; {try..finally}
end; {function GetQuery}

Finally, to avoid having to look up long lists of query IDs when programming, incorporate them into a unit of constant values so that you can use code like the following:

ExecuteQuery(qryRuntime, GET_CAPITAL, );
ExecuteQuery(qryRuntime, GET_COUNTRIES, 'DBDEMOS');

2009. augusztus 21., péntek

How to open and read the first frame in an AVI file


Problem/Question/Abstract:

How to open and read the first frame in an AVI file

Answer:

{File: AVIObjects.pas
Author: Liran Shahar
Purpose: AVI file objects routines to open,read and retrive poster frames (first frame in AVI file)
Copyright(C) 2001, Com-N-Sense Ltd, all rights reserved

Note: this unit is released as freeware. In other words, you are free  to use this unit in your own applications, however I retain all copyright to the code. LS}

unit AVIObjects;

interface

uses
  Windows, Graphics, Sysutils, Classes, VFW, Ole2;

type
  TAviFileStream = class(TPersistent)
  private
    aviFile: IAviFile;
    aviStream: IAviStream;
    aviFrame: IGetFrame;
    aviInfo: TAviStreamInfo;
  protected
    function GetFrameCount: cardinal; virtual;
    function GetDuration: double; virtual;
    function GetWidth: integer; virtual;
    function GetHeight: integer; virtual;
    function GetWantedBitmapFormat: PBitmapInfoHeader; virtual;
  public
    constructor Create; virtual;
    destructor Destroy; override;
    function Active: boolean; virtual;
    procedure Open(const Filename: AnsiString); virtual;
    procedure Close; virtual;
    procedure GetFrame(FrameNumber: cardinal; var DIB: PBitmapInfoHeader); virtual;
    property FrameCount: cardinal read GetFrameCount;
    property Duration: double read GetDuration;
    property ImageWidth: integer read GetWidth;
    property ImageHeight: integer read GetHeight;
  end;

implementation

constructor TAviFileStream.Create;
begin
  inherited Create;
  aviFile := nil;
  aviStream := nil;
  aviFrame := nil;
end;

destructor TAviFileStream.Destroy;
begin
  Close;
  inherited Destroy;
end;

function TAviFileStream.Active: boolean;
begin
  Result := (aviStream <> nil) and (aviFrame <> nil);
end;

function TAviFileStream.GetFrameCount: cardinal;
begin
  Result := aviInfo.dwLength;
end;

function TAviFileStream.GetDuration: double;
begin
  if (aviInfo.dwRate <> 0) and (aviInfo.dwScale <> 0) then
    Result := aviInfo.dwLength / (aviInfo.dwRate / aviInfo.dwScale)
  else
    Result := 0.0;
end;

function TAviFileStream.GetWidth: integer;
begin
  Result := aviInfo.rcFrame.Right - aviInfo.rcFrame.Left;
end;

function TAviFileStream.GetHeight: integer;
begin
  Result := aviInfo.rcFrame.Bottom - aviInfo.rcFrame.Top;
end;

function TAviFileStream.GetWantedBitmapFormat: PBitmapInfoHeader;
begin
  Result := nil;
end;

procedure TAviFileStream.Open(const Filename: AnsiString);
var
  iResult: integer;
  BmpInfoHeader: PBitmapInfoHeader;
begin
  Close;
  fillchar(aviInfo, sizeof(aviInfo), 0);
  iResult := AviFileOpen(aviFile, pchar(FileName), OF_READ + OF_SHARE_DENY_WRITE, nil);
  if iResult <> AVIERR_OK then
    raise Exception.Create('Cannot open AVI file ' + Filename);
  iResult := AVIFileGetStream(aviFile, aviStream, streamTypeVideo, 0);
  if iResult <> AVIERR_OK then
    raise Exception.Create('Cannot open stream for that file');
  iResult := AVIStreamInfo(aviStream, aviInfo, sizeof(aviInfo));
  if iResult <> AVIERR_OK then
    raise Exception.Create('Cannot read stream info');
  BmpInfoHeader := GetWantedBitmapFormat;
  aviFrame := AVIStreamGetFrameOpen(aviStream, BmpInfoHeader);
  if not assigned(aviFrame) then
    raise Exception.Create('Cannot find suitable decompressor');
  if assigned(BmpInfoHeader) then
    dispose(BmpInfoHeader);
end;

procedure TAviFileStream.Close;
var
  iResult: integer;
begin
  aviFrame := nil;
  aviStream := nil;
  aviFile := nil;
end;

procedure TAviFileStream.GetFrame(FrameNumber: cardinal; var DIB: PBitmapInfoHeader);
begin
  DIB := aviStreamGetFrame(aviFrame, FrameNumber)
end;

initialization
  CoInitialize(nil);
  AVIFileInit;

finalization
  AVIFileExit;
  CoUninitialize;

end.

2009. augusztus 20., csütörtök

How to run an application in a TOLEContainer without using the Insert Object dialog


Problem/Question/Abstract:

I want to run MS Word by using TOleContainer but I don't want to use the Insert Object dialog. How can I do that?

Answer:

{Creating a new Word document in an Olecontainer}
OleContainer1.CreateObject('Word.Document', False);
OleContainer1.DoVerb(ovShow);

{Loading an existing document in an Olecontainer}
OleContainer1.CreateObjectFromFile('C:\Docs\Doc1.doc', False);
OleContainer1.DoVerb(ovShow);

2009. augusztus 19., szerda

Convert Listbox.TabWidth to screen pixels


Problem/Question/Abstract:

How to convert Listbox.TabWidth to screen pixels

Answer:

PixelsX := TabWidth * LoWord(GetDialogBaseUnits) div 4 * Canvas.TextWidth(' ') div 4

2009. augusztus 18., kedd

How to save and restore font properties in the registry


Problem/Question/Abstract:

I tried to save user selected font settings in the registry. Therefore I declared a variable Wfont: TFont; and created it with Wfont := TFont.create; . All works fine, like setting a panel's font and so on, but when I try to write it to the registry using reg.writebinarydata('Font',wfont,sizeof(Tfont)), only 4 Bytes are stored. Ergo the font could not be loaded.

Answer:

Saving and restoring font properties in the registry:

uses
  typInfo, Registry;

function GetFontProp(anObj: TObject): TFont;
var
  PInfo: PPropInfo;
begin
  {try to get a pointer to the property information for a property with the name 'Font'. TObject.ClassInfo returns a pointer to the RTTI table, which we need to pass to GetPropInfo}
  PInfo := GetPropInfo(anObj.ClassInfo, 'font');
  Result := nil;
  if PInfo <> nil then
    {found a property with this name, check if it has the correct type}
    if (PInfo^.Proptype^.Kind = tkClass) and
      GetTypeData(PInfo^.Proptype^)^.ClassType.InheritsFrom(TFont) then
      Result := TFont(GetOrdProp(anObj, PInfo));
end;

function StyleToString(styles: TFontStyles): string;
var
  style: TFontStyle;
begin
  Result := '[';
  for style := Low(style) to High(style) do
  begin
    if style in styles then
    begin
      if Length(result) > 1 then
        result := result + ',';
      result := result + GetEnumname(typeInfo(TFontStyle), Ord(style));
    end;
  end;
  Result := Result + ']';
end;

function StringToStyle(S: string): TFontStyles;
var
  sl: TStringlist;
  style: TFontStyle;
  i: Integer;
begin
  Result := [];
  if Length(S) < 2 then
    Exit;
  if S[1] = '[' then
    Delete(S, 1, 1);
  if S[Length(S)] = ']' then
    Delete(S, Length(S), 1);
  if Length(S) = 0 then
    Exit;
  sl := TStringlist.Create;
  try
    sl.commatext := S;
    for i := 0 to sl.Count - 1 do
    begin
      try
        style := TFontStyle(GetEnumValue(Typeinfo(TFontStyle), sl[i]));
        Include(Result, style);
      except
      end;
    end;
  finally
    sl.free
  end;
end;

procedure SaveFontProperties(forControl: TControl; toIni: TRegInifile; const section:
  string);
var
  font: TFont;
  basename: string;
begin
  Assert(Assigned(toIni));
  font := GetFontProp(forControl);
  if not Assigned(font) then
    Exit;
  basename := forControl.Name + '.Font.';
  toIni.WriteInteger(Section, basename + 'Charset', font.charset);
  toIni.WriteString(Section, basename + 'Name', font.Name);
  toIni.WriteInteger(Section, basename + 'Size', font.size);
  toIni.WriteString(Section, basename + 'Color', '$' + IntToHex(font.color, 8));
  toIni.WriteString(Section, basename + 'Style', StyleToString(font.Style));
end;

procedure RestoreFontProperties(forControl: TControl; toIni: TRegInifile; const
  section: string);
var
  font: TFont;
  basename: string;
begin
  Assert(Assigned(toIni));
  font := GetFontProp(forControl);
  if not Assigned(font) then
    Exit;
  basename := forControl.Name + '.Font.';
  font.Charset := toIni.ReadInteger(Section, basename + 'Charset', font.charset);
  font.Name := toIni.ReadString(Section, basename + 'Name', font.Name);
  font.Size := toIni.ReadInteger(Section, basename + 'Size', font.size);
  font.Color := TColor(StrToInt(toIni.ReadString(Section, basename + 'Color', '$' +
    IntToHex(font.color, 8))));
  font.Style := StringToStyle(toIni.ReadString(Section, basename + 'Style',
    StyleToString(font.Style)));
end;

2009. augusztus 17., hétfő

How to create a TScrollBox without scrollbars


Problem/Question/Abstract:

How to create a TScrollBox without scrollbars

Answer:

Below is a TScrollbox descendent with properties to hide either scrollbar. It can also do a tiled bitmap background. The latter hasn't been made foolproof yet.

THideScrollbarScrollbox = class(TScrollbox)
private
  fHideVertScrollbar, fHideHorzScrollbar: Boolean;
  fVertPosition, fVertRange: Integer;
  fHorzPosition, fHorzRange: Integer;
  OldVisible, OldHorzVisible: Boolean;
  fBackBmp: TBitmap;
  function GetVertPosition: Integer;
  procedure SetVertPosition(const Value: Integer);
  function GetVertRange: Integer;
  procedure SetVertRange(const Value: Integer);
  procedure SetHideVertScrollbar(const Value: Boolean);
  procedure WMPaint(var msg: TWMPaint); message WM_PAINT;
  procedure SetHideHorzScrollbar(const Value: Boolean);
  function GetHorzPosition: Integer;
  function GetHorzRange: Integer;
  procedure SetHorzPosition(const Value: Integer);
  procedure SetHorzRange(const Value: Integer);
protected
  procedure PaintWindow(DC: HDC); override;
public
  constructor Create(AOwner: TComponent); override;
  procedure scrollinview(AControl: TControl);
  property BackBmp: TBitmap read fBackBmp write fBackBmp;
published
  property HideVertScrollbar: Boolean read fHideVertScrollbar write SetHideVertScrollbar;
  property HideHorzScrollbar: Boolean read fHideHorzScrollbar write SetHideHorzScrollbar;
  {use these to set positions and range:}
  property VertPosition: Integer read GetVertPosition write SetVertPosition;
  property VertRange: Integer read GetVertRange write SetVertRange;
  property HorzPosition: Integer read GetHorzPosition write SetHorzPosition;
  property HorzRange: Integer read GetHorzRange write SetHorzRange;
end;

implementation

{ THideScrollbarScrollbox }

constructor THideScrollbarScrollbox.Create(AOwner: TComponent);
begin
  inherited;
  OldVisible := VertScrollbar.Visible;
  fVertPosition := 0;
  fBackBmp := nil;
end;

function THideScrollbarScrollbox.GetHorzPosition: Integer;
begin
  if HorzScrollbar.Visible or not fHideHorzScrollbar then
  begin
    Result := HorzScrollbar.position;
    fHorzPosition := Result;
  end
  else
    Result := fHorzPosition;
end;

function THideScrollbarScrollbox.GetHorzRange: Integer;
begin
  if HorzScrollbar.Visible or not fHideHorzScrollbar then
  begin
    Result := HorzScrollbar.Range;
    fHorzRange := Result;
  end
  else
    Result := fHorzRange;
end;

function THideScrollbarScrollbox.GetVertPosition: Integer;
begin
  if VertScrollbar.Visible or not fHideVertScrollbar then
  begin
    Result := VertScrollbar.position;
    fVertPosition := Result;
  end
  else
    Result := fVertPosition;
end;

function THideScrollbarScrollbox.GetVertRange: Integer;
begin
  if VertScrollbar.Visible or not fHideVertScrollbar then
  begin
    Result := VertScrollbar.Range;
    fVertRange := Result;
  end
  else
    Result := fVertRange;
end;

procedure TileBitmap(ABm: TBitmap; aDC: HDC; bmw, bmh, cw, ch, cx, cy: Integer);
var
  x, y: Integer;
  BMDC: HDC;
begin
  y := cy;
  if bmw > 0 then
    if bmh > 0 then
    begin
      BMDC := ABm.Canvas.Handle;
      while y < ch do
      begin
        x := cx;
        if y + bmh > 0 then
          while x < cw do
          begin
            if x + bmw > 0 then
              BitBlt(aDC, x, y, bmw, bmh, BMDC, 0, 0, SRCCopy);
            x := x + bmw;
          end;
        y := y + bmh;
      end;
    end;
end;

procedure THideScrollbarScrollbox.PaintWindow(DC: HDC);
begin
  if fBackBmp <> nil then
  begin
    TileBitmap(fBackBmp, DC, fBackBmp.Width, fBackBmp.Height,
      clientwidth, clientheight, 0, -VertPosition);
  end
  else
    inherited;
end;

procedure THideScrollbarScrollbox.scrollinview(AControl: TControl);
var
  Rect: TRect;
begin
  if VertScrollbar.Visible or not fHideVertScrollbar then
    inherited scrollinview(AControl)
  else
  begin
    if AControl = nil then
      exit;
    Rect := AControl.ClientRect;
    dec(Rect.Left, HorzScrollbar.margin);
    inc(Rect.Right, HorzScrollbar.margin);
    dec(Rect.Top, VertScrollbar.margin);
    inc(Rect.Bottom, VertScrollbar.margin);
    Rect.TopLeft := screentoclient(AControl.ClienttoScreen(Rect.TopLeft));
    Rect.BottomRight := screentoclient(AControl.ClienttoScreen(Rect.BottomRight));
    if Rect.Top < 0 then
      VertPosition := VertPosition + Rect.Top
    else if Rect.Bottom > clientheight then
    begin
      if Rect.Bottom - Rect.Top > clientheight then
        Rect.Bottom := Rect.Top + clientheight;
      VertPosition := VertPosition + Rect.Bottom - clientheight;
    end;
  end;
end;

procedure THideScrollbarScrollbox.SetHideHorzScrollbar(const Value: Boolean);
begin
  if Value <> fHideHorzScrollbar then
  begin
    fHideHorzScrollbar := Value;
    if Value then
    begin
      OldHorzVisible := HorzScrollbar.Visible;
      HorzScrollbar.Visible := False;
    end
    else
      HorzScrollbar.Visible := OldHorzVisible;
    HorzRange := HorzRange;
    HorzPosition := HorzPosition;
  end;
end;

procedure THideScrollbarScrollbox.SetHideVertScrollbar(const Value: Boolean);
begin
  if Value <> fHideVertScrollbar then
  begin
    fHideVertScrollbar := Value;
    if Value then
    begin
      OldVisible := VertScrollbar.Visible;
      VertScrollbar.Visible := False;
    end
    else
      VertScrollbar.Visible := OldVisible;
    VertRange := VertRange;
    VertPosition := VertPosition;
  end;
end;

procedure THideScrollbarScrollbox.SetHorzPosition(const Value: Integer);
var
  Oldposition: Integer;
begin
  Oldposition := HorzPosition;
  fHorzPosition := Value;
  if fHorzPosition > HorzRange - clientwidth then
    fHorzPosition := HorzRange - clientwidth;
  if fHorzPosition < 0 then
    fHorzPosition := 0;
  if fHorzPosition = Oldposition then
    exit;
  if HorzScrollbar.Visible or not fHideHorzScrollbar then
    HorzScrollbar.position := Value
  else
    Scrollby(Oldposition - fHorzPosition, 0);
end;

procedure THideScrollbarScrollbox.SetHorzRange(const Value: Integer);
begin
  fHorzRange := Value;
  if HorzScrollbar.Visible or not fHideHorzScrollbar then
    HorzScrollbar.Range := Value;
end;

procedure THideScrollbarScrollbox.SetVertPosition(const Value: Integer);
var
  Oldposition: Integer;
begin
  Oldposition := VertPosition;
  fVertPosition := Value;
  if fVertPosition > VertRange - clientheight then
    fVertPosition := VertRange - clientheight;
  if fVertPosition < 0 then
    fVertPosition := 0;
  if fVertPosition = Oldposition then
    exit;
  if VertScrollbar.Visible or not fHideVertScrollbar then
    VertScrollbar.position := Value
  else
    Scrollby(0, Oldposition - fVertPosition);
end;

procedure THideScrollbarScrollbox.SetVertRange(const Value: Integer);
begin
  fVertRange := Value;
  if VertScrollbar.Visible or not fHideVertScrollbar then
    VertScrollbar.Range := Value;
end;

procedure THideScrollbarScrollbox.WMPaint(var msg: TWMPaint);
begin
  ControlState := ControlState + [csCustomPaint];
  inherited;
  ControlState := ControlState - [csCustomPaint];
end;

2009. augusztus 16., vasárnap

Good Thursday and Easter Date function


Problem/Question/Abstract:

Here is another function that calculates the Good Thursday and any other related date for eny year. The algorithm provided here is straightforward in the sense that it calculates the Good Thursday (which is the jewish passover) as the thursday ocurring in the same week as the first spring full moon. Obviously the function can be easily adapted to calculate any full moon down to the second.

Answer:

function good_thursday(year: integer): tdatetime;
const
  full_moon: tdatetime = 34804.33889; {15/4/95 8:08}
  sunday: tdatetime = 1;
  sinodic_month: tdatetime = 29.53058912;

var
  equinoccio: tdatetime;
  lunar_months: double;
  full_moon, weeks: double;

begin
  if year < 100 then
    if year year := year + 2000
  else
    year := year + 1900;
  equinoccio := encodedate(year, 3, 21);
  lunar_months := 10000 - Int(10000 - (equinoccio - full_moon) / sinodic_month);
  full_moon := full_moon + sinodic_month * lunar_months;
  weeks := 10000 - Int(10000 - (full_moon - sunday) / 7);
  good_thursday := sunday + 7 * weeks - 3;
end;

2009. augusztus 15., szombat

Revert to Win 3.1 form resizing behaviour


Problem/Question/Abstract:

Has anyone found a way to prevent the Paint method from firing when you're in the middle of resizing a form? In other words, is there some way to ghost the change until the user actually releases the mouse button, instead of redrawing the form constantly during the resize?

Answer:

You can revert to the way a window was resized in Win 3.1 - with a sizing frame and a redraw only when the user let go of the mouse.

In your forms declaration you place this:

private
        {Private declarations}
        FDragFullWindowState: LongBool;
        procedure WMEnterSizeMove(var msg: TMessage); message WM_ENTERSIZEMOVE;
        procedure WMExitSizeMove(var msg: TMessage); message WM_EXITSIZEMOVE;

The implementation is like this:

procedure TProdBuilderMainForm.WMEnterSizeMove(var msg: TMessage);
begin
  SystemParametersInfo(SPI_GETDRAGFULLWINDOWS, 0, @FDragFullWindowState, 0);
  if FDragFullWindowState then
    SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, Ord(False), nil, 0);
end;

procedure TProdBuilderMainForm.WMExitSizeMove(var msg: TMessage);
begin
  if FDragFullWindowState then
    SystemParametersInfo(SPI_SETDRAGFULLWINDOWS, Ord(True), nil, 0);
end;

2009. augusztus 14., péntek

Modify the idapi.cfg settings through code (2)


Problem/Question/Abstract:

How can my program access the idapi.cfg file and probably change its INIT (Local Share etc.) section?

Answer:

For 32bit only. You can of course use the registry to determine the default CFG File instead of passing it as a parameter here:

procedure ModifyCFG(const ACFGFile, AValue, AEntry, ACFGPath: string; SaveAsWin31:
  bool);
var
  hCfg: hDBICfg;
  pRecBuf, pTmpRec: pByte;
  pFields: pFLDDesc;
  Count: word;
  i: integer;
  Save: boolean;
  Reg: TRegistry;
const
  RegSaveWIN31: array[bool] of string = ('WIN32', 'WIN31');
begin
  hCfg := nil;
  pFields := nil;
  pRecBuf := nil;
  Save := False;
  Check(DbiOpenConfigFile(PChar(ACFGFile), False, hCfg));
  try
    Check(DbiCfgPosition(hCfg, PChar(ACfgPath))); {neccessary...?}
    Check(DbiCfgGetRecord(hCfg, PChar(ACfgPath), Count, nil, nil));
    pRecBuf := AllocMem(succ(Count) * 128); {128 additional safety...}
    pFields := AllocMem(Count * sizeof(FLDDesc));
    Check(DbiCfgGetRecord(hCfg, PChar(ACfgPath), Count, pFields, pRecBuf));
    for i := 1 to Count do
    begin
      if StrPas(pFields^.szName) = AEntry then
      begin
        pTmpRec := pRecBuf;
        Inc(pTmpRec, 128 * (i - 1));
        StrPCopy(PChar(pTmpRec), AValue);
      end;
      inc(pFields);
    end;
    dec(pFields, Count);
    Check(DbiCfgModifyRecord(hCfg, PChar(ACfgPath), Count, pFields, pRecBuf));
    Save := True;
  finally
    if hCfg <> nil then
      Check(DbiCloseConfigFile(hCfg, Save, True, SaveAsWin31));
    if pRecBuf <> nil then
      FreeMem(pRecBuf, succ(Count) * 128);
    if pFields <> nil then
      FreeMem(pFields, Count * sizeof(FLDDesc));
  end;
  {update registry SAVECONFIG value}
  Reg := TRegistry.Create;
  try
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    if not Reg.OpenKey('SOFTWARE\Borland\Database Engine', False) then
      ShowMessage('Configuration Path not found')
    else
    begin
      Reg.LazyWrite := False;
      Reg.WriteString('SAVECONFIG', RegSaveWIN31[SaveAsWin31]);
      Reg.CloseKey;
    end;
  finally
    Reg.Free;
  end;
  {DbiExit/Init to re-read cfg... make absolutely sure there are no active
        DB components when doing this (it's is best done by a loader app)}
  Session.Close;
  Session.Open;
end;

ACFGPath would be '\SYSTEM\INIT\', AEntry would be 'LOCAL SHARE' und AValue would be 'TRUE' or 'FALSE'.

2009. augusztus 13., csütörtök

Accept dropped files from the explorer


Problem/Question/Abstract:

This way you can drag and drop files to a specific control in a Delphi form

Answer:

Just create a project and add a ListBox component to Form1.

1. First, a procedure to handle the message but without handling it.

interface

procedure WMDROPFILES(var Msg: TMessage);

implementation

procedure TForm1.WMDROPFILES(var Msg: TMessage);
var
  pcFileName: PChar;
  i, iSize, iFileCount: integer;
begin
  pcFileName := ''; // to avoid compiler warning message
  iFileCount := DragQueryFile(Msg.WParam, $FFFFFFFF, pcFileName, 255);
  for i := 0 to iFileCount - 1 do
  begin
    iSize := DragQueryFile(Msg.wParam, 0, nil, 0) + 1;
    pcFileName := StrAlloc(iSize);
    DragQueryFile(Msg.WParam, i, pcFileName, iSize);
    if FileExists(pcFileName) then
      AddFile(pcFileName); // method to add each file
    StrDispose(pcFileName);
  end;
  DragFinish(Msg.WParam);
end;

2. Second, a WindowProc method to replace ListBox1 WindowProc default method and a variable to store ListBox1 WindowProc default method.

interface

procedure LBWindowProc(var Message: TMessage);

implementation

var
  OldLBWindowProc: TWndMethod;

procedure TForm1.LBWindowProc(var Message: TMessage);
begin
  if Message.Msg = WM_DROPFILES then
    WMDROPFILES(Message); // handle WM_DROPFILES message
  OldLBWindowProc(Message);
    // call default ListBox1 WindowProc method to handle all other messages
end;

3. In Form1 OnCreate event, initialize all.

procedure TForm1.FormCreate(Sender: TObject);
begin
  OldLBWindowProc := ListBox1.WindowProc; // store defualt WindowProc
  ListBox1.WindowProc := LBWindowProc; // replace default WindowProc
  DragAcceptFiles(ListBox1.Handle, True); // now ListBox1 accept dropped files
end;

4. In Form1 OnDestroy event, uninitialize all. Not necesary but a good practice.

procedure TForm1.FormDestroy(Sender: TObject);
begin
  ListBox1.WindowProc := OldLBWindowProc;
  DragAcceptFiles(ListBox1.Handle, False);
end;

5. To complete source code, the AddFile method.

interface

procedure AddFile(sFileName: string);

implementation

procedure TForm1.AddFile(sFileName: string);
begin
  ListBox1.Items.Add(sFilename);
end;

6. Do not forget to add ShellAPI unit to the uses clause.


Component Download: DroppedFiles.zip

2009. augusztus 12., szerda

How to get the Word version on a client PC


Problem/Question/Abstract:

Using D6 I need to extract the Word version on the client computer. The options are Word97, Word2000 and WordXP.

Answer:

function GetCurrentWordMajorVersion: Integer;
var
  vRegistry: TRegistry;
  vVersionStr: string;
  vVersion: string;
begin
  Result := -1;
  vRegistry := TRegistry.Create(KEY_READ);
  try
    vRegistry.RootKey := HKEY_CLASSES_ROOT;
    if vRegistry.OpenKeyReadOnly('Word.Application\CurVer') then
    begin
      {Get the default value: 'Word.Application.10'}
      vVersionStr := vRegistry.ReadString('');
      {Extract the major version from the string}
      vVersion := System.Copy(vVersionStr, Succ(LastDelimiter('.', vVersionStr)),
        MAXINT);
      {8=Word97, 9=Word2000, 10=Word2002, etc.}
      Result := StrToIntDef(vVersion, -1);
    end;
  finally
    vRegistry.Free;
  end;
end;

2009. augusztus 11., kedd

Binary search on an alphasorted TListView


Problem/Question/Abstract:

How to do a binary search on an alphasorted TListView

Answer:

If you want to use a fast searching algorithm (binary search for example) the listview has to be sorted on the column you do the duplicate check on. If it is not sorted you have to use the listviews FindCaption or FindData method, which does a linear search. To sort a listview use its AlphaSort or CustomSort methods.

A binary search on a alphasorted listview for a caption would be something like this (untested!):

{
Function ListviewBinarySearch

Parameters:

listview:
listview to search, assumed to be sorted, must be <> nil.

Item:
item caption to search for, cannot be empty

index:
returns the index of the found item, or the index where the item should be inserted if it is not already in the list. Returns True if there is an item with the passed caption in the list, false otherwise.

Description:
Uses a binary search and assumes that the listview is sorted ascending on the caption of the listitems. The search is case-sensitive, like the default alpha-sort routine used by the TListview class.

Note:
We use the lstrcmp function for string comparison since it is the function used by the default alpha sort routine. If the listview is sorted by another means (e.g. OnCompare event) this needs to be changed, the comparison method used must always be the same one used to sort the listview, or the search will not work!

Error Conditions: none
Created: 31.10.99 by P. Below
}

function ListviewBinarySearch(listview: TListview; const Item: string; var index:
  Integer): Boolean;
var
  first, last, pivot, res: Integer;
begin
  Assert(Assigned(listview));
  Assert(Length(item) > 0);
  Result := false;
  index := 0;
  if listview.items.count = 0 then
    Exit;
  first := 0;
  last := listview.items.count - 1;
  repeat
    pivot := (first + last) div 2;
    res := lstrcmp(PChar(item), Pchar(listview.items[pivot].caption));
    if res = 0 then
    begin
      { Found the item, return its index and exit. }
      index := pivot;
      result := true;
      Break;
    end
    else if res > 0 then
    begin
      { Item is larger than item at pivot }
      first := pivot + 1;
    end
    else
    begin
      { Item is smaller than item at pivot }
      last := pivot - 1;
    end;
  until
    last < first;
  index := first;
end;

2009. augusztus 10., hétfő

Access TSpeedButtons in a TGroupBox


Problem/Question/Abstract:

I need to put a lot of TSpeedButtons into a GroupBox (for example 20). For each button I set a GroupIndex. Is it possible to control what button was pressed without writing a SpeedButtonClick procedure for each button?

Answer:

The OnClick method passes the Sender in as a TObject. You can hook all the buttons up to the same OnClick methods and check to see which button was clicked something like this:

procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
  if Sender is TSpeedButton then
    with Sender as TSpeedButton do
    begin
      case GroupIndex of
        1: ;
        2: ;
        3: ;
      end;
    end;
end;

This is assuming the GroupIndex of each button is unique.

2009. augusztus 9., vasárnap

Get the name of a keyboard key


Problem/Question/Abstract:

Does anyone know how to get the name of a key from the keyboard? I want a method that sends the ASCII code and returns the correct name in string format.

Answer:

Start with calling VkKeyScan, then proceed to GetKeynameText, via MapVirtualKey.

function GetKeyname(ch: Char): string;
var
  scan: Word;
  virtual_keycode: Byte;
  keyname: array[0..128] of Char;
  lparam: Integer;
begin
  scan := VkKeyScan(ch);
  Result := '';
  if scan <> $FFFF then
  begin
    if (Scan and $100) <> 0 then
      Result := Result + '[Shift]';
    if (Scan and $200) <> 0 then
      Result := Result + '[Ctrl]';
    if (Scan and $400) <> 0 then
      Result := Result + '[Alt]';
    virtual_keycode := Lobyte(scan);
    lparam := MapVirtualKey(virtual_keycode, 0) shl 16;
    if lparam <> 0 then
      if GetKeyNametext(lparam, keyname, sizeof(keyname)) > 0 then
        Result := Result + '[' + keyname + ']';
  end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  memo1.text := GetKeyname(alignededit1.text[1]);
end;

2009. augusztus 8., szombat

How to close another application


Problem/Question/Abstract:

How to close another application

Answer:

library KillGUI;

uses
  Windows, Messages;

function PostQM(nCode: Integer; wParam: WParam; lparam: LParam): Lparam; stdcall;
begin
  PostQuitMessage(1);
  Result := 0;
end;

function ExitRP(nCode: Integer; wParam: WParam; lparam: LParam): Lparam; stdcall;
begin
  ExitProcess(1);
  Result := 0;
end;

procedure PostQuit(AHandle: THandle; Level: DWord); stdcall;
var
  tid: DWord;
  pid: DWord;
  hProcess: THandle;
begin
  tid := GetWindowThreadProcessId(AHandle, @pid);
  case Level of
    0:
      PostMessage(AHandle, WM_CLOSE, 0, 0);
    1:
      SetWindowsHookEx(WH_GETMESSAGE, PostQM, Hinstance, tid);
    2:
      SetWindowsHookEx(WH_GETMESSAGE, ExitRP, Hinstance, tid);
    3:
      begin
        hProcess := OpenProcess(PROCESS_TERMINATE, False, pid);
        TerminateProcess(hProcess, 1);
      end;
  end;
  PostThreadMessage(tid, 0, 0, 0);
end;

exports
  PostQuit name 'PostQuit';

begin
end.

2009. augusztus 7., péntek

Exception handling in threads


Problem/Question/Abstract:

Exception handling in threads

Answer:

One of my applications that ran fine on my system, particularly when run in the Delphi IDE, started to stop executing after displaying 'Application error has occurred'.
What was the reason?

I had moved I/O-depending parts of that application into separate threads in order to increase performance and improve responsiveness of the application to user input. It was a typical scenario, ideal for multi-threading.

It turned out that those threads threw an unexpected exception. I did have a global exception handler as a method of my main form/ property of TApplication, but that did not catch the exceptions raised by threads other than the main thread.

Therefore it is mandatory to do at least some basic exception handling on your threads. The simplest solution is to put a try-except-end block in the Execute method and silently eat all exceptions.

If you want to display the exception remember that the VCL itself is not thread-safe. You can make it threadsafe for the execution time of a method by calling this method with the Synchronize() function. The downside is that you cannot pass arguments to a synchronized function. You have to pass arguments via member variables of your thread class.


// This is the cheapest way to handle exceptions
// in threads. if you want to display the exception
// then you need to do this with a separate method,
// which has to be called synchronized..

procedure TSortThread.Execute;
begin
  try
    // do the sorting
  except
    // silently eat all exceptions
  end;
end;

2009. augusztus 6., csütörtök

COM Activation


Problem/Question/Abstract:

Did you ever wonder how COM obejcts are created? Did you know you are not the one that creates them? Read more about COM activation right here

Answer:

Introduction

COM has always been presented as something complex to digest and tedious to administer. Nothing can be further from the truth. What you need is, as with everything else, to understand a few principles. After that everything will start to make sense and you will be able to find your way in the land of COM. You may ask why COM and SOAP? Well, the answer is simple: because we can.

Windows comes equipped with an incredible set of tools and services that make distributed development a breeze. This is especially true if you are a Delphi developer because the way it wraps COM is so elegant that there's virtually no difference in writing a regular Delphi class and a COM object. Obviously the devil is in the details and that is what scares most of the people.

In this article I will explain a basic principle: what happens when you try to instantiate a COM object first. Then I will explain why this mechanism (pattern) is so useful. If you get lost check the "To recap" section below. It might help.

Registration and class factories

After you build a COM object like I explained in my Introduction to COM article you need to register it in order to make it available for your client applications. There are different ways to register a COM object:

You can use RegSvr32.exe which is under Windows\System32

You can install your COM object in a COM+ application (which was called Microsoft Transaction Server (MTS) Package under NT4)

You can click on the button Register in the Delphi type library editor from the IDE

Option 2 is the best way for a variety of reasons I will explain later. But why do you need to register a COM object? Why cannot you just use it?

Well, in order to use an object you need to create it. That is the problem. You are not the one that creates the COM object. Windows does it for you and it does that using a class factory that you provide with your COM object.

Take a look at the DSOAPNTier sample application and open the unit uOrderManager_Impl.pas. Go to the end to the initialization section. This is what you will find:

initialization
  TAutoObjectFactory.Create(ComServer, TOrderManager, Class_OrderManager,
    ciMultiInstance, tmApartment);
end.

This code runs as soon as the COM DLL is loaded and creates a class factory. Windows uses it to create the COM object.

Structure of a COM DLL

Class factory... Ok, the initialization section creates it. Windows then uses it... But wait a moment. Who gives Windows the reference to the class factory? Looking at the code, it only looks like a class that is created and potentially never destroyed. Is that a memory leak?

Obviously not. The answer is in the project file (.dpr). If you open it this is what you will see:

library DSOAPNTierLib;

uses
  ComServ, [..];
exports
  DllGetClassObject,
  DllCanUnloadNow,
  DllRegisterServer,
  DllUnregisterServer;

{$R *.TLB}

{$R *.RES}

begin
end.

Those 4 exported functions are the key to our class factory problem. In order for a regular DLL to be a COM DLL, it has to export those functions. If you open the unit ComServ.pas you will be able to see their formal declaration and the implementation that Delphi automatically provides for you.

Those functions are declared as:

function DllGetClassObject(const CLSID, IID: TGUID; var Obj): HResult; stdcall;
function DllCanUnloadNow: HResult; stdcall;
function DllRegisterServer: HResult; stdcall;
function DllUnregisterServer: HResult; stdcall;

I hope everything begins to make sense.

DllGetClassObject: it is the function that Windows calls to get the pointer to the class factory object (the one that gets automatically created in the initialization section). The first parameter indicate whose object's class factory it wants returned by using the class factory GUID. The second is a parameter that indicates what interface Windows wants to use to communicate with the class factory. This is almost always IID_IClassFactory or IID_IClassFactory2 which are system interfaces. The third is the pointer to the returned class factory.

After Windows calls this function it will have a pointer to the class factory. That object implements IClassFactory

IClassFactory = interface(IUnknown)
  ['{00000001-0000-0000-C000-000000000046}']
  function CreateInstance(const unkOuter: IUnknown; const iid: TIID; out obj): HResult;
    stdcall;
  function LockServer(fLock: BOOL): HResult; stdcall;
end;

There we go! Trought the method CreateInstance finally, after all this work, Windows is able to create the object!

DllCanUnloadNow: is called by Windows to indicate whether the server can be unloaded from memory because it is no longer in use.

DllRegisterServer, DLLUnregisterServer: they register and unregister the DLL by storing a bunch of informations in the Windows registry as we will see soon.

Registry

Ok, now we know how to get a pointer to the object that creates the object. We know how to tell it to create the object (IClassFactory) but there's another thing that is not clear. Who told Windows to load that DLL instead of another one? The answer is the registry.

When you call RegSrv32.exe (see above, how to register a COM DLL) you specify a file name (the DLLs). Regsrv32 first checks that those 4 functions exist in the DLL. If it is so then calls either DllRegisterServer or DllUnRegisterServer (depending on what you told it to do). That is all RegSvr32.exe does. It really doesn't care of what those functions do until they return 0.

When you create a COM DLL using Delphi, the VCL provides a default implementation for those 4 functions. Those simply store the GUIDs of the class factory, the GUIDs of the COM objects and associate the DLL name to them. This is really all that happens.

Take a look at the following screenshot:



Well, there's another small detail. The ProgID. That is just another redirection. A ProgID is the friendly name (a string) for your COM object such as MyLibrary.MyBusinessObject

You can create a COM object by using it's a GUID or a ProgID. When you use the ProgID, Windows will see to which GUID it is associated and the use that to retrieve all the rest of the information (DLL name). There's another section in the registry that starting from the ProgID will let you find the node I've just shown.

To recap

I hope that I succeeded in making it clear. There are many other details that should be mentioned, but this is meant to be an introduction, not a book on the subject. Here's again what happens in a step by step mode:

Your client application wants to create a COM object so does something like MyObject := CoMyObject.Create

The CoMyObject class (CoClass) tells Windows to return it a pointer to the class factory for the object

Windows scans into the registry and finds the name of the right DLL/EXE

Windows loads it and calls DllGetClassObject

The class factory returned by that call is finally used to create the object

Why is this so useful?

Well, for a variety of reasons.

By not being the one that directly creates the object, COM allows you to cross the boundaries of your local PC. A COM object doesn't necessarily live on the same pc of your client application but could be hosted by a remote server. This is what DCOM (Distributed COM) allows: remote creation and method invokation.

The other useful thing that comes out from this is COM+/MTS and object pooling/just in time activation

COM+ and just in time activation

When you use COM+ or MTS to register your COM object (which means, you create a COM+ application and you drop your DLL in it), you get some benefits in terms of scalability and performance that aren't available with regular in-process creation.

Take a look at the following screen-shot:



I created the "DSOAP Samples" COM+ application and I installed the Login and OrderManager objects into it.

So, what do I need to do in my client to create them now? Nothing. You would create them exactly the way you did before. Same code, no need to recompile or change anything. A few things will change anyways: scalability, creation time and execution speed (depending on what you do) will improve significantly.

The reasons for this lie in the just in time activation on one side, database connection pooling on the other. Actually let's also also add object pooling.

Just in time activation is that process trough which an object is put "asleep" until it is actually used. You create your object and let's say you keep it active without calling any method for 2 minutes. Wouldn't it be nice if something would just free it and call it only after 2 minutes? Well, this is what COM+ does for you transparently. This improves scalability of the server a lot. Instead of keeping memory utilized for nothing, COM+ is able to free the objects and whenever you will call them again it will recreate them for you and restore their state. All that is automatic.

The OrderManager class uses ADO inside to query the database. Before plugging the object in COM+, if we would have created, used and destroyed the object a hundred times, it would have taken let's say 100 seconds. This is because each time it would have had to reconnect to the database. By plugging OrdersManager in COM+ we now benefit from ADO connection pooling. COM+ will keep a list of active connections to the database for you and whenever a call is made, unless necessary, it will resuse one of the existing ones.

Finally (as you can see from the "Pooled" column on the right), COM+ objects can be pooled. There are a few things you need to do to make this happen but I just wanted to give you the idea that that is possible as well. The client wouldn't know the difference.

Conclusion

This is by no means an article that explains everything. There's far more that should be said but I think that I provided you with enough understanding on the subject to start digging into the subject by yourself.

Happy coding!

2009. augusztus 5., szerda

Sort TListView columns by date or time


Problem/Question/Abstract:

Is there any way to sort columns in a TListView by date or time when a user clicks on the header of the column?

Answer:

Solve 1:

LV1 is a TListView with vsReport.

function CustomDateSortProc(Item1, Item2: TListItem; ParamSort: integer): integer;
  stdcall;
begin
  result := 0;
  if StrToDateTime(item1.SubItems[0]) > StrToDateTime(item2.SubItems[0]) then
    Result := 1
  else if StrToDateTime(item1.SubItems[0]) < StrToDateTime(item2.SubItems[0]) then
    Result := -1;
end;

function CustomNameSortProc(Item1, Item2: TListItem; ParamSort: integer): integer;
  stdcall;
begin
  Result := CompareText(Item1.Caption, Item2.Caption);
end;

procedure TForm1.GetFilesClick(Sender: TObject);
var
  sr: TSearchRec;
  Item: TListItem;
begin
  if FindFirst('e:\*.*', faAnyFile, sr) = 0 then
    repeat
      if (sr.Attr and faDirectory) <> sr.Attr then
      begin
        item := LV1.items.add;
        item.Caption := sr.name;
        Item.SubItems.Add(DateTimeToStr(filedatetodatetime(sr.time)));
      end;
    until
      FindNext(sr) <> 0;
  FindClose(sr);
end;

procedure TForm1.LV1ColumnClick(Sender: TObject; Column: TListColumn);
begin
  if column = LV1.columns[0] then
    LV1.CustomSort(@CustomNameSortProc, 0)
  else
    LV1.CustomSort(@CustomDateSortProc, 0)
end;


Solve 2:

Open a new Delphi application project. Drop a listview (ListView1) onto the default form. Paste in the attached code. Hook up the FormCreate and ListView1ColumnClick event handlers.

The custom sort procedure (and the callback) save the day. There are some limits and drawbacks to this approach though. Since the listview is inherently unaware of data types, you have to bolt that onto the outside. This extra thrashing can represent a performance hit if you're doing something funky in the callback. This example uses up the TListView.Tag, TListColumn.Tag and TListItem.Data properties. This might clash with a scheme in place, or may sicken you because of its bold-faced greed. This system only allows for single-column sorts. This can easily be extended, though, by a reinterpretation of TListView.Tag into sort column_s_. No graphics in the column headers.

unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    ListView1: TListView;
    procedure FormCreate(Sender: TObject);
    procedure ListView1ColumnClick(Sender: TObject; Column: TListColumn);
  private
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

function UnformatText(const Text: string; const VarType: Integer): Variant;
begin
  {This is an ambitious function, in simple form. The standard text to type
        variable conversion is fairly weak, so this function is a good place to
        canonize that thinking.}
  if Length(Text) = 0 then
    Result := Null
  else
  begin
    case VarType of
      varBoolean:
        if CompareText(Text, 'True') = 0 then
          Result := True
        else if CompareText(Text, 'False') = 0 then
          Result := False
        else if CompareText(Text, 'Yes') = 0 then
          Result := True
        else if CompareText(Text, 'No') = 0 then
          Result := False
        else
        begin
          Result := Null;
        end;
    else
      {use the default handler}
      Result := VarAsType(Text, VarType);
    end;
  end;
end;

function LVItemValue(const Item: TListItem; const Col, VarType: Integer): Variant;
begin
  {get the indicated "cell's" text, return an empty string if either index is out of range}
  if Item = nil then
    Result := Null
  else if Col < 0 then
    Result := Null
  else if Col > Item.SubItems.Count then
    Result := Null
  else if Col = 0 then
    Result := UnformatText(Item.Caption, VarType)
  else
  begin
    Result := UnformatText(Item.SubItems[Col - 1], VarType);
  end;
end;

function LVSort(lParam1, lParam2: Integer; lParamSort: Integer): Integer; stdcall;
const
  NULL_COMPARE = -1; {-1 floats nulls to top, +1, to bottom}
var
  oLV: TListView;
  iSortCol: Integer;
  bSortAsc: Boolean;
  iSortVarType: Integer;
  vData1: Variant;
  vData2: Variant;
begin
  try
    {resolve the reference to the listview being sorted}
    oLV := TListView(lParamSort);
    {is "no sort" being requested?}
    if oLV.Tag = 0 then
    begin
      {not a very economic use of the data property...}
      Result := Integer(TListItem(lParam1).Data) - Integer(TListItem(lParam2).Data);
      exit;
    end;
    iSortCol := Abs(oLV.Tag) - 1;
    bSortAsc := oLV.Tag >= 0;
    {determine the data type}
    if iSortCol < 0 then
      iSortVarType := varString
    else if iSortCol >= oLV.Columns.Count then
      iSortVarType := varString
    else
    begin
      iSortVarType := oLV.Columns[iSortCol].Tag;
    end;
    {get the data of interest}
    vData1 := LVItemValue(TListItem(lParam1), iSortCol, iSortVarType);
    vData2 := LVItemValue(TListItem(lParam2), iSortCol, iSortVarType);
    {do some "null" handling that supercedes typed comparisons}
    if VarIsNull(vData1) and VarIsNull(vData2) then
      Result := 0 {they're both null}
    else if VarIsNull(vData1) then
      Result := NULL_COMPARE
    else if VarIsNull(vData2) then
      Result := -NULL_COMPARE
    else if vData1 > vData2 then
      Result := 1
    else if vData1 < vData2 then
      Result := -1
    else
    begin
      Result := 0;
    end;
    if not bSortAsc then
      Result := -Result;
  except
    Result := 0;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);

  function RandomNull(const Text: string): string;
  begin
    if Random(8) < 1 then
      Result := ''
    else
    begin
      Result := Text;
    end;
  end;

var
  oCol: TListColumn;
  oItem: TListItem;
  iItem: Integer;
begin
  Randomize;
  {set listview properties}
  with ListView1 do
  begin
    Items.Clear;
    Columns.Clear;
    Align := alClient;
    ReadOnly := True;
    SortType := stNone;
    Tag := 0;
    ViewStyle := vsReport;
  end;
  {default columns of different types}
  oCol := ListView1.Columns.Add;
  oCol.Caption := 'varDate';
  oCol.Tag := varDate;
  oCol.Width := 100;
  oCol := ListView1.Columns.Add;
  oCol.Caption := 'varBoolean';
  oCol.Tag := varBoolean;
  oCol.Width := 100;
  oCol := ListView1.Columns.Add;
  oCol.Caption := 'varInteger';
  oCol.Tag := varInteger;
  oCol.Width := 100;
  oCol := ListView1.Columns.Add;
  oCol.Caption := 'varCurrency';
  oCol.Tag := varCurrency;
  oCol.Width := 100;
  oCol := ListView1.Columns.Add;
  oCol.Caption := 'varString';
  oCol.Tag := varString;
  oCol.Width := 100;
  {add items to the listview}
  for iItem := 0 to 100 + Random(100) do
  begin
    {data property stores "original index" info}
    oItem := ListView1.Items.Add;
    oItem.Data := Pointer(iItem); {using this more like a Tag property}
    {plug in some fake data}
    oItem.Caption := RandomNull(FormatDateTime('dd-mmm-yyyy', Now() - Random(1000)));
    if Random(2) < 1 then
      oItem.SubItems.Add(RandomNull('Yes'))
    else
    begin
      oItem.SubItems.Add(RandomNull('No'));
    end;
    oItem.SubItems.Add(RandomNull(FloatToStr(0.01 * Random(100000))));
    oItem.SubItems.Add(RandomNull(IntToStr(Random(10000))));
    oItem.SubItems.Add(RandomNull(Char(65 + Random(26)) + Char(65 + Random(26)) +
      Char(65 + Random(26)) + Char(65 + Random(26)) + Char(65 + Random(26))));
  end;
end;

procedure TForm1.ListView1ColumnClick(Sender: TObject; Column: TListColumn);
begin
  {sort the sort column and order into the listview's tag}
  if ListView1.Tag = Column.Index + 1 then
    ListView1.Tag := -ListView1.Tag {desc sort}
  else if ListView1.Tag = -(Column.Index + 1) then
    ListView1.Tag := 0 {no sort}
  else
  begin
    ListView1.Tag := Column.Index + 1; {asc sort}
  end;
  {pass the listview such that it will be sent to the sort procedure}
  ListView1.CustomSort(LVSort, Integer(ListView1));
end;

end.

2009. augusztus 4., kedd

How does the API call to make a dialup network connection look like?


Problem/Question/Abstract:

How does the API call to make a dialup network connection look like?

Answer:

The following code creates an internet connection through dialup networking:

uses
  WinInet;

procedure TForm1.Button1Click(Sender: TObject);
begin
  if not InternetAutodial(INTERNET_AUTODIAL_FORCE_ONLINE, Application.Handle) then
    MessageDlg('No internet connection', mtError, [mbOk], 0);
end;

2009. augusztus 3., hétfő

How to determine the size of the Windows taskbar


Problem/Question/Abstract:

How to determine the size of the Windows taskbar

Answer:

Solve 1:

You need to get the Rect in the rc member of the APPBARData structure and subtract the top from the bottom.

procedure TForm1.Button1Click(Sender: TObject);
var
  appbardata: TAppBarData;
  Rect: TRect;
  taskBarHeight: Integer;
begin
  AppBarData.cbSize := 0;
  AppBarData.hWnd := 0;
  AppBarData.rc.Left := 0;
  AppBarData.rc.Top := 0;
  AppBarData.rc.Bottom := 0;
  AppBarData.rc.Right := 0;
  SHAppBarMessage(ABM_GETTASKBARPOS, appbardata);
  Rect := appbardata.rc;
  taskBarHeight := Rect.Bottom - Rect.Top;
  ShowMessage(IntToStr(taskBarHeight));
end;


Solve 2:

function GetTaskBarSize: TRect;
var
  wnd: HWND;
begin
  wnd := FindWindow('Shell_TrayWnd', nil);
  if wnd > 0 then
    GetWindowRect(wnd, Result)
  else
    Result := Rect(0, 0, 0, 0);
end;


Solve 3:

This is one way to get the height of the taskbar:

function GetTaskBarRect: TRect;
var
  TBData: TAppBarData;
begin
  TBData.cbSize := sizeof(TAppBarData);
  SHAppBarMessage(ABM_GETTASKBARPOS, TBData);
  Result := TBData.rc;
end;

2009. augusztus 2., vasárnap

How to automatically delete records in a database after 30 days


Problem/Question/Abstract:

Can anyone suggest an elegant way of automatically deleting database records if they are more than say 30 days old?

Answer:

Use a TQuery. Replace Table1.Tablename with your Tablename and replace FieldDate with the Date Field.

var
  MyDate: TDateTime;
begin
  MyDate := Date - 30;
  Query1.Active := False;
  Query1.SQL.Clear;
  Query1.SQL.Add('DELETE FROM "' + Table1.TableName + '"');
  Query1.SQL.Add('WHERE (FieldDate = "' + FormatDateTime('mm/dd/yyyy', MyDate) + '")');
  Query1.Active := True;
end;

2009. augusztus 1., szombat

How to mirror text horizontally or vertically on a TPaintBox


Problem/Question/Abstract:

How to mirror text horizontally or vertically on a TPaintBox

Answer:

This is actually not quite straightforward. The best way to do that is to first paint the text onto an off-screen bitmap and then paint that bitmap on screen using some weird coordinate manipulations. Drop a TPaintbox on the screen and connect a method to its OnPaint handler. Change the handler to the code below to see how this works:

procedure TForm1.PaintBox1Paint(Sender: TObject);
const
  test = 'Hello world';
var
  bmp: TBitmap;
  cv: TCanvas;
  ext: TSize;
  r: TRect;
begin
  cv := (Sender as TPaintbox).canvas;
  ext := cv.TextExtent(test);
  bmp := TBitmap.Create;
  try
    bmp.Width := ext.cx;
    bmp.Height := ext.cy;
    bmp.Canvas.Brush := cv.Brush;
    bmp.Canvas.Font := cv.Font;
    bmp.Canvas.FillRect(bmp.canvas.cliprect);
    bmp.Canvas.TextOut(0, 0, test);
    {draw text in normal orientation}
    cv.Draw(0, 0, bmp);
    r := Rect(ext.cx, 0, 0, ext.cy);
    OffsetRect(r, 0, ext.cy);
    {draw text horizontally mirrored}
    cv.CopyRect(r, bmp.canvas, bmp.canvas.ClipRect);
    r := Rect(0, ext.cy, ext.cx, 0);
    OffsetRect(r, 0, 2 * ext.cy);
    {draw text vertically mirrored}
    cv.CopyRect(r, bmp.canvas, bmp.canvas.ClipRect);
  finally
    bmp.Free
  end;
end;

The key here is to set up the target rectangle for CopyRect with left and right or top and bottom switched. Be warned, there is a slight potential that this code will cause acute indigestion for some video drivers!