Delphi Check for Default Printer Failing - delphi

I use the code below to prevent an exception when someone tries to print but doesn't have a default printer set. I've gotten a report from a user using the software remotely with citrix who has a network printer as the default printer. It raises an exception at the call to GetPrinter with the message "There is no default printer currently selected". They have no problems printing from other applications. What would be going wrong here?
function CheckForDefaultPrinter: boolean;
var
FDevice: PChar;
FDriver: PChar;
FPort: PChar;
FHandle: THandle;
CurrentPrinterName: string;
begin
//ensure default printer selected - bypass printer.pas bug
Printer.PrinterIndex := Printer.PrinterIndex;
GetMem (FDevice, 255);
GetMem (FDriver, 255);
GetMem (FPort, 255);
try
try
Printers.Printer.GetPrinter(FDevice, FDriver, FPort, FHandle);
except
on E:Exception do
ShowMessage(E.Message);
end;
CurrentPrinterName := FDevice;
finally
if FDevice <> nil then FreeMem (FDevice, 255);
if FDriver <> nil then FreeMem (FDriver, 255);
if FPort <> nil then FreeMem (FPort, 255);
end;
if CurrentPrinterName = '' then
begin
MessageDlg('You do not have a default printer defined.' +
#13#13 + 'Please select a printer before running a report.'+
#13#13 + 'Or the default printer name is blank. Please assign the printer a name.',
mtError,[mbOK],0);
Result:= False;
end
else
Result:= True;
end;

The error occurs before the code you posted can check it, because it fails during the first access to Printer.
You should use the WinAPI GetDefaultPrinter function directly instead to see if a default printer exists, before attempting to use the global Printer. Here's a sample console application (which utilizes an easier declaration of the function than the one contained in recent versions of Delphi) to demonstrate how to do so. The sample was compiled in XE 10 Seattle and tested on Windows 7 64-bit.
program Project1;
{$APPTYPE CONSOLE}
uses
System.SysUtils, WinAPI.Windows;
function GetDefaultPrinter(Buffer: PChar; var BufferSize: DWord): BOOL; stdcall;
external 'winspool.drv' name 'GetDefaultPrinterW'; // GetDefaultPrinterA on pre-Unicode Delphi versions
var
Buff: string;
BuffSize, Err: DWord;
begin
// Get size of buffer needed.
GetDefaultPrinter(nil, BuffSize);
SetLength(Buff, BuffSize);
// If this call fails, and GetLastError returns
// ERROR_FILE_NOT_FOUND, there is no default printer assigned.
if GetDefaultPrinter(PChar(Buff), BuffSize) then
WriteLn('Default printer: ', Buff)
else
begin
Err := GetLastError();
if Err = ERROR_FILE_NOT_FOUND then
WriteLn('No default printer assigned')
else
WriteLn('Failed. Error: ', Err);
end;
ReadLn;
end.
Note that the return value includes the terminating NULL (#0) according to the documentation. To remove it, simply SetLength(Buff, Length(Buff) - 1) after the call to GetDefaultPrinter returns.

On Windows you can assume that if there is more than 0 (zero) printers then there is a default printer.
This assmption is safe except if your application is running as a service.
So all you need is this:
if (Printer.Printers.Count=0) then
ShowMessage('Please install a printer before attempting to print.');
You need to make this check before you access most other properties/methods on the Printer object.
If your application is running as a service there will be no default printer. Here PrinterIndex will be -1 until you assign a value in code.

I tested your code with Delphi XE6, Win7Pro. Just changed a line
GetMem (FPort, 255);
try
try
Printer.GetPrinter(FDevice, FDriver, FPort, FHandle); // HERE
except
on E:Exception do
ShowMessage(E.Message);
end;
It works with a networked printer as default, even with cable unplugged.

Related

GetAdaptersInfo not working on Delphi XE6

I finally bit the bullet and bought XE6 and as expected, the Unicode conversion is turning into a bit of a nightmare. So if anyone can enlighten me on why this simple Windows API call fails, it would be most appreciated. The function does not return an error, the first call gets the correct buffer length, the second call fills the record with garbage.
This works fine under Delphi 2007 but fails on XE6 with unicode garbage in the pAdapterinfo return record even though it is explicitly declared with AnsiString in IpTypes.pas
System is Win7(64) but compiling for 32 bits.
uses iphlpapi, IpTypes;
function GetFirstAdapterMacAddress:AnsiString;
var pAdapterInfo:PIP_ADAPTER_INFO;
BufLen,Status:cardinal; i:Integer;
begin
result:='';
BufLen:= sizeof(IP_ADAPTER_INFO);
GetAdaptersInfo(nil, BufLen);
pAdapterInfo:= AllocMem(BufLen);
try
Status:= GetAdaptersInfo(pAdapterInfo,BufLen);
if (Status <> ERROR_SUCCESS) then
begin
case Status of
ERROR_NOT_SUPPORTED: raise exception.create('GetAdaptersInfo is not supported by the operating ' +
'system running on the local computer.');
ERROR_NO_DATA: raise exception.create('No network adapter on the local computer.');
else
raiselastOSerror;
end;
Exit;
end;
while (pAdapterInfo^.AddressLength=0) and (pAdapterInfo^.next<>nil) do
pAdapterInfo:=pAdapterInfo.next;
if pAdapterInfo^.AddressLength>0 then
for i := 0 to pAdapterInfo^.AddressLength - 1 do
result := result + IntToHex(pAdapterInfo^.Address[I], 2);
finally
Freemem(pAdapterInfo);
end;
end;
UPDATE:
I did some more checking. I created a new simple application with one form and a button and called the routine when the button was pressed and it worked.
The differences are...in the working form the size of IP_ADAPTER_INFO is 640 bytes.
When this routine is used in a more complex application it fails and the size of IP_ADAPTER_INFO displays as 1192 bytes.
At this point, it seems the complier is unilaterally deciding to change the type of the ansi chars in the structures to unicode chars. The debugger is showing AdapterName and description fields in unicode form. I did a grep of the system source code, there are no other versions of this data type declared in the library code apart from in the Indy library and that is just a duplicate.
Here is the data structure definition in IPtypes
PIP_ADAPTER_INFO = ^IP_ADAPTER_INFO;
{$EXTERNALSYM PIP_ADAPTER_INFO}
_IP_ADAPTER_INFO = record
Next: PIP_ADAPTER_INFO;
ComboIndex: DWORD;
AdapterName: array [0..MAX_ADAPTER_NAME_LENGTH + 3] of AnsiChar;
Description: array [0..MAX_ADAPTER_DESCRIPTION_LENGTH + 3] of AnsiChar;
AddressLength: UINT;
Address: array [0..MAX_ADAPTER_ADDRESS_LENGTH - 1] of BYTE;
Index: DWORD;
Type_: UINT;
DhcpEnabled: UINT;
CurrentIpAddress: PIP_ADDR_STRING;
IpAddressList: IP_ADDR_STRING;
GatewayList: IP_ADDR_STRING;
DhcpServer: IP_ADDR_STRING;
HaveWins: BOOL;
PrimaryWinsServer: IP_ADDR_STRING;
SecondaryWinsServer: IP_ADDR_STRING;
LeaseObtained: time_t;
LeaseExpires: time_t;
end;
Looks like a compiler bug.
There are several problems with your code:
You are not doing any error handling at all on the first call that calculates the buffer length. You don't even need that call, so get rid of it.
You are not doing adequate error handling on subsequent calls, in particular you are not handling the ERROR_BUFFER_OVERFLOW condition when GetAdaptersInfo() needs you to allocate more memory than you already have. Your are allocating only enough memory for one adapter, but GetAdaptersInfo() returns info for all adapters and thus needs a sufficient buffer to hold all of them at one time.
GetAdaptersInfo() does not use GetLastError(), so you need to call SetLastError() before you call RaiseLastOSError().
You are looping through the adapter list using the original pointer that you used to allocate the list, so you are causing a memory leak if the first adapter does not have a MAC address. You need to use a separate variable as the loop iterator so the original pointer is preserved so it can be freed correctly.
You are not taking into account the possibility that none of the adapters has a MAC address, so you will end up accessing a nil pointer after your while loop exits.
You appear to have multiple versions of the IpTypes unit on your machine, and the compiler is finding one that happens to use Char instead of AnsiChar in the IP_ADAPTER_INFO record so its size and field offsets are wrong.
With that said, try this instead:
uses
Winapi.iphlpapi, Winapi.IpTypes;
function GetFirstAdapterMacAddress: String;
var
pAdapterList, pAdapter: PIP_ADAPTER_INFO;
BufLen, Status: DWORD;
I: Integer;
begin
Result := '';
BufLen := 1024*15;
GetMem(pAdapterList, BufLen);
try
repeat
Status := GetAdaptersInfo(pAdapterList, BufLen);
case Status of
ERROR_SUCCESS:
begin
// some versions of Windows return ERROR_SUCCESS with
// BufLen=0 instead of returning ERROR_NO_DATA as documented...
if BufLen = 0 then begin
raise Exception.Create('No network adapter on the local computer.');
end;
Break;
end;
ERROR_NOT_SUPPORTED:
begin
raise Exception.Create('GetAdaptersInfo is not supported by the operating system running on the local computer.');
end;
ERROR_NO_DATA:
begin
raise Exception.Create('No network adapter on the local computer.');
end;
ERROR_BUFFER_OVERFLOW:
begin
ReallocMem(pAdapterList, BufLen);
end;
else
SetLastError(Status);
RaiseLastOSError;
end;
until False;
pAdapter := pAdapterList;
while pAdapter <> nil do
begin
if pAdapter^.AddressLength > 0 then
begin
for I := 0 to pAdapter^.AddressLength - 1 do begin
Result := Result + IntToHex(pAdapter^.Address[I], 2);
end;
Exit;
end;
pAdapter := pAdapter^.next;
end;
finally
FreeMem(pAdapterList);
end;
end;
The explanation is that the types declared in your third party IpTypes unit use Char. This is an alias to AnsiChar in pre-Unicode Delphi, and an alias to WideChar in Unicode Delphi. That would explain the fact that you see non-ANSI text when you inspect the content of the record.
The solution is to fix IpTypes to use AnsiChar in place of Char where appropriate. The best way to do that is to use the IpTypes shipped with Delphi rather than your third party version.
On top of that, the first call to GetAdaptersInfo is wrong. Not only do you fail to check the return value, but you pass nil for the buffer and yet also pass a non-zero length. I think it should go like this:
BufLen := 0;
if GetAdaptersInfo(nil, BufLen) <> ERROR_BUFFER_OVERFLOW then
raise ....
Of course, you way will work, but I'm just being a little pedantic here. Always check for errors when you call an API function.
Just to conclude this topic.
Changing IPtypes to winapi.IPtypes fixed the problem for me.
I think a third party component is doing something to confuse the compiler and giving the full link fixes it.

delphi passing running parameters to other instance via wm_copydata gives wrong result in Delphi XE2

This code used to work with Delphi 5, but with delphi XE2 does not work as expected. The string passed using wm_copydata will be cut.
procedure SendAppParameters(aMsgStr: string);
var
hwnd: THandle;
cds: CopyDataStruct;
begin
hwnd := FindWindow('TMyAppFormMain', nil); //<-- Substitute window classname with your own
if hwnd <> 0 then
begin
// showmessage(aMsgStr);
// prepare the data to copy
cds.dwData := 0;
cds.cbData := length(AMsgStr);
cds.lpData := PChar(AMsgStr);
// activate the destination window
SetForegroundWindow(hwnd);
// send the data to the first instance using a wm_CopyData message
SendMessage(hwnd, wm_CopyData, Application.Handle, integer(#cds));
end
end;
And in the Main Form I have:
procedure TMyAppFormMain.GotMessage_CopyData(var Msg: TWmCopyData);
var
MsgString: string;
I: Integer;
begin
MsgString := PChar(Msg.CopyDataStruct.lpData);
showmessage(MsgString);
end;
In fact your code has never been correct. It is broken even on ANSI versions of Delphi.
Let's take a look. You prepare the message like this:
cds.cbData := length(AMsgStr);
cds.lpData := PChar(AMsgStr);
On an ANSI Delphi that means that the text is marshalled up to but not including the null-terminator.
The receiver does this:
MsgString := PChar(Msg.CopyDataStruct.lpData);
This relies on there being a null-terminator present. There's no reason to expect that there would be and even to attempt to read beyond cds.cbData bytes is an error.
The recipient must take care to heed the value of cds.cbData that is sent and not read beyond the end of the buffer.
Now, the other issue is that you have moved to a Unicode Delphi and so text is now UTF-16 encoded.
To send the text I would write:
cds.cbData := length(AMsgStr)*SizeOf(Char);
cds.lpData := PChar(AMsgStr);
And on the receiving side it should be:
SetString(MsgString, PChar(Msg.CopyDataStruct.lpData),
Msg.CopyDataStruct.cbData div SizeOf(Char));
The cast that you use, integer(#cds), is incorrect. That should be LPARAM(#cds).

Serial (COM) ports name or identification

I've a program that access multiple serial ports using cport.
To configure, till now I simply listed all available comports in a combobox to make a selection, but the increasing number of drivers with (virtual) serial interfaces makes configuring for end-users troublesome.
The current detection works with createfile(), but that has the problem that you only get exists/nonexists and maybe "busy" as information.
However to improve, I need per COM port a identification string, like the hardware device/driver (device manager) it is connected too. This would make it easier for the user to narrow the comports down (since we deliver a finite number of serial cards)
Probably it is available from WMI, but that's quite a jungle, does sb have more concrete information, or better, code?
(Delphi XE3, Win7+, no solution that requires additional installing or deployment please)
If you want enumerate the COM ports getting a friendly name you can use the SetupAPI and the GUID_DEVINTERFACE_COMPORT device interface class.
Try this sample
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows,
SysUtils,
JvSetupApi;
const
GUID_DEVINTERFACE_COMPORT:TGUID='{86E0D1E0-8089-11D0-9CE4-08003E301F73}';
procedure EnumerateCOMPorts;
var
cbRequired : DWORD;
hdev : HDEVINFO;
idev : Integer;
did : TSPDeviceInterfaceData;
pdidd : PSPDeviceInterfaceDetailData;
PropertyBuffer : array[0..255] of Char;
DeviceInfoData: TSPDevInfoData;
PropertyRegDataType: DWORD;
RequiredSize: DWORD;
begin
// enumerate the com ports
hdev := SetupDiGetClassDevs(#GUID_DEVINTERFACE_COMPORT, nil, 0, DIGCF_PRESENT OR DIGCF_DEVICEINTERFACE);
if ( INVALID_HANDLE_VALUE <> THandle(hdev) ) then
begin
try
idev:=0;
ZeroMemory(#did, SizeOf(did));
did.cbSize := SizeOf(did);
repeat
if (SetupDiEnumDeviceInterfaces(hdev, nil, GUID_DEVINTERFACE_COMPORT, idev, did)) then
begin
cbRequired := 0;
SetupDiGetDeviceInterfaceDetail(hdev, #did, nil, 0, cbRequired, nil);
if (ERROR_INSUFFICIENT_BUFFER= GetLastError()) then
begin
pdidd:=AllocMem(cbRequired);
try
pdidd.cbSize := SizeOf(TSPDeviceInterfaceDetailData);
DeviceInfoData.cbSize:= SizeOf(DeviceInfoData);
RequiredSize:=0;
if (SetupDiGetDeviceInterfaceDetail(hdev, #did, pdidd, cbRequired, RequiredSize, #DeviceInfoData)) then
begin
PropertyRegDataType:=0;
RequiredSize:=0;
if SetupDiGetDeviceRegistryProperty(hdev, DeviceInfoData, SPDRP_FRIENDLYNAME, PropertyRegDataType, PBYTE(#PropertyBuffer[0]), SizeOf(PropertyBuffer), RequiredSize) then
Writeln(Format('Friendly Name - %s',[PropertyBuffer]));
if SetupDiGetDeviceRegistryProperty(hdev, DeviceInfoData, SPDRP_DEVICEDESC, PropertyRegDataType, PBYTE(#PropertyBuffer[0]), SizeOf(PropertyBuffer), RequiredSize) then
Writeln(Format('Description - %s',[PropertyBuffer]));
if SetupDiGetDeviceRegistryProperty(hdev, DeviceInfoData, SPDRP_LOCATION_INFORMATION, PropertyRegDataType, PBYTE(#PropertyBuffer[0]), SizeOf(PropertyBuffer), RequiredSize) then
Writeln(Format('Location - %s',[PropertyBuffer]));
end
else
RaiseLastOSError;
finally
FreeMem(pdidd);
end;
end;
end
else
Break;
inc(idev);
until false;
finally
SetupDiDestroyDeviceInfoList(hdev);
end;
end;
end;
begin
try
if not LoadsetupAPI then exit;
EnumerateCOMPorts;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
readln;
end.
This will return something like so
Note : The JvSetupApi unit is part of the JVCL library.
You can use HKEY_LOCAL_MACHINE \HARDWARE\DEVICEMAP\SERIALCOMM for COMxx style short names. Keep remember specify read only access to avoid Admin rights / UAC necessity. You can see both usb232 adapters and real comm ports.
You can also chek HKEY_LOCAL_MACHINE \SYSTEM\CurrentControlSet\Enum\Root\PORTS but it seems a bit tricky.

How to recover after ReWrite failure?

I want to open a typed file as random access. This is done by setting the FileMode in fmOpenReadWrite. This requires the file to exist and I test whether the file exists and if not, ReWrite it and close it. See code below.
var fl: file of _some_record_type_;
fn: string;
AssignFile (fl, fn);
if not FileExists (fn) then
begin
ReWrite (fl);
CloseFile (fl); // Now an empty file exists
end; // if
FileMode := fmOpenReadWrite;
Reset (FTrack_File);
// ...further rad and write operations...
This works great except when fn is an illegal file name, for example when specifying a non-existing drive. It raises an exception at ReWrite. I cannot recover from the error by surrounding the ReWrite by try..except because any reference to that file or any other file raises an access violation exception. It appears that some condition has been set that prevents any file i/o.
Somebody knows how to handle this situation?
You can switch to using exceptions (with {$I+}), and then use try..except. (It's usually the default, unless you've unchecked I/O Checking in the Project Options dialog (Project->Options->Delphi Compiler->Compiling->Runtime Errors->I/O checking from the main menu).
If that box is unchecked, it sets the option {$I-}, which uses IOResult.
If you want to keep using IOResult, you'll need to check it after using the file functions. Checking it automatically resets the InOutRes variable to 0, clearing the previous error value.
AssignFile (fl, fn);
if not FileExists (fn) then
begin
ReWrite (fl);
if IOResult <> 0 then
// You've had an error.
CloseFile (fl); // Now an empty file exists
end; // if
IOResult can be found in the System unit.
You really should be moving away from the old style IO routines, BTW. They're ancient, and don't properly work with Unicode data. You can accomplish the same thing using a TFileStream, which would give you proper exception handling and support for Unicode. Here's a quick console app sample (tested with XP3 on Win 7):
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, Classes, Windows;
type
TMyRec = record
anInt: Integer;
aBool: Boolean;
aByte: Byte;
end;
var
FS: TFileStream;
MyRec: TMyRec;
const
TheFile = 'C:\TempFiles\test.dat';
begin
MyRec.anInt := 12345;
MyRec.aBool := True;
MyRec.aByte := 128;
FS := TFileStream.Create(TheFile, fmCreate or fmOpenReadWrite);
try
FS.Write(MyRec, SizeOf(TMyRec));
// Clear the content and confirm it's been cleared
FillChar(MyRec, SizeOf(TMyRec), 0);
WriteLn('anInt: ', MyRec.anInt, ' aBool: ', MyRec.aBool, ' aByte: ', MyRec.aByte);
FS.Position := 0;
FS.Read(MyRec, SizeOf(TMyRec));
finally
FS.Free;
end;
// Confirm it's read back in properly
WriteLn('anInt: ', MyRec.anInt, ' aBool: ', MyRec.aBool, ' aByte: ', MyRec.aByte);
ReadLn;
end.

Checking Printer Messages using OPOS Drivers in Delphi

I'm trying to open a Point of Sale (POS) printer using the OPOS Drivers in Delphi (BDS2006), but don't have a clue on how to check the printer status.
How would I check for messages like Check Paper and Paper Jam from the printer?
I haven't used OPOS Drivers but I have done some work with POS Drivers for an Epson receipt printer connected to a cash drawer. What I discovered was that, if the printer is installed in Windows, you can then open a direct connection to it and make it do whatever you want.
The reason the printer is so slow is that it's using the graphical font functions of Windows. When you open the printer directly, you will set the mode to RAW and it will just send text out like an old-style dot-matrix. To kick the cash drawer open, you just send it the specific control codes as if you were going to print them. The printer intercepts the codes before it prints and kicks the drawer open.
BTW, I have no idea how this would work with Unicode. The printer I had only really worked with ASCII data. There might be variants designed for international markets that would work differently.
Here's the code I've used to make it work (VxMsgBox is just a cover to MessageBox):
{***************************************************************************}
{** PrintDirect2Printer **}
{***************************************************************************}
procedure PrintDirect2Printer(PrinterName, Data:pchar; dwByteCount:DWORD);
var PrinterHandle : THandle;
DocInfo : TDocInfo1;
dwJob : DWORD;
dwBytesWritten : DWORD;
begin
if not OpenPrinter(PrinterName, PrinterHandle, nil) then exit; //failed to open printer, abort
DocInfo.pDocName := 'Direct 2 Printer';
DocInfo.pOutputFile := nil;
DocInfo.pDataType := 'RAW';
dwJob:=StartDocPrinter(PrinterHandle, 1, #DocInfo);
if dwJob=0 then //failed to start a document
begin
ClosePrinter(PrinterHandle);
exit;
end;
if not StartPagePrinter(PrinterHandle) then
begin
EndDocPrinter(PrinterHandle);
ClosePrinter(PrinterHandle);
exit;
end;
if not WritePrinter(PrinterHandle, Data, dwByteCount, dwBytesWritten) then
begin
EndPagePrinter(PrinterHandle);
EndDocPrinter(PrinterHandle);
ClosePrinter(PrinterHandle);
exit;
end;
if not EndPagePrinter(PrinterHandle) then
begin
EndDocPrinter(PrinterHandle);
ClosePrinter(PrinterHandle);
exit;
end;
if not EndDocPrinter(PrinterHandle) then
begin
ClosePrinter(PrinterHandle);
exit;
end;
ClosePrinter(PrinterHandle);
if dwBytesWritten<>dwByteCount then
VxMsgBox('Print Direct To Printer failed.', 'Printer Error', mb_Ok);
end;
{***************************************************************************}
{** OpenPrintDirect2Printer **}
{***************************************************************************}
function OpenPrintDirect2Printer(PrinterName, DocName:pchar; var PrinterHandle:THandle):boolean;
var DocInfo : TDocInfo1;
dwJob : DWORD;
begin
result:=false;
if not OpenPrinter(PrinterName, PrinterHandle, nil) then exit; //failed to open printer, abort
DocInfo.pDocName := DocName;
DocInfo.pOutputFile := nil;
DocInfo.pDataType := 'RAW';
dwJob:=StartDocPrinter(PrinterHandle, 1, #DocInfo);
if dwJob=0 then //failed to start a document
begin
ClosePrinter(PrinterHandle);
exit;
end;
if not StartPagePrinter(PrinterHandle) then
begin
EndDocPrinter(PrinterHandle);
ClosePrinter(PrinterHandle);
exit;
end;
result:=true;
end;
{***************************************************************************}
{** WritePrintDirect2Printer **}
{***************************************************************************}
function WritePrintDirect2Printer(PrinterHandle:THandle; Data:pchar; dwByteCount:DWORD):boolean;
var dwBytesWritten : DWORD;
begin
result:=true;
if not WritePrinter(PrinterHandle, Data, dwByteCount, dwBytesWritten) then
result:=false;
if dwBytesWritten<>dwByteCount then
VxMsgBox('WritePrintDirect2Printer byte check failed.', 'Printer Error', mb_Ok);
end;
{***************************************************************************}
{** ClosePrintDirect2Printer **}
{***************************************************************************}
procedure ClosePrintDirect2Printer(var PrinterHandle:THandle);
begin
if not EndPagePrinter(PrinterHandle) then
begin
EndDocPrinter(PrinterHandle);
ClosePrinter(PrinterHandle);
PrinterHandle:=0;
exit;
end;
if not EndDocPrinter(PrinterHandle) then
begin
ClosePrinter(PrinterHandle);
PrinterHandle:=0;
exit;
end;
ClosePrinter(PrinterHandle);
PrinterHandle:=0;
end;
Are you using the ActiveX control from here: http://monroecs.com/oposccos.htm? It has an event for error status.
First of all you have to install the right support software for your device, which you probably have to download from the manufacturer's website. Keep in mind that sometimes, many devices (like receipt printers) contain standard hardware (ex EPSON TX-88III) although the brand name might differ.
The support software usually contains the driver, configuration tools, and possibly programming examples of how to use the driver. Make sure that the following steps are correctly completed:
Installation of driver, config tools is done
The device is correctly connected using the right cables (I had problems finding the correct serial cable, since there are many different types of them)
Your device is recognised by the configuration software (through the driver) and communicates well, at least it responds to some functions
Use the ActiveX control that was installed with the driver. It should have similar name with the driver.
After the above steps you will have a control in your application that provides you with all available functions, status properties and events (for paper, or anything other).

Resources