Shellexecute обработка ошибок

В Интернете полно примеров кода, которые используют функцию 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.

Как я это определил?

Есть много способов:

  1. TFileStream описан в документации Delphi. Если вы запустите поиск в справке Delphi, то найдёте его. ShellExecute и WinExec описаны в документации Windows. Если вы запустите поиск по справке Microsoft, то найдёте их.
  2. Вы можете зажать кнопку Ctrl и щёлкнуть левой кнопкой мыши по тексту идентификатора в редакторе кода Delphi. Delphi откроет вам объявление идентификатора под курсором мыши. Тогда вы увидите, что TFileStream объявлен как класс Delphi в модуле System.Classes, а ShellExecute и WinExec объявлены в модуле Winapi.ShellAPI следующим образом: «function ShellExecute; external shell32 name ‘ShellExecuteW’;». Откуда и следует, что TFileStream — код Delphi (т.к. вы можете видеть этот код на Паскале), а ShellExecute и WinExec — код Windows (т.к. они импортируются из системной библиотеки shell32.dll).
  3. Наведя курсор мыши на идентификатор и задержав курсор над ним, вы получите всплывающую подсказку, из которой можно увидеть, что в аргументах 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.

Почему же примеры кода в Интернете используют старые функции? На это есть несколько причин:

  1. Безграмотность программистов. Большинство программистов даже не читали документацию по этим функциям. В лучшем случае они видели укороченный пересказ описания функций.
  2. Примеры кода иллюстрируют идею. Они должны быть просты, ненужные детали (в частности: обработка ошибок) должны быть убраны. Автор примера кода подразумевает, что вы напишете свой код, использовав идею примера, а не будете копировать пример один-к-одному.

К счастью, теперь мы уже не совсем безграмотные программисты, и мы в курсе, что нам не нужно использовать функции ShellExecute и WinExec, а нужно использовать функции ShellExecuteEx и CreateProcess. Таким образом, если у вас на руках есть «типичный» пример кода, который использует функцию ShellExecute или WinExec, то вы можете исправить его, заменив вызовы старых функций на новые. Это можно сделать двумя способами. Во-первых, вы можете переписать код:

  1. Вставив вместо ShellExecute функцию ShellExecuteEx (и вместо WinExec — функцию CreateProcess),
  2. Добавив правильную обработку ошибок,
  3. Добавив недостающий код для вызова 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. Вот несколько особенностей, на которые хотелось бы обратить внимание:

  1. Обе функции сделаны в виде процедур (т.е. они не возвращают значения). Это сделано на тот случай, если кто-то по ошибке попытается применить их к примеру кода, который корректно использует ShellExecute/WinExec («корректно» — т.е. с обработкой ошибок);
  2. Добавлен Assert для гарантии корректного вызова (наличия обязательных параметров);
  3. Код с ShellExecuteEx инициализирует COM — об этом (помимо обработки ошибок) также часто забывают. Дело в том, что в Delphi COM автоматически инициализируется в главном потоке, поэтому эта ошибка обычно не заметна. Но если вы попытаетесь вызвать подобный «плохой» код из вторичного фонового потока, то получите ошибку RPC_E_THREAD_NOT_INIT = $8001010FCoInitialize has not been called on the current thread.«/»Обращение к CoInitialize из текущего потока не производилось.«);
  4. Мы указываем флаг SEE_MASK_NOASYNC (бывший SEE_MASK_FLAG_DDEWAIT), чтобы указать ShellExecuteEx подождать завершения всех асинхронных операций перед возвратом управления нам. Это нужно в двух случаях: если наш код вызывается из вторичного фонового потока и тут же выходит из потока, либо если наш код вызывается в потоке без оконной очереди сообщений. Это также довольно частая (и нетривиальная) ошибка;
  5. Мы указываем флаг SEE_MASK_FLAG_NO_UI, чтобы указать ShellExecuteEx, что мы сами обрабатываем ошибки (это подавит системный диалог с ошибками);
  6. Для аргументов мы используем приведение к Pointer вместо приведения к PChar — разница в том, что пустую строку приведение к PChar преобразует в указатель на #0, а приведение к Pointer — к nil;
  7. Мы делаем локальную копию командной строки для CreateProcess, чтобы избежать проблемы модификации константы (только для Unicode-приложения);
  8. Параметры используют типы данных Delphi (а не Windows), а также имеют значения по умолчанию для необязательных параметров. Это не нужно для цели «сделать кривой пример кода корректным», но удобно для самостоятельного использования функций-обёрток. Например:
    ShellExecute(Handle, '', Edit1.Text);

    Этот код откроет файл из Edit1.Text в программе по умолчанию. Обратите внимание, что мы не стали указывать все параметры, а также нам не пришлось приводить тип строки к PChar;

  9. Директивы IFDEF UNICODE делают код корректным как для ANSI (Delphi 2007 и ниже), так и для Unicode (Delphi 2009 и выше) версий Delphi (примечание: хотя код выше строго следует документации и указывает флаг SEE_MASK_UNICODE, но фактически этот флаг ничего не делает).

Пример исправления кода

Фух, теперь мы, наконец-то, можем исправить код «плохих» примеров!

Для этого я сделал поиск по «ShellExecute Delphi» (без кавычек, конечно же) и взял несколько примеров:

// Внимание! Код ниже не корректен!

ShellExecute(Handle, 'open', 'c:\Windows\notepad.exe', nil, nil, SW_SHOWNORMAL);
ShellExecute(Handle, 'open', 'c:\windows\notepad.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:\Windows\notepad.exe', '', '', SW_SHOWNORMAL);
ShellExecute(Handle, 'open', 'c:\windows\notepad.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:\Windows\notepad.exe');
ShellExecute(0, 'open', 'c:\windows\notepad.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«) только в специальных случаях:

  1. Вам явно нужно специальное действие вместо открытия файла. Например, «print«, «explore» или «runas«;
  2. Вам нужно получить ошибку «Указанному файлу не сопоставлено ни одно приложение для выполнения данной операции» в случае отсутствия программы для открытия таких файлов. В этом случае вы можете явно указать действие «open» — и тогда поиск действия выполняться не будет, диалог «Открыть с помощью» показываться не будет, и GetLastError вернёт вам ошибку ERROR_NO_ASSOCIATION = 1155. И в этом случае, при желании, вы можете сами показать диалог «Открыть с помощью» — использовав функцию SHOpenWithDialog (действие же «openas» сработает только если для типа файла нет других действий). Если же действие (verb) не указывать, то операция всегда будет успешной (исключая ошибки типа «файл не найден») — вне зависимости от наличия ассоциаций;
  3. Вам явно нужно действие «open«, но действием по умолчанию назначено что-то иное.

Заключение

Что ж, я надеюсь, что вы узнали что-то новое сегодня. Вы узнали, почему вам не следует использовать функции ShellExecute и WinExec, какие функции нужно использовать вместо них (ShellExecuteEx и CreateProcess), как нужно правильно делать обработку ошибок при вызове системных функций Windows, как упростить этот код, какие ещё есть подводные камни у ShellExecute(Ex) и CreateProcess.

Читать далее: Почему вам не следует использовать ShellExecuteEx.

См. также: проблемы с CreateProcess.


Форум программистов Vingrad

Модераторы: Snowy, bartram, MetalFan, bems, Poseidon, Riply

Поиск:

Ответ в темуСоздание новой темы
Создание опроса
> ShellExecute Как отловить его ошибки? 

:(

   

Опции темы

brick-rs
Дата 28.7.2004, 03:08 (ссылка)
| (нет голосов)
Загрузка ... Загрузка …




Быстрая цитата

Цитата

Новичок

Профиль
Группа: Участник
Сообщений: 11
Регистрация: 19.5.2004
Где: г. Мыски, Кемеров ская обл.

Репутация: нет
Всего: нет

Подскажите плиз, использую ShellExecute для открытия разных доков, и т.п… Вопрос встал как отловить его ошибки? причем как таковыми ошибками они не вываливаааются, но если при открытии дока что то у него не получилось то он просто молчит, если все нормально то он открывает все Ок. На предмет существования дока проверку делаю.
Для инфо: Делфи 6, конкретно интересует случай с открытием скажем дока на кт не установленно соответствующее приложение?
использую такой код:
pt:=StrPCopy(zFileName,Grid1.DataSource.DataSet.FieldByName(‘pathorig’).AsString);
if not FileExists(pt) then showmessage(‘Документ по либо отсутствует, либо перемещен.’)
else ShellExecute(Handle, ‘open’,pt,nil,nil, SW_SHOWNORMAL);

Это сообщение отредактировал(а) brick-rs — 28.7.2004, 03:10

PM MAIL WWW ICQ   Вверх
Cashey
Дата 28.7.2004, 06:59 (ссылка)
| (нет голосов)
Загрузка ... Загрузка …




Быстрая цитата

Цитата

Бессмертный
****

Профиль
Группа: Завсегдатай
Сообщений: 3441
Регистрация: 13.11.2002
Где: в столице

Репутация: нет
Всего: 60

Функция возвращает дескриптор открытого приложения. Если возвращаемое значение меньше или равно 32, это указывает на ошибку. Для Windows старше 95-го эти константы означают:
0 Системе не хватает памяти, выполняемый файл испорчен или произошло ошибочное перераспределение памяти.
2 Файл не найден.
3 Путь не найден.
5 Была попытка динамически связаться с задачей, была ошибка многопроцессорного выполнения или ошибка защиты сети.
6 Библиотека требует отдельных сегментов данных для каждой задачи.
8 Недостаточно памяти для запуска приложения.
10 Ошибочная версия Windows.
11 Ошибочный выполняемый файл. Или это не приложение Windows, или ошибка в .exe файле.
12 Приложение спроектировано для другой операционной системы.
13 Приложение спроектировано для MS-DOS 4.0.
14 Неизвестный тип выполняемого файла.
15 Попытка запустить приложение, работающее только на более ранних версиях Windows.
16 Попытка запустить второй экземпляр приложения, содержащего сегменты данных, не помеченные «только для чтения».
19 Попытка запустить архивированный файл. Файл должен быть разархивирован, прежде чем его можно будет загрузить.
20 Ошибочный файл одной из DLL, требуемой для приложения.
21 Приложение требует 32-битного расширения Windows.
31 Нет приложения, связанного с файлом указанного типа, или нет файла, связанного с указанной операцией.

———————

библия учит любить ближнего, а камасутра обучает как именно

PM Jabber   Вверх
brick-rs
Дата 28.7.2004, 08:04 (ссылка)
| (нет голосов)
Загрузка ... Загрузка …




Быстрая цитата

Цитата

Новичок

Профиль
Группа: Участник
Сообщений: 11
Регистрация: 19.5.2004
Где: г. Мыски, Кемеров ская обл.

Репутация: нет
Всего: нет

а вот собственно в том то и весь вопрос как этот дескриптор проверить? не знаю еще не разу не сталкивался
sample.gif плиз можно в кратце

PM MAIL WWW ICQ   Вверх
<Spawn>
Дата 28.7.2004, 11:00 (ссылка)
| (нет голосов)
Загрузка ... Загрузка …




Быстрая цитата

Цитата

Око кары:)
****

Профиль
Группа: Экс. модератор
Сообщений: 2776
Регистрация: 29.1.2003
Где: Екатеринбург

Репутация: 1
Всего: 64

brick-rs Все просто:

Код
procedure TForm1.Button1Click(Sender: TObject);
var
 ExecResult: THandle;
begin
 ExecResult := ShellExecute(Handle, 'open', 'E:\Test.txt', nil, nil, SW_SHOWNORMAL);
 if ExecResult < HINSTANCE_ERROR then
   case ExecResult of
     0:
       ShowMessage('The operating system is out of memory or resources.');
     ERROR_FILE_NOT_FOUND:
       ShowMessage('The specified file was not found.');
     ERROR_PATH_NOT_FOUND:
       ShowMessage('The specified path was not found.');
     ERROR_BAD_FORMAT:
       ShowMessage('The .EXE file is invalid (non-Win32 .EXE or error in .EXE image).');
     <и т.д.>
   end;
end;

Все константы есть в хелпе

———————

«Для некоторых людей программирование является такой же внутренней потребностью, подобно тому, как коровы дают молоко, или писатели стремятся писать» — Николай Безруков.

PM MAIL ICQ   Вверх
Vit
Дата 28.7.2004, 20:54 (ссылка)
| (нет голосов)
Загрузка ... Загрузка …




Быстрая цитата

Цитата

Vitaly Nevzorov
****

Профиль
Группа: Экс. модератор
Сообщений: 10964
Регистрация: 25.3.2002
Где: Chicago

Репутация: 1
Всего: 207

Используй CreateProcess — гораздо больше возможностей, и по отлову ошибок в том числе…

———————

With the best wishes, Vit
I have done so much with so little for so long that I am now qualified to do anything with nothing
Самый большой Delphi FAQ на русском языке здесь: www.drkb.ru

PM MAIL WWW ICQ   Вверх
saw666
Дата 15.10.2016, 09:51 (ссылка)
 | (голосов:1)
Загрузка ... Загрузка …




Быстрая цитата

Цитата

Новичок

Профиль
Группа: Участник
Сообщений: 1
Регистрация: 15.10.2016

Репутация: нет
Всего: нет

Цитата(Cashey @ 28.7.2004,  06:59)
Для Windows старше 95-го эти константы означают:
0    Системе не хватает памяти, выполняемый файл испорчен или произошло ошибочное перераспределение памяти. 
2  Файл не найден. 
3  Путь не найден.
5    Была попытка динамически связаться с задачей, была ошибка многопроцессорного выполнения или ошибка защиты сети. 
6    Библиотека требует отдельных сегментов данных для каждой задачи. 
8    Недостаточно памяти для запуска приложения. 
10    Ошибочная версия Windows. 
11    Ошибочный выполняемый файл. Или это не приложение Windows, или ошибка в .exe файле. 
12    Приложение спроектировано для другой операционной системы. 
13    Приложение спроектировано для MS-DOS 4.0. 
14    Неизвестный тип выполняемого файла. 
15    Попытка запустить приложение, работающее только на более ранних версиях Windows. 
16    Попытка запустить второй экземпляр приложения, содержащего сегменты данных, не помеченные «только для чтения». 
19    Попытка запустить архивированный файл. Файл должен быть разархивирован, прежде чем его можно будет загрузить. 
20    Ошибочный файл одной из DLL, требуемой для приложения. 
21    Приложение требует 32-битного расширения Windows. 
31    Нет приложения, связанного с файлом указанного типа, или нет файла, связанного с указанной операцией.

Специально зарегистрировался чтобы поблагодарить за это сообщение. Да, даже в 2016 году оно оказалось полезным. Спасибо! )))

PM MAIL   Вверх
navodri
Дата 24.5.2018, 10:24 (ссылка)
| (нет голосов)
Загрузка ... Загрузка …




Быстрая цитата

Цитата

Бывалый
*

Профиль
Группа: Участник
Сообщений: 173
Регистрация: 26.10.2007

Репутация: нет
Всего: -1

Вот так просто:

Код

if ShellExecute(Handle, 'open', 'E:\Test.txt', nil, nil, SW_SHOW) < 32 then
           MessageBox(Handle, 'Невозможно выполнить ShellExecute!', Ошибка открытия', MB_OK);

PM MAIL WWW   Вверх



















Ответ в темуСоздание новой темы
Создание опроса
Правила форума «Delphi: WinAPI и системное программирование»
Snowybartram
MetalFanbems
PoseidonRrader
Riply

Запрещено:

1. Публиковать ссылки на вскрытые компоненты

2. Обсуждать взлом компонентов и делиться вскрытыми компонентами

  • Литературу по Delphi обсуждаем здесь
  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Вопросы по реализации алгоритмов рассматриваются здесь
  • 90% ответов на свои вопросы можно найти в DRKB (Delphi Russian Knowledge Base) — крупнейшем в рунете сборнике материалов по Дельфи
  • 99% ответов по WinAPI можно найти в MSDN Library, оставшиеся 1% здесь

Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Snowy, bartram, MetalFan, bems, Poseidon, Rrader, Riply.

 

0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Delphi: WinAPI и системное программирование | Следующая тема »
_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

The error handling for ShellExecute is something of a disaster. Raymond Chen discusses it here: Why does ShellExecute return SE_ERR_ACCESSDENIED for nearly everything? So, even if you can convert the handful of possible ShellExecute errors into text, you’ll find that you invariably get Access denied. And that’s not very helpful.

The bottom line is that if you want real error reporting then you need to use ShellExecuteEx. If that fails you call GetLastError to get the Win32 error. To turn it into an exception with the descriptive text, call RaiseLastOSError. If you just want the descriptive text associated with an error, you call SysErrorMessage.

I have a VB6 application which opens files with their associated application using:

ShellExecute(0, "open", filename, params, vbNullString, vbNormalFocus)

This works perfectly.

Now I got a customer (running XP with Adobe Reader) who can’t open any PDF file using the above command. But the same file is being opened without any problems when double clicking it from Windows Explorer. I also tested the filename/-path combination on my machine to exclude those kind of problems.

I’m searching for any hints on what I could check to make sure ShellExecute is working. Or what can cause ShellExecute to fail this way?

Daniel Rikowski's user avatar

asked Jul 28, 2009 at 12:47

MicSim's user avatar

5

What’s the return value of ShellExecute? If it’s 0x0000001f (== 31, meaning SE_ERR_NOASSOC), than according to shellapi.h «There is no application associated with
the given file name extension.»
, which means that somehow the registration of the .pdf file extension got lost. Reinstalling Adobe Reader might help.

answered Jul 28, 2009 at 13:02

Thomas Freudenberg's user avatar

4

Further to Thomas’s answer, here’s some VB6 constants for possible return values of ShellExecute, with possible explanations (I think I originally took these from the MSDN page, return value section). A return value of 32 or less means the call failed. The specific value returned indicates what went wrong.

Const ERROR_BAD_FORMAT = 11&
Const ERROR_FILE_NOT_FOUND = 2&          
Const ERROR_PATH_NOT_FOUND = 3&          ' The specified path was not found. '
Const SE_ERR_ACCESSDENIED = 5            ' The operating system denied access to the specified file. '
Const SE_ERR_ASSOCINCOMPLETE = 27        ' The file name association is incomplete or invalid. '
Const SE_ERR_DDEBUSY = 30                ' The Dynamic Data Exchange (DDE) transaction could not be completed because other DDE transactions were being processed. '
Const SE_ERR_DDEFAIL = 29                ' The DDE transaction failed. '
Const SE_ERR_DDETIMEOUT = 28             ' The DDE transaction could not be completed because the request timed out. '
Const SE_ERR_DLLNOTFOUND = 32            ' The specified dynamic-link library (DLL) was not found. '
Const SE_ERR_FNF = 2                     ' The specified file was not found. '
Const SE_ERR_NOASSOC = 31                ' There is no application associated with the given file name extension. '
Const SE_ERR_OOM = 8                     '  out of memory '
Const SE_ERR_PNF = 3                     '  path not found '
Const SE_ERR_SHARE = 26                  ' A sharing violation occurred. '

Community's user avatar

answered Jul 28, 2009 at 13:10

MarkJ's user avatar

MarkJMarkJ

30.1k5 gold badges69 silver badges111 bronze badges

You have «open» as the verb, don’t do that, use vbNullString as the verb («Open» means the open verb, NULL means the default verb (If the user has not set a specific default, the default is open, if there is no open verb for that filetype, ShellExecute uses the first verb it finds))

answered Jul 28, 2009 at 14:03

Anders's user avatar

AndersAnders

97.6k12 gold badges110 silver badges164 bronze badges

5

Have a look at the return value of your ShellExecute call. From the MSDN:

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.

0: The operating system is out of memory or resources.

ERROR_FILE_NOT_FOUND: The specified file was not found.

ERROR_PATH_NOT_FOUND: The specified path was not found

(…)

Community's user avatar

answered Jul 28, 2009 at 13:09

Treb's user avatar

TrebTreb

19.9k7 gold badges54 silver badges87 bronze badges

1

Instead of using ShellExecute to ‘execute’ the PDF file, I use the FindExecutable API:

Private Const ERROR_FILE_NO_ASSOCIATION     As Long = 31
Private Const ERROR_FILE_NOT_FOUND          As Long = 2
Private Const ERROR_PATH_NOT_FOUND          As Long = 3
Private Const ERROR_FILE_SUCCESS            As Long = 32 
Private Const ERROR_BAD_FORMAT              As Long = 11

Private Declare Function FindExecutable Lib "shell32.dll" _
   Alias "FindExecutableA" _
  (ByVal lpFile As String, _
   ByVal lpDirectory As String, _
   ByVal sResult As String) As Long


Private Sub OpenDocument(sFile as string, sPath as string)
     Dim sResult As String
     Dim lSuccess As Long, lPos as long

     sResult = Space$(MAX_PATH)
     lSuccess = FindExecutable(sFile, sPath), sResult)
     Select Case lSuccess
        Case ERROR_FILE_NO_ASSOCIATION
            If Right$(sFile, 3) = "pdf" Then
                MsgBox "You must have a PDF viewer such as Acrobat Reader to view pdf files."
            Else
                MsgBox "There is no registered program to open the selected file." & vbCrLf & sFile
            End If
        Case ERROR_FILE_NOT_FOUND: MsgBox "File not found: " & sFile
        Case ERROR_PATH_NOT_FOUND: MsgBox "Path not found: " & sPath
        Case ERROR_BAD_FORMAT:     MsgBox "Bad format."
        Case Is >= ERROR_FILE_SUCCESS:
           lPos = InStr(sResult, Chr$(0))
           If lPos Then sResult = Left$(sResult, lPos - 1)
           Shell sResult & " " & sPath & sFile, True), vbMaximizedFocus
    End Select

End Sub

answered Jul 30, 2009 at 20:07

C-Pound Guru's user avatar

C-Pound GuruC-Pound Guru

15.9k6 gold badges46 silver badges67 bronze badges

2

  1. Uninstall and reinstall Acrobat Reader.
  2. Under «Documents and Settings», rename «username» folder to «usernamex» (you should be logged in with different admin user).
  3. Relogin as user and it creates a new «username» folder with a new user registry.
  4. Now it should work.

You can copy files from the usernamex folder to the new username folder (Desktop, Documents, etc.)

the Tin Man's user avatar

the Tin Man

159k42 gold badges215 silver badges304 bronze badges

answered Nov 25, 2009 at 7:56

layze's user avatar

layzelayze

111 bronze badge

I’ve had the same problem and it was not possible to change the VB6-Code. So I had to find another solution…

In my case it was a file with the extension «.xyz», but in reality it was a file for Microsoft Word, like a .doc-file.

When doubleclicking for the first time, Windows asks for a programm to open the file with. After that the doubleclick worked fine. But the ShellExecute didn’t. The problem is, that the ShellExecute performs a «right click» -> «open» on the file, and «open» didn’t exist in the context menu on my .xyz-file. There was just an «edit»… So the ShellExecute worked with «edit», but not with «open» as the second parameter.

And because I was not able to change the VB6-code, I opened the registry with regedit. In the Path «HKEY_CLASSES_ROOT\.doc» the standard-value was «Word.Document.8», in «HKEY_CLASSES_ROOT\.xyz» there was just «xyz_auto_file». So I changed just this value into «Word.Document.8», and everything worked perfectly. Now I have the same context menu as with a .doc-file, when I right-click on my .xyz-file.

And also the ShellExecute works perfectly…

answered Sep 10, 2014 at 11:48

user3256508's user avatar

1

I had the same issue with an existing program that is using the verb open instead of NULL when calling the ShellExecute function. I was able to fix the problem by adding the open verb like described here using the registry editor into the .pdf handler (mine was at HKEY_CLASSES_ROOT\pdf_auto_file). I think this is a problem within the Adobe Reader installer that is sometimes not adding the open verb during installation.

Here is the export of the registry values I added:

[HKEY_CLASSES_ROOT\pdf_auto_file\shell\Open\command]
@="\"C:\\Program Files\\Adobe\\Reader 11.0\\Reader\\AcroRd32.exe\" \"%1\""

answered Mar 4, 2015 at 17:41

Martin's user avatar

MartinMartin

10.8k14 gold badges59 silver badges67 bronze badges

I encountered the same problem as the OP within a compiled Visual Foxpro 9 application after updating from W7x64 to W10 public release.

I have Adobe Acrobat installed as well as Adobe Reader. Changing the default .pdf association from Reader to Acrobat and… It all works! Changing back to Reader with original failure (error code 31 — «There is no application associated with the given file-name extension.»). Beats me, but fortunately I don’t have to worry. I’m too old to care and will require all sites to stay with W7.

Either association works from file explorer

answered Sep 2, 2015 at 21:04

Geriatrix's user avatar

Calling a Unicode version (ShellExecuteW) from an command which only supports ANSII, experienced with a recent version of Inno Setup. ShellExecuteW worked for some ANSII string arguments but in this case not the required one, returning 2 (see below).
As a matter of interest, in either ANSII or Unicode, Inno’s internal function ShellExec also failed with code 5 for the reason the compiling process still had an open handle to the file.

answered Jun 26, 2017 at 17:50

Laurie Stearn's user avatar

Laurie StearnLaurie Stearn

9591 gold badge13 silver badges34 bronze badges

Here’s a function that translates a the windows error numbers to text. You can use the return value as the parameter and get back a more friendly message.

Private Declare Function FormatMessage Lib "kernel32" Alias "FormatMessageA" _
    (ByVal dwFlags As Long, lpSource As Long, ByVal dwMessageId As Long, _
    ByVal dwLanguageId As Long, ByVal lpBuffer As String, _
    ByVal nSize As Long, ByVal Arguments As Any) As Long

Private Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
Private Const FORMAT_MESSAGE_IGNORE_INSERTS = &H200
Private Const MAX_PATH = 260

Function TranslateDLLError(ByVal lngErrNum As Long) As String
   Dim sRtrnCode As String * MAX_PATH
   Dim lRet As Long

   On Error GoTo errTranslateDLLError(

   sRtrnCode = Space$(256)
   lRet = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM Or FORMAT_MESSAGE_IGNORE_INSERTS, 0&, lngErrNum, 0&, sRtrnCode, Len(sRtrnCode), 0&)
   If lRet > 0 Then
      Translate_DLL_Error = Replace$(Left(sRtrnCode, lRet), vbCrLf, "")
   Else
      Translate_DLL_Error = "Error not found."
   End If

   Exit Function

errTranslateDLLError(:
   TranslateDLLError( = "Unable to translate system error: " & CStr(lngErrNum)

End Function

answered Nov 25, 2009 at 13:18

jac's user avatar

jacjac

9,6562 gold badges35 silver badges63 bronze badges

Try this.
You have to associate the PDF file with any program (such as Acrobat x) to read PDF’s, then you can open PDF files with ShellExecute.

user's user avatar

user

86.9k18 gold badges197 silver badges190 bronze badges

answered May 10, 2012 at 14:24

Wilson Arguello's user avatar

1

Понравилась статья? Поделить с друзьями:
  • Shell32 dll ошибка при печати
  • Side light ошибка бмв
  • Shell ошибка при запуске
  • Sidebyside что это ошибка код 33
  • Sidebyside ошибка 33 как исправить