Keep Your InkJet Print Head Clean
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.

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.

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:

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:

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:

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.