3
votes

I have a C# WPF .NET 4.6 program that creates HTML files and I would like to print them automatically (without dialog) with a known, non-default printer. Of course this includes rendering the HTML first. Since the program creates these files, the HTML data can come from a MemoryStream, FileStream, or directly from string.

The program has settings that allows the user to specify which printer to print to in advance, using System.Drawing.Printing.PrinterSettings.InstalledPrinters, as each file may require a different printer. At printing time, the printer's name is known, but will likely be different from the Windows default printer.

I have researched a lot of other projects, but they do not seem to account for the printer being different from the default. Changing the default printer would be anti-social and would cause a world of threading-related pain. This seems to be the #1 accepted solution, though cannot be the best solution??


Research and solutions looked at:

Printing the contents of a WPF WebBrowser and Silently print HTML from WPF WebBrowser and corresponding MSDN forum discussions are insufficient as the COM ExecWB function only prints to the default printer (?)

The MSDN example only uses the Print() command on the WebBrowser, which again uses the default printer.

So I went down the route of trying to change the printer options. Programmatically changing the destination printer for a WinForms WebBrowser control was asked, but has a rather unsatisfactory answer, as it has a broken link, and I don't know what external programs the running computer has, so I cannot guarantee Adobe, OpenOffice, etc. OP mentioned that they parsed the ActiveX COM without going into detail. Sounds tricky.

Perhaps I could gleam something from writing to a RichTextBox like this project does, and hide the box?

I thought Silent print HTML file in C# using WPF is on a good path, however the original post has hard-coded numbers for screen dimensions‽ and OP mentioned that the printer cut off the document. The accepted (and bountied) answer again use the ExecWB default printer settings method.

execCommand("Print", false, IDon'tUnderstandThisArgument) also showed promise, as its answer was an updated MSDN answer, but the filestream sent to the printer does not allow HTML, nor does the DocumentStream from WebBrowser appear to work (the printer prints a single blank page).

How do I programatically change printer settings with the WebBrowser control? has very similar requirements to me, except changes the registry as a solution.

Other than researching how others have done it, I also tried printing the WPF WebBrowser directly as it is a Visual control:

    public static bool Print(string printer, Visual objToPrint)
    {
      if (string.IsNullOrEmpty(printer))
      {
        return false;
      }

      var dlg = new PrintDialog
      {
        PrintQueue = new PrintServer().GetPrintQueue(printer)
      };

      dlg.PrintTicket.CopyCount = 1;
      dlg.PrintTicket.PageOrientation = PageOrientation.Portrait;
      dlg.PrintTicket.PagesPerSheet = 1;

      dlg.PrintVisual(objToPrint, "Print description");
      return true;
    }

however this does not print anything (because the WebBrowser is not visible?). And tried a PrintDocument as the updated MSDN article suggested:

    public static async Task<bool> PrintHTMLAsync(string printer, string html)
    {
      bool result;
      using (var webBrowser = new System.Windows.Forms.WebBrowser())
      {
        webBrowser.DocumentCompleted += ((sender, e) => browserReadySemaphore.Release());
        byte[] buffer = Encoding.UTF8.GetBytes(html);
        webBrowser.DocumentStream = new MemoryStream(buffer);

        // Wait until the page loads.
        await browserReadySemaphore.WaitAsync();

        try
        {
          using (PrintDocument pd = new PrintDocument())
          {
            pd.PrinterSettings.PrinterName = printer;
            pd.PrinterSettings.Collate = false;
            pd.PrinterSettings.Copies = 1;
            pd.PrinterSettings.FromPage = 1;
            pd.PrinterSettings.ToPage = 1;
            pd.Print();
            result = true;
          }
        }
        catch (Exception ex)
        {
          result = false;
          Debug.WriteLine(ex);
        }

        return result;
      }
    }

with no joy.

I've also used the PRINT DOS command:

public static string PerformSilentPrinting(string fileName, string printerName)
{
  try
  {
    ProcessStartInfo startInfo = new ProcessStartInfo(fileName)
    {
      Arguments = string.Format("/C PRINT /D:\"{0}\" \"{1}\"", printerName, fileName),
      FileName = "cmd.exe",
      RedirectStandardOutput = true,
      UseShellExecute = false,
      WindowStyle = ProcessWindowStyle.Hidden,
    };

    // Will execute the batch file with the provided arguments
    Process process = Process.Start(startInfo);

    // Reads the output        
    return process.StandardOutput.ReadToEnd();
  }
  catch (Exception ex)
  {
    return ex.ToString();
  }
}

but the print command seems to only accept text files.

2

2 Answers

1
votes

The best solution i found was here

This takes care of printing silently. However that doesn't allow you to change from the default printer. So as a workaround, you can set the default printer to the printer you'd like to use, and then switch back once you're done printing.

I'll be something like this:

protected static class PrinterSetter
    {
        [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
        public static extern bool SetDefaultPrinter(string Name);
    }

then in the code referenced in the link above you can add:

if (wb != null)
            {
                PrinterSettings settings = new PrinterSettings();
                string defaultPrinter = settings.PrinterName;

                Printer.SetDefaultPrinter("Microsoft Print to PDF");

                wb.ExecWB(OLECMDID_PRINT, OLECMDEXECOPT_DONTPROMPTUSER, null, null);


                (new System.Action(() =>
                {
                    Thread.Sleep(5000);
                    Printer.SetDefaultPrinter(defaultPrinter);
                })).BeginInvoke(null, null);

            }
0
votes

EDIT: This solution works well if you only need to print one page of A4. However it will only print one page, and truncates anything past it.


In the end I went with a WinForms WebBrowser, copied the control into a Bitmap, and printed using PrintDialog, which is also in the System.Windows.Forms namespace.

using Microsoft.Win32;
using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Printing;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

public static class PrintUtility
{
  private static readonly SemaphoreSlim browserReadySemaphore = new SemaphoreSlim(0);

  // A4 dimensions.
  private const int DPI = 600;
  private const int WIDTH = (int)(8.3 * DPI);
  private const int HEIGHT = (int)(11.7 * DPI);

  public static void Print(this Image image, string printer, bool showDialog = false)
  {
    if (printer == null)
    {
      throw new ArgumentNullException("Printer cannot be null.", nameof(printer));
    }

    using (PrintDialog printDialog = new PrintDialog())
    {
      using (PrintDocument printDoc = new PrintDocument())
      {
        printDialog.Document = printDoc;
        printDialog.Document.DocumentName = "My Document";
        printDialog.Document.OriginAtMargins = false;
        printDialog.PrinterSettings.PrinterName = printer;

        printDoc.PrintPage += (sender, e) => 
        {
          // Draw to fill page
          e.Graphics.DrawImage(image, 0, 0, e.PageSettings.PrintableArea.Width, e.PageSettings.PrintableArea.Height);

          // Draw to default margins
          // e.Graphics.DrawImage(image, e.MarginBounds);
        };

        bool doPrint = !showDialog;
        if (showDialog)
        {
          var result = printDialog.ShowDialog();
          doPrint = (result == DialogResult.OK);
        }

        if (doPrint)
        {
          printDoc.Print();
        }
      }
    }
  }

  public static async Task<bool> RenderAndPrintHTMLAsync(string html, string printer)
  {
    bool result = false;

    // Enable HTML5 etc. (assuming we're running IE9+)
    SetFeatureBrowserFeature("FEATURE_BROWSER_EMULATION", 9000);

    // Force software rendering
    SetFeatureBrowserFeature("FEATURE_IVIEWOBJECTDRAW_DMLT9_WITH_GDI", 1);
    SetFeatureBrowserFeature("FEATURE_GPU_RENDERING", 0);

    using (var webBrowser = new WebBrowser())
    {
      webBrowser.ScrollBarsEnabled = false;
      webBrowser.Width = WIDTH;
      webBrowser.Height = HEIGHT;
      webBrowser.DocumentCompleted += ((s, e) => browserReadySemaphore.Release());
      webBrowser.LoadHTML(html);

      // Wait until the page loads.
      await browserReadySemaphore.WaitAsync();

      // Save the picture
      using (var bitmap = webBrowser.ToBitmap())
      {
        bitmap.Save("WebBrowser_Bitmap.bmp");
        Print(bitmap, printer);
        result = true;
      }
    }

    return result;
  }

  /// <summary>
  /// Make a Bitmap from the Control.
  /// Remember to dispose after.
  /// </summary>
  /// <param name="control"></param>
  /// <returns></returns>
  public static Bitmap ToBitmap(this Control control)
  {
    Bitmap bitmap = new Bitmap(control.Width, control.Height);
    Rectangle rect = new Rectangle(0, 0, control.Width, control.Height);
    control.DrawToBitmap(bitmap, new Rectangle(0, 0, control.Width, control.Height));
    return bitmap;
  }

  /// <summary>
  /// Required because of a bug where the WebBrowser only loads text once or not at all.
  /// </summary>
  /// <param name="webBrowser"></param>
  /// <param name="htmlToLoad"></param>
  /// <remarks>
  /// http://stackoverflow.com/questions/5362591/how-to-display-the-string-html-contents-into-webbrowser-control/23736063#23736063
  /// </remarks>
  public static void LoadHTML(this WebBrowser webBrowser, string htmlToLoad)
  {
    webBrowser.Document.OpenNew(true);
    webBrowser.Document.Write(htmlToLoad);
    webBrowser.Refresh();
  }

  /// <summary>
  /// WebBrowser Feature Control
  /// </summary>
  /// <param name="feature"></param>
  /// <param name="value"></param>
  /// <remarks>
  /// http://stackoverflow.com/questions/21697048/how-to-fix-a-opacity-bug-with-drawtobitmap-on-webbrowser-control/21828265#21828265
  /// http://msdn.microsoft.com/en-us/library/ie/ee330733(v=vs.85).aspx
  /// </remarks>
  private static void SetFeatureBrowserFeature(string feature, uint value)
  {
    if (LicenseManager.UsageMode != LicenseUsageMode.Runtime)
    {
      return;
    }

    var appName = Path.GetFileName(System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName);
    Registry.SetValue(
      @"HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Main\FeatureControl\" + feature,
      appName, 
      value, 
      RegistryValueKind.DWord);
  }
}