Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

Storing and Recalling Printer Settings in C#.NET

4.69/5 (8 votes)
4 Nov 2012Public Domain7 min read 87.6K   4.7K  
This article shows how you can save printer settings of any printer to a file or an arraylist to be recalled later.

Introduction

Based on an idea by Nicholas Piasecki

This article shows how you can save printer settings of any printer to a file or an arraylist to be recalled later.

Background

I have been working on a C#.NET application for the design of radio receivers for a while.

The application has a print facility to print various pages of text, graphs and a scale so you can see what you’re tuned into.

The print system allows you to select the standard print features such as copies, collate, colour, paper source, etc. If you need to select a particular function specific to your printer, then there is a “Printer properties” button which brings up the setup dialogue for the selected printer.

There is a section of the form where the printer settings can be stored in “Memories” to be squirreled away for later. Wouldn’t it be nice if the “Memories” could store all the print settings including those not accessible from the printer settings class.

What do you mean “not accessible from the printer settings class”?

Settings such as Orientation, Resolution, Paper source, copies, etc. are held in what is called a “Devmode structure”. There are two parts to the structure:

  1. A public area holding data for Orientation, Resolution, Paper source, copies, etc. This can be accessed through the printer settings class in .NET. Any printer has the ability to recognise settings for Orientation, Resolution, Paper source, copies, etc.
  2. A Private area holding data for settings specific to a particular printer. Paper stapling, folding, best photo mode, feeding the dog, putting out the cat, reminding you of your wedding anniversary, etc. There are no settings in the printer settings class to handle these.

The data in the structure is held in contiguous memory, the public area first, immediately followed by the private area. If I wanted to store the data in the private area, I was stuffed without a definition of the data in the private area (which, being printer specific, will vary from printer to printer). I did not want to manipulate the data, just store and recall it. There is a definition for the public part of the structure and can be found here.

I want to be able to store all the printer settings, for any installed printer, The PROBLEM, is (sorry, was – let’s get the tense right!) how to do it.

I have found a solution to the problem based upon an idea by Nicholas Piasecki who had an “Off the wall idea” Look at this link to see how he did it.

The idea of storing the data (the devmode structure) in a file and then when necessary, reload the file, overwrite the devmode structure in memory with the one on file is the way I went.

I have added a few twists but the idea is the same and therefore credit must go to Nicholas Piasecki as it was his idea.

The best part I think is that the devmode definition is not needed as I do not need to know what the variables names are or indeed the values, because I do not want to alter them. Just squirrel away copies of the data as a block, not as a collection of variables. The original version did use the devmode definition so you could find the values of dm_Size and dm_Extra variables. dm_Size is the size of the public part of the devmode structure, (on my Win 7 laptop this is 220 bytes regardless of printer), dm_Extra is the size of the private area of the devmode structure which varies from printer to printer. Both of these values are added together to allocate the right amount of memory. This is the “Size needed”. If you look at the printersettings class, there is a method called “GetHdevmode”, this will return the FULL size of the devmode structure! With my printers, there was no difference between GetHdevmode or dm_Size + dm_Extra, so I did away with the definition and used GetHdevmode instead, much easier.

Using the Code

There are three main methods.

The first opens up the printer dialogue (the one supplied by the printer manufacturer) so you can change the settings of the selected printer, including the printer specific ones.

The methods require the following platform invokes in order to work:

C#
[DllImport("winspool.Drv",
          EntryPoint = "DocumentPropertiesW",
          SetLastError = true,
          ExactSpelling = true,
CallingConvention = CallingConvention.StdCall)]
        static extern int DocumentProperties
            (IntPtr hwnd, IntPtr hPrinter,
            [MarshalAs(UnmanagedType.LPWStr)] 
            string pDeviceName,
            IntPtr pDevModeOutput,
            IntPtr pDevModeInput,
            int fMode);
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalFree(IntPtr handle);
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalLock(IntPtr handle);
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalUnlock(IntPtr handle);

The first method brings up the printer dialogue box:

C#
private PrinterSettings OpenPrinterPropertiesDialog(PrinterSettings printerSettings)
//Shows the printer settings dialogue that comes with the printer driver
{
    IntPtr hDevMode = IntPtr.Zero;
    IntPtr devModeData = IntPtr.Zero;
    IntPtr hPrinter = IntPtr.Zero;
    String pName = printerSettings.PrinterName;
    try
    {
        hDevMode = printerSettings.GetHdevmode(printerSettings.DefaultPageSettings);
        IntPtr pDevMode = GlobalLock(hDevMode);
        int sizeNeeded = DocumentProperties(this.Handle, IntPtr.Zero, pName, 
                              pDevMode, pDevMode, 0);//get needed size and allocate memory 
        if (sizeNeeded < 0)
        {
            MessageBox.Show("Bummer, Cant get size of devmode structure");
            Marshal.FreeHGlobal(devModeData);
            Marshal.FreeHGlobal(hDevMode);
            devModeData = IntPtr.Zero;
            hDevMode = IntPtr.Zero;
            return printerSettings;
        }
        devModeData = Marshal.AllocHGlobal(sizeNeeded);
        //show the native dialog 
        int returncode = DocumentProperties
               (this.Handle, IntPtr.Zero, pName, devModeData, pDevMode, 14);
        if (returncode < 0) //Failure to display native dialogue
        {
            MessageBox.Show("Dialogue Bummer, Got me a devmode, but the dialogue got stuck");
            Marshal.FreeHGlobal(devModeData);
            Marshal.FreeHGlobal(hDevMode);
            devModeData = IntPtr.Zero;
            hDevMode = IntPtr.Zero;
            return printerSettings;
        }
        if (returncode ==2) //User clicked "Cancel"
        {
            GlobalUnlock(hDevMode);//unlocks the memory
            if (hDevMode != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(hDevMode); //Frees the memory
                hDevMode = IntPtr.Zero;
            }
            if (devModeData != IntPtr.Zero)
            {
                GlobalFree(devModeData);
                devModeData = IntPtr.Zero;
            }
        }
        GlobalUnlock(hDevMode);//unlocks the memory
        if (hDevMode != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(hDevMode); //Frees the memory
            hDevMode = IntPtr.Zero;
        }
        if (devModeData != IntPtr.Zero)
        {
            printerSettings.SetHdevmode(devModeData);
            printerSettings.DefaultPageSettings.SetHdevmode(devModeData);
            GlobalFree(devModeData);
            devModeData = IntPtr.Zero;
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show("An error has occurred, caught and chucked back\n" + ex.Message);
    }
    finally
    {
        if (hDevMode != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(hDevMode);
        }
        if (devModeData != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(devModeData);
        }
    }
    return printerSettings;
}

The second method gets the devmode data and either saves it to a file or to an arraylist depending upon the value of a control variable. This method also uses the same platform invokes as before.

C#
private void GetDevmode(PrinterSettings printerSettings, int mode, String Filename)
//Grabs the devmode data in memory and stores in arraylist
{
    ///int mode
    ///1 = Save devmode structure to file
    ///2 = Save devmode structure to Byte array and arraylist
    IntPtr hDevMode = IntPtr.Zero;                        // handle to the DEVMODE
    IntPtr pDevMode = IntPtr.Zero;                          // pointer to the DEVMODE
    IntPtr hwnd = this.Handle;
    try
    {
        // Get a handle to a DEVMODE for the default printer settings
        hDevMode = printerSettings.GetHdevmode(printerSettings.DefaultPageSettings);
        // Obtain a lock on the handle and get an actual pointer so Windows won't
        // move it around while we're futzing with it
        pDevMode = GlobalLock(hDevMode);
        int sizeNeeded = DocumentProperties
               (hwnd, IntPtr.Zero, printerSettings.PrinterName, IntPtr.Zero, pDevMode, 0);
        if (sizeNeeded <= 0)
        {
            MessageBox.Show("Devmode Bummer, Cant get size of devmode structure");
            GlobalUnlock(hDevMode);
            GlobalFree(hDevMode);
            return;
        }
        DevModeArray = new byte[sizeNeeded];    //Copies the buffer into a byte array
        if (mode == 1)  //Save devmode structure to file
        {
            FileStream fs = new FileStream(Filename, FileMode.Create);
            for (int i = 0; i < sizeNeeded; ++i)
            {
                fs.WriteByte(Marshal.ReadByte(pDevMode, i));                       
            }
            fs.Close();
            fs.Dispose();
        }
        if (mode == 2)  //Save devmode structure to Byte array and arraylist
        {
            for (int i = 0; i < sizeNeeded; ++i)
            {
                DevModeArray[i] = (byte)(Marshal.ReadByte(pDevMode, i));
                //Copies the array to an arraylist where it can be recalled
            }
        }
        // Unlock the handle, we're done futzing around with memory
        GlobalUnlock(hDevMode);
        // And to boot, we don't need that DEVMODE anymore, either
        GlobalFree(hDevMode);
        hDevMode = IntPtr.Zero;
    }
    catch (Exception ex)
    {
        if (hDevMode != IntPtr.Zero)
        {
            MessageBox.Show("BUGGER");
            GlobalUnlock(hDevMode);
            // And to boot, we don't need that DEVMODE anymore, either
            GlobalFree(hDevMode);
            hDevMode = IntPtr.Zero;
        }
    }
}

The final method takes the devmode data from either a file on your HDD or from the arraylist, again depending upon a control variable and overwrites the structure in memory.

C#
private void SetDevmode(PrinterSettings printerSettings, int mode, String Filename)
//Grabs the data in arraylist and chucks it back into memory "Crank the suckers out"
{
    ///int mode
    ///1 = Load devmode structure from file
    ///2 = Load devmode structure from arraylist
    IntPtr hDevMode = IntPtr.Zero;                        // a handle to our current DEVMODE
    IntPtr pDevMode = IntPtr.Zero;                        // a pointer to our current DEVMODE
    Byte[] Temparray;
    try
    {
        DevModeArray = CurrentSetup.Devmodearray;
        // Obtain the current DEVMODE position in memory
        hDevMode = printerSettings.GetHdevmode(printerSettings.DefaultPageSettings);
        // Obtain a lock on the handle and get an actual pointer so Windows won't move
        // it around while we're futzing with it
        pDevMode = GlobalLock(hDevMode);
        // Overwrite our current DEVMODE in memory with the one we saved.
        // They should be the same size since we haven't like upgraded the OS
        // or anything.
        if (mode == 1)  //Load devmode structure from file
        {
            FileStream fs = new FileStream(Filename, FileMode.Open, FileAccess.Read);
            Temparray = new byte[fs.Length];
            fs.Read(Temparray, 0, Temparray.Length);
            fs.Close();
            fs.Dispose();
            for (int i = 0; i < Temparray.Length; ++i)
            {
                Marshal.WriteByte(pDevMode, i, Temparray[i]);
            }
        }
        if (mode == 2)  //Load devmode structure from arraylist
        {
            for (int i = 0; i < DevModeArray.Length; ++i)
            {
                Marshal.WriteByte(pDevMode, i, DevModeArray[i]);
            }
        }
        // We're done futzing
        GlobalUnlock(hDevMode);
        // Tell our printer settings to use the one we just overwrote
        printerSettings.SetHdevmode(hDevMode);
        printerSettings.DefaultPageSettings.SetHdevmode(hDevMode);
        // It's copied to our printer settings, so we can free the OS-level one
        GlobalFree(hDevMode);
    }
    catch (Exception ex)
    {
        if (hDevMode != IntPtr.Zero)
        {
            MessageBox.Show("BUGGER");
            GlobalUnlock(hDevMode);
            // And to boot, we don't need that DEVMODE anymore, either
            GlobalFree(hDevMode);
            hDevMode = IntPtr.Zero;
        }
    }
}

The remaining methods handle the arraylist, updating the form and a comparator.
The comparator allows to display two devmode data sources (either from an arraylist, datafile or both) and look at the differences between them. The results are displayed in a text box.

This solution is not perfect, but with the printers I have installed on my system, does appear to work. The solution is not intended as a standalone application to work alongside an application with print facilities, but can be adapted to work in your own application (that is assuming your application needs to drive a printer)!

I have tried this with my “Canon MP810” and was able to store and recall the settings that controlled:

  1. Preview before print
  2. Paper type, (plain, glossy photopaper, compact disk, T shirt transfers, etc.
  3. Sepia toning
  4. Image optimiser
  5. Photo optimise Pro and so on

I have also tried this on PDF creator (which installs itself as a printer) with great success.

However, “Send to one note” was a bit risky. If you set the print area from the Send to one note dialogue, save, recall, then have a look at the dialogue again, the print area is sometimes zero. I have since found out that if the print area is altered from the one note driver, the papersize becomes “Custom”. When this is passed back to printersettings class, the default width and height (Zero) are passed back, not the ones you have typed in. I have had to handle custom page sizes in a slightly different way.

If the arraylist is used, other data is stored as well including print quality, paper size, printer name and so on. If you choose not to use the devmode structure by setting “No (mouse)”, these settings are used instead. If you select “Yes (Man)”, the devmode structure is used instead, but if loading from file, don’t load the wrong structure for the selected printer.

If the wrong devmode structure is loaded, your computer could go round in ever decreasing circles and eventually achieve the impossible! The wrong devmode structure can be loaded by:

  1. Loading in the wrong binary file, there is no check on this. If you take a movie file, rename the extension to “.BIN”, the file will can appear in the file browser. If such a file is selected, you will overwrite the devmode structure and most of the operating system! You may need a GHOST image or a recovery disk if you overwrite the devmode structure with 50 shades of Grey!! (No, this will not put the printer into monochrome mode!) If the arraylist is used, the printer name is used to call the correct printer first, the arraylist is safer.
  2. Changing the operating system and or printer driver may cause problems as the devmode structure could be different. Don’t forget that a printer driver for one operating system does not usually work on another.

I may work on a way of letting other applications know of a change to the printer settings so this program can be used as a standalone program for controlling printer settings for other applications, but that is for later, unless any of you want to have a go?

As I said, this solution is intended to be included, in modified form, into another application. If you see any way to improve the solution, post the amendments so anyone (including me) can benefit.

Licensing (What’s That For)

Anything you see from me I consider to be “Public domain”. If you want to use it, download the project and go ahead.

The ZIP file that can be downloaded is a VS2005 C# .NET project.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication