В жизни каждого человека рано или поздно настает момент, когда требуется создать приложение, сворачивающееся в трей (системная панелька, "где часики"). Сейчас мы посмотрим, как это можно насишарпить© в WPF.
Термин «свернуть в трей» не совсем правильный с точки зрения программирования. На самом деле окно никуда не сворачивается, оно просто перестает отображаться на экране. За эффект «сворачивания» отвечает API-функция drawAnimatedRects(), которая рисует анимацию окна из одного прямоугольника на экране, в другой произвольный прямоугольник; если приглядеться, XP даже не утруждала себя прорисовкой содержимого окна, там только заголовок и чисто символический бордюр. При желании, конечным прямоугольником, в который «улетит» окно, можно сделать системный трей или даже свою иконку в нем, но в данном примере мы не будем заморачиваться на спецэффектах, а только построим необходимый и достаточный интерфейс пользователя.
Иконка же в трее существует сама по себе, вне зависимости от текущего состояния «сворачивания» или «разворачивания» окна. Она добавляется туда другими функциями Windows. Помню, в Delphi для этого нужно было вызывать чистый API (хотя, наверное, позже кто-нибудь догадался накидать самодостаточный компонент), в Windows.Forms для этого есть готовый класс NotifyIcon, а в WPF решили вообще не отвлекаться на эту тему.
Первым делом, давайте договоримся, что постоянно отображать и прятать иконку в трее в зависимости от того, развернуто окно или свернуто, смысла нет.
Раньше так делали, когда трей был «не резиновый», и каждый, кто в него «понаехал», приближал смерть панели задач. В ХР проблему заполняемости лотка почти решили, добавив анимацию навроде баяна, которая может прятать лишние иконки, освобождая место в панели задач под приложения первой необходимости. Но это постоянно приходилось мучительно настраивать (на что пойдет не каждый пользователь), да и при «раскрытии мехов баяна» в панели задач творилось черти что... В семерке трей наконец-то научили разворачиваться вверх, т.е. теперь без особого на то желания пользователя, иконки лотка вообще никак и никогда не повлияют на панель, для них не предназначенную.
Поэтому мы просто создадим иконку в начале работы приложения, а в конце уберем ее оттуда.
Иконку я стащил из первой попавшейся библиотеки Windows, сохранил, как файл .ico, и положил в ресурсы приложения под именем Icon1 (в Solution Explorer нужно открыть Properties - Resources.resx, слева вверху кнопка Add Resource... - Add Existing File...). В реальной жизни вы, конечно, поступите умнее меня и дадите этому ресурсу осмысленное имя.
По правилам хорошего тона, иконка в трее должна по щелчку левой кнопкой мыши показывать приложение, а по щелчку правой кнопкой - отображать какое-нибудь меню. Без меню пользователь чувствует себя неуютно, постоянно оглядывается через плечо, недоумевает и в конце концов сходит с ума или деинсталлирует ваше приложение. Понятно, что второй вариант нас не устраивает, поэтому создадим меню в XAML-файле главного окна приложения:
ContextMenu>
Window.Resources>
Как видно из кода, мы создаем экземпляр контекстного меню и храним его в ресурсах окна (x:Key необходим, чтобы потом достать этот экземпляр из кода). Первый пункт будет отвечать за показ или скрытие окна (для тех, кто любит все делать через пункты меню, а не через абстрактные щелчки по иконке); второй пункт символизирует все ваши пункты меню с функционалом, специфичным для вашего приложения; последний пункт отделен сепаратором, он будет выполнять выход из приложения. Отсутствие такого пункта меню (именно последнего в списке) раздражает пользователей чуть ли не сильнее ранее упомянутой мною причины деинсталляции. Чтобы прочувствовать, попробуйте менюшку от аутлука 2003, например. Там нет выхода. Т.е., прежде, чем вы сможете выгрузить приложение из памяти, вы обязаны отобразить его на экране. Это лишний шаг, глупость и нарушение конфиденциальности в некоторых случаях.
Итак, меню и иконка у нас есть. Запихиваем все это в трей в момент запуска приложения, (здесь и далее) правим класс главного окна приложения в файле MainWindow.xaml.cs:
// переопределяем обработку первичной инициализации приложения
protected override void OnSourceInitialized(EventArgs e) {
base.OnSourceInitialized(e); // базовый функционал приложения в момент запуска
createTrayIcon(); // создание нашей иконки
}
Сама иконка создается на основе класса Windows.Forms.NotifyIcon (для этого нужно добавить Windows.Forms в References проекта: Solution Explorer - References ткнуть правой кнопкой мыши, из контекстного меню выбрать Add Reference..., на закладке .Net указать на Windows.Forms). Можно было бы спуститься из WPF на уровень Windows API и создать иконку там, но в итоге мы не получили бы никакого выигрыша, кроме дополнительной работы вручную (ну ты понел, на что это похоже). Абсолютно все это уже реализовано в Windows.Forms, а без ссылки на это пространство имен мы все равно к API-функциям не спустимся.
private System.Windows.Forms.NotifyIcon TrayIcon = null;
private ContextMenu TrayMenu = null;
private bool createTrayIcon() {
bool result = false;
if(TrayIcon == null) { // только если мы не создали иконку ранее
TrayIcon = new System.Windows.Forms.NotifyIcon(); // создаем новую
TrayIcon.Icon = YourAppNamespace.Properties.Resources.icon1; // изображение для трея
// обратите внимание, за ресурсом с картинкой мы лезем в свойства проекта, а не окна,
// поэтому нужно указать полный namespace
TrayIcon.Text = "Here is tray icon text."; // текст подсказки, всплывающей над иконкой
TrayMenu = Resources["TrayMenu"] as ContextMenu; // а здесь уже ресурсы окна и тот самый x:Key
// сразу же опишем поведение при щелчке мыши, о котором мы говорили ранее
// это будет просто анонимная функция, незачем выносить ее в класс окна
TrayIcon.Click += delegate(object sender, EventArgs e) {
if((e as System.Windows.Forms.MouseEventArgs).Button == System.Windows.Forms.MouseButtons.Left) {
// по левой кнопке показываем или прячем окно
ShowHideMainWindow(sender, null);
}
else {
// по правой кнопке (и всем остальным) показываем меню
TrayMenu.IsOpen = true;
Activate(); // нужно отдать окну фокус, см. ниже
}
};
result = true;
}
else { // все переменные были созданы ранее
result = true;
}
TrayIcon.Visible = true; // делаем иконку видимой в трее
return result;
}
В строке Activate() мы отдаем фокус главному окну приложения. Если вы этого не сделаете, менюшка в трее будет вести себя очень подозрительно. Она будет висеть там и ждать щелчка вечно. Вспомните, наверняка вы сталкивались с парой таких программ, меню которых отображаются над треем и ничем от них не избавишься, хоть куда мышью тыкай; а самое обидное, когда нельзя трогать ни один из пунктов этой маленькой заразы. Все всплывающие меню должны конечно же пропадать, когда пользователь теряет интерес к приложению и переключается на другое.
Строкой TrayIcon.Visible = true мы вызываем процедуру перерисовки иконки в трее. Она необходима для первичного показа иконки и не помешает в случае, если мы захотим использовать этот метод еще раз. Раньше многие операционные системы были склонны терять все содержимое лотка при вылете процесса explorer.exe, насчет XP и висты с семеркой точно не скажу, но на всякий случай оставим возможность перерисовать иконку без создания ее заново. Поэтому строка вынесена из блока проверки существования иконки. То же самое и с результатом функции. Если вы уверены, что вам это не надо, можно смело удалить.
Функция показа или скрытия главного окна:
private void ShowHideMainWindow(object sender, RoutedEventArgs e) {
TrayMenu.IsOpen = false; // спрячем менюшку, если она вдруг видима
if(IsVisible) {// если окно видно на экране
// прячем его
Hide();
// меняем надпись на пункте меню
(TrayMenu.Items[0] as MenuItem).Header = "Show";
}
else { // а если не видно
// показываем
Show();
// меняем надпись на пункте меню
(TrayMenu.Items[0] as MenuItem).Header = "Hide";
WindowState = CurrentWindowState;
Activate(); // обязательно нужно отдать фокус окну,
// иначе пользователь сильно удивится, когда увидит окно
// но не сможет в него ничего ввести с клавиатуры
}
}
Это обычный обработчик события OnClick (потому и набор аргументов такой) для любого элемента интерфейса. Мы его указали, как обработчик нажатия на верхний пункт меню. Он же использован в анонимной функции обработки щелчка по самой иконке.
Свойство CurrentWindowState описано в классе окна следующим образом:
private WindowState fCurrentWindowState = WindowState.Normal;
public WindowState CurrentWindowState {
get { return fCurrentWindowState; }
set { fCurrentWindowState = value; }
}
Оно нужно только затем, чтобы хранить текущее состояние окна (нормальное или развернутое на весь экран), когда мы прячем окно. В момент показа мы должны вернуть окно именно в это состояние. Почему нельзя воспользоваться вшитым в класс окна подобным свойством? Потому что там есть еще третье значение - минимизированное окно на панели задач, - которое нам совсем не нужно в момент возвращения из «спрятанного» состояния. При вызове демона из ада, вызывающий ожидает увидеть его перед собой, а не спрятанным в шкафу. То же самое и с окном. В этом месте текста аналогия с вызовом проституток показалась мне какой-то надуманной, поэтому я от нее отказался.
Итак, напишем маленькую обработку, которая будет убирать вкладку окна с панели задач, если окно минимизировано. Это типичное поведение для приложений, требующих наличия иконки в трее.
// переопределяем встроенную реакцию на изменение состояния сознания окна
protected override void OnStateChanged(EventArgs e) {
base.OnStateChanged(e); // системная обработка
if(this.WindowState == System.Windows.WindowState.Minimized) {
// если окно минимизировали, просто спрячем
Hide();
// и поменяем надпись на менюшке
(TrayMenu.Items[0] as MenuItem).Header = "Show";
}
else {
// в противном случае запомним текущее состояние
CurrentWindowState = WindowState;
}
}
Здесь свойство окна WindowState конфликтует с одноименным типом из System.Windows.WindowState, поэтому я указал полный namspace и this. Среда от вас потребует только первого. А пятница первого, второго, пива и покурить.
Давайте еще научим приложение «сворачиваться в трей» и вместо закрытия окна. Это тоже типичное ожидаемое поведение для таких приложений. Выход из них обычно осуществляется с помощью нижнего пункта меню в трее (помните аутлук?) или с помощью пункта «Выход» в главном меню «Файл» (создать такой пункт вы можете самостоятельно в XAML-описании главного окна).
private bool fCanClose = false;
public bool CanClose { // флаг, позволяющий или запрещающий выход из приложения
get { return fCanClose; }
set { fCanClose = value; }
}
// переопределяем обработчик запроса выхода из приложения
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) {
base.OnClosing(e); // встроенная обработка
if(!CanClose) { // если нельзя закрывать
e.Cancel = true; выставляем флаг отмены закрытия
// запоминаем текущее состояние окна
CurrentWindowState = this.WindowState;
// меняем надпись в менюшке
(TrayMenu.Items[0] as MenuItem).Header = "Show";
// прячем окно
Hide();
}
else { // все-таки закрываемся
// убираем иконку из трея
TrayIcon.Visible = false;
}
}
Боже мой! Теперь мы никогда не сможем покинуть приложения! CanClose никогда не обратится в истину!
Не нужно паниковать. Вы же помните, что в самом начале последний пункт нашего контекстного меню ссылался на обработчик MenuExitClick(). На него же будет ссылаться и тот пункт главного меню, который вы создадите сами. А вот и его сложнейшая реализация:
private void MenuExitClick(object sender, RoutedEventArgs e) {
CanClose = true;
Close();
}
Итак, мы создали типовое приложение, работающее с треем, которое можно использовать как шаблон для других WPF-решений.
Приложение умеет:
1. Держать свою иконку в трее.
2. По щелчку на иконке левой кнопкой мыши прятать или показывать главное окно.
3. По щелчку на иконке правой кнопкой мыши отображать контекстное меню приложения, в котором реализованы обязательные пункты «Скрыть / Показать» и «Выход».
4. Прятать главное окно при его сворачивании.
5. Прятать главное окно вместо его закрытия.
Попутно реализованные вкусняшки:
1. Где только можно использован механизм WPF, даже в контекстом меню трея, которое мы чуть не потеряли, спускаясь на уровень Windows.Forms; а следовательно можно продолжать повсеместно использовать охрененную мощь WPF в построении интерфейса.
2. Необходимые обработчики реализованы переопределением стандартных protected-методов класса окна, таким образом не нужно помнить о них при навешивании своих обработок на события, предоставляемые окном в обычном приложении.
Скриншот пустого приложения с иконкой в трее и менюшкой, построенной на WPF:
Все про ремонт квартиры и офисов.