Utilisez l'API Win32 de surveillance du Spooler d'impression

DjmSoftWare

Ce tutoriel démontre l'utilisation des API win32 relatives à la surveillance du spooler d'impression. ♪

- Titre : Utilisez l'API Win32 de surveillance du Spooler d'impression

- Auteur : DjmSoftWare

- Parution : 21 mars 2003

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 DjmSoftWare . Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Cahier des Charges

- II. Solution proposée

- II-A. Méthode GetDefaultPrinter

- II-B. Méthode IsValidHandle

- II-C. Sauvegarde de la listView

- II-D. Lecture du fichier et initialisation de la listVIew

- II-E. Procédure de tri en fonction de la date et l'heure

- II-F. Constructeur de la classe principale

- II-G. Destruction de la classe principale

- II-H. Classe TPrinterNotifier

- II-I. Définition de la classe (PrintNotifier.h)

- II-J. Constructeur de la classe TPrinterNotifier

- II-K. Méthode Execute

- II-L. Méthode PostLog

- II-M. Destructeur de la classe TPrinterNotifier

- II-N. Gestionnaire de message WM_PrinterNotifier

- II-O. Gestionnaire de message WM_SortedListNotifier

- II-P. Classe de tri

- II-Q. Méthode Execute

I - Cahier des Charges

- énumérer toutes les imprimantes installées sur le PC en local ou en réseau.

- élément > pour chaque imprimante, afficher dans une ListView les paramètres suivants.

Date

Heure

JodId

ComputerName

User Name

Printer Name

Document

Page

Taille

- Charger et sauvegarder la ListView dans un fichier au format CSV.

- Consommer un minimum de ressources système.

- Possibilité de débogage et de trace (Log File) par paramètre dans la ligne de commande.

II - Solution proposée

Image de l'application.

Pour répondre au point 5 du cahier des charges, on va utiliser les API Win32.

HANDLE FindFirstPrinterChangeNotification(
HANDLE hPrinter, // handle to printer or print server to monitor for changes
DWORD fdwFlags, // flags that specify the conditions to monitor
DWORD fdwOptions, // reserved, must be zero
LPVOID pPrinterNotifyOptions // pointer to structure specifying printer information to monitor
);
BOOL FindNextPrinterChangeNotification(
HANDLE hChange, // handle to change notification object of interest
PDWORD pdwChange, // pointer to a value that indicates the condition that changed
LPVOID pPrinterNotifyOptions, // pointer to a structure that specifies a refresh flag
LPVOID *ppPrinterNotifyInfo // pointer to a pointer that receives printer information buffer
);
BOOL FindClosePrinterChangeNotification(
HANDLE hChange // handle to change notification object to close
);

Ces fonctions s'utilisent en relation avec les « Wait Function » des API Win32 ce qui nécessite l'utilisation d'une classe dédiée de type TThread.

On doit tout d'abord réaliser les points suivants :

- analyse de la ligne de commande pour déterminer si le mode debug est activé (param -debug) ;

- rechercher et stocker dans une StringList toutes les imprimantes installées ;

- créer une classe de type TThread pour le monitoring.

Définition de la classe de base (main.h)

//---------------------------------------------------------------------------
#ifndef MainH
#define MainH
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>
#include <ExtCtrls.hpp>
#include <FileCtrl.hpp>
#include <stdio.h>
#include "PrintNotifier.h"
#include "SortedJobThread.h"
#define JobFileName "JobList.csv"
//---------------------------------------------------------------------------
struct TJobInfoRecord {
AnsiString jDate;
AnsiString jtime;
DWORD jID;
AnsiString jPrinter;
AnsiString jComputer;
AnsiString jUser;
AnsiString jDocument;
int jPages;
DWORD jSize;

} ;

int __stdcall CustomSortProc(long Item1, long Item2, long ParamSort);

class TForm1 : public TForm {
__published: // Composants gérés par l'EDI
TListView *ListView1;
void __fastcall FormDestroy(TObject *Sender);
private :
int FileHandle;
bool FDebug;
TStringList* FPrinterList; // Liste des Imprimantes
AnsiString FDefaultPrinter; // nom de l'imprimante par défaut
AnsiString FPrinter; // nom de l'imprimante
HANDLE FHandle;
TJobInfoRecord* FJobRecord; // Enregistrement d'un job
TList* FJobListRec; // Liste des Enregistrements de Job
TPrinterNotifier* FPrintNotifier; // Ptr sur ThreadPrinterNotifier
TSortJobThread* FSortJobThread ; // Ptr sur TSortJobThread
int FThreadCount; // compteur de ThreadPrinterNotifier
TList* FThreadList; // Liste de Ptr sur ThreadPrinterNotifier
TStringList* FJobList; // Liste de Jobs (format csv)
void __fastcall GPrinterNotifier(TMessage &msg);
void __fastcall GUpdateListView(TMessage &msg);
void __fastcall FindInstalledPrinters();
void __fastcall GetDefaultPrinter();
bool __fastcall IsValidHandle(AnsiString &PrinterName);
void __fastcall SaveListViewToFile();
void __fastcall LoadListFromFile();
void __fastcall WriteLog(TJobInfoRecord *NewRec,int n);

// Déclarations de l'utilisateur
public : // Déclarations de l'utilisateur
__fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_PrinterNotifier,TMessage,GPrinterNotifier)
MESSAGE_HANDLER(WM_SortedListNotifier,TMessage,GUpdateListView)
END_MESSAGE_MAP(TForm)

} ; //---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

On va tout d'abord s'intéresser à rechercher toutes les imprimantes installées l'implémentation de la méthode FindInstalledPrinters() est définie de la manière suivante :

// find all installed Printers and Save PrinterList in FPrinterList
{
; DWORD Count=0,InfoCount=0,Flags=0,Level=0;
char ** PPBufContent;
unsigned char *PBuffer,* Buffer;
if (Win32Platform == VER_PLATFORM_WIN32_NT) // test si NT,XP
{
Flags=PRINTER_ENUM_CONNECTIONS | PRINTER_ENUM_LOCAL;
Level=4;
}
else
{
MessageDlg("Désolé cette application ne fonctionne que sous environnement\nWindows NT ou Windows XP",
mtError,
TMsgDlgButtons() << mbOK, 0);
Application->Terminate();
}
EnumPrinters(Flags,
NULL,
Level, // API Windows
NULL, // Count retourne la taille
0, // nécessaire pour le Buffer
&Count,
&InfoCount);
if (Count==0) return ; // pas d'imprimante -->Exit
Buffer= new unsigned char [Count]; // création du Buffer
PBuffer= Buffer; // PBuffer pointe sur le Buffer
EnumPrinters(Flags,
NULL, Level, //InfoCount contient le
Buffer, // Nbre d'éléments de type
Count, // PRINTER_INFO_4
&Count, // stocké dans le Buffer
&InfoCount);
if (InfoCount)
{
FPrinterList->Clear(); // on efface la Liste
for (DWORD n=0;n<InfoCount;n++)
{
PPBufContent=(char **)Buffer; //PPBufContent contient le contenu
Buffer+=sizeof (PRINTER_INFO_4); // déplacement dans le Buffer
FPrinterList->Add(*PPBufContent); // on additionne le résultat dans
}
} // la StringListe
delete []PBuffer; // destruction du Buffer

} ; }

On teste tout d'abord si le système d'exploitation est valide, car les fonctions de notifications ne sont implémentées que sous NT respectivement XP. On utilise ensuite l'API win32 EnumPrinters qui retourne dans la variable Count. La taille du buffer nécessaire pour stocker toutes les imprimantes trouvées dans le format PRINTER_INFO_4.

typedef struct _PRINTER_INFO_4 { // pri4
{
;
LPTSTR pPrinterName;
LPTSTR pServerName;
DWORD Attributes;
} PRINTER_INFO_4;

} ;

On appelle donc cette API EnumPrinters deux fois de suite lors du second appel, on a créé en mémoire un buffer d'unsigned char de taille égale à la variable Count en sortie de la fonction la variable InfoCount est initialisée avec le nombre d'imprimantes trouvées dans le format PRINTER_INFO_4, un résultat de 0 indique une erreur le format du Buffer retourné par Windows, c'est le suivant :

FPrinterList->Clear(); // on efface la Liste
{
;
for (DWORD n=0;n<InfoCount;n++)
{
PPBufContent=(char **)Buffer; //PPBufContent contient le contenu
Buffer+=sizeof (PRINTER_INFO_4); // déplacement dans le Buffer
FPrinterList->Add(*PPBufContent); // on additionne le résultat dans la StringListe
}

} ;

Pour les premiers tests, il peut être intéressant de travailler avec l'imprimante par défaut.

II-A - Méthode GetDefaultPrinter

void __fastcall TForm1::GetDefaultPrinter()
{
{
;
long BuffLength=0L; // recherche de l'imprimante par défaut
char DefaultPrinter [90];
BuffLength=GetProfileString("windows",
"device",
NULL,
DefaultPrinter,
sizeof (DefaultPrinter));
if (BuffLength)
{
FDefaultPrinter=DefaultPrinter;
FDefaultPrinter=FDefaultPrinter.SubString(1,FDefaultPrinter.Pos(",")-1);
} // Résultat dans FDefaultPrint

} ;
}

Pour utiliser les fonctions de notifications, on doit envoyer comme paramètre le Handle de l'imprimante.

II-B - Méthode IsValidHandle

bool __fastcall TForm1::IsValidHandle(AnsiString & PrinterName)
{
bool ReturnValue=false;
if (PrinterName!="")
{
ReturnValue=OpenPrinter(PrinterName.c_str(),
&FHandle,
NULL);
} // on essaye d'obtenir un Handle pour
return ReturnValue; // l'imprimante PrinterName
} // on retourne le résultat

Le Handle est stocké, s'il est valide dans le champ FHandle.

II-C - Sauvegarde de la listView

void __fastcall TForm1::SaveListViewToFile()
{
ListView1->Items->BeginUpdate();
if (FDebug)
{
SetCurrentDirectory("..");
ListView1->CustomSort(CustomSortProc,1);
}
TListItem* mListItem;
AnsiString Jobtxt;
AnsiString Separator=";";
for (int n=0;n<ListView1->Items->Count;n++)
{ // affectation de la première ligne
mListItem=ListView1->Items->Item[n]; // de la listView à mListItem
Jobtxt=mListItem->Caption; //affectation de Caption a Jobtxt
Jobtxt+=Separator; // ajout du séparateur
for (int i=0;i<8;i++) // itération dans la ligne N
{ // élément I
Jobtxt+=mListItem->SubItems->Strings[i];//affectation de Element[i]À Jobtxt
Jobtxt+=Separator; // ajout du séparateur
}
FJobList->Add(Jobtxt); // stockage de l'ansiString résultant
} // dans la la JobList
FJobList->SaveToFile(ExtractFilePath(ParamStr(0))+JobFileName);// sauvegarde
ListView1->Items->EndUpdate(); // fin du traitement de la ListView
}

On commence la méthode avec BeginUpdate ce qui va accélérer le traitement en empêchant l'affichage de mise à jour de la listView dans le mode debug les fichiers de trace sont mémorisés dans le directory Log le séparateur de texte est défini par <>, on itère simplement dans la listview, on fabrique une ligne complète avec les différents éléments de l'affichage, on stocke la ligne résultante dans la StringList et enfin on sauve le contenu de la StringList dans un fichier au format csv.

II-D - Lecture du fichier et initialisation de la listVIew

void __fastcall TForm1::LoadListFromFile()
{
try
{
FJobList->LoadFromFile(ExtractFilePath(ParamStr(0))+JobFileName);
ListView1->Items->BeginUpdate(); // début du traitement de la listView
TListItem* mListItem;
AnsiString Jobtxt;
AnsiString Separator=";";
int PosSep;
for (int n=0;n<FJobList->Count;n++)
{
FJobRecord=new TJobInfoRecord();
mListItem=ListView1->Items->Add();
Jobtxt= FJobList->Strings[n]; // extraction de la chaine
PosSep=Jobtxt.Pos(Separator); // recherche du premier séparateur
FJobRecord->jDate=Jobtxt.SubString(1,PosSep-1);
mListItem->Caption=FJobRecord->jDate; // Caption= 1er élément
Jobtxt.Delete(1,PosSep);
PosSep=Jobtxt.Pos(Separator); // on efface le premier élément
FJobRecord->jtime=Jobtxt.SubString(1,PosSep-1);
mListItem->SubItems->Add(FJobRecord->jtime);
Jobtxt.Delete(1,PosSep);
PosSep=Jobtxt.Pos(Separator);
FJobRecord->jID=atol(Jobtxt.SubString(1,PosSep-1).c_str());
mListItem->SubItems->Add(FJobRecord->jID);
Jobtxt.Delete(1,PosSep);
PosSep=Jobtxt.Pos(Separator);
FJobRecord->jComputer=Jobtxt.SubString(1,PosSep-1);
mListItem->SubItems->Add(FJobRecord->jComputer);
Jobtxt.Delete(1,PosSep);
PosSep=Jobtxt.Pos(Separator);
FJobRecord->jUser=Jobtxt.SubString(1,PosSep-1);
mListItem->SubItems->Add(FJobRecord->jUser);
Jobtxt.Delete(1,PosSep);
PosSep=Jobtxt.Pos(Separator);
FJobRecord->jPrinter=Jobtxt.SubString(1,PosSep-1);
mListItem->SubItems->Add(FJobRecord->jPrinter);
Jobtxt.Delete(1,PosSep);
PosSep=Jobtxt.Pos(Separator);
FJobRecord->jDocument=Jobtxt.SubString(1,PosSep-1);
mListItem->SubItems->Add(FJobRecord->jDocument);
Jobtxt.Delete(1,PosSep);
PosSep=Jobtxt.Pos(Separator);
FJobRecord->jPages=atoi(Jobtxt.SubString(1,PosSep-1).c_str());
mListItem->SubItems->Add(FJobRecord->jPages);
Jobtxt.Delete(1,PosSep);
PosSep=Jobtxt.Pos(Separator);
mListItem->SubItems->Add(Jobtxt.SubString(1,PosSep-1));
int Pos= Jobtxt.Pos("o");
if (Pos !=-1)
{
Jobtxt=Jobtxt.SubString(1,Pos-2);
FJobRecord->jSize=1024*atol(Jobtxt.c_str());
}
else
FJobRecord->jSize=atol(Jobtxt.SubString(1,PosSep-1).c_str());
FJobListRec->Add(FJobRecord);
} // fin du traitement de la ListView
FJobList->Clear();
ListView1->CustomSort(CustomSortProc,0);
ListView1->Items->EndUpdate();
// on efface le contenu de la List
}
catch (...){};
} //---------------------------------------------------------------------------

On utilise le traitement des exceptions pour empêcher d'intercepter C++ Builder d'afficher une fenêtre d'erreur lors de la première utilisation du programme le fichier n'existant pas.

L'appel à BeginUpdate empêche l'affichage de mise à jour de la listView, on itère ensuite dans le contenu de la stringList en utilisant le séparateur <;> pour séparer les éléments de la ListView.

II-E - Procédure de tri en fonction de la date et l'heure

int __stdcall CustomSortProc(long Item1, long Item2, long ParamSort)
{
TDateTime n1,n2;
TListItem *i1,*i2;
i1=(TListItem*)Item1;
i2=(TListItem*)Item2;
n1=TDateTime(i1->Caption)+TDateTime(i1->SubItems->Strings[0]);
n2=TDateTime(i2->Caption)+TDateTime(i2->SubItems->Strings[0]);
if (n1==n2) return 0;
if (ParamSort)
{
if (n1>n2) return 1;
if (n1;<n2) return -1;
}
else
{
if (n1>n2) return -1;
if (n1;<n2) return 1;
}
}

II-F - Constructeur de la classe principale

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
if (ParamCount())
{
if (ParamStr(1).LowerCase()=="-debug")
{
if (!DirectoryExists("log"))
CreateDir("Log");
SetCurrentDirectory("Log");
FDebug=true;
AnsiString Temp;
Temp="Logfile -";
Temp+=Date();
Temp+=" .log";
FileHandle=FileCreate(Temp);
Temp="Date\tHeure\tThreadId\tJobId\tComputer\tUser\tPrinter\tDocument\tPages\tSize";
Temp+="\r\n";
FileWrite(FileHandle,Temp.c_str(),Temp.Length());
}
}
FJobListRec= new TList(); // Construction de la Liste des Enregistrements de Job
FPrinterList= new TStringList(); // Construction de la Liste des imprimantes
FJobList=new TStringList(); // Construction de la Liste des Jobs
LoadListFromFile(); // Chargement du fichier JobListe +
FSortJobThread= new TSortJobThread(false,FJobListRec);// création de ThreadSortedJobList
FThreadList= new TList(); // Remplissage de la ListeView
FindInstalledPrinters(); // Recherche des Imprimantes Installées
for (int i=0;i<FPrinterList->Count;i++)
{
FPrinter=FPrinterList->Strings[i];
// GetDefaultPrinter(); // utilisé pour le Test
// FPrinter=FDefaultPrinter; // avec imprimante par défaut
if (IsValidHandle(FPrinter)) //si Handle Valide Construction d'un
{ // nouveau Thread par imprimante
FPrintNotifier= new TPrinterNotifier(false,
FPrinter.c_str(),
FThreadList->Count,
FHandle,FDebug);
FThreadList->Add((TPrinterNotifier*)FPrintNotifier);
} // ajout du Pteur de Thread dans la Liste
}
}

On teste tout d'abord si le programme a été lancé avec une ligne de commande. Si la réponse est positive et la valeur du paramètre de la ligne de commande égale à <-debug>. On crée le directory Log s'il n'existe pas, on place le champ FDebug à true, on crée un fichier log avec son entête. On crée une Liste FJobListRec de type Tlist, la StringList contenant les imprimantes, une StringListe contenant la liste des jobs. Chargement du fichier JobListText dans la ListView,création d'une Liste contenant tous les Threads créés recherche des imprimantes, on itère ensuite dans la StringList, on teste si le Handle est valide, si oui construction d'un objet Thread par imprimante. On stocke le pointeur résultant dans la Liste de Thread.

II-G - Destruction de la classe principale

delete FSortJobThread;
SaveListViewToFile(); // on sauve le contenu de la Listview dans un fichier
delete FPrinterList; // destruction de la Liste
for (int n=0;n<FThreadList->Count;n++) // destruction du contenu de la liste
delete (TPrinterNotifier*)FThreadList->Items[n];
delete FThreadList; // destruction de la Liste des PtrThreads
for (int n=0;n<FJobListRec->Count;n++) // destruction du contenu de la liste
delete (TJobInfoRecord*)FJobListRec->Items[n];
delete FJobListRec; // destruction de la Liste des PtrThreads
delete FJobList; // destruction de la JobListe
if (FDebug)
FileClose(FileHandle);

Sauvegarde de la listView dans le fichier JobList, destruction de la stringList contenant les imprimantes :

- destruction de tous les objets Threads créés ;

- destruction de la Liste de Threaad, fermeture du fichier de log si mode debug est activé.

II-H - Classe TPrinterNotifier

La classe TPrinterNotifier encapsule les API Win32 FindFirstPrinterChangeNotification et FindNextPrinterChangeNotification.

II-I - Définition de la classe (PrintNotifier.h)

//--------------------------------------------------------------------------- {
#ifndef PrintNotifierH
#define PrintNotifierH
//---------------------------------------------------------------------------
#include <Classes.hpp>
#include "Winspool.h"
#define WM_PrinterNotifier WM_APP+2000
#define LogFileName "ThreadLog"
#define FileExt ".log"

}
//--------------------------------------------------------------------------- {
class TPrinterNotifier : public TThread
{
private :
bool FDebugMode;
AnsiString FPrinterName;
HANDLE hNotifier;
int FileHandle;
AnsiString FPrinter;
AnsiString FComputer;
AnsiString FUser;
AnsiString FDocument;
DWORD FId;
HANDLE hPrinter;
int FThreadID;
int FPages;
int FTotPages;
DWORD FSize;
void __fastcall PostLog();
protected :
void __fastcall Execute();
public :
__fastcall TPrinterNotifier(bool CreateSuspended): TThread(CreateSuspended){};
__fastcall TPrinterNotifier(bool CreateSuspended, const char * PrinterName,
int TId,HANDLE PrinterHandle,bool ActiveDebug);
__fastcall ~TPrinterNotifier();
__property AnsiString Printer = { read = FPrinter };
__property AnsiString Computer = { read = FComputer };
__property AnsiString User = { read = FUser };
__property AnsiString Document = { read = FDocument };
__property DWORD Id = { read = FId };
__property int Pages = { read = FPages };
__property int TotPages = { read = FTotPages };
__property DWORD Size = { read = FSize };
};

} //---------------------------------------------------------------------------
#endif

Le but de cette classe est de transférer à la Form Principale (MainForm.pas) les informations sur le Job Courant. Le problème principal en utilisant un object Thread est la synchronisation avec le Thread VCL Principal l'utilisation de la méthode synchronize() fonctionne bien, mais est relativement lente dans notre cas si par exemple trois imprimantes sont installées et que trois tâches d'impression arrivent des problèmes de vitesse vont apparaître.

Une solution relativement simple pour contourner ce problème est l'envoi de messages aux ThreadVcl avec la méthode PostMessage,dans ce cas c'est la boucle de message de Windows qui synchronisera l'ensemble dans la FormPrincipale. On va ajouter un gestionnaire pour le message créé ainsi qu'une méthode adéquate.

void __fastcall GPrinterNotifier(TMessage &msg);
public : // Déclarations de l'utilisateur
__fastcall TForm1(TComponent* Owner);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_PrinterNotifier,TMessage,GPrinterNotifier)
END_MESSAGE_MAP(TForm)

Le message WM_PrinterNotifier WM_APP+2000 est défini dans l'entête de fichier. Le gestionnaire de message sera abordé ultérieurement.

Pour transférer les informations sur le job, j'ai défini des property permettant un accès en lecture seule des champs stockés dans la partie private de ma classe, j'ai ajouté également un nouveau constructeur avec mes besoins spécifiques, Id, PrinterName, PrinterHandle, debug).

II-J - Constructeur de la classe TPrinterNotifier

// constructeur du ThreadNotifier avec passage des paramètres
// PrinterName, ThreadId(Tid), Handle du Printer
__fastcall TPrinterNotifier::TPrinterNotifier(bool CreateSuspended ,
const char * PrinterName,
int TId,HANDLE PrinterHandle,
bool ActiveDebug)
: TThread(CreateSuspended)
{
FDebugMode=ActiveDebug;
FPrinterName=PrinterName;
FThreadID=TId;
hPrinter=PrinterHandle;
if (FDebugMode)
{
AnsiString Temp(LogFileName);
Temp+=" ";
Temp+=FPrinterName.SubString(3,20);
int pos=Temp.LastDelimiter("\\");
if (pos !=-1)
Temp.Delete(pos,2);
Temp+=" ";
Temp+=this ->ThreadID;
Temp+=FileExt;
FileHandle=FileCreate(Temp);
Temp="Date\tHeure\tThreadId\tJobId\tComputer\tUser\tPrinter\tDocument\tPages\tSize";
Temp+="\r\n";
FileWrite(FileHandle,Temp.c_str(),Temp.Length());
} }

Si le paramètre Activedebug est actif, on crée un nouveau Fichier par Object TPrinterNotifier
nommé par concaténation du printerName et du ThreadID.

II-K - Méthode Execute

La méthode Execute est le cœur de toute l'application, on trouve à l'intérieur l'appel aux fonctions FindFirstPrinterChangeNotification et FindNextPrinterChangeNotification.

PRINTER_NOTIFY_INFO *Pni; // déclaration des différentes Variables
unsigned long i;
char *pBuf;
unsigned long *adwData;
PRINTER_NOTIFY_OPTIONS Pob; // Pnobt contient
PRINTER_NOTIFY_OPTIONS_TYPE Pnobt[1];// 1 élément PRINTER_NOTIFY_OPTIONS_TYPE
unsigned short jobnf[10]; // Array JobInf
unsigned long ChangeReason;
i=0; jobnf[i++] = JOB_NOTIFY_FIELD_PRINTER_NAME; // attribution des
jobnf[i++] = JOB_NOTIFY_FIELD_MACHINE_NAME; // différentes valeurs
jobnf[i++] = JOB_NOTIFY_FIELD_USER_NAME; // devant être notifiées
jobnf[i++] = JOB_NOTIFY_FIELD_DOCUMENT;
jobnf[i++] = JOB_NOTIFY_FIELD_TOTAL_PAGES;
jobnf[i++] = JOB_NOTIFY_FIELD_TOTAL_BYTES;
Pnobt[0].Type = JOB_NOTIFY_TYPE; // type de notification
Pnobt[0].Count = i; // nombre d'éléments dans PField
Pnobt[0].pFields = jobnf; // assignation de jobnf
Pob.Version = 2; // Version toujours = à 2
Pob.Count = 1; // 1 élément PRINTER_NOTIFY_OPTIONS_TYPE
Pob.pTypes = Pnobt; // assignation de Pnobt
// FindFirstPrinterChangeNotification retourne un Handle sur un objet de
// notification initialisé avec PRINTER_CHANGE_ALL et la variable Pob
hNotifier = FindFirstPrinterChangeNotification( hPrinter,
PRINTER_CHANGE_ALL,
0,
&Pob);

// pas de notifier;;
while (!Terminated) // hNotifier égal au handle de
{ // l'object notification
if (!hNotifier) return ;
WaitForSingleObject(hNotifier, INFINITE);
// notification a eu lieu
if (!Terminated) // ordre de fin du Thread ?
{
Pob.Flags = 0; //demande de refresh
// FindNextPrinterChangeNotification rempli le contenu du pointeur pointé par
// Pni avec les éléments de notifications (jobinf) ayant changé
FindNextPrinterChangeNotification(hNotifier,
&ChangeReason,
&Pob,
(void **)&Pni);

if (Pni != NULL) // si pointeur diférent de NULL
{
for (i=0; i<Pni->Count; i++) // Itération
{
pBuf =(char *) Pni->aData[i].NotifyData.Data.pBuf;
adwData = Pni->aData[i].NotifyData.adwData; // assignation des résultats
FId=Pni->aData[i].Id;
switch (Pni->aData[i].Field) // itération dans la valeur
{ // des champs
case JOB_NOTIFY_FIELD_PRINTER_NAME:FPrinter=pBuf;
break ; // assignation des membres
case JOB_NOTIFY_FIELD_MACHINE_NAME:FComputer=pBuf; // de la classe
break ;
case JOB_NOTIFY_FIELD_USER_NAME: FUser=pBuf ;
break ;
case JOB_NOTIFY_FIELD_DOCUMENT:FDocument=pBuf;
break ;
case JOB_NOTIFY_FIELD_TOTAL_PAGES:FTotPages=adwData[0] ;
break ;
case JOB_NOTIFY_FIELD_TOTAL_BYTES:FSize=adwData[0];
break ;
}
} // plusieurs notifications peuvent avoir lieu successivement
// les champs ne sont pas tous remplis simultanément
// on doit tester si le nombre de pages est
// >=1 et que le champ Size soit >0
if (FDebugMode) PostLog();
if ((FSize>0)&&(FTotPages>=1))
PostMessage(Form1->Handle,WM_PrinterNotifier,FThreadID,0);
} // on poste le message au Thread VCL Principal (Form1)
FreePrinterNotifyInfo(Pni); // libération du Buffer
}
}
}
//---------------------------------------------------------------------------

On crée tout d'abord quatre variables :

PRINTER_NOTIFY_INFO *Pni; // Buffer initialisé en retour de l'appel à FindNextPrinterChangeNotification
PRINTER_NOTIFY_OPTIONS Pob; // Pnobt contient
PRINTER_NOTIFY_OPTIONS_TYPE Pnobt[1];// 1 élément PRINTER_NOTIFY_OPTIONS_TYPE
unsigned short jobnf[10];

On va ensuite initialiser le Tableau Jobnf avec les informations devant être notifiées :

i=0;
jobnf[i++] = JOB_NOTIFY_FIELD_PRINTER_NAME; // attribution des
jobnf[i++] = JOB_NOTIFY_FIELD_MACHINE_NAME; // différentes valeurs
jobnf[i++] = JOB_NOTIFY_FIELD_USER_NAME; // devant être notifiées
jobnf[i++] = JOB_NOTIFY_FIELD_DOCUMENT;
jobnf[i++] = JOB_NOTIFY_FIELD_TOTAL_PAGES;
jobnf[i++] = JOB_NOTIFY_FIELD_TOTAL_BYTES;

On initialise ensuite le tableau Pnobt[],ainsi que la variable Pob de type PRINTER_NOTIFY_OPTIONS :

Pnobt[0].Type = JOB_NOTIFY_TYPE; // type de notification
Pnobt[0].Count = i; // nombre d'éléments dans PField
Pnobt[0].pFields = jobnf; // assignation de jobnf
Pob.Version = 2; // Version toujours = À 2
Pob.Count = 1; // 1 élément PRINTER_NOTIFY_OPTIONS_TYPE
Pob.pTypes = Pnobt; // assignation de Pnobt

L'appel suivant à FindFirstPrinterChangeNotification va nous fournir un Handle qui sera initialisé par Windows lors d'une notification de job. On attend cette notification avec la procédure WaitForSingleObject, dès la signalisation d'éventuellement le programme sort de WaitForSingleObject. On initialise le membre Flags de la variable Pob à true ce qui a pour effet de demander un refresh à Windows. L'appel suivant à FindNextPrinterChangeNotification initialise la Pointeur PNI avec les éléments notificateurs.

if (Pni != NULL) // si pointeur diférent de NULL
{
for (i=0; i<Pni->Count; i++) // Itération
{
pBuf =(char *) Pni->aData[i].NotifyData.Data.pBuf;
adwData = Pni->aData[i].NotifyData.adwData; // assignation des résultats
FId=Pni->aData[i].Id;
switch (Pni->aData[i].Field) // itération dans la valeur
{ // des champs
case JOB_NOTIFY_FIELD_PRINTER_NAME:FPrinter=pBuf;
break ; // assignation des membres
case JOB_NOTIFY_FIELD_MACHINE_NAME:FComputer=pBuf; // de la classe
break ;
case JOB_NOTIFY_FIELD_USER_NAME: FUser=pBuf ;
break ;
case JOB_NOTIFY_FIELD_DOCUMENT:FDocument=pBuf;
break ;
case JOB_NOTIFY_FIELD_TOTAL_PAGES:FTotPages=adwData[0] ;
break ;
case JOB_NOTIFY_FIELD_TOTAL_BYTES:FSize=adwData[0];
break ;
}
} // plusieurs notifications peuvent avoir lieu successivement
// les champs ne sont pas tous remplis simultanément
// on doit tester si le nombre de pages est
// >=1 et que le champ Size soit >0
if (FDebugMode)PostLog();

Si le pointeur est non NULL on itère dans le buffer, on assigne les variables pBuf et adwData. Il ne reste plus qu'à travers un switch/case à assigner les membres concernés.

Il reste un problème à régler dans le cas d'un document. Il se peut que l'on ait plusieurs notifications de suite pour le même Job par exemple une page supplémentaire arrive dans le spooler, on devra donc filtrer les enregistrements reçus par JobId.

Le premier filtre est simplement établi de la manière suivante un message est envoyé à la forme principale uniquement si le nombre de pages est plus grand que 1 et que la taille du document est supérieure à 0. En plaçant un <-debug> dans la ligne de commande l'étude des fichiers trace sera très utiles pour comprendre la problématique de ces notifications multiples.

if (FDebugMode)PostLog();
if ((FSize>0)&&(FTotPages>=1))
PostMessage(Form1->Handle,WM_PrinterNotifier,FThreadID,0);
} // on poste le message au Thread VCL Principal (Form1)
FreePrinterNotifyInfo(Pni); // libération du Buffer

II-L - Méthode PostLog

void __fastcall TPrinterNotifier::PostLog()
{ char TimeBuffer[15];
SYSTEMTIME mSystime,mLocaTime;
TIME_ZONE_INFORMATION mTimeZone={0};
GetSystemTime(&mSystime);
GetTimeZoneInformation(&mTimeZone);
SystemTimeToTzSpecificLocalTime(&mTimeZone,
&mSystime,
&mLocaTime);
sprintf(TimeBuffer,"%d:%d:%d:%d",mLocaTime.wHour,
mLocaTime.wMinute,
mLocaTime.wSecond,
mLocaTime.wMilliseconds);
AnsiString Temp;
AnsiString Separator="\t";
Temp+=Date();
Temp+=Separator;
Temp+=TimeBuffer;
Temp+=Separator;
Temp+=FThreadID;
Temp+=Separator;
Temp+=FId;
Temp+=Separator;
Temp+=FComputer;
Temp+=Separator;
Temp+=FUser;
Temp+=Separator;
Temp+=FPrinter;
Temp+=Separator;
Temp+=FDocument;
Temp+=Separator;
Temp+=FTotPages;
Temp+=Separator;
Temp+=FPages;
Temp+=Separator;
Temp+=FSize;
Temp+="\r\n";
FileWrite(FileHandle,Temp.c_str(),Temp.Length());
}

Dans cette méthode, on crée un enregistrement par notification avec l'heure en format « hh:mm:ss:ms ».

II-M - Destructeur de la classe TPrinterNotifier

__fastcall TPrinterNotifier::~TPrinterNotifier()
{
if (Suspended) Resume();
SetEvent(hNotifier);// on sort du WaitforSingleObjet
Terminate(); // ordre de sortie du Thread (Terminated=true)
if (hNotifier) WaitFor(); // Attente de la fin du Thread
FindClosePrinterChangeNotification(hNotifier); //Libération du Handle hNotifier
ClosePrinter(hPrinter); // libération du Handle hPrinter
if (FDebugMode)FileClose(FileHandle);

}

Retour à la classe principale.

On a vu dans la classe précédente que l'objet PrinterNotifier communiquait avec le Thread principal par message interposé, ainsi que de la nécessité de supprimer les notifications identiques pour le même Job. On créera donc une nouvelle classe qui aura pour fonction de supprimer les multiples notifications.

II-N - Gestionnaire de message WM_PrinterNotifier

void __fastcall TForm1::GPrinterNotifier(TMessage &msg)
{ // FPrintNotifier contient le Ptr sur le Thread qui a transmis le Msg
FSortJobThread->Suspend(); // on suspend la fonction du SortedThread
FPrintNotifier=(TPrinterNotifier*)FThreadList->Items[msg.WParam];
FPrintNotifier->Suspend(); //on suspend le fonctionnement du Thread émetteur
TJobInfoRecord *NewRec= new TJobInfoRecord();
NewRec->jDate=Date();
NewRec->jtime=Time();
NewRec->jID=FPrintNotifier->Id;
NewRec->jComputer=FPrintNotifier->Computer;
NewRec->jPrinter=FPrintNotifier->Printer;
NewRec->jUser=FPrintNotifier->User;
NewRec->jDocument=FPrintNotifier->Document;
NewRec->jPages=FPrintNotifier->TotPages;
NewRec->jSize=FPrintNotifier->Size;
FJobListRec->Add((TJobInfoRecord*)NewRec);
if (FDebug)WriteLog(NewRec,msg.WParam);
FPrintNotifier->Resume();
FSortJobThread->Resume();
}
//---------------------------------------------------------------------------

FSortJobThread est un pointeur sur la classe de tri que nous créerons ultérieurement, on stoppe le fonctionnement de ce Thread.

FPrintNotifier contient le Ptr sur le Thread qui a transmis le Msg, on crée ensuite un nouvel objet de type.

TJobInfoRecord et on initialise ses champs avec les property du Thread transmetteur, si mode debug on écrit dans le LogFile, on ajoute l'objet de Type TJobInfoRecord dans une TList qui sera triée par la classe de tri.

II-O - Gestionnaire de message WM_SortedListNotifier

Le message WM_SortedListNotifier est généré par la classe de tri :

void __fastcall TForm1::GUpdateListView(TMessage &msg)
{
FSortJobThread->Suspend();
ListView1->Items->BeginUpdate();
// début du traitement de la listView
ListView1->Items->Clear();
TListItem* mListItem;
TJobInfoRecord *First;
for (int n=0;n<msg.WParam;n++)
{
First=(TJobInfoRecord*)FJobListRec->Items[n];
mListItem=ListView1->Items->Add();
mListItem->Caption=First->jDate;
mListItem->SubItems->Add(First->jtime);
mListItem->SubItems->Add(First->jID);
mListItem->SubItems->Add(First->jComputer);
mListItem->SubItems->Add(First->jUser);
mListItem->SubItems->Add(First->jPrinter);
mListItem->SubItems->Add(First->jDocument);
mListItem->SubItems->Add(First->jPages);
if (First->jSize >1024)
{
AnsiString Size= First->jSize 1024; // test pour affichage
mListItem->SubItems->Add(Size+ " Ko"); // en octets ou en Ko
}
else
mListItem->SubItems->Add(First->jSize);
}
ListView1->CustomSort(CustomSortProc,0);
ListView1->Items->EndUpdate();
FSortJobThread->Resume();
}
//---------------------------------------------------------------------------

On suspend d'abord le fonctionnement de la classe de tri.

On empêche la mise à jour de l'affichage dans la list View.

On efface le contenu de la list View.

On remplit itération dans la FJobListRec pour remplir la ListView.

Affichage en Ko de la taille du document.

On trie la liste en sens inverse.

On affiche le résultat.

On reprend le fonctionnement de la classe de tri.

II-P - Classe de tri

Cette classe a pour but d'éliminer les enregistrements en double :

//---------------------------------------------------------------------------
#ifndef SortedJobThreadH
#define SortedJobThreadH
//---------------------------------------------------------------------------
#include <Classes.hpp>
#define WM_SortedListNotifier WM_APP+2010
//---------------------------------------------------------------------------
class TSortJobThread : public TThread
{
private :
TList* FSortedList;
int FCounter;
int FThreadNbr;
protected :
void __fastcall Execute();
public :
__fastcall TSortJobThread(bool CreateSuspended): TThread(CreateSuspended){};
__fastcall TSortJobThread(bool CreateSuspended, TList* JobList,int ThreadNbr);
__fastcall ~TSortJobThread();
};
//---------------------------------------------------------------------------
#endif

On utilise la même méthode de communication avec le Thread VCl Principal, ce qui nécessite la création d'un message WM_SortedListNotifier.

On déclare un nouveau constructeur avec comme paramètre supplémentaire un pointeur sur la JobList.

II-Q - Méthode Execute

void __fastcall TSortJobThread::Execute()
{
int Iter=0,FirstPos=1;
TJobInfoRecord *First,*Second;
while (!Terminated)
{
while (Iter<=(FSortedList->Count-2)) // premier élément est égal au second
{ // on echange les éléments dans la liste,puis on efface le premier élément
if (Terminated) break ; // on teste si on trouve un doublon dans la liste
First=(TJobInfoRecord*)FSortedList->Items[Iter]; // n° Test (elem* elem/2)-elem;
// teste en boucle avec tous les éléments de la listewhile (FirstPos<FSortedList->Count)
{
if (Terminated) break ;
Second=(TJobInfoRecord*)FSortedList->Items[FirstPos];
if ((First->jID== Second->jID)&&(First->jDate==Second->jDate )) // même Id et Même Jour
{
FSortedList->Exchange(Iter,FirstPos);
FSortedList->Delete(FirstPos);
}
else
FirstPos++;
}
Sleep(2);
Iter++;
FirstPos=Iter+1;
} if (FCounter !=FSortedList->Count)
{
PostMessage(Form1->Handle,WM_SortedListNotifier,FSortedList->Count,0);
FCounter=FSortedList->Count;
}
Iter= FCounter-(FThreadNbr*2);
if (Iter<0)Iter=0;
FirstPos=Iter+1;
Sleep(2);
}
}
//---------------------------------------------------------------------------

Principe de fonctionnement

Tant que le premier élément de la liste est égal au second élément, on échange les éléments trouvés sinon on recommence. À partir du 2e élément ceci jusqu'à la fin de la liste si le champ Counter et différent du nombre d'éléments de la liste triée on a envoyé un message au ThreadVcl, pour rafraîchir la List View le champ Counter prend la valeur du nombre d'éléments de la liste triée et le cycle reprend dès le début.