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 affichée dans une ListView les paramètres suivants ;
  • Charger et Sauvegarder la ListView dans un fichier au format CSV ;
  • Consommer un minimum de ressources Systèmes ;
  • Possibilité de debugging et de trace (Log File) par paramètre dans la ligne de commande.
Date
Heure
JodId
ComputerName
User Name
Printer Name
Document
Page
Taille

II. Solution Proposée

Image de l'application.

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

 
Sélectionnez
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)

 
Sélectionnez
//---------------------------------------------------------------------------
#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 :

 
Sélectionnez
// 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É dansle 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.

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

On appelle donc cette API EnumPrinters 2 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 :

 
Sélectionnez
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 test il peut être intéressant de travailler avec l'imprimante par défaut.

II-A. Méthode GetDefaultPrinter

 
Sélectionnez
void __fastcall TForm1::GetDefaultPrinter()
{
{
    ;
    long BuffLength=0L; // recherche de l'imprimante par dÉfault
    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

 
Sélectionnez
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

 
Sélectionnez
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 a 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

 
Sélectionnez
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= 1 er É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 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 mis-à-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

 
Sélectionnez
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

 
Sélectionnez
__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 Imprimante 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 dans la Liste de Thread.

II-G. Destruction de la Classe Principale

 
Sélectionnez
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)

 
Sélectionnez
//--------------------------------------------------------------------------- {
    #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 3 imprimantes sont installées et que 3 taches 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.

 
Sélectionnez
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ée ultérieurement.

Pour transférer les informations sur le job j'ai défini des property permettant un accès en lecture seuls 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

 
Sélectionnez
// 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.

 
Sélectionnez
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; // assigantion 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 succéssivement
// 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 4 variables :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 soit 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.

 
Sélectionnez
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

 
Sélectionnez
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

 
Sélectionnez
__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 fonctions de supprimer les multiples notifications.

II-N. Gestionnaire de message WM_PrinterNotifier

 
Sélectionnez
void __fastcall TForm1::GPrinterNotifier(TMessage &msg)
{ // FPrintNotifier contient le Ptr sur le Thread qui a transmis le Msg
FSortJobThread->Suspend(); // on suspend le 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 :

 
Sélectionnez
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 :

 
Sélectionnez
//---------------------------------------------------------------------------
#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

 
Sélectionnez
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ément 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 2 em é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 repend dès le début.