Click here to Skip to main content
Click here to Skip to main content

Keep Your InkJet Print Head Clean

, 4 Jan 2009
Rate this:
Please Sign up or sign in to vote.
A utility that you can use to "exercise" your inkjet printer without wasting a lot of ink or paper

Introduction

I own a couple of inkjet printers. As most of you probably know, the most egregious problem with inkjet printers is their tendency to foul the print heads if not used on a frequent and regular basis. This utility allows you to exercise your print head while not wasting a lot of ink (or paper) in the process.

Why Did I Write This

Of course, there is already at least one utility available (also called AutoPrint)that performs the same function, but it does so in what I think is a rather clunky fashion, not to mention being a consummate waster of paper and ink. It relies on the presence of several JPG image files, and it prints the one that you specify on the command line. It is also strictly a console application. I figured I could do it better.

My AutoPrint

My implementation of AutoPrint is significantly different from the existing one. It provides both a WinForms interface, and a console-style interface, and it creates the necessary image on the fly, eliminating the need for additional files to be supplied in the installer. The various help file resources are stored as embedded resources that are extracted to an appropriate folder on the hard drive. There are a couple of nigglies that kinda ticked me off while I was writing this, but there doesn't seem to be an answer out there. I'll cover thoes issues as the article progresses.

The WinForms Interface

The WinForms interface consists of a single form.

screenshot01.png

At the top of the form is a list of installed printers. Beneath this list and on the left is a "Colors To Print" ListView control, and on the right a series of buttons. That actually give the utility its purpose in life.

Upon start-up, the application enumerates the installed printers, and populates the Installed Printers list control with nothing being preselected. The Colors To Print list is vacant, and all of the buttons on the right side are disabled. Upon selecting a printer, the application determines if the printer supports color printing, populates the Colors To Print list as dictated by that metric, and enables the right-side buttons. If the selected printer does NOT support color printing, the only color in the list will be Black, that color will be checked, and the Colors To Print list will be disabled.

screenshot02.png

Before we go further, I must add that there is apparently no method for determining precisely what colors are supported unless you interface with the driver, and while I'm not absolutely positive, I'm pretty sure that each printer/brand will be different, and besides that, the hardware manufacturers aren't exactly famous for being up front about how you would go about getting this info. In other words, it would end up being a complete waste of my time to try, and even if I was somehow able to figure it out, it would still only work on the printers I own.

Pressing Buttons

On the right side of the form, you'll see a number of buttons. They are as follows.

The Preview Button

This button allows you to see where the test image will be printed on the paper before you actually print. Each time you print, the image's position will move to the right or to the next line. Of course, there's a Print button on the Preview window so that you can print directly from the preview. Here's an example of what you might see:

preview.png

The Preview Extended Button

This button allows you to see what a full sheet of test images would look like. It should be used to verify that the calculated position of each test image is positioned on the page in such a way as to not exceed the bounds of the paper. AutoPrint determines each position by using metrics provided by the printer (via WMI). Before you print the first time, please use this button to ensure that AutoPrint is correctly calculating your page size. Here's an example of what you might see:

previewextended.png

Note - It will take a few seconds for AutoPrint to display the Preview window for this button, so be patient. Also, it's probably not wise to print from this Preview screen because it will a) be a waste of precious ink, and b) use up one complete side of a sheet of paper.

The Print Button

This button allows you to print a test image at the next calculated position. There is a CheckBox just above this button labeled "Reset position". If this CheckBox is checked when you click the Print button, the image will be printed at the first position on the page (the top-left corner). You should only need to do this if you changed paper for some reason after already having printed one or more test images.

The Save Colors Button

This button saves the selected colors for the current printer so that the next time you use AutoPrint, it will remember the colors that were previously selected, and the Colors To Print list will be initialized with those colors already checked.

The Make Cmd File Button

This button allows you to create a .CMD file, suitable for use in the Windows scheduler, or from the Windows DOS Command window. Instead of forcing you to type the contents and potentially make a mistake, AutoPrint creates the file for you. At this point, thefiles could even be double-clicked inside Windows Explorer and run like any other console application. The files are placed in one of the following folders:

In Vista:

   C:\ProgramData\AutoPrint

In XP

   C:\Documents and Settings\All Users\Application Data\AutoPrint

The name of the cmd file is the same as your printer name with underscores replacing and spaces (and in the event your printer is a network printer, backslashes are simply removed). The generated command line parameters includes the name of the printer, and the colors to be printed, and looks something like this:

   C:

   cd \Program Files\AutoPrint

   AutoPrint.exe name="EPSON Stylus Photo 1400 Series" 
colors="Black|Cyan|Magenta|Yellow|LightCyan|LightMagenta"

Just above the Make Cmd File button, you'll see a CheckBox labeled "Use Saved Colors". This creates a slightly different cmd file, like so:

   C:

   cd \Program Files\AutoPrint

   AutoPrint.exe config="EPSON_Stylus_Photo_1400_Series"

This forces AutoPrint to load the saved colors from the config file for the named printer (saved when you clicked the Save Colors button). As you can see, it creates a much simpler command line.

The Code

Ah yes, the reason we're all here. This program doesn't really have that much code, but what little there is is fairly interesting. The techniques exercised in this article include:

  • Printing and print preview
  • Generating bitmaps on the fly
  • Checked listview with an image list
  • Use of resources
  • Extracting resources to files on disk
  • Custom events
  • WMI
  • Creating/updating XML files with Linq-To-XML

Visual Elements - The Installed Printers Listbox Control

Initializing the list of printers is performed in the DiscoverPrinters() method. This method also provides our first exposure to the printing functionality built into .Net.

The PrinterSettings object contains the InstalledPrinters property, which returns a list of installed printers. Keep in mind that the objects in this list do NOT contain any viable ways to determine the "ready" status of a given printer, but simply that it is "installed". Since this list is appears to be populated based on the presence of drivers, disconnecting a printer after installing the drivers will have no effect on whether or not the printer is reported as "installed".

[RANT] Even though Windows has been around for almost 20 years, Microsoft is STILL falling down on mandating standards where printer drivers are concerned. The situation is even worse than the DirectShow camera standards debacle was (is?). [/RANT]

Anyway, we iterate through the list of installed printers, ignoring the built in non-printer devices (the Microsoft XPS Document Writer, and the FAX device) to see iif the given printer is valid. It if is, we instantiate a ProfileBitmap object, and add it to the ListBox control.

What's that, you say? Adding a non-string object to a ListBox? Why, yes, young Jedi. Since the ProfileBitmap object has an override for the ToString() method (that returns the printer name), we can just add the object to the ListBox control and let .Net call the ToString() method to populate the ListBox for us. This also eliminates the need for a separate list to hold ProfileBitmap objects.

private void DiscoverPrinters()
{
    foreach (string printerName in PrinterSettings.InstalledPrinters)
    {
        PrinterSettings printer = new PrinterSettings();
        printer.PrinterName = printerName;

        // We're only interested in printers that are NOT the built-in Microsoft 
        // XPS Document Writer or FAX.
        if (!printerName.Contains("Microsoft") && !printerName.Contains("Fax"))
        {
            PrinterSettings printer = new PrinterSettings();
            printer.PrinterName = printerName;
            if (printer.IsValid)
            {

                // Create a new profile for the printer and set its default color
                // selection.
                ProfileBitmap profile = new ProfileBitmap(printerName, ColorBarFlags.Black);

                // Set the color bars according to the profile's self-determined ability 
                // to support colors.
                if (profile.SupportsColor)
                {
                    profile.ColorBars |= (ColorBarFlags.Cyan|ColorBarFlags.Yellow|ColorBarFlags.Magenta);
                }

                // Add the printer to the listbox
                this.listboxPrinters.Items.Add(profile);
            }
        }
    }
}

The remainder of the controls on the form are either disabled (the buttons) or contain no data (the Colors To Print ListView control), and the for is awaiting user input.

Visual Elements - The Colors To Print ListView

The reason for this program to exist rests primarily on the color cartridges that are installed in a given printer. Printers contain from a single cartridge (the cheap Kodak printer that Al from ToolTime is pushing (it seems) everywhere) to as many as seven cartridges (some higher-dollar stuff from Epson). The Colors To Print ListView allows the user to select precisely which cartridges his printer has so that all of those cartridges can be exercised.

When the user selects a printer from the Installed Printers list, the Colors To Print ListView is populated with all of the printable colors, with a certain subset of those colors being preselected. If the program is being run for the first time, the preselected (checked) colors are Black, Cyan, Yellow, and Magenta, because these are the most widely available colors in *most* color inkjet printers. If the user had used the app before and clicked the Save Colors button, the preselected colors would reflect the colors that were selected at the time he clicked the button.

There is nothing unusual going on in this control. The color squares are resources embedded in the application binary, and the control itself is your standard ListView with checkboxes turned on. Here's the initialization code:

private void PopulateColorList(ProfileBitmap profile)
{
    // Remove the ItemChecked handler for the color listbox so we don't fire 
    // that event during the population of the listbox. We do this because as 
    // we populate the listbox when a printer is selected, it will fire the 
    // ItemChecked event as we check colors that are selected in the 
    // ProfileBitmap object.
    this.listOfColors.ItemChecked -= new System.Windows.Forms.ItemCheckedEventHandler(
                                        this.listOfColors_ItemChecked);
    listOfColors.BeginUpdate();
    listOfColors.Clear();
    if (imageListSmall.Images.Count == 0)
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.Black.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.LightBlack.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.Cyan.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.LightCyan.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.Yellow.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.Magenta.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.LightMagenta.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.Red.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.Green.ico")));
        imageListSmall.Images.Add(
            new Icon(assembly.GetManifestResourceStream("AutoPrint.Resources.Blue.ico")));
    }

    listOfColors.Items.Add(new ListViewItem("Black", 0));
    if (profile.SupportsColor)
    {
        listOfColors.Items.Add(new ListViewItem("Light Black", 1));
        listOfColors.Items.Add(new ListViewItem("Cyan", 2));
        listOfColors.Items.Add(new ListViewItem("Light Cyan", 3));
        listOfColors.Items.Add(new ListViewItem("Yellow", 4));
        listOfColors.Items.Add(new ListViewItem("Magenta", 5));
        listOfColors.Items.Add(new ListViewItem("Light Magenta", 6));
        listOfColors.Items.Add(new ListViewItem("Red", 7));
        listOfColors.Items.Add(new ListViewItem("Green", 8));
        listOfColors.Items.Add(new ListViewItem("Blue", 9));
        listOfColors.Items[0].Checked = (profile.ColorIsSet(ColorBarFlags.Black));
        listOfColors.Items[1].Checked = (profile.ColorIsSet(ColorBarFlags.LightBlack));
        listOfColors.Items[2].Checked = (profile.ColorIsSet(ColorBarFlags.Cyan));
        listOfColors.Items[3].Checked = (profile.ColorIsSet(ColorBarFlags.LightCyan));
        listOfColors.Items[4].Checked = (profile.ColorIsSet(ColorBarFlags.Yellow));
        listOfColors.Items[5].Checked = (profile.ColorIsSet(ColorBarFlags.Magenta));
        listOfColors.Items[6].Checked = (profile.ColorIsSet(ColorBarFlags.LightMagenta));
        listOfColors.Items[7].Checked = (profile.ColorIsSet(ColorBarFlags.Red));
        listOfColors.Items[8].Checked = (profile.ColorIsSet(ColorBarFlags.Green));
        listOfColors.Items[9].Checked = (profile.ColorIsSet(ColorBarFlags.Blue));
        listOfColors.Enabled = true;
    }
    else
    {
        listOfColors.Items[0].Checked = true;
        listOfColors.Enabled = false;
    }
    listOfColors.EndUpdate();

    // re-add the ItemChecked handler for the color listbox so we get events 
    // when an item is checked in the listbox. 
    this.listOfColors.ItemChecked += new System.Windows.Forms.ItemCheckedEventHandler(
                                        this.listOfColors_ItemChecked);
}

Visual Elements - The Colors To Print Sideways Label

Yeah, I admit it - it looks severely out-of-place on the form. However, I had developed the underlying code for another project that was eventually abandoned in favor of a more suitable solution to the problem this code addressed. I simply wanted to use it somewhere, and AutoPrint ended up being the hapless victim.

When I looked at the code, I thought - My god! Who wrote this crap!?", but I didn't let that dissuade me from using it, and I figured you guys would have a whale of a good time ripping it apart and suggesting better ways to accomplish the same thing. The Vertical label class is - well - not even derived from the Label object. It simply paints a bitmap in a rectangle area (whose metrics are described via the bounds of the PictureBox control on the form) with the specified font. The resulting bitmap is then can be rotated during the Paint event for the PictureBox control. Here are the significant parts of the VerticalLabel class.

The first function is responsible for actually drawing the bitmap.

public void CreateLabelBitmap()
{
    Rectangle rect      = new Rectangle(0, 0, this.Size.Width, this.Size.Height);
    Bitmap    bitmap    = new Bitmap(rect.Width, rect.Height);
    Graphics  graphics  = Graphics.FromImage(bitmap);
    Pen       rectPen   = null;
    SizeF     textSize;

    if (this.BorderStyle == BorderStyle.FixedSingle)
    {
        rectPen = new Pen(Color.Black, 1);
    }
    else
    {
        rectPen = new Pen(this.BackColor, 0);
    }

    textSize = graphics.MeasureString(this.Text, this.Font);

    graphics.Clear(this.BackColor);
    graphics.DrawRectangle(rectPen, rect.Left, rect.Top, rect.Width-1, rect.Height-1);

    Point point = CalcOrigin(rect, textSize);

    graphics.SmoothingMode     = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
    graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
    TextRenderer.DrawText(graphics, this.Text, this.Font, point, this.ForeColor, this.BackColor);
    m_bitmap = bitmap;
    graphics.Dispose();
}

This function translates the container control's rectangle to a horizontally-oriented size so that we can paint the text.

private Point CalcOrigin(Rectangle rect, SizeF textSize)
{
    Point point = new Point(0, 0);
    switch (this.TextAlign)
    {
        case ContentAlignment.TopCenter:
            point.X = rect.Left + 
                      (int)((rect.Width - textSize.Width) * 0.5);
            point.Y = rect.Top  + this.Padding.Top;
            break;
        case ContentAlignment.MiddleCenter:
            point.X = rect.Left + 
                      (int)((rect.Width  - textSize.Width) * 0.5);
            point.Y = rect.Top  + 
                      (int)((rect.Height - textSize.Height) * 0.5);
            break;
        case ContentAlignment.BottomCenter:
            point.X = rect.Left   + 
                      (int)((rect.Width   - textSize.Width) * 0.5);
            point.Y = rect.Bottom - 
                      this.Padding.Bottom - (int)textSize.Height;
            break;
        case ContentAlignment.TopLeft:
            point.X = rect.Left + this.Padding.Left;
            point.Y = rect.Top  + this.Padding.Top;
            break;
        case ContentAlignment.MiddleLeft:
            point.X = rect.Left + this.Padding.Left;
            point.Y = rect.Top  + 
                      (int)((rect.Height - (int)textSize.Height) * 0.5);
            break;
        case ContentAlignment.BottomLeft:
            point.X = rect.Left + this.Padding.Left;
            point.Y = rect.Bottom - 
                      this.Padding.Bottom - (int)textSize.Height;
            break;
        case ContentAlignment.TopRight:
            point.X = rect.Right - 
                      this.Padding.Right - (int)textSize.Width;
            point.Y = rect.Top   + this.Padding.Top;
            break;
        case ContentAlignment.MiddleRight:
            point.X = rect.Right - 
                      this.Padding.Right - (int)textSize.Width;
            point.Y = rect.Top   + 
                      (int)((rect.Height - (int)textSize.Height) * 0.5);
            break;
        case ContentAlignment.BottomRight:
            point.X = rect.Right  - 
                      this.Padding.Right  - (int)textSize.Width;
            point.Y = rect.Bottom - 
                      this.Padding.Bottom - (int)textSize.Height;
            break;
    }
    point.X += 1;
    point.Y += 1;
    return point;
}

Initialization is done in the constructor of the form:

private VerticalLabel		m_vertLabel		= new VerticalLabel();
private Bitmap				m_vertLabelBmp	= null;

//--------------------------------------------------------------------------------
public Form1()
{
    // ...more code...

    // initialize our vertical label picture box
    m_vertLabel.Size        = new Size(this.pictureboxSideLabel.Height, 
                                            this.pictureboxSideLabel.Width);
    m_vertLabel.Location    = new Point(0,0);
    m_vertLabel.Font        = new System.Drawing.Font("Arial", 14.25F, 
                              ((System.Drawing.FontStyle)((System.Drawing.FontStyle.Bold | 
                              System.Drawing.FontStyle.Italic))), 
                              System.Drawing.GraphicsUnit.Point, 
                              ((byte)(0)));
    m_vertLabel.TextAlign   = System.Drawing.ContentAlignment.MiddleCenter;
    m_vertLabel.BackColor   = System.Drawing.SystemColors.MenuBar;
    m_vertLabel.ForeColor   = System.Drawing.SystemColors.MenuText;
    m_vertLabel.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
    m_vertLabel.Text        = "Colors To Print";
    m_vertLabel.Visible     = true;
    m_vertLabel.CreateLabelBitmap();
    m_vertLabelBmp          = m_vertLabel.Bitmap;
    this.pictureboxSideLabel.Image = m_vertLabel.Bitmap;

    // ...more code...
}

And finally, the paint event handler does the rotation when the PictureBox control is painted:

private void pictureboxSideLabel_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.Clear(this.pictureboxSideLabel.BackColor);
    e.Graphics.TranslateTransform(0, m_vertLabel.Size.Width);
    e.Graphics.RotateTransform(270);
    int x = ((int)((this.pictureboxSideLabel.Size.Height - m_vertLabel.Size.Width) 
            * 0.5) * -1);
    e.Graphics.DrawImage(m_vertLabelBmp, new Point(x, 0));
}

The ProfileBitmap Object

The ProfileBitmap class is the heart of this application. It's responsible for loading/saving color and image position information, and generating/printing the image used to exercise the print head.

Loading/Saving Settings

The application uses Linq-To-XML for loading and saving settings. It's all pretty straightforward with nothing special occurring during either process. The saving code creates a new file if it needs to, and updates an existing file if it's available. Here's the code:

public void LoadSettings()
{
    // make the printer name a valid XML node name
    string printerName = m_printerName.Replace(" ", "_");;
    if (printerName.Contains("\\"))
    {
        printerName = printerName.Replace("\\", "_");
    }

    // esablish our data file name
    string path = GetDataFileName();

    // if the file exists, load it
    if (File.Exists(path))
    {
        try
        {
            XDocument doc = XDocument.Load(path);
            XElement element = doc.Element("ROOT").Element
                                    ("SETTINGS").Element(printerName);
            // Just because we didn't find the desired element doesn't 
            // mean it's an exception - it might just be a new printer.
            if (element != null)
            {
                XElement currentX  = element.Element("NextPositionX"); 
                XElement currentY  = element.Element("NextPositionY");
                XElement colorBars = element.Element("ColorsToPrint");
                if (currentX != null)
                {
                    m_currentX = Convert.ToInt32(currentX.Value);
                }
                if (currentY != null)
                {
                    m_currentY = Convert.ToInt32(currentY.Value);
                }
                if (colorBars != null)
                {
                    m_colorBars = Utility.MakeColorBars(colorBars.Value);
                }
            }
        }
        catch (Exception ex)
        {
            if (ex != null) {}
            throw;
        }
    }
}

//--------------------------------------------------------------------------------
public void SaveSettings()
{
    string printerName = m_printerName.Replace(" ", "_");;
    if (printerName.Contains("\\"))
    {
        printerName = printerName.Replace("\\", "_");
    }
    string path = GetDataFileName();

    try
    {
        bool newFile = true;
        XDocument doc;

        // if the file exists, load it
        if (File.Exists(path))
        {
            doc = XDocument.Load(path);
            newFile = false;
        }
        // otherwise, create it, and add the root/settings nodes
        else
        {
            doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), 
                                new XComment("AutoPrint data file"));
            var root = new XElement("ROOT",
                       new XElement("SETTINGS"));
            doc.Add(root);
        }

        // establish our printer element
        var element = new XElement(printerName,
                      new XElement("NextPositionX", m_currentX.ToString()),
                      new XElement("NextPositionY", m_currentY.ToString()),
                      new XElement("ColorsToPrint", 
                                   Utility.MakeColorString(m_colorBars)));

        // if the file is new, we'll add the printer element
        if (newFile)
        {
            doc.Element("ROOT").Element("SETTINGS").Add(element);
        }
        // otherwise
        else
        {
            // if the element exists, replavce it with our new one
            if (element != null)
            {
                XElement temp = doc.Element("ROOT").Element
                                ("SETTINGS").Element(printerName);
                if (temp == null)
                {
                    doc.Element("ROOT").Element("SETTINGS").Add(element);
                }
                else
                {
                    doc.Element("ROOT").Element("SETTINGS").Element
                                (printerName).ReplaceWith(element);
                }
            }
            // otherwsie, add it
            else
            {
                doc.Element("ROOT").Element("SETTINGS").Add(element);
            }
        }

        // save the file to disk
        doc.Save(path);
    }
    catch (Exception ex)
    {
        if (ex != null) {}
        throw;
    }
}

Printing/Previewing the Test Image

When you print in .Net, you have to setup an event handler for the PrintPage event. The really neat thing about this is that this event happens for both printing aAND previewing, which means your printing code is your previewing code. Again, this is one of the neater features of the .Net framework.

For our code, we have two preview modes, so we'll have to implement code to distinguish between the two. The normal preview mode shows where the next image will be printed. The "extended preview mode shhows the user what an entire page of test images should look like.

Next, we have a small difference bewteen preview mode and print mode. We only want the image positionadvanced if the user actually prints. What's really cool about this is that even if you preview and then print from the preview form, this code correctly handles the situation, and I didn't have to do *anything* to make that happen.

void printDoc_PrintPage(object sender, PrintPageEventArgs e)
{
    PrintDocument doc = sender as PrintDocument;
    if (doc.PrintController.IsPreview && this.m_isExtendedPreview)
    {
        // save the current X/Y position so we can restore it when we're done
        int oldCurrentX = m_currentX;
        int oldCurrentY = m_currentY;

        // this value allows us to reign in a runaway process. My printer only 
        // has room to print 70 of our nozzle-check images, so I figure a good 
        // value to force a stop would be 100.
        int repeats = 0;

        // since we're previewing in order to test the capacity of our page, 
        // let's start at the top-left corner of the page
        m_currentX = 0;
        m_currentY = 0;

        // print the image until we either run out of room, or we've printed 
        // 100 nozzle-check images on the page.
        do
        {
            PrintImage(e, doc);
            repeats++;
        } while (!m_firstPosition && repeats < 100);

        // reset our current X/Y coordinates.
        m_currentX = oldCurrentX;
        m_currentY = oldCurrentY;
    }
    else
    {
        // this is either a real print job, or a normal preview
        PrintImage(e, doc);
        // we only need to save the settings if we're actually printing because 
        // printing is what advances the current image position
        SaveSettings();
    }
}

Building The Test Image

The test image we use in AutoPrint is generated on the fly. This serves several important purposes.

  • It eliminates the need to burden the application with additional image resource files that the other application uses.
  • It allows a cetain amount of adaptability where adding aditional colorsis concerned.
  • It allows the user to select the colors supported by his printer without worrying about whether or not the correct image resource is available.
  • It allows the user a more conservation-minded approach to paper usage by tracking the position of the printed test image, thus providing a high degree of paper re-use. If the user printed a test image every other day, the same sheet of paper could be used for almost a year before being replaced.

The test image looks something like this:

testimage.png

The first thing we have to do is determine the printable area of the page. Fortunately, the PrintPageEventArgs object contains precisely the information we need.

private void PrintImage(PrintPageEventArgs e, PrintDocument doc)
{
    CountColorsUsed();

    int imageWidth  = 0;
    int imageHeight = 0;

    // to ease typing, we retrieve the printable area from the PrinterSettings 
    // object.
    int maxX = (int)e.PageSettings.PrintableArea.Width;
    int maxY = (int)e.PageSettings.PrintableArea.Height;

As an added bonus, part of the test image includes the date and time at which the image was printed. This can serves a s areminder to the user, helping him to determine the next day he might wat to print a test image. So, here, we determine the current date/time and persome some metric retrieval regarding the size of the date/time string in the image.

    // get the current date
    string   dateString = DateTime.Now.ToString();
    // create our graphics object
    Graphics graphics   = e.Graphics;
    // get the size of the string we'll be printing
    SizeF    textSize   = graphics.MeasureString(dateString, m_font);
    // set our pen width so we can draw our vertical lines
    float    penWidth   = textSize.Height;

Next, we want to set up for the vertical color bars that we'll be painting. We'll be painting them in the order they appear in the Colors To Print listview. In the interest of brevity, the code below only illustrates the black and Cyan bars being painted. Rest assured the rest of them resemble the Cyan painting code.

// keep track of the current position
    float    x          = m_currentX;
    float    y          = m_currentY;
    // specify how tall the lines will be
    float    barHeight  = 50;

    // initialize our brush and pen
    SolidBrush brush    = new SolidBrush(Color.Black);
    Pen        pen      = new Pen(brush, penWidth);

    // draw the text (always in black)
    graphics.DrawString(dateString, m_font, brush, x, y);

    y += penWidth + m_barGap;
    x += (float)penWidth * 0.5f;

    // check to see what colors we need to print, and print the associated 
    // vertical bar(s)
    if ((m_colorBars & ColorBarFlags.Black) == ColorBarFlags.Black)
    {
        graphics.DrawLine(pen, new PointF(x, y), new PointF(x, y+barHeight));
    }

    if ((m_colorBars & ColorBarFlags.Cyan) == ColorBarFlags.Cyan)
    {
        brush.Color = Color.FromArgb(0, 255, 255);
        pen.Brush = brush;
        x += penWidth + m_barGap;
        graphics.DrawLine(pen, new PointF(x, y), new PointF(x, y+barHeight));
    }

    //
    // ... other color bars are painted here
    //

Finally, we calculate the position at which to paint the next image, and advance the current position to that value.

y += barHeight + 2;

    imageWidth = Math.Max((int)textSize.Width, 
                          (int)((float)m_colorsUsed * (penWidth + m_barGap)));

    x = m_currentX + imageWidth;
    pen.Width = 1;
    pen.Color = Color.Black;
    graphics.DrawLine(pen, new PointF(x-imageWidth, y), new PointF(x, y));

    // clean up
    brush.Dispose();
    pen.Dispose();

    // finally we adjust our current position so we can re-use the same sheet 
    // of paper for the test.
    y += 5;
    x += 5;
    imageHeight = (int)y - m_currentY;

    bool advancePosition = ((!doc.PrintController.IsPreview) || 
                            (doc.PrintController.IsPreview && this.IsExtendedPreview));
    if (advancePosition)
    {
        m_currentX = (int)x;
    }
    if (m_currentX + imageWidth > maxX)
    {
        m_currentX = 0;
        m_currentY += imageHeight;
        if (m_currentY + imageHeight > maxY)
        {
            m_currentY = 0;
        }
    }
    // set the value that indicates that we are/aren't at the first print position
    m_firstPosition = (m_currentX == 0 && m_currentY == 0);
}

The ProfileBitmap Class - What's Left

The remainder of the class is comprised mostly of helper methods, and most of those deal with the translation of selected colors between a string representation and the actual ordinal values. We have to do this so that when/if the user creates a CMD file, the command line will be human-readable.

Creating Cmd Files

Another feature of the program is that it allows the user to create .cmd files so that he can utilize the Windows scheduler to print test images on a regular basis. Personally, I would probably never use the software this way, but there are folks out there that want this functionality. When the user clicks the Make Cmd File button, the following code is executed. Notice that a different command line parameter is generated if the Use Saved Colors checkbox is checked.

private void buttonMakeCmdFile_Click(object sender, EventArgs e)
{
    string name    = this.listboxPrinters.SelectedItem.ToString();
    string colors  = Utility.MakeColorString(GetSelectedColors());
    string command = "";
    string exeFile = System.IO.Path.GetFileName(Application.ExecutablePath);

    // determine what type of commandline we want
    if (this.checkboxUseSavedColors.Checked)
    {
        command = string.Format("{0} config=\"{1}\"", exeFile, name.Replace(" ", "_"));
    }
    else
    {
        command = string.Format("{0} name=\"{1}\" colors=\"{2}\"", exeFile, name, colors);
    }

    // determine the path/name of the cmd file
    string path = System.IO.Path.Combine(System.IO.Path.GetDirectoryName
                                         (ProfileBitmap.GetDataFileName()), 
                                         name.Replace(" ", "_") + ".cmd");

    try
    {
        // if the file exists, see if the user wants to overwrite it.
        if (File.Exists(path))
        {
            if (MessageBox.Show(string.Format("The file {0} already exists. Overwrite?", 
                path), "Cmd File Exists", MessageBoxButtons.YesNo) == DialogResult.No)
            {
                return;
            }
            File.Delete(path);
        }
        // Write the file - we need to change to the appropriate drive, 
        // and then to the appropriate folder before trying to execute 
        // this application.
        using (StreamWriter stream = new StreamWriter(path))
        {
            stream.WriteLine(string.Format("{0}", 
                             Application.ExecutablePath.Substring(0,2)));
            stream.WriteLine(string.Format("cd {0}", 
                             System.IO.Path.GetDirectoryName(Application.ExecutablePath)));
            stream.WriteLine(command);
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(string.Format("File could not be written.\n\n{0}\n\n{1}", 
                        ex.Message, ex.StackTrace), "Exception Encountered");
    }
}

The Help System

The Help system utilizes images and an HTML file stored as resources in the application. These resources are extracted to the ProgramData folder the first time the Help menu item is clicked by the user, or if a particular file is missing.

//--------------------------------------------------------------------------------
private void helpToolStripMenuItem_Click(object sender, EventArgs e)
{
    if (ExtractHelpResources())
    {
        FormHelp form = new FormHelp();
        form.ShowDialog();
    }
    else
    {
        MessageBox.Show("Help Resources could not be extracted from " +
                        "the exe file. Help is not avaiilable.", 
                        "Help Not Available");
    }
}

//--------------------------------------------------------------------------------
private bool ExtractHelpResources()
{
    bool extracted = false;

    // Vista won't allow you to modify folder contents in the Program Files 
    // folder, so we have to extract these files to the ProgramData folder.
    string appPath  = System.IO.Path.GetDirectoryName(ProfileBitmap.GetDataFileName());

    // build the fully qualified filenames for our extracted resources
    string helpFileName = System.IO.Path.Combine(appPath, "AutoPrintHelp.html");
    string image1Name = System.IO.Path.Combine(appPath, "screenshot01.png");
    string image2Name = System.IO.Path.Combine(appPath, "screenshot02.png");

    try
    {
        Assembly assembly = Assembly.GetExecutingAssembly();
        ResourceManager rm = new ResourceManager("AutoPrint.Resources", assembly);

        string buffer = "";
        Bitmap bmpBuffer = null;
        // extract screenshot 1
        if (!File.Exists(image1Name))
        {
            bmpBuffer = (Bitmap)rm.GetObject("screenshot01");
            bmpBuffer.Save(image1Name);
            extracted = true;
        }
        else
        {
            extracted = true;
        }

        // extract screenshot 2
        if (!File.Exists(image2Name))
        {
            bmpBuffer = (Bitmap)rm.GetObject("screenshot02");
            bmpBuffer.Save(image2Name);
            extracted = true;
        }
        else
        {
            extracted = true;
        }

        // extract the html file
        if (!File.Exists(helpFileName))
        {
            buffer = (string)rm.GetObject("AutoPrintHelp");
            using (StreamWriter stream = new StreamWriter(helpFileName))
            {
                stream.WriteLine(buffer);
                extracted = true;
            }
        }
        else
        {
            extracted = true;
        }
    }
    catch (Exception ex)
    {
        if (ex != null) {}
        extracted = false;
    }

    return extracted;
}

This HTML file is then displayed in a form containing a WebBrowser control. One item of note is that the Help form parses out the <img> tags and fully qualifies the path to the image file specified therein. For some reason, the WebBrowser control will not show an image unless it is a fully qualified path/file name. So, the form loads the HTML, finds each <img> tag, and adds the ProgramData path to the specified image file name. If you want to see the resulting HTML, you're going to have to enable the IsWebBrowserContextMenuEnabled property and recompile the application so you can get the the View Source menu item in the context menu.

Closing Comments

This application is pretty small and really doesn't do too much, but it exercises a few aspects of the .Net farmework that I've never used before, and has allowed me to discover other aspects of the framework that I had simply ignored. Not everyone needs thisapplication because many consider inkjet printers to be "old tech", but for those of us that have a need/desire to print color labels on CD/DVD disks, inkjet printers are the only viable solution.

I will be making this program available with an installer on my web site that will also allow the user to install .Net 3.5 if it's needed on his system. I only want to do this because there will be users out there that may not be as savvy as we here, at CodeProject.

History

  • 01/05/2008: Original article posted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

John Simmons / outlaw programmer
Software Developer (Senior)
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.
 
My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
Questiona5 Pinmembermilad16712-May-14 10:53 
GeneralMy vote of 5 Pinmembercodekicker4-May-13 22:29 
QuestionUse solvent, not ink... Pinmemberpt140129-Nov-11 22:25 
AnswerRe: Use solvent, not ink... PinmemberBillWoodruff5-Dec-12 1:49 
GeneralProgram will not load in Windows XP Pinmemberrodgoh16-Mar-10 15:32 
GeneralRe: Program will not load in Windows XP PinmemberJohn Simmons / outlaw programmer17-Mar-10 0:19 
GeneralRe: Program will not load in Windows XP Pinmemberrodgoh18-Mar-10 13:28 
GeneralRe: Program will not load in Windows XP PinmvpJohn Simmons / outlaw programmer28-Apr-11 1:47 
GeneralSoftware crash on vista Pinmemberpatdeb2-Jun-09 21:41 
GeneralRe: Software crash on vista PinmemberJohn Simmons / outlaw programmer10-Feb-10 3:21 
GeneralRe: Software crash on vista Pinmemberdotminet22-Sep-10 11:01 
GeneralCrash and Studio Version PinmemberReorX28-Jan-09 3:31 
GeneralRe: Crash and Studio Version [modified] PinmvpJohn Simmons / outlaw programmer16-Feb-09 3:55 
Generalviable ways to determine the "ready" status of a given printer PinmemberDuncan Edwards Jones21-Jan-09 6:36 
GeneralNice article PinmvpPete O'Hanlon4-Jan-09 10:53 
And a 5 from me.
 
"WPF has many lovers. It's a veritable porn star!" - Josh Smith
 

My blog | My articles | MoXAML PowerToys



QuestionWould it be practical to design a selective nozzle cleaner? Pinmembersupercat94-Jan-09 8:01 
AnswerRe: Would it be practical to design a selective nozzle cleaner? PinmvpJohn Simmons / outlaw programmer4-Jan-09 9:05 
GeneralRe: Would it be practical to design a selective nozzle cleaner? Pinmembersupercat94-Jan-09 17:34 
GeneralRe: Would it be practical to design a selective nozzle cleaner? PinmvpJohn Simmons / outlaw programmer4-Jan-09 23:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140814.1 | Last Updated 4 Jan 2009
Article Copyright 2009 by John Simmons / outlaw programmer
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid