Я постоянно имею дело с консольными программами, которые используются для хостинга служб WCF или .NET Remoting. Для отладки это самый удобный вариант: запускается быстро, консоль при случае используется для отладочной печати, и так далее. Выглядит такая программа обычно как-нибудь так (в случае с Remoting):
static void Main(string[] args)
{
// Запускаем сервер
var config = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
RemotingConfiguration.Configure(config, false);
// Работаем, пока не нажмут Enter
Console.WriteLine("Server started. Press ENTER to quit.");
Console.ReadLine();
}
Запустил, минимизировал такое окошко и крутится там себе сервер на фоне. Частенько бывает, что таких серверов запущено несколько (штуки три-четыре), и они постоянно торчат на панели задач. Изредка развернешь, чтобы прочитать, что там на консоли напечатано, а большую часть времени они только зря место занимают. Мне, честно говоря, это никогда особо не мешало. Пару недель назад один мой товарищ в разговоре заметил, что такие программки хорошо бы куда-нибудь прятать, оставляя одну иконку в системном трее. И с тех пор меня эти кнопки на панели задач стали раздражать.
Это такой мыслевирус, как кернинг: если человек не знает, что это такое, он живет себе спокойно. А если ему рассказать, да еще и показать на примере, чем плохой кернинг отличается от хорошего, — покой навеки утрачен. Теперь плохой кернинг постоянно будет ему бросаться в глаза на вывесках, объявлениях и рекламе в метро.
Короче говоря, пришлось-таки сделать небольшую библиотечку для сворачивания консоли в трей-иконку. Проблем тут, собственно, немного, но они есть (поэтому наивный вариант решения задачи — добавить компонент NotifyIcon с обработчиком DoubleClick, по которому что-нибудь там делать — не работает):
- В .NET Framework нету легального способа получить в свое распоряжение окно консоли
- В консольных приложениях нет конвейера сообщений
То есть, грубо говоря, добавить NotifyIcon можно (и в системном трее она будет нормально показываться), но обработчик события Click или DoubleClick вызываться не будет. Поскольку конвейера нет, событие просто некому переправить на обработку. А если и получится вызвать такой обработчик, непонятно, что в этом обработчике делать с консолью.
Чтобы справиться с этими проблемами, нужно 1) запустить свой конвейер сообщений в отдельном потоке и 2) заполучить хэндл окна консоли и прятать/показывать его средствами WinAPI. Выглядит так, как будто весь компонент будет состоять из сплошных вызовов p/invoke, то есть решение не будет блистать изяществом.
Однако выяснилось, что на деле не все так страшно. Оказывается, конвейеру WinForms — Application.Run(…) — вовсе не требуется главная форма. Он вполне сносно будет работать, если вместо этой формы ему подсунуть компонент NotifyIcon. То есть, первая часть задачи на самом деле сводится к запуску отдельного потока, в котором будет создаваться NotifyIcon и запускаться конвейер. И никакого p/invoke, что весьма приятно.
Хендл окна консоли, как выяснилось, иногда можно получить вот так: Process.GetCurrentProcess().MainWindowHandle. Здесь, разумеется, все портит слово «иногда»: на моем ноутбуке этот способ работает (Win7 x64), а на рабочем компьютере (WinXP x32) — нет. Вместо хендла возвращается IntPtr.Zero (MSDN говорит, что так задумано).
Так что для второй части задачи — прятать/показывать окно консоли — p/invoke все же нужен. Во-первых, нужен хендл консольного окна, во-вторых — метод ShowWindow(), который прячет или показывает окно по его хендлу. Весь WinAPI-мусор выносится в отдельный файл строчек примерно на 50 — ничего страшного. Жаль, правда, что портабельным этот вариант не будет, ну да хрен с ним, я и не претендовал на универсальность.
А дальше можно сделать приемлемую обертку, на иконку прицепить контекстное меню и заполнять его каким-нибудь fluent-интерфейсом:
using (var tools = new ConsoleTools())
{
// настраиваем иконку в системном трее
tools.SetNotifyIcon(SystemIcons.Shield)
.SetTooltip(Console.Title + ": double click to toggle visibility")
// добавляем обработчики событий (конвейер их обработает)
.OnClick((s, e) => Console.WriteLine("NotifyIcon clicked!"))
.OnDoubleClick((s, e) => tools.ConsoleVisible = !tools.ConsoleVisible)
// добавляем пункты контекстного меню
.AddMenuItem("About", SystemIcons.Question, ShowAboutBox)
.AddMenuSeparator()
.AddMenuItem("Hide console", (s, e) => tools.ConsoleVisible = false)
.AddMenuItem("Show console", (s, e) => tools.ConsoleVisible = true)
.AddMenuSeparator()
// метод CloseConsole() работает как нажатие кнопки [x] на окне консоли
.AddMenuItem("Close console",
SystemIcons.Error, (s, e) => tools.CloseConsole());
// А тут все как обычно, к примеру, RemotingConfiguration.Configure(...)
Console.WriteLine("Press Enter to exit...");
Console.ReadLine();
}
Остается только причесать немножко библиотеку и запустить ее куда-нибудь на CodePlex.