The HP Printer Display Hack (with financial goodness)

Sign in to queue


The HP Printer Display Hack is a simple background application that periodically checks the current price of a selected stock and sends it to the display of HP (and compatible) laser printers.


This app is based on an old hack from back to at least 1997 that uses the HP Job control language to change the text on the LCD status display. Some background on this hack can be found here: There are various versions of the hack code out there, and typically they all work the same way: you specify the address of the printer and the message to send, open a TCP connection to the printer over port 9100, and then send a command to update the display.

This app is a variation of that hack. It’s a tray application that periodically checks the stock price for a company and then sends a formatted message of the stock symbol and price to a specified printer.

To get the current stock price, we retrieve the data from Yahoo! through The data comes back in CSV format. To save a step in parsing the CSV columns, we use YQL, the Yahoo! Query Language. Yahoo! created YQL to provide a SQL-like API for querying data from various online web services. YQL! can return XML or JSON data, and we’ll take the XML and use LINQ to parse the data.


How to Use the App

The first time you run the app, the main form will appear and you'll be able to enter in the stock symbol and the IP address of your printer. Click the “Get Printer” button to view a dialog listing the available printers connected on port 9100.

There are two checkboxes. The first one is labeled “Start with Windows”. When this setting is saved, the following code is executed to tell Windows whether to start the app when user logs in:


private void StartWithWindows(bool start) 
    using (RegistryKey hkcu = Registry.CurrentUser) 
        using (RegistryKey runKey = hkcu.OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true)) 
            if (runKey == null) 
            if (start) 
                runKey.SetValue(wpfapp.Properties.Resources.Code4FunStockPrinter, Assembly.GetEntryAssembly().Location); 
                if (runKey.GetValue(wpfapp.Properties.Resources.Code4FunStockPrinter) != null) 


Private Sub StartWithWindows(ByVal start As Boolean)
    Using hkcu As RegistryKey = Registry.CurrentUser
        Using runKey As RegistryKey = hkcu.OpenSubKey("Software\Microsoft\Windows\CurrentVersion\Run", True)
            If runKey Is Nothing Then
            End If

            If start Then
                runKey.SetValue(My.Resources.Code4FunStockPrinter, System.Reflection.Assembly.GetEntryAssembly().Location)
                If runKey.GetValue(My.Resources.Code4FunStockPrinter) IsNot Nothing Then
                End If
            End If
        End Using
    End Using
End Sub

The enabled checkbox is used so that you can pause the sending of the stock price to the printer without having to exit the app. When you press the “Start” button, you are prompted to save any changed settings and the app hides the main form, leaving just the system tray icon. While the app is running, it will check the stock price every 5 minutes. If the price has changed, it tells the printer to display the stock symbol and price on the display.

A DispatcherTimer object is used to determine when to check the stock price. It’s created when the main form is created and will only execute the update code when the settings have been defined and enabled.

If an unexpected error occurs, the DispatcherUnhandledException event handler will log the error to a file and alert the user:


void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) 
    // stop the timer 

    // display the error 
    _mainWindow.LogText("Sending the stock prince to the printer was halted due to this error:" + e.Exception.ToString()); 

    // display the form 

    // Log the error to a file and notify the user 
    Exception theException = e.Exception; 
    string theErrorPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\PrinterDisplayHackError.txt"; 

    using (System.IO.TextWriter theTextWriter = new System.IO.StreamWriter(theErrorPath, true)) 
        DateTime theNow = DateTime.Now; 
        theTextWriter.WriteLine(String.Format("The error time: {0} {1}", theNow.ToShortDateString(), theNow.ToShortTimeString())); 
        while (theException != null) 
            theTextWriter.WriteLine("Exception: " + theException.ToString()); 
            theException = theException.InnerException; 

    MessageBox.Show("An unexpected error occurred. A stack trace can be found at:\n" + theErrorPath); 
    e.Handled = true; 


Private Sub App_DispatcherUnhandledException(ByVal sender As Object, ByVal e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)
    ' stop the timer

    ' display the error
    _mainWindow.LogText("Sending the stock prince to the printer was halted due to this error:" & e.Exception.ToString())

    ' display the form

    ' Log the error to a file and notify the user
    Dim theException As Exception = e.Exception
    Dim theErrorPath As String = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) & "\PrinterDisplayHackError.txt"
    Using theTextWriter As System.IO.TextWriter = New System.IO.StreamWriter(theErrorPath, True)
        Dim theNow As Date = Date.Now
        theTextWriter.WriteLine(String.Format("The error time: {0} {1}", theNow.ToShortDateString(), theNow.ToShortTimeString()))
        Do While theException IsNot Nothing
            theTextWriter.WriteLine("Exception: " & theException.ToString())
            theException = theException.InnerException
    End Using
    MessageBox.Show("An unexpected error occurred.  A stack trace can be found at:" & vbLf & theErrorPath)
    e.Handled = True
End Sub


The User Interface

The application currently looks like this:


Pressing the “Get Printer” button opens a dialog that looks like this:


The UI was designed with WPF and uses the basic edit controls as well as a theme from the WPF Themes project on CodePlex. On the main form, the stock symbol, printer IP address, and the check boxes using data bindings to bind each control to a custom setting are defined in the PrinterHackSettings class.

The settings are defined in a class descended from ApplicationSettingsBase. The .NET runtime will read and write the settings based on the rules defined here.

The big RichTextBog in the center of the form is used to display the last 10 stock price updates. The app keeps a queue of the stock price updates, and when the queue is updated it’s sent to the RichTextBox with the following code:


public void UpdateLog(RichTextBox rtb) 
    int i = 0; 
    TextRange textRange = new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd); 
    textRange.Text = string.Empty; 
    foreach (var lg in logs) 
        TextRange tr = new TextRange(rtb.Document.ContentEnd, rtb.Document.ContentEnd) { Text = String.Format("{0} : ", lg.LogTime.ToString("hh:mm:ss")) }; 
        tr.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.DarkRed); 
        tr = new TextRange(rtb.Document.ContentEnd, rtb.Document.ContentEnd) { Text = lg.LogMessage + Environment.NewLine }; 
        tr.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Black); 
    if (i > 10) 



Public Sub UpdateLog(ByVal rtb As RichTextBox)
    Dim i As Integer = 0

    For Each lg As LogEntry In logs
        i += 1
        Dim tr As New TextRange(rtb.Document.ContentEnd, rtb.Document.ContentEnd) With {.Text = String.Format("{0} : ", lg.LogTime.ToString("hh:mm:ss"))}
        tr.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Red)

        tr = New TextRange(rtb.Document.ContentEnd, rtb.Document.ContentEnd) With {.Text = lg.LogMessage & Environment.NewLine}
        tr.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.White)
    Next lg

    If i > 10 Then
    End If

End Sub

Displaying a notification trace icon

WPF does not provide any functionality for running an app with just an icon in the notification area of the taskbar. We need to tap into some WinForms functionality. Add a reference to the System.Windows.Form namespace to the project. In the App.xaml file, add an event handler to the Startup event. Visual Studio will wire up an Application.Startup event in the code behind file. We can use that event to add a WinForms.NotifyIcon and wireup a context menu to it:


private void Application_Startup(object sender, StartupEventArgs e) 
    _notifyIcon = new WinForms.NotifyIcon(); 
    _notifyIcon.DoubleClick += notifyIcon_DoubleClick; 
    _notifyIcon.Icon = wpfapp.Properties.Resources.Icon; 
    _notifyIcon.Visible = true; 

    WinForms.MenuItem[] items = new[] 
        new WinForms.MenuItem("&Settings", Settings_Click) { DefaultItem = true } , 
        new WinForms.MenuItem("-"), 
        new WinForms.MenuItem("&Exit", Exit_Click) 

    _notifyIcon.ContextMenu = new WinForms.ContextMenu(items); 
    _mainWindow = new MainWindow(); 

    if (!_mainWindow.SettingsAreValid()) 


Private Sub Application_Startup(ByVal sender As Object, ByVal e As StartupEventArgs)
    _notifyIcon = New System.Windows.Forms.NotifyIcon()
    AddHandler _notifyIcon.DoubleClick, AddressOf notifyIcon_DoubleClick

    _notifyIcon.Icon = My.Resources.Icon
    _notifyIcon.Visible = True

    Dim items() As System.Windows.Forms.MenuItem = {New System.Windows.Forms.MenuItem("&Settings", AddressOf Settings_Click) With {.DefaultItem = True}, New System.Windows.Forms.MenuItem("-"), New System.Windows.Forms.MenuItem("&Exit", AddressOf Exit_Click)}

    _notifyIcon.ContextMenu = New System.Windows.Forms.ContextMenu(items)

    _mainWindow = New MainWindow()

    If Not _mainWindow.SettingsAreValid() Then
    End If
End Sub


Getting the Stock Information

From the Yahoo Financial site, you get can download a CSV file for any specified stock. Here's a web site that documents the format needed to get the right fields: We want to return the stock symbol and the last traded price. That works out to be “s” and “l1”, respectively.

If you open the following URL with a browser, a file named quotes.csv will be returned:

You should get a file like this:


The first field is the stock symbol and the second is the last recorded price. You could just read that data and parse out the fields, but we can get the data in more readable format.

Yahoo! has a tool called the YQL Console that will you let you interactively query against Yahoo! and other web service providers. While it's overkill to use on a two column CSV file, it can be used to tie together data from multiple services.

To use our MSFT stock query with YQL, we format the query like this:

select * from csv where url='' and columns='symbol,price'

You can see this query loaded into the YQL Console here.


When you click the “TEST” button, the YQL query is executed and the results displayed in the lower panel. By default, the results are in XML, but you can also get the data back in JSON format.

Our result set has been transformed into the following XML:

<?xml version="1.0" encoding="UTF-8"?> 
<query xmlns:yahoo="" yahoo:count="1" yahoo:created="2012-08-23T02:36:06Z" yahoo:lang="en-US"> 


This XML document can be easily parsed in the application code. The URL listed below “THE REST QUERY” on the YQL page is the YQL query encoded so that it can be sent as a GET request. For this YQL query, we use the following URL:*%20from%20csv%20where%20url%3D''%20and%20columns%3D'symbol%2Cprice'

This is the URL that our application uses to get the stock price. Notice the MSFT in bold face—we replace that hard coded stock symbol with a format item and just use String.Format() to generate the URL at run time.

To get the stock price from our code, we can wrap this with the following method:


public string GetPriceFromYahoo(string tickerSymbol) 
    string price = string.Empty; 
    string url = string.Format("*%20from%20csv%20where%20url%3D'{0}%26f%3Dsl1'%20and%20columns%3D'symbol%2Cprice'", tickerSymbol); 

        Uri uri = new Uri(url); 
        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri); 
        HttpWebResponse resp = (HttpWebResponse)req.GetResponse(); 
        XDocument doc = XDocument.Load(resp.GetResponseStream()); 
        var ticker =    from query in doc.Descendants("query") 
                        from results in query.Descendants("results") 
                        from row in query.Descendants("row") 
                        select new { price = row.Element("price").Value }; 

        price = ticker.First().price; 
    catch (Exception ex) 
        price = "Exception retrieving symbol: " + ex.Message; 
    return price; 


Public Function GetPriceFromYahoo(ByVal tickerSymbol As String) As String
    Dim price As String

    Dim url As String = String.Format("*%20from%20csv%20where%20url%3D'{0}%26f%3Dsl1'%20and%20columns%3D'symbol%2Cprice'", tickerSymbol)

        Dim uri As New Uri(url)

        Dim req As HttpWebRequest = CType(WebRequest.Create(uri), HttpWebRequest)
        Dim resp As HttpWebResponse = CType(req.GetResponse(), HttpWebResponse)

        Dim doc As XDocument = XDocument.Load(resp.GetResponseStream())


        Dim ticker = From query In doc.Descendants("query") , results In query.Descendants("results") , row In query.Descendants("row") _
                     Let xElement = row.Element("price") _
                     Where xElement IsNot Nothing _
                     Select New With {Key .price = xElement.Value}

        price = ticker.First().price
    Catch ex As Exception
        price = "Exception retrieving symbol: " & ex.Message
    End Try

    Return price
End Function


While this code makes the readying of a two column CSV file more complicated than it needs to be, it makes it easier to adapt this code to read the results for multiple stock symbols and/or additional fields.

Getting the List of Printers

We are targeting a specific type of printer: those that use the HP PJL command set. Since we talk to these printers over port 9100, we only need to list the printers that listen on that port. We can use Windows Management Instrumentation (WMI) to list the printer TCP/IP addresses that are using port 9100.  The WMI class Win32_TCPIPPrinterPort can be used for that purpose, and we’ll use the following WMI query:

Select Name, HostAddress from Win32_TCPIPPrinterPort where PortNumber = 9100

This returns the list of port names and addresses on your computer that are being used over port 9100. Take that list and store it in a dictionary for a quick lookup:


static public Dictionary<string, IPAddress> GetPrinterPorts()
    var ports = new Dictionary<string, IPAddress>(); 

    ObjectQuery oquery = new ObjectQuery("Select Name, HostAddress from Win32_TCPIPPrinterPort where PortNumber = 9100"); 
    ManagementObjectSearcher mosearcher = new ManagementObjectSearcher(oquery); 

    using (var searcher = new ManagementObjectSearcher(oquery)) 
        var objectCollection = searcher.Get(); 
        foreach (ManagementObject managementObjectCollection in objectCollection) 
            var portAddress = IPAddress.Parse(managementObjectCollection.GetPropertyValue("HostAddress").ToString()); 
            ports.Add(managementObjectCollection.GetPropertyValue("Name").ToString(), portAddress); 

    return ports; 


Public Shared Function GetPrinterPorts() As Dictionary(Of String, IPAddress)
    Dim ports = New Dictionary(Of String, IPAddress)()

    Dim oquery As New ObjectQuery("Select Name, HostAddress from Win32_TCPIPPrinterPort where PortNumber = 9100")

    Dim mosearcher As New ManagementObjectSearcher(oquery)

    Using searcher = New ManagementObjectSearcher(oquery)
        Dim objectCollection = searcher.Get()

        For Each managementObjectCollection As ManagementObject In objectCollection
            Dim portAddress = IPAddress.Parse(managementObjectCollection.GetPropertyValue("HostAddress").ToString())
            ports.Add(managementObjectCollection.GetPropertyValue("Name").ToString(), portAddress)
        Next managementObjectCollection
    End Using

    Return ports
End Function


Next, we get the list of printers that this computer knows about. We could do that through WMI, but I decided to stay closer to the .NET Framework and use the LocalPrintServer class. The GetPrintQueues method returns a collection of print queues of the type PrintQueueCollection. We can then iterate through the PrintQueueCollection and look for all printers that have a port name that matches the names returned by the WMI query. That gives code that looks like this:


public class LocalPrinter 
    public string Name { get; set; } 
    public string PortName { get; set; } 
    public IPAddress Address { get; set; } 

static public List<LocalPrinter> GetPrinters() 
    Dictionary<string, IPAddress> ports = GetPrinterPorts(); 
    EnumeratedPrintQueueTypes[] enumerationFlags = { EnumeratedPrintQueueTypes.Local }; 

    LocalPrintServer printServer = new LocalPrintServer(); 
    PrintQueueCollection printQueuesOnLocalServer = printServer.GetPrintQueues(enumerationFlags); 

    return (from printer in printQueuesOnLocalServer 
            where ports.ContainsKey(printer.QueuePort.Name) 
            select new LocalPrinter() 
                Name = printer.Name, 
                PortName = printer.QueuePort.Name, 
                Address = ports[printer.QueuePort.Name] 


Public Class LocalPrinter
    Public Property Name() As String
    Public Property PortName() As String
    Public Property Address() As IPAddress
End Class

Public Shared Function GetPrinters() As List(Of LocalPrinter)
    Dim ports As Dictionary(Of String, IPAddress) = GetPrinterPorts()

    Dim enumerationFlags() As EnumeratedPrintQueueTypes = { EnumeratedPrintQueueTypes.Local }

    Dim printServer As New LocalPrintServer()

    Dim printQueuesOnLocalServer As PrintQueueCollection = printServer.GetPrintQueues(enumerationFlags)

    Return ( _
            From printer In printQueuesOnLocalServer _
            Where ports.ContainsKey(printer.QueuePort.Name) _
            Select New LocalPrinter() With {.Name = printer.Name, .PortName = printer.QueuePort.Name, .Address = ports(printer.QueuePort.Name)}).ToList()
End Function

Sending the Stock Price to the Printer

The way to send a message to a HP display is via a PJL command. PJL stands for Printer Job Language. Not all PJL commands are recognized by every HP printer, but if you have an HP laser printer with a display, the command should work. This should work for any printer that is compatible with HP’s PJL command set. For the common PJL commands, HP has an online document here.

We will be using the “Ready message display” PJL command. All PJL commands will start and end with a sequence of bytes called the “Universal Exit Language” or UEL. This sequence tells the printer that it’s about to receive a PJL command. The UEL is defined as


The format of the packet sent to the printer is be "UEL PJL command UEL". The Ready message display format is


To send the command that has the printer display “Hello World”, you would send the following sequence:

<ESC>%-12345X@PJL RDYMSG DISPLAY=”Hello World”[<CR>]<LF><ESC>%-12345X[<CR>]<LF>

We wrap this up in a class called SendToPrinter and the good stuff gets executed in the Send method, as listed below:


public class SendToPrinter 
    public string host { get; set; } 
    public int Send(string message) 
        IPAddress addr = null; 
        IPEndPoint endPoint = null; 

            addr = Dns.GetHostAddresses(host)[0]; 
            endPoint = new IPEndPoint(addr, 9100); 
        catch (Exception e) 
            return 1; 

        Socket sock = null; 
        String head = "\u001B%-12345X@PJL RDYMSG DISPLAY = \""; 
        String tail = "\"\r\n\u001B%-12345X\r\n"; 
        ASCIIEncoding encoding = new ASCIIEncoding(); 

            sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); 
        catch (Exception e) 
            return 1; 

        int bytes = (head + message + tail).Length; 
        return 0; 


Public Function Send(ByVal message As String) As Integer
    Dim endPoint As IPEndPoint = Nothing

        Dim addr As IPAddress = Dns.GetHostAddresses(Host)(0)

        endPoint = New IPEndPoint(addr, 9100)
        Return 1
    End Try

    Dim startPJLSequence As String = ChrW(&H1B).ToString() & "%-12345X@PJL RDYMSG DISPLAY = """
    Dim endPJLSequence As String = """" & vbCrLf & ChrW(&H1B).ToString() & "%-12345X" & vbCrLf

    Dim encoding As New ASCIIEncoding()

        Dim sock As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP)



        Return 1
    End Try

    Return 0
End Function


The Installer

The installer for this app was written with WiX, Windows Installer XML. WiX is an open source project created by Rob Mensching that lets you build Windows Installer .msi and .msm files from XML source code. I used the release candidate of WiX 3.6, but any recent version should work. Of course, you don’t need an installer if you build the app yourself.

Setting InstalScope to “perUser” designates this package as being a per-user install. Adding the property “WixAppFolder” and set to “WixPerUserFolder” tells WiX to install this app under %LOCALAPPDATA% instead of under %ProgramFiles%. This eliminates the need for the installer to request elevated rights and the UAC prompt:

<Wix xmlns=""> 
    <Product Id="*" Name="Coding4Fun Printer Display Hack" Language="1033" Version="" Manufacturer="Coding4Fun" UpgradeCode="e0a3eed3-b61f-46da-9bda-0d546d2a0622"> 
    <Package InstallerVersion="200" Compressed="yes" InstallScope="perUser" /> 
    <Property Id="WixAppFolder" Value="WixPerUserFolder" />

Because we are not touching any system settings, I eliminated the creation of a system restore point at the start of the installation process. This greatly speeds up the installation of the app, and is handled by adding a property named MSIFASTINSTALL with the value of “1”:

<Property Id="MSIFASTINSTALL" Value="1" />

I modified the UI sequence to skip over the end user license agreement. There is nothing to license here and no one reads EULAs anyways. To do this, I needed to download the WiX source code and extract a file named WixUI_Mondo.wxs. I added it to the installer project and renamed it to WixUI_MondoNoLicense.wxs. I also added a checkbox to the exit dialog to allow the user to launch the app after it been installed:

<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch Printer Display Hack" /> 
<Property Id="WixShellExecTarget" Value="[#exe]" /> 
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" /> 
    <UIRef Id="WixUI_MondoNoLicense"/> 
    <Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication"> 


When you build the installer, it generates two ICE91 warning messages. An ICE91 warning occurs when you install a file or shortcut into a per-user only folder. Since we have explicitly set the InstallScope to “perUser”, we can safely ignore these two warnings. If you hate warning messages, you can use the tool settings for the installer project to suppress ICE91 validation checks:



I have had various versions of this app running in my office for over a year. It’s been set to show our current stock price on the main printer in the development department. It’s fun to watch people walk near the printer just to check out the current stock price.

If you want to try this out, the download link for the source code and installer is at the top of the article!

About The Author

I am a senior R&D engineer for Tyler Technologies, working on our next generation of school bus routing software. I also am the leader of the Tech Valley .NET Users Group (TVUG). You can follow me at @anotherlab and check out my blog at I would list my G+ address, but I don’t use it. I started out with a VIC-20 and been slowly moving up the CPU food chain ever since.

I would like to thank Brian Peek on the Coding4Fun team for his encouragement and suggestions and for letting me steal large chunks of the UI code from his TweeVo project .


WiX, WPF, Printers

The Discussion

Add Your 2 Cents