I have code which starts Word application instance as follows
Microsoft.Office.Interop.Word.Application app = new Microsoft.Office.Interop.Word.Application();
app.Caption = "abcd_" + DateTime.Now.Ticks.ToString();
I now need to know process id of the word instance that was started.
I cannot use FindWindow to get window handle and GetWindowThreadProcessId to get process id from handle as the code does not work on Windows server 2008.
I get all word processes by using Process.GetProcessesByName("WINWORD").
Is there any property of Process which can give me value that we set in app.Caption ?
If not, is there any other property of Word.Application which I can set and later on read from Process array to identify correct instance of Word ?
You Could use the Process.MainWindowTitle property to judge if the process is by your code.
But there's some limitation:
When you using new Microsoft.Office.Interop.Word.Application(), the word windows is not visible by default. When it's hidden, the Process.MainWindowTitle is empty. So you show set it visible before you get the Pid.
After you open a document, the MainWindowTitle will change to the document's file name.
Here's my code:
static void Main(string[] args)
{
string uniCaption = Guid.NewGuid().ToString();
Word.Application oWordApp = new Word.Application();
oWordApp.Caption = uniCaption;
oWordApp.Visible = true;
Process pWord = getWordProcess(uniCaption);
//If you don't want to show the Word windows
oWordApp.Visible = false;
//Do other things like open document etc.
}
static Process getWordProcess(string pCaption)
{
Process[] pWords = Process.GetProcessesByName("WINWORD");
foreach (Process pWord in pWords)
{
if (pWord.MainWindowTitle == pCaption)
{
return pWord;
}
}
return null;
}
What about that (untestet):
Updated
Word.Application wdapp;
try
{
Process wordProcess = System.Diagnostics.Process.Start("Path to WINWORD.EXE");
wdApp = (Word.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Word.Application");
}
catch (COMException)
{
Type type = Type.GetTypeFromProgID("Word.Application");
wdapp = System.Activator.CreateInstance(type);
}
The wdApp should be the started word.
And you get the process id through wordProcess instance.
Related
I need to show a PDF file in a browser, by pressing a button in my C# app.
So the browser is eternal to my app.
On Win 10 I do it like this:
....
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = FindBrowser(),
Arguments = $"\"file:///{PDF_File}\""
}
process.Start();
....
private static string FindBrowser()
{
GroupCollection groups;
try
{
// finding the default browser in the registry is a two-step process:
using var key = Registry.CurrentUser.OpenSubKey(
#"SOFTWARE\Microsoft\Windows\Shell\Associations\URLAssociations\http\UserChoice");
var s = (string) key?.GetValue("ProgId");
using var command = Registry.ClassesRoot.OpenSubKey($"{s}\\shell\\open\\command");
var browserCommand = (string) command?.GetValue(null);
var regexpr = new Regex("^\"([^\"]*)\"");
groups = regexpr.Match(browserCommand).Groups;
if (!File.Exists(groups[1].Value))
{
throw new FileNotFoundException("NoBrowser");
}
}
but my customer says this does not work on Win 11 (I have no Win 11)
Is there a foolproof way to program this?
I use VS 2019 myself to develop and debug.
My solution works OK there.
So basically my app triggers an excel macro, from a file, that updates the file and then closes it.
When I open the file I set the "DisplayAlerts = false" variable in order to ignore all popups and it works as expected in my computer... however, a colleague of mine tried to use it and for every file, he gets the popup asking if he wants to save all changes...
Checked other questions about the popups in excel but all suggested solutions use "oBook.Saved = true;" or "oBook.Close(false);", but these did not work for me.
my code is as follows:
using Microsoft.Office.Interop.Excel;
public static bool Trigger_Macro_From_File(string path)
{
ApplicationClass oExcel = null;
Workbook oBook = null;
try
{
string filename = Path.GetFileName(path);
string macro_name = "!some_macro";
string macro = #"'" + filename + #"'" + macro_name;
// Create an instance of Microsoft Excel
oExcel = new ApplicationClass
{
DisplayAlerts = false,
Visible = false
};
oBook = oExcel.Workbooks.Open(path);
RunMacro(oExcel, new Object[] { macro });
oBook.Save();
oBook.Saved = true;
return true;
}
catch (Exception ex)
{
return false;
}
finally
{
oBook?.Close(false);
System.Runtime.InteropServices.Marshal.ReleaseComObject(oBook);
oBook = null;
oExcel?.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(oExcel);
oExcel = null;
GC.Collect();
}
}
Does anyone know anything about this?
Thanks in advance.
You could double-check that no other "Microsoft Excel" process is running in the Task Manager.
Let's say at some point in your development process you started your program and open the workbook with something like
xlWorkbook = xlApp.Workbooks.Open(filePath);
Then you encountered an exception for some reason, and killed the program without closing the file properly (workbook.Close(..), app.Quit(..) and so on).
The Microsoft Excel process is still running in the background, and has a handle on the file you want to edit. So you cannot execute an instruction that saves the file under the same name. This is why the popup is appearing.
This scenario is taken from the point of view of the developer, but the same behavior could have happened on your coworker's computer if your app crashed without quitting properly, and gets re-started.
Also, be careful that finally statement might not always be executed, so double-check which scenario could cause your app to close without releasing the COM object.
I'm writing a C# app that goes through a list of Word documents and changes their attached template using Interop. The idea is that if a bunch of Word documents are pointing to templates on a server that doesn't exist, we can use string replace to change the templates' paths to the correct ones.
Note that each document may have one of several different templates, and therefore I can't just change all of them to a specific path - I must use string replace.
My problem is that my Interop.Word.Document object doesn't return the correct attached template. When I open a Word document in MS Word and go to the templates window, I see that the attached template is a network file template that doesn't exist anymore. That's the path I want to change. But when I use get_AttachedTemplate() in C#, I get the path to another template...
Can anyone help me?
Note: I'm using Microsoft Word 16.0 Object Library.
using System;
using System.IO;
using System.Reflection;
using Microsoft.Office.Interop.Word;
using App = Microsoft.Office.Interop.Word.Application;
using Doc = Microsoft.Office.Interop.Word.Document;
namespace OfficeTemplateCleaner
{
internal class Program
{
private static void Main(string[] args)
{
Console.Write(#"Enter a string to search (e.g. \\old\path\): ");
var stringToFind = Console.ReadLine();
Console.Write($"Enter a string to replace \"{stringToFind}\": ");
var replaceWith = Console.ReadLine();
var files = new[]
{
#"\\path\to\one\test\document.doc"
};
App oWord = new App() { Visible = false };
foreach (var file in files)
{
if (!File.Exists(file))
{
continue;
}
// cache file modification times so that we can revert them to what they were originally
var fileInfo = new FileInfo(file);
var modifyTime = fileInfo.LastWriteTime;
//OBJECT OF MISSING "NULL VALUE"
object oMissing = Missing.Value;
//OBJECTS OF FALSE AND TRUE
object oTrue = true;
object oFalse = false;
//CREATING OBJECTS OF WORD AND DOCUMENT
Doc oWordDoc = new Doc();
//MAKING THE APPLICATION VISIBLE
oWord.Visible = true;
//ADDING A NEW DOCUMENT TO THE APPLICATION
oWordDoc = oWord.Documents.Open(file);
var templ = (Template) oWordDoc.get_AttachedTemplate();
var template = templ.FullName;
if (template.IndexOf(stringToFind) == -1)
{
continue;
}
var newTemplate = template.Replace(stringToFind, replaceWith);
oWordDoc.set_AttachedTemplate(newTemplate);
oWordDoc.SaveAs(file);
oWordDoc.Close();
fileInfo = new FileInfo(file);
fileInfo.LastWriteTime = modifyTime;
}
oWord.Quit();
Console.WriteLine("Done.");
Console.ReadLine();
}
}
}
This is a common issue with the Word interop: if the template location is not available then it's not possible to access this value. Word returns the template it can actually "reach" which by default will usually be Normal.dotm if the attached template cannot be found.
More reliable would be to work with the closed file via the Open XML SDK (it will be much faster, as well). Here's some code that will access the current information and change it. You will, of course, need to add the logic for which file is to be set as the new attached template.
string filePath = #"C:\Test\DocCopyTest.docm";
using (WordprocessingDocument pkgDoc = WordprocessingDocument.Open(filePath, true))
{
//Gets only the file name, not path; included for info purposes, only
DocumentFormat.OpenXml.ExtendedProperties.Template t = pkgDoc.ExtendedFilePropertiesPart.Properties.Template;
string tName = t.Text;
this.txtMessages.Text = tName;
//The attached template information is stored in the DocumentSettings part
//as a link to an external resource. So the information is not directly in the XML
//it's part of the "rels", meaning it has to be accessed indirectly
MainDocumentPart partMainDoc = pkgDoc.MainDocumentPart;
ExternalRelationship r = null;
string exRel_Id = "";
string exRelType = "";
//the file location of the new template, as a URI
Uri rUri = new Uri("file:////C:/Test/DocCopy_Test2.dotm");
Array exRels = partMainDoc.DocumentSettingsPart.ExternalRelationships.ToArray();
foreach (ExternalRelationship exRel in exRels)
if (exRel.RelationshipType.Contains("attachedTemplate"))
{
exRel_Id = exRel.Id;
exRelType = exRel.RelationshipType;
System.Diagnostics.Debug.Print(exRel_Id + " " + exRelType);
partMainDoc.DocumentSettingsPart.DeleteExternalRelationship(exRel);
r = partMainDoc.DocumentSettingsPart.AddExternalRelationship(exRelType, rUri, exRel_Id);
System.Diagnostics.Debug.Print(r.Uri.ToString());
break;
}
}
Following on from this thread Starting application before target application
I have an application which gets passed a parameter (a filename) and does some registry work before opening Microsoft InfoPath.
I need to open InfoPath with the parameter that was passed to the original application.
Here is how I open InfoPath
System.Diagnostics.Process prc = new System.Diagnostics.Process();
prc.StartInfo.Arguments = ConvertArrayToString(Constants.Arguments);
//prc.StartInfo.Arguments = "hello";
prc.StartInfo.FileName = Constants.PathToInfoPath;
prc.Start();
Note that when I set the Arguments to "hello" InfoPath pops up a message saying cannot find file "hello" however when I set it Constants.Arguments I get an error and Windows asks me if I want to debug or close the applicatiion.
Here is how I set Constants.Arguments in the Main(string[] args)
static void Main(string[] args)
{
Constants.Arguments = args;
//...
}
And here is ConvertArrayToString
private string ConvertArrayToString(string[] arr)
{
string rtn = "";
foreach (string s in arr)
{
rtn += s;
}
return rtn;
}
I suppose the format of the parameter is causing the error, any idea why?
The value of Arguments after being stringed is
c:\users\accountname\Desktop\HSE-000403.xml
Edit:
Thanks to N K's answer.
The issue is in order for my application to open when InfoPath files are opened, I have changed the name of INFOPATH.EXE to INFOPATH0.EXE and my application is called INFOPATH.EXE and is in the InfoPath folder, so when files are opened my application opens.
Now when I do not change the name (eg I leave it as INFOPATH.EXE) it works as expected, however if it is called anything other than that then I get the error.
Unfortunately I need my application to open first.
I tried the below and it's works fine. Let me know what you get with this. (Don't forget to change path to files)
class Program
{
static void Main(string[] args)
{
System.Diagnostics.Process prc = new System.Diagnostics.Process();
prc.StartInfo.Arguments = string.Join("", Constants.Arguments);
prc.StartInfo.FileName = Constants.PathToInfoPath;
prc.Start();
}
}
public class Constants
{
public static string PathToInfoPath = #"C:\Program Files (x86)\Microsoft Office\Office14\INFOPATH.EXE";
public static string[] Arguments = new string[] { #"c:\users\accountname\Desktop\HSE-000403.xml" };
}
When i started my app and opened word file i see 3 process winword.exe in taskmanager.
After i called 'close' function 1 process winword.exe closed.
When i called worddoc.close() or wordapp.quit() in destructor, i got exception "COM object that has been separated from its underlying RCW cannot be used."
public class WordHelper
{
private object nullobj = System.Reflection.Missing.Value;
public string context = "";
Microsoft.Office.Interop.Word.Document doc = new Document();
Microsoft.Office.Interop.Word.Application wordApp = new Application();
public WordHelper(string FileName)
{
//Open word file
}
//somefunction fo work with file
public void CloseWord()
{
doc.Close();
wordApp.Quit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(wordApp);
}
~WordHelper()
{
//i got exception
doc.Close();
wordApp.Quit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(wordApp);
}
}
How i call my class
WordHelper wddoc = new WordHelper("C:\\Test Word\\Test.docx");
wddoc.CloseWord(); //this line i use and can close 1 process not 3
//One process close after i close application
At the end i want close all winword.exe which was opened by my application, and i want to close them in destructor. At the end, i need to close all 'winword.exe' which was opened by my application, and i need to close them in destructor.
You are doing this in a finalizer (different than a destructor). Finalizers are non-deterministic, meaning by the time they run class members may have already been finalized and thus would no longer be valid.
I would implement the Dispose pattern and explicitly control the lifetime of your word COM objects. This answer has a lot of good links that may help you.
//To find out the WINWORD.exe process and set RealTime Priority to WINWORD.exe
//This is used to quick process
Process[] proces = Process.GetProcessesByName("WINWORD");
foreach (Process proc in proces)
{
proc.PriorityClass = ProcessPriorityClass.RealTime;
}