C# – Invoke printer properties dialog and save printer settings

Sometimes when we developed our application we would like to open a system dialog, for example the Printer properties dialog shown in image below.

Printer properties

1. Invoke printer properties

Then how to invoke this dialog from our application? To invoke this system dialog we need to know the api which windows operating system calls to open it. Indicating this api is a really hard mission. To accomplish it, you must be either a very very good reverser or a system programmer of windows. Therefore when you want to invoke such dialog, I recommend you to search on internet about this api to save your time. In this current example, we need to call the api “DocumentPropertiesW” from winspool.Drv to open the printer properties.

[DllImport("winspool.Drv", EntryPoint = "DocumentPropertiesA", SetLastError = true, ExactSpelling = true,
CallingConvention = CallingConvention.StdCall)]
public static extern int DocumentProperties(IntPtr hwnd, IntPtr hPrinter,
[MarshalAs(UnmanagedType.LPStr)] string pDeviceNameg, IntPtr pDevModeOutput, IntPtr pDevModeInput, int fMode);

The DocumentProperties function retrieves or modifies printer initialization information or displays a printer-configuration property sheet for the specified printer. The following code snippet below show how to call the printer properties dialog by giving the printer’s name.
You can see that the DocumentProperties function is called twice. The first time fMode is set to 0, that means “the DocumentProperties function returns the number of bytes required by the printer driver’s DEVMODE data structure“. The second time it was called with fMode = DM_IN_BUFFER OR DM_IN_PROMPT OR DM_OUT_BUFFER ( DM_IN_BUFFER = 8, DM_IN_PROMPT = 4, DM_OUT_BUFFER = 2) to use all options. The printer name is passed through property PrinterName of variable whose type is PrinterSettings.

 private void btnInvoke_Click(object sender, EventArgs e)
        {
            PrinterSettings psSettings = new PrinterSettings();
            psSettings.PrinterName = cbPrinters.SelectedItem.ToString();
            IntPtr ipDevMode = psSettings.GetHdevmode(psSettings.DefaultPageSettings);
            IntPtr pDevMode = GlobalLock(ipDevMode);
            int nSize = DocumentProperties(this.Handle, IntPtr.Zero, psSettings.PrinterName, pDevMode, ref pDevMode, 0);
            IntPtr ipDevModeData = Marshal.AllocHGlobal(nSize);
            DocumentProperties(this.Handle, IntPtr.Zero, psSettings.PrinterName, ipDevModeData, ref pDevMode, 14);
            GlobalUnlock(ipDevMode);
            psSettings.SetHdevmode(ipDevModeData);
            psSettings.DefaultPageSettings.SetHdevmode(ipDevModeData);
            GlobalFree(ipDevMode);
            Marshal.FreeHGlobal(ipDevModeData);
        }

Through this example we can realize that it is possible to call system dialog of operation system with c# if we know the correct api.

2. Update

2.1 Update 30.04.2012

If we want to save printer settings permantly we can use the code listing below

public class PrinterSettingsApis
{
	public static bool SetPrinterProperty(string printerName, DM dmProperty, long dmPropertyValue,
		out string errorMessage)
	{
		errorMessage = string.Empty;
		var functionReturnValue = false;
		var pd = default(PRINTER_DEFAULTS);
		var pinfo = new PRINTER_INFO_9();
		var dm = new DEVMODE();
		var nBytesNeeded = 0;
		var nRet = 0;
		var nJunk = default(int);
		var hPrinter = default(IntPtr);
		var ptrPrinterInfo = default(IntPtr);

		pd.DesiredAccess = PRINTER_DEFAULTS_DesiredAccess.PRINTER_ACCESS_USE;
		nRet = WinApis.OpenPrinter(printerName, out hPrinter, ref pd);
		if ((nRet == 0) | (hPrinter.ToInt64() == 0))
		{
			if (WinApis.GetLastError() == 5)
			{
				errorMessage = "Access denied -- See the article for more info.";
			}
			else
			{
				errorMessage = "Cannot open the printer specified " + "(make sure the printer name is correct).";
			}
			return false;
		}
		nRet = WinApis.DocumentProperties(IntPtr.Zero, hPrinter, printerName, IntPtr.Zero, IntPtr.Zero, 0);
		if ((nRet < 0))
		{
			errorMessage = "Cannot get the size of the DEVMODE structure.";
			goto cleanup;
		}
		var iparg = Marshal.AllocCoTaskMem(nRet + 100);
		nRet = WinApis.DocumentProperties(IntPtr.Zero, hPrinter, printerName, iparg, IntPtr.Zero,
			DEVMODEfModes.DM_OUT_BUFFER);
		if ((nRet < 0))
		{
			errorMessage = "Cannot get the DEVMODE structure.";
			goto cleanup;
		}
		dm = (DEVMODE)Marshal.PtrToStructure(iparg, dm.GetType());
		if (!Convert.ToBoolean(dm.dmFields & dmProperty))
		{
			errorMessage =
				"You cannot modify the setting flag for this printer because it does not support the setting or the driver does not support setting it from the Windows API.";
			goto cleanup;
		}

		switch (dmProperty)
		{
			case DM.Orientation:
				dm.dmOrientation = (short)dmPropertyValue;
				break;

			case DM.PaperSize:
				dm.dmPaperSize = (short)dmPropertyValue;
				break;

			case DM.Copies:
				dm.dmCopies = (short)dmPropertyValue;
				break;

			case DM.NUP:
				dm.dmNup = (short)dmPropertyValue;
				break;
		}

		Marshal.StructureToPtr(dm, iparg, true);
		nRet = WinApis.DocumentProperties(IntPtr.Zero, hPrinter, printerName, pinfo.pDevMode, pinfo.pDevMode,
			DEVMODEfModes.DM_IN_BUFFER | DEVMODEfModes.DM_OUT_BUFFER);
		if ((nRet < 0))
		{
			errorMessage = "Unable to set setting to this printer.";
			goto cleanup;
		}
		WinApis.GetPrinter(hPrinter, 9, IntPtr.Zero, 0, ref nBytesNeeded);
		if ((nBytesNeeded == 0))
		{
			errorMessage = "GetPrinter failed.";
			goto cleanup;
		}
		ptrPrinterInfo = Marshal.AllocCoTaskMem(nBytesNeeded + 100);
		nRet = WinApis.GetPrinter(hPrinter, 9, ptrPrinterInfo, nBytesNeeded, ref nJunk) ? 1 : 0;
		if ((nRet == 0))
		{
			errorMessage = "Unable to get shared printer settings.";
			goto cleanup;
		}
		pinfo = (PRINTER_INFO_9)Marshal.PtrToStructure(ptrPrinterInfo, pinfo.GetType());
		pinfo.pDevMode = iparg;
		pinfo.pSecurityDescriptor = 0;
		Marshal.StructureToPtr(pinfo, ptrPrinterInfo, true);
		nRet = WinApis.SetPrinter(hPrinter, 9, ptrPrinterInfo, 0) ? 1 : 0;
		if ((nRet == 0))
		{
			errorMessage = "Unable to set shared printer settings.";
		}
		functionReturnValue = Convert.ToBoolean(nRet);
		cleanup:
		if ((hPrinter.ToInt64() != 0))
			WinApis.ClosePrinter(hPrinter);
		return functionReturnValue;
	}
}

2.2 Update 01.11.2015

For setting “Pages per sheet” we can set value to NUP property.

 if (!PrinterApis.PrinterSettingsApis.SetPrinterProperty(cbPrinters.SelectedItem.ToString(), PrinterApis.WinNative.DM.NUP, 1, out errorMessage))
                MessageBox.Show(@"Failed", errorMessage);

2.3 Update 05.12.2015

Update to use PRINTER_INFO_9 instead of PRINTER_INFO_2.

3. Source code

Source code: https://bitbucket.org/hintdesk/dotnet-invoke-printer-properties-dialog

19 thoughts on “C# – Invoke printer properties dialog and save printer settings”

  1. Hi,

    You’re code is very nice and simple. But I found the passing a PrinterSettings parameter tinstead of building a new one every time when calling the method, does not change the Printer Properties Dialog. For example if I make some changes in the Printer Properties Dialog, closed it and then open it again it does not show the changes, even if the PrinterSeetings object seems to store the data.

    Any idea?
    Thanks

  2. Nice code. I happily used it for a long time on Win-XP.

    On Win7 though the first call of DocumentProperties always returns -1, and then the call to AlloHGlobal fails with an OutOfMemory-Exception!

    Setting the 4th parameter to IntPtr.Zero seems to help and the function now works on Win-XP and Win-7.

    int nSize = DocumentProperties(this.Handle, IntPtr.Zero, psSettings.PrinterName, IntPtr.Zero, ref pDevMode, 0);

  3. Nice thing, working as intended. But I prefer libararies in a common sense so I don’t have to alway cut & paste code each time I want to use such “special abilities”. Finally (I prefer VB.NET) I modified your code so it extends the original PrinterSettings class with a function that performs the DocumentProperties dialog.

    Imports System.Runtime.InteropServices
    Imports System.Runtime.CompilerServices
    Imports System.Drawing.Printing

    Module PrinterSettingsEx

    Private Const DM_PROMPT As Integer = 4
    Private Const DM_MODIFY As Integer = 8
    Private Const DM_COPY As Integer = 2
    Private Const DM_IN_BUFFER As Integer = DM_MODIFY
    Private Const DM_OUT_BUFFER As Integer = DM_COPY

    _
    Private Function DocumentProperties(ByVal hwnd As IntPtr, ByVal hPrinter As IntPtr, ByVal pDeviceName As String, ByVal pDevModeOutput As IntPtr, ByVal pDevModeInput As IntPtr, ByVal fMode As Integer) As Integer
    End Function

    Private Declare Function GlobalLock Lib "kernel32.dll" (ByVal hMem As IntPtr) As IntPtr
    Private Declare Function GlobalUnlock Lib "kernel32.dll" (ByVal hMem As IntPtr) As IntPtr
    Private Declare Function GlobalFree Lib "kernel32.dll" (ByVal hMem As IntPtr) As IntPtr

    _
    Public Function ShowDocumentProperties(ByVal psSettings As PrinterSettings, ByVal OwnerForm As Form) As Boolean

    Dim ipDevMode As IntPtr = psSettings.GetHdevmode(psSettings.DefaultPageSettings)
    Dim pDevMode As IntPtr = GlobalLock(ipDevMode)
    Dim nSize As Integer = DocumentProperties(OwnerForm.Handle, IntPtr.Zero, psSettings.PrinterName, IntPtr.Zero, IntPtr.Zero, 0)
    Dim ipDevModeData As IntPtr = Marshal.AllocHGlobal(nSize)

    Dim result As Integer = DocumentProperties(OwnerForm.Handle, IntPtr.Zero, psSettings.PrinterName, ipDevModeData, pDevMode, DM_PROMPT Or DM_IN_BUFFER Or DM_OUT_BUFFER)
    GlobalUnlock(ipDevMode)
    If result = 1 Then
    psSettings.SetHdevmode(ipDevModeData)
    psSettings.DefaultPageSettings.SetHdevmode(ipDevModeData)
    End If

    GlobalFree(ipDevMode)
    Marshal.FreeHGlobal(ipDevModeData)
    Return (result = 1)

    End Function

    End Module

    Converting this one back to c# should not be difficult as you obviously don’t need the declarations for GlobalLock & Co.

  4. Sorry… The “<” and “>” chars in the code block seem to be interpreted as (incorrect) html so these parts don’t show up, so I try again with “[” and “]” instead of them. In VB they have to be changed back to “<” and “>”

    Imports System.Runtime.InteropServices
    Imports System.Runtime.CompilerServices
    Imports System.Drawing.Printing

    Module PrinterSettingsEx

    Private Const DM_PROMPT As Integer = 4
    Private Const DM_MODIFY As Integer = 8
    Private Const DM_COPY As Integer = 2
    Private Const DM_IN_BUFFER As Integer = DM_MODIFY
    Private Const DM_OUT_BUFFER As Integer = DM_COPY

    [DllImport("winspool.Drv", EntryPoint:="DocumentPropertiesW", SetLastError:=True, ExactSpelling:=True, CallingConvention:=CallingConvention.StdCall)] _
    Private Function DocumentProperties(ByVal hwnd As IntPtr, ByVal hPrinter As IntPtr, ByVal pDeviceName As String, ByVal pDevModeOutput As IntPtr, ByVal pDevModeInput As IntPtr, ByVal fMode As Integer) As Integer
    End Function

    Private Declare Function GlobalLock Lib "kernel32.dll" (ByVal hMem As IntPtr) As IntPtr
    Private Declare Function GlobalUnlock Lib "kernel32.dll" (ByVal hMem As IntPtr) As IntPtr
    Private Declare Function GlobalFree Lib "kernel32.dll" (ByVal hMem As IntPtr) As IntPtr

    [Extension()] _
    Public Function ShowDocumentProperties(ByVal psSettings As PrinterSettings, ByVal OwnerForm As Form) As Boolean

    Dim ipDevMode As IntPtr = psSettings.GetHdevmode(psSettings.DefaultPageSettings)
    Dim pDevMode As IntPtr = GlobalLock(ipDevMode)
    Dim nSize As Integer = DocumentProperties(OwnerForm.Handle, IntPtr.Zero, psSettings.PrinterName, IntPtr.Zero, IntPtr.Zero, 0)
    Dim ipDevModeData As IntPtr = Marshal.AllocHGlobal(nSize)

    Dim result As Integer = DocumentProperties(OwnerForm.Handle, IntPtr.Zero, psSettings.PrinterName, ipDevModeData, pDevMode, DM_PROMPT Or DM_IN_BUFFER Or DM_OUT_BUFFER)
    GlobalUnlock(ipDevMode)
    If result = 1 Then
    psSettings.SetHdevmode(ipDevModeData)
    psSettings.DefaultPageSettings.SetHdevmode(ipDevModeData)
    End If

    GlobalFree(ipDevMode)
    Marshal.FreeHGlobal(ipDevModeData)
    Return (result = 1)

    End Function

    End Module

  5. hello,I have create crystal report for receipt print and I want two receipt as copy and orignal on same page . So, How can I set printer’s properties(pages per sheet) using code ?

    Can you plz suggest me to do the same??

  6. I m using DEVMODE structure obtained from pininvoke, then i m populating it with the array obtained from PrintTicketToDevMode convertor.
    But it seems that only first of array is stores in structure and all others got some offset from actual position.

  7. Hi, great work 🙂 I was trying to change any properties for almost two days and cannot find any suitable way in C#. Could you tell me what is default license at bitbucket? There is no license at your source code so as far as I know, we cannot use it anyway. Regards MP

  8. @Marcin: I don’t know if there is a default license on bitbucket. But for my source code the license is WTFPL (Do What the Fuck You Want to Public License). 🙂

  9. @admin I was looking for an answer for few days… Maybe you are able to help me. Do u know can I set printer halftone using c#?

  10. Hello,
    Thank you for this much needed functionality in C#.
    I am having hard time getting the code in SetPrinterProperty to build. I can bring up Printer Properties dialog by using code in btnInvoke_Click but changes are not saved after clicking OK button on printer properties dialog. Code in SetPrinterProperty () doesn’t build, I am missing some namespaces most likely. Where do I get DM dmproperty to pass onto the function SetPrinterProperty and where is the class DEVMODE so I create object var dm = new DEVMODE(); where I can find these constants var pd = default(PRINTER_DEFAULTS);
    var pinfo = new PRINTER_INFO_9();
    Basically my problem is saving the changes to printer properties dialog and also if possible know what changes user made in that dialog.
    Please help. Thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *