0

I am going to ask and answer my own question. I would have thought needing to create a null printer was not a rare thing, but I spent waaay too long googling and assembling bit and pieces before I had a working solution. I found lots of answers on creating a null printer via the Windows GUI but info on doing it programmatically was relatively scarce and scattered. Hopefully my answer or the better suggestions it elicites will save some time for some other poor shmo.

  • 4
    While it's perfectly acceptable to ask and answer your own question here, the question you *ask* must meet the same quality standards as any other question. This does not. Please [edit] to clearly explain the problem you're trying to solve and ask a clearly stated question, which the answer you've posted can then address. – Ken White Sep 26 '16 at 21:59
  • 1
    My apologies. I thought the title itself clearly stated the question. Perhaps I'm too close to it to see how it does not? Offer me some specific criticism and I'll do my best to improve it. – MickeyfAgain_BeforeExitOfSO Sep 26 '16 at 22:13
  • 1
    Remove all content of the question and add reason why you needed to create "null printer". Currently post only contains "searched alot" and "I'm awesome" text and no clear reason why one would do that. (Your question is likely on-topic when you get it in shape). – Alexei Levenkov Sep 26 '16 at 22:18
  • I gave you specific criticism: Clearly *explain the problem* - it should be explained clearly enough to be of benefit to a future reader here; they should be able to determine if it's the same problem they're trying to solve. I have no idea what the *problem* is or why you think a *null printer* solves it. Why would you need a *null printer*? How would you expect it to elicit *better suggestions* when no one knows the issue you're trying to solve? – Ken White Sep 26 '16 at 22:18
  • 1
    OK, you want to know not *what* I wanted to do, but *why* I needed (or thought I needed) to do it. I didn't realize that motives entered into it, I thought what was of interest was just "how do I do X". I'll re-write and update. Thank you for being patient with me. – MickeyfAgain_BeforeExitOfSO Sep 26 '16 at 22:28

1 Answers1

2

This "worked for me". I imagine there is a more elegant way of achieving this, and I expect any number of suggestions for improving/correcting the code, but I was not able to find an succinct complete answer to what I thought would have been a relatively common need. I had a fairly specific requirement, obviously this code can be generalized, proper error handling can be added, etc.

//pd is a PrintDocument. used like:

PrintController printController = new StandardPrintController();
pd.PrintController = printController;

NullPrinter np = new NullPrinter();                
if (!np.NullPortExists())
{
   np.CreateNullPort();
}

if (!np.NullPrinterExists())
{
    np.CreateNullPrinter();
}

pd.PrinterSettings.PrinterName = "NUL_PRINTER";


/*********************************************/ 
using System;
using System.Management; // This must also be added as a reference
using System.Drawing.Printing;
using System.Runtime.InteropServices;

namespace YourFavoriteNamespace
{
    //
    // This helper class has methods to determine whether a 'Nul Printer' exists,
    // and to create a null port and null printer if it does not.
    //
    public class NullPrinter
    {
    // Printer port management via Windows GUI (Windows 7, probably same in other versions):
    // 
    //      Go to printers & devices
    //      Select any printer
    //      Click on Print server properties
    //      Select Ports tab
    //      Add or remove (local) port
    //      To remove a local port, if "in use", stop and restart the print spooler service.
    //      It seems that the most recently used port will be "in use" until the system restarts,
    //      or until another port is used.
    //      A port may also be added when adding a printer.
    //      Valid names for a Null port appear to be NUL, NULL, NUL: - all case insensitive. 

    public bool NullPortExists()
    {
        for (int i = 0; i < PrinterSettings.InstalledPrinters.Count; i++)
        {
           string printerName = PrinterSettings.InstalledPrinters[i];
           string query = string.Format("SELECT * from Win32_Printer WHERE Name LIKE '%{0}'", printerName);
           ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);
           ManagementObjectCollection coll = searcher.Get();
           foreach (ManagementObject printer in coll)
           {
               string pName = printer["PortName"].ToString();
               if (pName.Equals("NULL", StringComparison.InvariantCultureIgnoreCase) ||
                pName.Equals("NUL", StringComparison.InvariantCultureIgnoreCase) ||
                pName.Equals("NUL:", StringComparison.InvariantCultureIgnoreCase))
               {
                return true;
               }
           }
        }
        return false;
    }

    // The application that uses this requires a printer specifically named "NUL_PRINTER"
    public bool NullPrinterExists()
    {
        for (int i = 0; i < PrinterSettings.InstalledPrinters.Count; i++)
        {
            if (PrinterSettings.InstalledPrinters[i] == "NUL_PRINTER")
            {
                return true;
            }
        }
        return false;
    }

    public bool CreateNullPort()
    {
        return Winspool.AddLocalPort("NUL") == 0 ? true : false;
    }

    public void CreateNullPrinter()
    {
        Winspool.AddPrinter("NUL_PRINTER");
    }

}
/*********************************************************/
    //
    // This Winspool class was mostly borrowed and adapted 
    // from several different people's blog posts, 
    // the links to which I have lost. 
    // Thank you, whoever you are.
    //
    public static class Winspool
    {
        [StructLayout(LayoutKind.Sequential)]
        private class PRINTER_DEFAULTS
        {
            public string pDatatype;
            public IntPtr pDevMode;
            public int DesiredAccess;
        }

        [DllImport("winspool.drv", CharSet = CharSet.Auto)]
        static extern IntPtr AddPrinter(string pName, uint Level, [In] ref PRINTER_INFO_2 pPrinter);

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        struct PRINTER_INFO_2
        {
            public string pServerName,
              pPrinterName,
                  pShareName,
                  pPortName,
                  pDriverName,
                  pComment,
                  pLocation;
            public IntPtr pDevMode;
            public string pSepFile,
                  pPrintProcessor,
                  pDatatype,
                  pParameters;
            public IntPtr pSecurityDescriptor;
            public uint Attributes,
                  Priority,
                  DefaultPriority,
                  StartTime,
                  UntilTime,
                  Status,
                  cJobs,
                  AveragePPM;
        }

        [DllImport("winspool.drv", EntryPoint = "XcvDataW", SetLastError = true)]
        private static extern bool XcvData(
            IntPtr hXcv,
            [MarshalAs(UnmanagedType.LPWStr)] string pszDataName,
            IntPtr pInputData,
            uint cbInputData,
            IntPtr pOutputData,
            uint cbOutputData,
            out uint pcbOutputNeeded,
            out uint pwdStatus);

        [DllImport("winspool.drv", EntryPoint = "OpenPrinterA",  SetLastError = true)]
        private static extern int OpenPrinter(
            string pPrinterName,
            ref IntPtr phPrinter,
            PRINTER_DEFAULTS pDefault);

        [DllImport("winspool.drv", EntryPoint = "ClosePrinter")]
        private static extern int ClosePrinter(IntPtr hPrinter);

        public static int AddLocalPort(string portName)
        {
            PRINTER_DEFAULTS def = new PRINTER_DEFAULTS();

            def.pDatatype = null;
            def.pDevMode = IntPtr.Zero;
            def.DesiredAccess = 1; //Server Access Administer

            IntPtr hPrinter = IntPtr.Zero;

            int n = OpenPrinter(",XcvMonitor Local Port", ref hPrinter, def);
            if (n == 0)
            return Marshal.GetLastWin32Error();

            if (!portName.EndsWith("\0"))
            portName += "\0"; // Must be a null terminated string

            // Must get the size in bytes. .NET strings are formed by 2-byte characters
            uint size = (uint)(portName.Length * 2);

            // Alloc memory in HGlobal to set the portName
            IntPtr portPtr = Marshal.AllocHGlobal((int)size);
            Marshal.Copy(portName.ToCharArray(), 0, portPtr, portName.Length);

            uint NotUsedByUs;
            uint xcvResult; 

            XcvData(hPrinter, "AddPort", portPtr, size, IntPtr.Zero, 0,  out NotUsedByUs, out xcvResult);

            ClosePrinter(hPrinter);
            Marshal.FreeHGlobal(portPtr);

            return (int)xcvResult;
        }

        public static void AddPrinter(string PrinterName)
        {
          IntPtr mystrptr = new IntPtr(0);    
          IntPtr mysend2;
          PRINTER_INFO_2 pi = new PRINTER_INFO_2();

          pi.pServerName =  "";
          pi.pPrinterName = PrinterName;
          pi.pShareName = "NUL";
          pi.pPortName = "NUL";
          pi.pDriverName = "Generic / Text Only";
          pi.pComment = "No Comment";
          pi.pLocation = "Local";
          pi.pDevMode = mystrptr;
          pi.pSepFile = "";
          pi.pPrintProcessor = "WinPrint";
          pi.pDatatype = "RAW";
          pi.pParameters = "";
          pi.pSecurityDescriptor = mystrptr;
          mysend2 = AddPrinter(null,2, ref pi);                    
        }
    }

}
  • Hey @mickeyf, Did you figure out any other way of doing this or improved your code? – jNewbie Oct 17 '17 at 04:48
  • 1
    @jnewbie I had lots on may plate and this turned out to be "good enough" for the situation, so I did not spend any more time on it. I hope it is of some help to you. I won't question your motives. – MickeyfAgain_BeforeExitOfSO Oct 17 '17 at 12:45
  • Hahaha, thank you! Do you have a code to get the data sent to the created printer? – jNewbie Oct 17 '17 at 19:11
  • 1
    @jnewbie Just send it as to any other printer. It of course finally goes into the bit bucket but in the meantime Windows creates each page. You can monitor this in methods you write for PrintDocument.BeginPrint, PrintDocument.PrintPage, etc. events. One use of this is that you can determine the number of pages actually required even in cases where that data is so variable that it would be extremely awkward to calculate it in advance in some other way, say where a grid might have data that in some cases wraps so that the row heights can't be relied on to always be the same. – MickeyfAgain_BeforeExitOfSO Oct 17 '17 at 19:56
  • I tried that but didn't work! Can please you give me some example of how can I read any data sent to the created printer? – jNewbie Oct 17 '17 at 20:20
  • I'm trying to receive the data "printed" outside the program, for example, if I print a PDF file or a WebSite using the created printer, I need to get the PDF in my program! – jNewbie Oct 17 '17 at 21:31
  • I think you need to know more than I have time or space to describe here. Start with https://msdn.microsoft.com/en-us/library/system.drawing.printing.printdocument(v=vs.110).aspx, and maybe search for related code examples on handling the classes events. I may be wrong, but I think PDFs are actually images, so you won't be able to simply get text out of that, if that's what you're after. – MickeyfAgain_BeforeExitOfSO Oct 17 '17 at 21:38
  • There are several free programs that "Print" to a pdf file, most browsers have a 'save a copy' when viewing pdfs, and any web page itself can be saved as html then 'printed' to one of those pdf printers. One of those options may be a better approach for what you want. – MickeyfAgain_BeforeExitOfSO Oct 17 '17 at 21:42