MS Word 2010 mailmerge in Delphi 5 - delphi

could anyone help?
I've inherited some software written in Delphi 5 which allows member data and fields from a database (.ADT file) to be used merged in to word.
It works fine with all version of Word except 2010 where it won't load any documents and shows the error:
"That Method is not available on that object"
I have been told the solution is to replace the preset components OpWord and OpDataSet with Ole variants. I have done so with OpWord using:
wrdApp := CreateOleObject('Word.Application');
and the documents now load up but without any merge field data. Can anyone let me know how to extract this data from the database, as the OpDataSet seems to simply just point at the table?
Or can anyone suggest a better solution than the one I'm trying. I'm very new to Delphi so I'm in abit over my head
Edit: (Requested Info)
Sorry I have more details and code if required.
The components appear to belong to a library called OfficePartner along with TOpExcel,TOpOutlook and others.
The .doc is selected from a popup ListPane on Form30, opened and populated with merge field data from Table 4. Table 1 is the members database:
{Use Table4 as we can Set a range on it}
Table4.SetRange([Table1.FieldByName('Member Id').AsString],[Table1.FieldByName('Member Id').AsString]);
{Open Word}
OpWord1.Connected := True;
{Open the Test Document}
OpWord1.OpenDocument(DocumentDirectory + '\' + Form30.ListBox1.Items[Form30.ListBox1.ItemIndex]);
{Populate the Test Document}
OpWord1.ActiveDocument.MailMerge.OfficeModel := OpDataSetModel1;
OpWord1.ActiveDocument.PopulateMailMerge;
OpWord1.ActiveDocument.ExecuteMailMerge;
I hope this helps...

Here is a little procedure for word mail merge that I used way back for D6, it's a just snippet and you have to include in some class, I don't have Delphi anymore so can't compile to make sure that it works, anyway here it is, hope it helps:
procedure MailMergeWord;
var
WordApp: TWordApplication;
WordDoc: TWordDocument;
doc : WordDocument;
FileName: OleVariant;
xx: integer;
begin
WordApp := TWordApplication.Create(nil);
WordApp.ConnectKind := ckNewInstance;
WordDoc := TWordDocument.Create(WordApp);
FileName := 'TemplateDoc.doc';
doc := WordApp.Documents.Open(FileName,EmptyParam,EmptyParam,EmptyParam,EmptyParam
,EmptyParam,EmptyParam,EmptyParam,EmptyParam
,EmptyParam);
WordDoc.ConnectTo(Doc);
for xx := 1 to WordDoc.Fields.Count do
WordDoc.Fields.Item(xx).Result.Text := OnWordVariable(WordDoc.Fields.Item(xx).Code.Text);
WordDoc.PrintOut;
WordDoc.Free;
WordApp.Free;
end;
function OnWordVariable(varName: string): string;
begin
Result := 'Value based on variable name';
end;

Related

how can i add hyperlink into open word document in OleContainer

i want to use MsWord as editor for my HTML document.
i open anther form with some list of files.
i want the user to select one of the files
and add this as alink in the open document (at the place the user select)
i open HTML document in word created in olecontainer.
with :
with OleContainerFrame do
begin
OleContainer1.CreateObjectFromFile(FileToEditName{myfile.html}, False);
OleContainer1.AutoActivate := aaGetFocus;
OleContainer1.DoVerb(ovOpen);
OleContainer1.Run;
end;
how can i add this link, as :
AddHperyLink(SomeText,TheHyperLink)....
at the place the user select
Suppose there is a TEdit on your form which contains a URI (I used the BBC's site). Then the following code will add a hyperlink to it in the active Word document in your OLEContainer:
procedure TForm1.Button1Click(Sender: TObject);
begin
OleContainer1.OleObject.ActiveDocument.Hyperlinks.Add(
Anchor := OleContainer1.OleObject.Selection.Range,
Address := Edit1.Text, // contains e.g. http://www.bbc.co.uk
TextToDisplay := 'Link'
);
end;
The way this works is that OleContainer1.OleObject is a variant reference to Word.Application (see e.g. the Word2000.Pas unit that comes with Delphi) and once you have this reference you can call Word's automation methods using late (or early) binding.
Btw the unusual syntax of the arguments to OleContainer1.OleObject.ActiveDocument.Hyperlinks.Add is a special syntax that Delphi supports to enable named parameters to be used in latebound calls.
Update: You say in a comment that you have tried the code above but get the error "Method 'Selection' not supported by automation object". When I put together my test project, I didn't have an association set up between HTML and MS Word, so I write the code necessary to activate Word and load an HTML file into it. I do this in the FormCreate event:
procedure TForm1.FormCreate(Sender: TObject);
var
V : OleVariant;
AFileName : String;
begin
OleContainer1.CreateObject('Word.Application', False);
OleContainer1.Run;
V := OleContainer1.OleObject;
Caption := V.Name;
V.Visible := True;
AFileName := ExtractFilePath(Application.ExeName) + 'Hello.Html';
V.Documents.Add(AFileName);
end;
Note that this and Button1Click are the entire code of my project and it inserts the link as you asked. If you get a different result, I think it must be because of some detail of your set-up that we readers can't see.
yes that work.
i didnot now we can use
(Anchor := ....
);
but now
word remove the execet PATH and change it to 'href="../../../../MzIAI/Images/2019-06/12/45545_5679.Pdf">'
and remove full path

Delphi, retrieve both visible text and hidden hyperlink when pasting into a delphi application

How can I do that? I've been looking all over the internet to find some clues but failed.
You can click on a link in the browser and copy it and then paste it into a word doc document for example.
I using a tcxGrid with some fields and want to paste this link into the field. The field will show you the text but if you click on it it will open the browser with this link.
I can fix all the later part but I don't know how to extract the text and the link from the clipboard.
Does anyone know how to do it?
I've found an old article that describes how you can do it but the result is not good. I get Chinese text instead of HTML.. see below my test code:
function TForm2.clipBoardAsHTML: string;
var
CF_HTML: UINT;
CFSTR_INETURL: UINT;
URL: THandle;
HTML: THandle;
Ptr: PChar;
begin
CF_HTML := RegisterClipboardFormat('HTML Format');
CFSTR_INETURL := RegisterClipboardFormat('UniformResourceLocator');
result := '';
with Clipboard do
begin
Open;
try
HTML := GetAsHandle(CF_HTML);
if HTML <> 0 then
begin
Ptr := PChar(GlobalLock(HTML));
if Ptr <> nil then
try
Result := Ptr;
finally
GlobalUnlock(HTML);
end;
end;
finally
Close;
end;
end;
end;
Data looks like:
敖獲潩㩮⸱ര匊慴瑲呈䱍〺〰〰〰ㄲര䔊摮呈䱍〺〰〰㈰㐳ള匊慴
and much more.
So something is wrong with my code it looks.. :(
The recommended format CFSTR_INETURL does not exist in the clipboard when takes a copy from Firefox, and Excel so I couldn't get any data using that format.
==================================
Latest test - Retrieve of format names.
procedure TForm2.Button2Click(Sender: TObject);
var
i: integer;
s: string;
szFmtBuf: array[0..350] of PWideChar;
fn: string;
fmt: integer;
begin
Memo1.Clear;
for i := 0 to clipBoard.FormatCount - 1 do
begin
fmt := clipBoard.Formats[i];
getClipBoardFormatName(fmt,#szFmtBuf,sizeOf(szFmtBuf));
fn := WideCharToString(#szFmtBuf);
if fmt >= 49152 then
Memo1.Lines.Add(fmt.ToString+ ' - '+fn);
end;
end;
Finally I made this code work :) but the main question how I'll get the url from the clipboard are still unsolved. :(
If I loop through all found formats I only get garbage from them.
The formats from Firefox looks:
49161 - DataObject
49451 - text/html
49348 - HTML Format
50225 - text/_moz_htmlcontext
50223 - text/_moz_htmlinfo
50222 - text/x-moz-url-priv
49171 - Ole Private Data
It really depends on which format(s) the copier decides to place on the clipboard. It may place multiple formats on the clipboard at a time.
A hyperlink with url and optional text may be represented using either:
the Shell CFSTR_INETURL format (registered name: 'UniformResourceLocator') containing the URL of the link, and the CF_(UNICODE)TEXT format containing the text of the link, if any.
the CF_HTML format (registered name: 'HTML Format') containing whole fragments of HTML, including <a> hyperlinks and optional display text.
The VCL's TClipboard class has HasFormat() and GetAsHandle() methods for accessing the data of formats other than CF_(UNICODE)TEXT (which can be retrieved using the TClipboard.AsText property).
You need to use the Win32 RegisterClipboardFormat() function at runtime to get the format IDs for CFSTR_INETURL and CF_HTML (using the name strings mentioned above) before you can then use those IDs with HasFormat() and GetAsHandle().
You can also enumerate the formats that are currently available on the clipboard, using the TClipboard.FormatCount and TClipboard.Formats[] properties. For format IDs in the $C000..$FFFF range, use the Win32 GetClipboardFormatName() function to retrieve the names that were originally registered with RegisterClipboardFormat().

Delphi: SetFileDate creates wrong LastWriteTime (Summer/Wintertime)

i am downloading a file from my server (i only get the bytes and a DateTime for the lastwritetime attribute) and after downloading the data i create a new file on my local machine and want to set the lastwritetime attribute.
For this i am using the following method:
procedure SetFileDate(const FileName: string; NewDate: TDateTime);
var
FileDate, FileHandle: Integer;
begin
try
FileDate := DateTimeToFileDate(NewDate);
FileHandle := FileOpen(FileName, fmOpenReadWrite or fmShareDenyWrite);
if FileHandle > 0 then
begin
FileSetDate(FileHandle, FileDate);
FileClose(FileHandle);
end;
except
begin
// ERROR Log
err.Msg('FileReqThrd.SetFileDate');
end;
end;
end;
For the 'NewDate' parameter i use the DateTime which i get from my server.
I tried to convert the DateTime from the server like this to get the valid lastwritetime (i am requesting the data from a WCF this is why i am converting it to UTCDateTime, the untouched data from the WCF service is TXSDateTime):
TDateTime cloudFileDateTime := StrToDateTime(DateTimeToStr(cloudDownloadResult.FileCloudData.Lastwritetime.AsUTCDateTime));
But in the end my lastwritetime attribute from files which have a lastwritetime in the wintertime period are wrong with -1h.
I hope you understand my problem and can give me an idea how to solve it.
Best regards
The easiest way to do this is to call TFile.SetLastWriteTimeUtc from the System.IOUtils unit.
TFile.SetLastWriteTimeUtc(FileName,
DateTimeUtc);
If this function is not available use the Win32 API function SetFileTime.
You'll also need DateTimeToSystemTime and then SystemTimeToFileTime in that scenario.
The answer provided by David (to use TFile.SetLastWriteTimeUtc) is correct. However, there was some discussion in the comments about bugs. As I am unable to comment (due to lack of rep), I'll add this here for anyone who comes across this problem in future.
While TFile.SetLastWriteTimeUtc works correctly, TFile.GetLastWriteTimeUtc does indeed have a bug relating to daylight saving time. There is a bug report filed with Embarcadero, and it looks like they've now fixed it in Delphi 10.3 Rio (though I haven't tried it yet).
If you are working with an older version of Delphi, you will have to work around the problem via use of the Windows API. e.g. GetFileAttributesEx:
function GetFileModTimeUtc(filePath: string): TDateTime;
var data: TWin32FindData;
var sysTime: TSystemTime;
begin
if GetFileAttributesEx(PChar(filePath), GetFileExInfoStandard, #data) and
FileTimeToSystemTime(data.ftLastWriteTime, sysTime) then begin
Result := SystemTimeToDateTime(sysTime);
end else begin
raise Exception.Create('Unable to get last file write time for ' + filePath);
end;
end;

Restoring dataset from delimiter separated values file

In short, i'm new to delphi and I want to achieve the following:
I have table definition as .cds file {index, data, date}, and some data in .csv format.
I want to load the .csv file to the table and show log it's changes and errors (ex: invalid date format).
Question
How to solve this task elegantly?
I would use JvCsvDataSet (JEDI JVCL component) because it parses CSV files properly, and then use a data-pump component, to move the data into the client dataset, along with some validation.
But if all you really need to do is provide a CSV file, to a data-aware control, I would leave out the ClientDataSet completely, and just use a component built for the purpose you are trying to do. Don't use a screw as a nail, or a nail as a screw. They are both made of metal, but they do different jobs.
CSV file table definitions are quite different in purpose, to a CDS table definition, and the JvCsvDataSet provides a simple string property which you can set up to give the metadata (field datatypes like integer or string or date-time, and associated field names, for CSV files that lack a header row) more easily, than you could hope to do it in ClientDatSet.
You can read line by line from the .csv, set each line to 'DelimitedText' of a StringList, append a record to the dataset, loop the string list to set each field's value and then post to the dataset. You can put the 'field value assinging'/'posting' in a try-except block and log any error message of raised exceptions together with information you like (e.g. malformed field value/name, line number, and/or entire line etc.) to a file f.i.
(I don't understand what you mean by 'changes', from what I understood, lines from the .csv will be inserted to a dataset, hence all changes will be inserts.)
edit: To be able to discuss on something concrete (I'm having a hard time grasping the task :))
Sample data (part of CodeGear sample 'Clients.cds'):
Davis;Jennifer;1023495,0000;100
Cranberry
St.;Wellesley;MA;02181;516-292-3945;01.01.93
Jones;Arthur;2094056,0000;10 Hunnewell
St;Los
Altos;CA;94024;415-941-4321;07.02.81
Parker;Debra;1209395,0000;74 South
St;Atherton;CA;98765;916-213-2234;23.10.90
Sawyer;Dave;3094095,0000;101 Oakland
St;Los
Altos;CA;94022;415-948-9998;21.12.89
White;Cindy;1024034,0000;1 Wentworth
Dr;Los
Altos;CA;94022;415-948-6547;01.10.92
procedure TForm1.FormCreate(Sender: TObject);
begin
CDS.FieldDefs.Add('LAST_NAME', ftString, 20);
CDS.FieldDefs.Add('FIRST_NAME', ftString, 20);
CDS.FieldDefs.Add('ACCT_NBR', ftInteger);
CDS.FieldDefs.Add('ADDRESS_1', ftString, 30);
CDS.FieldDefs.Add('CITY', ftString, 15);
CDS.FieldDefs.Add('STATE', ftString, 2);
CDS.FieldDefs.Add('ZIP', ftString, 5);
CDS.FieldDefs.Add('TELEPHONE', ftString, 12);
CDS.FieldDefs.Add('DATE_OPEN', ftDate);
CDS.CreateDataSet;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
csv: TextFile;
Rec: string;
Fields: TStringList;
LineNo: Integer;
i: Integer;
begin
Fields := TStringList.Create;
try
Fields.StrictDelimiter := True;
Fields.Delimiter := ';';
AssignFile(csv, ExtractFilePath(Application.ExeName) + 'clients.csv');
try
Reset(csv);
LineNo := 0;
while not Eof(csv) do begin
Inc(LineNo);
Readln(csv, Rec);
Fields.DelimitedText := Rec;
CDS.Append;
for i := 0 to Fields.Count - 1 do
try
CDS.Fields[i].Value := Fields[i]; // Variant conversion will raise
// exception where conversion from string fails
except
on E:EDatabaseError do begin
CDS.Cancel; // Failed, discard the record
// log the error instead of showing a message
ShowMessage(Format('Cannot set field "%s" at line %d' + sLineBreak +
'Error: %s', [CDS.Fields[i].FieldName, LineNo, E.Message]));
Break; // Continue with next record
end;
end;
if CDS.State = dsInsert then // It's not dsInsert if we Cancelled the Insert
try
CDS.Post;
except
on E:EDatabaseError do begin
// log error instead of showing
ShowMessage(Format('Cannot post line %d' + sLineBreak + 'Error: %s',
[LineNo, E.Message]));
CDS.Cancel;
end;
end;
end;
finally
CloseFile(csv);
end;
finally
Fields.Free;
end;
end;
procedure TForm1.CDSBeforePost(DataSet: TDataSet);
begin
// Superficial posting error
if CDS.FieldByName('LAST_NAME').AsString = '' then
raise EDatabaseError.Create('LAST_NAME cannot be empty');
end;
AFAIK, there's no direct way to load .csv data into a TClientDataset.
The easiest way I can think of would be to use the TTextDataSet (found in Demos\Delphi\Database\TextData, available from Start->All Programs->Embarcadero RAD Studio XE->Samples). You can use it just like any other TDataSet, meaning you can read from it's Fields or use FieldByName, and it supports Bof, Eof, Next, and Prior.
You can simply iterate through and try to assign to your CDS columns, and it will generate errors you can then handle or log.
You can install TTextDataset like any other component, or just add the unit to the uses clause and create it at runtime. There's a readme.htm file in the folder that doesn't explain much; the key properties are FileName and Active. :)
It includes both a pre-designed package (TextPkg.dproj) and a test app (TextTest.dproj). There's also a project group (TextDataGroup.groupproj) - you can simply open this in the IDE, build and install the TextPkg package, and then compile and run the test app. The source for the test app shows usage pretty well.
In the off-chance that your database is DBISAM, you can simply use the IMPORT SQL statement.
import table "tablename" from "myinputfile.csv" Delimiter ',';
Other databases may have a similar feature.

OLE Automation: How do i copy text between Word documents without using the clipboard

While doing som Word automation from Delphi XE, I have two documents open simultaneously. I want to copy the contents of a given range of one document to another range in the other document. How can I do this?
Consider the following code:
procedure TForm1.ManipulateDocuments;
var
vDoc1,vDoc2 : TWordDocument;
vFilename : olevariant;
vRange1,vRange2 : Range;
begin
vDoc1 := TWordDocument.Create(nil);
vDoc2 := TWordDocument.Create(nil);
try
vFilename := 'c:\temp\test1.doc';
vDoc1.ConnectTo(FWordApp.Documents.Open(vFilename,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam));
vFilename := 'c:\temp\test2.doc';
vDoc2.ConnectTo(FWordApp.Documents.Open(vFilename,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam,EmptyParam));
vRange1 := GetSourceRange(vDoc1);
vRange2 := GetDestinationRange(vDoc2);
vRange2.CONTENTS := vRange1.CONTENTS; //What should I substitute for CONTENTS?
finally
vDoc1.Free;
vDoc2.Free;
end;
end;
Is there something I could substitute for CONTENTS? I can't use text, since I want to copy formatting, bookmarks, field codes etc. Do I have to do it another way alltogether? Any suggestions?
I don't know a way for earlier versions of Word, but for newer versions (2007 and up) you can export a range from a document to a fragment file, and then import it from another document. If you want early binding, you might need to import the type library (msword.olb), I don't know if Delphi XE has it. Otherwise the code might look like this:
function GetTempFileName(Prefix: string): string;
begin
SetLength(Result, MAX_PATH);
GetTempPath(MAX_PATH, PChar(Result));
windows.GetTempFileName(PChar(Result), PChar(Prefix), 0, PChar(Result));
end;
procedure TForm2.Button1Click(Sender: TObject);
const
// wdFormatDocument = 0;
wdFormatRTF = $00000006;
var
WordApp : OleVariant;
fragment: string;
vDoc1, vDoc2: OleVariant;
vRange1, vRange2: OleVariant;
begin
try
WordApp := GetActiveOleObject('Word.Application');
except
WordApp := CreateOleObject('Word.Application');
end;
WordApp.Visible := True;
vDoc1 := WordApp.Documents.Open(ExtractFilePath(Application.ExeName) + 'test1.doc');
vRange1 := vDoc1.Range(20, 120); // the export range
fragment := GetTempFileName('frg');
vRange1.ExportFragment(fragment, wdFormatRTF);
try
vDoc2 := WordApp.Documents.Open(ExtractFilePath(Application.ExeName) + 'test2.doc');
vRange2 := vDoc2.Range(15, 15); // where to import
vRange2.ImportFragment(fragment);
finally
DeleteFile(fragment);
end;
end;
With my test, 'document' format threw an error (something like not being able to insert XML formatting), hence usage of RTF format.
edit:
With earlier versions, it seems to be possible to insert a named selection from one document to a selection in another document. The result seems not to be perfect regarding formatting if one of the selections happens to be in the middle of some text. But otherwise it seems to be working good.
...
WordApp.Visible := True;
vDoc1 := WordApp.Documents.Open(ExtractFilePath(Application.ExeName) + 'test1.doc');
vRange1 := vDoc1.Range(20, 188); // the transfer range
vDoc1.Bookmarks.Add('TransferSection', vRange1); // arbitrary bookmark name
vDoc2 := WordApp.Documents.Open(ExtractFilePath(Application.ExeName) + 'test2.doc');
vRange2 := vDoc2.Range(103, 104); // where to import the bookmark
vRange2.Select;
vDoc2.ActiveWindow.Selection.InsertFile(vDoc1.FullName, 'TransferSection');
vDoc1.Bookmarks.Item('TransferSection').Delete; // no need for the bookmark anymore
 
If you can use the Office Open XML-format (ie. the docx file format that was introduced in Word 2007), then you can do this without automation.
Word versions prior to 2007 must install a compatibility pack which will enable docx-files for Word 2003, 2002 and 2000.
The docx-file is actually a zip-file that contains several xml-files. Try to change the extension of a docx-file from .docx to .zip and open this file in eg. WinZip.
So... Unzip docx-file and grab the xml-part you need. As pure string or as a xml document. Then you can inject this xml-part into the other docx-file. You need to know where in the xml-structure to grab/insert the xml, though. This will depend on how well you know the document structure and how much editing the user is allowed to do in the document.
I don't know how Word will handle duplicate bookmark names etc with this approach.
It seems I found the canonical solution to this question while digged into similar problem. The FormattedText property of Range object is the exact what do you need. Just use:
vRange2.FormattedText := vRange1;
and the contents of vRange1 will be copied into vRange2. Also, this works too:
vRange2 := vRange1;
Though, the second statement doesn't copy the formatting.
Why not use the clipboard? If all the text is selected in vDoc1, then to copy this to the clipboard involves one simple call: vDoc1.copy. Similarly, copying the contents of the clipboard to the second document requires one simple call: vDoc2.paste. The clipboard buffer will hold all the formatting information.

Resources