Total Commander Knowledge Base

Есть вопрос?

Поищите ответ в самой большой русскоязычной базе знаний по Total Commander!

Основы написания WFX-плагина на Borland Delphi

© Copyright
Эта статья защищена авторским правом, и на её правку наложены ограничения.

К написанию этого мини-мануала меня подтолкнула сложившаяся несправедливость: до сих пор мне не попалось ни одной толковой доки по написанию WFX-плагинов. О нет, сам я конечно не претендую на то, что этот мануал окажется "толковым", но чем чёрт не шутит? Естественно, есть официальный справочный файл, описывающий WFX API, и, теоретически, его достаточно. А вот на практике придётся столкнуться с заморочками, о которых в официальных документах не пишут.
Когда я писал свой первый плагин, я натворил в нём такую тучу ошибок, что чувство стыда потом не давало мне спать по ночам (отчего спал я исключительно днём, что, в свою очередь, обернулось проблемами на работе и в универе :)). Но это было позже, а вначале я сочинял и радовался...
В общем, очень надеюсь, что эта дока всё-таки поможет начинающему плагинописателю избежать моих ошибок. Опытные авторы, возможно, захотят что-то поправить или дополнить - я всегда открыт для предложений и мой e-mail известен.
Но - приступим, собственно, к мануалу.

На чём писать?

На чём писать, по большей части - абсолютно всё равно, лишь бы на выбранном языке можно было создать полноценную 32-хбитную динамическую библиотеку. Все современные ЯП и их компиляторы это, как правило, позволяют. Я, правда, сталкивался с написанием DLL всего в паре-тройке языков, так что каких-то конкретных особенностей могу и не знать, но ничего существенного, вроде бы, упустить не должен.
Единственная рекомендация при выборе языка программирования - использовать языки, снабжённые полноценным компилятором. Объяснять почему так - долго, нудно, неинтересно, и вообще к теме статьи не относится. Короче говоря - никаких Visual Basic'ов, и ничего, использующего .NET.
Подавляющее большинство плагинов написано на Delphi&Builder&VC++, так что если вы остановитесь на одном из этих средств программирования - всё точно будет в порядке. Примеры я буду приводить на Borland Delphi, если возникнет необходимость - то и на других языках.

Что потребуется?

Потребуются три вещи:

  1. Собственно, умение программировать. Умеете вы или нет - решать не мне, но раз взялись за создание плагина - буду считать, что умеете. К тому же, уровень программерского навыка для простого плагина потребуется не такой уж и большой. Само собой - чем сложнее идея, реализовать которую вы задумали, тем большее умение нужно.
  2. Знание английского языка. Потому что прочесть справочный файл (найти его можно на wincmd.ru), описывающий WFX API в любом случае придётся, ибо к нему я буду отсылать всегда, когда мне будет слишком лениво объяснять какие-то простые вещи.
    Опять же - знание ангельского нужно самое минимальное (как говорил один мой знакомый умный дядька: "Без знания английского в программировании делать нечего").
  3. Упорство. Потому что отладка плагина может продолжаться от пяти минут до бесконечности, и вы не будете понимать - вы ли это допустили ошибку (98% вероятности) или это Гислер где-то у себя чего-то напортачил.

Ещё одним желательным, но не обязательным, параметром является хотя бы поверхностное знание WinAPI. Знание это облегчит написание плагина многократно, да и вообще - в ближайшие десять лет от программирования на WinAPI никуда не деться, так что оно по любому понадобится.

Зачем это надо?

Вот сейчас подумайте, _чем_ ваш плагин будет выгодно отличаться от той же программы, реализованной иначе? Преимущество у реализации плагином, по сути, только одно (но зато какое!) - интеграция с TC. Однако, этого преимущество решающее; несмотря на то, что 99% плагинов просто выполняют функции других программ, "тоталовцы" будут пользоваться именно плагинами.
Теперь подумайте ещё раз: а _можно_ ли WFX-плагином реализовать вашу идею вообще? Теоретически, плагом можно сделать что угодно, но всё-таки лучше их использовать по первоначальному предназначению - для доступа к файловым системам (и тому, что на файловые системы хотя бы отдалённо смахивает). Советую прочесть статью Файлсистемные плагины: обзор возможностей для понимания предмета.
А теперь думать не надо. Теперь надо смотреть - не украли ли вашу идею у вас из головы подлые плагиаторы, написавшие ваш плагин до вас :). А то оно по всякому бывает...

Я сказал "Поехали"

Собственно, приступим к написанию плага. Первый наш плагин будет построчно выводить содержимое заданного текстового файла в виде каталога (каждая строка будет соответствовать имени файла). Должно получиться простенько и совсем понятно.
Запускаем Delphi (да, я выше уже сказал - будет именно Delphi). Создаём новый проект динамической библиотеки: File->New->Other->DLL Wizard (кстати, почему это называется "визардом"? Никаким "мастером" или "помощником" там близко не пахнет).

Создание проекта DLL

Сразу сохраним файл проекта под нужным нам именем. Правило "Как вы яхту назовёте, так она и поплывёт" здесь не действует - т.е. имя полученной библиотеки (совпадает с именем проекта) и имя, которое будет показываться в панели TC - две разные вещи. Но во избежание путаницы дадим проекту осмысленное имя, например TEXT_PANEL. Дабы бинарник компилировался не с расширением "dll" а с вожделенным "wfx", зададим это расширение в свойствах проекта, либо просто добавим директиву {$E wfx} после объявления uses.
А теперь - небольшое лирическое отступление, которое можно пропустить тем, кто сталкивался с написанием динамических библиотек.

О внутреннем устройстве DLL

Человеку, ранее никогда не писавшему DLL, может показаться, что тут есть какие-то существенные различия. Вовсе нет, написание DLL практически не отличается от написания обычного EXE, разве что придётся учитывать несколько особенностей:

  1. Отладка библиотеки немного геморройнее отладки exe-ешника (тем более, что тот же борландовский дебаггер сам далеко не образец для подражания, в плане отсутствия глюков). Впрочем, в нашем примере особых проблем не возникнет, но готовым надо быть ко всему.
  2. Библиотека (в нашем случае - плагин) может использоваться внешним приложением (в нашем случае - TC) только потому, что мы написали часть её внутренностей в соответствии с некими правилами (в нашем случае - с WFX API).
  3. Обмен строковыми данными между DLL и приложением _ДОЛЖЕН_ осуществляться _ТОЛЬКО_ через указатели (об этом, кстати, упоминает комментарий, автоматически добавленный в наш проект). Конкретно это выражается вот в чём: забываем о типе widestring и используем для обмена строковыми данными только pchar. Тип string тоже придётся оставить, ибо по умолчанию он воспринимается именно как widestring.

В Delphi (как в других языках - не знаю) возможно передавать строковые данные и через widestring с помощью подключения дополнительной библиотеки, но: а) это очень и очень некрасиво б) в плагине нам это всё равно ничем не поможет.
Напомню - указатели используются только при обмене данными. Внутри плагинов можно свободно пользоваться любыми типами, но опять же - если нам всё равно приходится возиться с пойнтерами, так уж давайте возиться до конца :) Если вы ничего не понимаете в работе с указателями - в принципе, ничего страшного, есть шанс, что необходимое понимание придёт в процессе.

Есть, конечно, и другие отличия, но о них я буду упоминать только по мере того, как мы непосредственно с ними столкнёмся. Так что возвратимся к плагину.

Необходимый минимум

Собственно, уже сейчас у нас есть нечто с расширением wfx. Однако, при попытке установить плагин, TC ругнётся примерно следующим образом:

Ошибка подключения плагина

Само собой - ведь существует набор функций, которые обязательно должны присутствовать в WFX-плагине, и без которых плагин будет неработоспособен. Официально их четыре. Неофициально - тоже четыре :). Если эти функции не описать в плагине, TC просто откажется его устанавливать. Дабы не повторяться потом несколько раз, просто приведу необходимый минимальный код проекта:

library TEXT_PANEL;

uses
 fsplugin,
 SysUtils,
 Windows;

{$E wfx}
{$R *.res}

function  FsInit(PluginNr:integer;pProgressProc:tProgressProc;pLogProc:tLogProc;
pRequestProc:tRequestProc):integer; stdcall;
Begin
End;

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;
Begin
End;

function FsFindNext(Hdl:thandle;var FindData:tWIN32FINDDATA):bool; stdcall;
Begin
End;

function FsFindClose(Hdl:thandle):integer; stdcall;
Begin
end;

exports
      FsInit,
      FsFindFirst,
      FsFindNext,
      FsFindClose;

begin
end.

Юнит fsplugin, добавленный в uses, содержит описания констант и типов, используемых любых WFX-плагином. Взять его можно из справки по WFX API.

Вот в таком виде плагин будет съеден Тоталом без вопросов. Пройдёмся теперь по описанным функциям.
FsInit пока трогать не нужно. Эта функция используется для более плотного, так скажем, взаимодействия с TC - вывода прогрессбаров и стандартных запросов, а также логирования действий. Пока нам ничего такого не требуется, а вот с FsFindFirst, FsFindNext, FsFindClose нам придётся ознакомиться вплотную.
Эти три функции - практически аналогичны стандартным апишным функциям FindFirstFile, FindNextFile, FindClose (в меньшей мере - VCL-функциям FindFirst, FindNext, FindClose). Тем, кто работал с тем или другим, будет совсем просто разобраться, остальным дам подсказку. Эти функции получают список файлов, который и должен отобразить TC. FsFindFirst получает первый файл из списка, FsFindNext - все остальные, FsFindClose не получает ничего, но в ней может проводиться какая-то деиницилизация на усмотрение автора. Порядок вызова функций, думаю, ясен. FsFindFirst вызывается, само собой, тогда, когда Тоталу нужно получить список (при входе в плагин, в каталог внутри плагина, при обновлении панели и т.п.).
Разберём подробнее, что передаётся в эти функции, и что мы в них должны возвращать:

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;

path - указатель на строку с текущим путём внутри плагина. Т.е., когда мы "заходим" в плагин, путь этот будет "\". Если в файловой системе плагина есть какой-то каталог, и мы в него заходим, то путь будет "\имя_каталога". По этому пути можно определять в какой ветке файловой системы плагина мы сейчас находимся.

FindData - запись (или "по сишному" - структура), хранящая всю информацию о файле, который TC должен добавить в список. Не будем забывать, что мы можем работать с чем-то, что никак на файлы не похоже (у нас это будут строки в файле). Однако же оказывается, что с помощью такой записи вывести можно очень даже многое.
Ознакомиться с типом tWIN32FINDDATA желательно всё-таки по Win32s Programmers Reference (поставляется практически с любой версией Delphi/Builder) либо в MSDN. Там эта структура описана как WIN32_FIND_DATA, в Delphi VCL всё практически аналогично.
Я же подробно останавливаться на этом не хочу, отмечу только что FindData.cFilename у нас должна будет содержать имя файла, FindData.dwFileAttributes - его атрибуты (в т.ч. - каталог это или файл). Остальные члены структуры хотя и нужны, но в примере могут быть отброшены.

Результатом функции может являться любое численное значение, и его мы можем использовать для своих целей (например, сохранить указатель на какую-то собственную структуру или что-то подобное). Однако, если вернуть INVALID_HANDLE_VALUE, то это будет означать ошибку внутри функции, после чего тотал уже не станет вызывать FsFindNext и FsFindClose. Чаще всего результат нам этот просто не будет нужен, так что смело приравниваем его к 0.

Попробуем сделать так, чтобы в файловой системе плагина лежал один файл "Hello, world.txt". Напишем примерно так:

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;
Begin
FindData.dwFileAttributes :=0; //0 - обычный файл без каких-либо атрибутов
FindData.cFileName :='Hello, world.txt'; //имя файла
result:=0; //функция нормально отработала
End;

Правильно? Нет! Но ведь работает!? И всё-таки, правильно будет так:

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;
Begin
FindData.dwFileAttributes :=0; //0 - обычный файл без каких-либо атрибутов
StrPCopy(FindData.cFileName,'Hello, world.txt'); //имя файла
result:=0; //функция нормально отработала
End;

Не забываем, что FindData.cFileName - это array [0..259] of char. Если мы приравняем к нему обычную строковую константу так, как написано в первом случае - всё будет в порядке, но если мы приравняем к нему строковую переменную.. у нас просто ничего не получится. Вот этот код:

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;
var
s:string;
Begin
FindData.dwFileAttributes :=0;
s:='Hello, world.txt';
FindData.cFileName :=s;
result:=0;
End;

просто не станет компилироваться. А поскольку работать надо будет не с константами а с переменными, то надо как-то выкручиваться. Самый простой выход описан во втором случае - функция StrPCopy решает все проблемы. Про другие способы, и про работу с ASCIIZ-строками, я распространяться не буду, это предмет отдельного изучения. Тем, кто будет писать не на VCL а на WinAPI объяснения вообще не требуются - там это главное и основное.

Перейдём к FsFindNext:

function FsFindNext(Hdl:thandle;var FindData:tWIN32FINDDATA):bool; stdcall;

Hdl - значение, которое вернула FsFindFirst. Как помним - у нас это 0, поэтому его пока не трогаем.

FindData - то же самое, что и у FsFindFirst. Однако запрашивается, само собой, уже следующий в списке файл.

Результат функции указывает, есть ли в списке ещё файлы (true), или нет (false). Т.е. функция будет вызываться до тех пор, пока не вернёт false - и каждый раз запрашивать информацию о следующем файле.
Для демонстрации напишем такой код:

library TEXT_PANEL;

uses
 fsplugin,
 SysUtils,
 Windows;

var
 FilesCount_Integer=0;

{$E wfx}
{$R *.res}

function FsInit(PluginNr:integer;pProgressProc:tProgressProc;pLogProc:tLogProc;
pRequestProc:tRequestProc):integer; stdcall;
Begin
End;

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;
Begin
FindData.dwFileAttributes :=0; //0 - обычный файл без каких-либо атрибутов
StrPCopy(FindData.cFileName,'Hello, world.txt'); //имя файла
result:=0; //функция нормально отработала
FilesCount:=0;
End;

function FsFindNext(Hdl:thandle;var FindData:tWIN32FINDDATA):bool; stdcall;
Begin
result:=True;
FindData.dwFileAttributes :=0;
StrPCopy(FindData.cFileName,'File_'+IntToStr(FilesCount)); //генерируем имя файла из счётчика
Inc (FilesCount);
if FilesCount=101 then result:=false;
End;

function FsFindClose(Hdl:thandle):integer; stdcall;
Begin
end;

exports
      FsInit,
      FsFindFirst,
      FsFindNext,
      FsFindClose;

begin
end.

И тут можем столкнуться со страннейшей проблемой - плагин будет работать, но время от времени при запуске (входе в файловую систему) выдавать ошибку. Но ведь всё написано верно, так в чём заморочка?
А она скрыта так, что сразу не поймёшь. Дело в том, что некоторые члены FindData мы не заполнили за их временной ненадобностью. Так вот, понадеявшись, что они останутся пустыми, мы обманулись. Они будут не пустыми, они будут случайными, а случайные данные там, где это не предусмотрено - практически готовая ошибка. В нашем конкретном случае потребуется заполнить FindData.nFileSizeHigh и FindData.nFileSizeLow:

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;
Begin
FindData.dwFileAttributes :=0; //0 - обычный файл без каких-либо атрибутов
FindData.nFileSizeHigh :=0; //
FindData.nFileSizeLow :=0; // Убираем случайные данные из записи о размере файла.
StrPCopy(FindData.cFileName,'Hello, world.txt'); //имя файла
result:=0; //функция нормально отработала
FilesCount:=0;
End;
 
function FsFindNext(Hdl:thandle;var FindData:tWIN32FINDDATA):bool; stdcall;
Begin
result:=True;
FindData.dwFileAttributes :=0;
FindData.nFileSizeHigh :=0;
FindData.nFileSizeLow :=0;
StrPCopy(FindData.cFileName,'File_'+IntToStr(FilesCount)); //генерируем имя файла из счётчика
Inc (FilesCount);
if FilesCount=101 then result:=false;
End;

FindData.nFileSizeHigh и FindData.nFileSizeLow отвечают за размер файла, соответственно старшее и младшее двойное слово. Общий размер файла вычисляется как (nFileSizeHigh * $FFFF) + nFileSizeLow.

Ухищрения в отладке

После того, как мы написали наипростейший, но уже функционирующий плагин, пора задуматься о том, как его отлаживать. До сих пор мы просто запускали его в TC, а это неудобно - после каждой перекомпиляции приходится перезапускать TC, да и искать плагин в списке (если их много) - тоже раздражает.
В общем, в меню Delphi идём в Run->Parameters... и прописываем:

Host Application: путь к totalcmd.exe
Parameters: /i=какой_нибудь_несуществующий_файл.ini

Настройка отладчика

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

Продолжаем писательство

Собственно, полученных знаний уже хватит, чтобы написать пример, который был задуман в самом начале. Путь к текстовому файлу пусть будет пока задан статически, например c:\text.txt

Код:

library TEXT_PANEL;

uses
 fsplugin,
 SysUtils,
 Windows;

Const
 MainFileName='c:\text.txt';

var
 MainFile:TextFile;

{$E wfx}
{$R *.res}

function FsInit(PluginNr:integer;pProgressProc:tProgressProc;pLogProc:tLogProc;
pRequestProc:tRequestProc):integer; stdcall;
Begin
End;

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;
var
TmpFileName:ShortString;
Begin
Result:=0;
AssignFile (MainFile,MainFileName);
Reset (MainFile);
if not EOF (MainFile) then
 begin
 ReadLn (MainFile,TmpFileName);
 FindData.dwFileAttributes :=0;
 FindData.nFileSizeHigh :=0;
 FindData.nFileSizeLow :=Length(TmpFileName);
 StrPCopy (FindData.cFileName,TmpFileName);
 end else
 begin
 CloseFile (MainFile);
 result:=INVALID_HANDLE_VALUE;
 end;
End;

function FsFindNext(Hdl:thandle;var FindData:tWIN32FINDDATA):bool; stdcall;
var
TmpFileName:ShortString;
Begin
Result:=True;
if not EOF (MainFile) then
 begin
 ReadLn (MainFile,TmpFileName);
 FindData.dwFileAttributes :=0;
 FindData.nFileSizeHigh :=0;
 FindData.nFileSizeLow :=Length(TmpFileName);
 StrPCopy (FindData.cFileName,TmpFileName);
 end else result:=False;
End;

function FsFindClose(Hdl:thandle):integer; stdcall;
Begin
CloseFile (MainFile);
end;

exports
      FsInit,
      FsFindFirst,
      FsFindNext,
      FsFindClose;

begin
end.

Результат работы плагина

Можно заметить, что нами была использована функция FsFindClose (в неё мы закрыли читаемый файл), а также то, что размер файла, отображаемый на панели TC соответствует длине его имени. Что будет происходить если файла c:\text.txt не существует, если он пуст, и если длина любой его строки более 255 символов, я предоставляю вам выяснить самим.

Копаем глубже

Сделаем теперь вот что: пусть файл, из которого происходит чтение, должен будет лежать в папке с плагином. Как его найти? Для exe-файла, использующего forms всё просто - анализируем Application.Exename или Paramstr(0), а в DLL нет ни того, ни другого.
Решение, само собой, есть (и им, кстати, можно пользоваться и при создании exe-файлов):

var
PointerPluginFile:pchar;
StrPluginFileName:ShortString;

begin
GetMem (PointerPluginFile,MAX_PATH);
GetModuleFilename (hInstance,PointerPluginFile,MAX_PATH);
StrPluginFileName:=Strpas (PointerPluginFile);
FreeMem (PointerPluginFile);
end.

Дальше нам остаётся только проанализировать StrPluginFileName и, таким образом, найти где лежит наш файл.
Сделаем следующий шаг навстречу знанию: разберёмся с тем, как работать внутри файловой системы плагина. Сделаем следующее: пусть копирование локального файла в файловую систему плагина вызовет добавление его имени в используемый текстовый файл, а копирование из плагина - создаст на локальной файловой системе в текущем каталоге текстовый файл, содержащий имя копируемого псевдофайла. Надеюсь, вы поняли, что я написал, потому что сейчас мы это будем реализовывать :)
За файловые операции копирования отвечают функции FsPutFile & FsGetFile. Первая вызывается при получении файла (т.е. когда копируем "в плагин"), вторая, соответственно, наоборот. Изучим их досконально:

function FsPutFile(LocalName,RemoteName:pchar;CopyFlags:integer):integer; stdcall;

Параметры у функции следующие:

LocalName - полное имя копируемого локального файла. Если это файл в сети - это будет UNC имя. Зачем это имя нужно - говорить излишне.

RemoteName - имя, введённое пользователем, под которым файл должен быть сохранён в ФС плагина. Само собой, мы можем начхать на все юзверьские претензии, и именовать файл как нам вздумается :). Жаль только, что менять можно лишь имя файла, а вот путь мы изменить не сможем. Иногда это приводит к неприятностям, о которых я расскажу в самом конце статьи. Имя всегда начинается со слеша ("\") что иногда необходимо-таки учитывать.

CopyFlags - флаг или комбинация флагов, описывающая требуемое поведение плагина при копировании. С их помощью TC может сообщить, что была задана опция "перезаписывать имеющиеся файлы" или что закачка должна быть продолжена после обрыва. За подробностями я, как и обещал в начале, отсылаю вас в справку по WFX API.

Вернуть функция должна сигнал о том, как прошло копирование. Опять же - все флаги вместе с описаниями перечислены в справке, так что останавливаться на этом я не стану.

С функцией FsGetFile всё чуть сложнее. Разберём и её:

function FsGetFile(RemoteName,LocalName:pchar;CopyFlags:integer;RemoteInfo:pRemoteInfo):integer; stdcall;

Как видим - здесь добавился ещё один параметр - RemoteInfo. Этот параметр хранит сведения о размере, атрибутах и датах копируемого файла. Атрибуты, например, могут нам понадобиться, если мы реализуем плагин для работы с FTP (чтобы и на сервере и на локальном диске всё было идентично), а сведения о размере - для того, чтобы отобразить верные сведения в диалоге копирования (об этом тоже будет рассказано ниже).

В остальном функции аналогичны. Дальше - смотрим написанный мною код:

library TEXT_PANEL;

uses
 fsplugin,
 SysUtils,
 Windows;

var
 MainFile:TextFile;
 MainFileName:string;

{$E wfx}
{$R *.res}

function FsInit(PluginNr:integer;pProgressProc:tProgressProc;pLogProc:tLogProc;
pRequestProc:tRequestProc):integer; stdcall;
Begin
End;

function FsFindFirst(path :pchar;var FindData:tWIN32FINDDATA):thandle; stdcall;
var
TmpFileName:ShortString;
Begin
Result:=0;
FindData.dwFileAttributes :=0;
FindData.nFileSizeHigh :=0;
FindData.nFileSizeLow :=0;
AssignFile (MainFile,MainFileName);
Reset (MainFile);
if not EOF (MainFile) then
 begin
 ReadLn (MainFile,TmpFileName);
 FindData.nFileSizeLow :=Length(TmpFileName);
 StrPCopy (FindData.cFileName,TmpFileName);
 end else
 begin
 StrPCopy (FindData.cFileName,'No records in text.txt');
 end;
End;

function FsFindNext(Hdl:thandle;var FindData:tWIN32FINDDATA):bool; stdcall;
var
TmpFileName:ShortString;
Begin
Result:=True;
if not EOF (MainFile) then
 begin
 ReadLn (MainFile,TmpFileName);
 FindData.dwFileAttributes :=0;
 FindData.nFileSizeHigh :=0;
 FindData.nFileSizeLow :=Length(TmpFileName);
 StrPCopy (FindData.cFileName,TmpFileName);
 end else result:=False;
End;

function FsFindClose(Hdl:thandle):integer; stdcall;
Begin
CloseFile (MainFile);
end;

function FsPutFile(LocalName,RemoteName:pchar;CopyFlags:integer):integer; stdcall;
Begin
AssignFile (MainFile,MainFileName);
Append (MainFile);
WriteLn (MainFile,ExtractFilename(StrPas(RemoteName)));
CloseFile (MainFile);
Result:=FS_FILE_OK;
End;

function sGetFile(RemoteName,LocalName:pchar;CopyFlags:integer;RemoteInfo:pRemoteInfo):integer; stdcall;
var
TmpLocalFile:TextFile;
TmpLocalFileName:ShortString;
Begin
TmpLocalFileName:=StrPas (LocalName);
AssignFile (TmpLocalFile,TmpLocalFileName);
Rewrite (TmpLocalFile);
WriteLn (TmpLocalFile,StrPas(RemoteName));
CloseFile (TmpLocalFile);
Result:=FS_FILE_OK;
End;

exports
      FsInit,
      FsFindFirst,
      FsFindNext,
      FsFindClose,
      FsPutFile,
      FsGetFile;

var
PointerPluginFile:pchar;
StrPluginFileName:ShortString;

begin
GetMem (PointerPluginFile,MAX_PATH);
GetModuleFilename (hInstance,PointerPluginFile,MAX_PATH);
StrPluginFileName:=Strpas (PointerPluginFile);
FreeMem (PointerPluginFile);
MainFileName:=ExtractFilePath(StrPluginFileName)+'text.txt';
if not FileExists (MainFileName) then FileCreate (MainFileName);
end.

Не забываем добавлять имена экспортируемых функций в раздел exports, - иначе Тотал ими не сможет воспользоваться.

Думаю, на этом с основами плагинописания мы разобрались. В этом деле - главное начать, с остальным, я уверен, вы разберётесь в процессе. С разбором некоторых функций могут возникнуть проблемы - но вы справитесь, ничего сложного там нет.

И напоследок...

Напоследок несколько... даже не советов, а напутствий что ли:

  1. Все функции работы с файлами вызываются для каждого файла отдельно, так что при дебаге учитывайте это. Иногда приходится искать багу, выпозающую только при обработке 1328 файла в списке, вот где настоящее буддийское просветление приходит... А если мы копируем папку - функция копирования будет вызываться отдельно для каждого файла в ней.
  2. Иногда кажется, что TC слишком медленно отображает содержимое плага, особенно если он выводит список в несколько тысяч файлов. Не спешите винить себя - так и должно быть, вывод большого списка файлов у TC занимает много времени.
  3. Если какая-то функция не имплементирована, то при её вызове TC просто ничего не будет делать. Уберите из Exports FsGetFile - и плагин не будет реагировать на нажатия F5 в своей ФС. Но имплементированные функции необходимо реализовывать - если оставить функцию без тела (т.е. без кода между begin..end), а потом попытаться её вызывать, то Тотал может повести себя непредсказуемо - начиная от выдачи сообщений об ошибках, заканчивая зависанием.
  4. Имена файлов с "нехорошими" символами - слешами, двоеточиями и прочим, могут восприниматься некорректно. Например, если выдать в списке файл, содержащий "/", то TC, при некоторых условиях, воспринимает его как папку, и даже "входит" в неё по Ctrl+PgDown.
  5. Не забывайте возвращать какой-то результат во всех функциях. Пусть вам этот результат нафиг не нужен - лучше приравнять его к нулю, чем оставить на волю случая.
  6. Иногда бывает нужно "заставить" TC выполнить какую-то встроенную команду. Например мне однажды требовалось обновлять панели Тотала после вызова свойств файла. Делается это довольно просто:
    PostMessage(FindWindow ('TTOTAL_CMD', nil), WM_USER+51, 540, 0);

    где 540 - номер нужной команды, в данном случае - cm_RereadPanels. Номера команд можно найти в файле WINCMD.INC, лежащем в каталоге TC.

На этом остановлюсь. Возможно - будет продолжение, но это зависит только от ваших вопросов (которые, всё-таки, сначала желательно задавать на http://forum.wincmd.ru).

Павел Дубровский
15.07.2006