Приветствую всех. В сегодняшней статье речь пойдёт о том, как можно реализовать собственный высокоуровневый API в управляемом коде для работы с устройствами печати, от установки нового монитора печати в системе и до получения обработанного драйвером устройства печати документа с порта принтера.
Как и в прошлый раз, статья будет полезна для ознакомления разработчикам младшего и среднего звена. В процессе изучения материала, Вы узнаете как можно обращаться к низкоуровневым DLL WinAPI в C# с помощью P/Invoke, как установить, настроить и удалить из системы мониторы печати, драйвера принтера, само устройство печати, открыть и связать порт для перенаправления входных данных с устройства печати на монитор, познакомитесь с ключевыми моментами применения маршалирования. Так же мы на практическом примере разберёмся, как с помощью нашего API можно удобно манипулировать устройствами печати в системе, узнаем как можно перехватить обработанные данные после печати с принтера и, например, отправить их на сервер.
Сперва я предлагаю обрисовать примерный список задач, которые нам необходимо будет решать в данной статье. Пускай он будет выглядеть следующим образом:
В рамках статьи мы будем использовать уже реализованый итальянским разработчиком Lorenzo Monti монитор печати mfilemon.dll, а в качестве драйвера для принтера используем официальный драйвер Microsoft PostScript Printer Driver. По сути, монитор и драйвер могут быть любыми, в зависимости от требований вашей задачи.
Я не буду рассматривать в данной статье теоретическую базу по печати документов, различиям между принтерами с поддержкой PCL/PostScript и принтерами GDI и прочую базу. Всю необходимую информацию по данной тематике можно в избытке найти на просторах сети. Но кое-что знать всё же необходимо, чтобы лучше понимать как реализовать поставленные перед нами задачи.
Начнём с самого главного — процессом печати в Windows рулит служба Spooler. В директории C:/Windows/System32/ имеется её собственный низкоуровневый драйвер winspool.drv, в котором заложено множество точек входа для обращения к службе печати и выполнения целого ряда действий, от получения системной директории драйверов печати и имени установленного по умолчанию принтера в системе до манипуляций с очередью задач на печать. Для тех, кто хочет написать собственный монитор (о чём, возможно, я когда-нибудь так же расскажу в одной из будущих статей), помимо winspool.h ещё понадобится winsplp.h из DDK, представляющий дополнительный функционал для сборки драйвера в соответствии спецификации Spooler.
Далее, полнофункциональное устройство печати в Windows, говоря простым языком, состоит из монитора печати, открытого на мониторе порта, собственно устройства печати (принтера) и предварительно установленного драйвера к нему (рис. 1). На одном мониторе может быть открыто сразу несколько портов, к одному порту может быть привязано сразу несколько принтеров. Это важно учитывать при удалении того или иного компонента из системы, если, например, Вы захотите удалить определённый порт, предварительно нужно будет снести и все устройства печати, которые к нему привязаны.

Platform Invocation Services (PInvoke) — платформа д��я обращения к точкам входа DLL (функциям), написанным в неуправляемом коде (C/C++ и WinAPI), из кода управляемого (C#/VB и .NET). Для того, чтобы обратиться к низкоуровневому коду из .NET, нам нужно описать External-методы из DLL, а так же структуры, которые в них используются. Рассмотрим каждый из сучаев по порядку на примерах.
Пример 1. Вызов метода GetPrinterDriverDirectory из драйвера winspool.drv.
Первым делом, нам нужно знать, что возвращает метод и что принимает в аргументах при вызове. Для этого лезем в документацию и читаем описание сигнатуры метода. Обратите внимание, в дальнейшем к документации низкоуровневого API нам придётся обращаться сплошь и рядом, далее по ходу статьи я не буду больше указывать о необходимости этого действия при реализации тех или иных методов/структур, т.к. они требуются по умолчанию.
Описание каждого параметра функции так же можно найти в документации. Здесь важно понимать, что параметры могут быть только входными (In), только выходными (Out), как входными, так и выходными одновременно (In/Out), а так же опциональными (либо входными, либо выходными, в зависимости от других параметров). Так же нам нужно знать, какой тип данных в .NET нужно сопоставить WDT-типу (здесь, в большинстве случаев, работает правило «размеры выделяемой памяти значимым типам .NET соответствуют размерам выделяемой памяти базовым типам C++, для остальных есть IntPtr»).
Теперь, опираясь на полученные сведения, опишем метод в управляемом коде на C#:
Атрибут DllImportAttribute позволяет нам указать параметры обращения к низкоуровневой точке входа. В WinAPI большинство функций написаны с учётом двух основных кодировок — Unicode и ANSI, функции имеют окончания W и A соответственно. Если нам нужно обратиться к конкретному методу в кодировке, но при этом мы не хотим рефакторить основное имя описываемого метода, мы можем указать имя точки входа явно, передав его в соответствующий аргумент атрибута (например, GetPrinterDriverDirectoryW). Так же при этом не забываем указать аргумент CharSet = CharSet.Unicode (в нашем случае, кодировка определяется автоматически). По всем другим полезным атрибутам можно найти информацию в официальной документации.
Атрибут InAttribute в большинстве случаев можно опустить, т.к. аргументы в методах C# по умолчанию передаются по значению. Атрибут OutAttribute мы указываем в тех случаях, когда тип передаваемого аргумента — ссылочный, но данные должны быть выходными. Для выходных данных аргументов значимых типов мы указываем ref, т.е. передаём аргумент по ссылке.
Пример 2. Вызов метода AddMonitor из драйвера winspool.drv.
В данном примере используется структура данных MONITOR_INFO_2, которую нам нужно будет предварительно описать в коде C#.
Здесь важно понимать, что структуры, говоря «народным» языком, — это набор выделенных участков памяти определенного размера, хранящих значения. Каждый из таких участков представляет собой поле данных определённого типа. Ни о каких именах полей и их атрибутах речи не идёт, никаких метаданных, присущих изменяемым типам (классам), только размеры выделяемой памяти для хранения данных. Это означает, что имена членов структуры, как и само имя структуры, могут быть любыми, важно соблюсти соответствие выделяемых размеров памяти каждому члену. Для таких целей существует атрибут StructLayoutAttribute, позволяющий управлять физическим размещением полей данных класса или структуры в памяти. Есть множество способов управления размещением данных полей в памяти, можно явно задавать смещение полей, указывать абсолютный размер структуры, задавать кодировку для способа маршалирования, упаковку, размещать сегменты полей в порядке очерёдности друг за другом и т.д. Примеры реализаций этих способов можно найти здесь. Конкретно для нашей задачи отлично подойдёт последний способ, который мы указываем через LayoutKind.Sequential.
Как это работает: мы объявили структуру, указали в атрибуте размещение полей в памяти с помощью LayoutKind.Sequential, указали типы данных полей, для WinAPI типы данных в структурах — значимые, а значит их размер нам известен, в неуправляемом коде это sizeof(), в управляемом — Marshal.SizeOf(). Всё.
Понятие «маршалирование» (оно же «маршалинг», оно же «Marshalling») описывает процесс преобразования данных, хранимых в памяти, из одного представления в другое. В случае с .NET в целом и P/Invoke в частности — это преобразование типов от неуправляемого кода к CLR. Маршалирование позволяет облегчить процесс работы с памятью в управляемом коде. Для этих целей предусмотрены два главных класса — Marshal и MarshalAsAttribute. Атрибут MarshalAsAttribute позволяет явно задать соответствие типов для преобразования из неуправляемого кода в управляемый (подобно мапированию при сериализации типов). Он может применяться только к полям типа, к параметрам метода с указанием уточнения через param:, а так же к возвращаемому значению через return:. Класс Marshal содержит множество полезных статических методов для работы с указателями, выделения памяти, определения размеров, смещения и прочего. Так же нам пригодится атрибут FlagsAttribute, позволяющий настроить преобразование низкоуровневых битовых флагов в enum'ы C#.
С теорией разобрались, пора продумать архитектуру нашего будущего API. Здесь нет какой-либо конкретной или оптимальной философии, каждый сам выбирает нужные паттерны проектирования кода в зависимости от условий решаемой задачи. Для нашего случая я решил поступить следующим образом: весь код будущей библиотеки будет состоять из двух основных модулей — «фабрики» классов и интерфейсов, реализующих эти классы. Публичная реализация будет давать возможность получить список всех установленных компонентов в системе, установить/удалить компонент и прочее. Внутренняя реализация будет работать с маршалированием и P/Invoke. Для специфических случаев мы сможем создавать экземпляры наших классов и вызывать их методы, для базовых случаев будем обращаться к нашей «фабрике». Визуально это всё можно представить примерно следующим образом (рис. 2):

Для решения задачи в рамках статьи нам понадобятся реализации IMonitor, IPort, IDriver и IPrinter, собственно сам класс фабрики PrintingApi, а так же вспомагательные флаги. Остальное пока что опустим.
Первым делом, давайте напишем базовый интерфейс для всех наших компонентов печати:
Здесь всё просто, у каждого компонента будет имя и два метода для установки/удаления в системе, с возможностью работы с удалённой машиной.
Теперь напишем основу для нашей фабрики:
Думаю, из документированных комментариев понятно что здесь к чему. Синглтоном создаём новый статический экземпляр класса в статическом конструкторе, описываем два делегата EnumInfo и EnumInfo2 для вызова нативных методов получения данных в наших будущих классах, описываем методы-хэлперы над нативными методами.
В большинстве случаев, весь процесс работы с нативными методами будет сводиться к следующей последовательности действий:
Для работы с буферами char** (массивы строк) я рекомендую использовать StringBuilder. У него есть уже готовые перегрузки, работающие с указателями, а так же реализована вся необходимая функциональность по маршалированию.
Для перехвата и генерации исключений в нашем API предусмотрим отдельный класс:
Здесь мы сразу прописали для удобства основные коды нативных ошибок при работе со службой печати.
Теперь нам понадобится реализовать пару перечислимых типов для более удобной работы с кодом и минимизации передачи неверных аргументов в нативные методы:
Для преобразования Environment в строку и наоборот, реализуем два метода расширения:
Вместо двух методов расширения и enum Environment можно обойтись обычными строковыми константами, я пошёл таким путём просто потому, что хотел изначально запретить передавать в имени окружения невалидную отсебятину, но сохранить при этом возможность выбирать из заранее заданного ограниченного набора строк.
Дабы не писать в каждом классе реализации перегрузок методов установки и удаления компонента, реализуем базовый абстрактный класс:
Пора нам приступить к реализации монитора печати. Сперва нам понадобится нативная структура, которую мы уже описывали в теоретической части:
Теперь нам нужен интерфейс для реализации мониторов печати:
Пока что этого достаточно. В будущем, если понадобится расширять функционал не нарушая полиморфизм, благодаря нашим интерфейсам мы без проблем сможем реализовать эту задачу.
Нативный метод добавления монитора в систему мы так же уже описали в теоретическом блоке статьи, теперь опишем методы получения списка установленных мониторов, удаления мониторов и реализуем класс нашего монитора:
Теперь у нас весь необходимый функционал для работы с мониторами печати находится в одном классе. Добавим так же в нашу фабрику метод создания экземпляра монитора:
Убедимся в работоспособности нашего кода. Добавляем Unit-тест, прописываем константы имени и путей к dll монитора для удобства, реализуем тестовые методы, покрывающие основные сегменты кода:
Не забываем закинуть в тестовый каталог mfilemon.dll монитора печати.
С портом проделываем аналогичные действия. Сперва нам понадобится IPort:
Теперь описываем нативную структуру данных порта:
Spooler предусматривает два способа открытия и закрытия порта: первый — использовать базовые методы AddPort/DeletePort, второй — использовать средства XcvData и различные хэлперы над ним. Второй вариант для нас предпочтительнее, т.к. в первом случае понадобится указатель на диалоговое окно процесса установки, что нам отнюдь не нужно. Для XCV нам дополнительно понадобятся:
Отлично, теперь у нас есть всё что нужно для реализации класса порта:
Теперь расширим наш PrintingApi за счёт внесения функционала для работы с портами:
Также не забываем добавить проверку в метод удаления монитора вместо TODO:
Теперь убедимся в работоспособности нашего кода и можно приступать к следующему этапу:
С драйверами всё точно так же, как и в случае с мониторами. Сперва описываем нативную структуру данных, затем интерфейс, реализуем интерфейс, расширяем функционал API и тестируем, не забыв предварительно закинуть в тестовый каталог файлы драйверов:
А сейчас пришло время реализовать, пожалуй, самый главный компонент, обеспечивающий взаимосвязь между UI и монитором печати — принтер. Здесь тоже мало отличий от предыдущих манипуляций:
Для получения списка установленных принтеров нам так же понадобится флаг:
Для того, чтобы изменения в системе вступили в силу, после установки принтера нам понадобится перезапустить службу печати вручную. Напишем статический метод в классе PrintingApi, который будет запускать/перезапускать Spooler. Это так же актуально для случаев, когда служба печати на компьютере была изначально остановлена:
Необходимо будет подключить в проект ссылку на System.ServiceProcess.dll. Здесь всё просто: запускаем контроллер службы, проверяем статус, если запущена — останавливаем, ждём пока остановится, затем запускаем и ждём пока статус поменяется на «запущено», в случае ошибки (например, если служба в данный момент занята) пытаемся повторить процедуру ещё четыре раза.
На этом базовый функционал для работы с устройствами печати можно считать готовым. Итак, что умеет наш API на данный момент:
Делаем последний общий тест для класса PrintingApi и переходим к заключительной части статьи:


Обратите внимание, практически все нативные методы Spooler блокируют поток, в котором они вызываются, не забывайте проводить операции с нашим API в асинхронном режиме, дабы избежать подвисания главного STA-потока UI.
После установки виртуального принтера в систему, необходимо сконфигурировать монитор. Здесь всё зависит от спецификации монитора, для этого нужно изучать его документацию. Конкретно в нашем случае, mfilemon.dll конфигурируется с помощью реестра:
Теперь в каталоге «D:/Printing Tests/» будут появляться уже сформированные PostScript-файлы описания страниц, мы можем делать с ними что угодно, хоть преобразовать в текстовый формат или PDF, распарсив по старинке (упаси Боже) или воспользовавшись средствами GhostScript, хоть переслать на сервер, хоть загрузить в память. Осталось только перехватить момент создания файла после печати, для этого в System.IO предусмотрен класс FileSystemWatcher, который следит за изменением состояния файловой системы и может вызывать наши обработчики событий:
Обработчик события выглядит примерно следующим образом:
Собственно, на этом нашу задачу можно считать полностью решённой.
Как и всегда, этой статьёй я ни на что не претендую. Сегодня я попытался объяснить «на пальцах» решение довольно специфической (но, как показал опыт последних пары лет — всё ещё актуальной) задачи средствами языка C#, настолько доходчиво и подробно, насколько это было для меня возможно. Надеюсь, статья была для Вас полезной и Вы не потратили время попусту.
Изложенного выше материала должно с лихвой хватить для написания высокоуровневых хэлперов над неуправляемым кодом практически любой сложности, а так же базового понимания работы службы печати Spooler.
Исходный код с приложенными Unit-тестами к проекту можно найти здесь. NuGet-пакет для использования в своих проектах доступен здесь.
Благодарю за внимание!
Как и в прошлый раз, статья будет полезна для ознакомления разработчикам младшего и среднего звена. В процессе изучения материала, Вы узнаете как можно обращаться к низкоуровневым DLL WinAPI в C# с помощью P/Invoke, как установить, настроить и удалить из системы мониторы печати, драйвера принтера, само устройство печати, открыть и связать порт для перенаправления входных данных с устройства печати на монитор, познакомитесь с ключевыми моментами применения маршалирования. Так же мы на практическом примере разберёмся, как с помощью нашего API можно удобно манипулировать устройствами печати в системе, узнаем как можно перехватить обработанные данные после печати с принтера и, например, отправить их на сервер.
Постановка задачи
Сперва я предлагаю обрисовать примерный список задач, которые нам необходимо будет решать в данной статье. Пускай он будет выглядеть следующим образом:
- Возможность управлять полным циклом установки устройства печати в системе;
- Возможность управлять полным циклом удаления устройства печати из системы;
- Возможность конфигурировать устройство печати;
- По возможности, оптимизировать повторное использование кода в проекте, а так же сделать структуру API максимально функциональной и удобной;
- Максимально возможно повысить отказоустойчивость API, но при этом сохранить доступ к низкоуровневым исключениям Win32;
- Обеспечить совместимость с системами, начиная с Windows XP и заканчивая Windows 10;
- Возможность обработки данных печати в коде, использующем наш API.
В рамках статьи мы будем использовать уже реализованый итальянским разработчиком Lorenzo Monti монитор печати mfilemon.dll, а в качестве драйвера для принтера используем официальный драйвер Microsoft PostScript Printer Driver. По сути, монитор и драйвер могут быть любыми, в зависимости от требований вашей задачи.
Немного теории
Я не буду рассматривать в данной статье теоретическую базу по печати документов, различиям между принтерами с поддержкой PCL/PostScript и принтерами GDI и прочую базу. Всю необходимую информацию по данной тематике можно в избытке найти на просторах сети. Но кое-что знать всё же необходимо, чтобы лучше понимать как реализовать поставленные перед нами задачи.
Начнём с самого главного — процессом печати в Windows рулит служба Spooler. В директории C:/Windows/System32/ имеется её собственный низкоуровневый драйвер winspool.drv, в котором заложено множество точек входа для обращения к службе печати и выполнения целого ряда действий, от получения системной директории драйверов печати и имени установленного по умолчанию принтера в системе до манипуляций с очередью задач на печать. Для тех, кто хочет написать собственный монитор (о чём, возможно, я когда-нибудь так же расскажу в одной из будущих статей), помимо winspool.h ещё понадобится winsplp.h из DDK, представляющий дополнительный функционал для сборки драйвера в соответствии спецификации Spooler.
Далее, полнофункциональное устройство печати в Windows, говоря простым языком, состоит из монитора печати, открытого на мониторе порта, собственно устройства печати (принтера) и предварительно установленного драйвера к нему (рис. 1). На одном мониторе может быть открыто сразу несколько портов, к одному порту может быть привязано сразу несколько принтеров. Это важно учитывать при удалении того или иного компонента из системы, если, например, Вы захотите удалить определённый порт, предварительно нужно будет снести и все устройства печати, которые к нему привязаны.

P/Invoke
Platform Invocation Services (PInvoke) — платформа д��я обращения к точкам входа DLL (функциям), написанным в неуправляемом коде (C/C++ и WinAPI), из кода управляемого (C#/VB и .NET). Для того, чтобы обратиться к низкоуровневому коду из .NET, нам нужно описать External-методы из DLL, а так же структуры, которые в них используются. Рассмотрим каждый из сучаев по порядку на примерах.
Пример 1. Вызов метода GetPrinterDriverDirectory из драйвера winspool.drv.
Первым делом, нам нужно знать, что возвращает метод и что принимает в аргументах при вызове. Для этого лезем в документацию и читаем описание сигнатуры метода. Обратите внимание, в дальнейшем к документации низкоуровневого API нам придётся обращаться сплошь и рядом, далее по ходу статьи я не буду больше указывать о необходимости этого действия при реализации тех или иных методов/структур, т.к. они требуются по умолчанию.
BOOL GetPrinterDriverDirectory( _In_ LPTSTR pName, _In_ LPTSTR pEnvironment, _In_ DWORD Level, _Out_ LPBYTE pDriverDirectory, _In_ DWORD cbBuf, _Out_ LPDWORD pcbNeeded );
Описание каждого параметра функции так же можно найти в документации. Здесь важно понимать, что параметры могут быть только входными (In), только выходными (Out), как входными, так и выходными одновременно (In/Out), а так же опциональными (либо входными, либо выходными, в зависимости от других параметров). Так же нам нужно знать, какой тип данных в .NET нужно сопоставить WDT-типу (здесь, в большинстве случаев, работает правило «размеры выделяемой памяти значимым типам .NET соответствуют размерам выделяемой памяти базовым типам C++, для остальных есть IntPtr»).
Теперь, опираясь на полученные сведения, опишем метод в управляемом коде на C#:
[DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] extern bool GetPrinterDriverDirectory(string serverName, string environment, uint level, [Out] StringBuilder driverDirectory, uint bufferSize, ref uint bytesNeeded);
Атрибут DllImportAttribute позволяет нам указать параметры обращения к низкоуровневой точке входа. В WinAPI большинство функций написаны с учётом двух основных кодировок — Unicode и ANSI, функции имеют окончания W и A соответственно. Если нам нужно обратиться к конкретному методу в кодировке, но при этом мы не хотим рефакторить основное имя описываемого метода, мы можем указать имя точки входа явно, передав его в соответствующий аргумент атрибута (например, GetPrinterDriverDirectoryW). Так же при этом не забываем указать аргумент CharSet = CharSet.Unicode (в нашем случае, кодировка определяется автоматически). По всем другим полезным атрибутам можно найти информацию в официальной документации.
Атрибут InAttribute в большинстве случаев можно опустить, т.к. аргументы в методах C# по умолчанию передаются по значению. Атрибут OutAttribute мы указываем в тех случаях, когда тип передаваемого аргумента — ссылочный, но данные должны быть выходными. Для выходных данных аргументов значимых типов мы указываем ref, т.е. передаём аргумент по ссылке.
Пример 2. Вызов метода AddMonitor из драйвера winspool.drv.
В данном примере используется структура данных MONITOR_INFO_2, которую нам нужно будет предварительно описать в коде C#.
typedef struct _MONITOR_INFO_2 { LPTSTR pName; LPTSTR pEnvironment; LPTSTR pDLLName; } MONITOR_INFO_2, *PMONITOR_INFO_2; BOOL AddMonitor( _In_ LPTSTR pName, _In_ DWORD Level, _In_ LPBYTE pMonitors );
Здесь важно понимать, что структуры, говоря «народным» языком, — это набор выделенных участков памяти определенного размера, хранящих значения. Каждый из таких участков представляет собой поле данных определённого типа. Ни о каких именах полей и их атрибутах речи не идёт, никаких метаданных, присущих изменяемым типам (классам), только размеры выделяемой памяти для хранения данных. Это означает, что имена членов структуры, как и само имя структуры, могут быть любыми, важно соблюсти соответствие выделяемых размеров памяти каждому члену. Для таких целей существует атрибут StructLayoutAttribute, позволяющий управлять физическим размещением полей данных класса или структуры в памяти. Есть множество способов управления размещением данных полей в памяти, можно явно задавать смещение полей, указывать абсолютный размер структуры, задавать кодировку для способа маршалирования, упаковку, размещать сегменты полей в порядке очерёдности друг за другом и т.д. Примеры реализаций этих способов можно найти здесь. Конкретно для нашей задачи отлично подойдёт последний способ, который мы указываем через LayoutKind.Sequential.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] struct MonitorInfo { [MarshalAs(UnmanagedType.LPTStr)] public string Name; [MarshalAs(UnmanagedType.LPTStr)] public string Environment; [MarshalAs(UnmanagedType.LPTStr)] public string DllName; } [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] static extern bool AddMonitor(string serverName, uint level, ref MonitorInfo monitor);
Как это работает: мы объявили структуру, указали в атрибуте размещение полей в памяти с помощью LayoutKind.Sequential, указали типы данных полей, для WinAPI типы данных в структурах — значимые, а значит их размер нам известен, в неуправляемом коде это sizeof(), в управляемом — Marshal.SizeOf(). Всё.
Маршалирование
Понятие «маршалирование» (оно же «маршалинг», оно же «Marshalling») описывает процесс преобразования данных, хранимых в памяти, из одного представления в другое. В случае с .NET в целом и P/Invoke в частности — это преобразование типов от неуправляемого кода к CLR. Маршалирование позволяет облегчить процесс работы с памятью в управляемом коде. Для этих целей предусмотрены два главных класса — Marshal и MarshalAsAttribute. Атрибут MarshalAsAttribute позволяет явно задать соответствие типов для преобразования из неуправляемого кода в управляемый (подобно мапированию при сериализации типов). Он может применяться только к полям типа, к параметрам метода с указанием уточнения через param:, а так же к возвращаемому значению через return:. Класс Marshal содержит множество полезных статических методов для работы с указателями, выделения памяти, определения размеров, смещения и прочего. Так же нам пригодится атрибут FlagsAttribute, позволяющий настроить преобразование низкоуровневых битовых флагов в enum'ы C#.
Архитектура будущего API
С теорией разобрались, пора продумать архитектуру нашего будущего API. Здесь нет какой-либо конкретной или оптимальной философии, каждый сам выбирает нужные паттерны проектирования кода в зависимости от условий решаемой задачи. Для нашего случая я решил поступить следующим образом: весь код будущей библиотеки будет состоять из двух основных модулей — «фабрики» классов и интерфейсов, реализующих эти классы. Публичная реализация будет давать возможность получить список всех установленных компонентов в системе, установить/удалить компонент и прочее. Внутренняя реализация будет работать с маршалированием и P/Invoke. Для специфических случаев мы сможем создавать экземпляры наших классов и вызывать их методы, для базовых случаев будем обращаться к нашей «фабрике». Визуально это всё можно представить примерно следующим образом (рис. 2):

Для решения задачи в рамках статьи нам понадобятся реализации IMonitor, IPort, IDriver и IPrinter, собственно сам класс фабрики PrintingApi, а так же вспомагательные флаги. Остальное пока что опустим.
Кодовая база
Первым делом, давайте напишем базовый интерфейс для всех наших компонентов печати:
/// <summary> /// Представляет базовый интерфейс для реализации устройств, связанных с печатью (мониторы печати, порты принтеров, принтеры). /// </summary> public interface IPrintableDevice { /// <summary> /// Наименование устройства печати. /// </summary> string Name { get; } /// <summary> /// Устанавливает устройство печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> void Install(string serverName); /// <summary> /// Удаляет устройство печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> void Uninstall(string serverName); }
Здесь всё просто, у каждого компонента будет имя и два метода для установки/удаления в системе, с возможностью работы с удалённой машиной.
Теперь напишем основу для нашей фабрики:
class PrintingApi
/// <summary> /// Представляет API для работы со службой печати. /// </summary> public class PrintingApi { /// <summary> /// Вспомагательный делегат для оптимизации кода в P/Invoke. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <param name="level">Уровень структуры данных.</param> /// <param name="structs">Указатель на структуру данных.</param> /// <param name="bufferSize">Размер буфера.</param> /// <param name="bytesNeeded">Число требуемых байт для выделения памяти.</param> /// <param name="bufferReturnedLength">Число выделенных байт памяти.</param> /// <returns></returns> internal delegate bool EnumInfo(string serverName, uint level, IntPtr structs, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength); /// <summary> /// Вспомагательный делегат для оптимизации кода в P/Invoke. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <param name="environment">Окружение.</param> /// <param name="level">Уровень структуры данных.</param> /// <param name="structs">Указатель на структуру данных.</param> /// <param name="bufferSize">Размер буфера.</param> /// <param name="bytesNeeded">Число требуемых байт для выделения памяти.</param> /// <param name="bufferReturnedLength">Число выделенных байт памяти.</param> /// <returns></returns> internal delegate bool EnumInfo2(string serverName, string environment, uint level, IntPtr structs, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength); /// <summary> /// Фабрика классов для <see cref="PrintingApi"/>. /// </summary> public static PrintingApi Factory { get; protected set; } /// <summary> /// Статический инициализатор класса <see cref="PrintingApi"/>. /// </summary> static PrintingApi() { Factory = new PrintingApi(); } /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="level">Уровень структуры.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo handler, string serverName, uint level) where T : struct { uint bytesNeeded = 0; uint bufferReturnedLength = 0; if (handler(serverName, level, IntPtr.Zero, 0, ref bytesNeeded, ref bufferReturnedLength)) return null; int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != PrintingException.ErrorInsufficientBuffer) throw new PrintingException(lastWin32Error); IntPtr pointer = Marshal.AllocHGlobal((int)bytesNeeded); try { if (handler(serverName, level, pointer, bytesNeeded, ref bytesNeeded, ref bufferReturnedLength)) { IntPtr currentPointer = pointer; T[] dataCollection = new T[bufferReturnedLength]; Type type = typeof(T); for (int i = 0; i < bufferReturnedLength; i++) { dataCollection[i] = (T)Marshal.PtrToStructure(currentPointer, type); currentPointer = (IntPtr)(currentPointer.ToInt64() + Marshal.SizeOf(type)); } return dataCollection; } throw new PrintingException(Marshal.GetLastWin32Error()); } catch (Exception e) { throw new PrintingException(e.Message, e); } finally { Marshal.FreeHGlobal(pointer); } } /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo handler, string serverName) where T : struct => GetInfo<T>(handler, serverName, 2); /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="level">Уровень структуры.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo handler, uint level) where T : struct => GetInfo<T>(handler, null, level); /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo handler) where T : struct => GetInfo<T>(handler, null); /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="level">Уровень структуры.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo2 handler, string serverName, string arg, uint level) where T : struct { uint bytesNeeded = 0; uint bufferReturnedLength = 0; if (handler(serverName, arg, level, IntPtr.Zero, 0, ref bytesNeeded, ref bufferReturnedLength)) return null; int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != PrintingException.ErrorInsufficientBuffer) throw new PrintingException(lastWin32Error); IntPtr pointer = Marshal.AllocHGlobal((int)bytesNeeded); try { if (handler(serverName, arg, level, pointer, bytesNeeded, ref bytesNeeded, ref bufferReturnedLength)) { IntPtr currentPointer = pointer; T[] dataCollection = new T[bufferReturnedLength]; Type type = typeof(T); for (int i = 0; i < bufferReturnedLength; i++) { dataCollection[i] = (T)Marshal.PtrToStructure(currentPointer, type); currentPointer = (IntPtr)(currentPointer.ToInt64() + Marshal.SizeOf(type)); } return dataCollection; } throw new PrintingException(Marshal.GetLastWin32Error()); } catch (Exception e) { throw new PrintingException(e.Message, e); } finally { Marshal.FreeHGlobal(pointer); } } /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo2 handler, string serverName, string arg) where T : struct => GetInfo<T>(handler, serverName, arg, 2); /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="level">Уровень структуры.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo2 handler, string arg, uint level) where T : struct => GetInfo<T>(handler, null, arg, level); /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo2 handler, string arg) where T : struct => GetInfo<T>(handler, null, arg); /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="level">Уровень структуры.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo2 handler, uint level) where T : struct => GetInfo<T>(handler, null, level); /// <summary> /// Получает коллекцию нативных структур Spooler API. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <returns>Коллекция нативных структур Spooler API.</returns> internal static T[] GetInfo<T>(EnumInfo2 handler) where T : struct => GetInfo<T>(handler, null); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo handler, string serverName, uint level, out T[] dataCollection, out PrintingException e) where T : struct { dataCollection = null; e = null; try { dataCollection = GetInfo<T>(handler, serverName, level); return true; } catch (PrintingException ex) { e = ex; } return false; } /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo handler, string serverName, uint level, out T[] dataCollection) where T : struct => TryGetInfo(handler, serverName, level, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo handler, string serverName, out T[] dataCollection, out PrintingException e) where T : struct => TryGetInfo(handler, serverName, 2, out dataCollection, out e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo handler, string serverName, out T[] dataCollection) where T : struct => TryGetInfo(handler, serverName, 2, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo handler, uint level, out T[] dataCollection, out PrintingException e) where T : struct => TryGetInfo(handler, null, level, out dataCollection, out e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo handler, uint level, out T[] dataCollection) where T : struct => TryGetInfo(handler, null, level, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo handler, out T[] dataCollection, out PrintingException e) where T : struct => TryGetInfo(handler, null, out dataCollection, out e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo handler, out T[] dataCollection) where T : struct => TryGetInfo(handler, null, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, string serverName, string arg, uint level, out T[] dataCollection, out PrintingException e) where T : struct { dataCollection = null; e = null; try { dataCollection = GetInfo<T>(handler, serverName, arg, level); return true; } catch (PrintingException ex) { e = ex; } return false; } /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, string serverName, string arg, uint level, out T[] dataCollection) where T : struct => TryGetInfo(handler, serverName, arg, level, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, string serverName, string arg, out T[] dataCollection, out PrintingException e) where T : struct => TryGetInfo(handler, serverName, arg, 2, out dataCollection, out e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, string serverName, string arg, out T[] dataCollection) where T : struct => TryGetInfo(handler, serverName, arg, 2, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, string arg, uint level, out T[] dataCollection, out PrintingException e) where T : struct => TryGetInfo(handler, null, arg, level, out dataCollection, out e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, string arg, uint level, out T[] dataCollection) where T : struct => TryGetInfo(handler, null, arg, level, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, string arg, out T[] dataCollection, out PrintingException e) where T : struct => TryGetInfo(handler, null, arg, out dataCollection, out e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="arg">Дополнительный аргумент делегата.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, string arg, out T[] dataCollection) where T : struct => TryGetInfo(handler, null, arg, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, uint level, out T[] dataCollection, out PrintingException e) where T : struct => TryGetInfo(handler, null, level, out dataCollection, out e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="level">Уровень структуры.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, uint level, out T[] dataCollection) where T : struct => TryGetInfo(handler, null, level, out dataCollection, out PrintingException e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <param name="e">Исключение, возникшее во время операции.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, out T[] dataCollection, out PrintingException e) where T : struct => TryGetInfo(handler, null, out dataCollection, out e); /// <summary> /// Получает коллекцию нативных структур Spooler API и в случае успеха возвращает True. Вспомагательный метод для оптимизации кода в P/Invoke. /// </summary> /// <typeparam name="T">Тип структуры.</typeparam> /// <param name="handler">Делегат-обработчик нативного метода из Spooler API.</param> /// <param name="dataCollection">Коллекция нативных структур Spooler API.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> internal static bool TryGetInfo<T>(EnumInfo2 handler, out T[] dataCollection) where T : struct => TryGetInfo(handler, null, out dataCollection, out PrintingException e); }
Думаю, из документированных комментариев понятно что здесь к чему. Синглтоном создаём новый статический экземпляр класса в статическом конструкторе, описываем два делегата EnumInfo и EnumInfo2 для вызова нативных методов получения данных в наших будущих классах, описываем методы-хэлперы над нативными методами.
В большинстве случаев, весь процесс работы с нативными методами будет сводиться к следующей последовательности действий:
- Вызвать метод в первый раз, чтобы он вернул false и код нативной ошибки 122 («неинициализированный буфер»). В аргументах при этом мы указываем дефолтные значения аргументов (нулевые) для обработки буфера. Это нужно для того, чтобы метод вернул нам изменённые значения инициализации буфера и передал их в наши переменные по ссылке, после того как метод отработает, у нас будет необходимая информация для вызова метода с уже корректными значениями аргументов для инициализации буфера;
- Дальше нам нужно получить указатель на наш буфер. Делается это с помощью метода Marshal.AllocHGlobal(), который выделит память в соответствии указанному нами размеру байт (которые мы уже получили ранее по ссылке при первом вызове нативного метода) и вернёт нам экземпляр IntPtr;
- Теперь можно обрабатывать наш буфер. Либо преобразуем указатель на буфер в структуру с помощью Marshal.PtrToStructure(), либо делаем обратное действие с помощью Marshal.StructureToPtr и передаём указатель на структуру в метод, в зависимости от задачи и сигнатуры метода;
- Не забываем за перехват возможных ошибок Win32 с помощью Marshal.GetLastWin32Error(), а так же освободить память, выделенную ранее для нашего буфера, с помощью Marshal.FreeHGlobal().
Для работы с буферами char** (массивы строк) я рекомендую использовать StringBuilder. У него есть уже готовые перегрузки, работающие с указателями, а так же реализована вся необходимая функциональность по маршалированию.
Для перехвата и генерации исключений в нашем API предусмотрим отдельный класс:
class PrintingException
/// <summary> /// Представляет ошибку менеджера печати. /// </summary> [Serializable] public class PrintingException : Win32Exception { #region Error Codes /// <summary> /// Код ошибки "Файл не найден". /// </summary> public const int ErrorFileNotFound = 2; /// <summary> /// Код ошибки "Неинициализированный буфер". /// </summary> public const int ErrorInsufficientBuffer = 122; /// <summary> /// Код ошибки "Модуль не найден". /// </summary> public const int ErrorModuleNotFound = 126; /// <summary> /// Код ошибки "Имя принтера задано неверно". /// </summary> public const int ErrorInvalidPrinterName = 1801; /// <summary> /// Код ошибки "Указан неизвестный монитор печати". /// </summary> public const int ErrorMonitorUnknown = 3000; /// <summary> /// Код ошибки "Указанный драйвер принтера занят". /// </summary> public const int ErrorPrinterDriverIsReadyUsed = 3001; /// <summary> /// Код ошибки "Не найден файл диспетчера очереди". /// </summary> public const int ErrorPrinterJobFileNotFound = 3002; /// <summary> /// Код ошибки "Не был произведен вызов StartDocPrinter". /// </summary> public const int ErrorStartDocPrinterNotCalling = 3003; /// <summary> /// Код ошибки "Не был произведен вызов AddJob". /// </summary> public const int ErrorAddJobNotCalling = 3004; /// <summary> /// Код ошибки "Указанный процессор печати уже установлен". /// </summary> public const int ErrorPrinterProcessorAlreadyInstalled = 3005; /// <summary> /// Код ошибки "Указанный монитор печати уже установлен". /// </summary> public const int ErrorMonitorAlreadyInstalled = 3006; /// <summary> /// Код ошибки "Указанный монитор печати не имеет требуемых функций". /// </summary> public const int ErrorInvalidMonitor = 3007; /// <summary> /// Код ошибки "Указанный монитор печати сейчас уже используется". /// </summary> public const int ErrorMonitorIsReadyUsed = 3008; #endregion /// <summary> /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>. /// </summary> public PrintingException() : base() { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>. /// </summary> /// <param name="nativeErrorCode">Код ошибки Win32.</param> public PrintingException(int nativeErrorCode) : base(nativeErrorCode) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>. /// </summary> /// <param name="message">Сообщение об ошибке.</param> public PrintingException(string message) : base(message) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>. /// </summary> /// <param name="nativeErrorCode">Код ошибки Win32.</param> /// <param name="message">Сообщение об ошибке.</param> public PrintingException(int nativeErrorCode, string message) : base(nativeErrorCode, message) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>. /// </summary> /// <param name="message">Сообщение об ошибке.</param> /// <param name="innerException"></param> public PrintingException(string message, Exception innerException) : base(message, innerException) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="PrintingException"/>. /// </summary> /// <param name="info">Данные для сериализации.</param> /// <param name="context">Контекст потока сериализации.</param> public PrintingException(SerializationInfo info, StreamingContext context) : base(info, context) { } }
Здесь мы сразу прописали для удобства основные коды нативных ошибок при работе со службой печати.
Теперь нам понадобится реализовать пару перечислимых типов для более удобной работы с кодом и минимизации передачи неверных аргументов в нативные методы:
/// <summary> /// Окружение системы. /// </summary> public enum Environment { /// <summary> /// Текущее окружение системы. /// </summary> Current, /// <summary> /// Windows NT x86. /// </summary> X86, /// <summary> /// Windows x64. /// </summary> X64, /// <summary> /// Windows IA64. /// </summary> IA64, } /// <summary> /// Тип порта для монитора печати. /// </summary> [Flags] public enum PortType { /// <summary> /// Запись данных. /// </summary> Write = 0x1, /// <summary> /// Чтение данных. /// </summary> Read = 0x2, /// <summary> /// Перенаправление данных. /// </summary> Redirected = 0x4, /// <summary> /// Отправка данных на сервер. /// </summary> NetAttached = 0x8, } /// <summary> /// Протокол данных печати. /// </summary> public enum DataType : uint { RAW = 1, LPR = 2, }
Для преобразования Environment в строку и наоборот, реализуем два метода расширения:
/// <summary> /// Представляет статический класс расширений типов. /// </summary> public static class PrintingExtensions { /// <summary> /// Возвращает имя окружение системы, совместимое с WinAPI. /// </summary> /// <param name="environment">Окружение системы.</param> /// <returns>Строковое представление имени окружения системы.</returns> internal static string GetEnvironmentName(this Environment environment) { switch (environment) { default: return null; case Environment.X86: return "Windows x86"; case Environment.X64: return "Windows x64"; case Environment.IA64: return "Windows IA64"; } } /// <summary> /// Возвращает <see cref="Environment"/>, эквивалентный входной строке имени окружения. /// </summary> /// <param name="environmentString">Входная строка имени окружения.</param> /// <returns><see cref="Environment"/>, эквивалентный входной строке имени окружения.</returns> internal static Environment GetEnvironment(this string environmentString) { environmentString = environmentString.ToLower(); if (environmentString.Contains("x86")) return Environment.X86; if (environmentString.Contains("x64")) return Environment.X64; if (environmentString.Contains("ia64")) return Environment.IA64; return Environment.Current; } }
Вместо двух методов расширения и enum Environment можно обойтись обычными строковыми константами, я пошёл таким путём просто потому, что хотел изначально запретить передавать в имени окружения невалидную отсебятину, но сохранить при этом возможность выбирать из заранее заданного ограниченного набора строк.
Дабы не писать в каждом классе реализации перегрузок методов установки и удаления компонента, реализуем базовый абстрактный класс:
abstract class PrintableDevice
/// <summary> /// Представляет базовый класс для всех компонентов устройства печати. /// </summary> public abstract class PrintableDevice : IPrintableDevice { /// <summary> /// Наименование компонента устройства печати. /// </summary> public virtual string Name { get; protected set; } /// <summary> /// Инициализирует новый экземпляр класса <see cref="PrintableDevice"/>. /// </summary> /// <param name="name"></param> /// <exception cref="ArgumentNullException"/> public PrintableDevice(string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name"); Name = name; } /// <summary> /// Устанавливает компонента устройства печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <exception cref="FileNotFoundException" /> /// <exception cref="PrintingException" /> public abstract void Install(string serverName); /// <summary> /// Устанавливает компонента устройства печати на локальной машине. /// </summary> /// <exception cref="FileNotFoundException" /> /// <exception cref="PrintingException" /> public void Install() => Install(null); /// <summary> /// Устанавливает компонента устройства печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <param name="e">Исключение, возникшее в процессе установки.</param> /// <returns>True, если процедура установки прошла успешно, иначе False.</returns> public bool TryInstall(string serverName, out PrintingException e) { e = null; try { Install(serverName); } catch (PrintingException ex) { e = ex; return false; } return true; } /// <summary> /// Устанавливает компонента устройства печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <returns>True, если процедура установки прошла успешно, иначе False.</returns> public bool TryInstall(string serverName) => TryInstall(serverName, out PrintingException e); /// <summary> /// Устанавливает компонента устройства печати на локальной машине. /// </summary> /// <param name="e">Исключение, возникшее в процессе установки.</param> /// <returns>True, если процедура установки прошла успешно, иначе False.</returns> public bool TryInstall(out PrintingException e) => TryInstall(null, out e); /// <summary> /// Устанавливает компонента устройства печати на локальной машине. /// </summary> /// <returns>True, если процедура установки прошла успешно, иначе False.</returns> public bool TryInstall() => TryInstall(out PrintingException e); /// <summary> /// Удалает компонента устройства печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <exception cref="PrintingException" /> public abstract void Uninstall(string serverName); /// <summary> /// Удаляет компонента устройства печати на локальной машине. /// </summary> /// <exception cref="PrintingException" /> public void Uninstall() => Uninstall(null); /// <summary> /// Удалает компонента устройства печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <param name="e">Исключение, возникшее в процессе удаления.</param> /// <returns>True, если процедура удаления прошла успешно, иначе False.</returns> public bool TryUninstall(string serverName, out PrintingException e) { e = null; try { Uninstall(serverName); } catch (PrintingException ex) { e = ex; return false; } return true; } /// <summary> /// Удалает компонента устройства печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <returns>True, если процедура удаления прошла успешно, иначе False.</returns> public bool TryUninstall(string serverName) => TryUninstall(serverName, out PrintingException e); /// <summary> /// Удалает компонента устройства печати на локальной машине. /// </summary> /// <param name="e">Исключение, возникшее в процессе удаления.</param> /// <returns>True, если процедура удаления прошла успешно, иначе False.</returns> public bool TryUninstall(out PrintingException e) => TryUninstall(null, out e); /// <summary> /// Удалает компонента устройства печати на локальной машине. /// </summary> /// <returns>True, если процедура удаления прошла успешно, иначе False.</returns> public bool TryUninstall() => TryUninstall(out PrintingException e); }
Монитор печати
Пора нам приступить к реализации монитора печати. Сперва нам понадобится нативная структура, которую мы уже описывали в теоретической части:
struct MonitorInfo
/// <summary> /// Представляет структуру для хранения информации о мониторе принтера. /// </summary> [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct MonitorInfo { /// <summary> /// Наименование монитора. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string Name; /// <summary> /// Окружение, для которого был написан драйвер (например, Windows NT x86, Windows IA64 или Windows x64). /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string Environment; /// <summary> /// Имя файла *.dll монитора. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string DllName; }
Теперь нам нужен интерфейс для реализации мониторов печати:
interface IMonitor
/// <summary> /// Представляет базовый интерфейс для реализации мониторов печати. /// </summary> public interface IMonitor : IPrintableDevice { /// <summary> /// Окружение, для которого был написан драйвер (например, Windows NT x86, Windows IA64 или Windows x64). /// </summary> Environment Environment { get; } /// <summary> /// Имя файла *.dll монитора. /// </summary> string Dll { get; } }
Пока что этого достаточно. В будущем, если понадобится расширять функционал не нарушая полиморфизм, благодаря нашим интерфейсам мы без проблем сможем реализовать эту задачу.
Нативный метод добавления монитора в систему мы так же уже описали в теоретическом блоке статьи, теперь опишем методы получения списка установленных мониторов, удаления мониторов и реализуем класс нашего монитора:
class Monitor
/// <summary> /// Представляет монитор печати для открытия портов. /// </summary> public class Monitor : PrintableDevice, IMonitor { /// <summary> /// Окружение, для которого был написан драйвер (например, Windows NT x86, Windows IA64 или Windows x64). /// </summary> public virtual Environment Environment { get; protected set; } /// <summary> /// Имя файла *.dll монитора. /// </summary> public virtual string Dll { get; protected set; } /// <summary> /// Возвращает список всех установленных в системе мониторов печати. /// </summary> public static Monitor[] All { get { if (!PrintingApi.TryGetInfo(EnumMonitors, out MonitorInfo[] monitorInfo)) return null; Monitor[] monitors = new Monitor[monitorInfo.Length]; for (int i = 0; i < monitorInfo.Length; i++) monitors[i] = new Monitor(monitorInfo[i].Name, monitorInfo[i].DllName, monitorInfo[i].Environment.GetEnvironment()); return monitors; } } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Monitor"/>. /// </summary> /// <param name="name">Наименование монитора печати.</param> /// <param name="dll">Имя файла *.dll монитора.</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows NT x86, Windows IA64 или Windows x64).</param> /// <exception cref="ArgumentNullException" /> public Monitor(string name, string dll, Environment environment) : base(name) { if (string.IsNullOrEmpty(dll)) throw new ArgumentNullException("dll"); Environment = environment; Dll = dll; } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Monitor"/>. /// </summary> /// <param name="name">Наименование монитора печати.</param> /// <param name="dll">Имя файла *.dll монитора.</param> /// <exception cref="ArgumentNullException" /> public Monitor(string name, string dll) : this(name, dll, Environment.Current) { } /// <summary> /// Устанавливает монитор печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <exception cref="FileNotFoundException"/> /// <exception cref="PrintingException"/> public override void Install(string serverName) { try { if (!File.Exists(Dll)) throw new FileNotFoundException("Не удалось найти файл монитора печати", Dll); string dllName = Path.GetFileName(Dll); string dllPath = Path.Combine(System.Environment.SystemDirectory, dllName); File.Copy(Dll, dllPath, true); MonitorInfo monitorInfo = new MonitorInfo { Name = Name, Environment = Environment.GetEnvironmentName(), DllName = File.Exists(dllPath) ? dllName : Dll, }; if (AddMonitor(serverName, 2, ref monitorInfo)) return; if (Marshal.GetLastWin32Error() == PrintingException.ErrorMonitorAlreadyInstalled && TryUninstall(serverName) && AddMonitor(serverName, 2, ref monitorInfo)) return; else throw new PrintingException(Marshal.GetLastWin32Error()); } catch (Exception e) { throw new PrintingException(e.Message, e); } } /// <summary> /// Удалает монитор печати на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <exception cref="PrintingException"/> public override void Uninstall(string serverName) { try { if (!All.Select(m => m.Name).Contains(Name)) return; /// TODO: Добавить удаление октрытых на мониторе портов. if (DeleteMonitor(serverName, Environment.GetEnvironmentName(), Name)) return; if (Marshal.GetLastWin32Error() == PrintingException.ErrorMonitorUnknown) return; if (DeleteMonitor(serverName, Environment.GetEnvironmentName(), Name)) return; throw new PrintingException(Marshal.GetLastWin32Error()); } catch (Exception e) { throw new PrintingException(e.Message, e); } } #region Native /// <summary> /// Производит установку монитора принтера в систему. /// </summary> /// <param name="serverName">Имя сервера, на который необходимо произвести установку. Если равно null - устанавливает на локальную машину.</param> /// <param name="level">Номер версии структуры. Должен быть равен 2.</param> /// <param name="monitor">Экземпляр структуры <see cref="MonitorInfo"/>.</param> /// <returns>True, если операция выполнена успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool AddMonitor(string serverName, uint level, ref MonitorInfo monitor); /// <summary> /// Возвращает указатель на буфер экземпляров структур <see cref="MonitorInfo"/>. /// </summary> /// <param name="serverName">Имя сервера, с которого требуется получить список мониторов. Если равно null - получает на локальной машине.</param> /// <param name="level">Номер версии структуры. Должен быть равен 1 или 2.</param> /// <param name="monitors">Указатель на буфер экземпляров структур <see cref="MonitorInfo"/>.</param> /// <param name="bufferSize">Размер буфера экземпляров структур <see cref="MonitorInfo"/> (в байтах).</param> /// <param name="bytesNeeded">Число полученных байт размера буфера.</param> /// <param name="bufferReturnedLength">Число экземпляров структур.</param> /// <returns>True, если операция выполнена успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool EnumMonitors(string serverName, uint level, IntPtr monitors, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength); /// <summary> /// Производит удаление монитора принтера из системы. /// </summary> /// <param name="serverName">Имя сервера, на который необходимо произвести установку. Если равно null - устанавливает на локальную машину.</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="monitorName">Имя удаляемого монитора.</param> /// <returns>True, если операция выполнена успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool DeleteMonitor(string serverName, string environment, string monitorName); #endregion }
Теперь у нас весь необходимый функционал для работы с мониторами печати находится в одном классе. Добавим так же в нашу фабрику метод создания экземпляра монитора:
Расширяем функционал класса PrintingApi
/// <summary> /// Возвращает коллекцию всех установленных мониторов в системе. /// </summary> public static Monitor[] Monitors => Monitor.All; /// <summary> /// Создаёт новый монитор печати в системе. /// </summary> /// <param name="name">Наименование монитора печати.</param> /// <param name="dll">Путь к файлу dll монитора печати.</param> /// <param name="environment">Окружение, для которого был написан монитор печати.</param> /// <param name="serverName">Наименование сервера, на котором производится установка монитора печати.</param> /// <returns>Экземпляр монитора печати.</returns> public Monitor CreateMonitor(string name, string dll, Environment environment, string serverName) { Monitor monitor = new Monitor(name, dll, environment); monitor.TryInstall(serverName); return monitor; } /// <summary> /// Создаёт новый монитор печати в системе. /// </summary> /// <param name="name">Наименование монитора печати.</param> /// <param name="dll">Путь к файлу dll монитора печати.</param> /// <param name="environment">Окружение, для которого был написан монитор печати.</param> /// <returns>Экземпляр монитора печати.</returns> public Monitor CreateMonitor(string name, string dll, Environment environment) => CreateMonitor(name, dll, environment, null); /// <summary> /// Создаёт новый монитор печати в системе. /// </summary> /// <param name="name">Наименование монитора печати.</param> /// <param name="dll">Путь к файлу dll монитора печати.</param> /// <param name="serverName">Наименование сервера, на котором производится установка монитора печати.</param> /// <returns>Экземпляр монитора печати.</returns> public Monitor CreateMonitor(string name, string dll, string serverName) => CreateMonitor(name, dll, Environment.Current, null); /// <summary> /// Создаёт новый монитор печати в системе. /// </summary> /// <param name="name">Наименование монитора печати.</param> /// <param name="dll">Путь к файлу dll монитора печати.</param> /// <returns>Экземпляр монитора печати.</returns> public Monitor CreateMonitor(string name, string dll) => CreateMonitor(name, dll, null);
Убедимся в работоспособности нашего кода. Добавляем Unit-тест, прописываем константы имени и путей к dll монитора для удобства, реализуем тестовые методы, покрывающие основные сегменты кода:
Unit-тест для монитора печати
/// <summary> /// Представляет тестовый модуль класса <see cref="Monitor"/>. /// </summary> [TestClass] public class MonitorTests { /// <summary> /// Наименование монитора. /// </summary> protected const string MonitorName = "Test Monitor"; /// <summary> /// Путь к dll монитора. /// </summary> protected const string MonitorDll = "D:/Printing Tests/mfilemon.dll"; /// <summary> /// Неправильный путь к dll монитора. /// </summary> protected const string FailedMonitorDll = "noexist.dll"; /// <summary> /// Тест локальной установки монитора. /// </summary> [TestMethod] public void InstallTest() { Monitor monitor = new Monitor(MonitorName, MonitorDll); monitor.Install(); Assert.IsTrue(Monitor.All.Select(m => m.Name).Contains(MonitorName)); } /// <summary> /// Тест локального удаления монитора. /// </summary> [TestMethod] public void UninstallTest() { Monitor monitor = new Monitor(MonitorName, MonitorDll); monitor.Uninstall(); Assert.IsFalse(Monitor.All.Select(m => m.Name).Contains(MonitorName)); } /// <summary> /// Тест локальной установки монитора с перехватом состояния установки. /// </summary> [TestMethod] public void TryInstallTest() { Monitor monitor = new Monitor(MonitorName, MonitorDll); bool f = monitor.TryInstall(); Assert.IsTrue(f); Assert.IsTrue(Monitor.All.Select(m => m.Name).Contains(MonitorName)); } /// <summary> /// Тест локального удаления монитора с перехватом состояния удаления. /// </summary> [TestMethod] public void TryUninstallTest() { Monitor monitor = new Monitor(MonitorName, MonitorDll); bool f = monitor.TryUninstall(); Assert.IsTrue(f); Assert.IsFalse(Monitor.All.Select(m => m.Name).Contains(MonitorName)); } /// <summary> /// Тест неправильной локальной установки монитора. /// </summary> [TestMethod] [ExpectedException(typeof(PrintingException))] public void InstallFailedTest() { Monitor monitor = new Monitor(MonitorName, FailedMonitorDll); monitor.Install(); Assert.IsFalse(Monitor.All.Select(m => m.Name).Contains(MonitorName)); } /// <summary> /// Тест неправильной локальной установки монитора с перехватом состояния установки. /// </summary> [TestMethod] public void TryInstallFailedTest() { Monitor monitor = new Monitor(MonitorName, FailedMonitorDll); bool f = monitor.TryInstall(); Assert.IsFalse(f); Assert.IsFalse(Monitor.All.Select(m => m.Name).Contains(MonitorName)); }
Не забываем закинуть в тестовый каталог mfilemon.dll монитора печати.
Порт
С портом проделываем аналогичные действия. Сперва нам понадобится IPort:
interface IPort
/// <summary> /// Представляет базовый интерфейс для реализации портов печати. /// </summary> public interface IPort : IPrintableDevice { /// <summary> /// Монитор, на котором открыт порт. /// </summary> IMonitor Monitor { get; } /// <summary> /// Описание порта. /// </summary> string Description { get; } /// <summary> /// Тип порта. /// </summary> PortType Type { get; } }
Теперь описываем нативную структуру данных порта:
struct PortInfo
/// <summary> /// представляет информацию о порте монитора принтера. /// </summary> [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct PortInfo { /// <summary> /// Наименование поддерживаемого порта (например, "LPT1:"). /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string PortName; /// <summary> /// Наименование установленного монитора принтера (например, "PJL monitor"). Может быть равно null. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string MonitorName; /// <summary> /// Описание порта (например, если <see cref="PortName"/> равен "LPT1:", <see cref="Description"/> будет равен "printer port"). Может быть равно null. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string Description; /// <summary> /// Тип порта. /// </summary> public PortType Type; /// <summary> /// Зарезервировано. Должен быть равен 0. /// </summary> internal uint Reserved; }
Spooler предусматривает два способа открытия и закрытия порта: первый — использовать базовые методы AddPort/DeletePort, второй — использовать средства XcvData и различные хэлперы над ним. Второй вариант для нас предпочтительнее, т.к. в первом случае понадобится указатель на диалоговое окно процесса установки, что нам отнюдь не нужно. Для XCV нам дополнительно понадобятся:
enum PrinterAccess - права доступа к данным принтера
/// <summary> /// Права доступа к принтеру. /// </summary> internal enum PrinterAccess { /// <summary> /// Полный доступ к данным принтера. /// </summary> ServerAdmin = 0x01, /// <summary> /// Доступ к чтению данных принтера. /// </summary> ServerEnum = 0x02, /// <summary> /// Полный доступ к использованию принтера. /// </summary> PrinterAdmin = 0x04, /// <summary> /// Ограниченный доступ к использованию принтера. /// </summary> PrinterUse = 0x08, /// <summary> /// Полный доступ к данным очереди печати. /// </summary> JobAdmin = 0x10, /// <summary> /// Чтение данных очереди печати. /// </summary> JobRead = 0x20, /// <summary> /// Стандартные права доступа. /// </summary> StandardRightsRequired = 0x000F0000, /// <summary> /// Самый полный доступ. /// </summary> PrinterAllAccess = (StandardRightsRequired | PrinterAdmin | PrinterUse), }
struct PrinterDefaults - установки принтера для XcvData
/// <summary> /// Представляет установки принтера для <see cref="Port.XcvData(IntPtr, string, IntPtr, uint, IntPtr, uint, out uint, out uint)"/>. /// </summary> [StructLayout(LayoutKind.Sequential)] internal struct PrinterDefaults { /// <summary> /// Тип данных (по умолчанию равен null). /// </summary> public IntPtr DataType; /// <summary> /// Режим работы (по умолчанию равен null). /// </summary> public IntPtr DevMode; /// <summary> /// Права доступа к принтеру. /// </summary> public PrinterAccess DesiredAccess; }
struct PortData - структура данных порта для XcvData
/// <summary> /// Представляет данные принтера для <see cref="Port.XcvData(System.IntPtr, string, System.IntPtr, uint, System.IntPtr, uint, out uint, out uint)"/>. /// </summary> [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] internal struct PortData { /// <summary> /// Наименование порта. /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string PortName; /// <summary> /// Номер версии (по умолчанию равен 1). /// </summary> public uint Version; /// <summary> /// Протокол. /// </summary> public DataType Protocol; /// <summary> /// Размер буфера данных. /// </summary> public uint BufferSize; /// <summary> /// Размер зарезервированного буфера. /// </summary> public uint ReservedSize; /// <summary> /// Адрес хоста. /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 49)] public string HostAddress; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)] public string SNMPCommunity; public uint DoubleSpool; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 33)] public string Queue; /// <summary> /// IP-адрес. /// </summary> [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)] public string IPAddress; /// <summary> /// Зарезервированный буфер. /// </summary> [MarshalAs(UnmanagedType.ByValArray, SizeConst = 540)] public byte[] Reserved; /// <summary> /// Номер порта. /// </summary> public uint PortNumber; public uint SNMPEnabled; public uint SNMPDevIndex; }
enum XcvDataType - тип операции для XcvData
/// <summary> /// Тип операции для <see cref="Port.XcvData(IntPtr, string, IntPtr, uint, IntPtr, uint, out uint, out uint)"/>. /// </summary> internal enum XcvDataType { /// <summary> /// Добавить новый порт. /// </summary> AddPort, /// <summary> /// Удалить существующий порт. /// </summary> DeletePort, }
Отлично, теперь у нас есть всё что нужно для реализации класса порта:
class Port
/// <summary> /// Представляет порт для запуска принтера. /// </summary> public class Port : PrintableDevice, IPort { /// <summary> /// Монитор, на котором открыт порт. /// </summary> public virtual IMonitor Monitor { get; protected set; } /// <summary> /// Описание порта. /// </summary> public virtual string Description { get; protected set; } /// <summary> /// Тип порта. /// </summary> public virtual PortType Type { get; protected set; } /// <summary> /// Возвращает список всех установленных в системе портов печати. /// </summary> public static Port[] All { get { if (!PrintingApi.TryGetInfo(EnumPorts, out PortInfo[] portInfo)) return null; Port[] ports = new Port[portInfo.Length]; for (int i = 0; i < portInfo.Length; i++) ports[i] = new Port(portInfo[i].PortName, portInfo[i].Description, portInfo[i].Type, portInfo[i].MonitorName); return ports; } } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="type">Тип порта.</param> /// <param name="monitorName">Наименование монитора печати, на котором открыт порт.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name, string description, PortType type, string monitorName) : base(name) { Description = description; Type = type; Monitor[] monitors = PrintingApi.Monitors; if (monitors.Select(m => m.Name).Contains(monitorName)) Monitor = monitors.Where(m => m.Name == monitorName).FirstOrDefault(); } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="type">Тип порта.</param> /// <param name="monitor">Монитор печати, на котором открыт порт.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name, string description, PortType type, IMonitor monitor) : this(name, description, type, monitor?.Name) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="type">Тип порта.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name, string description, PortType type) : this(name, description, type, null as string) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="monitorName">Наименование монитора печати, на котором открыт порт.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name, string description, string monitorName) : this(name, description, PortType.Redirected, monitorName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="monitor">Монитор печати, на котором открыт порт.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name, string description, IMonitor monitor) : this(name, description, PortType.Redirected, monitor) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name, string description) : this(name, description, null as string) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="monitor">Монитор печати, на котором открыт порт.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name, IMonitor monitor) : this(name, null, monitor) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="type">Тип порта.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name, PortType type) : this(name, null, type) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Port"/>. /// </summary> /// <param name="name">Наименование порта.</param> /// <exception cref="ArgumentNullException"/> /// <exception cref="PrintingException"/> public Port(string name) : this(name, null as string) { } /// <summary> /// Открывает порт на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <exception cref="FileNotFoundException"/> /// <exception cref="PrintingException"/> public override void Install(string serverName) { try { if (All.Select(p => p.Name).Contains(Name)) Uninstall(serverName); PrinterDefaults defaults = new PrinterDefaults { DesiredAccess = PrinterAccess.ServerAdmin }; if (!OpenPrinter($",XcvMonitor {Monitor.Name}", out IntPtr printerHandle, ref defaults)) throw new PrintingException(Marshal.GetLastWin32Error()); PortData portData = new PortData { Version = 1, Protocol = DataType.RAW, PortNumber = 9100, // 9100 = RAW, 515 = LPR. ReservedSize = 0, PortName = Name, IPAddress = serverName, SNMPCommunity = "public", SNMPEnabled = 1, SNMPDevIndex = 1, }; uint size = (uint)Marshal.SizeOf(portData); portData.BufferSize = size; IntPtr pointer = Marshal.AllocHGlobal((int)size); Marshal.StructureToPtr(portData, pointer, true); try { IntPtr outputData = IntPtr.Zero; uint outputDataSize = 0; if (!XcvData(printerHandle, Enum.GetName(typeof(XcvDataType), XcvDataType.AddPort), pointer, size, outputData, outputDataSize, out uint outputNeeded, out uint status)) throw new PrintingException(Marshal.GetLastWin32Error()); } catch (Exception e) { throw new PrintingException(e.Message, e); } finally { Marshal.FreeHGlobal(pointer); ClosePrinter(printerHandle); } } catch (Exception e) { throw new PrintingException(e.Message, e); } } /// <summary> /// Закрывает порт на удалённой машине. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <exception cref="FileNotFoundException"/> /// <exception cref="PrintingException"/> public override void Uninstall(string serverName) { try { if (!All.Select(p => p.Name).Contains(Name)) return; /// TODO: Удалить все принтеры, привязанные к порту. PrinterDefaults defaults = new PrinterDefaults { DesiredAccess = PrinterAccess.ServerAdmin }; if (!OpenPrinter($",XcvPort {Name}", out IntPtr printerHandle, ref defaults)) throw new PrintingException(Marshal.GetLastWin32Error()); PortData portData = new PortData { Version = 1, Protocol = DataType.RAW, PortNumber = 9100, ReservedSize = 0, PortName = Name, IPAddress = serverName, SNMPCommunity = "public", SNMPEnabled = 1, SNMPDevIndex = 1, }; uint size = (uint)Marshal.SizeOf(portData); portData.BufferSize = size; IntPtr pointer = Marshal.AllocHGlobal((int)size); Marshal.StructureToPtr(portData, pointer, true); try { IntPtr outputData = IntPtr.Zero; uint outputDataSize = 0; if (!XcvData(printerHandle, Enum.GetName(typeof(XcvDataType), XcvDataType.DeletePort), pointer, size, outputData, outputDataSize, out uint outputNeeded, out uint status)) throw new PrintingException(Marshal.GetLastWin32Error()); } catch (Exception e) { throw new PrintingException(e.Message, e); } finally { Marshal.FreeHGlobal(pointer); ClosePrinter(printerHandle); } } catch (Exception e) { throw new PrintingException(e.Message, e); } } #region Native /// <summary> /// Получает указатель на принтер. /// </summary> /// <param name="printerName">Имя принтера.</param> /// <param name="printer">Указатель на принтер.</param> /// <param name="printerDefaults">Установки для <see cref="XcvData(IntPtr, string, IntPtr, uint, IntPtr, uint, out uint, out uint)"/>.</param> /// <returns>True, если операция выполнена успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool OpenPrinter(string printerName, out IntPtr printer, ref PrinterDefaults printerDefaults); /// <summary> /// Освобождает ресурсы принтера. /// </summary> /// <param name="printer">Указатель принтера.</param> /// <returns>True, если операция выполнена успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool ClosePrinter(IntPtr printer); /// <summary> /// Производит оперции с принтером. /// </summary> /// <param name="printer">Указатель на принтер.</param> /// <param name="dataType">Тип операции.</param> /// <param name="inputData">Входные данные.</param> /// <param name="inputDataSize">Размер буфера входных данных.</param> /// <param name="outputData">Выходные данные.</param> /// <param name="outputDataSize">Размер буфера выходных данных.</param> /// <param name="outputNeeded">Размер указателя на выходные данные.</param> /// <param name="status">Статус операции.</param> /// <returns>True, если операция выполнена успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool XcvData(IntPtr printer, string dataType, IntPtr inputData, uint inputDataSize, IntPtr outputData, uint outputDataSize, out uint outputNeeded, out uint status); /// <summary> /// Возвращает указатель на буфер экземпляров структур <see cref="PortInfo"/>. /// </summary> /// <param name="serverName">Имя сервера, с которого требуется получить список портов. Если равно null - получает на локальной машине.</param> /// <param name="level">Номер версии структуры. Должен быть равен 1 или 2.</param> /// <param name="ports">Указатель на буфер экземпляров структур <see cref="PortInfo"/>.</param> /// <param name="bufferSize">Размер буфера экземпляров структур <see cref="PortInfo"/> (в байтах).</param> /// <param name="bytesNeeded">Число полученных байт размера буфера.</param> /// <param name="bufferReturnedLength">Число экземпляров структур.</param> /// <returns>True, если операция выполнена успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool EnumPorts(string serverName, uint level, IntPtr ports, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength); #endregion }
Теперь расширим наш PrintingApi за счёт внесения функционала для работы с портами:
Добавляем методы работы с портами в PrintingApi
/// <summary> /// Возвращает коллекцию всех октрытых портов печати в системе. /// </summary> public static Port[] Ports => Port.All; /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="type">Тип порта.</param> /// <param name="monitor">Монитор печати, на котором открывается порт.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, string description, PortType type, Monitor monitor, string serverName) { Port port = new Port(name, description, type, monitor); monitor.TryInstall(serverName); return port; } /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="type">Тип порта.</param> /// <param name="monitor">Монитор печати, на котором открывается порт.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, string description, PortType type, Monitor monitor) => OpenPort(name, description, type, monitor, null); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="type">Тип порта.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, string description, PortType type, string serverName) => OpenPort(name, description, type, null, serverName); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="type">Тип порта.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, string description, PortType type) => OpenPort(name, description, type, null as string); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="monitor">Монитор печати, на котором открывается порт.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, string description, Monitor monitor, string serverName) => OpenPort(name, description, PortType.Redirected, monitor, serverName); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="monitor">Монитор печати, на котором открывается порт.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, string description, Monitor monitor) => OpenPort(name, description, monitor, null); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="monitor">Монитор печати, на котором открывается порт.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, Monitor monitor, string serverName) => OpenPort(name, null, monitor, serverName); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="monitor">Монитор печати, на котором открывается порт.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, Monitor monitor) => OpenPort(name, monitor, null); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="type">Тип порта.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, PortType type, string serverName) => OpenPort(name, null, type, serverName); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="type">Тип порта.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, PortType type) => OpenPort(name, type, null); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, string description, string serverName) => OpenPort(name, description, null, serverName); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <param name="description">Описание порта.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name, string description) => OpenPort(name, description, null as string); /// <summary> /// Открывает новый порт печати в системе. /// </summary> /// <param name="name">Наименование порта.</param> /// <returns>Экземпляр порта печати.</returns> public Port OpenPort(string name) => OpenPort(name, null as string);
Также не забываем добавить проверку в метод удаления монитора вместо TODO:
IEnumerable<Port> openPorts = Port.All.Where(p => p.Monitor?.Name == Name); foreach (Port openPort in openPorts) openPort.Uninstall(serverName);
Теперь убедимся в работоспособности нашего кода и можно приступать к следующему этапу:
Unit-тест для работы с портами печати
/// <summary> /// Представляет тестовый модуль класса <see cref="Port"/>. /// </summary> [TestClass] public class PortTests { /// <summary> /// Наименование порта. /// </summary> protected const string PortName = "TESTPORT:"; /// <summary> /// Описание порта. /// </summary> protected const string PortDescription = "Description for " + PortName; /// <summary> /// Наименование монитора. /// </summary> protected const string MonitorName = "mfilemon"; /// <summary> /// Наименование несуществующего монитора. /// </summary> protected const string FailedMonitorName = "noexist"; /// <summary> /// Тест локальной установки порта. /// </summary> [TestMethod] public void InstallTest() { Port port = new Port(PortName, PortDescription, MonitorName); port.Install(); Assert.IsTrue(Port.All.Select(p => p.Name).Contains(PortName)); } /// <summary> /// Тест локального удаления порта. /// </summary> [TestMethod] public void UninstallTest() { Port port = new Port(PortName, PortDescription, MonitorName); port.Uninstall(); Assert.IsFalse(Port.All.Select(p => p.Name).Contains(PortName)); } /// <summary> /// Тест локальной установки порта с перехватом состояния установки. /// </summary> [TestMethod] public void TryInstallTest() { Port port = new Port(PortName, PortDescription, MonitorName); bool f = port.TryInstall(); Assert.IsTrue(f); Assert.IsTrue(Port.All.Select(p => p.Name).Contains(PortName)); } /// <summary> /// Тест локального удаления порта с перехватом состояния удаления. /// </summary> [TestMethod] public void TryUninstallTest() { Port port = new Port(PortName, PortDescription, MonitorName); bool f = port.TryUninstall(); Assert.IsTrue(f); Assert.IsFalse(Port.All.Select(p => p.Name).Contains(PortName)); } /// <summary> /// Тест неправильной локальной установки порта. /// </summary> [TestMethod] [ExpectedException(typeof(PrintingException))] public void InstallFailedTest() { Port port = new Port(PortName, PortDescription, MonitorName); port.Install(); Assert.IsFalse(Port.All.Select(p => p.Name).Contains(PortName)); } /// <summary> /// Тест неправильной локальной установки порта с перехватом состояния установки. /// </summary> [TestMethod] public void TryInstallFailedTest() { Port port = new Port(PortName, PortDescription, FailedMonitorName); bool f = port.TryUninstall(); Assert.IsTrue(f); Assert.IsFalse(Port.All.Select(p => p.Name).Contains(PortName)); } }
Драйвер принтера
С драйверами всё точно так же, как и в случае с мониторами. Сперва описываем нативную структуру данных, затем интерфейс, реализуем интерфейс, расширяем функционал API и тестируем, не забыв предварительно закинуть в тестовый каталог файлы драйверов:
struct DriverInfo
/// <summary> /// Представляет структуру для хранения информации о драйвере устройства принтера. /// </summary> [StructLayout(LayoutKind.Sequential)] public struct DriverInfo { /// <summary> /// Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно). /// </summary> public uint Version; /// <summary> /// Имя драйвера (например, "QMS 810"). /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string Name; /// <summary> /// Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64). /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string Environment; /// <summary> /// Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll"). /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string DriverPath; /// <summary> /// Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd"). /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string DataFile; /// <summary> /// Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll"). /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string ConfigFile; /// <summary> /// Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp"). /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string HelpFile; /// <summary> /// Зависимые файлы. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string DependentFiles; /// <summary> /// Наименование монитора. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string MonitorName; /// <summary> /// Тип данных принтера по умолчанию. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string DefaultDataType; }
interface IDriver
/// <summary> /// Представляет интерфейс для реализации сущностей драйверов печати. /// </summary> public interface IDriver : IPrintableDevice { /// <summary> /// Монитор печати. /// </summary> IMonitor Monitor { get; } /// <summary> /// Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно). /// </summary> uint Version { get; } /// <summary> /// Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64). /// </summary> Environment Environment { get; } /// <summary> /// Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll"). /// </summary> string Dll { get; } /// <summary> /// Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd"). /// </summary> string DataFile { get; } /// <summary> /// Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll"). /// </summary> string ConfigFile { get; } /// <summary> /// Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp"). /// </summary> string HelpFile { get; } /// <summary> /// Зависимые файлы. /// </summary> string DependentFiles { get; } /// <summary> /// Тип данных принтера по умолчанию. /// </summary> DataType DefaultDataType { get; } }
class Driver
/// <summary> /// Представляет данные для работы с драйвером печати. /// </summary> public class Driver : PrintableDevice, IDriver { /// <summary> /// Монитор печати. /// </summary> public virtual IMonitor Monitor { get; protected set; } /// <summary> /// Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно). /// </summary> public virtual uint Version { get; protected set; } /// <summary> /// Окружение, для которого был написан драйвер. /// </summary> public virtual Environment Environment { get; protected set; } /// <summary> /// Полный или относительный путь к файлу драйвера устройства. /// </summary> public virtual string Dll { get; protected set; } /// <summary> /// Полный или относительный путь к файлу данных драйвера. /// </summary> public virtual string DataFile { get; protected set; } /// <summary> /// Полный или относительный путь к dll данных конфигурации драйвера. /// </summary> public virtual string ConfigFile { get; protected set; } /// <summary> /// Полный или относительный путь к dll данных HLP-файла драйвера. /// </summary> public virtual string HelpFile { get; protected set; } /// <summary> /// Зависимые файлы. /// </summary> public virtual string DependentFiles { get; protected set; } /// <summary> /// Тип данных принтера по умолчанию. /// </summary> public virtual DataType DefaultDataType { get; protected set; } /// <summary> /// Путь к системному каталогу драйверов печати. /// </summary> public static string Directory { get; protected set; } /// <summary> /// Возвращает коллекцию всех установленных драйверов печати в системе. /// </summary> public static Driver[] All { get { if (!PrintingApi.TryGetInfo(EnumPrinterDrivers, 3, out DriverInfo[] driverInfo)) return null; Driver[] drivers = new Driver[driverInfo.Length]; for (int i = 0; i < driverInfo.Length; i++) drivers[i] = new Driver(driverInfo[i].Name, driverInfo[i].DriverPath, driverInfo[i].DataFile, driverInfo[i].ConfigFile, driverInfo[i].HelpFile, driverInfo[i].Version, driverInfo[i].Environment.GetEnvironment(), (DataType)Enum.Parse(typeof(DataType), driverInfo[i].DefaultDataType ?? "RAW", true), driverInfo[i].DependentFiles, driverInfo[i].MonitorName); return drivers; } } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <param name="dependentFiles">Зависимые файлы.</param> /// <param name="monitorName">Наименование монитора печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType, string dependentFiles, string monitorName) : base(name) { Dll = dll; DataFile = dataFile; ConfigFile = configFile; HelpFile = helpFile; Version = version; Environment = environment; DefaultDataType = defaultDataType; DependentFiles = dependentFiles; Monitor[] monitors = PrintingApi.Monitors; if (monitors.Select(m => m.Name).Contains(monitorName)) Monitor = monitors.Where(m => m.Name == monitorName).FirstOrDefault(); } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <param name="dependentFiles">Зависимые файлы.</param> /// <param name="monitor">Монитор печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType, string dependentFiles, IMonitor monitor) : this(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, monitor?.Name) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <param name="monitorName">Наименование монитора печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType, string monitorName) : this(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, null, monitorName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <param name="monitor">Монитор печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType, IMonitor monitor) : this(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, monitor?.Name) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="monitorName">Наименование монитора печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, string monitorName) : this(name, dll, dataFile, configFile, helpFile, version, environment, DataType.RAW, monitorName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="monitor">Монитор печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, IMonitor monitor) : this(name, dll, dataFile, configFile, helpFile, version, environment, monitor?.Name) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="monitorName">Наименование монитора печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, string monitorName) : this(name, dll, dataFile, configFile, helpFile, version, Environment.Current, monitorName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="monitor">Монитор печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, IMonitor monitor) : this(name, dll, dataFile, configFile, helpFile, version, monitor?.Name) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="monitorName">Наименование монитора печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, string monitorName) : this(name, dll, dataFile, configFile, helpFile, 3, monitorName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="monitor">Монитор печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string helpFile, IMonitor monitor) : this(name, dll, dataFile, configFile, helpFile, monitor?.Name) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="monitorName">Наименование монитора печати.</param> public Driver(string name, string dll, string dataFile, string configFile, string monitorName) : this(name, dll, dataFile, configFile, null, monitorName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="monitor">Монитор печати.</param> public Driver(string name, string dll, string dataFile, string configFile, IMonitor monitor) : this(name, dll, dataFile, configFile, monitor?.Name) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Driver"/>. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> public Driver(string name, string dll, string dataFile, string configFile) : this(name, dll, dataFile, configFile, null as string) { } /// <summary> /// Статический инициализатор класса <see cref="Driver"/>. /// </summary> static Driver() { uint length = 1024; StringBuilder driverDirectory = new StringBuilder((int)length); uint bytesNeeded = 0; if (!GetPrinterDriverDirectory(null, null, 1, driverDirectory, length, ref bytesNeeded)) throw new PrintingException(Marshal.GetLastWin32Error()); Directory = driverDirectory.ToString(); } /// <summary> /// Устанавливает драйвер удалённо в системе. /// </summary> /// <param name="serverName">Имя сервера.</param> public override void Install(string serverName) { try { if (!File.Exists(Dll)) throw new PrintingException($"Не удалось найти файл драйвера '{Dll}'"); if (!File.Exists(DataFile)) throw new PrintingException($"Не удалось найти файл драйвера '{DataFile}'"); if (!File.Exists(ConfigFile)) throw new PrintingException($"Не удалось найти файл драйвера '{ConfigFile}'"); if (All.Select(d => d.Name).Contains(Name)) Uninstall(serverName); string systemDriverPath = Path.Combine(Directory, Path.GetFileName(Dll)); string systemDataPath = Path.Combine(Directory, Path.GetFileName(DataFile)); string systemConfigPath = Path.Combine(Directory, Path.GetFileName(ConfigFile)); string systemHelpPath = Path.Combine(Directory, Path.GetFileName(HelpFile)); File.Copy(Dll, systemDriverPath, true); File.Copy(DataFile, systemDataPath, true); File.Copy(ConfigFile, systemConfigPath, true); if (File.Exists(HelpFile)) File.Copy(HelpFile, systemHelpPath, true); DriverInfo driverInfo = new DriverInfo { Version = Version, Name = Name, Environment = Environment.GetEnvironmentName(), DriverPath = File.Exists(systemDriverPath) ? systemDriverPath : Dll, DataFile = File.Exists(systemDataPath) ? systemDataPath : DataFile, ConfigFile = File.Exists(systemConfigPath) ? systemConfigPath : ConfigFile, HelpFile = File.Exists(systemHelpPath) ? systemHelpPath : HelpFile, DependentFiles = DependentFiles, MonitorName = Monitor?.Name, DefaultDataType = Enum.GetName(typeof(DataType), DefaultDataType), }; if (AddPrinterDriver(serverName, Version, ref driverInfo)) return; int lastWin32ErrorCode = Marshal.GetLastWin32Error(); if (lastWin32ErrorCode == 0) return; throw new PrintingException(lastWin32ErrorCode); } catch (Exception e) { throw new PrintingException(e.Message, e); } } /// <summary> /// Удаляет драйвер удалённо в системе. /// </summary> /// <param name="serverName">Имя сервера.</param> public override void Uninstall(string serverName) { try { if (!All.Select(d => d.Name).Contains(Name)) return; /// TODO: Удалить все принтеры, использующие драйвер. if (DeletePrinterDriver(serverName, Environment.GetEnvironmentName(), Name)) return; throw new PrintingException(Marshal.GetLastWin32Error()); } catch (Exception e) { throw new PrintingException(e.Message, e); } } #region Native /// <summary> /// Возвращает путь к системной директории с установленными драйверами принтера. /// </summary> /// <param name="serverName">Имя сервера, с которого требуется получить путь к директории драйвера принтера. Если равно null - получает локальный путь.</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="level">Номер версии структуры. Должен быть равен 1.</param> /// <param name="driverDirectory">Путь к драйверу принтера.</param> /// <param name="bufferSize">Размер буфера для вывода пути.</param> /// <param name="bytesNeeded">Число полученных байт пути.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool GetPrinterDriverDirectory(string serverName, string environment, uint level, [Out] StringBuilder driverDirectory, uint bufferSize, ref uint bytesNeeded); /// <summary> /// Добавляет драйвер в систему. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <param name="level">Номер версии структуры. Должен быть равен 1, 2, 3, 4, 5, 6 или 8.</param> /// <param name="driverInfo">Данные драйвера.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool AddPrinterDriver(string serverName, uint level, ref DriverInfo driverInfo); /// <summary> /// Возвращает указатель на буфер экземпляров структур <see cref="DriverInfo"/>. /// </summary> /// <param name="serverName">Имя сервера, с которого требуется получить список портов. Если равно null - получает на локальной машине.</param> /// <param name="environment">Окружение (например, Windows x86, Windows IA64, Windows x64, или Windows NT R4000). Если параметр равен null, /// используется вызываемое окружение клиента (не сервера). Если параметр равен "all", метод верёт список драйверов для всех платформ, /// для которых они были установлены на сервере.</param> /// <param name="level">Номер версии структуры. Должен быть равен 1, 2, 3, 4, 5, 6 или 8.</param> /// <param name="drivers">Указатель на буфер экземпляров структур <see cref="DriverInfo"/>.</param> /// <param name="bufferSize">Размер буфера экземпляров структур <see cref="DriverInfo"/> (в байтах).</param> /// <param name="bytesNeeded">Число полученных байт размера буфера.</param> /// <param name="bufferReturnedLength">Число экземпляров структур.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool EnumPrinterDrivers(string serverName, string environment, uint level, IntPtr drivers, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength); /// <summary> /// Удаляет драйвер из системы. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <param name="environment">Окружение (например, Windows x86, Windows IA64, Windows x64, или Windows NT R4000). Если параметр равен null, /// используется вызываемое окружение клиента (не сервера). Если параметр равен "all", метод верёт список драйверов для всех платформ, /// для которых они были установлены на сервере.</param> /// <param name="driverName">Наименование драйвера.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool DeletePrinterDriver(string serverName, string environment, string driverName); #endregion }
Расширяем PrintingApi методами работы с драйверами
/// <summary> /// Возвращает коллекцию всех установленных драйверов печати в системе. /// </summary> public static Driver[] Drivers => Driver.All; /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <param name="dependentFiles">Зависимые файлы.</param> /// <param name="monitor">Монитор печати.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType, string dependentFiles, Monitor monitor, string serverName) { Driver driver = new Driver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, monitor); driver.TryInstall(serverName); return driver; } /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <param name="dependentFiles">Зависимые файлы.</param> /// <param name="monitor">Монитор печати.</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType, string dependentFiles, Monitor monitor) => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, monitor, null); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <param name="dependentFiles">Зависимые файлы.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType, string dependentFiles, string serverName) => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, null, serverName); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <param name="dependentFiles">Зависимые файлы.</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType, string dependentFiles) => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, dependentFiles, null as string); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="defaultDataType">Тип данных принтера по умолчанию.</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, DataType defaultDataType) => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, defaultDataType, null); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment, string serverName) => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, DataType.RAW, serverName); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="environment">Окружение, для которого был написан драйвер (например, Windows x86, Windows IA64 или Windows x64).</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, Environment environment) => InstallDriver(name, dll, dataFile, configFile, helpFile, version, environment, null); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version, string serverName) => InstallDriver(name, dll, dataFile, configFile, helpFile, version, Environment.Current, serverName); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="version">Номер версии операционной системы, для которой был написан драйвер. Поддерживаемые значения - 3 и 4 (V3 и V4 номера версий драйвера соответственно).</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, uint version) => InstallDriver(name, dll, dataFile, configFile, helpFile, version, null); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile, string serverName) => InstallDriver(name, dll, dataFile, configFile, helpFile, 3, serverName); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <param name="helpFile">Полный или относительный путь к dll данных HLP-файла драйвера (например, "C:\DRIVERS\Pscript.hlp").</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile, string helpFile) => InstallDriver(name, dll, dataFile, configFile, helpFile, null); /// <summary> /// Устанавливает новый драйвер в системе. /// </summary> /// <param name="name">Наименование драйвера.</param> /// <param name="dll">Полный или относительный путь к файлу драйвера устройства (например, "C:\DRIVERS\Pscript.dll").</param> /// <param name="dataFile">Полный или относительный путь к файлу данных драйвера (например, "C:\DRIVERS\Qms810.ppd").</param> /// <param name="configFile">Полный или относительный путь к dll данных конфигурации драйвера (например, "C:\DRIVERS\Pscriptui.dll").</param> /// <returns>Экземпляр драйвера печати.</returns> public Driver InstallDriver(string name, string dll, string dataFile, string configFile) => InstallDriver(name, dll, dataFile, configFile, null);
Добавляем удаление драйверов, привязанных к монитору печати в методе удаления монитора
IEnumerable<Driver> drivers = Driver.All.Where(d => d.Monitor?.Name == Name); foreach (Driver driver in drivers) driver.Uninstall(serverName);
Тестируем с помощью Unit-тестов
/// <summary> /// Представляет тестовый модуль класса <see cref="Driver"/>. /// </summary> [TestClass] public class DriverTests { /// <summary> /// Наименование драйвера. /// </summary> protected const string DriverName = "Test Driver"; /// <summary> /// Наименование монитора. /// </summary> protected const string MonitorName = "mfilemon"; /// <summary> /// Наименование несуществующего монитора. /// </summary> protected const string FailedMonitorName = "noexist"; protected const string DllPath = "D:/Printing Tests/pscript.dll"; protected const string DataPath = "D:/Printing Tests/testprinter.ppd"; protected const string ConfigPath = "D:/Printing Tests/pscriptui.dll"; protected const string HelpPath = "D:/Printing Tests/pscript.hlp"; /// <summary> /// Тест локальной установки драйвера. /// </summary> [TestMethod] public void InstallTest() { Driver driver = new Driver(DriverName, DllPath, DataPath, ConfigPath, HelpPath, MonitorName); driver.Install(); Assert.IsTrue(Driver.All.Select(d => d.Name).Contains(DriverName)); } /// <summary> /// Тест локального удаления драйвера. /// </summary> [TestMethod] public void UninstallTest() { Driver driver = new Driver(DriverName, DllPath, DataPath, ConfigPath, HelpPath, MonitorName); driver.Uninstall(); Assert.IsFalse(Driver.All.Select(d => d.Name).Contains(DriverName)); } /// <summary> /// Тест локальной установки драйвера с перехватом состояния установки. /// </summary> [TestMethod] public void TryInstallTest() { Driver driver = new Driver(DriverName, DllPath, DataPath, ConfigPath, HelpPath, MonitorName); bool f = driver.TryInstall(); Assert.IsTrue(f); Assert.IsTrue(Driver.All.Select(d => d.Name).Contains(DriverName)); } /// <summary> /// Тест локального удаления драйвера с перехватом состояния удаления. /// </summary> [TestMethod] public void TryUninstallTest() { Driver driver = new Driver(DriverName, DllPath, DataPath, ConfigPath, HelpPath, MonitorName); bool f = driver.TryUninstall(); Assert.IsTrue(f); Assert.IsFalse(Driver.All.Select(d => d.Name).Contains(DriverName)); } /// <summary> /// Тест неправильной локальной установки драйвера. /// </summary> [TestMethod] [ExpectedException(typeof(PrintingException))] public void InstallFailedTest() { Driver driver = new Driver(DriverName, DllPath + "failed", DataPath, ConfigPath, HelpPath, FailedMonitorName); driver.Install(); Assert.IsFalse(Driver.All.Select(d => d.Name).Contains(DriverName)); } /// <summary> /// Тест неправильной локальной установки драйвера с перехватом состояния установки. /// </summary> [TestMethod] public void TryInstallFailedTest() { Driver driver = new Driver(DriverName, DllPath + "failed", DataPath, ConfigPath, HelpPath, FailedMonitorName); bool f = driver.TryInstall(); Assert.IsTrue(f); Assert.IsFalse(Driver.All.Select(d => d.Name).Contains(DriverName)); } }
Устройство печати
А сейчас пришло время реализовать, пожалуй, самый главный компонент, обеспечивающий взаимосвязь между UI и монитором печати — принтер. Здесь тоже мало отличий от предыдущих манипуляций:
struct PrinterInfo
/// <summary> /// Представляет структуру данных принтера. /// </summary> [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] public struct PrinterInfo { /// <summary> /// Имя сервера. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string ServerName; /// <summary> /// Наименование принтера. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string PrinterName; /// <summary> /// Публичное наименование принтера. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string ShareName; /// <summary> /// Наименование порта, привязанного к принтеру. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string PortName; /// <summary> /// Наименование драйвера принтера. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string DriverName; /// <summary> /// Описание принтера. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string Comment; /// <summary> /// Местоположение принтера. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string Location; public IntPtr DevMode; [MarshalAs(UnmanagedType.LPTStr)] public string SepFile; /// <summary> /// Процессор печати, связанный с принтером. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string PrintProcessor; /// <summary> /// Тип данных печати принтера. /// </summary> [MarshalAs(UnmanagedType.LPTStr)] public string DataType; [MarshalAs(UnmanagedType.LPTStr)] public string Parameters; public IntPtr SecurityDescriptor; public uint Attributes; public uint Priority; public uint DefaultPriority; public uint StartTime; public uint UntilTime; public uint Status; public uint cJobs; public uint AveragePPM; }
Для получения списка установленных принтеров нам так же понадобится флаг:
enum PrinterEnumFlag
/// <summary> /// Флаги выборки принтеров при получении их списка. /// </summary> [Flags] internal enum PrinterEnumFlag { Default = 0x00000001, Local = 0x00000002, Connections = 0x00000004, Favorite = 0x00000004, Name = 0x00000008, Remote = 0x00000010, Shared = 0x00000020, Network = 0x00000040, Expand = 0x00004000, Container = 0x00008000, IconMask = 0x00ff0000, Icon1 = 0x00010000, Icon2 = 0x00020000, Icon3 = 0x00040000, Icon4 = 0x00080000, Icon5 = 0x00100000, Icon6 = 0x00200000, Icon7 = 0x00400000, Icon8 = 0x00800000, Hide = 0x01000000, All = 0x02000000, Category3D = 0x04000000, }
interface IPrinter
/// <summary> /// Представляет интерфейс для реализации принтеров. /// </summary> public interface IPrinter : IPrintableDevice { /// <summary> /// Порт, к которому привязан принтер. /// </summary> IPort Port { get; } /// <summary> /// Драйвер, который связан с принтером. /// </summary> IDriver Driver { get; } /// <summary> /// Публичное наименование принтера. /// </summary> string ShareName { get; } /// <summary> /// Имя сервера, на котором запущен принтер. /// </summary> string ServerName { get; } /// <summary> /// Описание устройства принтера. /// </summary> string Description { get; } /// <summary> /// Расположение принтера. /// </summary> string Location { get; } string SepFile { get; } /// <summary> /// Параметры принтера. /// </summary> string Parameters { get; } /// <summary> /// Тип данных печати. /// </summary> DataType DataType { get; } }
class Printer
/// <summary> /// Представляет устройство принтера. /// </summary> public class Printer : PrintableDevice, IPrinter { /// <summary> /// Порт, к которому привязан принтер. /// </summary> public virtual IPort Port { get; protected set; } /// <summary> /// Драйвер, который связан с принтером. /// </summary> public virtual IDriver Driver { get; protected set; } /// <summary> /// Публичное наименование принтера. /// </summary> public virtual string ShareName { get; protected set; } /// <summary> /// Описание устройства принтера. /// </summary> public virtual string Description { get; protected set; } /// <summary> /// Тип данных печати. /// </summary> public virtual DataType DataType { get; protected set; } /// <summary> /// Процессор очереди печати. /// </summary> public virtual string Processor { get; protected set; } /// <summary> /// Имя сервера, на котором запущен принтер. /// </summary> public virtual string ServerName { get; protected set; } /// <summary> /// Расположение принтера. /// </summary> public virtual string Location { get; protected set; } /// <summary> /// Параметры принтера. /// </summary> public virtual string Parameters { get; protected set; } public virtual string SepFile { get; protected set; } /// <summary> /// Задаёт или возвращает принтер по умолчанию. /// </summary> public static Printer Default { get { uint length = 0; if (GetDefaultPrinter(null, ref length)) return null; int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != PrintingException.ErrorInsufficientBuffer) throw new PrintingException(lastWin32Error); StringBuilder printerName = new StringBuilder((int)length); if (!GetDefaultPrinter(printerName, ref length)) throw new PrintingException(Marshal.GetLastWin32Error()); string name = printerName.ToString(); return All.Where(p => p.Name == name).FirstOrDefault(); } set { if (!SetDefaultPrinter(value?.Name)) throw new PrintingException(Marshal.GetLastWin32Error()); } } /// <summary> /// Список всех установленных принтеров в системе. /// </summary> public static Printer[] All { get { uint bytesNeeded = 0; uint bufferReturnedLength = 0; uint level = 2; PrinterEnumFlag flags = PrinterEnumFlag.Local | PrinterEnumFlag.Network; if (EnumPrinters(flags, null, level, IntPtr.Zero, 0, ref bytesNeeded, ref bufferReturnedLength)) return null; int lastWin32Error = Marshal.GetLastWin32Error(); if (lastWin32Error != PrintingException.ErrorInsufficientBuffer) throw new PrintingException(lastWin32Error); IntPtr printersPtr = Marshal.AllocHGlobal((int)bytesNeeded); try { if (EnumPrinters(flags, null, level, printersPtr, bytesNeeded, ref bytesNeeded, ref bufferReturnedLength)) { IntPtr currentPrinterPtr = printersPtr; PrinterInfo[] printerInfo = new PrinterInfo[bufferReturnedLength]; Printer[] printers = new Printer[bufferReturnedLength]; Type type = typeof(PrinterInfo); for (int i = 0; i < bufferReturnedLength; i++) { printerInfo[i] = (PrinterInfo)Marshal.PtrToStructure(currentPrinterPtr, type); currentPrinterPtr = (IntPtr)(currentPrinterPtr.ToInt64() + Marshal.SizeOf(type)); printers[i] = new Printer(printerInfo[i].PrinterName, printerInfo[i].PortName, printerInfo[i].DriverName, printerInfo[i].PrintProcessor, printerInfo[i].ShareName, printerInfo[i].ServerName, printerInfo[i].Comment, (DataType)Enum.Parse(typeof(DataType), printerInfo[i].DataType), printerInfo[i].Location, printerInfo[i].Parameters, printerInfo[i].SepFile); } return printers; } throw new PrintingException(Marshal.GetLastWin32Error()); } catch { return null; } finally { Marshal.FreeHGlobal(printersPtr); } } } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> /// <param name="parameters">Параметры принтера.</param> /// <param name="sepFile"></param> public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description, DataType dataType, string location, string parameters, string sepFile) : base(name) { Port[] ports = PrintingApi.Ports; Driver[] drivers = PrintingApi.Drivers; if (ports.Select(p => p.Name).Contains(portName)) Port = ports.Where(p => p.Name == portName).FirstOrDefault(); if (drivers.Select(d => d.Name).Contains(driverName)) Driver = drivers.Where(d => d.Name == driverName).FirstOrDefault(); Processor = processorName; ShareName = shareName; ServerName = serverName; Description = description; DataType = dataType; Location = location; Parameters = parameters; SepFile = sepFile; } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> /// <param name="parameters">Параметры принтера.</param> /// <param name="sepFile"></param> public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description, DataType dataType, string location, string parameters, string sepFile) : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description, dataType, location, parameters, sepFile) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> /// <param name="parameters">Параметры принтера.</param> public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description, DataType dataType, string location, string parameters) : this(name, portName, driverName, processorName, shareName, serverName, description, dataType, location, parameters, null) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> /// <param name="parameters">Параметры принтера.</param> public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description, DataType dataType, string location, string parameters) : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description, dataType, location, parameters) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description, DataType dataType, string location) : this(name, portName, driverName, processorName, shareName, serverName, description, dataType, location, null) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description, DataType dataType, string location) : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description, dataType, location) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description, DataType dataType) : this(name, portName, driverName, processorName, shareName, serverName, description, dataType, null) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description, DataType dataType) : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description, dataType) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName, string description) : this(name, portName, driverName, processorName, shareName, serverName, description, DataType.RAW) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName, string description) : this(name, port?.Name, driver?.Name, processorName, shareName, serverName, description) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> public Printer(string name, string portName, string driverName, string processorName, string shareName, string serverName) : this(name, portName, driverName, processorName, shareName, serverName, null) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> public Printer(string name, IPort port, IDriver driver, string processorName, string shareName, string serverName) : this(name, port?.Name, driver?.Name, processorName, shareName, serverName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> public Printer(string name, string portName, string driverName, string processorName, string shareName) : this(name, portName, driverName, processorName, shareName, null) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> public Printer(string name, IPort port, IDriver driver, string processorName, string shareName) : this(name, port?.Name, driver?.Name, processorName, shareName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> /// <param name="processorName">Наименование процессора печати.</param> public Printer(string name, string portName, string driverName, string processorName) : this(name, portName, driverName, processorName, null) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> public Printer(string name, IPort port, IDriver driver, string processorName) : this(name, port?.Name, driver?.Name, processorName) { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="portName">Наименование порта.</param> /// <param name="driverName">Наименование драйвера.</param> public Printer(string name, string portName, string driverName) : this(name, portName, driverName, "WinPrint") { } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> public Printer(string name, IPort port, IDriver driver) : this(name, port?.Name, driver?.Name) { } /// <summary> /// Устанавливает принтер в системе. /// </summary> /// <param name="serverName">Имя сервера.</param> public override void Install(string serverName) { try { if (All.Select(p => p.Name).Contains(Name)) Uninstall(serverName); PrinterInfo printerInfo = new PrinterInfo { ServerName = serverName, PrinterName = Name, ShareName = ShareName, PortName = Port?.Name, DriverName = Driver?.Name, Comment = Description, Location = Location, DevMode = new IntPtr(0), SepFile = SepFile, PrintProcessor = Processor, DataType = Enum.GetName(typeof(DataType), DataType), Parameters = Parameters, SecurityDescriptor = new IntPtr(0), }; if (AddPrinter(serverName, 2, ref printerInfo)) return; int lastWin32ErrorCode = Marshal.GetLastWin32Error(); if (lastWin32ErrorCode == 0) return; throw new PrintingException(lastWin32ErrorCode); } catch (Exception e) { throw new PrintingException(e.Message, e); } } /// <summary> /// Удаляет принтер из системы. /// </summary> /// <param name="serverName">Имя сервера.</param> public override void Uninstall(string serverName) { try { if (!All.Select(p => p.Name).Contains(Name)) return; PrinterDefaults defaults = new PrinterDefaults { DesiredAccess = PrinterAccess.PrinterAllAccess }; if (!NET.Port.OpenPrinter(Name, out IntPtr printerHandle, ref defaults)) throw new PrintingException(Marshal.GetLastWin32Error()); if (!DeletePrinter(printerHandle)) { int lastWin32ErrorCode = Marshal.GetLastWin32Error(); if (lastWin32ErrorCode == PrintingException.ErrorInvalidPrinterName) return; throw new PrintingException(lastWin32ErrorCode); } NET.Port.ClosePrinter(printerHandle); } catch (Exception e) { throw new PrintingException(e.Message, e); } } #region Native /// <summary> /// Устанавливает принтер в системе. /// </summary> /// <param name="serverName">Имя сервера.</param> /// <param name="level">Уровень структуры.</param> /// <param name="printerInfo">Структура данных.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.StdCall)] internal static extern bool AddPrinter(string serverName, uint level, [In] ref PrinterInfo printerInfo); /// <summary> /// Возвращает имя принтера, установленного в системе по умолчанию. /// </summary> /// <param name="printerName">Имя принтера.</param> /// <param name="bytesNeeded">Размер символьного буфера имени.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool GetDefaultPrinter([Out] StringBuilder printerName, ref uint bytesNeeded); /// <summary> /// Устанавливает имя принтера по умолчанию. /// </summary> /// <param name="printerName">Имя устанавливаемого по умолчанию принтера.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool SetDefaultPrinter(string printerName); /// <summary> /// Получает список установленных в системе принтеров. /// </summary> /// <param name="flags">Флаги для выборки результатов.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="level">Уровень структуры.</param> /// <param name="printers">Указатель на буфер структур.</param> /// <param name="bufferSize">Размер буфера.</param> /// <param name="bytesNeeded">Число требуемых байт для выделения памяти под буфер.</param> /// <param name="bufferReturnedLength">Размер полученного буфера.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool EnumPrinters(PrinterEnumFlag flags, string serverName, uint level, IntPtr printers, uint bufferSize, ref uint bytesNeeded, ref uint bufferReturnedLength); /// <summary> /// Удаляет принтер из системы. /// </summary> /// <param name="printer">Указатель на принтер.</param> /// <returns>True, если операция прошла успешно, иначе False.</returns> [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true, CallingConvention = CallingConvention.StdCall)] internal static extern bool DeletePrinter(IntPtr printer); #endregion }
Расширяем PrintingApi
/// <summary> /// Возвращает коллекцию всех установленных устройств печати в системе. /// </summary> public static Printer[] Printers => Printer.All; /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> /// <param name="parameters">Параметры при��тера.</param> /// <param name="sepFile"></param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description, DataType dataType, string location, string parameters, string sepFile) { Printer printer = new Printer(name, port, driver, processorName, shareName, serverName, description, dataType, location, parameters, sepFile); printer.TryInstall(serverName); return printer; } /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> /// <param name="parameters">Параметры принтера.</param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description, DataType dataType, string location, string parameters) => RunPrinter(name, port, driver, processorName, shareName, serverName, description, dataType, location, parameters, null); /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <param name="location">Местоположение принтера.</param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description, DataType dataType, string location) => RunPrinter(name, port, driver, processorName, shareName, serverName, description, dataType, location, null); /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <param name="dataType">Тип данных печати.</param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description, DataType dataType) => RunPrinter(name, port, driver, processorName, shareName, serverName, description, dataType, null); /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <param name="description">Описание принтера.</param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName, string description) => RunPrinter(name, port, driver, processorName, shareName, serverName, description, DataType.RAW); /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <param name="serverName">Имя сервера.</param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName, string serverName) => RunPrinter(name, port, driver, processorName, shareName, serverName, null); /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <param name="shareName">Публичное наименование принтера.</param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver, string processorName, string shareName) => RunPrinter(name, port, driver, processorName, shareName, null); /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <param name="processorName">Наименование процессора печати.</param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver, string processorName) => RunPrinter(name, port, driver, processorName, null); /// <summary> /// Инициализирует новый экземпляр класса <see cref="Printer"/>. /// </summary> /// <param name="name">Наименование принтера.</param> /// <param name="port">Порт, к которому привязан принтер.</param> /// <param name="driver">Драйвер, который связан с принтером.</param> /// <returns>Экземпляр устройства печати.</returns> public Printer RunPrinter(string name, Port port, Driver driver) => RunPrinter(name, port, driver, "WinPrint");
Добавляем удаление всех привязанных принтеров к удаляемому порту и драйверу
IEnumerable<Printer> printers = Printer.All.Where(p => p.Driver?.Name == Name); foreach (Printer printer in printers) printer.Uninstall(serverName);
Тестируем
/// <summary> /// Представляет тестовый модуль класса <see cref="Printer"/>. /// </summary> [TestClass] public class PrinterTests { /// <summary> /// Наименование принтера. /// </summary> protected const string PrinterName = "Test Printer"; /// <summary> /// Наименование порта. /// </summary> protected const string PortName = "TESTPORT:"; /// <summary> /// Наименование драйвера. /// </summary> protected const string DriverName = "Test Driver"; /// <summary> /// Тест локальной установки принтера. /// </summary> [TestMethod] public void InstallTest() { Printer printer = new Printer(PrinterName, PortName, DriverName); printer.Install(); Assert.IsTrue(Printer.All.Select(p => p.Name).Contains(PrinterName)); } /// <summary> /// Тест локального удаления принтера. /// </summary> [TestMethod] public void UninstallTest() { Printer printer = new Printer(PrinterName, PortName, DriverName); printer.Uninstall(); Assert.IsFalse(Printer.All.Select(p => p.Name).Contains(PrinterName)); } /// <summary> /// Тест локальной установки принтера с перехватом состояния установки. /// </summary> [TestMethod] public void TryInstallTest() { Printer printer = new Printer(PrinterName, PortName, DriverName); bool f = printer.TryInstall(); Assert.IsTrue(f); Assert.IsTrue(Printer.All.Select(p => p.Name).Contains(PrinterName)); } /// <summary> /// Тест локального удаления принтера с перехватом состояния удаления. /// </summary> [TestMethod] public void TryUninstallTest() { Printer printer = new Printer(PrinterName, PortName, DriverName); bool f = printer.TryUninstall(); Assert.IsTrue(f); Assert.IsFalse(Printer.All.Select(p => p.Name).Contains(PrinterName)); } }
Для того, чтобы изменения в системе вступили в силу, после установки принтера нам понадобится перезапустить службу печати вручную. Напишем статический метод в классе PrintingApi, который будет запускать/перезапускать Spooler. Это так же актуально для случаев, когда служба печати на компьютере была изначально остановлена:
/// <summary> /// Перезагружает службу печати. /// </summary> /// <returns>True, если операция прошла успешно, иначе False.</returns> public static bool TryRestart() { int tryCount = 5; while (tryCount > 0) { try { ServiceController sc = new ServiceController("Spooler"); if (sc.Status != ServiceControllerStatus.Stopped || sc.Status != ServiceControllerStatus.StopPending) { sc.Stop(); sc.WaitForStatus(ServiceControllerStatus.Stopped); } sc.Start(); sc.WaitForStatus(ServiceControllerStatus.Running); return sc.Status == ServiceControllerStatus.Running; } catch { tryCount--; } } return false; }
Необходимо будет подключить в проект ссылку на System.ServiceProcess.dll. Здесь всё просто: запускаем контроллер службы, проверяем статус, если запущена — останавливаем, ждём пока остановится, затем запускаем и ждём пока статус поменяется на «запущено», в случае ошибки (например, если служба в данный момент занята) пытаемся повторить процедуру ещё четыре раза.
На этом базовый функционал для работы с устройствами печати можно считать готовым. Итак, что умеет наш API на данный момент:
- Получать коллекцию установленных в системе мониторов, драйверов, открытых портов и запущеных принтеров;
- Устанавливать новый монитор. В случае, если такой монитор уже установлен — переустанавливать его;
- Удалять монитор. В случае, если на мониторе открыты порты или привязаны драйвера — предварительно удалять и их тоже;
- Открывать новый порт. В случае, если такой порт уже открыт — переоткрывать его;
- Закрывать порт. В случае, если к порту привязаны принтеры — предварительно отключать их;
- Устанавливать драйвера принтера. В случае, если драйвер уже установлен — переустанавливать его;
- Удалять драйвера принтера. В случае, если драйвер используется принтерами — предварительно отключать их;
- Запускать принтер. В случае, если принтер с заданным именем уже запущен — перезапускать его;
- Отключать принтер;
- Отлавливать любые ошибки CLR и Win32;
- Перезапускать службу печати.
Делаем последний общий тест для класса PrintingApi и переходим к заключительной части статьи:
Unit-тест для проверки последовательной установки всех компонентов устройства печати
[TestClass] public class PrintingApiTests { protected const string MonitorName = "mfilemon"; protected const string PortName = "TESTPORT:"; protected const string DriverName = "Test Driver"; protected const string PrinterName = "Test Printer"; protected const string MonitorFile = "D:/Printing Tests/mfilemon.dll"; protected const string DriverFile = "D:/Printing Tests/pscript5.dll"; protected const string DriverDataFile = "D:/Printing Tests/testprinter.ppd"; protected const string DriverConfigFile = "D:/Printing Tests/ps5ui.dll"; protected const string DriverHelpFile = "D:/Printing Tests/pscript.hlp"; [TestMethod] public void PrinterInstallationTest() { PrintingApi.TryRestart(); Monitor monitor = PrintingApi.Factory.CreateMonitor(MonitorName, MonitorFile); Port port = PrintingApi.Factory.OpenPort(PortName, monitor); Driver driver = PrintingApi.Factory.InstallDriver(DriverName, DriverFile, DriverDataFile, DriverConfigFile, DriverHelpFile, 3, Environment.Current, DataType.RAW, null, monitor); Printer printer = PrintingApi.Factory.RunPrinter(PrinterName, port, driver); PrintingApi.TryRestart(); Assert.IsNotNull(printer); } }


Обратите внимание, практически все нативные методы Spooler блокируют поток, в котором они вызываются, не забывайте проводить операции с нашим API в асинхронном режиме, дабы избежать подвисания главного STA-потока UI.
Работа с данными
После установки виртуального принтера в систему, необходимо сконфигурировать монитор. Здесь всё зависит от спецификации монитора, для этого нужно изучать его документацию. Конкретно в нашем случае, mfilemon.dll конфигурируется с помощью реестра:
string monitorName = "mfilemon"; string portName = "TESTPORT:"; string keyName = $"SYSTEM\\CurrentControlSet\\Control\\Print\\Monitors\\{monitorName}\\{portName}"; Registry.LocalMachine.CreateSubKey(keyName); using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey(keyName, true)) { regKey.SetValue("OutputPath", "D:/Printing Tests/", RegistryValueKind.String); regKey.SetValue("FilePattern", "%r_%c_%u_%Y%m%d_%H%n%s_%j.ps", RegistryValueKind.String); regKey.SetValue("Overwrite", 0, RegistryValueKind.DWord); regKey.SetValue("UserCommand", string.Empty, RegistryValueKind.String); regKey.SetValue("ExecPath", string.Empty, RegistryValueKind.String); regKey.SetValue("WaitTermination", 0, RegistryValueKind.DWord); regKey.SetValue("PipeData", 0, RegistryValueKind.DWord); }
Теперь в каталоге «D:/Printing Tests/» будут появляться уже сформированные PostScript-файлы описания страниц, мы можем делать с ними что угодно, хоть преобразовать в текстовый формат или PDF, распарсив по старинке (упаси Боже) или воспользовавшись средствами GhostScript, хоть переслать на сервер, хоть загрузить в память. Осталось только перехватить момент создания файла после печати, для этого в System.IO предусмотрен класс FileSystemWatcher, который следит за изменением состояния файловой системы и может вызывать наши обработчики событий:
// Инициализируем, указываем наш каталог для слежения за изменением состояния, указываем файловую маску, отфильтровываем ненужные уведомления об изменении состояния директории. FileSystemWatcher fileSystemWatcher = new FileSystemWatcher("D:/Printing Tests/", "*.ps") { NotifyFilter = NotifyFilters.DirectoryName }; fileSystemWatcher.NotifyFilter = fileSystemWatcher.NotifyFilter | NotifyFilters.FileName; fileSystemWatcher.NotifyFilter = fileSystemWatcher.NotifyFilter | NotifyFilters.Attributes; fileSystemWatcher.Created += new FileSystemEventHandler(PrinterHandler); // Подписываемся на событие создания файла. try { fileSystemWatcher.EnableRaisingEvents = true; // Активируем события. } catch (ArgumentException e) { }
Обработчик события выглядит примерно следующим образом:
void PrinterHandler(object sender, FileSystemEventArgs e) { // Проверяем тип изменения состояния. switch (e.ChangeType) { // Событие создания файла. В этом ветвлении так же можно задать и другие события, при необходимости. case WatcherChangeTypes.Created: try { // TODO: Здесь желательно сделать ожидание разблокировки файла, если он по каким-либо причинам занят (повысит отказоустойчивость). byte[] fileData = File.ReadAllBytes(e.FullPath); // У нас есть полный путь к нашему файлу, а значит здесь мы можем делать с ним что захотим. // Здесь мы можем обработать полученные данные. File.Delete(e.FullPath); // По завершению мы можем тут же удалить файл, если он больше не нужен. } catch (Exception ex) { } break; } }
Собственно, на этом нашу задачу можно считать полностью решённой.
Заключение
Как и всегда, этой статьёй я ни на что не претендую. Сегодня я попытался объяснить «на пальцах» решение довольно специфической (но, как показал опыт последних пары лет — всё ещё актуальной) задачи средствами языка C#, настолько доходчиво и подробно, насколько это было для меня возможно. Надеюсь, статья была для Вас полезной и Вы не потратили время попусту.
Изложенного выше материала должно с лихвой хватить для написания высокоуровневых хэлперов над неуправляемым кодом практически любой сложности, а так же базового понимания работы службы печати Spooler.
Исходный код с приложенными Unit-тестами к проекту можно найти здесь. NuGet-пакет для использования в своих проектах доступен здесь.
Благодарю за внимание!
