How to print a docx to a specific printer using Microsoft.Office.Interop.Word.Document.PrintOut()

20,260

Solution 1

TL;DR You can't print to a specific printer. You have to change the default printer to what you want to use. Then print as normal.

It may be that things have changed dramatically since we first did our work in this area, but we were unable to find any way to print to a specific printer. So what we did in place of that was change the system's default printer to what we wanted, print all the documents that we wanted on that computer, then change it back to what it was before. (actually, we quit changing it back because nothing else was expecting any particular default printer, so it didn't matter).

Short answer:

Microsoft.Office.Interop.Word._Application _app = [some valid COM instance];
Microsoft.Office.Interop.Word.Document doc = _app.Documents.Open(ref fileName, ...);
doc.Application.ActivePrinter = "name of printer";
doc.PrintOut(/* ref options */);

But, we found this highly unreliable! Read on for more details:

If you haven't done so already, I heavily suggest building your own wrapper classes to deal with all the mundane work bits of _Document and _Application. It may not be as bad now as it was then (dynamic was not an option for us), but it's still a good idea. You'll note that certain delicious bits of code are absent from this ... I tried to focus on the code that is related to what you are asking. Also, this DocWrapper class is a merging of many separate parts of the code - forgive the disorder. Lastly, if you consider the exception handling odd (or just poor design by throwing Exception) -- remember I'm trying to put together parts of code from many places (while also leaving out our own custom types). Read the comments in the code, they matter.

class DocWrapper
{
  private const int _exceptionLimit = 4;

  // should be a singleton instance of wrapper for Word
  // the code below assumes this was set beforehand
  // (e.g. from another helper method)
  private static Microsoft.Office.Interop.Word._Application _app;

  public virtual void PrintToSpecificPrinter(string fileName, string printer)
  {
    // Sometimes Word fails, so needs to be restarted.
    // Sometimes it's not Word's fault.
    // Either way, having this in a retry-loop is more robust.
    for (int retry = 0; retry < _exceptionLimit; retry++)
    {
      if (TryOncePrintToSpecificPrinter(fileName, printer))
        break;

      if (retry == _exceptionLimit - 1) // this was our last chance
      {
        // if it didn't have actual exceptions, but was not able to change the printer, we should notify somebody:
        throw new Exception("Failed to change printer.");
      }
    }
  }

  private bool TryOncePrintToSpecificPrinter(string fileName, string printer)
  {
    Microsoft.Office.Interop.Word.Document doc = null;

    try
    {
      doc = OpenDocument(fileName);

      if (!SetActivePrinter(doc, printer))
        return false;

      Print(doc);

      return true; // we did what we wanted to do here
    }
    catch (Exception e)
    {
      if (retry == _exceptionLimit)
      {
        throw new Exception("Word printing failed.", e);
      }
      // restart Word, remembering to keep an appropriate delay between Quit and Start.
      // this should really be handled by wrapper classes
    }
    finally
    {
      if (doc != null)
      {
        // release your doc (COM) object and do whatever other cleanup you need
      }
    }

    return false;
  }

  private void Print(Microsoft.Office.Interop.Word.Document doc)
  {
    // do the actual printing:
    doc.Activate();
    Thread.Sleep(TimeSpan.FromSeconds(1)); // emperical testing found this to be sufficient for our system
    // (a delay may not be required for you if you are printing only one document at a time)
    doc.PrintOut(/* ref objects */);
  }

  private bool SetActivePrinter(Microsoft.Office.Interop.Word.Document doc, string printer)
  {
    string oldPrinter = GetActivePrinter(doc); // save this if you want to preserve the existing "default"

    if (printer == null)
      return false;

    if (oldPrinter != printer)
    {
      // conditionally change the default printer ...
      // we found it inefficient to change the default printer if we don't have to. YMMV.
      doc.Application.ActivePrinter = printer;
      Thread.Sleep(TimeSpan.FromSeconds(5)); // emperical testing found this to be sufficient for our system
      if (GetActivePrinter(doc) != printer)
      {
        // don't quit-and-restart Word, this one actually isn't Word's fault -- just try again
        return false;
      }

      // successful printer switch! (as near as anyone can tell)
    }

    return true;
  }

  private Microsoft.Office.Interop.Word.Document OpenDocument(string fileName)
  {
    return _app.Documents.Open(ref fileName, /* other refs */);
  }

  private string GetActivePrinter(Microsoft.Office.Interop.Word._Document doc)
  {
    string activePrinter = doc.Application.ActivePrinter;
    int onIndex = activePrinter.LastIndexOf(" on ");
    if (onIndex >= 0)
    {
      activePrinter = activePrinter.Substring(0, onIndex);
    }
    return activePrinter;
  }
}

Solution 2

There's a way to specify the printer, but don't set it as system default (I use C++/CLR, but it should be portable to C#):

String^ printername = "...";
auto wordapp= gcnew Microsoft::Office::Interop::Word::Application();
if (wordapp != nullptr)
{
  cli::array<Object^>^ argValues= gcnew cli::array<Object^>(2);
  argValues[0]= printername;
  argValues[1]= 1;
  cli::array<String^>^ argNames= gcnew cli::array<String^>(2);
  argNames[0]= "Printer";
  argNames[1]= "DoNotSetAsSysDefault";
  Object^ wb= wordapp->WordBasic;
  wb->GetType()->InvokeMember( "FilePrintSetup", System::Reflection::BindingFlags::InvokeMethod, nullptr, wb, argValues, nullptr, nullptr, argNames);
}

Solution 3

I inherited a C# project that used Word.Application. Since it was a C# project and the translation seemed pretty straightforward I used @Lars C++ code and added a C# translated method to my project. Posting the direct translation of his code here in the hopes of making someone's life a little easier:

string printername = "...";
var wordapp = new Microsoft.Office.Interop.Word.Application();
if (wordapp != null)
{
    object[] argValues = new object[2];
    argValues[0] = printername;
    argValues[1] = 1;
    string[] argNames = new string[2];
    argNames[0] = "Printer";
    argNames[1] = "DoNotSetAsSysDefault";
    var wb = wordapp.WordBasic;
    wb.GetType().InvokeMember("FilePrintSetup", System.Reflection.BindingFlags.InvokeMethod, null, wb, argValues, null, null, argNames);
}
Share:
20,260
andrew
Author by

andrew

Software dev working in state govt. Doing asp.net work atm.

Updated on July 09, 2022

Comments

  • andrew
    andrew almost 2 years

    This seems like such a simple need, but for some reason I cannot find how I can accomplish this. I have code like this:

    Microsoft.Office.Interop.Word.Application word = new Microsoft.Office.Interop.Word.Application();
    MemoryStream documentStream = getDocStream();
    FileInfo wordFile = new FileInfo("c:\\test.docx");
    object fileObject = wordFile.FullName;
    object oMissing = System.Reflection.Missing.Value;
    Microsoft.Office.Interop.Word.Document doc = wordInstance.Documents.Open(ref fileObject, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing);
    doc.Activate();
    doc.PrintOut(oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing, oMissing);
    

    I need to have a config setting drive which printer and tray are used. After searching around I found Microsoft.Office.Interop.Word.Application.ActivePrinter which is a settable string property that the documentation says takes "the name of the active printer", but I don't know what it means for a printer to the the "Active Printer", especially when I have two of them. How can this be accomplished?