Warsztat.GDCompo!ProjektyMediaArtykułyQ&AForumOferty pracyPobieranie

Opisz napotkaną sytuację, a redakcja niezwłocznie znajdzie rozwiązanie!

wyślij anuluj

Własny VFS - wtyczka do Total Commandera

Wstęp

Ten artykuł adresowany jest do średnio zaawansowanych i zaawansowanych programistów. Opisuje sposób, w jaki można napisać wtyczkę do managera plików Total Commander obsługującą własny format archiwum (VFS). Stanowi uzupełnienie oficjalnej dokumentacji SDK do pisania wtyczek do tego programu i ma na celu pomóc w opanowaniu sztuki pisania tych wtyczek. Równocześnie nie jest to kompletny tutorial i nie pokazuje, jak krok po kroku napisać wtyczkę wraz z implementacją obsługi swojego VFS. Tym bardziej nie uczy on, jak zaprojektować i zaimplementować własny format archiwum. Pokazuje tylko szkielet takiej wtyczki.

Do zrozumienia artykułu potrzebna jest znajomość programowania w C++, w tym posługiwania się wskaźnikami, plikami i danymi binarnymi. Nie jest natomiast wymagana umiejętność tworzenia bibliotek DLL - zostanie to wyjaśnione od podstaw. Kod w artykule napisany jest w języku C++ z użyciem środowiska Visual Studio 2008 i przeznaczony jest dla systemu Windows.

Total Commander (dawniej Windows Commander) to popularny manager plików dla Windows. Jego autorem jest Szwajcar, Christian Ghisler. Oficjalna strona programu to www.ghisler.com. Jest to świetny, a zdaniem wielu (w tym moim) najlepszy manager plików. Ma ogromne możliwości, jest bardzo wygodny, a poza tym jest w dużym stopniu konfigurowalny. Total Commander jest na licencji Shareware, ale wersja testowa ma pełną funkcjonalność.

Wtyczki do Total Commandera (ang. plugins) występują w czterech odmianach:

  1. WCX - Packer Plugin. Obsługuje archiwum w danym formacie pliku, np. ZIP, RAR. Właśnie tego typu wtyczkę nauczymy się pisać.
  2. WFX - File System Plugin. Obsługuje nowy system plików (np. systemy plików linuksowe jak ext2, protokoły sieciowe typu SFTP). Jest widoczny w Otoczeniu sieciowym (dysk "\").
  3. WLX - Lister Plugin. Obsługuje podgląd plików danego typu w Listerze (F3) - np. formaty grafiki wektorowej, dokumenty.
  4. WDX - Content Plugin. Przegląd parametrów wyciągniętych z treści plików (np. informacji EXIF). Dostępny podczas wyszukiwania plików, w okienku Znajdź Pliki (Alt+F7).

Zmieniając nieco temat, w programowaniu gier często spotyka się tworzenie własnych formatów VFS - wirtualnego systemu plików (ang. Virtual File System). Chodzi o format pliku archiwum, w którym spakowane - i zwykle skompresowane - jest wiele mniejszych plików, zupełnie jak w formatach kompresji typu ZIP czy RAR. Oprócz zaprojektowania takiego formatu i zaimplementowania jego obsługi (zwykle tylko odczytu) w kodzie gry, trzeba też napisać narzędzie do tworzenia takich archiwów. Najprościej jest oczywiście napisać program konsolowy uruchamiany z odpowiednimi parametrami, ale każdy chyba przyzna, że korzystanie z takiego programu nie należy do najwygodniejszych opcji. Z drugiej strony, trudno się zdobyć na napisanie w tym celu programu okienkowego. Dlatego wtyczka do Total Commandera obsługująca format VFS to może być w takiej sytuacji najlepszy wybór.

Aby napisać wtyczkę do Total Commandera, należy ściągnąć oficjalne SDK. Znajdziesz je na stronie programu, w dziale Plugins. Na końcu każdej tabeli z listą wtyczek znajduje się pakiet SDK do wtyczek danego rodzaju. Bezpośredni link do pakietu dla wtyczek typu Packer (WCX): wcx_ref2.12.zip. Pakiet zawiera dokumentację w formacie HLP oraz plik nagłówkowy dla Delphi i C++.

Utworzenie wtyczki

Zajmijmy się teraz utworzeniem szkieletu wtyczki. Wtyczka do Total Commandera typu Packer to biblioteka DLL, która eksportuje określone w dokumentacji funkcje działające w określony sposób oraz ma zmienione rozszerzenie pliku na "wcx". Jeśli chcesz, możesz pobrać mój gotowy szablon dla Visual C++ 2008 tutaj: TestWcx.zip. Jeśli natomiast wolisz dowiedzieć się, jak to zrobić samemu...

Uruchom Visual C++ i utwórz nowy projekt: File / New / Project. Wybierz typ projektu: Visual C++ / Win32 / Win32 Project. Wypełnij pola Location i Name. W kreatorze Win32 Application Wizard wybierz DLL i zaznacz Empty project. Tak powstał pusty projekt biblioteki DLL.

Musisz teraz jakoś dołączyć nagłówek dostarczony z SDK do Total Commandera - plik wcxhead.h. Możesz po prostu skopiować go i dodać do swojego projektu (wtedy w #include użyj cudzysłowów: "wcxhead.h"). Inaczej, możesz dodać ścieżkę gdzie ten plik się znajduje do ścieżek Include files (mam nadzieję, że wiesz o czym mówię?) i wtedy włączyć ten nagłówek za pomocą #include <wcxhead.h>.

Następnie stwórz plik cpp. Kliknij prawym klawiszem na projekt i wybierz Add / New Item, Code / C++ File (.cpp). Tu znajdzie się kod wtyczki, który jest opisany niżej. Najpierw jednak wejdź do opcji projektu i przestaw kilka rzeczy:

  • General, obydwie konfiguracje: Character Set = Not Set - żeby domyślnym typem znaków były zwykłe znaki char (kodowanie ANSI), a nie dwubajtowe wchar_t (Unicode).
  • C/C++ / Code Generation: Runtime Library: W konfiguracji Debug przestaw na: Multi-threaded Debug (/MTd), a w konfiguracji Release na: Multi-threaded (/MT). Dzięki temu skompilowany plik nie będzie wymagał bibliotek DLL, których użytkownicy nie mający zainstalowanego Visual C++ często nie posiadają w swoim systemie.
  • Linker / General: Output File = "$(OutDir)\$(ProjectName).wcx" - to zmienia rozszerzenie skompilowanego pliku z dll na wcx.

Na początek musimy zaimplementować kilka funkcji, które zawsze w takiej wtyczce być muszą. Na razie nie będą robiły zbyt wiele. Szczegóły wyjaśnię w następnych rozdziałach. Póki co, pokażę kod pliku cpp:

#include <windows.h>
#include <wcxhead.h>

struct ARC_CONTEXT
{
  // Tu zadeklaruj dane archiwum otwartego do odczytu.
  int NumFilesRead;
};

extern "C"
int __stdcall GetPackerCaps()
{
  return
    PK_CAPS_MULTIPLE |
    PK_CAPS_SEARCHTEXT;
}

extern "C"
HANDLE __stdcall OpenArchive(tOpenArchiveData *ArchiveData)
{
  ARC_CONTEXT *Context = new ARC_CONTEXT;
  Context->NumFilesRead = 0;
  return (HANDLE)Context;
}

extern "C"
int __stdcall CloseArchive(HANDLE hArcData)
{
  ARC_CONTEXT *Context = (ARC_CONTEXT*)hArcData;
  delete Context;
  return 0;
}

extern "C"
int __stdcall ReadHeader(HANDLE hArcData, tHeaderData *HeaderData)
{
  ARC_CONTEXT *Context = (ARC_CONTEXT*)hArcData;
  ZeroMemory(HeaderData, sizeof(tHeaderData));

  // TODO - trzeba zaimplementować

  if (Context->NumFilesRead == 0)
  {
    strcpy_s(HeaderData->FileName, _countof(HeaderData->FileName), "Test.txt");
    Context->NumFilesRead++;
    return 0;
  }
  else
    return E_END_ARCHIVE;
}

extern "C"
int __stdcall ProcessFile(HANDLE hArcData, int Operation, char *DestPath, char *DestName)
{
  // TODO - trzeba zaimplementować

  return 0;
}

extern "C"
void __stdcall SetChangeVolProc(HANDLE hArcData, tChangeVolProc pChangeVolProc1)
{
  // Nie używamy.
}

extern "C"
void __stdcall SetProcessDataProc(HANDLE hArcData, tProcessDataProc pProcessDataProc)
{
  // Na razie nic.
}

Dopiski extern "C" wbrew nazwie nie znaczą, że funkcje są napisane w C a nie w C++. Znaczą tylko, że nazwy tych funkcji nie zostaną przez kompilator udekorowane (jak to kompilator C++ ma w zwyczaju robić), tylko będą pozostawione w oryginalnej formie. Z kolei __stdcall to konwencja wywołania, czyli kolejność przekazywania parametrów przez stos. Jedno i drugie jest potrzebne, żeby prawidłowo wyeksportować funkcje do użytku dla Total Commandera. Przy swoich własnych, wewnętrznych funkcjach nie musisz tego pisać.

Żeby plik DLL był kompletny, trzeba jeszcze te funkcje wyeksportować. Jednym ze sposobów na zrobienie tego jest utworzenie pliku DEF. W tym celu kliknij prawym klawiszem na projekt i wybierz: Add / New Item, Code / Module-Definition File (.def). Plik DEF to plik tekstowy w specjalnym formacie, który definiuje po prostu spis funkcji eksportowanych z kompilowanego pliku DLL. Nasz początkowy plik DEF ma postać jak pokazana poniżej. W miarę dodawania nowych funkcji używanych przez Total Commander, ich nazwy trzeba też tutaj dopisywać.

LIBRARY  "TestWcx"

EXPORTS
  OpenArchive
  CloseArchive
  ReadHeader
  ProcessFile
  SetChangeVolProc
  SetProcessDataProc
  GetPackerCaps

Najwyższy czas zajrzeć do dokumentacji (plik "WCX Writer's Reference.hlp"). Jest tam rozdział "Error Codes", który wymienia stałe do używania przez wtyczkę jako kody błędów. Większość funkcji musi zwrócić rezultat jako powodzenie lub niepowodzenie, a o błędach informować właśnie zwracając odpowiedni kod (tak jak pokazane wyżej funkcje CloseArchive, ReadHeader). Na przykład jeśli próbujesz otworzyć plik i to się nie udaje, powinieneś przerwać funkcję i zwrócić wartość stałej E_EOPEN. Sukces jest oznaczany przez wartość 0.

Teraz chciałbym opisać funkcję GetPackerCaps. Jej zadaniem jest zwrócić sumę bitową (operator "|") flag mówiących, jakie możliwości i cechy posiada dana wtyczka. Dopisując do niej nowe funkcje będziemy równocześnie dodawali tu nowe flagi. Póki co, dwie dotychczas wpisane oznaczają:

  • PK_CAPS_MULTIPLE - archiwum może zawierać wiele plików. To niby oczywiste, ale są formaty, które tego nie potrafią - np. GZ.
  • PK_CAPS_SEARCHTEXT - Total Commander może zaglądać do wnętrza archiwów w naszym formacie podczas wyszukiwania. To tylko informacja dla programu i niczego nie trzeba robić, żeby to działało.

Uwaga! Kiedy będziemy instalowali wtyczkę w Total Commanderze (następny rozdział), trzeba pamiętać, że tą listę możliwości program pobiera z funkcji GetPackerCaps tylko jeden raz, podczas instalowania wtyczki. Dlatego po zmianie tych flag trzeba wtyczkę od nowa zainstalować, nie wystarczy przekompilować WCX i zrestartować Total Commandera.

Instalacja wtyczki

Jak zainstalować skompilowaną wtyczkę WCX w Total Commanderze? Są dwa sposoby. Pierwszy, ten "ręczny", polega na zarejestrowaniu pliku WCX w konfiguracji programu. W tym celu trzeba wejść do: Konfiguracja / Opcje / Wtyczki / Wtyczki pakera (.WCX) / Konfiguruj. Następnie wprowadź rozszerzenie plików archiwum, kliknij Nowy, wybierz plik TestWcx.wcx i potwierdź OK, OK.

Również w tym okienku można deinstalować wtyczki, choć niełatwo wpaść na pomysł, jak to się robi. Trzeba w tym celu: 1. wybrać z pola wyboru rozszerzenie pliku (podświetli się ścieżka do powiązanej z nim wtyczki), 2. kliknąć na liście w pozycję "(żaden)", 3. ponownie wybrać z pola wyboru rozszerzenie pliku, 4. potwierdzić pojawiające się pytanie o zmianę skojarzenia, 5. choć tego nie widać, wtyczka została odinstalowana - okno można zamknąć OK, po ponownym otwarciu wtyczki już nie będzie.

Drugi sposób to przygotowanie paczki, która będzie się instalowała automatycznie. To dobry pomysł, aby ukończoną wtyczkę dać innym użytkownikom. Przygotowanie takiej paczki jest bardzo proste. Wystarczy spakować do dowolnego obsługiwanego domyślnie formatu archiwum (np. ZIP) plik WCX i opcjonalnie inne pliki, a także specjalnie przygotowany plik pluginst.inf. Po wejściu do takiego archiwum Total Commander sam zorientuje się, że znalazł przeznaczoną dla niego wtyczkę i wyświetli okienko, w którym zaproponuje jej zainstalowanie.

Plik pluginst.inf ma prostą składnię. To plik tekstowy w formacie INI. Jego oficjalna dokumentacja znajduje się w tym temacie forum programu: Plugin auto-install HOWTO (TC 6.5 or newer). Plik może wyglądać na przykład tak:

[plugininstall]
description=SuperGame VFS Packer Plugin
type=wcx
file=TestWcx.wcx
defaultdir=TestWcx
defaultextension=vfs
Readme=TestWcx_Readme.txt

Gdzie poszczególne wartości:

  • description - opis wyświetlany przy propozycji zainstalowania.
  • type - dla wtyczek typu Packer musi być "wcx".
  • file - nazwa pliku WCX.
  • defaultdir - domyślny katalog instalacji.
  • defaultextension - domyślne rozszerzenia dla plików w naszym formacie archiwum (oddzielone przecinkami)
  • Readme (opcjonalny) - nazwa pliku tekstowego README z dokumentacją wtyczki.

Po zainstalowaniu wtyczki na jeden z opisanych sposobów możesz ją łatwo wypróbować. W tym celu utwórz nowy plik o odpowiednim rozszerzeniu (wciśnij Shift+F4 i wpisz np. "Test.vfs"). Następnie "wejdź" do niego klawiszem Enter. Powinieneś zobaczyć w środku jeden plik - "Test.txt", bo nasz kod jest na razie tak napisany, żeby zawsze tylko jego zwracać jako wnętrze archiwum.

Jak testować taką wtyczkę? Podczas pisania na pewno wiele razy trzeba będzie zmieniać kod, kompilować go i sprawdzać, czy dobrze działa. Ja stosuję taką metodę: Instaluję w Total Commanderze wtyczkę z tej lokalizacji, do której jest kompilowana (czyli TestWcx\Debug\TestWcx.wcx). Następnie wyłączam Total Commander, zmieniam kod, kompiluję, włączam program ponownie i sprawdzam działanie wchodząc do testowego archiwum.

Myślę, że nie mając do dyspozycji normalnego debuggera warto zorganizować sobie mechanizm drukowań kontrolnych zapisywanych gdzieś do pliku, żeby móc logować to co się dzieje w naszym kodzie i tym samym badać, dlaczego nie działa tak jak powinien. Ja użyłem w tym celu czegoś takiego:

#ifdef _DEBUG
  #define DEBUG_LOGGING_ENABLED
#endif

#ifdef DEBUG_LOGGING_ENABLED
  #define DEBUG_LOG(Format,...) { DebugLog((Format), __VA_ARGS__); }
#else
  #define DEBUG_LOG(Format, ...) { }
#endif

void DebugLog(const char *Format, ...)
{
  FILE *File;
  errno_t r = fopen_s(&File, "C:\\Tmp\\TestWcx.log", "a");
  if (r != 0) return;

  va_list parmList;
  va_start(parmList, Format);
  vfprintf(File, Format, parmList);
  va_end(parmList);

  fclose(File);
}

// Przykładowe logowanie:
DEBUG_LOG("Komunikat");
DEBUG_LOG("Otwieram archiwum: %s, liczba plików: %i", ArcName, FileCount);

Odczytywanie archiwum

Przejdźmy wreszcie do rzeczy. Funkcjonalność, którą musi posiadać każda wtyczka WCX, to 1. listowanie plików i katalogów archiwum, 2. rozpakowywanie wybranych plików i katalogów. Takie rozpakowywanie odbywa się w Total Commanderze przez "wejście" do archiwum, a następnie zwykłe przekopiowanie zaznaczonych plików do katalogu po drugiej stronie poleceniem Kopiowanie (F5).

Sposób, w jaki Total Commander używa API wtyczki do wykonania tych zadań, nie jest zbyt optymalny. Otóż za każdym razem (przy wejściu do archiwum, przy rozpakowywaniu jakiś plików) od nowa otwiera on archiwum, po czym przechodzi po kolei wszystkie pliki i katalogi z jego wnętrza, każąc każdy kolejny rozpakować ("extract") lub pominąć ("skip"). Cóż... pozostaje się dostosować do takiego podejścia.

Od tej pory już nie będę pokazywał kompletnych przykładów kodu, tylko opiszę ogólnie, jak co zrobić. Zacznijmy od otwarcia i zamknięcia archiwum. Służą do tego funkcje:

HANDLE __stdcall OpenArchive(tOpenArchiveData *ArchiveData);
int __stdcall CloseArchive(HANDLE hArcData);

Funkcja OpenArchive ma za zadanie otworzyć plik do odczytu i zainicjalizować wewnętrzne struktury danych, które potem pozostałe funkcje wykorzystają do przechodzenia pliku (nazwałem je tutaj "kontekstem", stąd nazwa mojej struktury ARC_CONTEXT).

Otrzymana struktura tOpenArchiveData zawiera dwa interesujące pola.

  • char* ArcName - zawiera ścieżkę do pliku archiwum, który trzeba otworzyć.
  • int OpenMode - to tryb otwarcia.

Ten tryb to może być jedna z dwóch stałych:

  • PK_OM_LIST - archiwum jest otwierane tylko w celu wylistowania plików i katalogów. Nie będzie potrzeby rozpakowywania. Sekwencja wywołań to: OpenArchive, ReadHeader, ReadHeader, ..., CloseArchive.
  • PK_OM_EXTRACT - jakieś pliki będą rozpakowywane lub testowane. Po każdym wywołaniu ReadHeader nastąpi wywołanie ProcessFile. Tak więc sekwencja wywołań to: OpenArchive, ReadHeader, ProcessFile, ReadHeader, ProcessFile, ..., CloseArchive.

Funkcja OpenArchive ma za zadanie otworzyć archiwum i zwrócić jakiś swój "uchwyt" do kontekstu, który potem zostanie przekazany innym funkcjom operującym na tym archiwum. My w pokazanym wyżej przykładzie alokujemy strukturę ARC_CONTEXT i jako uchwyt zwracamy wskaźnik na nią. Jeśli otwarcie się nie powiedzie, funkcja OpenArchive powinna zwrócić 0, a wcześniej ustawić pole OpenResult otrzymanej struktury na odpowiedni kod błędu.

Funkcja CloseArchive zamyka archiwum. Oczywiście otrzymuje ona uchwyt zwrócony przez OpenArchive. W naszym przykładzie rzutuje go sobie z powrotem na wskaźnik do ARC_CONTEXT i zwalnia tą strukturę.

Funkcja ReadHeader ma zwrócić informację o kolejnym pliku lub katalogu z archiwum. Jeśli się to uda, wypełnia pola otrzymanej struktury i zwraca 0. Jeśli się to nie uda, bo osiągnięty został koniec listy plików, zwraca E_END_ARCHIVE. Wtedy dane wpisane do otrzymanej struktury nie mają znaczenia. Oczywiście jeśli nastąpił inny błąd, zwraca odpowiedni kod błedu. Nagłówek funkcji wygląda tak:

int __stdcall ReadHeader(HANDLE hArcData, tHeaderData *HeaderData);

Zaś struktura tHeaderData posiada takie interesujące nas pola:

  • char FileName[260]- tu wpisz nazwę pliku lub katalogu razem z całą ścieżką w archiwum, np. "Dir1\SubDir\File2.txt".
  • int PackSize- rozmiar pliku spakowanego w archiwum.
  • int UnpSize- oryginalny rozmiar pliku.
  • int FileAttr- atrybuty pliku. Jeśli to katalog, ustaw 0x10. Inne możliwe wartości znajdziesz w dokumentacji.
  • int FileTime - data i czas modyfikacji.

Pole FileTime musi zostać zakodowane w pewnym bardzo sprytnym formacie, w którym pełna data i czas zapisana jest w jednej 32-bitowej wartości nadającej się do porównywania jako liczba. Jego wadą jest, że rok musi być między 1980 a 2100, a dokładność to 2 sekundy. Na szczęście nie musimy się zajmować jego poszczególnymi bitami, bo tak się szczęśliwie składa, że ten format to nic innego, jak złożone ze sobą 16-bitowe wartości znanego formatu daty i czasu DOS. Wszystko potrafią załatwić odpowiednie funkcje WinAPI. Oto przykładowy kod, który pobiera datę modyfikacji pliku i koduje ją w formacie Total Commandera:

WIN32_FILE_ATTRIBUTE_DATA Attr;
GetFileAttributesEx(FileName, GetFileExInfoStandard, &Attr);
FILETIME LocalTime;
FileTimeToLocalFileTime(&Attr.ftLastWriteTime, &LocalTime);
WORD DosDate, DosTime;
FileTimeToDosDateTime(&LocalTime, &DosDate, &DosTime);
int TotalCmdFileTime = (int)( ((unsigned)DosDate << 16) | (unsigned)DosTime );

Jest też wersja tej funkcji o nazwie ReadHeaderEx. Jeśli ją zaimplementujesz, właśnie ona będzie wywoływana zamiast ReadHeader. Przyjmuje ona strukturę tHeaderDataEx i różni się tym, że pozwala zwracać rozmiar pliku jako liczbę 64-bitową. Zaimplementuj więc tą funkcję, jeśli planujesz przetwarzać pliki większe niż 2 GB i jesteś gotów używać wszędzie w swoim kodzie liczb 64-bitowych do zapisywania rozmiarów i pozycji w plikach.

Czas teraz opisać funkcję ProcessFile. Jej zadaniem jest "przetworzyć" plik, którego dane zwróciło ostatnie wywołanie ReadHeader. Jej nagłówek wygląda tak:

int __stdcall ProcessFile(HANDLE hArcData, int Operation, char *DestPath, char *DestName);

Parametrem Operation może być:

  • PK_SKIP - pomiń ten plik.
  • PK_TEST - przetestuj treść tego pliku.
  • PK_EXTRACT - rozpakuj ten plik do lokalizacji na dysku wskazanej przez parametry DestPath i DestName.

Uwaga! Jest tutaj pewna niedogodność. Jak mówi dokumentacja, lokalizacja pliku docelowego może być przekazana na jeden z dwóch sposobów. Albo DestPath jest NULL, a DestName zawiera pełną ścieżkę, albo też DestPath zawiera katalog, a DestName samą nazwę pliku. Ja w swoim kodzie rozwiązałem to w taki uniwersalny sposób:

// Funkcja pomocnicza
void MakePath(
  std::string *Out,
  const std::string *Dir, const std::string *FileName, const std::string *Ext = NULL)
{
  if (Dir)
    *Out = *Dir;
  else
    Out->clear();
  if (Dir && !Dir->empty() && FileName && !FileName->empty())
    if (!Out->empty() && (*Out)[Out->length()-1] != '\\')
      *Out += '\\';
  if (FileName)
    *Out += *FileName;
  if (Ext && !Ext->empty())
  {
    if (!Out->empty() && (*Out)[Out->length()-1] != '.' && (*Ext)[0] != '.')
      *Out += '.';
    *Out += *Ext;
  }
}

// Wewnątrz ProcessFile
std::string FinalDstFname;
if (DestName == NULL && DestPath == NULL)
{
}
else if (DestPath == NULL)
  FinalDstFname = DestName;
else if (DestName == NULL)
  FinalDstFname = DestPath;
else
  MakePath(&FinalDstFname, &string(DestPath), &string(DestName));
// Rozpakuj do FinalDstFname...

Zwracam też uwagę na fakt, że Total Commander lubi dodawać do przekazywanych ścieżek kończący '\' tam, gdzie chodzi o katalog. Na przykład można dostać ścieżkę "D:\Unpacked\Dir1\SubDir\". Trzeba o tym pamiętać i uwzględniać taką możliwość tam gdzie to konieczne.

Kolejny temat to funkcja CanYouHandleThisFile. Warto ją zaimplementować, a równocześnie do GetPackerCaps dodać flagę PK_CAPS_BY_CONTENT. Wówczas wtyczka zyskuje umiejętność rozpoznawania swojego formatu po treści, a nie tylko po rozszerzeniu pliku. Total Commander pozwala wtedy "wejść" do takiego pliku nawet, jeśli ma inne rozszerzenie. Jeśli z powodu skojarzenia z innym programem klawisz Enter nie działa jak należy, można to zrobić skrótem Ctrl+PgDn. Sama funkcja CanYouHandleThisFile nie wymaga chyba szerszego wyjaśnienia. Powinna otworzyć podany plik i sprawdzić w jego treści (w jakimś nagłówku), czy on faktycznie jest w formacie obsługiwanym przez tą wtyczkę. W wyniku powinna zwrócić FALSE albo TRUE. Jej nagłówek to:

BOOL __stdcall CanYouHandleThisFile(char *FileName);

Dobrze napisana wtyczka powinna w czasie pracy aktualizować pasek postępu. Służy do tego wywołanie zwrotne funkcji, której wskaźnik dostajemy przekazywany do funkcji SetProcessDataProc. Brzmi to zawile, ale w praktyce sprowadza się do zaimplementowania tej funkcji i zachowania otrzymanego wskaźnika w jakiejś zmiennej globalnej. Na przykład:

// Zmienna globalna - wskaźnik na funkcję zwrotną
tProcessDataProc g_ProcessDataProc = NULL;

extern "C"
void __stdcall SetProcessDataProc(HANDLE hArcData, tProcessDataProc pProcessDataProc)
{
  g_ProcessDataProc = pProcessDataProc;
}

Uwaga! Funkcja SetProcessDataProc może być wywoływana przez Total Commander w różnych momentach. Chociaż posiada parametr hArcData z kontekstem otwartego do odczytu archiwum (używanym w funkcjach OpenArchive, CloseArchive, ReadHeader, ProcessFile), to jednak często wywoływana jest bez związku z takim przetwarzaniem konkretnego archiwum. Parametr hArcData jest wtedy równy NULL. Tak więc nie można otrzymanego wskaźnika zapamiętać w takim kontekście, trzeba to zrobić w zmiennej globalnej.

Typ otrzymanego wskaźnika na funkcję jest zdefiniowany tak:

typedef int (__stdcall *tProcessDataProc)(char *FileName, int Size);

Wywołanie funkcji spod tego wskaźnika aktualizuje pasek postępu w okienku Total Commandera. Można jej używać na dwa sposoby. Pierwszy to raportowanie postępu w rozpakowywaniu plików podczas wywołań funkcji ProcessFile. Do tego wystarczy podać FileName = NULL, a jako Size liczbę bajtów wypakowanych od poprzedniego wywołania tej funkcji.

Drugi sposób to "ręczne" sterowanie paskiem postępu. Jest dobry, jeżeli chcesz w ProcessFile tylko zbierać listę plików do rozpakowania, a całe przetwarzanie wykonać w CloseArchive. Nadaje się też do pokazywania postępu podczas operacji pakowania i usuwania, o których będzie mowa w następnym rozdziale. Polega na podawaniu jako FileName nazwy aktualnie przetwarzanego pliku, a jako Size odpowiednio przygotowanej liczby ujemnej:

  • -1 ... -100 oznacza 1% ... 100% - aktualizację górnego paska postępu, przeznaczonego do wyświetlania postępu przetwarzania bieżącego pliku.
  • -1000 ... -1100 oznacza 0% ... 100% - aktualizację dolnego paska postępu, przeznaczonego do wyświetlania postępu całej operacji.

Tą funkcję możesz wywoływać właściwie kiedy chcesz i jak chcesz. Uważam jednak, że jej zbyt częste wywoływanie może spowolnić proces, bo wymuszanie odrysowania okienka zbyt często może zajmować więcej czasu, niż właściwe przetwarzanie danych. Dlatego w swoim kodzie zrobiłem coś takiego, co pozwoliło mi odświeżać pasek postępu tylko co określoną liczbę milisekund, niezależnie jak szybko udaje się przetwarzać dane.

char Buf[BUF_SIZE];
unsigned CurrentBytes;
// Liczba bajtów przetworzonych od ostatniej aktualizacji paska postępu
unsigned BytesToUpdate = 0;
DWORD CurrTime;
DWORD LastProgressUpdateTime = 0;
const DWORD PROGRESS_UPDATE_MILLISECONDS = 200;

while (Bytes > 0)
{
  CurrentBytes = Bytes;
  if (CurrentBytes > BUF_SIZE) CurrentBytes = BUF_SIZE;
  Src.Read(Buf, CurrentBytes);
  Dst.Write(Buf, CurrentBytes);

  BytesToUpdate += CurrentBytes;
  CurrTime = GetTickCount();
  if (CurrTime >= LastProgressUpdateTime + PROGRESS_UPDATE_MILLISECONDS)
  {
    if (g_ProcessDataProc)
    {
      (*g_ProcessDataProc)(NULL, (int)BytesToUpdate);
      BytesToUpdate = 0;
    }
    LastProgressUpdateTime = CurrTime;
  }
  Bytes -= CurrentBytes;
}

Dodatkowo, funkcja tProcessDataProc zwraca zero, jeśli użytkownik kliknął w okienku postępu przycisk anulowania Cancel. Warto obsłużyć taką możliwość, chociaż trzeba uważać, by zrobić to prawidłowo. Plik, który rozpocząłeś rozpakowywać, należy zamknąć, a następnie skasować. Z kolei jeśli jesteś w trakcie modyfikowania archiwum, w ogóle nie powinno się dać anulować procesu w takim momencie, które mogłoby pozostawić archiwum w stanie uszkodzonym.

Modyfikowanie archiwum

Kolejny stopień wtajemniczenia to dopisanie możliwości tworzenia i modyfikowania archiwów przez wtyczkę. Od strony użytkownika tych funkcji używa się tak:

  • Aby utworzyć nowe archiwum: zaznacz jakieś pliki i/lub katalogi, wybierz z menu Pliki / Spakuj (Alt+F5), w sekcji Paker zaznacz "->" i wybierz rozszerzenie swojego formatu, wreszcie potwierdź przyciskiem OK.
  • Aby dodać lub zastąpić pliki w istniejącym archiwum: "wejdź" do archiwum, po drugiej stronie zaznacz jakieś pliki i/lub katalogi oraz wydaj polecenie Kopiowanie (F5) lub ZmPrzes (F6), aby spakować je do archiwum.
  • Aby usunąć pliki i/lub katalogi z archiwum: "wejdź" do archiwum, zaznacz wybrane elementy i wydaj polecenie Usuń (F8).

Od strony kodu natomiast, do przeprowadzania tych operacji służą dwie nowe funkcje, które trzeba zaimplementować. W sytuacji modyfikowania archiwum (jak wszystkie wymienione wyżej) w ogóle nie są wywoływane funkcje OpenArchive, CloseArchive, ReadHeader czy ProcessFile. Każda taka operacja (spakowanie wybranych plików i katalogów do archiwum, usunięcie wybranych plików i katalogów z archiwum) to pojedyncze wywołanie specjalnej funkcji, która powinna wykonać całą pracę od początku do końca.

Pierwszą z tych funkcji jest PackFiles. Będzie mogła być wywoływana, jeśli pośród flag zwracanych przez GetPackerCaps znajdzie się PK_CAPS_NEW (która oznacza możliwość pakowania nowych archiwów) i/lub PK_CAPS_MODIFY (która oznacza możliwość dodawania i zastępowania plików w istniejącym archiwum). Nagłówek tej funkcji wygląda tak:

int __stdcall PackFiles(char *PackedFile, char *SubPath, char *SrcPath, char *AddList, int Flags);

Gdzie:

  • PackedFile to ścieżka do archiwum, które ma zostać utworzone lub zmodyfikowane.
  • SubPath jest NULL, jeśli pliki mają zostać wpakowane do katalogu głównego archiwum lub jest ścieżką wewnątrz archiwum, jeśli użytkownik wszedł głębiej do katalogów w jego wnętrzu i chce wpakować zaznaczone po drugiej stronie elementy do takiego podkatalogu.
  • AddList to lista plików i katalogów do spakowania (szerzej o tym parametrze za chwilę).
  • SrcPath to ścieżka (tym razem już prawdziwa, fizyczna) do plików przeznaczonych do spakowania. Tak więc każdy plik wymieniony na liście AddList jest ścieżką względną względem SrcPath.
  • Flags zawiera bit PK_PACK_MOVE_FILES, jeśli pliki mają zostać przeniesione zamiast skopiowane do archiwum. W praktyce oznacza to, że po pomyślnym spakowaniu należy pliki i katalogi źródłowe skasować.

Pozostaje pytanie, w jaki sposób przez parametr AddList przekazana jest cała lista elementów do spakowania, a nie tylko ścieżka do jednego pliku? Otóż ta lista jest zakodowana jako ciąg następujących kolejno po sobie łańcuchów zakończonych zerem, a cała jest zakończona dwoma zerami. Innymi słowy, jest ciągiem łańcuchów zakończonych zerem, z których ostatni łańcuch jest pusty. Można z niej pobrać i przetworzyć kolejne łańcuchy na przykład taką pętlą:

while (*AddList != '\0')
{
  MyPackFile(SrcPath, AddList);
  AddList += strlen(AddList) + 1;
}

Co ogólnie powinna zrobić funkcja PackFiles? Tak naprawdę, ona służy do trzech różnych rzeczy: 1. tworzenia nowych archiwów, 2. dodawania plików i katalogów do archiwum, 3. zastępowania plików i katalogów w archiwum. Proponuję wyobrazić ją sobie jako następujący algorytm:

  1. Jeśli plik PackedFile nie istnieje, to znaczy, że tworzymy nowe archiwum. Utwórz ten plik, zapisz nagłówek.
  2. Jeśli plik PackedFile istnieje, to znaczy, że pakujemy pliki do istniejącego archiwum. Otwórz ten plik, wczytaj i sprawdź nagłówek.
  3. Dla każdego pliku z listy AddList (poprzedzonego ścieżką SrcPath):
    1. Jeśli ten plik (poprzedzony ścieżką SubPath) istnieje w archiwum, zstąp go.
    2. Jeśli ten plik nie istnieje w archiwum, dodaj go.
    3. W czasie pakowania pliku aktualizuj pasek postępu.
  4. Zamknij archiwum.

Druga z funkcji do modyfikowania archiwum nosi nazwę DeleteFiles. Będzie mogła być wywoływana, jeśli GetPackerCaps zwróci flagę PK_CAPS_DELETE. Ta flaga oznacza, że pliki i katalogi we wnętrzu archiwum będą mogły być kasowane. Nagłówek funkcji wygląda następująco:

int __stdcall DeleteFiles(char *PackedFile, char *DeleteList);

Jej interfejs nie wymaga szerszych wyjaśnień. PackedFile to ścieżka do pliku z istniejącym archiwum, zaś DeleteList to lista ścieżek do plików i katalogów wewnątrz archiwum, które trzeba skasować. Ta lista jest zbudowana podobnie, jak parametr AddList opisanej wyżej funkcji PackFiles. Funkcja powinna, podobnie jak PackFiles, zwrócić stan powodzenia - 0 jeśli wszystko się udało lub odpowiednią stałą E_, jeśli wystąpił błąd. Dobrze będzie też nie zapomnieć w jej implementacji o aktualizowaniu paska postępu.

Uwaga! Dokumentacja nie wspomina o tym, że Total Commander podczas kasowania katalogów przekazuje jako element listy DeleteList ścieżkę zakończoną "\*.*". Na przykład jeśli archiwum zawiera jeden katalog, w nim jeden podkatalog i w nim jeden plik, to skasowanie głównego katalogu spowoduje przekazanie do DeleteFiles takiej listy ścieżek:

Dir1\SubDir\File2.txt
Dir1\SubDir\*.*
Dir1\*.*

Inne funkcje

Wtyczka może posiadać okienko konfiguracji. Aby je zaimplementować, trzeba w GetPackerCaps zwrócić flagę PK_CAPS_OPTIONS oraz napisać funkcję ConfigurePacker. Z punktu widzenia użytkownika dostęp do tego okienka zapewnia polecenie Pliki / Spakuj, przycisk Konfiguruj. Nagłówek funkcji wygląda tak:

void __stdcall ConfigurePacker(HWND Parent, HINSTANCE DllInstance);

Jeśli znasz WinAPI, to parametrów nie muszę chyba wyjaśniać (a jeśli nie znasz, to i tak nic tu nie pomogę :) W najprostszym przypadku kod tej funkcji może tylko pokazać MessageBox z informacją o wtyczce i jej autorze. Jeśli natomiast utworzyłeś plik zasobów i dodałeś do niego okienko dialogowe, to możesz je wyświetlić kodem podobnym do tego:

extern "C"
void __stdcall ConfigurePacker(HWND Parent, HINSTANCE DllInstance)
{
  DialogBox(DllInstance, MAKEINTRESOURCE(IDD_DIALOG_CONFIG), Parent, &ConfigDlgProc);
}

BOOL CALLBACK ConfigDlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
  // Procedura obsługi komunikatów okna dialogowego - wiadomo...
}

Powstaje tutaj pytanie, gdzie zapamiętywać konfigurację wtyczki. Okazuje się, że Total Commander i tutaj przychodzi z pomocą. Zaraz po załadowaniu biblioteki WCX z wtyczką wywołuje funkcję PackSetDefaultParams (o ile ją zaimplementujesz i wyeksportujesz), w której w parametrze dps->DefaultIniName przekazuje ścieżkę do pliki INI zalecanego dla wtyczek do przechowywania konfiguracji. Należy ją skopiować do jakiejś swojej zmiennej globalnej. Inna opcja proponowana przez Ghislera to założenie własnego pliku w tym samym katalogu, na który wskazuje otrzymana ścieżka.

Plik INI to jednak nic strasznego i często jest najlepszym wyborem do takich prostych zastosowań. Nie musisz pisać własnego parsera tego formatu. WinAPI dostarcza gotowych funkcji do jego obsługi. Opis tych funkcji w MSDN znajduje się na dole listy w rozdziale Registry Functions. Najbardziej interesujące z nich to GetPrivateProfileString i WritePrivateProfileString. Na przykład aby odczytać wartość zapisaną w pliki INI w takiej postaci:

[MyVfsWcx]
CompressionLevel=Best

Użyj takiego wywołania:

char Buf[256];
GetPrivateProfileString(
  "MyVfsWcx", // Nazwa sekcji
  "CompressionLevel", // Nazwa klucza
  "Default", // Wartość domyślna
  Buf, _countof(Buf), // Bufor wyjściowy
  dps->DefaultIniName); // Nazwa pliku INI ze ścieżką

Na zakończenie wspomnę o innych funkcjach Total Commandera, które działają również wewnątrz archiwów tak jak na zwykłych plikach i katalogach. Nie musisz nic szczególnego robić, żeby zadziałały - wystarczy wszystko to, co opisałem wyżej.

  • Możesz policzyć sumaryczny rozmiar zajmowany przez katalog wciskając Spację.
  • Możesz policzyć sumaryczny rozmiar każdego katalogu na liście wciskając Alt+Shift+Enter.
  • Możesz podejrzeć zawartość pliku za pomocą Listera wciskając F3. Plik z archiwum zostanie wtedy wypakowany do katalogu tymczasowego.
  • Możesz edytować plik za pomocą systemowego Notatnika wciskając F4. Plik z archiwum zostanie wtedy wypakowany do katalogu tymczasowego, a jeśli zostanie zmieniony, Total Commander spakuje jego nową wersję z powrotem.
  • Możesz edytować plik za pomocą domyślnie skojarzonego z nim programu wciskając po prostu Enter. Plik z archiwum zostanie wtedy wypakowany do katalogu tymczasowego, a jeśli zostanie zmieniony, Total Commander spakuje jego nową wersję z powrotem.
  • Możesz wyszukiwać pliki i katalogi, a także podane słowo w treści plików za pomocą Polecenia / Szukaj (Alt+F7). Nie zapomnij zaznaczyć pola Przeszukuj archiwa.
Adam Sawicki
http://asawicki.info
24.02.2009

Tekst dodał:
Adam Sawicki
24.02.2009 21:13

Ostatnia edycja:
Adam Sawicki
05.05.2012 14:24

Kategorie:

Aby edytować tekst, musisz się zalogować.

# Edytuj Porównaj Czas Autor Rozmiar
#4 edytuj (poprz.) 05.05.2012 14:24 Adam Sawicki 37.56 KB (-5)
#3 edytuj (poprz.) (bież.) 05.05.2012 14:19 Adam Sawicki 37.56 KB (+6)
#2 edytuj (poprz.) (bież.) 05.05.2012 14:15 Adam Sawicki 37.55 KB (+375)
#1 edytuj (bież.) 24.02.2009 21:13 Adam Sawicki 37.19 KB
Zwykły
Do sprawdzenia
Do akceptacji
  • Napisz komentarz:
    Aby dodać swój komentarz, musisz się zalogować.
Licencja Creative Commons

Warsztat używa plików cookies. | Copyright © 2006-2017 Warsztat · Kontakt · Regulamin i polityka prywatności
build #ff080b4740 (Tue Mar 25 11:39:28 CET 2014)