Delphi OleVariant to array of string from COM Library - string

I have Delphi 2006 client application. This client recieves an Olevariant type data from COM server. The function is:
procedure OnLimitsChanged(var SymbolsChanged: {??PSafeArray}OleVariant);
This function returns an array of string. I can´t read OleVariant type data from delphi.
From Excel VBA it´s working:
Private Sub g_Realtime_OnLimitsChanged(SymbolsChanged() As String)
Dim i%
Dim Salir As Boolean
If UBound(SymbolsChanged) <> -1 Then
i = 0: Salir = False
While Not Salir
If SymbolsChanged(i) = Simbolo Then
LlamarALimites
Salir = True
Else
i = i + 1
If i > UBound(SymbolsChanged) Then Salir = True
End If
Wend
End If
End Sub
I tried to convert OleVariant to Psafearray...
procedure TfmConfiguracion.RecibirNuevosTicks(ASender: TObject;
var ArrayTicks : Olevariant);
var
Data : pSafeArray;
i,iLow, iHigh : Integer;
value : wideString;
begin
Data:=PSafeArray(TVarData(ArrayTicks).VArray);
SafeArrayGetLBound(Data,1,iLow);
SafeArrayGetUBound(Data,1,iHigh);
for i:=iLow to iHigh do
begin
SafeArrayGetElement(Data,i,Value);
Showmessage(Value);
end;
But I recieve an except in this line:
SafeArrayGetLBound(Data,1,iLow);
Debugger Fault Notification
Project ... faulted with message: ' access violation at 0x751de18c: read of address 0xabababab'. Process Stopper. Use Step or Run to continue
Any advice and suggestions will be greatly appreciated.

The RTL has a VarArrayAsPSafeArray() function for extracting a PSafeArray correctly from an (Ole)Variant:
procedure TfmConfiguracion.RecibirNuevosTicks(ASender: TObject; var ArrayTicks : OleVariant);
var
Data : PVarArray; // RTL's version of PSafeArray
//...
begin
Data := VarArrayAsPSafeArray(ArrayTicks);
//...
end;
If the (Ole)Variant does not contain an array, an exception will be raised. Or you can use VarIsArray() to check it manually:
procedure TfmConfiguracion.RecibirNuevosTicks(ASender: TObject; var ArrayTicks : OleVariant);
var
Data : PVarArray;
//...
begin
if not VarIsArray(ArrayTicks) then Exit;
Data := VarArrayAsPSafeArray(ArrayTicks);
//...
end;
That being said, (Ole)Variant has build-in support for accessing PSafeArray element data, so you don't really need to resort to accessing PSafeArray directly (unless you want an extra performance boost, in which case you need to validate the PSafeArray yourself before you access its data):
procedure TfmConfiguracion.RecibirNuevosTicks(ASender: TObject; var ArrayTicks : Olevariant);
var
i : Integer;
value : String;
begin
if not VarIsArray(ArrayTicks) then Exit;
for i := VarArrayLowBound(ArrayTicks, 1) to VarArrayHighBound(ArrayTicks, 1) do
begin
Value := ArrayTicks[i];
ShowMessage(Value);
end;

The RTL has the function let access a Varant array as a SAFEARRAY:
function VarArrayAsPSafeArray(const V: Variant): PSafeArray;
I wanted to document how to do the reverse.
Variant is a structure
In Delphi a Variant is an opaque blob. But internally it is really the TVarData structure (aka the Windows VARIANT structure). A variant can hold different types of data. You indicate which type through the VType member. The value of the VType member tells you how to interpret the rest of the structure:
a 32-bit Integer (VT_I4)
Variant
VType: Word = VT_I4; //3
VInteger: Integer;
a IUnknown interface (VT_UNKNOWN)
Variant
VType: Word = VT_UNKNOWN; //13
VUnknown: Pointer; //actually IUnknown
an BSTR (aka WideString in Delphi)
Variant
VType: Word = VT_BSTR; //8
VOleStr: PWideChar;
In the case that the variant is a SAFEARRAY of 32-bit integers:
Variant
VType: Word = (VT_ARRAY or VT_I4);
VArray: PVarArray;
And then VArray points to a SAFEARRAY strucuture:
Variant
VType: Word = (VT_ARRAY or VT_I4);
VArray: PVarArray;
cDims: Word;
fFeatures: Word;
cbElements: LongWord;
cLocks: LongWord;
pvData: Pointer;
rgsabound: array[0..0] of TSafeArrayBound;
What if we start with a SAFEARRAY
There are times, particularly when interacting with COM or .NET that you:
have to supply a PSafeArray,
or are given a PSafeArray.
You can construct a SafeArray easily enough, if you use Delphi's functions to create a variant array. Delphi does the heavy lifting to creating the underlying SafeArray that your "variant array" actually is.
But we want to go the other way; we are given a PSafeArray, and we want to wrap it up inside a Delphi Variant variable, so that it handles all the ugliness and lifetime.
assemblies: PSafeArray;
assemblies := DefaultAppDomain.GetAssemblies;
How can we deal with this pointer to a SAFEARRAY?
function PSafeArrayToVariant(psa: PSafeArray): OleVariant;
begin
TVarData(v).VType = (VT_ARRAY or VT_xxxx);
TVarData(v).VArray := PVarArray(psa);
end;
except we need to know what the SafeArray contains; we need to fill in the VT_xxxx in the above code.
Fortunately, one of the members of the SAFEARRAY structure tells what VType the members of the array are:
fFeatures: Word;
FADF_BSTR: It is an array of BSTRs (VT_BSTR)
FADF_UNKNOWN: It is an array of IUnknown (VT_UNKNOWN)
FADF_DISPATCH: It is an array of IDispatch (VT_DISPATCH)
FADF_VARIANT: It is an array of Variants (VT_VARIANT)
FADF_HAVEVARTYPE: You can get the type using SafeArrayGetVartype
Final function
function SafeArrayGetVartype(psa: PSafeArray): TVarType; safecall; external 'OleAut32.dll';
function PSafeArrayToVariant(psa: PSafeArray): OleVariant;
var
features: Word;
vt: TVarType;
const
FADF_HAVEVARTYPE = $80;
begin
features := psa^.fFeatures;
if (features and FADF_UNKNOWN) = FADF_UNKNOWN then
vt := VT_UNKNOWN
else if (features and FADF_DISPATCH) = FADF_DISPATCH then
vt := VT_DISPATCH
else if (features and FADF_VARIANT) = FADF_VARIANT then
vt := VT_VARIANT
else if (features and FADF_BSTR) <> 0 then
vt := VT_BSTR
else if (features and FADF_HAVEVARTYPE) <> 0 then
vt := SafeArrayGetVartype(psa)
else
vt := VT_UI4; //assume 4 bytes of *something*
TVarData(Result).VType := VT_ARRAY or vt;
TVarData(Result).VArray := PVarArray(psa);
end;

Related

How can I access a range of rows in Excel?

I am trying to translate this Excel VBA code to Delphi:
ActiveSheet.Rows(r & ":5000").WrapText = True
ActiveSheet.Rows(r & ":5000").AutoFit
However in the Excel2010 unit in Delphi _Worksheet.Rows is an object, not a function or an array object, I also can't find any Items property or similar.
uses
Excel2010;
procedure Test;
var
Sheet: ExcelWorksheet;
R: Integer;
begin
R := 3;
Sheet.Rows[R.ToString + ':5000'].WrapText := True;
// Sheet.Rows.WrapText := True;
end;
The compiler message is:
[dcc32 Error] Unit1.pas(110): E2149 Class does not have a default property
What is the correct translation of the VBA code?
How can I access a certain range of Rows in Excel?
With early binding, selecting A column (rows 3..5000) and using EntireRow, for example like this:
uses Excel2010;
procedure TForm14.Button1Click(Sender: TObject);
var
Excel: ExcelApplication;
Wbook: ExcelWorkbook;
Sheet: ExcelWorksheet;
begin
Excel := CoExcelApplication.Create;
Wbook := Excel.Workbooks.Add(EmptyParam, LOCALE_USER_DEFAULT);
Sheet := Wbook.ActiveSheet as ExcelWorksheet;
Excel.Visible[LOCALE_USER_DEFAULT] := True;
Sheet.Range['A3','A5000'].EntireRow.WrapText := True;
Sheet.Range['A3','A5000'].EntireRow.AutoFit;
//...
end;
The thing is, if you work with Excel from Delphi using early binding (e.g. using CoExcelApplication.Create), you
are working with the raw interfaces Excel exposes, rather than the variants
you get working with late binding (using CreateOleObject('Excel.Application')).
Both methods have their strengths, early binding is best for speed and taking advantage
of Delphi's type-checking and code completion, whereas late binding is useful
for avoiding to have to specify all the arguments of the methods which have
a lot of them.
In Excel_Tlb (or whatever your Excel import unit is called), maybe the best way to think
of the Rows member of the _Worksheet interface is as a function which returns
a dispinterface to an ExcelRange object. The members which return an ExcelRange interface take two arguments, which specify the top left and bottom right cells defining the range. So, one way to do something along the lines you've
asked about is the following:
That's how you get at and use the Item property you were wondering about.
procedure TDefaultForm.TestRange;
var
WB : _Workbook;
vWB,
vRange,
vSheet : OleVariant;
Sheet: _Worksheet;
Range : ExcelRange;
begin
lcid := LOCALE_USER_DEFAULT;
Excel := CoExcelApplication.Create;
Excel.Visible[LOCALE_USER_DEFAULT] := True;
WB := Excel.Workbooks.Add(EmptyParam, LOCALE_USER_DEFAULT);
Sheet := WB.ActiveSheet as ExcelWorksheet;
Range := Sheet.Range['A1', 'B2'];
Range.RowHeight := 33;
Range.Item[1, 1] := 'some text long enough to wrap';
Range.Item[1, 1].WrapText := True;
Range.Item[1, 2] := 'more text long enough to wrap';
Range.Item[2, 2] := 'other text long enough to wrap';
// The following shows how to use the special syntax for passing arguments
// in late-binding
Excel.DisplayAlerts[LOCALE_USER_DEFAULT] := False; // suppresses "Overwrite?" prompt if file already exists
vWB := WB;
// Compare the following with what you would need if you called WB.SaveAs()
vWB.SaveAs(FileName := ExtractFilePath(Application.ExeName) + 'Test.Xlsx');
// some things using late binding
vSheet := Sheet;
vRange := vSheet.Range['c3'];
vRange.Value := 'some value';
vRange := vSheet.Range['d3:e4'];
vRange.Value := 'another value';
// retrieve the ExcelRange object from the vRange variant
Range := IDispatch(vRange) as ExcelRange;
end;
Here is an example of how to use an ExcelRange
var
lcid: Integer;
Range: ExcelRange;
Worksheet: _WorkSheet;
Row: Integer;
begin
lcid := LOCALE_USER_DEFAULT;
Wbk := ExcelApplication1.Workbooks.Open(Filename,
EmptyParam,EmptyParam,EmptyParam,
EmptyParam,EmptyParam,EmptyParam,
EmptyParam,EmptyParam,EmptyParam,
EmptyParam,EmptyParam,EmptyParam,
EmptyParam,EmptyParam,lcid);
WorkSheet := Wbk.Worksheets.Item['Sheet1'] as _Worksheet;
WorkSheet.Activate(lcid);
Row := 1;
Range := WorkSheet.Range['A'+IntToStr(row), 'F'+IntToStr(row)];
Range.Insert(xlShiftDown, xlFormatFromLeftOrAbove); // shift down and copy
WorkSheet.Cells.Item[row, 6] := edtVatRate.Value;
end;

Why do I get unexpected values when I access an object stored in a list-view item?

When I use a record in a thread, e.g.:
type
TClient = Class(TThread)
public
mcmd:record
cid:Byte;
kd, recvresponse:Boolean;
end;
when I try to access a variable it returns a random value.
Is it a problem with the compiler?
I'm using Delphi 6 on Windows 7 x64 & what I do to access it is:
var
c:TClient;
begin
if listview1.itemfocused = nil then Exit;
c := ListView1.ItemFocused.Data;
if c = nil then Exit;
ShowMessage(IntToStr(c.mcid.cid));
end;
how I assign the itemfocused data variable is by doing:
Procedure TClient.New;
Begin
Main.ListView1.Items.BeginUpdate;
item := Main.ListView1.Items.Add;
item.Caption := Split(dat1, '|', 0);
item.SubItems.Add(ip);
item.Data := #Self;
Main.ListView1.Items.EndUpdate;
End;
which I call by Synchronize.
Self is a local variable. The pointer you store in Data becomes invalid as soon as New returns. Instead of storing a pointer to Self, you need to store its value:
Item.Data := Self;
If that doesn't compile, type-cast.

Defining a string type to be a certain format

I was wondering if there's a way to define a type of string or similar in delphi 7 which is intended to be in a particular format, or matching certain specifications? For example, I'd like to define a TSizeString type which accepts values such as 4x6 or 9x12 or maybe even 2.5x10.75. It should require the x as the only deliminator between two numbers. So there should never be anything like x9 or 65 or 2-4 or 4-6x6-2 and not even 4 x 6.
Just INTEGER + 'x' + INTEGER or SINGLE + 'x' + SINGLE.
Similar I guess to how like a TFilename works, standard filenames may look like C:\MyPath\MyFile.txt or \\Storage\SomeDir\SomeFile.doc
In newer versions of Delphi, advanced records and operator overloading are very handy in this case:
type
TSizeString = record
x, y: single;
public
class operator Implicit(const S: string): TSizeString;
class operator Implicit(const S: TSizeString): string;
end;
implementation
class operator TSizeString.Implicit(const S: string): TSizeString;
var
DelimPos: integer;
begin
DelimPos := Pos('x', S);
if (DelimPos = 0) or (not TryStrToFloat(Copy(S, 1, DelimPos-1), result.X)) or
(not TryStrToFloat(Copy(S, DelimPos + 1), result.y)) then
raise Exception.CreateFmt('Invalid format of size string "%s".', [S]);
end;
class operator TSizeString.Implicit(const S: TSizeString): string;
begin
result := FloatToStr(S.x) + 'x' + FloatToStr(S.y);
end;
Now you can do
procedure TForm1.Button1Click(Sender: TObject);
var
S: TSizeString;
begin
S := '20x30'; // works
ShowMessage(S);
S := 'Hello World!'; // exception raised
ShowMessage(S);
end;
In older versions of Delphi, you simply have to write a class, or create a basic record to hold your size (and then, of course, you can create functions that convert between such records and formatted strings).
Special types, like TFileName and TCaption are nothing special, like Andreas mentioned, but they can be used to register a specific property editor in the IDE. This will help entering such values through the object inspector.
To really enforce such a value, if your string is a property of an object, you can write a setter for it.
Otherwise, I should make a TSize class that has properties for the two integers, and an AsString property that combines its properties to a string.
type
TSize = class
private
FLeftInt, FRightInt: Integer;
function GetString: string;
procedure SetString(Value: string);
public
property LeftInt: Integer read FLeftInt write FLeftInt;
property RightInt: Integer read FRightInt write FRightInt;
property AsString: string read GetString write SetString;
end;
function TSize.GetString: string;
begin
Result := Format('%dx%d', [FLeftInt, FRightInt]);
end;
function TSize.SetString(Value: string);
begin
// Validate and parse Value. Set LeftInt and RightInt.
end;
Simplest way is just to use a function, and always use it when defining your strings...
function MyString(numA, numB: single) : string;
begin
Result := FloatToStr(numA) + 'x' + FloatToStr(numB)
end;
If you want to get fancier, you can do it as a class which allows a direct string assignment as a property, but which parses the string for compliance.

Short Strings in a Variant Record?

I'd like to be able to access sections of a short string as part of a record
Something like
TMyRecord = record
case Boolean of
True:
(
EntireString: String[20];
);
False
(
StringStart: String[8];
StringMiddle: String[4];
StringEnd: String[8];
);
end;
Is this possible or would I have to declare each char individually
TMyRecord = record
private
Chars: Array[1..20] of Char;
Function GetStringStart:String;
Procedure SetStringStart(Value: String);
public
Property StringStart: String read GetStringStart write SetStringStart; // Can I have properties on a record?
end;
Function GetStringStart: String;
begin
Result := Chars[1] + Char[2]....;
end;
Procedure SetStringStart(Value: String);
begin
for i := 1 to 8 do
begin
Chars[i] := Value[i];
end;
end;
Is this possible / worth the effort?
A Delphi short string contains more than just the string contents. The initial byte in the data structure contains the length of the string. This is why short strings are limited to 255 characters.
So, you can't use short strings in your variant array the way you propose.
What you could do is adapt your second approach based on getter and setter methods to be a bit more readable.
For example:
function TMyRecord.GetStringStart: string;
begin
SetString(Result, #Chars[1], 8);
end;
You might consider using a string rather than a char array, but it's a little hard to be 100% sure of that advice without knowing exactly what your underlying problem is.
As a final thought, why not turn the problem around? Store 3 strings: StartString, MiddleString and EndString. Then have a property backed with a getter and setter called EntireString. When you read EntireString it pieces it together from the 3 individual parts, and when you write to it it pulls the individual parts out. I suspect it would be easier that way around.
Your first sample doesn't consider the length byte. The memory layout looks like this:
case True:
L12345678901234567890
^....................
case False:
L12345678L1234L12345678
^........^....^........
(L = length byte).
Depending on your requirements (e.g.: Are the partial strings always 8, 4 and 8 Chars?) I'd try storing the partial strings and make EntireString the property, using System.Copy, StrUtils.LeftStr etc.
ShortString has an implied length, so your first example will map the length parts of the substrings on top of the main string.
Your second sample is the way to start, with these notes:
properties on records are possible
you should think of the length of each sub-string (or is it always a fixed array of 20 characters?)
Edit
It totally depend on the reason you want this, and mixing character arrays and strings will get you into trouble because strings can be shorter than the array length.
Small example:
program VariantRecordsWithCharactersAndStrings;
{$APPTYPE CONSOLE}
uses
SysUtils,
Math;
const
Size20 = 20;
Size8 = 8;
Size4 = 4;
type
TChar20 = array[0..Size20-1] of Char;
TChar8 = array[0..Size8-1] of Char;
TChar4 = array[0..Size4-1] of Char;
TMyRecord = record
class var FillCharValue: Byte;
function GetEntireString: string;
function GetStringStart: string;
function GetStringMiddle: string;
function GetStringEnd: string;
procedure SetEntireString(const Value: string);
procedure SetStringStart(const Value: string);
procedure SetStringMiddle(const Value: string);
procedure SetStringEnd(const Value: string);
property EntireString: string read GetEntireString write SetEntireString;
property StringStart: string read GetStringStart write SetStringStart;
property StringMiddle: string read GetStringMiddle write SetStringMiddle;
property StringEnd: string read GetStringEnd write SetStringEnd;
procedure SetCharArray(const CharArrayPointer: PChar; const CharArraySize: Integer; const Value: string);
case Boolean of
True:
(
CharFull: TChar20;
);
False:
(
CharStart: TChar8;
CharMiddle: TChar4;
CharEnd: TChar8;
);
end;
function TMyRecord.GetEntireString: string;
begin
Result := CharFull;
end;
function TMyRecord.GetStringStart: string;
begin
Result := CharStart;
end;
function TMyRecord.GetStringMiddle: string;
begin
Result := CharMiddle;
end;
function TMyRecord.GetStringEnd: string;
begin
Result := CharEnd;
end;
procedure TMyRecord.SetEntireString(const Value: string);
begin
SetCharArray(CharFull, SizeOf(CharFull), Value);
end;
procedure TMyRecord.SetCharArray(const CharArrayPointer: PChar; const CharArraySize: Integer; const Value: string);
begin
FillChar(CharArrayPointer^, CharArraySize, FillCharValue);
Move(Value[1], CharArrayPointer^, Min(CharArraySize, SizeOf(Char)*Length(Value)));
end;
procedure TMyRecord.SetStringStart(const Value: string);
begin
SetCharArray(CharStart, SizeOf(CharStart), Value);
end;
procedure TMyRecord.SetStringMiddle(const Value: string);
begin
SetCharArray(CharMiddle, SizeOf(CharMiddle), Value);
end;
procedure TMyRecord.SetStringEnd(const Value: string);
begin
SetCharArray(CharEnd, SizeOf(CharEnd), Value);
end;
var
MyRecord: TMyRecord;
procedure Dump();
begin
Writeln(MyRecord.EntireString);
Writeln(MyRecord.StringStart);
Writeln(MyRecord.StringMiddle);
Writeln(MyRecord.StringEnd);
end;
procedure TestWithFillCharValue(const FillCharValue: Byte);
begin
Writeln('Testing with FillCharValue ', FillCharValue);
TMyRecord.FillCharValue := FillCharValue;
MyRecord.EntireString := '123456789001234567890';
Dump();
MyRecord.StringStart := 'AAA';
MyRecord.StringMiddle := 'BBB';
MyRecord.StringEnd := 'CCC';
Dump();
end;
begin
try
TestWithFillCharValue(0); // this will truncated all the sub arrays when you pass strings that are too short
TestWithFillCharValue(20); // when using Unicode, this fails even more horribly
Write('Press <Enter>');
Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
This class does more or less what you want:
it has overlapping data structures
when you assign the arrays: no problem
when you assign the strings: be aware when strings get to short
As other stated, it won't work, because the variant-sized record will add some lengths for StringStart/StringMiddle/StringEnd in the middle of the EntireString type.
You are confusing the *char type of C with the pascal shortstring type. There is an hidden character at position [0] which is the shortstring length.
You could use regular string type, then split in on purpose:
procedure StringSplit(const EntireString: string; out StringStart, StringMiddle, StringEnd: string);
begin
if length(EntireString)<>20 then
exit;
StringStart := copy(EntireString,1,8);
StringMiddle := copy(EntireString,9,4);
StringEnd := copy(EntireString,13,8);
end;
Note that the out parameter type will set all output String* variables into '' before calling the function.
This version will expect entering entire string of 20 chars long.
You could use shortstrings, but with custom types of the exact length, if you want to avoid hidden copies from/to string[255] (which occur when you use a shortstring type and work with string[n] with n<255):
type
String20 = string[20];
String4 = string[4];
String8 = string[8];
procedure StringSplit(const EntireString: String20; out StringStart: String8;
out StringMiddle: String4; out StringEnd: String8);
begin
if length(EntireString)<>20 then
exit;
StringStart := copy(EntireString,1,8);
StringMiddle := copy(EntireString,9,4);
StringEnd := copy(EntireString,13,8);
end;

Delphi - OLE variant passing problem (RsLinx OPC, Group Adding working with only from constants)

We got OPC job. I cannot installed RsLinx to my Win7 (and XP mode too) because of errors, so I send my test app to the real place, and somebody testing it.
Because I don't have DLL, I cannot make Delphi interface, so I need to do OLE Calls only.
I got an interesting problem with Group Add.
I demonstrate it:
procedure TForm1.Button8Click(Sender: TObject);
var
r, g : variant;
s : string;
v : variant;
ws : WideString;
begin
Log('Connect');
r := CreateOleObject('RSI.OPCAutomation');
r.Connect('RSLinx OPC Server');
Log('Add as constant');
g := r.OPCGroups.Add('MONKEY_C');
Log('Name ' + g.Name);
Log('Add as string');
s := 'MONKEY_S';
g := r.OPCGroups.Add(s);
Log('Name ' + g.Name);
Log('Add as variant');
s := 'MONKEY_V';
v := s;
g := r.OPCGroups.Add(v);
Log('Name ' + g.Name);
Log('Add as ole variant');
s := 'MONKEY_OV';
v := VarAsType(s, varOleStr);
g := r.OPCGroups.Add(v);
Log('Name ' + g.Name);
Log('Add as widestring');
s := 'MONKEY_WS';
ws := WideString(s);
g := r.OPCGroups.Add(ws);
Log('Name ' + g.Name);
Log('Add as widestring var');
s := 'MONKEY_WSV';
ws := WideString(s);
v := ws;
g := r.OPCGroups.Add(v);
Log('Name ' + g.Name);
r := 0;
end;
The result was:
Connect
Add as constant
Name MONKEY_C
Add as string
Name _Group0
Add as variant
Name _Group1
Add as ole variant
Name _Group2
Add as widestring
Name _Group3
Add as widestring var
Name _Group4
So the problem that I cannot add any Group than constant defined...
I need to know HOW Delphi compile this constant to I can convert my variant value to this format.
Can anybody help me in this theme?
Thanks:
dd
Hi!
So the problem is mysterious.
I found another errors in the pure OLE calls.
function TDDRsOPCObject.IndexOfGroup(GroupName: string): integer;
var
ogs, g : variant;
i : integer;
s : string;
begin
CheckObject;
Result := -1;
ogs := FObj.OPCGroups;
s := '';
for i := 1 to ogs.Count do begin
g := ogs.Item(i); // This is working
if AnsiCompareText(g.Name, GroupName) = 0 then begin
Result := i;
Exit;
end;
end;
end;
function TDDRsOPCObject.GetGroupByName(GroupName: string): variant;
var
idx : integer;
ogs, g : variant;
begin
CheckObject;
VarClear(Result);
idx := IndexOfGroup(GroupName);
ogs := FObj.OPCGroups;
if idx <> -1
then begin
g := ogs.Item(idx); // HERE I GOT: The parameter is incorrect
Result := g;
end;
end;
So it is interesting: the IndexOfGroup with same call is working, the GetGroupByName is not... :-(
So I determined I do not continue my fighting with windmills (Don Q).
I got TLB from a dear user that have Delphi7 (in Win7 the Delphi6 cannot produce OLE interface), and I found Kassl.
May these interfaces can help me...
Thanks:
dd
As far as I know the constant and the strings are all converted to an OleString/BSTR (WideString). But since you are having these problems... probably not.
What does the documentation of OPCGroups.Add say? What is expected?
Do you have a type library? Maybe you can import them and use the interface directly.
Edit:
The documentation isn't very clear.
There are a few things you can try:
Check in CPU view what the Delphi compiler made of the code with the constant, maybe you see some hints there about what to do with your strings.
Try this code.
code:
const
OPC_GROUP_NAME: WideString = 'MONKEY_C';
<...>
g := r.OPCGroups.Add(OPC_GROUP_NAME);
Log('Name ' + g.Name);
When above code works, try this:
const
{$J+} //writable constants on
OPC_GROUP_NAME: WideString = 'dummy';
{$J-}
<...>
OPC_GROUP_NAME := 'MONKEY_BLA';
g := r.OPCGroups.Add(OPC_GROUP_NAME);
Log('Name ' + g.Name); //should be: 'Name MONKEY_BLA'
Note: I don't like step 2, but if it works.. why not. To me it seems like there is a bug in the com-library you use.
Edit2:
I looked at the code generated by using the constant and using a normal string. With the constant I see the address of the first character being pushed on the stack, with the string I see the address of a pointer to a string being pushed on the stack.
With the code below I can simulate the same behaviour as with the constant:
var
lWideArray: array[0..40] of WideChar;
s: string;
i: Integer;
<..>
s := 'MONKEY_FOO';
for i := 0 to Length(lWideArray) - 1 do
begin
if i < Length(s) then
lWideArray[i] := WideChar(s[i+1])
else
lWideArray[i] := #0;
end;
g := r.OPCGroups.Add(WideString(lWideArray));
Log('Name ' + g.Name);
There are some issues in your code, also it would be nice to know which version of Delphi you're using, and what parameter type the Add() call use. Anyway some hints:
ws := WideString(s);
That's a wrong typecast. It won't convert your string to a WideString, it will just force the memory to be interpreted as such. Use
ws := s;
The compile will take care to call the conversion routine.
You do not have to invent the wheel. There are a lot of libraries, examples and sample code how to use OPC with Delphi. For free Delphi OPC servers and clients, take a look here: http://www.opcconnect.com/delphi.php.

Resources