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

== Мониторинг принтера ==
Если организация оказывает вычислительные услуги, то в один прекрасный момент перед ней неизбежно встанет проблема тарификации, напрямую связанная с автоматическим учетом ресурсов. Эта статья рассказывает о том, как автоматизировать учет использования принтера.
В перечень услуг интернет-кафе давно уже входит распечатка текстовых и других документов. Однако организация этого дела все еще остается на этапе минимальной автоматизации. Вот если бы удалось написать программку, которая отслеживала бы, кто, когда и сколько напечатал, да интегрировать ее в систему контроля компьютерного клуба, то это не только избавило бы администраторов от лишнего диалога с клиентом, но и придало клубу более современный вид.
== Теоретические основы мониторинга ==
В теории мониторинг принтера в Windows 98 не является чем-то неимоверно сложным - те же драйверы не просто осуществляют этот самый мониторинг, но и предоставляют возможности по управлению работой принтера. Мы же пока будем довольствоваться лишь отслеживанием посылаемых на печать задачами. Для этого в арсенале Windows предусмотрено два метода.
Первый - использование метода отслеживания сообщения 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 - количество занесенных в буфер записей, в которых содержится актуальная информация.
 Таблица 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 	Время, прошедшее с момента начала печати
Для нормальной работы этой функции, прежде всего, необходимо раздобыть идентификатор принтера, который мы должны поместить в параметр 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 - количество актуальных записей, занесенных в буфер.
 Таблица 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.
Level	Name 	PrinterEnum 	Описание
1 	Name <> nil	TPrinterEnum1 	Name указывает на название перечисляемого объекта
2 	Name = nil 	TPrinterEnum1 	Перечисляет всех доступных принт-провайдеров
2, 5 	Name <> nil	TPrinterEnum2, TPrinterEnum5	Перечисляет принтеры принт-сервера с именем Name
2, 5 	Name = nil 	TPrinterEnum2, TPrinterEnum5	Перечисляет принтеры, подключенные на локальной машине
4 	Name = nil 	TPrinterEnum4 	Перечисляет принтеры, доступные локальной машине, в том числе сетевые
== Практическая реализация ==
Для практической реализации мы, как всегда, воспользуемся Delphi и создадим сначала свежее приложение (New Application). Далее для демонстрации работы мы перенесем на форму 2 компонента: TreeView и ListBox
Далее занесем в 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:
type
 TForm1 = class(TForm)
 TreeView1: TTreeView;
 ListBox1: TListBox;
 procedure FormCreate(Sender: TObject);
 procedure TreeView1Click(Sender: TObject);
 procedure FormDestroy(Sender: TObject);
 private
 { Private declarations }
 PrintersRoot: TTreeNode;
 PH: Cardinal;
 procedure WMPrinterStatus(var Msg: TMessage); message WM_SPOOLERSTATUS;
 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 которого присутствует список доступных локальных принтеров (рисунок ниже). Открываем принтер (двойной щелчок на названии) и запускаем любой текстовый редактор. Открываем документ (для пробы можно воспользоваться и графическим редактором). А теперь: Файл р Печать - и мы видим…
== Послесловие ==
Итак, теперь мы в курсе, как отслеживать состояние очереди на печать. И не просто отслеживать, а еще и автоматически обрабатывать эту информацию в корыстных целях :).
Впрочем, этот метод не единственный в своем роде. К примеру, помимо реакции на событие WM_SPOOLERSTATUS, можно реализовать обработку очереди посредством функций FindFirstPrinterChangeNotification, FindNextPrinterChangeNotification, FindClosePrinterChangeNotification, которые позволяют отслеживать не только добавление и удаление заданий в(из) очереди, но и состояние очереди в процессе печати, а также предоставляют ряд дополнительных возможностей. Кроме того, эти методы позволяют настроить реакцию программы под более конкретные задачи (Printer, PrinterDriver, Form, Job…), что, конечно, позитивно сказывается на работоспособности системы в целом, особенно при больших нагрузках на нее.
К отрицательным же сторонам вопроса можно отнести тот факт, что при обработке FindFirstPrinterChangeNotification, FindNextPrinterChangeNotification, FindClosePrinterChangeNotification необходимо реализовывать отдельный поток для отслеживания этих действий, что само по себе непростая задача для человека, никогда не работавшего с потоками вплотную.
Author: Михаил Продан (2005.09.28)
Source: http://www.cpp.com.ua
ID: 03223
Category:Принтер
Category:Статьи