|
При выполнении в лабораторной работе упражнения №1 , а именно при выполнении нижеследующего кода: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using Microsoft.Xna.Framework.Graphics;
namespace Application1 { public partial class MainForm : Form { // Объявим поле графического устройства для видимости в методах GraphicsDevice device;
public MainForm() { InitializeComponent();
// Подпишемся на событие Load формы this.Load += new EventHandler(MainForm_Load);
// Попишемся на событие FormClosed формы this.FormClosed += new FormClosedEventHandler(MainForm_FormClosed); }
void MainForm_FormClosed(object sender, FormClosedEventArgs e) { // Удаляем (освобождаем) устройство device.Dispose(); // На всякий случай присваиваем ссылке на устройство значение null device = null; }
void MainForm_Load(object sender, EventArgs e) { // Создаем объект представления для настройки графического устройства PresentationParameters presentParams = new PresentationParameters(); // Настраиваем объект представления через его свойства presentParams.IsFullScreen = false; // Включаем оконный режим presentParams.BackBufferCount = 1; // Включаем задний буфер // для двойной буферизации // Переключение переднего и заднего буферов // должно осуществляться с максимальной эффективностью presentParams.SwapEffect = SwapEffect.Discard; // Устанавливаем размеры заднего буфера по клиентской области окна формы presentParams.BackBufferWidth = this.ClientSize.Width; presentParams.BackBufferHeight = this.ClientSize.Height;
// Создадим графическое устройство с заданными настройками device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, this.Handle, presentParams); }
protected override void OnPaint(PaintEventArgs e) { device.Clear(Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue);
base.OnPaint(e); } } } Выбрасывается исключение: Невозможно загрузить файл или сборку "Microsoft.Xna.Framework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d" или один из зависимых от них компонентов. Не удается найти указанный файл. Делаю все пунктуально. В чем может быть проблема? |
События и команды в WPF
Размещение вариантов заголовков окна в ресурсах приложения
В коде вспомогательных функций используется словарь ресурсов приложения, которые мы сейчас определим.
-
Откройте файл разметки App.xaml и дополните его определением ресурсов для хранения неизменяемой части
заголовка окна. Областью видимости этих ресурсов будет все приложение
<Application x:Class="Notepad1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml">
<Application.Resources>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle1">
Title="Window1: Управление состоянием источников команд"
</String>
<String xmlns="clr-namespace:System;assembly=Mscorlib" x:Key="ApplicationTitle2">
Title="Window2: Управление состоянием источников команд"
</String>
</Application.Resources>
</Application>Создание заготовок обработчиков
Будем мы использовать механизм команд WPF или нет, но без событий и их обработчиков никак не обойтись. Начнем с того факта, что для решения наших задач можно создать обработчики с одинаковой сигнатурой, определяемой делегатом пространства имен System.Windows:
public delegate void RoutedEventHandler(object sender, RoutedEventArgs e)
Имена обработчикам будем присваивать в соответствии с решаемой ими задачей. Обработчики создадим вручную и их необязательно сразу присоединять к событиям источников, поскольку неприсоединенные обработчики будут считаться обычными методами класса. Наиболее полно все задачи представлены в главном меню приложения, поэтому для каждой из них нужно создать свою группу обработчиков. Выполним все это по порядку, размещая группы обработчиков в соответствующих частях класса Window1.
-
Добавьте в часть класса Window1, расположенную в файле File.cs, следующие пустые обработчики
по количеству задач меню в разделе File
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач File
//
//------------------------------------------------------
private void NewOnExecute(object sender, RoutedEventArgs e)
{
}
private void OpenOnExecute(object sender, RoutedEventArgs e)
{
}
private void SaveOnExecute(object sender, RoutedEventArgs e)
{
}
private void SaveAsOnExecute(object sender, RoutedEventArgs e)
{
}
private void PageSetupOnExecute(object sender, RoutedEventArgs e)
{
}
private void PrintPreviewOnExecute(object sender, RoutedEventArgs e)
{
}
private void PrintOnExecute(object sender, RoutedEventArgs e)
{
}
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
}
}
}-
Добавьте в часть класса Window1, расположенную в файле Edit.cs, следующие пустые обработчики
по количеству задач меню в разделе Edit
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач Edit
//
//------------------------------------------------------
private void UndoOnExecute(object sender, RoutedEventArgs e)
{
}
private void RedoOnExecute(object sender, RoutedEventArgs e)
{
}
private void CutOnExecute(object sender, RoutedEventArgs e)
{
}
private void CopyOnExecute(object sender, RoutedEventArgs e)
{
}
private void PasteOnExecute(object sender, RoutedEventArgs e)
{
}
private void DeleteOnExecute(object sender, RoutedEventArgs e)
{
}
private void FindOnExecute(object sender, RoutedEventArgs e)
{
}
private void FindNextOnExecute(object sender, RoutedEventArgs e)
{
}
private void ReplaceOnExecute(object sender, RoutedEventArgs e)
{
}
private void GoToOnExecute(object sender, RoutedEventArgs e)
{
}
private void SelectAllOnExecute(object sender, RoutedEventArgs e)
{
}
}
}-
Добавьте в часть класса Window1, расположенную в файле Other.cs, следующие пустые обработчики
по количеству задач меню в оставшихся разделах
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Прочие обработчики
//
//------------------------------------------------------
private void FontOnExecute(object sender, RoutedEventArgs e)
{
}
private void WordWrapOnExecute(object sender, RoutedEventArgs e)
{
}
private void AboutOnExecute(object sender, RoutedEventArgs e)
{
}
}
}-
Запустите приложение
и убедитесь, что ошибок компиляции нет, но функциональности от выполненной
нами работы в нем пока не прибавилось
Регистрация обработчиков в разметке
Мы подготовили интерфейс окна Window1 и дальше наступает этап кодирования функциональности. В окне Window1 мы откажемся от услуг механизма команд, а будем использовать обычную технологию, основанную на обработчиках событий. Начнем постепенно подключать обработчики к источникам задач, используя событие Click.
-
Подключите декларативно
в файле разметки Window1.xaml к каждому источнику события Click соответствующий
обработчик и назначьте имена элементам для последующего управления ими из
кода
Обратите внимание на некоторую некорректность работы подсказчика кода IntelliSense, который не предлагает нам список уже созданных обработчиков. Он ищет их в застраничном файле Window1.xaml.cs, в то время как обработчики находятся в других файлах частичного класса. В данном случае это не слишком большое неудобство, поскольку мнемоника составления имен у нас строго соблюдена и мы вряд ли ошибемся. Но в больших практических проектах это следует иметь ввиду, прежде чем разбивать класс окна по отдельным файлам.
Разметка с подключенными обработчиками и присвоенными именами будет выглядеть так (файл приводится целиком)
<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
>
<Window.Resources>
<!-- File -->
<Image x:Shared="False" x:Key="iconNew" Source="Images/NewDocumentHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconOpen" Source="Images/OpenHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconSave" Source="Images/SaveHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPageSetup" Source="Images/PrintSetupHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrintPreview" Source="Images/PrintPreviewHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPrint" Source="Images/PrintHS.png" Width="16" Height="16" />
<!-- Edit -->
<Image x:Shared="False" x:Key="iconUndo" Source="Images/Edit_UndoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconRedo" Source="Images/Edit_RedoHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCut" Source="Images/CutHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconCopy" Source="Images/CopyHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconPaste" Source="Images/PasteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconDelete" Source="Images/DeleteHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFind" Source="Images/FindHS.png" Width="16" Height="16" />
<Image x:Shared="False" x:Key="iconFont" Source="Images/FontHS.png" Width="16" Height="16" />
</Window.Resources>
<DockPanel LastChildFill="True">
<!-- Меню -->
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<!-- Сокращенные варианты подключения иконок с использованием статических ресурсов -->
<MenuItem Name="itemNew" Click="NewOnExecute" Header="_New" InputGestureText="Ctrl+N"
Icon="{StaticResource iconNew}" />
<MenuItem Name="itemOpen" Click="OpenOnExecute" Header="_Open..." InputGestureText="Ctrl+O"
Icon="{StaticResource iconOpen}" />
<MenuItem Name="itemSave" Click="SaveOnExecute" Header="_Save" InputGestureText="Ctrl+S"
Icon="{StaticResource iconSave}" />
<MenuItem Name="itemSaveAs" Click="SaveAsOnExecute" Header="Save _As..." />
<Separator />
<MenuItem Name="itemPageSetup" Click="PageSetupOnExecute"
Header="Page Set_up..." Icon="{StaticResource iconPageSetup}" />
<MenuItem Name="itemPrintPreview" Click="PrintPreviewOnExecute" Header="P_rint Preview"
InputGestureText="Ctrl+F2" Icon="{StaticResource iconPrintPreview}" />
<MenuItem Name="itemPrint" Click="PrintOnExecute"
Header="_Print..." InputGestureText="Ctrl+P" Icon="{StaticResource iconPrint}" />
<Separator />
<MenuItem Name="itemExit" Click="ExitOnExecute" Header="E_xit" />
</MenuItem>
<MenuItem Header="_Edit">
<MenuItem Name="itemUndo" Click="UndoOnExecute"
Header="_Undo" InputGestureText="Ctrl+Z" Icon="{StaticResource iconUndo}" />
<MenuItem Name="itemRedo" Click="RedoOnExecute"
Header="_Redo" InputGestureText="Ctrl+Y" Icon="{StaticResource iconRedo}" />
<Separator></Separator>
<MenuItem Name="itemCut" Click="CutOnExecute"
Header="Cu_t" InputGestureText="Ctrl+X" Icon="{StaticResource iconCut}" />
<MenuItem Name="itemCopy" Click="CopyOnExecute"
Header="_Copy" InputGestureText="Ctrl+C" Icon="{StaticResource iconCopy}" />
<MenuItem Name="itemPaste" Click="PasteOnExecute"
Header="_Paste" InputGestureText="Ctrl+V" Icon="{StaticResource iconPaste}" />
<MenuItem Name="itemDelete" Click="DeleteOnExecute"
Header="De_lete" InputGestureText="Del" Icon="{StaticResource iconDelete}" />
<Separator></Separator>
<MenuItem Name="itemFind" Click="FindOnExecute"
Header="_Find..." InputGestureText="Ctrl+F" Icon="{StaticResource iconFind}" />
<MenuItem Name="itemFindNext" Click="FindNextOnExecute"
Header="Find _Next" InputGestureText="F3" />
<MenuItem Name="itemReplace" Click="ReplaceOnExecute"
Header="_Replace..." InputGestureText="Ctrl+H" />
<MenuItem Name="itemGoTo" Click="GoToOnExecute"
Header="_Go To..." InputGestureText="Ctrl+G" />
<Separator></Separator>
<MenuItem Name="itemSelectAll" Click="SelectAllOnExecute"
Header="Select _All" InputGestureText="Ctrl+A" />
</MenuItem>
<MenuItem Header="F_ormat">
<MenuItem Name="itemFont" Click="FontOnExecute"
Header="_Font..." Icon="{StaticResource iconFont}" />
<Separator />
<MenuItem Name="itemWordWrap" Click="WordWrapOnExecute"
Header="_Word Wrap" IsCheckable="True" IsChecked="True" InputGestureText="Ctrl+W" />
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Name="itemAbout" Click="AboutOnExecute"
Header="_About" />
</MenuItem>
</Menu>
<!-- Панель инструментов -->
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Button Name="btnNew" Click="NewOnExecute" Width="23" Content="{StaticResource iconNew}" />
<Button Name="btnOpen" Click="OpenOnExecute" Width="23" Content="{StaticResource iconOpen}" />
<Button Name="btnSave" Click="SaveOnExecute" Width="23" Content="{StaticResource iconSave}" />
</ToolBar>
<ToolBar>
<Button Name="btnUndo" Click="UndoOnExecute" Width="23" Content="{StaticResource iconUndo}" />
<Button Name="btnRedo" Click="RedoOnExecute" Width="23" Content="{StaticResource iconRedo}" />
<Separator />
<Button Name="btnCut" Click="CutOnExecute" Width="23" Content="{StaticResource iconCut}" />
<Button Name="btnCopy" Click="CopyOnExecute" Width="23" Content="{StaticResource iconCopy}" />
<Button Name="btnPaste" Click="PasteOnExecute" Width="23" Content="{StaticResource iconPaste}" />
<Button Name="btnDelete" Click="DeleteOnExecute" Width="23" Content="{StaticResource iconDelete}" />
</ToolBar>
<ToolBar Header="Find:">
<TextBox Width="100" />
<Button Name="btnFind" Click="FindOnExecute" Width="23" Content="{StaticResource iconFind}" />
</ToolBar>
</ToolBarTray>
<!-- Строка состояния -->
<StatusBar DockPanel.Dock="Bottom" Height="32" Name="statusBar">
<Label>Simulator Application is Loading</Label>
<Separator />
<ProgressBar Height="20" Width="100" IsIndeterminate="True" />
</StatusBar>
<!-- Многострочное текстовое поле редактирования -->
<TextBox TextWrapping="Wrap"
AcceptsReturn="True"
AcceptsTab="True"
VerticalScrollBarVisibility="Auto"
Name="txtBox1"
>
<TextBox.ContextMenu>
<ContextMenu Width="100">
<MenuItem Name="contextCut" Click="CutOnExecute"
Header="Cu_t" Icon="{StaticResource iconCut}" />
<MenuItem Name="contextCopy" Click="CopyOnExecute"
Header="_Copy" Icon="{StaticResource iconCopy}" />
<MenuItem Name="contextPaste" Click="PasteOnExecute"
Header="_Paste" Icon="{StaticResource iconPaste}" />
<MenuItem Name="contextDelete" Click="DeleteOnExecute"
Header="De_lete" Icon="{StaticResource iconDelete}" />
</ContextMenu>
</TextBox.ContextMenu>
</TextBox>
</DockPanel>
</Window>Столько имен нам не понадобиться, но для единообразия мы, на всякий случай, промаркировали все источники задач (может пригодится).
-
Запустите приложение
для проверки отсутствия синтаксических ошибок
Следующим шагом следует наполнить созданные заготовки обработчиков практическим кодом в соответствии с логикой работы приложения. Поскольку наша задача состоит не в создании полнофункционального приложения целиком, а в иллюстрации технологии его создания с применением механизма команд, реализуем намеченные задачи частично. Некоторые функции, ввиду их большого объема и сложности кодирования, мы только обозначим выдачей соответствующих диалоговых окон или сообщений. Управление доступностью элементов-источников задач пользовательского интерфейса пока отложим 'на потом' (или навсегда!).
Реализация обработчиков раздела меню File
-
Выделите корень проекта Notepad1 и командой Project/Add Reference подключите сборки System.Windows.Forms.dll и System.Drawing.dll
-
Модифицируйте код файла File.cs следующим образом
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
// Для сборок: System.Windows.Forms.dll, System.Drawing.dll
using sdp = System.Drawing.Printing;// Псевдоним пространства имен
using swf = System.Windows.Forms; // Псевдоним пространства имен
using PageSetupDialog = System.Windows.Forms.PageSetupDialog;// Псевдоним класса
namespace Notepad1
{
partial class Window1
{
//------------------------------------------------------
//
// Обработчики источников задач File
//
//------------------------------------------------------
private void NewOnExecute(object sender, RoutedEventArgs e)
{
// Пользователь передумал или была ошибка записи изменений
if (!CheckModifiedAndSaveIt())
return;
// Изменений нет или они успешно сохранены
//txtBox1.Text = String.Empty; // Вариант I
//txtBox1.Text = ""; // Вариант II
txtBox1.Clear(); // Вариант III
strLoadedFile = null;
IsModified = false;
UpdateTitle();
txtBox1.Focus();
}
private void OpenOnExecute(object sender, RoutedEventArgs e)
{
if(DisplayOpenDialog())
txtBox1.CaretIndex = txtBox1.Text.Length;// Курсор в конец
txtBox1.Focus();// Передача фокуса
}
private void SaveOnExecute(object sender, RoutedEventArgs e)
{
if (String.IsNullOrEmpty(strLoadedFile))
DisplaySaveDialog(String.Empty);
else
SaveFile(strLoadedFile);
txtBox1.Focus();
}
private void SaveAsOnExecute(object sender, RoutedEventArgs e)
{
DisplaySaveDialog(strLoadedFile);
txtBox1.Focus();
}
private void PageSetupOnExecute(object sender, RoutedEventArgs e)
{
// Ограничемся только показом окна Windows Forms
PageSetupDialog dlg = new PageSetupDialog();
// Без настроек не работает. Зададим хотя бы по умолчанию
dlg.PageSettings = new sdp.PageSettings();
dlg.PrinterSettings = new sdp.PrinterSettings();
dlg.ShowDialog();
txtBox1.Focus();
}
private void PrintPreviewOnExecute(object sender, RoutedEventArgs e)
{
sdp.PrintDocument document = new sdp.PrintDocument();
document.DocumentName = strLoadedFile;
swf.PrintPreviewDialog dlg = new swf.PrintPreviewDialog();
dlg.Document = document;
dlg.UseAntiAlias = true;// Включить сглаживание
dlg.ShowDialog();
txtBox1.Focus();
}
private void PrintOnExecute(object sender, RoutedEventArgs e)
{
sdp.PrintDocument document = new sdp.PrintDocument();
document.DocumentName = strLoadedFile;
swf.PrintDialog dlg = new swf.PrintDialog();
dlg.Document = document;
dlg.ShowDialog();
txtBox1.Focus();
}
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
if (!CheckModifiedAndSaveIt())
return; // Пользователь передумал выходить
Close();
}
}
}Обратите внимание на использование псевдонимов пространств имен добавленных к проекту библиотечных сборок. Еще раз вспомним, что при совместном применении пользовательских интерфейсов WPF и Windows Forms имена типов одной из технологий следует прописывать полностью или использовать псевдонимы. Иначе в коде могут возникнуть конфликты имен, а если компилятор их не обнаружит, то и серьезные ошибки времени выполнения.
-
Запустите приложение,
испытайте правильность функционирования обработчиков, изучите их код
Обработка системной кнопки
Пока в приложение закрался один существенный недостаток. Когда документ содержит несохраненные изменения и пользователь завершает работу приложения по нашей команде Exit, то все в порядке - приложение извещает о необходимости их сохранить. Но когда окно закрывается по системной кнопке, то извещение отсутствует. Исправим это, для чего воспользуемся событием Closing. В отличии от Closing событие Closed возбудается, когда отменить закрытие окна уже невозможно, а можно только что-то доделать.
-
Присоедините к открывающему
дескриптору окна Window1 в файле Window1.xaml обработчик события Closing,
которое будет возбуждаться при попытке закрытия окна любым способом
<Window x:Class="Notepad1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1: Управление состоянием источников команд"
Width="500" Height="375"
MinWidth="500" MinHeight="375"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResizeWithGrip"
Loaded="Window_Loaded"
Icon="Notepad.ico"
Closing="Window_Closing"
>-
Заполните обработчик
события Closing следующим кодом
private void Window_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
if (!CheckModifiedAndSaveIt())
{
e.Cancel = true;
return; // Пользователь передумал выходить
}
}-
Запустите приложение
- теперь сообщение о несохраненных изменениях выводится и при попытке закрытия
окна системной кнопкой
Обратите внимание, что при закрытии окна через наше меню задачей Exit на кнопку отказа от сохранения надо щелкать 2 раза. Это связано с тем, что в обработчике события Closing проверка повторяется. Значит при выходе через меню проверку в обработчике события Closing нужно блокировать с помощью флага.
-
Добавьте в класс Window1 поле-флаг _IsExitItem и модифицируйте соответствующим образом обработчики
bool _IsExitItem = false;
private void ExitOnExecute(object sender, RoutedEventArgs e)
{
if (!CheckModifiedAndSaveIt())
return; // Пользователь передумал выходить
_IsExitItem = true;
Close();
}private void Window_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
/*
// Эта проверка была бы надежнее
if(_IsExitItem)
return;
*/
// !_IsExitItem должен в условии стоять первым
if (!_IsExitItem && !CheckModifiedAndSaveIt())
{
e.Cancel = true;
_IsExitItem = false;
return; // Пользователь передумал выходить
}
}В последнем обработчике есть один поучительный нюанс: если в условии проверку значения флага поставить последним, то код будет реагировать на наши нововведения. Это происходит потому, что логическое умножение проверяется компилятором слева направо до первого ложного значения. В нашем случае если флаг _IsExitItem==true, то функция CheckModifiedAndSaveIt() выполняться уже не будет. Такой код менее надежен, поскольку зависит от компилятора, да и мы (или сопровождающий программист) можем случайно переставить члены выражения местами. Поэтому лучше заменить этот код на более ясный, как показано в коментариях.
-
Запустите приложение
и испытайте работу закрытия окна при несохраняемых изменениях