Link to home
Start Free TrialLog in
Avatar of moonrise
moonrise

asked on

Question on Madshi Print Monitor

Hi Madshi,

I have been looking at your Print Monitor and so far it works great, even from Win 98 printing to a network printer.

My goal is to be able to count the number of pages printed to each printer.  I ran your demo application printMonitor then printed 1 page in Notepad. The output in printMonitor includes several StartPage, including 2 with the process notepad.exe and 1 with the process spoolsv.exe

Is there a rule that would apply in all cases to know how many pages are printed?

Also, in some cases I would like to cancel the printing after so many pages. Is it still possible to cancel part of a job by the time it shows on printMonitor?

Thank you.
Avatar of DaFox
DaFox

Hi moonrise,

Have a look at this code, http://www.assarbad.org/stuff/prtmon3vivi.zip. It comes with full source code!! I guess it's easier to solve a problem if you got the entire source.

Regards,
Markus
ASKER CERTIFIED SOLUTION
Avatar of Madshi
Madshi

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of moonrise

ASKER

The other solution proposed does not work with win 98, plus the great advantage I see in your solution is that print jobs can be interrupted easily.

Because i want my application to work the same with local and network printers, it looks like I will have to use what i get from the applications and not from the spooler (only get the StartPage and Endpage from the applicationn not the spooler) for network printers.

I do however have two issues:

1 - when the user sets the number of copies to be printerd > 1 the application still does StartPage only one per page (Spooler does it for each page printed). Do you know of a way to get the number of copies requested from the application.

2 - Does the application find out if the printer is offline or out of paper?

thank you.
These are print related questions, so again: I'm no expert there. But according to the documentation you can probably either use the API "DocumentProperties" or the API GetPrinter(level2) to get access to the printer's current DEVMODE. Part of that structure is the number of copies.

(2) I'm not sure what you mean there? I guess you can call GetPrinter again to find out whether the printer is offline or not. If the printer is out of paper, probably calling StartDoc will fail. But I'm just guessing here...

Regards, Madshi.
Thank you for your help. Now I just have to play a bit more with ala that but I feel confident I'll be able to do what I want.

One little question before I leave you alone with this topic, how do I point to the string values in  (what do I need to do with api to get the string value?)

    with TPrintNotification(pointer(Message.wParam)^) do

      PanelPrintingStatus.Caption := api;
That code you posted there should be correct. Or what do you mean? I'm not sure whether I understood your question correctly.
You're right, I had a mismatch in the record definition TPrintNotification - all fine now
hi again,

to stop pages from being printed, I use option 3 above,  *not* call the original API, but just return true.

I thought I could just set a registry value in my main program - a flag called 'Allowed to Print'  - so in HandlePrintNotification(var Message: TMessage);

          if (api = 'CreateDCW') or (api = 'CreateDCA') then
          begin
            if fMaximumPagesPrinted > 0 then
              WriteBool('Allowed to Print', True)
            else
              WriteBool('Allowed to Print', False);
          end;

            if (api = 'EndPage') then
            begin
              Inc(fJobNumberOfPages);

              if fMaximumPagesPrinted < fJobNumberOfPages then
                WriteBool('Allowed to Print', False)
              else
                WriteBool('Allowed to Print', True);
            end;

and then in the hook I have

      if ReadBool('Allowed to Print') then
        result := StartPageNext(dc)
      else
        result := 1;

but that does not seem to work, is there a better way to have the main program pass information to the hook?
Are you just fooling StartPage? I think you should really fool StartPage + EndPage at least. Maybe some more APIs.

The application is writing that registry value? When does it do that? Can it be a timing problem? Printing might be very fast. It could be that the printing application has finished printing all pages before your application has decided that printing should stop.

Generally misusing the registry is possible, but not nice. Instead have a look at madCodeHook.CreateGlobalFileMapping + MapViewOfFile. The application can write into a shared buffer and all DLL copies can read from it this way.
Hi, I got everything working except for the number of copies printed. I get the value from CreateDCACallback or CreateDCWCallback from the DM: PDeviceMode structure - dmCopies but this value is always set to 1074 no matter what. Let me know if you have any idea about this one.


Is the DM_COPIES flag set in "DM.dmFields"?

Either way, maybe the print code is setting the copies later. Either by calling SetPrinter or by calling DocumentProperties. If I understand the documentation right, both APIs could be used to change the "dmCopies" field.
So I'd try to hook SetPrinter + DocumentProperties, too.
I think you are right - it might be set later. Could you please show me the code to hook SetPrinter + DocumentProperties (I will add 500 points for that)
Currently I don't have the time to do so. How about trying to get it to work yourself? If you run into problems, just post them here.

You know, if I do the work for you this time, I'll probably also have to do it for you the next time. It's better if you try to do it yourself. Then you learn how it works and can do it all by yourself the next time. At least that's how I see it.
You are right, here is what I have done so far - I hope I am on the right track. I have a specific question below - I am not too good with pointers. Thank you.

I added:

  HookAPI('gdi32.dll', 'SetPrinter',@SetPrinterCallback,@SetPrintereNext  );

Added a parameter DM in NotifyApplication to receive the PDeviceMode info
 
procedure NotifyApplication(PrintAPI: TPrintAPI;
                            DeviceA: PChar; DeviceW: PWideChar;
                            DIA: PDocInfoA; DIW: PDocInfoW;
                            DM: PDeviceMode;
                            Result: Boolean);
var

....I have a case in NotifyApplication to do different actions depending on the API - for setprinter I set the number of copies
    paSetPrinter:
      begin
        Copies := DM.dmCopies;
      end;

In SetPrinterNext I only do something if level 2 - I receive a LPBYTE (a pointer to an array of bytes that may contain printer data)

Question: how do I send the PDeviceMode portion from  pPrinter: LPBYTE below to replace the ???? below.

function SetPrinterNext(hPrinter: Handle; Level: DWord; pPrinter: LPBYTE; Command: DWORD): Integer; stdcall;
begin
  Result := SetPrinterNext(hPrinter, Level, pPrinter, Command);

  if Level = 2 then
  begin
    NotifyApplication(paSetPrinter,
                      nil, nil,
                      nil, nil,
                      ???????
                      Result > 0);
  end;
end;

My record sent to my application by SendIpcMessage now looks as follows:

  TPrintNotification = record
    PrintAPI: TPrintAPI;  // This is a list I created instead of using strings
    Params: array [0..MAX_PATH] of char;
    Copies: Integer;
  end;


I think this one should work:

if (result <> 0) and (Level = 2) and (pPrinter <> nil) then
  begin
    NotifyApplication(paSetPrinter,
                      nil, nil,
                      nil, nil,
                      PPrinterInfo2(pPrinter)^.pDevMode,
                      Result > 0);
  end;

But please note, that you should check "DM.dmFields". SetPrinter might be called with a wild random Copies number. You should evaluate the Copies number only if the DM.dmFields flags say so. This applies to CreateDC, too.
Here it is all - does not seem to call SetPrinterCallback - if you have a minute please look over this code - otherwise I appreciate your help so far. This stuff is  bit harder than what I'm used to do.

library HookPrintAPIs;

{$IMAGEBASE $5a000000}

uses Windows, madCodeHook, madStrings, MyTypes, Constants, sysutils, Printers, Winspool;

procedure SetPrintingAllowed(pPrintingAllowed: Boolean); export; forward;

exports
  SetPrintingAllowed index 1;

type
  PHookRec = ^THookRec;
  THookRec = record
    PrintingAllowed: Boolean;
  end;

  TPrintAPI = (paCreateDCA, paCreateDCW, paStartDocA, paStartDocW, paEndDoc, paStartPage, paEndPage, paSetPrinter);

  // This is what the printer hook sends
  TPrintNotification = record
    PrintAPI: TPrintAPI;
    Params: array [0..MAX_PATH] of char;
    Copies: Integer;
  end;

const
  rHookRec: PHookRec = nil;

procedure SetPrintingAllowed(pPrintingAllowed: Boolean);
begin
  rHookRec^.PrintingAllowed := pPrintingAllowed;
end;

procedure NotifyApplication(PrintAPI: TPrintAPI;
                            DeviceA: PChar; DeviceW: PWideChar;
                            DIA: PDocInfoA; DIW: PDocInfoW;
                            DM: PDeviceMode;
                            Result: Boolean);
var
  vPrintNotification: TPrintNotification;
  arrChA  : array [0..MAX_PATH] of char;
  arrChW  : array [0..MAX_PATH] of wideChar;
  Session : dword;
begin
  vPrintNotification.PrintAPI := PrintAPI;

  // Populate some values - not required for all APIs
  case PrintAPI of
    paCreateDCA:
      begin
        if (DeviceA <> nil) then
        begin
          lstrcpyA(arrChA, DeviceA);

          arrChA[11] := #0;
          if lstrcmpA('\\.\DISPLAY', arrChA) = 0 then
            // We don't want to display dcs!
            exit;
          lstrcpyA(vPrintNotification.Params, arrChA);
        end;
      end;
    paCreateDCW:
      begin
        if (DeviceW <> nil) then
        begin
          lstrcpyW(arrChW, DeviceW);

          arrChW[11] := #0;
          if lstrcmpW('\\.\DISPLAY', arrChW) = 0 then
            exit;

          WideToAnsi(DeviceW, arrChA);
          lstrcpyA(vPrintNotification.Params, arrChA);
        end;
      end;
    paStartDocA:
      begin
        if DIA^.lpszDocName <> nil then
          lstrcpyA(vPrintNotification.Params, DIA^.lpszDocName);
      end;
    paStartDocW:
      begin
        if DIW^.lpszDocName <> nil then
        begin
          WideToAnsi(DIW^.lpszDocName, arrChA);
          lstrcpyA(vPrintNotification.Params, arrChA);
        end;
      end;
    paSetPrinter:
      begin
        vPrintNotification.Copies := DM^.dmCopies;
      end;
  end;

  // Which terminal server (XP fast user switching) session shall we contact?
  if AmSystemProcess and (GetCurrentSessionId = 0) then
    // some system process are independent of sessions
    // so let's contact the PrintMonitor application instance
    // which is running in the current input session
    Session := GetInputSessionId
  else
    // we're an application running in a specific session
    // let's contact the PrintMonitor application instance
    // which runs in the same session as we do
    Session := GetCurrentSessionId;

  // Now send the composed strings to our log window
  // hopefully there's an instance running in the specified session
  SendIpcMessage(pchar('PrintMonitor' + IntToStrEx(Session)), @vPrintNotification, sizeOf(vPrintNotification));
end;

var
  CreateDCANext : function (Driver, Device, Output: pchar; dm: PDeviceModeA): dword; stdcall;
  CreateDCWNext : function (Driver, Device, Output: pwidechar; DM: PDeviceModeW): dword; stdcall;
  StartDocANext : function (DC: dword; const DI: TDocInfoA): Integer; stdcall;
  StartDocWNext : function (DC: dword; const DI: TDocInfoW): Integer; stdcall;
  EndDocNext    : function (DC: dword): Integer; stdcall;
  StartPageNext : function (DC: dword): Integer; stdcall;
  EndPageNext   : function (DC: dword): Integer; stdcall;
  SetPrinterNext: function (hPrinter: THandle; Level: DWord; pPrinter: dword; Command: DWORD): Integer; stdcall;

function CreateDCACallback(Driver, Device, Output: PChar; DM: PDeviceModeA): dword; stdcall;
begin
  Result := CreateDCANext(Driver, Device, Output, DM);

  // We log this call only if it is a printer DC creation
  if (Device <> nil) and (not IsBadReadPtr(Device, 1)) and (Device^ <> #0) then
    NotifyApplication(paCreateDCA,
                      Device, nil,
                      nil, nil,
                      nil,
                      False);
end;

function CreateDCWCallback(Driver, Device, Output: PWideChar; DM: PDeviceModeW) : dword; stdcall;
begin
  Result := CreateDCWNext(Driver, Device, Output, DM);

  if (Device <> nil) and (not IsBadReadPtr(Device, 2)) and (Device^ <> #0) then
    NotifyApplication(paCreateDCW,
                      nil, Device,
                      nil, nil,
                      nil,
                      False);
end;

function StartDocACallback(DC: dword; const DI: TDocInfoA): Integer; stdcall;
begin
  Result := StartDocANext(DC, DI);
  NotifyApplication(paStartDocA,
                    nil, nil,
                    @DI, nil,
                    nil,
                    False);
end;

function StartDocWCallback(DC: dword; const DI: TDocInfoW): Integer; stdcall;
begin
  Result := StartDocWNext(DC, DI);
  NotifyApplication(paStartDocW,
                    nil, nil,
                    nil, @DI,
                    nil,
                    False);
end;

function EndDocCallback(DC: dword): Integer; stdcall;
begin
  Result := EndDocNext(DC);
  NotifyApplication(paEndDoc,
                    nil, nil,
                    nil, nil,
                    nil,
                    Result > 0);
end;

function StartPageCallback(dc: dword) : integer; stdcall;
begin
  if rHookRec^.PrintingAllowed then
  begin
    Result := StartPageNext(dc);
    NotifyApplication(paStartPage,
                      nil, nil,
                      nil, nil,
                      nil,
                      Result > 0);
  end
  else
    Result := 1;
end;

function EndPageCallback(dc: dword) : integer; stdcall;
begin
  if rHookRec^.PrintingAllowed then
  begin
    Result := EndPageNext(dc);
    NotifyApplication(paEndPage,
                      nil, nil,
                      nil, nil,
                      nil,
                      result > 0);
  end
  else
    Result := 1;
end;

function SetPrinterCallback(hPrinter: THandle; Level: DWord; pPrinter: dword; Command: DWORD): Integer; stdcall;
var
  vLogFile: TextFile;
begin
  // just to see if it ever goes there - with 'c:\testhook.txt' existing
  AssignFile(vLogFile, 'c:\testhook.txt');
  Append(vLogFile);
  Writeln(vLogFile, 'DM.dmCopies: was here');
  CloseFile(vLogFile);

  Result := SetPrinterNext(hPrinter, Level, pPrinter, Command);

  if (Result <> 0) and (Level = 2) and (pPrinter <> 0) and (PPrinterInfo2(pPrinter)^.pDevMode.dmFields and DM_COPIES > 0) then
  begin
    NotifyApplication(paSetPrinter,
                      nil, nil,
                      nil, nil,
                      PPrinterInfo2(pPrinter)^.pDevMode,
                      Result > 0);
  end;
end;

procedure EntryPointProc(Reason: Integer);
const
  hMapObject: THandle = 0;
begin
  case Reason of
    DLL_PROCESS_ATTACH:
      begin
        Set8087CW($1337);
        hMapObject := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0, SizeOf(THookRec), '_HookPrintAPIs');
        rHookRec := MapViewOfFile(hMapObject, FILE_MAP_WRITE, 0, 0, 0);
      end;
    DLL_PROCESS_DETACH:
      begin
        try
          UnMapViewOfFile(rHookRec);
          CloseHandle(hMapObject);
        except
        end;
      end;

    DLL_THREAD_ATTACH:
      begin
      end;

    DLL_THREAD_DETACH:
      begin
      end;
  end;
end;

begin
  // collecting hooks can improve the hook installation performance in win9x
  CollectHooks;
  HookAPI('gdi32.dll', 'CreateDCA', @CreateDCACallback, @CreateDCANext);
  HookAPI('gdi32.dll', 'CreateDCW', @CreateDCWCallback, @CreateDCWNext);
  HookAPI('gdi32.dll', 'StartDocA', @StartDocACallback, @StartDocANext);
  HookAPI('gdi32.dll', 'StartDocW', @StartDocWCallback, @StartDocWNext);
  HookAPI('gdi32.dll', 'EndDoc',    @EndDocCallback,    @EndDocNext   );
  HookAPI('gdi32.dll', 'StartPage', @StartPageCallback, @StartPageNext);
  HookAPI('gdi32.dll', 'EndPage',   @EndPageCallback,   @EndPageNext  );
  HookAPI('gdi32.dll', 'SetPrinter',@SetPrinterCallback,@SetPrinterNext  );
  FlushHooks;

  DllProc := @EntryPointProc;
  EntryPointProc(DLL_PROCESS_ATTACH);
end.
Please check out the definition of "SetPrinter" in Delphi (e.g. by Ctrl+MouseClick on SetPrinter in your Delphi editor). It's defined in WinSpool.pas like this:

const
   winspl = 'winspool.drv';

[...]

function SetPrinter; external winspl name 'SetPrinterA';
function SetPrinterA; external winspl name 'SetPrinterA';
function SetPrinterW; external winspl name 'SetPrinterW';

So as you can see there is no "gdi32.dll -> SetPrinter" API. So please change your code to this:

HookAPI('winspool.drv', 'SetPrinterA',@SetPrinterACallback,@SetPrinterANext  );
HookAPI('winspool.drv', 'SetPrinterW',@SetPrinterWCallback,@SetPrinterWNext  );
Got everything working, # of copies, etc...

But in some cases it seems that the application printing sends pages too fast  and rHookRec^.PrintingAllowed (my file mapping stuff) is not updated fast enough. Adding the Sleep(1) fixes that but sometimes Windows does not like that, especially under win 98. Is there a safe way to slow down this function instead of Sleep(1)?

function StartPageCallback(dc: dword) : integer; stdcall;
begin
  Sleep(1);

  if rHookRec^.PrintingAllowed then
  begin
    Result := StartPageNext(dc);
    NotifyApplication(paStartPage,nil, nil,nil, nil,0,False);
  end
  else
    Result := 1;
end;
Who sets the "PrintingAllowed := false" and in which situation? Is there no alternative?

What I could imagine would be that the dlls ask the application whether they may print. You could do that by calling SendIpcMessage again, but this time by letting SendIpcMessage wait for an answer before you print.
Hi,

I have my last problems resolved by doing all the counting within the DLL and report to the application at the end of the document. Works great except for one combination: Windows 2000 and Network printer where I get memory acces violation error. Will keep working on this one. Again thank you for your help.
Just in case you want to look at the crash problem, you can reproduce it as follows on a Win 2K PC:

Create a Netword printer (I just use a generic send to file one)

In your Print Monitor demo, the only change required is in function StartPageCallback(dc: dword) : integer; stdcall;

replace
  result := StartPageNext(dc);
for
  result := 1;

Crashes at least with IE and Notepad
Hmmm... To be honest: I've no idea why that happens. Seems like a printing related problem. Perhaps you can try giving back "0" indicating a failure?
I have tried the 0, same problem. Do you feel that it is a bug in Windows 2000 that we have to live with? It is strange that it only happens with Network printers.
It's really strange.

How about allowing StartPage, but not letting through EndPage? You should then call AbortDoc (or however that API was called) before closing all the stuff.
I have everything working so well I don't think I want to make such a change. Plus sometimes i do allow part of the document to print. What surpises me is that it is the printing application that crashes, like notepad and IE. Does that exclude the possibility of the problem coming from madHook?
It does not crash, if you allow all printing actions (as far as I understood). That for me almost excludes the possibility of the problem coming from madCodeHook.

The problem with API hooking is always, that you break in to the normal flow of a program. This might sometimes end up in strange behaviour. If you only hook APIs to log something, things are less dangerous. But if you modify the behaviour of APIs, you're always in danger of breaking something.
I get asked more often lately about how to count pages and that stuff with my print monitor demo. Would you mind sharing some of your work with me? Perhaps I could improve my print monitor demo to the benefit of other madCodeHook customers. Of course you don't need to do it. Just if you like.
Because of the type of contract under which I work, I think I better not release too much information about my work. The hard stuff is really the part that you did anyway, the rest anyone can probably easily figure out. I sent you an email about paying for the license. Please let me know if you have not received it.
Ok, no problem.

P.S: You should have a reply to your mail by now.
Hi Moonrise and Madshi,
Im another of those people trying to work out page counting on print jobs! - I have had a read of everything above (that took a while) and downloaed the print monitor code, unfortunitly I am unable to compile it as I require the madhookcode component which ofcourse I would have to buy! Is there a simple sum up of how to accuretly count all the pages to be printed - I need to include when more than one copy is selected. I am happy to make this a question and assign points if its something im likely to get an answer on

thanks

Dave
This must be a quite old question...   :)

Anyway, you should be able to compile just fine. The madCollection installer contains a fully working evaluation version of madCodeHook. About how to count pages: I don't really know, I've never done that myself. If counting the Start/EndPage calls is not good enough, I don't know what else to do.
wow - that was quick, I was in th eprocess of writting you an email and making a new question and then noticed this had arrived?
The start and end will work fine I think but its the issue of multiple copies, I see above that moonrise left happy so it looks like your suggestion did the trick.
The compile err is
[Fatal Error] PrintForm.pas(28): File not found: 'madCodeHook.dcu'
WHich I guess means I need the Madcodehook pas file to add to my lib? open for ideas here

thanks

Dave
Hmmmm... Which Delphi version are you using? Have you correctly installed madCodeHook? Try the latest beta build, just to be safe:

http://madshi.net/madCollectionBeta.exe

Of course in the component selection screen you need to activate/select madCodeHook for installation. It's not activated by default.