Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Программирование для начинающих и не только


©Gigabyte 2005

Мониторинг принтера

Сегодня практически в каждом интернет-кафе к доступным услугам входит так же и распечатка текстовых и других документов. Но вот огранизация этого дела все еще остается на этапе минимальной автоматизации. Вот если бы удалось написать програмку, которая бы отслеживала кто, когда и скольо на печатал, и интегрировать ее в систему контроля копмьютерного клуба, то это не только бы избавило наших <бедных> администраторов от лишнего диалога с клиентом, но и придало клубу более современный оттенок.

Теоретические основы мониторинга

В теории мониторинг принтера в Windows 98 не является чем-то черезвычайно сложным т.к. те же драйверы принтеров его не просто осуществляют, но также и дают возможность управления работой принтера. Мы же пока будем довольствоваться только слежением за посылаемыми на печать задачами. Для этого в арсенале Windows предусмотрено 2 метода. Первый из них - Использование метода отслеживания сообщения WM_SPOOLERSTATUS, которое система посылает всякий раз при добавлении нового задания в очередь или же удалении оного из нее. Второй способ - использование функций FindFirstPrinterChangeNotification, FindNextPrinterChangeNotification и FindClosePrinterChangeNotification. В этой заметке мы рассмотрим первый из них.

Как и большинство сообщений Windows WM_SPOOLERSTATUS возвращает в структуре TMessage некоторую полезную информацию, которую вы можете ипользовать в своих нуждах. Но к сожаление никакой действительно важной и необходимой нам информации (кроме количества оставшихся в очереди заданий) эта структура не несет. Но к счастью в Windows есть дополнительные методы для определения всей жизненно важной для нас информации. Так среди прочих в модуле WinSpool есть функция EnumJobs, которая возвращает количество и характеристики заданий печати, в которых содержится вся необходимая нам информация; от названия документа, машины и имени ползователя, который откправил этот документ на песать, до времени когда это было сделать и количества страниц, которые были посланы на печать. Эта функция выглядит следующим образом:


function EnumJobs(hPrinter: THandle; FirstJob, NoJobs, Level: DWORD; pJob: Pointer; cbBuf: DWORD;
  var pcbNeeded, pcReturned: DWORD): BOOL; stdcall;
где hPrinter - идентификатор принтера к которому мы делаем запрос.
FirstJob - номер первого запрашиваемого задания
NoJobs - количество запрашиваемых заданий
Level - тип структуры которая передается в pJob
PJob - указатель на массив переменных типа JOB_INFO_1 при Level=1 и JOB_INFO_2 при Level=2. см. Таблицы 1,2
CbBuf - размер буфера данных.
PcbNeeded - переменная в которую заносится количество записанных в буфер данных при удачном завершении функции или же необходимый размер буфера в байтах при неудачном завершении функции.
PcReturned - Количество записей занесенных в буфер в которых содержится актуальная информация.

Для нормальной работы этой функции нам необходимо прежде всего раздобыть идентификатор принтера, который мы должны поместить в параметр hPrinter. Для этого сущеструет функция OpenPrinter, которая открывает соответствующий принтер и выдает необходимый идентификатор:


function OpenPrinter(pPrinterName: PChar; var phPrinter: THandle; 
 pDefault: PPrinterDefaults): BOOL; stdcall;
где ppPrinterName - имя принтера, который следует открыть.
phPrinter - переменная в которую будет записан идентификатор открытого принтера
pDefault - структура содержащая данные необходимай для инициализации принтера.

Если в системе присудствует только отдин принтер (подключенный к компьютеру админа), то параметр ppPrinterName может быть строго зафиксирован в самом коде приложения, если же принтеров в сети несколько (подключенный к серверу, к компьютеру администратоа, к компьютеру кассы и т.п.), то лучшим вариантом будет дать возможность ползователю самостоятельно выбирать какой из принтеров следует инспектировать. Для этого можно воспользоваться функцией перечисления доступных принтеров:


function EnumPrinters(Flags: DWORD; Name: PChar; Level: DWORD;
  pPrinterEnum: Pointer; cbBuf: DWORD; var pcbNeeded, pcReturned: DWORD): BOOL; stdcall;
Здесь Flags - флаги которые дазают особенности перечисления принтеров (см. Таблицу.3)
Name - название принт-объекта. Значение этого параметра используется в паре с Level (см.Таблицу 4)
Level - индекс уровня используется вместе с Name (см. Таблицу 4)
PPrinterEnum - указатель на массив элементов одного из типов TPrinterInfo1, TPrinterInfo2, TPrinterInfo4, TPrinterInfo5. (см. Таблицу 4)
CbBuf - размер буфера pPrinterEnum.
PcbNeeded - переменная в которой содержится количество байт скопированных при удачноз завершении операции или же размер недостающей памяти если значение cbBuf слишкм мало.
PcReturned - количество актуальных записей занесенных в буфер.

Практическая реализация

Для пракотческой реализации мы как всегде воспользуемя Delphi и сперва создадим свежее приложение щелкнув на пункте New->Application. Далее для демонстрации работы мы перенесем на форму 2 компонента: TreeView и ListBox.(рис.1). Дале занесем в OnCreate код для перечисления доступных принтеров:


procedure TForm1.FormCreate(Sender: TObject);
var PI:array[0..1023] of PRINTER_INFO_1;
    Needed,Returned:Cardinal;i:integer;
begin
PrintersRoot:=TreeView1.Items.AddFirst(nil,'Printers');
if not EnumPrinters(PRINTER_ENUM_LOCAL,nil,1,@PI,SizeOf(PI),Needed,Returned) then
 ListBox1.Items.Add(SysErrorMessage(GetLastError));
For i:=0 to Returned-1 do
 TreeView1.Items.AddChild(PrintersRoot,PI[i].pName);
end;
Здесь мы сначала создаем родительксий узел в TreeView куда будем записывать все найденные принтеры. Далее запускаем поиск локальных принтеров посредством передачи в EnumPrinters флага PRINTER_ENUM_LOCAL. После этого при успешном завершении функции мы переносим названия всех принтеров в TreeView, или же выводим сообщение об ошибке.

Теперь у нас есть список всех доступных принтеров и мы со спокойной душей можем предложить пользователю выбрать и открыть один из них. Для этого в реакцию на событие OnDblClick компонента TreeView1 мы запишем следующий код:


procedure TForm1.TreeView1Click(Sender: TObject);
begin
 if not OpenPrinter(PChar(TreeView1.Selected.Text),PH,nil) then
  ListBox1.Items.Add(SysErrorMessage(GetLastError));
end;
Теперь после двойного щелчка на названии принтера мы открываем его и записываем полученный идентификатор в переменную PH, которая объявлена в секции private нашей формы. А для пущей правильности кода мы в OnDestroy нашей формы напишем:

procedure TForm1.FormDestroy(Sender: TObject);
begin
 ClosePrinter(PH);
end;
Далее нам предстоит реализовать слежение за сообщением WM_SPOOLERSTATUS и для этого я рекомендую выбрать способ предоставляемый Delphi посредством служебного слова message:

  TForm1 = class(TForm)
    TreeView1: TTreeView;
    ListBox1: TListBox;
    procedure FormCreate(Sender: TObject);
    procedure TreeView1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
   PrintersRoot:TTreeNode;
   PH:Cardinal;
   procedure WMPrinterStatus(var Msg:TMessage); message WM_SPOOLERSTATUS;
    { Private declarations }
  public
    { Public declarations }
  end;
Здесь метод WMPrinterStatus будет вызываться только при поступлении сообщения WM_SPOOLERSTATUS.

В коде реализации этого метода мы с вами напишем вот что:


procedure TForm1.WMPrinterStatus(var Msg:TMessage);
var i:integer;
   Job2:Array[0..1023] of JOB_INFO_2;Needed,Returned:Cardinal;
begin
Msg.Result:=0;
ListBox1.Items.Add('Jobs Left:'+IntToStr(Msg.WParamLo));
if not EnumJobs(PH,0,1024,2,@Job2,SizeOf(Job2),Needed,Returned) then 
 ShowMessage(SysErrorMessage(GetLastError));
For i:=0 to Returned-1 do
 With Job2[i] do
  ListBox1.Items.Add(Format('%s %s %s',[pPrinterName,pDocument,pUserName]))
end;
Все. Наш проект готов, и надо его надо только скомпилить.

После запуска приложения на экране появляется уже знакомое нам окно, в компоненте TreeView которого присутствует список доступных локальных принтеров (рис.2). Открываем принтер (двойной щелчек на названии) и запускаем любой текстовый редактор. Открываем документ (для пробы можно воспользоваться и графическим радактором). А теперь: <Файл->Печать> и мы видим:.(рис.3).

Послесловие

Итак теперь мы приблизительно знаем, как отслеживать состояние очереди на печать. И не просто отслеживать, а автоматически обрабатывать эту информацию в своих корыстных целях. Но так или иначе, этот метод не эдинственный в своём роде. Так, кроме ракции на событие WM_SPOOLERSTATUS можно реализовать обработку очереди посредством функций FindFirstPrinterChangeNotification, FindNextPrinterChangeNotification, FindClosePrinterChangeNotification, которые позволяют не отслеживать не только добавление и удаление заданий в(из) очереди, но также и состояние очереди в процесе печати, и много других возможностей. Кроме того, эти методы дают возможность настроить реакцию программы под более конкретные задачи (Printer, PrinterDriver, Form, Job:), что конечно позитивно сказывается на работоспособности системы вцелом, особенно при больших нагрузках на нее.

К негативным же сторонам можно отнести то, что при обработке FindFirstPrinterChangeNotification, FindNextPrinterChangeNotification, FindClosePrinterChangeNotification необходимо реализовывать отдельный поток для отслеживания этих действий, что само по себе непростая задача для человека, который еще ни разу не работал с потоками вплотную.

©Gigabyte 2005
Таблица.1. Структура JOB_INFO_1
JobId:DWORDИдентификатор задания
PPrinterName:PCharНазвание принтера на который бало послано задание
PMachineName:PCharНазвание машины которая послала задание
PUserName:PCharПользователь, который послала задание
PDocument:PCharНазвание документа посланого на печать
PDataType:PCharСтрока-идентификатор типа данных
PStatus:PCharСтрока-идентификатор состояния печати
Status:DWORDУпрощенное состояние печати. Используется если PStatus=nil
Priority:DWORDПриоритет задания
Position:DWORDПозиция задания в очереди для печати
TotalPages:DWORDКоличаство страниц задания
PagesPrinted:DWORDКоличество отпечатанных страниц
SubmittedВремя, когда документ был послан на печать.

Таблица.2. JOB_INFO_2. Поля дополняющие JOB_INFO_1
PNotifyName:PCharИмя пользователя, который должен быть извещен при завершении печати или возникновении ошибок
PPrintProcessor:PCharНазвание принт-процесора, который должен быть задействован при печати этого дазания
PParameters:PCharПраметры для принт-процесора
PDriverName:PCharНазвание драйвера принтера, который должен быть задействован при печати
PDevModeУказатель на структуру содержащую данный инициализации принтера
PSecurityDescriptorВрегда NULL
StartTimeВремя начала печати
UntilTimeВремя конца печати
SizeРазмер в байтах данного задания
TimeВремя которое прошло от начала печати задания

Таблица.3 Флаги для EnumPrinters
PRINTER_ENUM_LOCALИгнорирует Name и перечисляет все локальные принтеры
PRINTER_ENUM_NAMEПеречисляет принтеры заданные параметорм Name, Если Name=nil, то перечисляет всех досупных принт-провайдеров
PRINTER_ENUM_SHAREDПеречисляет расшаренные принтеры, должен использоваться в паре с другим флагом
PRINTER_ENUM_DEFAULTТолько в Win95. Возвращает информацию о принтере по-умолчанию
PRINTER_ENUM_CONNECTIONSТолько NT. Возвращает информацию о принтерах, к которым было обращение
PRINTER_ENUM_NETWORKТолько NT. Перечисляет сетевые принтеры в домене данного компьютера. Используется при Level=1
PRINTER_ENUM_REMOTEТолько NT. Перечисляет сетевые принтеры и принт-серверы в домене данного компьютера. Используется при Level=1

Таблица.4
LevelNamePrinterEnumОписание
1Name<>nilTPrinterEnum1Name указывает на название перечисляемого объекта
1Name=nilTPrinterEnum1Перечисляет всех доступных принт-провайдеров
2,5Name<>nilTPrinterEnum2, TPrinterEnum5Перечисляет принтеры принт-сервера с именем Name
2,5Name=nilTPrinterEnum2, TPrinterEnum5Перечисляет принтеры подключенные на локальной машине
4Name=nilTPrinterEnum4Перечисляет принтеры доступные локальной машине в т.ч. и сетевые.

В избранное