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

Материал из DRKB


Мониторинг принтера[править | править код]

Если организация оказывает вычислительные услуги, то в один прекрасный момент перед ней неизбежно встанет проблема тарификации, напрямую связанная с автоматическим учетом ресурсов. Эта статья рассказывает о том, как автоматизировать учет использования принтера.

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


Теоретические основы мониторинга[править | править код]

В теории мониторинг принтера в 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