В Интернете полно примеров кода, которые используют функцию ShellExecute
(реже — функцию WinExec
). Однако, суровая правда состоит в том, что вам никогда не нужно использовать эти функции.
Почему?
Суть проблемы
Посмотрите на такой кусок кода:
var FS: TFileStream; begin FS := TFileStream.Create('C:MyFile.txt', fmOpenRead or fmShareDenyWrite); // ...
Вопрос: что будет, если при открытии файла возникнет ошибка? Скажем, файла такого нет или он эксклюзивно открыт другой программой.
Ну, конструктор класса TFileStream
возбудит исключение — что «отменит» код после этой строки и переведёт управление на обработчик ошибок. По умолчанию это будет глобальный обработчик объекта Application
, который покажет сообщение об ошибке:
С таким сообщением очевидно, что именно произошло, не так ли?
P.S. А в общем случае это может быть какой-то ваш код для обработки ошибок. Или, к примеру, если вы используете трейсер исключений, то он создаст вам отчёт о этой проблеме и отправит его вам на почту.
Вроде всё хорошо и красиво, да?
Да. А теперь посмотрим на такой код:
ShellExecute(Handle, 'open', 'C:MyFile.txt', nil, nil, SW_SHOWNORMAL);
Вопрос: что будет, если при открытии файла возникнет ошибка? Например, нет ни одной программы для открытия таких файлов. Или программа есть, но её действие называется не open
, а edit
? Да или та же ошибка: нет такого файла?
Ответ: а ничего не будет. Ваш код этого не заметит и продолжит тихо выполняться дальше, считая, что операция прошла успешно. И тогда программист побежит на форумы, спрашивая «почему не работает код».
Обработка ошибок в коде Delphi и коде Windows
Почему так происходит?
Дело в том, что TFileStream
— это код Delphi. ShellExecute
и WinExec
— это код Windows.
Как я это определил?
Есть много способов:
TFileStream
описан в документации Delphi. Если вы запустите поиск в справке Delphi, то найдёте его.ShellExecute
иWinExec
описаны в документации Windows. Если вы запустите поиск по справке Microsoft, то найдёте их.- Вы можете зажать кнопку Ctrl и щёлкнуть левой кнопкой мыши по тексту идентификатора в редакторе кода Delphi. Delphi откроет вам объявление идентификатора под курсором мыши. Тогда вы увидите, что
TFileStream
объявлен как класс Delphi в модулеSystem.Classes
, аShellExecute
иWinExec
объявлены в модулеWinapi.ShellAPI
следующим образом: «function ShellExecute; external shell32 name ‘ShellExecuteW’;». Откуда и следует, чтоTFileStream
— код Delphi (т.к. вы можете видеть этот код на Паскале), аShellExecute
иWinExec
— код Windows (т.к. они импортируются из системной библиотекиshell32.dll
). - Наведя курсор мыши на идентификатор и задержав курсор над ним, вы получите всплывающую подсказку, из которой можно увидеть, что в аргументах
TFileStream.Create
используются типы данных Delphi (такие какstring
), а в аргументахShellExecute
иWinExec
используются типы данных Windows (такие какPChar
).
Хорошо, мы тремя способами выяснили, что TFileStream
— это код Delphi, а ShellExecute
и WinExec
— это код Windows. И что с того?
Дело в том, что код Delphi для сообщения об ошибках использует механизм исключений, а код Windows использует механизм кодов ошибок.
Возбуждение исключения означает, что весь следующий код пропускается, а управление передаётся на код обработки ошибок. Иными словами, если вы ничего специально не делаете, не пишете никаких фильтров, обёрток, обработчиков, то всё равно ситуация по умолчанию для исключения — это его обработка (и показ сообщения об ошибке в частности).
С кодами ошибок дело обстоит иначе, ведь это просто числа (и логические значения), возвращаемые функциями. Если вы их не сохраняете в переменные, не проверяете, то они пропадают. Таким образом, ситуация по умолчанию для кодов ошибок — это игнорирование. Иными словами, вам всегда нужно писать специальный код, чтобы реагировать на ошибки в таких функциях.
P.S. Когда на форуме кто-то задаёт вопрос «почему не работает код», в 90% случаев причина вопроса в том, что программист не расставил правильную обработку ошибок. Чаще всего он вызывает функции Windows, не удосуживаясь проверять коды ошибок.
P.P.S. Не всегда код без обработки ошибок будет выполняться далее при ошибке.
Окей, как же нам нужно проверять ошибки вызова функций ShellExecute
и WinExec
? Что ж, откроем их описание в документации и почитаем:
Return value
Type: HINSTANCE
If the function succeeds, it returns a value greater than 32. If the function fails, it returns an error value that indicates the cause of the failure. The return value is cast as an HINSTANCE for backward compatibility with 16-bit Windows applications. It is not a true HINSTANCE, however. It can be cast only to an int and compared to either 32 or the following error codes below.
О чём говорит нам этот текст? Хотя он действительно описывает способ обработки ошибок, мы пока подождём его использовать: выделенная мной часть говорит о том, что эта функция (ShellExecute
) создана для 16-битной Windows (т.е. Windows 3.11 и младше).
Функции
ShellExecute
иWinExec
используют семантику единого адресного пространства для всех программ. Как мы знаем, в современных 32-битных (и выше) Windows (т.е. Windows 95 и старше) каждый процесс получает свой экземпляр «памяти» (адресного пространства), который изолирован и никак не связан с другими программами. Это было не так в 16-битных Windows: в ней все программы запускались в одной «памяти» (в едином адресном пространстве).В такой модели программа идентифицировалась по экземпляру (
HINSTANCE
) её модуля. Поскольку адресное пространство было едино, то каждая программа загружалась в одно и то же пространство и, следовательно, имела уникальный экземпляр своего исполняемого модуля (exe-файла). Когда же адресное пространство стало своим у каждой программы, все программы стали загружаться в свои собственные адресные пространства. И описатель и экземпляр загруженного модуля стали одинаковы для всех программ (а именно: $00400000) и, таким образом, не могли более использоваться для идентификации запущенных программ.И раз функции
ShellExecute
иWinExec
возвращают экземпляр загруженного модуля (HINSTANCE
) как идентификатор запущенной программы, то они созданы для Windows 3.11 (и более ранних ОС) и крайне плохо приспособлены для Windows 95 (и более новых систем).
Говоря кратко: ShellExecute
и WinExec
— это устаревшее говно мамонта начала 90-х годов. Они созданы в действительно доисторические времена. 640 Кб. Сегменты. Ближние и дальние указатели. Нет виртуальной памяти. Кооперативная многозадачность. Эти функции устарели в 1995 году. Ни один код, написанный после 1995 года, не должен использовать эти функции.
Но почему бы их не использовать? Ведь то, что функции устарели, ещё не означает, что они отсутствуют. Они есть и работают. Дело в том, что чтобы правильно сделать обработку ошибок для вызовов этих функций, вам нужно написать вот такого монстра:
var ErrorCode: Integer; begin ErrorCode := Integer(ShellAPI.ShellExecute(Handle, 'open', 'notepad', nil, nil, SW_SHOWNORMAL)); if ErrorCode <= HINSTANCE_ERROR { = 32 } then begin case ErrorCode of 0: Application.MessageBox(PChar('The operating system is out of memory or resources.'), 'Error', MB_OK or MB_ICONERROR); ERROR_FILE_NOT_FOUND: Application.MessageBox(PChar('The specified file was not found.'), 'Error', MB_OK or MB_ICONERROR); ERROR_PATH_NOT_FOUND: Application.MessageBox(PChar('The specified path was not found.'), 'Error', MB_OK or MB_ICONERROR); ERROR_BAD_FORMAT: Application.MessageBox(PChar('The .exe file is invalid (non-Win32 .exe or error in .exe image).'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_ACCESSDENIED: Application.MessageBox(PChar('The operating system denied access to the specified file.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_ASSOCINCOMPLETE: Application.MessageBox(PChar('The file name association is incomplete or invalid.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_DDEBUSY: Application.MessageBox(PChar('The DDE transaction could not be completed because other DDE transactions were being processed.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_DDEFAIL: Application.MessageBox(PChar('The DDE transaction failed.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_DDETIMEOUT: Application.MessageBox(PChar('The DDE transaction could not be completed because the request timed out.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_DLLNOTFOUND: Application.MessageBox(PChar('The specified DLL was not found.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_FNF: Application.MessageBox(PChar('The specified file was not found.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_NOASSOC: Application.MessageBox(PChar('There is no application associated with the given file name extension. This error will also be returned if you attempt to print a file that is not printable.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_OOM: Application.MessageBox(PChar('There was not enough memory to complete the operation.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_PNF: Application.MessageBox(PChar('The specified path was not found.'), 'Error', MB_OK or MB_ICONERROR); SE_ERR_SHARE: Application.MessageBox(PChar('A sharing violation occurred.'), 'Error', MB_OK or MB_ICONERROR); else Application.MessageBox(PChar(Format('Unknown Error %d', [ErrorCode])), 'Error', MB_OK or MB_ICONERROR); end; Exit; end;
Хорошо, но если нам нельзя использовать функции ShellExecute
и WinExec
, то что же нам нужно использовать?
Правильное решение
Ответ: вместо функции ShellExecute
следует использовать функцию ShellExecuteEx
, а вместо функции WinExec
следует использовать функцию CreateProcess
.
Почему же примеры кода в Интернете используют старые функции? На это есть несколько причин:
- Безграмотность программистов. Большинство программистов даже не читали документацию по этим функциям. В лучшем случае они видели укороченный пересказ описания функций.
- Примеры кода иллюстрируют идею. Они должны быть просты, ненужные детали (в частности: обработка ошибок) должны быть убраны. Автор примера кода подразумевает, что вы напишете свой код, использовав идею примера, а не будете копировать пример один-к-одному.
К счастью, теперь мы уже не совсем безграмотные программисты, и мы в курсе, что нам не нужно использовать функции ShellExecute
и WinExec
, а нужно использовать функции ShellExecuteEx
и CreateProcess
. Таким образом, если у вас на руках есть «типичный» пример кода, который использует функцию ShellExecute
или WinExec
, то вы можете исправить его, заменив вызовы старых функций на новые. Это можно сделать двумя способами. Во-первых, вы можете переписать код:
- Вставив вместо
ShellExecute
функциюShellExecuteEx
(и вместоWinExec
— функциюCreateProcess
), - Добавив правильную обработку ошибок,
- Добавив недостающий код для вызова
ShellExecuteEx
иCreateProcess
(эти функции мощнее по возможностям и требуют больше аргументов).
Или вы можете написать функции-переходники (что мы и сделаем ниже). Функция должна иметь то же имя и те же аргументы, что и оригинал (т.е. ShellExecute
и WinExec
), но вызывать она должна современные функции (т.е. ShellExecuteEx
и CreateProcess
). Добавив такую функцию перед любым «плохим» примером кода, мы автоматически сделаем его «хорошим».
Обработка ошибок вызовов ShellExecuteEx и CreateProcess
Чтобы правильно написать вызов ShellExecuteEx
и CreateProcess
— нам всё ещё нужно научиться правильно обрабатывать ошибки вызова этих функций. Функции ShellExecuteEx
и CreateProcess
, равно как и функции ShellExecute
и WinExec
, являются функциями Windows, а не Delphi. Иными словами, ShellExecuteEx
и CreateProcess
также используют коды ошибок (а не исключения) для сообщения о проблемах выполнения.
Как и ранее, нам нужно открыть документацию функций ShellExecuteEx
и CreateProcess
, чтобы прочитать, как следует обрабатывать их ошибки:
Return value
Type: BOOL
Returns TRUE if successful; otherwise, FALSE. Call GetLastError for extended error information.
Как мы видим, способ обработки ошибок одинаков для этих функций. И он, вообще-то, является стандартом для большинства классических функций Windows. Функция возвращает логическое значение: истина или ложь, сигнализирующее об успешном или не успешном выполнении. Если выполнение было не успешно, то точную причину подскажет функция GetLastError
, которая вернёт одну из констант ERROR_xyz
, определённых в модуле Winapi.Windows
. Чисто, чтобы удовлетворить ваше любопытство: вот список некоторых стандартных кодов ошибок (это не полный список — вы можете добавлять и свои собственные коды ошибок).
Этот код ошибки (только стандартный, а не пользовательский) можно конвертировать в текстовое сообщение с помощью вспомогательной функции SysErrorMessage
. Таким образом, чтобы правильно обработать результат вызова ShellExecuteEx
или CreateProcess
(а равно как и любой другой не устаревшей классической функции Windows), мы можем сделать следующее (решение в лоб):
if not ShellExecuteEx({ ... }) then begin Application.MessageBox(PChar(SysErrorMessage(GetLastError)), 'Ошибка', MB_OK or MB_ICONERROR); Exit; end;
if not CreateProcess({ ... }) then begin Application.MessageBox(PChar(SysErrorMessage(GetLastError)), 'Ошибка', MB_OK or MB_ICONERROR); Exit; end;
Конечно, это ужасно неудобно, не так ли? Сравните этот громоздкий код с вызовов TFileStream.Create
из примера выше. Нельзя ли с этим что-то сделать?
Можно. Идея здесь заключается в том, что мы конвертируем код ошибки в исключение. Чтобы не делать это вручную, можно воспользоваться уже готовой вспомогательной функцией: RaiseLastOSError
. А чтобы не писать подобный неуклюжий if
, мы можем сделать вспомогательную функцию. Вообще-то, такая функция уже есть, и она называется Win32Check
. Итого, наш код примет вид:
Win32Check(ShellExecuteEx({ ... }));
Win32Check(CreateProcess({ ... }));
Этот код функционально эквивалентен:
TFileStream.Create({ ... });
Т.е. если при его выполнении возникнет ошибка, то возбудится исключение, код дальше по тексту будет пропущен, а управление будет передано на ближайший обработчик ошибок, по умолчанию таковым будет обработчик от объекта Application
, который и покажет сообщение об ошибке:
Как видим, при этом код вызова системных функций Windows практически не отличается от вызова кода Delphi (нужно просто не забывать добавлять вызовы Win32Check
) и не требует дополнительного кода (весь вспомогательный код уже написан в самой Delphi). Клёво, да?
P.S. Вообще-то, хотя функция
RaiseLastOSError
— кросс-платформенна, ноWin32Check
— нет.Win32Check
помечена как «platform-specific», хотя она не делает ничего, кроме вызоваRaiseLastOSError
. Это не проблема в нашем случае, поскольку мы всё равно вызываем функции Windows (т.е. функции, специфичные для одной конкретной платформы).Кросс-платформенный же способ — это функция
CheckOSError
. К сожалению, из-за кросс-платформенности эта функция крайне неудобна для использования в Windows. Во-первых, с ней код становится сложнее:ShellExecuteEx({...}); CheckOSError(GetLastError);Во-вторых, эта функция проверяет, что код ошибки должен быть отличен от «успех» (в случае с Windows — отличен от
ERROR_SUCCESS
). Хотя сперва это кажется логичным — но только до тех пор, пока вы не столкнётесь с таким поведением. Поэтому гораздо лучше использоватьRaiseLastOSError
:if not ShellExecuteEx({...}) then RaiseLastOSError;Это и корректнее и кросс-платформенно.
А в общем и целом, если вы пишете под Windows и вызываете функции Windows — просто используйте
Win32Check
.
Простые обёртки к ShellExecuteEx и CreateProcess
Итак, теперь мы готовы написать функции-обёртки для исправления «плохих» примеров кода:
uses ActiveX, ShellApi; procedure ShellExecute(const AWnd: HWND; const AOperation, AFileName: String; const AParameters: String = ''; const ADirectory: String = ''; const AShowCmd: Integer = SW_SHOWNORMAL); var ExecInfo: TShellExecuteInfo; NeedUnitialize: Boolean; begin Assert(AFileName <> ''); NeedUnitialize := Succeeded(CoInitializeEx(nil, COINIT_APARTMENTTHREADED or COINIT_DISABLE_OLE1DDE)); try FillChar(ExecInfo, SizeOf(ExecInfo), 0); ExecInfo.cbSize := SizeOf(ExecInfo); ExecInfo.Wnd := AWnd; ExecInfo.lpVerb := Pointer(AOperation); ExecInfo.lpFile := PChar(AFileName); ExecInfo.lpParameters := Pointer(AParameters); ExecInfo.lpDirectory := Pointer(ADirectory); ExecInfo.nShow := AShowCmd; ExecInfo.fMask := SEE_MASK_NOASYNC { = SEE_MASK_FLAG_DDEWAIT для старых версий Delphi } or SEE_MASK_FLAG_NO_UI; {$IFDEF UNICODE} // Необязательно, см. http://www.transl-gunsmoker.ru/2015/01/what-does-SEEMASKUNICODE-flag-in-ShellExecuteEx-actually-do.html ExecInfo.fMask := ExecInfo.fMask or SEE_MASK_UNICODE; {$ENDIF} {$WARN SYMBOL_PLATFORM OFF} Win32Check(ShellExecuteEx(@ExecInfo)); {$WARN SYMBOL_PLATFORM ON} finally if NeedUnitialize then CoUninitialize; end; end;
procedure WinExec(const ACmdLine: String; const ACmdShow: UINT = SW_SHOWNORMAL); var SI: TStartupInfo; PI: TProcessInformation; CmdLine: String; begin Assert(ACmdLine <> ''); CmdLine := ACmdLine; UniqueString(CmdLine); FillChar(SI, SizeOf(SI), 0); FillChar(PI, SizeOf(PI), 0); SI.cb := SizeOf(SI); SI.dwFlags := STARTF_USESHOWWINDOW; SI.wShowWindow := ACmdShow; SetLastError(ERROR_INVALID_PARAMETER); {$WARN SYMBOL_PLATFORM OFF} Win32Check(CreateProcess(nil, PChar(CmdLine), nil, nil, False, CREATE_DEFAULT_ERROR_MODE {$IFDEF UNICODE}or CREATE_UNICODE_ENVIRONMENT{$ENDIF}, nil, nil, SI, PI)); {$WARN SYMBOL_PLATFORM ON} CloseHandle(PI.hThread); CloseHandle(PI.hProcess); end;
В целом, этот код достаточно прямолинеен: мы просто копируем параметры от ShellExecute
/WinExec
к ShellExecuteEx
/CreateProcess
, добавляя дополнительные параметры, требуемые ShellExecuteEx
/CreateProcess
. Вот несколько особенностей, на которые хотелось бы обратить внимание:
- Обе функции сделаны в виде процедур (т.е. они не возвращают значения). Это сделано на тот случай, если кто-то по ошибке попытается применить их к примеру кода, который корректно использует
ShellExecute
/WinExec
(«корректно» — т.е. с обработкой ошибок); - Добавлен
Assert
для гарантии корректного вызова (наличия обязательных параметров); - Код с
ShellExecuteEx
инициализирует COM — об этом (помимо обработки ошибок) также часто забывают. Дело в том, что в Delphi COM автоматически инициализируется в главном потоке, поэтому эта ошибка обычно не заметна. Но если вы попытаетесь вызвать подобный «плохой» код из вторичного фонового потока, то получите ошибкуRPC_E_THREAD_NOT_INIT
=$8001010F
(«CoInitialize has not been called on the current thread.
«/»Обращение к CoInitialize из текущего потока не производилось.
«); - Мы указываем флаг
SEE_MASK_NOASYNC
(бывшийSEE_MASK_FLAG_DDEWAIT
), чтобы указатьShellExecuteEx
подождать завершения всех асинхронных операций перед возвратом управления нам. Это нужно в двух случаях: если наш код вызывается из вторичного фонового потока и тут же выходит из потока, либо если наш код вызывается в потоке без оконной очереди сообщений. Это также довольно частая (и нетривиальная) ошибка; - Мы указываем флаг
SEE_MASK_FLAG_NO_UI
, чтобы указатьShellExecuteEx
, что мы сами обрабатываем ошибки (это подавит системный диалог с ошибками); - Для аргументов мы используем приведение к
Pointer
вместо приведения кPChar
— разница в том, что пустую строку приведение кPChar
преобразует в указатель на#0
, а приведение кPointer
— кnil
; - Мы делаем локальную копию командной строки для
CreateProcess
, чтобы избежать проблемы модификации константы (только для Unicode-приложения); - Параметры используют типы данных Delphi (а не Windows), а также имеют значения по умолчанию для необязательных параметров. Это не нужно для цели «сделать кривой пример кода корректным», но удобно для самостоятельного использования функций-обёрток. Например:
ShellExecute(Handle, '', Edit1.Text);
Этот код откроет файл из Edit1.Text в программе по умолчанию. Обратите внимание, что мы не стали указывать все параметры, а также нам не пришлось приводить тип строки к
PChar
; - Директивы
IFDEF UNICODE
делают код корректным как для ANSI (Delphi 2007 и ниже), так и для Unicode (Delphi 2009 и выше) версий Delphi (примечание: хотя код выше строго следует документации и указывает флагSEE_MASK_UNICODE
, но фактически этот флаг ничего не делает).
Пример исправления кода
Фух, теперь мы, наконец-то, можем исправить код «плохих» примеров!
Для этого я сделал поиск по «ShellExecute Delphi» (без кавычек, конечно же) и взял несколько примеров:
// Внимание! Код ниже не корректен! ShellExecute(Handle, 'open', 'c:Windowsnotepad.exe', nil, nil, SW_SHOWNORMAL);
ShellExecute(Handle, 'open', 'c:windowsnotepad.exe', 'c:text.txt', nil, nil, SW_SHOWNORMAL);
ShellExecute(Form1.Handle, nil, PChar(Site), nil, nil, SW_SHOW);
ShellExecute(Form1.Handle, nil, 'mailto:semen@krovatka.net?subject=delphi', nil, nil, SW_RESTORE);
Как мы можем исправить этот код?
Чтобы исправить этот код, нам нужно, во-первых, вставить код функций-обёрток перед этими «плохими» кусками кода, а, во-вторых, заменить nil
на ''
, а преобразования PChar
/ PAnsiChar
/PWideChar
и вовсе убрать — поскольку вместо системных типов Windows (PChar
) наш код использует обычные строки Delphi.
Итого:
ShellExecute(Handle, 'open', 'c:Windowsnotepad.exe', '', '', SW_SHOWNORMAL);
ShellExecute(Handle, 'open', 'c:windowsnotepad.exe', 'c:text.txt', '', '', SW_SHOWNORMAL);
ShellExecute(Form1.Handle, '', Site, '', '', SW_SHOW);
ShellExecute(Form1.Handle, '', 'mailto:semen@krovatka.net?subject=delphi', '', '', SW_RESTORE);
Этот код теперь «волшебным» образом стал правильным и корректным.
И под «правильным» здесь я подразумеваю только те моменты, что мы уже обговаривали: обработка ошибок, COM, асинхронные операции. К примеру, Form1.Handle
вместо Handle
— как минимум идеологически неверно и некоторых случаях даже и не корректно. Жёстко зашитые пути к программам — ещё косяк. Явный запуск программ — ещё одна проблема.
Обратите внимание, что с нашими обёртками описатель окна (первый параметр ShellExecute
) можно не указывать — этот параметр не будет использоваться для показа сообщений об ошибках, поскольку мы указали флаг SEE_MASK_FLAG_NO_UI
. И хотя флаг SEE_MASK_FLAG_NO_UI
не подавляет диалог «Открыть с помощью» (см. ниже), но описатель окна не используется при показе этого диалога. Поэтому вы можете передавать туда 0.
P.S. Если вы используете
ShellExecuteEx
с действием «runas
» для запуска процесса с элевацией, то вам нужно передавать корректный описатель окна (HWND
): он будет использоваться для идентификации вашего процесса как приложения первого плана. Если же вы его не укажете, то ваше приложение будет считаться фоновым приложением. В этом случае запрос UAC на повышение прав не будет показан на экране сразу, а появится в свёрнутом (и мигающем) виде на панели задач.
Кроме того, с нашими обёртками не обязательно указывать все параметры — ведь они сделаны опциональными.
Итого, при желании код может быть упрощён до:
ShellExecute(0, 'open', 'c:Windowsnotepad.exe');
ShellExecute(0, 'open', 'c:windowsnotepad.exe', 'c:text.txt');
ShellExecute(0, '', Site);
ShellExecute(0, '', 'mailto:semen@krovatka.net?subject=delphi');
P.S. Обратите внимание, что этот код правилен и корректен только при условии наличия функции-обёртки выше по тексту — ведь именно там сосредоточенна обработка ошибок, COM и асинхронных операций. Без функции-обёртки этот код не будет верным.
Прочие проблемы
Отсутствие обработки ошибок, отсутствие инициализации COM и отсутствие обработки асинхронных операций — это ещё не все проблемы в типичных примерах кода для ShellExecute
.
В частности, довольно часто примеры используют действие (verb) «open
» для открытия файлов. Однако корректнее не указывать действие вообще — т.е. передавать во второй параметр nil
(для системных функций) или ''
(пустую строку) — для нашей функции-обёртки. Дело в том, что иногда действие «open
» может отсутствовать, либо не быть действием по умолчанию. К примеру, достаточно часто действие по умолчанию для типов файлов документов является «edit
«. И если вы явно укажете действие «open
» для открытия таких файлов, то либо вы откроете файл не в той программе (не в той, что назначена по умолчанию), либо получите ошибку «Указанному файлу не сопоставлено ни одно приложение для выполнения данной операции»/»No application is associated with the specified file for this operation.». Например, в некоторых версиях Windows действие ‘open’ для .lnk файлов не назначено. Вызов ShellExecute(0, 'open', 'my.lnk', ...)
вернёт ошибку, но ShellExecute(0, nil, 'my.lnk', ...)
будет успешен.
Чтобы не гадать, какое же действие назначено по умолчанию для этого типа файла — просто не указывайте его. В этом случае система сама выберет действие по умолчанию, будь это «open
«, «edit
«, «explore
«, «print
» или что-то иное. А если действия по умолчанию не назначено, то система выполнит «open
«. А если его нет, то она возьмёт первое попавшееся действие. А если действий вообще нет — то покажет диалог «Открыть с помощью»:
Итого: всегда передавайте nil
/''
в качестве действия (verb
) в ShellExecute
(Ex
). Явно указывайте действие (такое как «open
«) только в специальных случаях:
- Вам явно нужно специальное действие вместо открытия файла. Например, «
print
«, «explore
» или «runas
«; - Вам нужно получить ошибку «Указанному файлу не сопоставлено ни одно приложение для выполнения данной операции» в случае отсутствия программы для открытия таких файлов. В этом случае вы можете явно указать действие «
open
» — и тогда поиск действия выполняться не будет, диалог «Открыть с помощью» показываться не будет, иGetLastError
вернёт вам ошибкуERROR_NO_ASSOCIATION
=1155
. И в этом случае, при желании, вы можете сами показать диалог «Открыть с помощью» — использовав функциюSHOpenWithDialog
(действие же «openas
» сработает только если для типа файла нет других действий). Если же действие (verb) не указывать, то операция всегда будет успешной (исключая ошибки типа «файл не найден») — вне зависимости от наличия ассоциаций; - Вам явно нужно действие «
open
«, но действием по умолчанию назначено что-то иное.
Заключение
Что ж, я надеюсь, что вы узнали что-то новое сегодня. Вы узнали, почему вам не следует использовать функции ShellExecute
и WinExec
, какие функции нужно использовать вместо них (ShellExecuteEx
и CreateProcess
), как нужно правильно делать обработку ошибок при вызове системных функций Windows, как упростить этот код, какие ещё есть подводные камни у ShellExecute
(Ex
) и CreateProcess
.
Читать далее: Почему вам не следует использовать ShellExecuteEx
.
См. также: проблемы с CreateProcess
.
Форум программистов Vingrad
Модераторы: Snowy, bartram, MetalFan, bems, Poseidon, Riply |
Поиск: |
|
ShellExecute Как отловить его ошибки? |
Опции темы |
brick-rs |
|
||
Новичок Профиль
Репутация: нет
|
Подскажите плиз, использую ShellExecute для открытия разных доков, и т.п… Вопрос встал как отловить его ошибки? причем как таковыми ошибками они не вываливаааются, но если при открытии дока что то у него не получилось то он просто молчит, если все нормально то он открывает все Ок. На предмет существования дока проверку делаю. Это сообщение отредактировал(а) brick-rs — 28.7.2004, 03:10 |
||
|
|||
Cashey |
|
||
Бессмертный Профиль
Репутация: нет
|
Функция возвращает дескриптор открытого приложения. Если возвращаемое значение меньше или равно 32, это указывает на ошибку. Для Windows старше 95-го эти константы означают: ——————— библия учит любить ближнего, а камасутра обучает как именно |
||
|
|||
brick-rs |
|
||
Новичок Профиль
Репутация: нет
|
а вот собственно в том то и весь вопрос как этот дескриптор проверить? не знаю еще не разу не сталкивался |
||
|
|||
<Spawn> |
|
||
Око кары:) Профиль
Репутация: 1
|
brick-rs Все просто:
——————— «Для некоторых людей программирование является такой же внутренней потребностью, подобно тому, как коровы дают молоко, или писатели стремятся писать» — Николай Безруков. |
||
|
|||
Vit |
|
||
Vitaly Nevzorov Профиль
Репутация: 1
|
Используй CreateProcess — гораздо больше возможностей, и по отлову ошибок в том числе… ——————— With the best wishes, Vit |
||
|
|||
saw666 |
|
||
Новичок Профиль Репутация: нет
|
Специально зарегистрировался чтобы поблагодарить за это сообщение. Да, даже в 2016 году оно оказалось полезным. Спасибо! ))) |
||
|
|||
navodri |
|
||
Бывалый Профиль Репутация: нет
|
Вот так просто:
|
||
|
|||
|
Правила форума «Delphi: WinAPI и системное программирование» | |
|
Запрещено: 1. Публиковать ссылки на вскрытые компоненты 2. Обсуждать взлом компонентов и делиться вскрытыми компонентами
Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Snowy, bartram, MetalFan, bems, Poseidon, Rrader, Riply. |
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей) |
0 Пользователей: |
« Предыдущая тема | Delphi: WinAPI и системное программирование | Следующая тема » |
-9 / 0 / 0 Регистрация: 17.12.2014 Сообщений: 125 |
|
1 |
|
27.01.2017, 10:16. Показов 3777. Ответов 9
Доброго времени суток. Ф-я SellExecute в случае удачного выполнения возвращает HINSTANCE. В случае неудачного. возвращает код ошибки от 0 до 32. как получить этот самый код в Int?
0 |
управление сложностью 1687 / 1300 / 259 Регистрация: 22.03.2015 Сообщений: 7,545 Записей в блоге: 5 |
|
27.01.2017, 10:20 |
2 |
Пихайте код в попытку и обрабатывайте номера исключений
0 |
735 / 525 / 130 Регистрация: 31.05.2013 Сообщений: 2,929 Записей в блоге: 3 |
|
27.01.2017, 16:30 |
3 |
Я думаю через «насильное» преобразование можно в инт. MSDN
0 |
D1973 Модератор 8487 / 5647 / 2290 Регистрация: 21.01.2014 Сообщений: 24,229 Записей в блоге: 3 |
||||
02.02.2017, 12:42 |
4 |
|||
Сообщение было отмечено Jurixx как решение Решение
описания этих номеров можно найти в Shellapi.h /* ShellExecute() and ShellExecuteEx() error codes */ /* regular WinExec() codes */ #endif /* WINVER >= 0x0400 */ /* error values for ShellExecute() beyond the regular WinExec() codes */
1 |
Супер-модератор 32583 / 21054 / 8132 Регистрация: 22.10.2011 Сообщений: 36,326 Записей в блоге: 8 |
|
02.02.2017, 13:16 |
5 |
0 |
-9 / 0 / 0 Регистрация: 17.12.2014 Сообщений: 125 |
|
06.02.2017, 16:16 [ТС] |
6 |
volvo, Ну а что советуешь взамен?
0 |
735 / 525 / 130 Регистрация: 31.05.2013 Сообщений: 2,929 Записей в блоге: 3 |
|
06.02.2017, 16:24 |
7 |
Jurixx, а Вы статью прочтите, там на каждом шагу говрится что нужно взамен =)
0 |
-9 / 0 / 0 Регистрация: 17.12.2014 Сообщений: 125 |
|
06.02.2017, 16:44 [ТС] |
8 |
Спасибо Кэп, прочитал… Добавлено через 2 минуты
0 |
Супер-модератор 32583 / 21054 / 8132 Регистрация: 22.10.2011 Сообщений: 36,326 Записей в блоге: 8 |
|
06.02.2017, 16:59 |
9 |
{ = 32 } — это дельфийские комментарии, если что.
0 |
-9 / 0 / 0 Регистрация: 17.12.2014 Сообщений: 125 |
|
07.02.2017, 10:02 [ТС] |
10 |
volvo, точно, не люблю делфи)
0 |
_ShellExecuteEx($Box)
Func _ShellExecuteEx($sCmd, $sArgs = "", $sFolder = "", $sVerb = "", $iState = @SW_SHOWNORMAL, $hWnd = 0)
Local $stINFO = DllStructCreate("long;long;long;ptr;ptr;ptr;ptr;long;long;long;ptr;long;long;long;long")
Local $stVerb = DllStructCreate("char[15];char")
Local $stPath = DllStructCreate("char[255];char")
Local $stArgs = DllStructCreate("char[255];char")
Local $stWDir = DllStructCreate("char[255];char")
DllStructSetData($stVerb, 1, $sVerb)
DllStructSetData($stPath, 1, $sCmd)
DllStructSetData($stWDir, 1, $sFolder)
DllStructSetData($stArgs, 1, $sArgs)
DllStructSetData($stINFO, 1, DllStructGetSize($stINFO))
DllStructSetData($stINFO, 2, BitOR(0xC, 0x40, 0x400))
DllStructSetData($stINFO, 3, $hWnd)
DllStructSetData($stINFO, 4, DllStructGetPtr($stVerb))
DllStructSetData($stINFO, 5, DllStructGetPtr($stPath))
DllStructSetData($stINFO, 6, DllStructGetPtr($stArgs))
DllStructSetData($stINFO, 7, DllStructGetPtr($stWDir))
DllStructSetData($stINFO, 8, $iState)
Local $aRet = DllCall("Shell32.dll", "int", "ShellExecuteEx", "ptr", DllStructGetPtr($stINFO))
If Not IsArray($aRet) Or Not $aRet[0] Then
Return SetError(2, 0, 0)
EndIf
Return 1
EndFunc
Это перевод One possible reason why ShellExecute returns SE_ERR_ACCESSDENIED and ShellExecuteEx returns ERROR_ACCESS_DENIED. Автор: Реймонд Чен.
(этот странный заголовок написан для поисковой оптимизации)
Один клиент сообщил, что при вызове ShellExecute
эта функция иногда завершалась с ошибкой SE_ERR_ACCESSDENIED
(= 5) — в зависимости от того, что они пытались открыть (а если бы они использовали ShellExecuteEx
, то они получали бы ошибку ERROR_ACCESS_DENIED
(перевод поста)).
После долгой «игры в пинг-понг», проверки файловых ассоциаций и т.п., один из разработчиков из нашей команды использовал свой хрустальный шар и спросил: «А вы случайно не вызываете её из MTA?» (MTA = multi-threaded apartment, многопоточный апартмент).
«Да», — ответил клиент. — «ShellExecute
вызывается из специально выделенного MTA-потока. Проблема в этом?».
Ну, вообще-то, да. И об этом явно сказано в документации к ShellExecute
:
Поскольку
ShellExecute
может делегировать выполнение расширениям Оболочки (источникам данных, обработчикам контекстных меню, реализациям действий), которые активируются через COM, то COM должен быть инициализирован до того, как ваш код вызоветShellExecute
. Некоторые расширения Оболочки требуют, чтобы COM был инициализирован в однопоточном апартменте (STA).
Как правило, функции Оболочки требуют STA. Вспомните, что MTA подразумевает отсутствие интерфейса пользователя. Если вы попытаетесь использовать apartment-threaded объект из MTA-потока, вам потребуется маршаллер, а если такого маршаллера нет, то вызов провалится.
Это также объясняет, почему вызов ShellExecute
завершается неудачей только для некоторых типов файлов: если обработка типа файла не требует создания объекта COM, то ситуация с несовпадением MTA/STA никогда не произойдёт.
Вы можете использовать reinterpret_cast для перевода возвращаемого значения:
int retValue = reinterpret_cast<int>(ShellExecute(0, QString("open").toStdWString().c_str(), Path.toStdWString().c_str(), 0, 0, SW_SHOWNORMAL));
if (retValue < 32)
qDebug() << "Error";
в то время как это также может привести к предупреждению:
предупреждение C4302: ‘reinterpret_cast’: усечение от ‘HINSTANCE’ до ‘int’
но, по крайней мере, это работает. В MSDN также предлагается использовать метод int.
https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx
«Если функция завершается успешно, она возвращает значение больше 32. Если функция выходит из строя, она возвращает значение ошибки, указывающее причину сбоя. Возвращаемое значение отображается как HINSTANCE для обратной совместимости с 16-разрядными приложениями Windows. Однако это не правда, HINSTANCE. Его можно использовать только для int и сравнить с 32 или следующими кодами ошибок ниже. ‘
Еще один простой вызов,
который можно использовать для запуска
программ, — это Shell
Execute.
Этот вызов во многом напоминает WinExec,
однако он поддерживает обработку типов
файлов, зарегистрированных графической
оболочкой операционной системы. Например,
если при помощи Shell
Execute
вы попробуете запустить файл с расширением
ТХТ, будет
запущена программа Notepad
или любая другая программа, которая
используется в вашей системе для
просмотра текстовых файлов.
HINSTANCE ShellExecute(
HWND hwnd,
LPCTSTR lpOperation,
LPCTSTR lpFile,
LPCTSTR lpParameters,
LPCTSTR lpDirectory,
INT nShowCmd
);
В качестве аргументов
функция ShellExecute
принимает дескриптор окна (на случай,
если возникнет необходимость в сообщениях
об ошибках) и операционную строку, такую
как open
(открыть), print
(распечатать) или explore
(исследовать). В
качестве операционной строки можно
передать NULL-строку.
В этом случае указанный вами файл будет
открыт (open).
Также функции ShellExecute
необходимо сообщить
имя файла и любые параметры командной
строки (обычно NULL).
Наконец, последние два аргумента — это
текущий каталог и константа функции
ShowWindow
(как и в случае с
WinExec).
Возвращаемое значение точно
такое же, как и у WinExec.
Если вы указываете в
качестве третьего аргумента функции
Shell
Execute
имя исполняемого
файла, вы можете не использовать другие
аргументы, кроме аргумента параметров
командной строки и константы ShowWindow.
Для файлов документов
(например, ТХТ или DOC)
значение этих аргументов обычно равно
NULL.
Функцию ShellExecute
можно использовать,
например, для того, чтобы открыть корневой
каталог диска С:
ShellExecute(handle,
«open», «c:\». NULL, NULL, SW_SHOWNORMAL);
Вы можете заменить строку
«open»
на строку «explore»,
а также указать в качестве третьего
параметра имя абсолютно любого каталога.
Другим аналогичным системным
вызовом является вызов ShellExecuteEx.
Этот вызов фактически
является полным аналогом ShellExecute,
однако в качестве
аргумента он принимает указатель на
структуру, поля которой во многом
совпадают с аргументами вызова
ShellExecute.
Помимо этого по
завершении своей работы вызов
ShellExecuteEx
помещает в одно из полей
этой структуры дескриптор вновь
запущенного процесса.
Завершение процесса
Процесс можно завершить четырьмя
способами:
-
входная функция первичного потока
возвращает управление (рекомендуемый
способ), -
один из потоков процесса
вызывает функцию ExitProcess
(нежелательный
способ); -
поток другого процесса
вызывает функцию TerminateProcess
(тоже нежелательно); -
все потоки процесса умирают по своей
воле (большая редкость),
Возврат управления входной функцией первичного потока
Приложение следует
проектировать так, чтобы его процесс
завершался только после возврата
управления входной функцией первичного
потока. Это единственный способ,
гарантирующий корректную очистку всех
ресурсов, принадлежавших первичному
потоку.
Функция ExitProcess
Процесс завершается, когда
один из его потоков вызывает ExitProcess:
VOID
ExitProcess(UINT
fuExitCode);
Эта функция завершает
процесс и заносит в параметр fuExitCode
код завершения процесса. Возвращаемого
значения у ExitProcess нет,
так как результат ее действия — завершение
процесса. Если за вызовом этой функции
в программе присутствует какой-нибудь
код, он никогда не исполняется. Заметьте,
что такой вызов ExitProcess
приводит к уничтожению
процесса или потока, когда выполнение
функции еще не завершилось. Что касается
операционной системы, то здесь все в
порядке: она корректно очистит все
ресурсы, выделенные процессу или потоку.
Но в приложении, написанном на С/С++,
следует избегать вызова этих функций,
так как библиотеке С/С++ скорее всего не
удастся провести
должную очистку.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #
- #