Абсолютное ("полностью квалифицированное") имя начинается с имени диска или сервера и указывает все компоненты пути, например: "
C:\Projects\TestProject\Data.txt
" или "\\SERVER\Projects\TestProject\Data.txt
". Такое имя всегда однозначно указывает на файл - вне зависимости от любых внешних факторов.Относительное имя содержит не все компоненты пути и указывает файл относительно другого каталога, имя которого в самом имени не указано, например: "
Data.txt
" или "..\Data.txt
". Для определения точного положения файла недостаточно одного относительного имени, необходимо ещё имя каталога, относительно которого будет трактоваться это имя. Поэтому один и тот же относительный путь может ссылаться на разные файлы. К примеру, путь "Data.txt
" ссылается на C:\Projects\TestProject\Data.txt
, если текущий каталог (или каталог, относительно которого происходит разрешение имени) равен C:\Projects\TestProject
, но этот же путь будет ссылаться на C:\Windows\Data.txt
, если текущий каталог - C:\Windows
.Подробнее о файловых именах можно почитать здесь.
Здесь же, в этой статье, я хочу показать, что вам никогда не нужно использовать относительные имена файлов.
Суть проблемы
Очень часто начинающие программисты используют относительные пути к файлам для работы с файлами внутри папки своей программы, например:Assign(F, 'input.txt'); Reset(F); // ...Что не так с этим кодом?
Начинающий программист считает, что имя "
input.txt
" будет вычисляться относительно пути к его программе. Иными словами, если программа (.exe-файл) лежит в "C:\Projects\TestProject
", то, указав "input.txt
" в Assign
, мы откроем файл "C:\Projects\TestProject\input.txt
". Это попросту неверно!Все относительные пути вычисляются относительно т.н. текущего каталога. Проблема состоит в том, что начинающие программисты не понимают, что это такое. Они считают, что текущий каталог - это каталог программы. Это не так:
- Для начала, текущий каталог при старте вашей программы задаётся не вами, а вызывающим вас процессом. Все функции запуска программы имеют строковый параметр для передачи туда имени каталога, который станет текущим для запускаемой программы. И вызывающая вас программа может передать туда всё, что угодно. Это может быть папка с вашей программой, да. Но это может быть и любая другая папка;
- Далее, к примеру, если ваша программа запускается через ярлык на рабочем столе или ярлык в меню Пуск / Программы, то текущий каталог для вашей программы указан в свойствах ярлыка. Например, сама Delphi запускается с текущим каталогом = папке с проектами (например,
C:\Program Files\Borland\Delphi 7\Projects\
или даже простоC:\Projects\
) - что, очевидно, не равно папке с программой (C:\Program Files\Borland\Delphi 7\Bin\
); - А если вы пишете программу, которая открывает файлы какого-то типа, то вы, вероятно, назначите свою программу для открытия таких файлов (ассоциируете тип файлов в вашей программой). Но когда пользователь дважды-щёлкнет по такому файлу, ваша программа запустится с текущим каталогом равным каталогу открываемого файла. Т.е. текущий каталог будет
C:\Documents
, а неC:\Projects\TestProject
; - А если вы пишете код для службы (Win32 Service), то текущий каталог будет
C:\Windows\System32
.
P.S. Кроме того, размещение файлов с данными/конфигурацией в папке с программой - крайне плохая идея, если только вы не пишете портативную (portable) программу (см. также).
Далее, есть же такие функции как
GetCurrentDir
и (что интереснее) SetCurrentDir
. "Set" решительным образом намекает на то, что текущий каталог - вещь не фиксированная и его можно менять. Иными словами, текущий каталог не только может быть не равен каталогу программы, но и вообще может меняться в процессе выполнения программы! Действительно:Assign(F, 'input.txt'); Reset(F); // <- OK, нет ошибки // ...
SetCurrentDir('C:\Windows'); Assign(F, 'input.txt'); Reset(F); // <- возбуждает ошибку "файл не найден", поскольку файла C:\Windows\input.txt нет // ...
Конечно, тот факт, что текущий каталог можно менять, сам по себе ещё не означает проблему. Но посмотрите на такой код:
if not OpenDialog1.Execute then Exit; OutputFileName := OpenDialog1.FileName; Assign(F, 'input.txt'); Reset(F); // <- возбуждает ошибку "файл не найден" // ...Что случилось? Дело в том, что внешний код (а именно - код диалога открытия файла) поменял текущий каталог. Ваш код оказался не готов к этому.
P.S. Почему вообще диалог открытия файла меняет текущий каталог? Потому что вы (= прикладные программисты) пишете код с использованием относительных имён файлов.Иными словами, проблема состоит в том, что кто угодно может менять текущий каталог в любой момент времени. Даже если в вашем коде нет вызовов другого внешнего кода, который меняет текущий каталог, всё равно текущий каталог может быть изменён другим потоком в вашей программе. Даже если вы сами не создаёте других потоков, потоки могут быть созданы DLL, которые загружены в вашу программу. Помимо системных DLL, это могут быть DLL от любых оконных ловушек, расширителей оболочек и даже антивирусов.
Вывод? Код, который адресует файл относительным именем работает благодаря случайности, а именно: благодаря тому, что никакой другой код не изменил текущий каталог перед тем, как вы вызвали функцию доступа к файлу, передав ей относительное имя файла.
Неправильные решения
Прежде чем мы посмотрим на правильное решение, давайте сделаем обзор того, чего делать не нужно.Многие либо первым действием в программе, либо непосредственно перед выполнением участка кода с использованием относительных имён явно меняют текущий каталог на каталог программы. Например:
AppPath := ExtractFilePath(ParamStr(0)); SetCurrentDir(AppPath); // AppPath = 'C:\Projects\TestProject\' Assign(F, 'input.txt'); Reset(F); // <- OK, нет ошибки // ...Или же сохраняют/восстанавливают текущий каталог перед вызовом кода, который потенциально может менять текущий каталог, например:
CurrentDir := GetCurrentDir; if not OpenDialog1.Execute then Exit; OutputFileName := OpenDialog1.FileName; SetCurrentDir(CurrentDir); Assign(F, 'input.txt'); Reset(F); // <- OK, нет ошибки // ...
Что не так с этими решениями?
Во-первых, текущий каталог, являясь глобальной переменной, может быть изменён другим потоком как раз между вызовами
SetCurrentDir
и Assign
. Если этого не происходит, то ваш (некорректный) код работает благодаря случайности (случайность состоит в том, что этого не произошло).Во-вторых, если вы форсированно устанавливаете свой текущий каталог, отбрасывая каталог, заданный вам вызывающим, вы можете открыть не тот файл! Например, пусть вы пишете программу-конвертер, пусть она конвертирует картинки из формата .png в формат .jpg, пусть вашу программу можно вызвать, передав ей имя файла в командной строке. Тогда пользователь может вызвать вас так:
C:\Documents>"C:\Converter\convert.exe" "holidays.png"
(здесь C:\Documents>
является приглашением командной строки, а "C:\Converter\convert.exe" "holidays.png"
- непосредственно командной строкой).Т.е. пользователь открыл консоль, он находится в папке
C:\Documents
и вызывает вас (convert.exe
) из папки C:\Converter
, передавая вам имя файла (holidays.png
) параметром командной строки. (Почти аналогичная ситуация будет если пользователь дважды-щёлкнет по файлу
holidays.png
в открытой папке C:\Documents
в Проводнике - при условии, что ваша программа ассоциирована с .png файлами).В этом случае ваша программа (
convert.exe
) запустится из папки C:\Converter
, но текущим каталогом для неё будет C:\Documents
. Если вы форсированно смените текущий каталог на папку с программой (C:\Converter
), то не сможете открыть файл holidays.png
, поскольку он находится в папке C:\Documents
, а не C:\Converter
.В-третьих, если вы передаёте имена файлов между процессами, то текущий каталог в вашей программе никак не связан с текущим каталогом в программе, с которой вы общаетесь. Например, самый типичный случай: пусть вы пишете просмотрщик файлов, ваша программа ограничена одним экземпляром. Если вас запускают на просмотр файла, вы проверяете, не открыты ли вы уже, если да - то вы передаёте в первый экземпляр имя файла и выходите.
Имя файла вам могли передать в относительном формате (к примеру, вызвали вас из командной строки). Вы передали это имя "как есть" в первую копию своей программы. Однако текущий каталог первой копии равен неизвестно чему, он не равен текущему каталогу вашей второй копии. Как бы вы не меняли текущий каталог в первой копии программы перед попыткой доступа к файлу, который вам передали, вы никак не можете знать, каким же был текущий каталог в другой программе (вашей второй копии). И снова, ваш код, оперирующий относительными путями файлов, будет работать только благодаря случайности (в этом случае случайность состоит в том, что текущий каталог оказался одинаков в обоих экземплярах вашей программы).
Более того, когда вы меняете текущий каталог на какой-то - этот каталог открывается вашей программой. В частности, это означает, что вы не можете удалить этот каталог.
Зачем нужны относительные пути?
Зачем же вообще относительные пути, если они так плохи?Ну, относительные пути нужны для человека-оператора. Они экономят время на набор текста. Действительно, вместо того, чтобы вводить длинный путь вида
C:\Documents and Settings\Admin\Documents\Data from 2014\March.doc
- вы можете ввести просто March.doc
(конечно же, при условии, что вы "находитесь" в каталоге C:\Documents and Settings\Admin\Documents\Data from 2014\
).Правильное решение
Из вышесказанного напрямую следует правильное решение. Раз относительные имена предназначены для человека, то вам (вашему коду) не следует их использовать. Всё, что вы можете сделать с относительным именем - перевести его в абсолютное. И оперировать в дальнейшем только абсолютным именем файла.Для этого вам понадобится функция нормализации внешних данных, которая преобразует относительное имя файла, принятое от пользователя, в абсолютное, пригодное для использования в коде программы. Как минимум, функция нормализации должна преобразовывать относительное имя файла в абсолютное. Но помимо этого желательно также выполнить и другие операции. Вот конкретный список действий:
- Развернуть переменные окружения;
- Преобразовать относительное имя в абсолютное;
- Канонизировать путь, свернув '.', '..' и лишние разделители каталогов;
- Преобразовать короткий путь в длинный.
А пока давайте сформулируем свод правил, которому вам стоит придерживаться:
- Каждый раз, когда вы получаете имя файла из внешнего источника (командной строки, конфигурации, диалога и т.п.) - сохраняйте его в переменную с суффиксом
Unsafe
. Например,DocumentFileNameUnsafe := ParamStr(1)
; - Не передавайте переменные с суффиксом
Unsafe
в функции открытия файлов; - Не передавайте переменные с суффиксом
Unsafe
в другие программы (через IPC, командную строку и т.п.); - Вы можете сохранять переменные с суффиксом
Unsafe
в файл конфигурации; - Передайте переменную с суффиксом
Unsafe
в функцию нормализации и сохраните результат в переменной без суффиксаUnsafe
. Например,DocumentFileName := PathSearchAndQualify(DocumentFileNameUnsafe)
; - Вы можете передавать переменные без суффикса
Unsafe
в функции открытия файлов и другие программы (IPC, командная строка и т.п.); - Если вам точно известно имя файла (оно задано константой в коде) и вы знаете папку, в которой лежит файл (не обязательно константа, но хотя бы логическое размещение вида "каталог программы", "подкаталог ABC папки Application Data"), то получите путь к каталогу, затем добавьте к нему имя файла и сохраните результат в переменную без суффикса
Unsafe
. Если имя файла, заданное в константе, содержит '.' или '..' - выполните нормализацию перед сохранением в переменную; - Измените текущий каталог на, скажем,
C:\Windows\System32
(разумеется, путь надо задавать не константой, а получать черезGetSystemDirectory
) сразу после того, как вы нормализовали все имена файлов из параметров командной строки; - Если вы запускаете внешнюю программу, передавая ей имя файла для открытия, то задайте текущий каталог для запускаемой программы равным каталогу, содержащему открываемый файл (даже хотя вы передаёте полное имя файла);
- Используйте суффикс
Dir
для переменных и функций, которые хранят/возвращают путь (к каталогу) без ведомого разделителя (например, 'C:\Windows'). Используйте суффиксPath
для переменных и функций, которые хранят/возвращают путь с ведомым разделителем (например, 'C:\Windows\'). Избегайте использования переменных и функций, для которых вы не знаете, будет ли в конце пути разделитель. Преобразуйте такие переменные и функции вDir
илиPath
с помощьюExcludeTrailingPathDelimiter
иIncludeTrailingPathDelimiter
соответственно, например:CurrentPath := IncludeTrailingPathDelimiter(GetCurrentFolder)
. Эта семантика сDir
/Path
защитит вас от неверных результатов видаExtractFileDir(...) + 'input.txt' = 'C:\Programinput.txt'
илиExtractFilePath(...) + '\input.txt' = 'C:\Program\\input.txt'
. - Старайтесь хранить имена каталогов с ведущим разделителем, а имена файлов - без разделителя. Например, 'C:\Windows\', но 'C:\Windows\notepad.exe';
- Если вы работаете в Unicode-версии Delphi (Delphi 2009 и выше) и хотите передать имя файла во внешний код (программу или DLL) - преобразуйте имя файла в короткое имя файла (
PathGetShortPath
- см. ниже). Это увеличит шансы правильного открытия файла, если вызываемый код не поддерживает Unicode или неверно обрабатывает пробелы; - Если вы передаёте имя файла по IPC - всегда предпочитайте Unicode-форму (используйте
WideString
).
Тем не менее, существует один случай, когда вам нужно использовать в своём коде относительные пути. Речь идёт о сохранении путей в "конфигурацию" относительно некого корневого элемента. Например, это может быть портативная (portable) программа, сохраняющая пути к файлам, относительно каталога с программой. Или это может быть многофайловый документ. Например, проект Delphi сохраняется в .dpr файл (а его настройки - в .dproj файл в том же каталоге). При этом настройки проекта Delphi сохраняют пути до файлов проекта в относительной форме - пути рассчитываются относительно каталога с .dpr/.dproj.
И если у вас возникает аналогичная ситуация, то действовать следует так:
- Проведите нормализацию имён файлов, как указано в алгоритме выше, сохранив их в переменные без суффикса
Unsafe
; - Получите каталог, относительно которого вам нужно сохранять пути (каталог с программой, каталог с корневым файлом документа и т.п.). Нормализуйте его и сохраните в переменную без суффикса
Unsafe
; - Получите относительный путь для вашего пути файла из п1 относительно каталога из п2 с помощью функции
PathGetRelativePath
(см. ниже раздел практики). Сохраните результат в переменную с префиксомUnsafe
; - Запишите переменную из п3 в вашу конфигурацию или документ.
Практические примеры
Давайте посмотрим, как эти рекомендации нужно делать на примерах.Реализация: голая Delphi
Во-первых, даже в Delphi "из коробки" есть несколько подходящих функций (некоторые функции могут отсутствовать в старых версиях Delphi):{ ExpandFileName expands the given filename to a fully qualified filename. The resulting string consists of a drive letter, a colon, a root relative directory path, and a filename. Embedded '.' and '..' directory references are removed. } function ExpandFileName(const FileName: string): string; overload; { ExpandUNCFileName expands the given filename to a fully qualified filename. This function is the same as ExpandFileName except that it will return the drive portion of the filename in the format '\\servername\sharename if that drive is actually a network resource instead of a local resource. Like ExpandFileName, embedded '.' and '..' directory references are removed. } function ExpandUNCFileName(const FileName: string): string; overload; { ExtractRelativePath will return a file path name relative to the given BaseName. It strips the common path dirs and adds '..\' on Windows, and '../' on Linux for each level up from the BaseName path. Note: Directories passed in should include trailing backslashes} function ExtractRelativePath(const BaseName, DestName: string): string; overload; { IsRelativePath returns a boolean value that indicates whether the specified path is a relative path. } function IsRelativePath(const Path: string): Boolean; { ExtractShortPathName will convert the given filename to the short form by calling the GetShortPathName API. Will return an empty string if the file or directory specified does not exist } function ExtractShortPathName(const FileName: string): string;
Реализация: JCL
Во-вторых, хочу заметить, что если вы используете JCL (JEDI Code Library), то весь код у вас уже есть - в файлеJclFileUtils
:
// Возвращает длинное имя по короткому function PathGetLongName(const APath: string): string; // Возвращает короткое имя по длинному function PathGetShortName(const APath: string): string; // Конвертирует абсолютное имя в относительное function PathGetRelativePath(const AOrigin, ADestination: string): string; // Канонизирует путь, удаляя из него специальные каталоги '.' и '..' function PathCanonicalize(const APath): string; // Возвращает True, если путь - абсолютный function PathIsAbsolute(const APath: string): Boolean; // Возвращает True, если путь ABase содержится в APath function PathIsChild(const APath, ABase: string): Boolean; function PathIsEqualOrChild(const APath, ABase: string): Boolean; // Возвращает True, если путь начинается с диска function PathIsDiskDevice(const APath: string): Boolean; // Возвращает True, если путь начинается с имени сервера function PathIsUNC(const APath: string): Boolean;И хотя здесь нет
PathGetAbsolutePath
/ExpandFileName
, но эта функция тривиальна:
// Конвертирует относительное имя в абсолютное function PathGetAbsolutePath(const APath: string; const ABase: string = ''): string; var BaseDir: string; begin if PathIsAbsolute(APath) then begin Result := APath; Exit; end; if ABase = '' then BaseDir := ExcludeTrailingPathDelimiter(GetCurrentDir) else BaseDir := ExcludeTrailingPathDelimiter(PathGetAbsolutePath(ExcludeTrailingPathDelimiter(ABase))); if APath = '' then Result := BaseDir else if APath[1] = PathDelim then Result := BaseDir + APath else Result := IncludeTrailingPathDelimiter(BaseDir) + APath; end;
Реализация: системные функции
В-третьих, я также предлагаю вам воспользоваться функциями системы. Заметьте, что хотя эти функции относятся к функциям Оболочки (Shell), они также относятся к т.н. группе "легковесных вспомогательных функций" (Shell Lightweight Utility Functions) и импортируются изShlwAPI.dll
, а не из ShellAPI.dll
. В частности, это означает, что у них нет тяжёлых зависимостей и им не нужен COM - в отличие от высококоуровневых функций Оболочки.И для этого я предлагаю создать отдельный модуль (File / New / Other / Unit) и сохранить его, скажем, с именем
ShellFileSupport.pas
. В этот модуль мы поместим весь вспомогательный код. Сложные функции мы будем импортировать из системы, а простые функции напишем сами. Вот какие функции мы реализуем:type TDriveNumber = 0..25; TPathCharType = (gctInvalid, gctLFNChar, gctSeparator, gctShortChar, gctWild); TPathCharTypes = set of TPathCharType; TCleanupResult = (pcsReplacedChar, pcsRemovedChar, pcsTruncated); TCleanupResults = set of TCleanupResult; PCleanupResults = ^TCleanupResults; const InvalidDrive = TDriveNumber(-1); // Возвращает тип символа из пути function PathGetCharType(const AChar: Char): TPathCharTypes; // Возвращает номер диска из пути (InvalidDrive при ошибке) function PathGetDriveNumber(const APath: String): TDriveNumber; // Формирует путь к корневому каталогу заданного диска function PathBuildRoot(const ADrive: TDriveNumber): String; // Канонизирует путь, удаляя из него специальные каталоги '.' и '..' function PathCanonicalize(const APath: String): String; // Соединяет два пути, добавляя, при необходимости, разделитель пути function PathAppend(const APath, AMore: String): String; // Аналог PathAppend, но возвращает каноничный путь (с удалёнными '.' и '..') function PathCombine(const APath, AMore: String): String; // Возвращает True, если указанный путь (файл/каталог) существует // Реализуем на случай, если вы не хотите использовать бажный FileExists/DirectoryExists из Delphi // См. // http://qc.embarcadero.com/wc/qcmain.aspx?d=3513 // http://qc.embarcadero.com/wc/qcmain.aspx?d=10731 // http://qc.embarcadero.com/wc/qcmain.aspx?d=52905 function PathFileExists(const APath: String): Boolean; // включает в себя и файл и каталог function PathIsDirectory(const APath: String): Boolean; // Возвращает True, если путь не содержит разделителей пути (':' и '\') function PathIsFileSpec(const APath: String): Boolean; // Возвращает True, если путь - относительный function PathIsRelative(const APath: String): Boolean; // Возвращает True, если путь - абсолютный function PathIsAbsolute(const APath: String): Boolean; // Заключает строку в кавычки при необходимости (наличие пробелов) function PathQuoteSpaces(const APath: String; const AForce: Boolean = False): String; // Формирует относительный путь к ATo из (относительно) AFrom (ведомый '\' обозначает каталог) function PathRelativePathTo(const AFrom, ATo: String): String; // Разрешает относительное имя в абсолютное, дополнительно канонизируя путь function PathSearchAndQualify(const APath: String): String; // Возвращает короткое имя по длинному function PathGetShortPath(const APath: String): String; // Возвращает длинное имя по короткому function PathGetLFNPath(const APath: String): String; // Возвращает True, если путь - допустим function PathIsValid(const APath: String): Boolean; // Создаёт командную строку для запуска программы. Результат этой функции можно передавать в CreateProcess function PathProcessCommand(const AProgram: String; const AParameters: array of String): String;Заметьте, что некоторые функции мы реализуем сами, поэтому они работают несколько иначе, чем системные. Кроме того, для некоторых функций мы добавляем дополнительный функционал. Всё это сделано для того, чтобы упростить использование функций. Дело в том, что эти функции несколько узко-специализированы. Например, системный
PathQuoteSpaces
не обрабатывает кавычки внутри строки, а PathCanonicalize
не преобразует некорректные разделители каталогов. Поэтому в своих функциях мы дополнительно исправляем эти упущения.Взять готовый модуль можно здесь, а пример работы функций посмотреть здесь.
P.S. К сожалению, Delphi не поддерживает передачу кавычек в параметрах командной строки. В этом случае функцииPathQuoteSpaces
иPathProcessCommand
используют семантику C runtime: кавычки внутри командной строки защищаются символом '\'. Но в любом случае такие параметры нельзя будет прочитать внутри Delphi-программы, если только вы не реализуете свой собственный разбор командной строки. Но зато их может прочитать другая программа, которая используетCommandLineToArgvW
.
Реализации: вывод
Так или иначе, у вас есть богатый выбор: вы можете использовать встроенные функции, свои собственные полностью реализованные функции (взяв готовые из библиотеки JCL или написав свои) или же вы можете использовать системные функции, импортировав их изShlwAPI.dll
.Вот как может выглядеть "функция нормализации", упомянутая выше, в разделе "Правильное решение":
// Delphi function SafePath(const APath: String): String; begin Result := ExpandFileName(APath); end;
// JCL (JclFileUtils + JclSysInfo) function SafePath(const APath: String): String; var Path: String; begin Path := APath; ExpandEnvironmentVar(Path); Result := PathGetLongName(PathCanonicalize(PathGetAbsolutePath(Path))); end;
// Shell function SafePath(const APath: String): String; begin Result := PathGetLFNPath(PathSearchAndQualify(APath)); end;А вот также упомянутая функция создания относительного пути:
// Delphi function UnsafePath(const ARootPath, ATarget: String): String; begin Result := ExtractRelativePath(ARootPath, ATarget); end;
// JCL (JclFileUtils + JclSysInfo) function UnsafePath(const ARootPath, ATarget: String): String; begin Result := PathGetRelativePath(ARootPath, ATarget); end;
// Shell function UnsafePath(const ARootPath, ATargetPathUnsafe: String): String; begin Result := PathRelativePathTo(ARootPath, ATarget); end;Здесь
ARootPath
- каталог с программой, документом и т.п., относительно которого нужно создать путь. ATarget
- имя файла, которое нужно сохранить в конфигурацию, документ и т.п. Result
- собственно результат, который нужно сохранить. Например:PathUnsafe := UnsafePath('C:\Windows\System32\', 'C:\Windows\Temp\input.txt'); // PathUnsafe = '..\Temp\input.txt'
Да, вам также понадобится функция, чтобы "увести" текущий каталог на безопасное место. Вот подходящий код:
function GetWindowsSystemPath: String; var Required: Cardinal; begin Result := ''; Required := GetSystemDirectory(nil, 0); if Required <> 0 then begin SetLength(Result, Required); SetLength(Result, GetSystemDirectory(PChar(Result), Required); end; if Result <> '' then Result := IncludeTrailingPathDelimiter(Result); end; procedure ResetCurrentDirectory; begin SetCurrentDir(GetWindowsSystemPath); end;
Примеры кода
Открываем файл в известной папке
Неправильный код:// ВНИМАНИЕ: код ниже не корректен Assign(F, 'input.txt'); Reset(F); // ...А вот правильный вариант этого кода:
FileName := ExtractFilePath(ParamStr(0)) + 'input.txt'; AssignFile(F, FileName); Reset(F); // ...или:
FileName := GetApplicationDataPath + 'MyApplication\input.txt'; ForceDirectories(ExtractFilePath(FileName)); AssignFile(F, FileName); Reset(F); // ...Где
GetApplicationDataPath
- ваша функция получения пути к папке Application Data (подробнее). Читаем имя файла из командной строки
Неправильный код:// ВНИМАНИЕ: код ниже не корректен function ProcessCommandLine: Boolean; begin Result := (ParamCount > 0); if not Result then Exit; OpenDocument(ParamStr(1)); end;А вот правильный вариант этого кода:
function ProcessCommandLine: Boolean; var FileName: String; FileNameUnsafe: String; begin Result := (ParamCount > 0); if not Result then Exit; FileNameUnsafe := ParamStr(1); FileName := SafePath(FileNameUnsafe); OpenDocument(FileName); ResetCurrentDirectory; end;Здесь
OpenDocument
- какая-то ваша функция открытия файла (например, загрузить .png файл и показать его на форме). Предполагается, что вы вызовете ProcessCommandLine
при запуске программы, например:procedure TMainForm.FormCreate(Sender: TObject); begin if not ProcessCommandLine then ClearDocument; // тоже какая-то ваша функция (если надо) end;(в примере выше
ProcessCommandLine
удобно сделать методом главной формы)Просмотрщик: передаём имя файла в первый экземпляр программы
Неправильный код:// ВНИМАНИЕ: код ниже не корректен function ProcessCommandLine: Boolean; begin Result := (ParamCount > 0); if not Result then Exit; if PassFileToFirstInstance(ParamStr(1)) then TerminateProcess(GetCurrentProcess, 0) else OpenDocument(ParamStr(1)); end;А вот правильный вариант этого кода:
function ProcessCommandLine: Boolean; var FileName: String; FileNameUnsafe: String; begin Result := (ParamCount > 0); if not Result then Exit; FileNameUnsafe := ParamStr(1); FileName := SafePath(FileNameUnsafe); if PassFileToFirstInstance(FileName) then TerminateProcess(GetCurrentProcess, 0) else OpenDocument(FileName); ResetCurrentDirectory; end; procedure TMainForm.FormCreate(Sender: TObject); begin if not ProcessCommandLine then ClearDocument; end;Здесь
PassFileToFirstInstance
- ваша функция, которая проверяет, запущена ли уже ваша программа, и если да, то передаёт ей имя файла. Она может выглядеть как-то так (показаны фрагменты):type PIPCSharedData = ^TIPCSharedData; TIPCSharedData = packed record MainProcessPID: DWord; MainProcessWND: HWND; end; const IPCName = '28A68ADBB80941648FDABF83E6EC5E7E'; замените на своё значение! (Ctrl + Shift + G в редакторе кода) function PassFileToFirstInstance(const AFileName: WideString): Boolean; // ... begin SharedMemHandle := CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE, 0, SizeOf(TIPCSharedData), IPCName); Win32Check(SharedMemHandle <> 0); if ((SharedMemHandle <> 0) and (GetLastError = ERROR_ALREADY_EXISTS)) then begin P := MapViewOfFile(SharedMemHandle, FILE_SHARE_READ, 0, 0, 0); Win32Check(P <> nil); try // ... MPWnd := P^.MainProcessWND; MPPID := P^.MainProcessPID; if MPWnd <> 0 then begin AllowSetForegroundWindow := GetProcAddress(GetModuleHandle('user32.dll'), 'AllowSetForegroundWindow'); if Assigned(AllowSetForegroundWindow) then AllowSetForegroundWindow(MPPID); FillChar(CDP, SizeOf(CDP), 0); CDP.dwData := 1; CDP.lpData := Pointer(AFileName); CDP.cbData := Length(AFileName) * SizeOf(AFileName[1]); Result := (SendMessage(MPWnd, WM_COPYDATA, 0, Integer(@CDP)) > 0); end; // ... finally UnmapViewOfFile(P); end; // ... end;
и не лень, тебе шизику, такое выдумывать?
ОтветитьУдалитья люблю шмар жарить, потому что они огонь!
и не лень, тебе жарителю шмар, такое читать?
УдалитьПри открытии программы нужно копировать путь ExtractFilePath(ParamStr(0)) или ExtractFileDir(Application.ExeName) в переменную и везде подставлять к пути файлов
ОтветитьУдалить+1
Удалитьа помимо этого еще и GetCurrentDIr сохранять в переменной при старте. И подставлять там, где нужно открыть файл относительно текущей папки на момент запуска. (как защиту от диалогов)
Я у себя весь код по работе с папками обычно выношу в отдельный класс, с методами для получения имён файлов и папок.
ОтветитьУдалитьПримерно так:
type
TLazyDirs = class
strict private
FTempDirs: TStringList;
FStartupDirectory: string;
public
class function GetUserSettingsFolderOld: string;
class function GetUserSettingsFolder2: string;
class function GetBuildOptionsPresetsFileName: string;
class function GetCustomDelphiInstallationsPresetsFileName: string;
...
class function GetLazyBuilderIniFilename: string;
class function GetDefaultLazyProfileFilename: string;
class function GetFormSizesFilename: string;
function GenNewUnexistingTempDirName(const aDeleteDirOnAppExit: Boolean): string;
procedure DeleteAllCreatedTempDirs;
constructor Create; virtual;
destructor Destroy; override;
end;
Помимо группировки, это еще и упрощает автоматическую миграцию настроек (считали из старого места, сохранили в новое).
Это реальный бред сивой кобылы. Профилирование путей из покон веков делается относительными CurDir по причине расширения программы относительно пользователей и методов старта. Например если одним компьютером пользуется несколько пользователей и exe файл храниться в C:\Program Files\MyApp\myapp.exe то неужели все пользователи должны получать конфиг из Program Files? А как писать конфиг пользователю у которого нет и не будет Admin Rights? Во первых это экономия char* буффера, во вторых программа не обязана работать только с одними и теми-же файлами. В третьих программа ДОЛЖНА ЗНАТЬ свою папку запуска в любом случае. При правильном чтении/установке текущего пути никаких таких косяков быть не должно.
ОтветитьУдалитьДля примера скажу, я работаю в компании где работает около 1400 сотрудников, из них около 700 пользуется так называемым TeamServer через Remote Desktop Protocol. Некоторый из нашего софта определяет текущую папку запуска, и профилирует настройки и доступы относительно текущего пользователя, а не засирает папки общего назначения. Это помогает системным администраторам правильно удалять старые файлы пользователей. Aleksey Timohin правильно написал - НУЖНО ИСПОЛЬЗОВАТЬ directory helper. который бы правильно знал где, и что лежит.
И чем же использование полного абсолютного пути мешает адресовать файл, скажем, в AppData?
УдалитьМсье знает толк... то кричит про устаревшие функции, то спокойно приводит овердохрена кода с использованием Assign и Reset. Может быть стоит слезть с палёной Delphi7 ?
ОтветитьУдалитьСлова "...начинающие программисты используют..." никаких мыслей в голове не вызывают? :)
УдалитьПосле фразы "P.S. Кроме того, размещение файлов с данными/конфигурацией в папке с программой - крайне плохая идея" стало понятно откуда берутся такие горе-программисты, которые засирают AppData своей фигней (и которая, конечно же, после анинсталла проги там и остается).
ОтветитьУдалитьНормальные люди определяют путь к екзешнику, имя юзера и оппа! файл вида "конфиг_юзернэйм.***" в каталоге с программой готов.
Рекомендую подтянуть матчасть.
УдалитьРекомендую дальше "учить" писать подобное. Потому что, когда приезжаешь на вызов по поводу "помогите нет места на диске" и видишь засранный остатками "чудо"-программам от "чудо"-программистов каталог AppData - душа аж радуется. 5 минут на чистку, 55 минут на глубокомысленное сидение за компом, оплата - за час работы.
УдалитьСоглашусь. Идеи AppData и реестра были бы хорошими, если бы система хоть как-то следила за соответствиями между программами и их данными (да вообще внесёнными в систему изменениями). В Андроиде и в браузерах с этим чуть строже.
УдалитьВ этом блоге (по крайней мере, в переводах Чена) пару раз проскакивала мысль, что это в порядке вещей, «для удобства пользователя» сохранить некую информацию о программе перманентно, чтобы при переустановке она могла сказать «оп-па, я уже всё знаю» и перенять прежние настройки. Может быть, в идеальном мире это и работает. Но с тем бардаком, что есть, возможность удалить программу полностью и навсегда (в т. ч. переустановить с нуля) мне кажется более приоритетной. Некоторые предоставляют импорт-экспорт настроек самостоятельно.
Поддерживаю. И сам уже наелся с относительными путями (после чего вытравливаю везде), и коллеги до сих пор мучаются. Текущая директория - глобальная переменная аж в рамках системы, поэтому рекомендация ее НЕ применять на порядок сильнее, чем касательно глобальных переменных.
ОтветитьУдалитьЕдинственное замечание - по поводу расширения переданных в параметрах путей. Мне кажется, что здесь произвол программы неуместен и вызывающий должен получить то, что заказывал. Как пример:
cd c:\mydocs
megaeditor.exe readme.txt
А megaeditor расширяет readme.txt до своей папки и либо ругается на отсутствие файла, либо открывает не то.