Блог → Как корректно завершить работу приложения в Win32?

Вопрос может показаться надуманным, но почти каждый, кто занимался программированием под Windows, рано или поздно сталкивался с этой проблемой (и странным он покажется, конечно же, только для тех, чей опыт работы с WinAPI невелик). Написать эту заметку меня побудили мои собственные мытарства, отсутствие полностью корректного решения вообще, а также большое количество так называемых "окончательных решений", разбросанных там и сям по различным FAQ, блогам, форумам, конференциям и т.д.

Читатели посерьезнее, наверно, уже напряглись, и думают - "ну-ну, почитаем, чего он тут насочинял... Microsoft то ведь давно уже написала, как закрывать приложения под Windows, и документик соответствующий в MSDN есть...". Всё так, и документик, конечно, есть. Называется "Howto: Terminate an Application 'Cleanly' in Win32" (Article ID: Q178893). Однако, во-первых, этот способ трудно назвать полноценным (он не решает проблему до конца), в во-вторых - не у всех, к сожалению, есть доступ к MSDN. К тому же, никогда не мешает разобраться в проблеме поглубже.

Надеюсь, для вас, дорогие мои читатели, не будут новостью строки из того самого документа, наиболее полно отражающие суть всех дальнейших выкрутасов: "Хотя нет гарантированного чистого пути, чтобы закрыть приложение в Win32, есть шаги, которые Вы можете предпринять, чтобы быть уверенным, что приложение использует наилучший метод для освобождения ресурсов". Этими шагами мы и займёмся. Примечание: код тестировался на Windows 95/98, для 32 и 16-ти разрядных процессов. Не консольных! Это вообще отдельная песня.

Итак: на какие шаги целесообразно разбить работу по закрытию приложения? В MSDN описано три этапа:
1. Отослать WM_CLOSE всем top-level окнам, принадлежащим процессу;
2. Подождать, при этом дать приложению достаточное время - чтобы, например, пользователь успел закрыть все модальные диалоги, которые программа вывела при закрытии;
3. Если приложение не закрылось, уничтожить его с помощью TerminateProcess.

Ну и какие проблемы, спросите вы? Всё дело в том, что вызова TerminateProcess надо избегать всеми способами. После такого "аварийного" завершения процесса в системе остается масса "мусора" - неосвобожденные ресурсы, индикаторы на панели задач, неочищенные области экрана и т.д.

Что же можно сделать еще? Ну, например - почему бы не пробежаться по потокам процесса, и не отослать каждому WM_QUIT? Ведь как только все потоки процесса закрыты (закрыт последний), завершается и процесс. В штатных ситуациях сообщение WM_QUIT генерируется, когда поток вызывает PostQuitMessage. Эта процедура помещает WM_QUIT в очередь сообщений потока, и не ждёт его обработки - просто указывает системе, что поток надо будет закрыть. Т.е. принудительная рассылка WM_QUIT фактически убивает поток "изнутри", мы выполняем те же действия, что и PostQuitMessage. Вот соответствующая процедура:

procedure KillPidThreads(PID:dword);
var {закрываем потоки, относящиеся к данному процессу}
ProcHandle: THandle;
aThreadEntry: ThreadENtry32;
begin
{Делаем "снимок" состояния системы}
ProcHandle := CreateToolHelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if ProcHandle = -1 then Exit;
aThreadEntry.dwSize := Size0f(ThreadENtry32);
if Thread32First(ProcHandle,aThreadEntry) then
begin
{если хозяин потока - наш процесс}
if aThreadEntry.th32OwnerProcessID = pid then
{то отсылаем WM_QUIT}
PostThreadMessage(aThreadEntry.th32ThreadID,WM_QUIT,0,0);
while Thread32next{ProcHandle,aThreadEntry) do
begin
{аналогично}
if athreadentry.th32OwnerProcessID = pid then
PostThreadMessage(aThreadEntry.th32ThreadID,WM_QUIT,0,0};
end;
end;
CloseHandle(ProcHandle);
end;


Таким вот образом (в простых случаях) можно эмулировать нормальное закрытие приложения; т.е. прикрыть примерно 99% приложений, окна которых не отвечают на WM_CLOSE. А что же оставшийся 1%? Это "упрямые" приложения - например, такие как IE. Ни закрытие окон, ни завершение потоков его не берёт.

Далее - не надо думать, что "мусор" в системе остается только в "чрезвычайных обстоятельствах", т.е. после вызова TerminateProcess. Такое может произойти, даже если приложение послушно закрылось по WM_CLOSE. Например, программа Netscape mail notification utility после "смерти" оставляет иконку на панели задач. В случае с панелью приложения (application bar) - после закрытия самого приложения может остаться занятой некоторая область экрана. Почему эти моменты не учтены в MSDN'овском примере?

Очень просто - пример, очевидно, дан для программ, "полностью отвечающих требованиям Microsoft", т.е. обязанных обрабатывать всевозможные события, учитывать нюансы работы, правильно вести себя в предусмотренных аварийных ситуациях, и т.д. Где вы видели такую программу, кроме как у Microsoft (да и то, кстати, с натяжкой)?

Итак - вот код, отвечающий за "принудительное" снятие панели приложения с учёта (что, естественно, освобождает занятую область экрана). Это действие совмещается с закрытием top-level окон, т.к. практически не влияет на выполнение основной процедуры их закрытия.

procedure KillByTopWindows(pid:dword);
begin
{закрываем все top-level окна, параллельно пытаясь снять панель для каждого окна}
EnumWindows(@enumwindouwscaliback,pid);
end;
function EnumWindowsCallback(window:hwnd;pid:dword):bool;stdcall;
var {вспомогательная callback-функция для EnumWindows}
winpid:dword;
appbar:appbardata;{Структура, отвечающая за панель}
begin
appbar.cbSize:=sizeof(appbar);
GetWindowThreadProcessID(window,@winpid); {получаем процесс (PIO) для окна}
if winpid = pid then
{если окно принадлежит нашему процессу}
begin
appbar.hWnd:=window; {указываем, что панель принадлежит закрываемому окну}
shappbarmessage(abm_remove,appbar); {пытаемся снять панель с учёта}
PostMessage(window,WM_CLOSE,0,0); {и отсылаем WM_CL0SE}
end;
result:=true;{nepeьиpaeм окна дальше...}
end;


Отдельно стоит вынести "снятие с учёта"» индикатора на панели задач, т.к. во-первых, этот момент можно проигнорировать, а во-вторых, он существенно замедляет работу. Замедляет, потому что, по всей видимости, нет никакого способа узнать идентификатор значка (кроме как перебором). Аналогично предыдущему:

procedure KillNotify(pid:dword);
begin {убираем знчок}
EnumWindows(@enumwindouwcallbacknotify,pid);
end;
function EnumWindowsCallbackNotify(window:hwnd;pid:dword):bool;stdcall;
var {вспомогательная callback-функция для EnumlWindows}
winpid:dword; {вcпoмoгaтeльнaя переменная}
NID:NotifyIcondata; {Структура, отвечающая за иконку}
cntr:integer; {Счётчик}
begin
result:=true;
NID.cbSize:=sizeof(nid);
GetWindowThreadProcessld(window,@winpid); {получаем процесс (PID) для окна}
if winpid = pid then {если окно принадлежит нашему процессу}
begin
result:=false;
nid.Wnd:=window; {Заполняем поле, отвечающее за окно-владельца}
{перебор (до описателя окна, и котя можно покрыть весь диапазон, но...)}
forcntr:=1 to window do
begin
NID.uID:=cntr; {ID! Мы его не знаем, и поэтому перебор}
if Shell_NotifyIcon(NIM_Delete,@NID) then break; {удаляем иконку}
end;
end;
end;


Таким образом, наш план таков: ещё до закрытия окон - уничтожить иконку на панели задач (если она есть, конечно), отослать WM_CLOSE всем top-level окнам, принадлежащим процессу, параллельно (на всякий случай) снимая (возможно, несуществующую) панель приложения с учёта, подождав нужное время - отослать (если необходимо) WM_QUIT всем потокам процесса, ещё немного подождать, и, если приложение всё-таки не закрылось - уничтожить его с помощью TerminateProcess. Конечно, приведённый код не претендует на совершенство, и я не сомневаюсь, что многие найдут недочёты (знаю, что кое-где не хватает проверок, и т.п.). Если у вас есть действительно дельные замечания, предложения по усовершенствованию, обязательно пишите!