EDN Admin
Well-known member
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. <h3>Introduction</h3> 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: http://www.irongeek.com/i.php?page=security/networkprinterhacking. http://www.irongeek.com/i.php?page=security/networkprinterhacking. 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 http://finance.yahoo.com/ finance.yahoo.com . 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. http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/2012-08-27_12-19-35_454%5B4%5D.jpg" rel="lightbox <img title="2012-08-27_12-19-35_454" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/2012-08-27_12-19-35_454_thumb%5B1%5D.jpg" alt="2012-08-27_12-19-35_454" width="640" height="360" border="0 <h4>How to Use the App</h4> The first time you run the app, the main form will appear and youll 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: C#
<pre class="brush: csharp
private void StartWithWindows(bool start)
{
using (RegistryKey hkcu = Registry.CurrentUser)
{
using (RegistryKey runKey = hkcu.OpenSubKey(@"SoftwareMicrosoftWindowsCurrentVersionRun", true))
{
if (runKey == null)
return;
if (start)
runKey.SetValue(wpfapp.Properties.Resources.Code4FunStockPrinter, Assembly.GetEntryAssembly().Location);
else
{
if (runKey.GetValue(wpfapp.Properties.Resources.Code4FunStockPrinter) != null)
runKey.DeleteValue(wpfapp.Properties.Resources.Code4FunStockPrinter);
}
}
}
}
[/code] VB
<pre class="brush: vb
Private Sub StartWithWindows(ByVal start As Boolean)
Using hkcu As RegistryKey = Registry.CurrentUser
Using runKey As RegistryKey = hkcu.OpenSubKey("SoftwareMicrosoftWindowsCurrentVersionRun", True)
If runKey Is Nothing Then
Return
End If
If start Then
runKey.SetValue(My.Resources.Code4FunStockPrinter, System.Reflection.Assembly.GetEntryAssembly().Location)
Else
If runKey.GetValue(My.Resources.Code4FunStockPrinter) IsNot Nothing Then
runKey.DeleteValue(My.Resources.Code4FunStockPrinter)
End If
End If
End Using
End Using
End Sub
[/code] 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 http://msdn.microsoft.com/en-us/library/System.Windows.Threading.DispatcherTimer.aspx 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 http://msdn.microsoft.com/en-us/library/system.windows.application.dispatcherunhandledexception.aspx DispatcherUnhandledException event handler will log the error to a file and alert the user: C#
<pre class="brush: csharp
void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
// stop the timer
_mainWindow.StopPrinterHacking();
// display the error
_mainWindow.LogText("Sending the stock prince to the printer was halted due to this error:" + e.Exception.ToString());
// display the form
ShowMainForm();
// 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;
}
[/code] VB
<pre class="brush: vb
Private Sub App_DispatcherUnhandledException(ByVal sender As Object, ByVal e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)
stop the timer
_mainWindow.StopPrinterHacking()
display the error
_mainWindow.LogText("Sending the stock prince to the printer was halted due to this error:" & e.Exception.ToString())
display the form
ShowMainForm()
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
Loop
End Using
MessageBox.Show("An unexpected error occurred. A stack trace can be found at:" & vbLf & theErrorPath)
e.Handled = True
End Sub
[/code] <h4>The User Interface</h4> The application currently looks like this: http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/image%5B8%5D-2.png" rel="lightbox <img title="image" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/image_thumb%5B2%5D-5.png" alt="image" width="330" height="400" border="0 Pressing the “Get Printer” button opens a dialog that looks like this: <img title="image" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/image_thumb%5B1%5D-3.png" alt="image" width="480" height="320" border="0 The UI was designed with WPF and uses the basic edit controls as well as a theme from the http://wpfthemes.codeplex.com/ 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 http://msdn.microsoft.com/en-us/library/ms379611.aspx 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: C#
<pre class="brush: csharp
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)
{
i++;
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)
logs.Dequeue();
rtb.ScrollToEnd();
}
[/code] <h4>VB
<pre class="brush: vb
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
logs.Dequeue()
End If
rtb.ScrollToEnd()
End Sub
[/code]</h4><h4>Displaying a notification trace icon</h4> 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 http://msdn.microsoft.com/en-us/library/system.windows.application.startup.aspx Application.Startup event in the code behind file. We can use that event to add a http://msdn.microsoft.com/en-us/library/system.windows.forms.notifyicon.aspx WinForms.NotifyIcon and wireup a context menu to it: C#
<pre class="brush: csharp
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())
_mainWindow.Show();
else
_mainWindow.StartPrinterHacking();
}
[/code] VB
<pre class="brush: vb
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
_mainWindow.Show()
Else
_mainWindow.StartPrinterHacking()
End If
End Sub
[/code] <h4>Getting the Stock Information</h4> From the Yahoo Financial site, you get can download a CSV file for any specified stock. Heres a web site that documents the format needed to get the right fields: http://www.gummy-stuff.org/Yahoo-data.htm http://www.gummy-stuff.org/Yahoo-data.htm . 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: http://download.finance.yahoo.com/d/quotes.csv?s=MSFT&f=sl1 http://download.finance.yahoo.com/d/quotes.csv?s=MSFT&f=sl1 You should get a file like this: <img title="quotes_csv" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/quotes_csv_thumb.png" alt="quotes_csv" width="286" height="114" border="0 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 http://developer.yahoo.com/yql/console/ YQL Console that will you let you interactively query against Yahoo! and other web service providers. While its 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: <pre class="brush: sql
select * from csv where url=http://download.finance.yahoo.com/d/quotes.csv?s=MSFT&f=sl1 and columns=symbol,price
[/code] You can see this query loaded into the YQL Console http://y.ahoo.it/ckoII here . http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/yql%5B2%5D.png" rel="lightbox <img title="yql" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/yql_thumb.png" alt="yql" width="640" height="404" border="0 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: <pre class="brush: xml
<?xml version="1.0" encoding="UTF-8"?>
<query xmlns:yahoo="http://www.yahooapis.com/v1/base.rng" yahoo:count="1" yahoo:created="2012-08-23T02:36:06Z" yahoo:lang="en-US
<results>
<row>
<symbol>MSFT</symbol>
<price>30.54</price>
</row>
</results>
</query>
[/code] 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: http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20csv%20where%20url%3Dhttp%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes.csv%3Fs%3DMSFT%26f%3Dsl1%20and%20columns%3Dsymbol%2Cprice http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20csv%20where%20url%3Dhttp%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes.csv%3Fs%3D MSFT %26f%3Dsl1%20and%20columns%3Dsymbol%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: C#
<pre class="brush: csharp
public string GetPriceFromYahoo(string tickerSymbol)
{
string price = string.Empty;
string url = string.Format("http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20csv%20where%20url%3Dhttp%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes.csv%3Fs%3D{0}%26f%3Dsl1%20and%20columns%3Dsymbol%2Cprice", tickerSymbol);
try
{
Uri uri = new Uri(url);
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
XDocument doc = XDocument.Load(resp.GetResponseStream());
resp.Close();
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;
}
[/code] VB
<pre class="brush: vb
Public Function GetPriceFromYahoo(ByVal tickerSymbol As String) As String
Dim price As String
Dim url As String = String.Format("http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20csv%20where%20url%3Dhttp%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes.csv%3Fs%3D{0}%26f%3Dsl1%20and%20columns%3Dsymbol%2Cprice", tickerSymbol)
Try
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())
resp.Close()
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
[/code] 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. <h4>Getting the List of Printers</h4> 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 http://msdn.microsoft.com/en-us/library/aa394582(v=VS.85).aspx Windows Management Instrumentation (WMI) to list the printer TCP/IP addresses that are using port 9100. The WMI class http://msdn.microsoft.com/en-us/library/windows/desktop/aa394492%28v=vs.85%29.aspx 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: C#
<pre class="brush: csharp
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;
}
[/code] VB
<pre class="brush: vb
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
[/code] 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 http://msdn.microsoft.com/en-us/library/system.printing.localprintserver.aspx LocalPrintServer class. The http://msdn.microsoft.com/en-us/library/ms552938.aspx GetPrintQueues method returns a collection of print queues of the type http://msdn.microsoft.com/en-us/library/system.printing.printqueuecollection.aspx 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: C#
<pre class="brush: csharp
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]
}).ToList();
}
[/code] <h4>VB
<pre class="brush: vb
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
[/code]</h4><h4>Sending the Stock Price to the Printer</h4> 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 http://h20000.www2.hp.com/bizsupport/TechSupport/Document.jsp?lang=en&cc=us&objectID=bpl01965&jumpid=reg_r1002_usen_c-001_title_r0001 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 <ESC>%-12345X The format of the packet sent to the printer is be "UEL PJL command UEL". The Ready message display format is @PJL RDYMSG DISPLAY=”message”[<CR>]<LF> 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: C#
<pre class="brush: csharp
public class SendToPrinter
{
public string host { get; set; }
public int Send(string message)
{
IPAddress addr = null;
IPEndPoint endPoint = null;
try
{
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 = ""rnu001B%-12345Xrn";
ASCIIEncoding encoding = new ASCIIEncoding();
try
{
sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
sock.Connect(endPoint);
sock.Send(encoding.GetBytes(head));
sock.Send(encoding.GetBytes(message));
sock.Send(encoding.GetBytes(tail));
sock.Close();
}
catch (Exception e)
{
return 1;
}
int bytes = (head + message + tail).Length;
return 0;
}
}
[/code] VB
<pre class="brush: vb
Public Function Send(ByVal message As String) As Integer
Dim endPoint As IPEndPoint = Nothing
Try
Dim addr As IPAddress = Dns.GetHostAddresses(Host)(0)
endPoint = New IPEndPoint(addr, 9100)
Catch
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()
Try
Dim sock As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP)
sock.Connect(endPoint)
sock.Send(encoding.GetBytes(startPJLSequence))
sock.Send(encoding.GetBytes(message))
sock.Send(encoding.GetBytes(endPJLSequence))
sock.Close()
Catch
Return 1
End Try
Return 0
End Function
[/code] <h4>The Installer</h4> The installer for this app was written with http://wix.sourceforge.net/ 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: <pre class="brush: xml
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi
<Product Id="*" Name="Coding4Fun Printer Display Hack" Language="1033" Version="1.0.0.0" Manufacturer="Coding4Fun" UpgradeCode="e0a3eed3-b61f-46da-9bda-0d546d2a0622
<Package InstallerVersion="200" Compressed="yes" InstallScope="perUser" />
<Property Id="WixAppFolder" Value="WixPerUserFolder" />
[/code] 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 http://msdn.microsoft.com/en-us/library/dd408005%28v=VS.85%29.aspx MSIFASTINSTALL with the value of “1”: <pre class="brush: xml
<Property Id="MSIFASTINSTALL" Value="1" />
[/code] 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: <pre class="brush: xml
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch Printer Display Hack" />
<Property Id="WixShellExecTarget" Value="[#exe]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
<UI>
<UIRef Id="WixUI_MondoNoLicense"/>
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication
WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed
</Publish>
</UI>
[/code] 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 http://msdn.microsoft.com/en-us/library/aa369053%28VS.85%29.aspx 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: <img title="toolsettings" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/toolsettings_thumb.png" alt="toolsettings" width="598" height="295" border="0 <h3>Conclusion</h3> 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! <h3>About The Author</h3> 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 http://www.tvug.net Tech Valley .NET Users Group (TVUG) . You can follow me at https://twitter.com/anotherlab @anotherlab and check out my blog at http://anotherlab.rajapet.net anotherlab.rajapet.net . 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 http://www.brianpeek.com/ 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 . <img src="http://m.webtrends.com/dcs1wotjh10000w0irc493s0e_6x1g/njs.gif?dcssip=channel9.msdn.com&dcsuri=http://channel9.msdn.com/Feeds/RSS&WT.dl=0&WT.entryid=Entry:RSSView:eaaedf601e2149849607a0be004cd67b
View the full article
<pre class="brush: csharp
private void StartWithWindows(bool start)
{
using (RegistryKey hkcu = Registry.CurrentUser)
{
using (RegistryKey runKey = hkcu.OpenSubKey(@"SoftwareMicrosoftWindowsCurrentVersionRun", true))
{
if (runKey == null)
return;
if (start)
runKey.SetValue(wpfapp.Properties.Resources.Code4FunStockPrinter, Assembly.GetEntryAssembly().Location);
else
{
if (runKey.GetValue(wpfapp.Properties.Resources.Code4FunStockPrinter) != null)
runKey.DeleteValue(wpfapp.Properties.Resources.Code4FunStockPrinter);
}
}
}
}
[/code] VB
<pre class="brush: vb
Private Sub StartWithWindows(ByVal start As Boolean)
Using hkcu As RegistryKey = Registry.CurrentUser
Using runKey As RegistryKey = hkcu.OpenSubKey("SoftwareMicrosoftWindowsCurrentVersionRun", True)
If runKey Is Nothing Then
Return
End If
If start Then
runKey.SetValue(My.Resources.Code4FunStockPrinter, System.Reflection.Assembly.GetEntryAssembly().Location)
Else
If runKey.GetValue(My.Resources.Code4FunStockPrinter) IsNot Nothing Then
runKey.DeleteValue(My.Resources.Code4FunStockPrinter)
End If
End If
End Using
End Using
End Sub
[/code] 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 http://msdn.microsoft.com/en-us/library/System.Windows.Threading.DispatcherTimer.aspx 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 http://msdn.microsoft.com/en-us/library/system.windows.application.dispatcherunhandledexception.aspx DispatcherUnhandledException event handler will log the error to a file and alert the user: C#
<pre class="brush: csharp
void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
// stop the timer
_mainWindow.StopPrinterHacking();
// display the error
_mainWindow.LogText("Sending the stock prince to the printer was halted due to this error:" + e.Exception.ToString());
// display the form
ShowMainForm();
// 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;
}
[/code] VB
<pre class="brush: vb
Private Sub App_DispatcherUnhandledException(ByVal sender As Object, ByVal e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)
stop the timer
_mainWindow.StopPrinterHacking()
display the error
_mainWindow.LogText("Sending the stock prince to the printer was halted due to this error:" & e.Exception.ToString())
display the form
ShowMainForm()
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
Loop
End Using
MessageBox.Show("An unexpected error occurred. A stack trace can be found at:" & vbLf & theErrorPath)
e.Handled = True
End Sub
[/code] <h4>The User Interface</h4> The application currently looks like this: http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/image%5B8%5D-2.png" rel="lightbox <img title="image" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/image_thumb%5B2%5D-5.png" alt="image" width="330" height="400" border="0 Pressing the “Get Printer” button opens a dialog that looks like this: <img title="image" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/image_thumb%5B1%5D-3.png" alt="image" width="480" height="320" border="0 The UI was designed with WPF and uses the basic edit controls as well as a theme from the http://wpfthemes.codeplex.com/ 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 http://msdn.microsoft.com/en-us/library/ms379611.aspx 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: C#
<pre class="brush: csharp
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)
{
i++;
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)
logs.Dequeue();
rtb.ScrollToEnd();
}
[/code] <h4>VB
<pre class="brush: vb
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
logs.Dequeue()
End If
rtb.ScrollToEnd()
End Sub
[/code]</h4><h4>Displaying a notification trace icon</h4> 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 http://msdn.microsoft.com/en-us/library/system.windows.application.startup.aspx Application.Startup event in the code behind file. We can use that event to add a http://msdn.microsoft.com/en-us/library/system.windows.forms.notifyicon.aspx WinForms.NotifyIcon and wireup a context menu to it: C#
<pre class="brush: csharp
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())
_mainWindow.Show();
else
_mainWindow.StartPrinterHacking();
}
[/code] VB
<pre class="brush: vb
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
_mainWindow.Show()
Else
_mainWindow.StartPrinterHacking()
End If
End Sub
[/code] <h4>Getting the Stock Information</h4> From the Yahoo Financial site, you get can download a CSV file for any specified stock. Heres a web site that documents the format needed to get the right fields: http://www.gummy-stuff.org/Yahoo-data.htm http://www.gummy-stuff.org/Yahoo-data.htm . 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: http://download.finance.yahoo.com/d/quotes.csv?s=MSFT&f=sl1 http://download.finance.yahoo.com/d/quotes.csv?s=MSFT&f=sl1 You should get a file like this: <img title="quotes_csv" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/quotes_csv_thumb.png" alt="quotes_csv" width="286" height="114" border="0 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 http://developer.yahoo.com/yql/console/ YQL Console that will you let you interactively query against Yahoo! and other web service providers. While its 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: <pre class="brush: sql
select * from csv where url=http://download.finance.yahoo.com/d/quotes.csv?s=MSFT&f=sl1 and columns=symbol,price
[/code] You can see this query loaded into the YQL Console http://y.ahoo.it/ckoII here . http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/yql%5B2%5D.png" rel="lightbox <img title="yql" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/yql_thumb.png" alt="yql" width="640" height="404" border="0 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: <pre class="brush: xml
<?xml version="1.0" encoding="UTF-8"?>
<query xmlns:yahoo="http://www.yahooapis.com/v1/base.rng" yahoo:count="1" yahoo:created="2012-08-23T02:36:06Z" yahoo:lang="en-US
<results>
<row>
<symbol>MSFT</symbol>
<price>30.54</price>
</row>
</results>
</query>
[/code] 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: http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20csv%20where%20url%3Dhttp%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes.csv%3Fs%3DMSFT%26f%3Dsl1%20and%20columns%3Dsymbol%2Cprice http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20csv%20where%20url%3Dhttp%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes.csv%3Fs%3D MSFT %26f%3Dsl1%20and%20columns%3Dsymbol%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: C#
<pre class="brush: csharp
public string GetPriceFromYahoo(string tickerSymbol)
{
string price = string.Empty;
string url = string.Format("http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20csv%20where%20url%3Dhttp%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes.csv%3Fs%3D{0}%26f%3Dsl1%20and%20columns%3Dsymbol%2Cprice", tickerSymbol);
try
{
Uri uri = new Uri(url);
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
XDocument doc = XDocument.Load(resp.GetResponseStream());
resp.Close();
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;
}
[/code] VB
<pre class="brush: vb
Public Function GetPriceFromYahoo(ByVal tickerSymbol As String) As String
Dim price As String
Dim url As String = String.Format("http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20csv%20where%20url%3Dhttp%3A%2F%2Fdownload.finance.yahoo.com%2Fd%2Fquotes.csv%3Fs%3D{0}%26f%3Dsl1%20and%20columns%3Dsymbol%2Cprice", tickerSymbol)
Try
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())
resp.Close()
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
[/code] 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. <h4>Getting the List of Printers</h4> 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 http://msdn.microsoft.com/en-us/library/aa394582(v=VS.85).aspx Windows Management Instrumentation (WMI) to list the printer TCP/IP addresses that are using port 9100. The WMI class http://msdn.microsoft.com/en-us/library/windows/desktop/aa394492%28v=vs.85%29.aspx 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: C#
<pre class="brush: csharp
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;
}
[/code] VB
<pre class="brush: vb
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
[/code] 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 http://msdn.microsoft.com/en-us/library/system.printing.localprintserver.aspx LocalPrintServer class. The http://msdn.microsoft.com/en-us/library/ms552938.aspx GetPrintQueues method returns a collection of print queues of the type http://msdn.microsoft.com/en-us/library/system.printing.printqueuecollection.aspx 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: C#
<pre class="brush: csharp
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]
}).ToList();
}
[/code] <h4>VB
<pre class="brush: vb
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
[/code]</h4><h4>Sending the Stock Price to the Printer</h4> 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 http://h20000.www2.hp.com/bizsupport/TechSupport/Document.jsp?lang=en&cc=us&objectID=bpl01965&jumpid=reg_r1002_usen_c-001_title_r0001 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 <ESC>%-12345X The format of the packet sent to the printer is be "UEL PJL command UEL". The Ready message display format is @PJL RDYMSG DISPLAY=”message”[<CR>]<LF> 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: C#
<pre class="brush: csharp
public class SendToPrinter
{
public string host { get; set; }
public int Send(string message)
{
IPAddress addr = null;
IPEndPoint endPoint = null;
try
{
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 = ""rnu001B%-12345Xrn";
ASCIIEncoding encoding = new ASCIIEncoding();
try
{
sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
sock.Connect(endPoint);
sock.Send(encoding.GetBytes(head));
sock.Send(encoding.GetBytes(message));
sock.Send(encoding.GetBytes(tail));
sock.Close();
}
catch (Exception e)
{
return 1;
}
int bytes = (head + message + tail).Length;
return 0;
}
}
[/code] VB
<pre class="brush: vb
Public Function Send(ByVal message As String) As Integer
Dim endPoint As IPEndPoint = Nothing
Try
Dim addr As IPAddress = Dns.GetHostAddresses(Host)(0)
endPoint = New IPEndPoint(addr, 9100)
Catch
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()
Try
Dim sock As New Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP)
sock.Connect(endPoint)
sock.Send(encoding.GetBytes(startPJLSequence))
sock.Send(encoding.GetBytes(message))
sock.Send(encoding.GetBytes(endPJLSequence))
sock.Close()
Catch
Return 1
End Try
Return 0
End Function
[/code] <h4>The Installer</h4> The installer for this app was written with http://wix.sourceforge.net/ 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: <pre class="brush: xml
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi
<Product Id="*" Name="Coding4Fun Printer Display Hack" Language="1033" Version="1.0.0.0" Manufacturer="Coding4Fun" UpgradeCode="e0a3eed3-b61f-46da-9bda-0d546d2a0622
<Package InstallerVersion="200" Compressed="yes" InstallScope="perUser" />
<Property Id="WixAppFolder" Value="WixPerUserFolder" />
[/code] 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 http://msdn.microsoft.com/en-us/library/dd408005%28v=VS.85%29.aspx MSIFASTINSTALL with the value of “1”: <pre class="brush: xml
<Property Id="MSIFASTINSTALL" Value="1" />
[/code] 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: <pre class="brush: xml
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch Printer Display Hack" />
<Property Id="WixShellExecTarget" Value="[#exe]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />
<UI>
<UIRef Id="WixUI_MondoNoLicense"/>
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication
WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed
</Publish>
</UI>
[/code] 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 http://msdn.microsoft.com/en-us/library/aa369053%28VS.85%29.aspx 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: <img title="toolsettings" src="http://files.channel9.msdn.com/wlwimages/1932b237046e4743a4e79e6800c0220f/toolsettings_thumb.png" alt="toolsettings" width="598" height="295" border="0 <h3>Conclusion</h3> 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! <h3>About The Author</h3> 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 http://www.tvug.net Tech Valley .NET Users Group (TVUG) . You can follow me at https://twitter.com/anotherlab @anotherlab and check out my blog at http://anotherlab.rajapet.net anotherlab.rajapet.net . 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 http://www.brianpeek.com/ 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 . <img src="http://m.webtrends.com/dcs1wotjh10000w0irc493s0e_6x1g/njs.gif?dcssip=channel9.msdn.com&dcsuri=http://channel9.msdn.com/Feeds/RSS&WT.dl=0&WT.entryid=Entry:RSSView:eaaedf601e2149849607a0be004cd67b
View the full article