I have some working code using Delphi's TXMLDocument class, and using the TransformNode method to perform XSLT translation.
But, I need to enable XSLT Javascript functions (<msxml:script> tags) and - after much googling - this means I need to set the AllowXsltScript property of the IXMLDOMDocument2 to true.
http://msdn.microsoft.com/en-us/library/windows/desktop/ms760290(v=vs.85).aspx
I've achieved this successfully - but only by modifying the source of the Delphi Library function CreateDOMDocument in msxmldom.pas.
function CreateDOMDocument: IXMLDOMDocument;
var doc :IXMLDOMDocument2;
begin
doc := TryObjectCreate([CLASS_DOMDocument60, CLASS_DOMDocument40, CLASS_DOMDocument30,
CLASS_DOMDocument26, msxml.CLASS_DOMDocument]) as IXMLDOMDocument2;
if not Assigned(doc) then
raise DOMException.Create(SMSDOMNotInstalled);
doc.setProperty('AllowXsltScript', true); // Allow XSLT scripts!!
Result := doc;
end;
Obviously this is far from satisfactory - so how can I access IXMLDOMDocument2 objects without modifying library code??
You can override the create function via the MSXMLDOMDocumentCreate variable:
unit Unit27;
interface
uses
xmldoc, xmlintf, msxml, msxmldom, Forms, SysUtils,
ActiveX, ComObj, XmlDom, XmlConst,
Windows, Messages, Classes, Controls, StdCtrls;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function TryObjectCreate(const GuidList: array of TGuid): IUnknown;
var
I: Integer;
Status: HResult;
begin
Status := S_OK;
for I := Low(GuidList) to High(GuidList) do
begin
Status := CoCreateInstance(GuidList[I], nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, IDispatch, Result);
if Status = S_OK then Exit;
end;
OleCheck(Status);
end;
function CreateDOMDocument2: IXMLDOMDocument;
var
Doc2 : IXMLDOMDocument2;
begin
Doc2 := TryObjectCreate([CLASS_DOMDocument60, CLASS_DOMDocument40, CLASS_DOMDocument30,
CLASS_DOMDocument26, msxml.CLASS_DOMDocument]) as IXMLDOMDocument2;
if not Assigned(Doc2) then
raise DOMException.Create(SMSDOMNotInstalled);
Doc2.setProperty('AllowXsltScript', true);
Result := Doc2;
end;
procedure TForm1.FormCreate(Sender: TObject);
var
Doc : IXMLDocument;
begin
Doc := TXMLDocument.Create(nil);
Doc.LoadFromFile('c:\temp\test.xml');
end;
initialization
MSXMLDOMDocumentCreate := CreateDOMDocument2;
end.
Note that in XE3 and above, MSXMLDOMDocumentCreate is deprecated in favor of subclassing TMSXMLDOMDocumentFactory and overriding it's CreateDOMDocument function. For future reference, here's an example for XE3 and XE4:
interface
type
TMSXMLDOMDocument2Factory = class(TMSXMLDOMDocumentFactory)
public
class function CreateDOMDocument: IXMLDOMDocument; override;
end;
implementation
{ TMSXMLDOMDocument2Factory }
class function TMSXMLDOMDocument2Factory.CreateDOMDocument: IXMLDOMDocument;
begin
Result := inherited;
if not Assigned(Result) then
raise DOMException.Create(SMSDOMNotInstalled);
AddDOMProperty('AllowXsltScript', True);
SetDOMProperties(Result as IXMLDOMDocument2);
end;
initialization
MSXMLDOMDocumentFactory := TMSXMLDOMDocument2Factory;
end.
Related
Delphi 7 / QuickReport 5.02.2
We've used similar code for several years but have run into an issue recently now that we're migrating workstations to Windows 10. Previously, we were using Windows 7 and all was fine. Maybe there's something I'm missing or doing wrong?
Here's a simple test project I put together to test this. When the report is within a DLL every call to Printer.GetPrinter fails in Windows 10. Though, if the report is on a form within the main application it works fine.
Below is the code, and a zipped up folder for anyone that's interested. There is the dependency on QuickReport though, which can't be helped. Thanks for looking.
https://1drv.ms/u/s!AsbtokV75aocsXM6MQZcrvwpHKcg
DLL Project.
library test_dll;
uses
SysUtils,
Classes,
Forms,
report in 'report.pas' {report_test};
{$R *.res}
function Report_Print(PrinterName: Widestring): Integer; export;
var
Receipt: Treport_test;
begin
try
Receipt := Treport_test.Create(nil);
try
Receipt.Print(PrinterName);
Receipt.Close;
finally
Receipt.Free;
end;
except
Application.HandleException(Application.Mainform);
end;
Result := 1;
end;
exports
Report_Print;
begin
end.
Report Unit
unit report;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, QRCtrls, QuickRpt, ExtCtrls, Printers, QRpCtrls, QRPrntr;
type
Treport_test = class(TForm)
QuickRep1: TQuickRep;
DetailBand1: TQRBand;
TitleBand1: TQRBand;
QRLabel1: TQRLabel;
SummaryBand1: TQRBand;
QRLabel2: TQRLabel;
QRLabel3: TQRLabel;
private
{ Private declarations }
public
{ Public declarations }
procedure Print(const PrinterName: string);
end;
var
report_test: Treport_test;
procedure SetupPrinter(QuickRep: TQuickRep; const PrinterName: string);
function SelectPrinter(QuickRep: TQuickRep; const PrinterName: string): boolean;
implementation
var
DLL_QRPrinter: TQRPrinter;
{$R *.dfm}
function SelectPrinter(QuickRep: TQuickRep; const PrinterName: string): boolean;
var
i: integer;
compareLength: integer;
windowsPrinterName: string;
selectedPrinter: Integer;
defaultPrinterAvailable: Boolean;
begin
defaultPrinterAvailable := True;
try // an exception will occur if there is no default printer
i := Printer.printerIndex;
if i > 0 then ; // this line is here so Delphi does not generate a hint
except
defaultPrinterAvailable := False;
end;
compareLength := Length(PrinterName);
if (not Assigned(QuickRep.QRPrinter)) then
begin
QuickRep.QRPrinter := DLL_QRPrinter;
end;
// Look for the printer.
selectedPrinter := -1;
// Attempt #1: first try to find an exact match
for i := 0 to QuickRep.QRPrinter.Printers.Count - 1 do
begin
windowsPrinterName := Copy(QuickRep.QRPrinter.Printers.Strings[i], 1, compareLength);
if (UpperCase(windowsPrinterName) = UpperCase(PrinterName)) then
begin
selectedPrinter := i;
Break;
end;
end;
// Attempt #2: if no exact matches, look for the closest
if (selectedPrinter < 0) then
for i := 0 to QuickRep.QRPrinter.Printers.Count - 1 do
begin
windowsPrinterName := Copy(QuickRep.QRPrinter.Printers.Strings[i], 1, compareLength);
if (Pos(UpperCase(PrinterName), UpperCase(QuickRep.QRPrinter.Printers.Strings[i])) > 0) then
begin
selectedPrinter := i;
Break;
end;
end;
// Attempt #3: if no exact matches, and nothing close, use default printer
if (selectedPrinter < 0) and (defaultPrinterAvailable) then
selectedPrinter := QuickRep.Printer.printerIndex;
Result := False;
if (selectedPrinter > -1) then
begin
QuickRep.PrinterSettings.PrinterIndex := selectedPrinter;
Result := True;
end;
end;
procedure SetupPrinter(QuickRep: TQuickRep; const PrinterName: string);
begin
//check if we have the default printer instead of the selected printer
SelectPrinter(QuickRep, PrinterName);
QuickRep.Page.Units := Inches;
QuickRep.Page.Length := 11;
end;
procedure Treport_test.Print(const PrinterName: string);
begin
SetupPrinter(QuickRep1, PrinterName);
QuickRep1.Print;
end;
initialization
DLL_QRPrinter := TQRPrinter.Create(nil);
finalization
DLL_QRPrinter.Free;
DLL_QRPrinter := nil;
end.
Test Application
program Project1;
uses
Forms,
Unit1 in 'Unit1.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
Main Form
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, QRPrntr,
Dialogs, StdCtrls, QuickRpt, QRCtrls, ExtCtrls, Printers, QRPCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
type
TPrintReport = function(PrinterName: Widestring): Integer;
var
Form1: TForm1;
procedure PrintReport(const PrinterName: string);
implementation
var
DLLHandle: THandle = 0;
POS: TPrintReport = nil;
{$R *.dfm}
procedure PrintReport(const PrinterName: string);
begin
try
POS(PrinterName);
except on e: Exception do
ShowMessage(e.Message);
end;
end;
procedure LoadDLL;
var
DLLName: string;
DLLRoutine: PChar;
begin
DLLName := 'test_dll.dll';
DLLRoutine := 'Report_Print';
if not (FileExists(DLLName)) then
raise Exception.CreateFmt('The DLL "%s" is missing. Build the DLL project and try again.', [DLLName]);
Application.ProcessMessages;
DLLHandle := LoadLibrary(PChar(DLLName));
Application.ProcessMessages;
if (DLLHandle = 0) then
raise Exception.CreateFmt('Error: %s, while attempting to load DLL %s.', [IntToStr(GetLastError), DLLName]);
POS := GetProcAddress(DLLHandle, DLLRoutine);
if (#POS = nil) then
raise Exception.CreateFmt('Error: %s, while attempting get address to %s in DLL %s.', [IntToStr(GetLastError), DLLRoutine, DLLName]);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
LoadDLL;
ShowMessage('dll loaded');
PrintReport('MyPrinter');
FreeLibrary(DLLHandle);
end;
end.
Snippet from QuickReport
procedure TPrinterSettings.ApplySettings;
var
Cancel : boolean;
begin
FPrinter.GetPrinter(FDevice, FDriver, FPort, DeviceMode);
DevMode := GlobalLock(DeviceMode);
begin
SetField(dm_paperlength);
...
DeviceMode is 0, so SetField throws an access violation. See below.
Access violation at address 036BFBA7 in module 'test_dll.dll'. Write of address 00000028.
Try comment out those 2 lines for GetPrinter and for DevMode
procedure TPrinterSettings.ApplySettings;
var
Cancel : boolean;
begin
// FPrinter.GetPrinter(FDevice, FDriver, FPort, DeviceMode);
// DevMode := GlobalLock(DeviceMode);
begin
SetField(dm_paperlength);
...
end
uses ComObj, ActiveX, StdVcl;
if Printer.Printers.Count>0 then
begin
FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
FWMIService := FSWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', '');
FWbemObject := FWMIService.Get(Format('Win32_Printer.DeviceID="%s"',[Printer.Printers.Strings[0]]));
if not VarIsClear(FWbemObject) then
FWbemObject.SetDefaultPrinter();
end;
new solution
Windows 10 have not default printer with this code u can set the default printer
I managed to distill one of the underlying issues rooted in my question How to trace _AddRef / _Release calls for OLE Automation objects in the unit below.
I'll answer this answer too, just in case anyone else bumps into this.
The question: with the below code, why doesn't WINWORD.EXE always quit (sometimes it does quit).
The unit can probably be trimmed down even more.
unit Unit2;
interface
uses
Winapi.Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls,
WordXP;
type
TForm2 = class(TForm)
WordXPFailsToQuitButton: TButton;
procedure WordXPFailsToQuitButtonClick(Sender: TObject);
private
FWordApplication: TWordApplication;
strict protected
function GetWordApplication: TWordApplication; virtual;
function GetWordApplication_Documents: Documents; virtual;
procedure WordApplication_DocumentBeforeClose(ASender: TObject; const Doc: _Document; var Cancel: WordBool); virtual;
procedure WordApplication_Quit(Sender: TObject); virtual;
property WordApplication: TWordApplication read GetWordApplication;
property WordApplication_Documents: Documents read GetWordApplication_Documents;
end;
var
Form2: TForm2;
implementation
uses
Vcl.OleServer;
{$R *.dfm}
function TForm2.GetWordApplication: TWordApplication;
begin
if not Assigned(FWordApplication) then
begin
FWordApplication := TWordApplication.Create(nil);
FWordApplication.AutoConnect := False;
FWordApplication.AutoQuit := False;
FWordApplication.ConnectKind := ckNewInstance;
FWordApplication.OnDocumentBeforeClose := WordApplication_DocumentBeforeClose;
FWordApplication.OnQuit := WordApplication_Quit;
FWordApplication.Connect;
end;
Result := FWordApplication;
end;
function TForm2.GetWordApplication_Documents: Documents;
begin
Result := WordApplication.Documents;
if not Assigned(Result) then
raise EAccessViolation.Create('WordApplication.Documents');
end;
procedure TForm2.WordXPFailsToQuitButtonClick(Sender: TObject);
begin
try
WordApplication_Documents.Add(EmptyParam, EmptyParam, EmptyParam, EmptyParam);
WordApplication.Visible := True;
WordApplication.ActiveDocument.Close(False, EmptyParam, EmptyParam);
finally
WordApplication.OnQuit := nil;
WordApplication.OnDocumentBeforeClose := nil;
WordApplication.AutoQuit := True;
WordApplication.Disconnect;
WordApplication.Free;
FWordApplication := nil;
end;
end;
procedure TForm2.WordApplication_DocumentBeforeClose(ASender: TObject; const Doc: _Document; var Cancel: WordBool);
begin
FWordApplication.Disconnect;
end;
procedure TForm2.WordApplication_Quit(Sender: TObject);
begin
FWordApplication.Disconnect;
end;
end.
Answer part 1:
Comment out the disconnect in the below event:
procedure TForm2.WordApplication_DocumentBeforeClose(ASender: TObject; const Doc: _Document; var Cancel: WordBool);
begin
// FWordApplication.Disconnect;
end;
The event will be called during the DocumentClose(...) method, then disconnect and delete the OLE interface from the FWordApplication instance.
I have not yet figured out which reference is dangling, but this effectively keeps WINWORD.EXE alive most of the times.
Answer part 2:
Sometimes WINWORD.EXE does quit because toe WordApplication_DocumentBeforeClose event is not called. The reason is that the code runs so fast that Word is not fully initialized yet to perform the event.
This is a follow up to this post.
I refined my requirement based on the accepted answer posted here.
My *.dpr file:
program DuckD11;
{$APPTYPE CONSOLE}
uses
SysUtils,
uDuckTyping in 'uDuckTyping.pas',
uBirds in 'uBirds.pas';
procedure DoSomething(AObject: TObject);
begin
Duck(AObject).Quack;
end;
var
Bird: TBird;
Ganagana: TGanagana;
Canard: TCanard;
begin
Writeln('Duck typing :');
Writeln;
Bird := TBird.Create('Bird');
try
DoSomething(Bird);
finally
Bird.Free;
end;
Ganagana := TGanagana.Create;
try
DoSomething(Ganagana);
finally
Ganagana.Free;
end;
Canard := TCanard.Create;
try
DoSomething(Canard);
finally
Canard.Free;
end;
Readln;
end.
uBirds.pas listing:
unit uBirds;
interface
uses
SysUtils;
type
{$METHODINFO ON}
TBird = class
private
FName: string;
public
constructor Create(AName: string);
procedure Quack;
end;
TGanagana = class
private
const cName = 'Ganagana';
public
procedure Quack;
end;
TCanard = class
private
const cName = 'Canard';
public
procedure Quack;
end;
{$METHODINFO OFF}
implementation
{ TBird }
constructor TBird.Create(AName: string);
begin
FName := AName;
end;
procedure TBird.Quack;
begin
Writeln(Format(' %s->Quack',[Self.FName]));
end;
{ TGanagana }
procedure TGanagana.Quack;
begin
Writeln(Format(' %s=>Quack',[Self.cName]));
end;
{ TCanard }
procedure TCanard.Quack;
begin
Writeln(Format(' %s::Quack',[Self.cName]));
end;
end.
My attempt coding uDuckTyping.pas:
unit uDuckTyping;
interface
type
IDuck = interface
['{41780389-7158-49F7-AAA5-A4ED5AE2699E}']
procedure Quack;
end;
function Duck(AObject: TObject): IDuck;
implementation
uses
ObjAuto;
type
TDuckObject = class(TInterfacedObject, IDuck)
private
FObj: TObject;
// ???
protected
procedure Quack;
public
constructor Create(AObject: TObject);
end;
function Duck(AObject: TObject): IDuck;
begin
Result := TDuckObject.Create(AObject);
end;
{ TDuckObject }
constructor TDuckObject.Create(AObject: TObject);
begin
FObj := AObject;
// ???
end;
procedure TDuckObject.Quack;
begin
// ???
end;
end.
My question:
I want to use
ObjAuto.GetMethodInfo to ascertain the existence of the wrapped Quack method.
ObjAuto.ObjectInvoke to invoke the wrapped Quack method.
How can I complete the code ?
I end up getting it to work after many trial:
Modifications in the uDucktyping.pas unit:
Fields added as private in TDuckObject class definition
FQuackPMethodInfo: PMethodeInfoHeader;
FParamIndexes: array of Integer;
FParams: array of Variant;
Initialization of FQuackPMethodInfo in TDuckObject.Create implementation
FQuackPMethodInfo := GetMethodInfo(AObject, ShortString('Quack'));
To append just after FObj initialization statement.
Invokation of "Quack" within TDuckObject.Quack implementation
if Assigned(FQuackPMethodInfo) then
ObjectInvoke(FObj, FQuackPMethodInfo, FParamIndexes, FParams);
Using: Delphi 2010 and the JEDI Windows API and JWSCL
I am trying to assign the Logon As A Service privilege to a user using LsaAddAccountRights function but it does not work ie. after the function returns, checking in Group Policy Editor shows that the user still does not have the above mentioned privilege.
I'm running the application on Windows XP.
Would be glad if someone could point out what is wrong in my code:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls, JwaWindows, JwsclSid;
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function AddPrivilegeToAccount(AAccountName, APrivilege: String): DWORD;
var
lStatus: TNTStatus;
lObjectAttributes: TLsaObjectAttributes;
lPolicyHandle: TLsaHandle;
lPrivilege: TLsaUnicodeString;
lSid: PSID;
lSidLen: DWORD;
lTmpDomain: String;
lTmpDomainLen: DWORD;
lTmpSidNameUse: TSidNameUse;
lPrivilegeWStr: String;
begin
ZeroMemory(#lObjectAttributes, SizeOf(lObjectAttributes));
lStatus := LsaOpenPolicy(nil, lObjectAttributes, POLICY_LOOKUP_NAMES, lPolicyHandle);
if lStatus <> STATUS_SUCCESS then begin
Result := LsaNtStatusToWinError(lStatus);
Exit;
end;
try
lTmpDomainLen := DNLEN; // In 'clear code' this should be get by LookupAccountName
SetLength(lTmpDomain, lTmpDomainLen);
lSidLen := SECURITY_MAX_SID_SIZE;
GetMem(lSid, lSidLen);
try
if LookupAccountName(nil, PChar(AAccountName), lSid, lSidLen, PChar(lTmpDomain),
lTmpDomainLen, lTmpSidNameUse) then begin
lPrivilegeWStr := APrivilege;
lPrivilege.Buffer := PChar(lPrivilegeWStr);
lPrivilege.Length := Length(lPrivilegeWStr) * SizeOf(Char);
lPrivilege.MaximumLength := lPrivilege.Length;
lStatus := LsaAddAccountRights(lPolicyHandle, lSid, #lPrivilege, 1);
Result := LsaNtStatusToWinError(lStatus);
end
else
Result := GetLastError;
finally
FreeMem(lSid);
end;
finally
LsaClose(lPolicyHandle);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
AddPrivilegeToAccount('Sam', 'SeServiceLogonRight');
end;
end.
Thanks in advance.
To be able to use LsaAddAccountRights you should open policy handle with additional POLICY_CREATE_ACCOUNT flag (POLICY_CREATE_ACCOUNT | POLICY_LOOKUP_NAMES) in LsaOpenPolicy or use MAXIMUM_ALLOWED instead of both flags.
I have a TList. It contains a collection of objects of the same type. These objects are descended from a TPersistent, and have about 50 different published properties.
In my application, the user can issue a search of these objects, and the results of the search are displayed in a TDrawGrid, with the specific columns displayed being based on the properties being searched. For example, if the user searches on 'invoice', an 'invoice' column is displayed in the results' grid. I would like to be able to let the user sort this grid. The kicker, of course, is that I wont know up front what columns are in the grid.
Normally to sort a TList, I'd just make a function, such as SortOnName( p1, p2), and call the TList's sort() method. I'd like to go one step further and find a way to pass a property name to the sort method and use RTTI to make the comparison.
I could, of course, make 50 different sort methods and just use that. Or, set a variable globally or as part of the class doing all this work to indicate to the sorting method what to sort on. But I was curious if any of the Delphi pro's out there had other ideas on how to implement this.
Delphi 7 version
Here's an example of how to achieve that. I used Delphi2010 to implement it but it should work in Delphi7 at least as I used TypInfo unit directly.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm1 = class(TForm)
ListBox1: TListBox;
Edit1: TEdit;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
FList: TList;
procedure DoSort(PropName: String);
procedure DoDisplay(PropName: String);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
uses
TypInfo;
var
PropertyName: String;
type
TPerson = class
private
FName: String;
FAge: Integer;
published
public
constructor Create(Name: String; Age: Integer);
published
property Name: String read FName;
property Age: Integer read FAge;
end;
{ TPerson }
constructor TPerson.Create(Name: String; Age: Integer);
begin
FName := Name;
FAge := Age;
end;
function ComparePersonByPropertyName(P1, P2: Pointer): Integer;
var
propValueP1, propValueP2: Variant;
begin
propValueP1 := GetPropValue(P1, PropertyName, False);
propValueP2 := GetPropValue(P2, PropertyName, False);
if VarCompareValue(propValueP1, propValueP2) = vrEqual then begin
Result := 0;
end else if VarCompareValue(propValueP1, propValueP2) = vrGreaterThan then begin
Result := 1;
end else begin
Result := -1;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
FList := TList.Create;
FList.Add(TPerson.Create('Zed', 10));
FList.Add(TPerson.Create('John', 20));
FList.Add(TPerson.Create('Mike', 30));
FList.Add(TPerson.Create('Paul', 40));
FList.Add(TPerson.Create('Albert', 50));
FList.Add(TPerson.Create('Barbara', 60));
FList.Add(TPerson.Create('Christian', 70));
Edit1.Text := 'Age';
DoSort('Age'); // Sort by age
DoDisplay('Age');
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
DoSort(Edit1.Text);
DoDisplay(Edit1.Text);
end;
procedure TForm1.DoSort(PropName: String);
begin
PropertyName := PropName;
FList.Sort(ComparePersonByPropertyName);
end;
procedure TForm1.DoDisplay(PropName: String);
var
i: Integer;
strPropValue: String;
begin
ListBox1.Items.Clear;
for i := 0 to FList.Count - 1 do begin
strPropValue := GetPropValue(FList[i], PropName, False);
ListBox1.Items.Add(strPropValue);
end;
end;
end.
BTW, I used a simple form with a listbox, an edit and a button. The listbox shows the contents of the list (FList) sorted. The button is used to sort the list according to what the user has typed in the editbox.
Delphi 2010 version (uses references to methods)
unit Unit2;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TForm2 = class(TForm)
ListBox1: TListBox;
Edit1: TEdit;
Button1: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
FList: TList;
FPropertyName: String; { << }
procedure DoSort(PropName: String);
procedure DoDisplay(PropName: String);
function CompareObjectByPropertyName(P1, P2: Pointer): Integer; { << }
public
{ Public declarations }
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
uses
TypInfo;
type
TPerson = class
private
FName: String;
FAge: Integer;
published
public
constructor Create(Name: String; Age: Integer);
published
property Name: String read FName;
property Age: Integer read FAge;
end;
{ TPerson }
constructor TPerson.Create(Name: String; Age: Integer);
begin
FName := Name;
FAge := Age;
end;
/// This version uses a method to do the sorting and therefore can use a field of the form,
/// no more ugly global variable.
/// See below (DoSort) if you want to get rid of the field also ;)
function TForm2.CompareObjectByPropertyName(P1, P2: Pointer): Integer; { << }
var
propValueP1, propValueP2: Variant;
begin
propValueP1 := GetPropValue(P1, FPropertyName, False);
propValueP2 := GetPropValue(P2, FPropertyName, False);
if VarCompareValue(propValueP1, propValueP2) = vrEqual then begin
Result := 0;
end else if VarCompareValue(propValueP1, propValueP2) = vrGreaterThan then begin
Result := 1;
end else begin
Result := -1;
end;
end;
procedure TForm2.FormCreate(Sender: TObject);
begin
FList := TList.Create;
FList.Add(TPerson.Create('Zed', 10));
FList.Add(TPerson.Create('John', 20));
FList.Add(TPerson.Create('Mike', 30));
FList.Add(TPerson.Create('Paul', 40));
FList.Add(TPerson.Create('Albert', 50));
FList.Add(TPerson.Create('Barbara', 60));
FList.Add(TPerson.Create('Christian', 70));
Edit1.Text := 'Age';
DoSort('Age'); // Sort by age
DoDisplay('Age');
end;
procedure TForm2.Button1Click(Sender: TObject);
begin
DoSort(Edit1.Text);
DoDisplay(Edit1.Text);
end;
procedure TForm2.DoSort(PropName: String);
begin
FPropertyName := PropName; { << }
FList.SortList(CompareObjectByPropertyName); { << }
/// The code above could be written with a lambda, and without CompareObjectByPropertyName
/// using FPropertyName, and by using a closure thus referring to PropName directly.
/// Below is the equivalent code that doesn't make use of FPropertyName. The code below
/// could be commented out completely and just is there to show an alternative approach.
FList.SortList(
function (P1, P2: Pointer): Integer
var
propValueP1, propValueP2: Variant;
begin
propValueP1 := GetPropValue(P1, PropName, False);
propValueP2 := GetPropValue(P2, PropName, False);
if VarCompareValue(propValueP1, propValueP2) = vrEqual then begin
Result := 0;
end else if VarCompareValue(propValueP1, propValueP2) = vrGreaterThan then begin
Result := 1;
end else begin
Result := -1; /// This is a catch anything else, even if the values cannot be compared
end;
end);
/// Inline anonymous functions (lambdas) make the code less readable but
/// have the advantage of "capturing" local variables (creating a closure)
end;
procedure TForm2.DoDisplay(PropName: String);
var
i: Integer;
strPropValue: String;
begin
ListBox1.Items.Clear;
for i := 0 to FList.Count - 1 do begin
strPropValue := GetPropValue(FList[i], PropName, False);
ListBox1.Items.Add(strPropValue);
end;
end;
end.
I marked with { << } the main changes.
Upgrade to Delphi >= 2009, and then you can use anonymous methods to pass a function declaration directly into TList.Sort.
An example can be found at
http://delphi.about.com/od/delphitips2009/qt/sort-generic.htm
I don't know of any other way, other than the methods you describe in your question.