Athena

Frequently Asked Questions

Brian Long (www.blong.com)

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

This paper contains just a few of the many frequently asked questions (FAQs) that I encounter. Because I write a monthly problem solving column (called The Delphi Clinic) for The Delphi Magazine, some of these will already have appeared in print.

The conference slot for this paper was a last minute one, and the session is to be hosted both by me (Brian Long) and also Bob Swart (aka Dr. Bob). This paper was therefore put together at the last minute, and so does not contain any of the contributions that Bob will make during the session.

The fact that this is a shared session should also explain why the list of FAQs presented here is quite short.

With those caveats in mind, I still hope that the information presented here will be both contemporary and useful.

Click here to download the files associated with this paper.

How do I talk to Microsoft Word?

Microsoft Word is an Automation server, which is a particular type of COM server application. Automation servers can be controlled either by talking directly to the COM interface defined by the application, or by using a Variant to talk (implicitly) to the IDispatch interface supported by the application. COM, interfaces and Automation are all discussed in detail in dedicated sessions at DCon ’99.

To automation Word with a Variant variable (supported since Delphi 2), you initialise the Variant with a call to CreateOleObject (from the ComObj unit). Then, the Variant acts just like the object exposed by the Automation server. You can call methods and access properties, so long as you know what they are. When Word is installed, there is an optional VBA help file that can be installed as well, which describes all the functionality supported by the Word Automation object(s).

Automation through a Variant allows optional parameters to be omitted, where you are happy with their default values. So, if you are automating Microsoft Word, and you wish to save a document to disk you can call the SaveAs method of the document object, and ignore most of the eleven parameters that it takes, as shown in Listing 1.

Listing 1: Automation using a Variant

uses
  ComObj;

procedure TForm1.Button1Click(Sender: TObject);
var
  WordApplication, WordDocument: Variant;
begin
  WordApplication := CreateOleObject('Word.Application');
  WordDocument := WordApplication.Documents.Add;
  WordApplication.Selection.TypeText('Hello world');
  WordDocument.SaveAs(FileName := 'C:\Doc.Doc', AddToRecentFiles := False);
  WordApplication.Quit(False)
end;

When Automating through proper COM interfaces (supported since Delphi 3), you must first import the server’s type library (Project | Import Type Library…) and then add the generated unit to your uses clause. When using interfaces, Delphi does not allow you to miss out any optional arguments (which is unfortunate when there are many of them). However, Delphi 4 does provide an undocumented variable that you can pass in place of those you have no interest in. This EmptyParam variable is defined in the System unit and is used in this Listing 2.

Listing 2: Automation using a COM interface, and using EmptyParam

uses
  Word_TLB;

procedure TForm1.Button1Click(Sender: TObject);
var
  WordApplication: _Application;
  WordDocument: _Document;
  TmpVariant, TmpVariant2: OleVariant;
begin
  WordApplication := CoApplication.Create;
  WordDocument := WordApplication.Documents.Add(EmptyParam, EmptyParam);
  WordApplication.Selection.TypeText('Hello world');
  TmpVariant := 'C:\Doc.Doc';
  TmpVariant2 := False;
  WordDocument.SaveAs(TmpVariant, EmptyParam, EmptyParam,
    EmptyParam, TmpVariant2, EmptyParam, EmptyParam, 
    EmptyParam, EmptyParam, EmptyParam, EmptyParam);
  WordApplication.Quit(TmpVariant2, EmptyParam, EmptyParam);
end;

Delphi 5 helps a bit further. Firstly, it supports wrapping Automation servers into components, and pre-installs all the Microsoft Office 97 Automation server components onto the component palette. Secondly, it overloads the methods that take optional parameters and defines various versions with varying numbers of parameters. This means you can call the appropriate overloaded version and omit the last n parameters.

Listing 3 works after dropping a TWordApplication component on the form, with its ConnectKind property set to ckNewInstance, and then dropping a TWordDocument on the form with ConnectKind set to ckAttachToInterface.

Listing 3: Automation using Delphi 5's new Automation server components

procedure TForm1.Button1Click(Sender: TObject);
var
  TmpVariant, TmpVariant2: OleVariant;
begin
  WordDocument.ConnectTo(WordApplication.Documents.Add(EmptyParam, EmptyParam));
  WordApplication.Selection.TypeText('Hello world');
  TmpVariant := 'C:\Doc.Doc';
  TmpVariant2 := False;
  WordDocument.SaveAs(TmpVariant, EmptyParam,
    EmptyParam, EmptyParam, TmpVariant2);
  WordApplication.Quit(TmpVariant2);
end;

How do I launch an application?

Listing 4 a routine that launches a program, optionally with command-line parameters. The Win32 API CreateProcess is used for the job.

Listing 4: A routine to launch applications

procedure RunCommand(const Cmd, Params: String);
var
  SI: TStartupInfo;
  PI: TProcessInformation;
  CmdLine: String;
begin
  //Fill record with zero byte values
  FillChar(SI, SizeOf(SI), 0);
  //Set mandatory record field
  SI.cb := SizeOf(SI);
  //Ensure Windows mouse cursor reflects launch progress
  SI.dwFlags := StartF_ForceOnFeedback;
  //Set up command line
  CmdLine := Cmd;
  if Length(Params) > 0 then
    CmdLine := CmdLine + #32 + Params;
  //Try and launch child process. Raise exception on failure
  Win32Check(CreateProcess(
    nil, PChar(CmdLine), nil, nil, False, 0, nil, nil, SI, PI));
  //Wait until process has started its main message loop
  WaitForInputIdle(PI.hProcess, Infinite);
  //Close process and thread handles
  CloseHandle(PI.hThread);
  CloseHandle(PI.hProcess);
end;

Listing 5 shows an alternative routine that can also take names of files and launch the associated application, thanks it using ShellExecuteEx instead of CreateProcess.

Both these listings come from the RunCmd unit that accompanies this paper.

Listing 5: Another routine to launch applications, supporting file associations

uses ShellAPI;
…
//Extended version of RunCommand which can handle file associations
procedure RunCommandEx(const Cmd, Params: String);
var
  SEI: TShellExecuteInfo;
begin
  //Fill record with zero byte values
  FillChar(SEI, SizeOf(SEI), 0);
  //Set mandatory record field
  SEI.cbSize := SizeOf(SEI);
  //Ask for an open process handle
  SEI.fMask := see_Mask_NoCloseProcess;
  //Tell API which window any error dialogs should be modal to
  SEI.Wnd := Application.Handle;
  //Set up command line
  SEI.lpFile := PChar(Cmd);
  if Length(Params) > 0 then
    SEI.lpParameters := PChar(Params);
  SEI.nShow := sw_ShowNormal;
  //Try and launch child process. Raise exception on failure
  if not ShellExecuteEx(@SEI) then
    Abort;
  //Wait until process has started its main message loop
  WaitForInputIdle(SEI.hProcess, Infinite);
  //Close process handle
  CloseHandle(SEI.hProcess);
end;

How do I find if an application is running?

The key to this question is to identify if the main window of the application is in existence. The FindWindow API is used for this job. This API takes two arguments, one of which is the caption of the sought window, the other of which is its window class. You can pass either one of these textual pieces of information, or both of them, depending on what you know. If you supply only one, specify nil for the other.

In the case of a Delphi application, the window class happens to match the VCL form class type.

For those cases where you have not produced the application you are looking for, you can find the Window class using some investigation during the development process. The WinSight32 tool that ships with Delphi will list all windows open on the Windows desktop and give information on their caption and window class as well as their position and parent window. Figure 1 shows it running, giving information on a copy of Microsoft Word. As you can see, the class name for Word’s main window is OpusApp.

Figure 1: WinSight32 showing details of Microsoft Word's main window

A call to FindWindow('OpusApp', nil) would return either the window handle of Word’s main window, or 0. This window handle can be stored in a variable of type HWnd and used with other Windows APIs to manipulate it.

How can I have my application launched when Windows starts?

Other than creating a shortcut in the Windows Startup folder (reasonably complex, particularly as you will need to delete it when the application launches), the way most Windows applications achieve this is via the Windows registry.

What you need to do is to programmatically add a string value for your program under HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce.

If you write an appropriate value there when Windows is terminating, then during its next relaunch Windows will execute all the commands in the RunOnce section after this user logs in, and then delete the registry references.

On the other hand, if you write a value into HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\RunOnce, Windows will execute the program before any user logs in (and will wait for the program to finish before doing so).

The wm_EndSession message gets sent around to all top level windows in all applications if Windows is actually about to close, having found that no applications objected to the user’s close request. A message handler for this message can be used to write your program’s last will and testament into the registry before being terminating. Listing 6, from the sample project Restart.Dpr, contains a possible implementation.

Listing 6: Code to make your application be launched as Windows starts

type
  TForm1 = class(TForm)
    …
  public
    procedure WMEndSession(var Msg: TWMEndSession);
      message wm_EndSession;
  end;
…
uses
  Registry;
…
procedure TForm1.WMEndSession(var Msg: TWMEndSession);
const
  Restart = 'Software\Microsoft\Windows\CurrentVersion\RunOnce';
begin
  if Msg.EndSession then
  begin
    with TRegistry.Create do
      try
        //If you want to run your app before any user
        //logs in then uncomment the next line of code
        //RootKey := HKEY_LOCAL_MACHINE;
        if OpenKey(Restart, True) then
          //Write a value with an arbitrary name,
          //But the full path to your exe as a value
          WriteString(Application.Title, Application.ExeName)
      finally
        Free //Destructor calls CloseKey for me
      end;
    Msg.Result := 0
  end;
  inherited
end;

How can I allow a component to be dragged around a form (or drag a form, but not with caption bar)?

The easiest way to accomplish this is to send the same message to the target window (be it a control or a form) that a form receives when the user drags it with the mouse. The other (more involved way) is to pretend that the target window is part of a caption bar (that is to say part of the non-client area of the window).

The following describes how the system works by default. Every time the mouse is moved, messages are sent to the window under the mouse (or that has captured the mouse) indicating a mouse movement (wm_MouseMove) and to check for a possible new mouse cursor requirement (wm_SetCursor). Additionally, a message is sent to find out whether the mouse is over part of the non-client area of the window. If it is reported to be over the caption bar and the left mouse button is pushed (generating a wm_NCLButtonDown message) and then moved, a special wm_SysCommand message is generated. You can see these messages being traced in WinSight32 in Figure 2.

Figure 2: Delphi's WinSight32 tool tracing some important messages

If you, as a user, choose Move from any system menu (the one obtained by clicking on the icon at the top left of the form, or by pressing Alt+Space), you are able to move the window around the screen with the cursor keys on your keyboard. You can match this programmatically by sending a wm_SysCommand message with a WParam value of sc_Move. The particular wm_SysCommand message seen in Figure 2 is almost, but not quite, the same. It has a WParam value of sc_Move + 2, which equates to mouse dragging of the window.

What all this means is that we can drag any window around the screen by the mouse in one of two ways. Either we can trap for wm_NCHitTest messages in the appropriate component class and return a value of HTCaption (the more difficult option) or we can send the window a wm_SysCommand with a WParam of sc_Move + 2 whenever the mouse is pressed on the component (much easier).

The latter option is by far the easiest general case. The Drag.Dpr project that accompanies this paper shows how to make a generic OnMouseDown event handler (which in this case is shared between the form and a panel on the form) to achieve dragging controls upon demand (see Listing 7). Notice that if there is any mouse capture enabled, you must disable it.

Listing 7: Code to allow the user to drag a component around a form, or drag a form without using the caption bar

procedure TForm1.GenericMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  ReleaseCapture;
  (Sender as TControl).Perform(wm_SysCommand, sc_Move + 2, 0);
end;

The other approach requires message handling for the appropriate windows. Listing 8 comes from Drag2.Dpr and shows a suitable wm_NCHitTest for a form. This allows the form to be dragged by the caption bar as normal, and also the client area. Note however that care is taken to ensure that the border icons and normal border resizing are still allowed to function normally. Only the client area is modified to allow dragging.

Listing 8: Code to allow the form to be dragged by its client area

TForm1 = class(TForm)
public
  procedure WMNCHitTest(var Msg: TMessage);
    message wm_NCHitTest;
end;
…
procedure TForm1.WMNCHitTest(var Msg: TMessage);
begin
  inherited;
  if Msg.Result = HTClient then
    Msg.Result := HTCaption
end;

How do I add a custom mouse cursor into my application?

Custom cursors are added via the global Screen object (defined in the Forms unit). You can manufacture a .RES file containing a cursor resource with, for example, the Image Editor that comes with Delphi, or maybe Inprise’s Resource Workshop. You can then link this resource file to your program, and load the cursor.

Make a new project, drop a button on the form and make an event handler for it. In the interface section of the unit, declare two constants crMyCursor1 and crMyCursor2 with values greater than zero.

Now use the Image Editor to define two cursors in a resource. Call the resources NewCursor1 and NewCursor2 and save them in a resource file NEWCURSORS.RES. To get the resource file linked in you use a compiler directive (a statement that looks rather like a comment). The line looks like this and can go pretty much anywhere - we will put it below the one that’s already in the implementation section of this unit:

Listing 9: Compiler directive to link custom cursors into a project

{$R NEWCUR.RES}

Make an OnCreate handler for the form and in there we can load the cursors (with the LoadCursor API) from the resource into the Cursors array property that the Screen object gives us.

Listing 10: Code to load custom cursors from linked resource file

procedure TForm1.FormCreate(Sender: TObject);
begin
  Screen.Cursors[crMyCursor1] := LoadCursor(HInstance, 'NewCursor1');
  Screen.Cursors[crMyCursor2] := LoadCursor(HInstance, 'NewCursor2');
end;

This particular construct allows us to later refer to our cursors just by using the constants that we defined. Listing 11 shows the button handler that has been patiently waiting.

Listing 11: Code to set custom cursors for a button and the form

procedure TForm1.Button1Click(Sender: TObject);
begin
  Button1.Cursor := crMyCursor1;
  Cursor := crMyCursor2;
end;

The project CustomCursors.Dpr contains all this code.

How do I change the DataSource property of lots of data aware controls without a big nested if statement?

RTTI is the solution to this. The Object Inspector can do exactly this job, and it does it using RTTI. This code (from the DataSourceRTTI.Dpr project) represents a routine to do just what you want. The event handlers that follow it show how to set the DataSource property of a list of components to a specified TDataSource component.

Listing 12: Code to set DataSource property of many data aware controls at once

uses
  TypInfo;

procedure SetDataSource(DataSource: TDataSource;
  const ControlsToChange: array of const);
var
  I: integer;
  PropInfo: PPropInfo;
begin
  for I := Low(ControlsToChange) to High(ControlsToChange) do
    with TVarRec(ControlsToChange[I]) do
      { Sanity check to see if it is an object }
      if VType = vtObject then
      begin
        PropInfo := GetPropInfo(VObject.ClassInfo, 'DataSource');
        if Assigned(PropInfo) then
          SetOrdProp(VObject, PropInfo, LongInt(DataSource));
      end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  SetDataSource(DataSource1, [DBGrid1, DBNavigator1]);
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  SetDataSource(DataSource2, [DBGrid1, DBNavigator1]);
end;

A more generic routine that can set any ordinal, floating point or string property to a specified value is used in the GenericPropertyRTTI.Dpr project. The code from above now changes to this.

Listing 13: A more generic property setting routine in action

uses
  TypInfo;

//SetTo must be an array where the first element is the target new value
procedure ChangeProperty(const Prop: String; const SetTo,
                         Components: array of const);
var
  I: integer;
  PropInfo: PPropInfo;
  Obj: TObject;
begin
 for I := Low(Components) to High(Components) do
    if TVarRec(Components[I]).VType = vtObject then
    begin
      Obj := TVarRec(Components[I]).VObject;
      PropInfo := GetPropInfo(Obj.ClassInfo, Prop);
      if Assigned(PropInfo) then
        with TVarRec(SetTo[Low(SetTo)]) do
          case VType of
            vtInteger, vtBoolean, vtChar, vtObject:
              SetOrdProp(Obj, PropInfo, VInteger);
            vtExtended:
              SetFloatProp(Obj, PropInfo, VExtended^);
            vtString:
              SetStrProp(Obj, PropInfo, VString^);
          end;
    end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  ChangeProperty('DataSource', [DataSource1], [DBGrid1, DBNavigator1])
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  ChangeProperty('DataSource', [DataSource2], [DBGrid1, DBNavigator1])
end;

Changes in Delphi5’s RTTI support mean that this can be slightly altered for Delphi 5 compilation (although it doesn’t have to be). This code comes from GenericPropertyRTTI2.Dpr and, as you can see, is just slightly simpler (no PPropInfo variable, and the use of IsPublishedProp).

Listing 14: A Delphi 5 version of the generic property setting code

procedure ChangeProperty(const Prop: String;
  const SetTo, Components: array of const);
var
  I: integer;
  Obj: TObject;
begin
  for I := Low(Components) to High(Components) do
    if TVarRec(Components[I]).VType = vtObject then
    begin
      Obj := TVarRec(Components[I]).VObject;
      if IsPublishedProp(Obj, Prop) then
        with TVarRec(SetTo[Low(SetTo)]) do
          case VType of
            vtInteger, vtBoolean, vtChar, vtObject:
              SetOrdProp(Obj, Prop, VInteger);
            vtExtended:
              SetFloatProp(Obj, Prop, VExtended^);
            vtString:
              SetStrProp(Obj, Prop, VString^);
          end;
    end;
end;

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware. If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi award in 2000.

Click here to download the files associated with this paper.