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

Monitoring a Printer Queue from VB.NET

By , 2 Jun 2009
 

Introduction

Some of the API calls used in this example are only supported on Windows NT, 2000, XP and .NET server. Therefore this technique does not apply to Windows 95, Windows 98 or Windows ME.

Although VB.NET printer handling has improved immeasurably over that offered by Visual Basic 6, there is still a need to turn to the Windows API in order to monitor a print queue.

Get a Handle to the Printer You Want to Monitor

All of the API calls that access the printer or spooler need a printer handle. This is obtained by passing the unique printer device name to the OpenPrinter API call and must be released by the ClosePrinter API call when it is no longer needed.

<DllImport("winspool.drv", EntryPoint:="OpenPrinterA", _ 
    SetLastError:=True, CharSet:=CharSet.Ansi, _ 
    ExactSpelling:=True, _ 
    CallingConvention:=CallingConvention.StdCall)> _ 
    Public Shared Function OpenPrinter(ByVal pPrinterName As String, _ 
    ByRef phPrinter As Int32, _ 
    ByVal pDefault As Int32) As Boolean 

    End Function 

<DllImport("winspool.drv", EntryPoint:="ClosePrinter", _ 
    SetLastError:=True, _ 
    ExactSpelling:=True, _ 
    CallingConvention:=CallingConvention.StdCall)> _ 

    Public Shared Function ClosePrinter _
    (ByVal hPrinter As Int32) As Boolean 

    End Function 

Ask for the Notifications You are Interested in

To minimise the impact of a printer watch on system performance, we can specify precisely which printer events we are interested in. This is done by passing a parameter to FindFirstPrinterChangeNotification using one or more of the following values:

Public Enum Printer_Change_Notification_General_Flags 
    PRINTER_CHANGE_FORM = &H70000 
    PRINTER_CHANGE_PORT = &H700000 
    PRINTER_CHANGE_JOB = &HFF00 
    PRINTER_CHANGE_PRINTER = &HFF 
    PRINTER_CHANGE_PRINT_PROCESSOR = &H7000000 
    PRINTER_CHANGE_PRINTER_DRIVER = &H70000000 
End Enum 

Public Enum Printer_Change_Notification_Form_Flags 
    PRINTER_CHANGE_ADD_FORM = &H10000 
    PRINTER_CHANGE_SET_FORM = &H20000 
    PRINTER_CHANGE_DELETE_FORM = &H40000 
End Enum 

Public Enum Printer_Change_Notification_Port_Flags 
    PRINTER_CHANGE_ADD_PORT = &H100000 
    PRINTER_CHANGE_CONFIGURE_PORT = &H200000 
    PRINTER_CHANGE_DELETE_PORT = &H400000 
End Enum 

Public Enum Printer_Change_Notification_Job_Flags 
    PRINTER_CHANGE_ADD_JOB = &H100 
    PRINTER_CHANGE_SET_JOB = &H200 
    PRINTER_CHANGE_DELETE_JOB = &H400 
    PRINTER_CHANGE_WRITE_JOB = &H800 
End Enum 

Public Enum Printer_Change_Notification_Printer_Flags 
    PRINTER_CHANGE_ADD_PRINTER = &H1 
    PRINTER_CHANGE_SET_PRINTER = &H2 
    PRINTER_CHANGE_DELETE_PRINTER = &H4 
    PRINTER_CHANGE_FAILED_CONNECTION_PRINTER = &H8 
End Enum 

Public Enum Printer_Change_Notification_Processor_Flags 
    PRINTER_CHANGE_ADD_PRINT_PROCESSOR = &H1000000 
    PRINTER_CHANGE_DELETE_PRINT_PROCESSOR = &H4000000 
End Enum 

Public Enum Printer_Change_Notification_Driver_Flags 
    PRINTER_CHANGE_ADD_PRINTER_DRIVER = &H10000000 
    PRINTER_CHANGE_SET_PRINTER_DRIVER = &H20000000 
    PRINTER_CHANGE_DELETE_PRINTER_DRIVER = &H40000000
End Enum

Specify the Information You want Returned for the Event

When an event occurs -- for example, if a job is added to the print queue -- you will probably want to get information about the job that caused that event. Again, in order to minimise the impact on the system, you specify exactly which fields you want information from. For a print job event, the possible fields are:

 Public Enum Job_Notify_Field_Indexes 
    JOB_NOTIFY_FIELD_PRINTER_NAME = &H0 
    JOB_NOTIFY_FIELD_MACHINE_NAME = &H1 
    JOB_NOTIFY_FIELD_PORT_NAME = &H2 
    JOB_NOTIFY_FIELD_USER_NAME = &H3 
    JOB_NOTIFY_FIELD_NOTIFY_NAME = &H4 
    JOB_NOTIFY_FIELD_DATATYPE = &H5 
    JOB_NOTIFY_FIELD_PRINT_PROCESSOR = &H6 
    JOB_NOTIFY_FIELD_PARAMETERS = &H7 
    JOB_NOTIFY_FIELD_DRIVER_NAME = &H8 
    JOB_NOTIFY_FIELD_DEVMODE = &H9 
    JOB_NOTIFY_FIELD_STATUS = &HA 
    JOB_NOTIFY_FIELD_STATUS_STRING = &HB 
    JOB_NOTIFY_FIELD_SECURITY_DESCRIPTOR = &HC 
    JOB_NOTIFY_FIELD_DOCUMENT = &HD 
    JOB_NOTIFY_FIELD_PRIORITY = &HE 
    JOB_NOTIFY_FIELD_POSITION = &HF 
    JOB_NOTIFY_FIELD_SUBMITTED = &H10 
    JOB_NOTIFY_FIELD_START_TIME = &H11 
    JOB_NOTIFY_FIELD_UNTIL_TIME = &H12 
    JOB_NOTIFY_FIELD_TIME = &H13 
    JOB_NOTIFY_FIELD_TOTAL_PAGES = &H14 
    JOB_NOTIFY_FIELD_PAGES_PRINTED = &H15 
    JOB_NOTIFY_FIELD_TOTAL_BYTES = &H16 
    JOB_NOTIFY_FIELD_BYTES_PRINTED = &H17 
End Enum 

To inform the print spooler that you want information on these fields, you create a PRINTER_NOTIFY_OPTIONS structure that is passed to FindFirstPrinterChangeNotification and which holds a pointer to an array of PRINTER_NOTIFY_OPTIONS_TYPE, one for each of the above fields that you require. These structures are documented on MSDN.

In VB.NET, it is easy to translate these structures into classes that can be passed to the API, by being marshaled as if they were structures:

<StructLayout(LayoutKind.Sequential)> _ 
Public Class PrinterNotifyOptionsType 
    Public wType As Int16 
    Public wReserved0 As Int16 
    Public dwReserved1 As Int32 
    Public dwReserved2 As Int32 
    Public FieldCount As Int32 
    Public pFields As IntPtr 

    Private Sub SetupFields() 

        '\\ Free up the global memory 
        If pFields.ToInt32 <> 0 Then 
            Marshal.FreeHGlobal(pFields) 
        End If 

        If wType = Printer_Notification_Types.JOB_NOTIFY_TYPE Then 
            FieldCount = JOB_FIELDS_COUNT 
            pFields = Marshal.AllocHGlobal((JOB_FIELDS_COUNT * 2) - 1) 
            
            '\\ Put the field indexes in the unmanaged array
            Marshal.WriteInt16(pFields, 0, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_PRINTER_NAME)) 
            Marshal.WriteInt16(pFields, 2, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_MACHINE_NAME)) 
            Marshal.WriteInt16(pFields, 4, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_PORT_NAME)) 
            Marshal.WriteInt16(pFields, 6, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_USER_NAME)) 
            Marshal.WriteInt16(pFields, 8, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_NOTIFY_NAME)) 
            Marshal.WriteInt16(pFields, 10, 
                CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_DATATYPE)) 
            Marshal.WriteInt16(pFields, 12, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_PRINT_PROCESSOR)) 
            Marshal.WriteInt16(pFields, 14, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_PARAMETERS)) 
            Marshal.WriteInt16(pFields, 16, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_DRIVER_NAME)) 
            Marshal.WriteInt16(pFields, 18, 
                CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_DEVMODE)) 
            Marshal.WriteInt16(pFields, 20, 
                CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_STATUS)) 
            Marshal.WriteInt16(pFields, 22, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_STATUS_STRING)) 
            Marshal.WriteInt16(pFields, 24, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_SECURITY_DESCRIPTOR)) 
            Marshal.WriteInt16(pFields, 26, 
                CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_DOCUMENT)) 
            Marshal.WriteInt16(pFields, 28, 
                CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_PRIORITY)) 
            Marshal.WriteInt16(pFields, 30, 
                CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_POSITION)) 
            Marshal.WriteInt16(pFields, 32, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_SUBMITTED)) 
            Marshal.WriteInt16(pFields, 34, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_START_TIME)) 
            Marshal.WriteInt16(pFields, 36, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_UNTIL_TIME)) 
            Marshal.WriteInt16(pFields, 38, 
                CShort(Job_Notify_Field_Indexes.JOB_NOTIFY_FIELD_TIME)) 
            Marshal.WriteInt16(pFields, 40, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_TOTAL_PAGES)) 
            Marshal.WriteInt16(pFields, 42, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_PAGES_PRINTED)) 
            Marshal.WriteInt16(pFields, 44, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_TOTAL_BYTES)) 
            Marshal.WriteInt16(pFields, 46, 
                CShort(Job_Notify_Field_Indexes.
                JOB_NOTIFY_FIELD_BYTES_PRINTED)) 
        End If 
    End Sub 

    Public Sub New(ByVal value As Printer_Notification_Types) 
        wType = value 
        Call SetupFields() 
    End Sub
  
End Class   

Starting the Watch

To start the printer watch, you need to pass the printer handle to FindFirstPrinterChangeNotification:

<DllImport("winspool.drv", 
    EntryPoint:="FindFirstPrinterChangeNotification", _ 
    SetLastError:=True, CharSet:=CharSet.Ansi, _ 
    ExactSpelling:=True, _ 
    CallingConvention:=CallingConvention.StdCall)> _ 

    Public Shared Function FindFirstPrinterChangeNotification _ 
    (<InAttribute()> ByVal hPrinter As Int32, _ 
    <InAttribute()> ByVal fwFlags As Int32, _ 
    <InAttribute()> ByVal fwOptions As Int32, _ 
    <InAttribute(), MarshalAs(UnmanagedType.LPStruct)> 
    ByVal pPrinterNotifyOptions As PrinterNotifyOptions) As Int32 

    End Function 

Waiting for a Notification

In the Visual Basic 6 implementation of this, a great deal of complexity was added by the fact that it is a single-threaded system. Thus, when the program was waiting for the printer notification, it was effectively locked up. In Visual Basic .NET, this is no longer necessary because it supports asynchronous events and threading.

The FindFirstPrinterChangeNotification API call returns a Windows synchronization wait handle. This can be used by the VB.NET Common Language Runtime to trigger a particular subroutine whenever that synchronisation object is signalled. This is done with the Threading.RegisteredWaitHandle object:

Private Shared _mhPrinterChangeNotification As RegisteredWaitHandle 

    Dim wh As New ManualResetEvent(False) 
    wh.Handle = mhWait 
    _mhPrinterChangeNotification = 
        ThreadPool.RegisterWaitForSingleObject(wh, 
        New WaitOrTimerCallback(AddressOf PrinterNotifyWaitCallback), 
        wh, -1, True)

Here, PrinterNotifyWaitCallback is a public subroutine that has the correct signature for WaitOrTimerCallback:

Public Sub PrinterNotifyWaitCallback( _ 
    ByVal state As Object, _ 
    ByVal timedOut As Boolean) 

Getting Information About the Event that Occurred

When the wait object is triggered, you have to call FindNextPrinterChangeNotification to find out what event triggered it and get the details.

<DllImport("winspool.drv", 
    EntryPoint:="FindNextPrinterChangeNotification", _ 
    SetLastError:=True, CharSet:=CharSet.Ansi, _ 
    ExactSpelling:=True, _ 
    CallingConvention:=CallingConvention.StdCall)> _ 

    Public Shared Function FindNextPrinterChangeNotification _ 
    (<InAttribute()> ByVal hChangeObject As Int32, _ 
    <OutAttribute()> ByRef pdwChange As IntPtr, _ 
    <InAttribute(), MarshalAs(UnmanagedType.LPStruct)> 
    ByVal pPrinterNotifyOptions As PrinterNotifyOptions, _ 
    <OutAttribute()> ByRef lppPrinterNotifyInfo As IntPtr ) As Boolean 

    End Function

This returns a 32-bit number in pdwChange that indicates what event has occurred. For example, this will contain PRINTER_CHANGE_ADD_JOB when a job is added to the print queue. Additionally, it returns a pointer to data allocated by the spooler in lppPrinterNotifyInfo, which contains a PRINTER_NOTIFY_INFO structure, followed by an array of PRINTER_NOTIFY_INFO_DATA structures. Again, in VB.NET these can be represented by classes:

<StructLayout(LayoutKind.Sequential)> _ 
Public Class PRINTER_NOTIFY_INFO 
    Public Version As Int32 
    Public Flags As Int32 
    Public Count As Int32 
End Class 

You can populate these classes from a pointer, using Marshal.PtrToStructure:

Private msInfo As New PRINTER_NOTIFY_INFO() 
    '\\ Read the data of this printer notification event 
    Marshal.PtrToStructure(lpAddress, msInfo)

History

  • 16 Aug 2006
    • Release 2.0.3 of the component. Allows monitoring multiple printers and gives more detail on the print job events raised. The code is migrated to .NET 2.0.
  • 6 Nov 2006
    • Added new status TonerLow and properties Colour, Collate and PrintQuality to the PrinterInformation class
  • 15 August 2007
    • Updated source code: now uses the SafeWaitHandle class
  • 29 May 2009
    • New source code which is converted to .NET 2 and also uses the Unicode versions of the API calls for international support

License

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

About the Author

Duncan Edwards Jones
Software Developer (Senior) JP Morgan
Ireland Ireland
Member
C# / SQL Server developer
Microsoft MVP 2006, 2007
Visual Basic .NET

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Questionevent not called on subsequent print [modified]memberAjakblackgoat21 Aug '11 - 19:26 
first of all, thanks for your component.
 
However, I wrote an app to catch the events for testing. I tested on IE and print to a shared printer on another PC. The component triggers the jobadd event only for the first time, but none of the events are called on subsequent printing to the same printer.
 

thanks

modified on Monday, August 22, 2011 4:05 AM

GeneralMy vote of 5memberisslava200722 Mar '11 - 6:02 
Very useful article!
I would like to advise adding comment about dependency between printer driver and the Printer Spooler.
QuestionGet number of pages before printingmemberfastflood18 Mar '11 - 8:51 
Hello,
Thanks for this great work : easy to use and very usefull.
 
Nevertheless, I have a little problem.
When a user print a document, I'd like to pause the printing and ask for the user "you will print xx pages. OK ?"
I paused the printer in the jobAdded event, but the number of pages is always set to 0. I saw in that forum that is was normal because this counter is set after the print... so too late.
I tried to use JobWritten event : I have the number of pages but it's too late to pause the printing.
 
Anyone has an idea to resolve this problem ?
 
Thanks a lot.
 
Olivier
AnswerRe: Get number of pages before printingmemberdiegofliess9 Aug '11 - 9:59 
I know it's late to answer, but just founded the question while surfing.
 
Why don't you pause the event at the JobAdded event. And show the message to the user at the JobWritten and then depending on his answer you delete or resume the job ?
 
DF
GeneralRe: Get number of pages before printingmemberfastflood15 Aug '11 - 22:50 
Thanks for your suggestion. I will try again but I remember I have tried to pause the job but the page count was set to 0 at this moment.
GeneralWatermark in every printed pagememberfredccruz20 Jan '11 - 1:51 
I have a print account solution, we interact with the windows Print API and we can capture a lot of informations about print jobs... such as user name, printer namer, document name, etc... but one of my clients asked if there is a way of putting a little watermark on the pages printed with the user name and the datetime of all printed pages. Well, I was wondering if you know some way of doing that...
 
Thanks a lot!
GeneralMy vote of 3membercocacora5 Jan '11 - 20:32 
How to get Monitor pinted NumberOfPages ?
 
Thank .
GeneralRe: My vote of 3membercocacora7 Jan '11 - 22:42 
I insert pji.NumberOfPagesPrinted but return pji.NumberOfPagesPrinted = 0 Alway but oject pji.NumberOfPages is working ??
 
try
{

_spooler = new PrintQueue(new PrintServer(), _spoolerName);
pji = _spooler.GetJob((int)data[i].Id);
 
if (!objJobDict.ContainsKey(intJobID))
{
objJobDict[intJobID] = pji.Name + " ++ " + pji.NumberOfPages;
}
else
{
strJobName = pji.Name + "NumberOfPages=" + pji.NumberOfPages + "Printed=" + pji.NumberOfPagesPrinted;
}
GeneralRe: My vote of 3memberDuncan Edwards Jones8 Jan '11 - 1:28 
In what event?
 
I use JOB_WRITTEN event and chech .Printed = True
'--8<------------------------
Ex Datis:
Duncan Jones
 
Free eBook: Printing - a .NET Developer's Guide (Part 1)

GeneralRe: My vote of 3 [modified]membercocacora8 Jan '11 - 16:51 
Dear Sir
 
I check stattus printed = false, tested printer ( Foxit Phantom Printer )
 
// Test insert Code
 
pji = _spooler.GetJob(intJobID);
if (!objJobDict.ContainsKey(intJobID))
objJobDict[intJobID] = pji.Name;
strJobName = pji.Name + " Page=" + pji.NumberOfPages + "IsPaperOut" + pji.IsPaperOut;
 
// pji.IsPaperOut;Return : pji.IsPaperOut = false;
 
Please Guid Coding for me Sir.
 
Thank You

modified on Saturday, January 8, 2011 11:02 PM

GeneralMy vote of 1memberMatthysDT27 Oct '10 - 22:15 
project doesn't work and the developer doesn't respond
GeneralRe: My vote of 1memberDuncan Edwards Jones30 Oct '10 - 23:38 
What response do you want?
'--8<------------------------
Ex Datis:
Duncan Jones
 
Free eBook: Printing - a .NET Developer's Guide (Part 1)

GeneralMy vote of 5membermaragu22 Oct '10 - 1:30 
I�ve learned a lot with this article, I have some doubts but is great. Thank�s
Generalcant get the source code runmemberhomer favenir18 Dec '09 - 18:51 
hi,
i downloaded the source code and i tried to run it.
but there was an error
 
[code]
A project with an Output Type of Class Library cannot be started directly.
In order to debug this project, add an executable project to this solution which
references the library project. set the executable project as the startup project
[/code]
 
anyone please help
 
thanks
GeneralRe: cant get the source code runmemberDuncan Edwards Jones25 Jan '10 - 5:48 
but that is exactly as expected as this is a component that you would need to add to your own project...so you need to add an executable project to this solution which references the library project. set the executable project as the startup project
 
'--8<------------------------
Ex Datis:
Duncan Jones
Merrion Computing Ltd

GeneralPrinter Monitor ComponentmemberPete Anfinsen6 Nov '09 - 6:45 
Duncan,
 
Sorry to use this forum. I cannot figure out how to reach you otherwise.
 
We purchased PMC source from you in 2007, and need the latest and greatest version. What is the current status of the source for this component? The link that points to the source on the licensing page (www.merrioncomputing.com/Download/PrintQueueWatch/PrinterQueueWatchLicensing.htm) does not work any more. Neither does the Contact us link (www.merrioncomputing.com/simplemail.htm)
 
Please reply via e-mail.
 
Thanks,
Pete Anfinsen
HealthPartners
Bloomington, MN
peteDOTMDOTAnfinsenATHealthPartnersDOTCOM
GeneralRe: Printer Monitor ComponentmemberDuncan Edwards Jones7 Nov '09 - 23:36 
The latest code is on CodePlex[^] as my own website is being wound down...
 
'--8<------------------------
Ex Datis:
Duncan Jones
Merrion Computing Ltd

QuestionMonitoring a PrintDocumentmemberAntonio Sandoval18 Sep '09 - 13:40 
I need to monitor a PrintDocument job. The problem is how to get the jobID, I was thinking on do something like this:
 
ManualResetEvent _event =new ManualResetEvent(false);
PrintQueueWatchComponent_JobAdded(...)
{
_event.Set();
}
 
PrintDocument2 pd=new PrintDocument2();
_event.Reset();
pd.Print();
_event.WaitForOne();
pd.JobId = pqwc.Jobs[pqwc.Jobs.Count-1].JobID;
 
But I´m not sure if there is another best option, or if it can be problematic because is not thread safe.
 
Thanks in advance
AnswerRe: Monitoring a PrintDocumentmembertbotten29 Mar '10 - 22:07 
Check the sample code/howto at http://printqueuewatch.codeplex.com/wikipage?title=Getting%20started&referringTitle=Documentation[^]
This should get you going.
 
HTH
 
Thierry
Questionhow to keep copy of printed document or snapshot of printed document [modified]memberashukasama30 Aug '09 - 3:07 
Hi Duncan,
 
i want to keep copy of printed document or snapshot of printed document, so we can keep eye on what is printing or is there any misuse is happening.
 
Thanks
Ashish
 
modified on Monday, August 31, 2009 4:53 AM

AnswerRe: how to keep copy of printed document or snapshot of printed documentmemberDuncan Edwards Jones15 Sep '09 - 9:12 
There is a print spooler option "Keep printed documents" you can select from control panel -> printers
 
It causes the .spl files not to be deleted when printed. You can then parse the spool file to see what was printed - see my other articles on how to do this for EMF spool files.
 
'--8<------------------------
Ex Datis:
Duncan Jones
Merrion Computing Ltd

GeneralSource code problemsmemberribaraki18 Aug '09 - 22:35 
It seems that you uploaded different versions of your code, apparently the test project has a reference to a new version of the library project, if possible include both projects on one file to simplify using your code, please check...
 
IBaraki

GeneralMy vote of 1memberiztapalapan17 Aug '09 - 9:27 
Not well documented
GeneralThe dmCopies problem for MS Wordmemberleungktw19 Jul '09 - 20:56 
Hello,
 
We need to monitor the total number of pages in each print jobs in order to charge back the cost of printing for individual user. I learned about the MS Word bug of incorrectly setting the number of copies in the print job information. Just wonder whether there is any progress in dealing with this problem or is there any work around with that?
 
Thanks in advance.
 
Cheers.
Kevin
Generalwho has the source code of vs2003membernetiger200227 Jun '09 - 17:19 
please send to me : pang.hq@gmail.com
thank you! Smile | :)

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130516.1 | Last Updated 2 Jun 2009
Article Copyright 2002 by Duncan Edwards Jones
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid