Program do usuwania błędnych skrótów.


    Program przedstawiony w tym kursie praktycznym ma za zadanie przeskanowanie Menu Start w poszukiwaniu błędnych skrótów, czyli takich które prowadzą do nieistniejących już plików. Wykorzystam tutaj funkcje wyszukiwania plików, odczytywania informacji o skrótach i sprawdzania ścieżek dostępu do specjalnych folderów, wszystkie te funkcje można znaleźć w serwisie, tutaj zostaną one nieco zmodyfikowane by dostosować je do potrzeb programu. Opis tworzenie programu postaram przedstawić się w maksymalnie najprostszy sposób tak by mogli go zrozumieć początkujący. Nie będę tutaj opisywał sposobu działania kodu, ale opiszę rolę jaką w programie pełnią jego poszczególne składniki.

Zaczynamy od utworzenia nowego projektu, zapisujemy go w katalogu, a następnie przechodzimy do formularza Form1  i zmieniamy jego właściwości w oknie Object Inspector na zakładce Properies według podanego wzoru:

 

BorderIcons -> biMaximize = false (usuwa ikonę maksymalizacja z paska tytułowego)

BorderStyle = bsSingle (zmienia zachowanie formularza uniemożliwiając zmianę jego rozmiaru)

Height = 424

Position = poDesktopCenter (ustawia pozycjonowanie formularza w chwili uruchomienia na wyśrodkowane na pulpicie)

Width = 870

 

Następnie umieszczamy na formularzu komponenty tak jak widać na załączonym obrazku:


 

Następnie ustawiamy właściwości poszczególnych komponentów według wzoru:
 

TButton: Skan

Caption = Skanuj

Left = 16

Top = 24

TButton: Del

Caption = Usuń

Left = 104

Top = 24

TProgressBar: ProgressBar1

Left = 200

Top = 28

Width = 649

TListView: ListView1

Columns = (Add news) Skrót; (Add news) Plik

Height = 313

Left = 16

Top = 64

ViewStyle = vsReport

Width = 833

 

Teraz możemy przystąpić do kodowania na wstępie w pliku źródłowym (Unit1.cpp) na samej górze dodajemy wpis #define NO_WIN32_LEAN_AND_MEAN, a trochę niżej w sekcji include włączmy do projektu bibliotekę shlobj.h:


// Plik źródłowy Unit1.cpp
//--------------------------------
#define NO_WIN32_LEAN_AND_MEAN

#include <vcl.h>
#pragma hdrstop

#include "Unit1.h"
#include <shlobj.h>

//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"

TForm1 *Form1;

Wpis NO_WIN32_LEAN_END_MEAN musi być dodany ponieważ korzystamy z biblioteki shlobj.h, a w środowisku Borland C++ Builder w wersji 6 ta biblioteka wywołuje błędy, a ten wpis naprawia ten błąd umożliwiając korzystanie z tej biblioteki. Biblioteka shlobj.h jest potrzebna ponieważ w dalszej części programowania będzie wykorzystywali znajdujące się w niej funkcje, a to po to, żeby nie tworzyć tychże funkcji samodzielnie od podstaw - takie ułatwienie, ale nie należy się tym przejmować, ponieważ programiście nieustannie tworzą jakieś biblioteki, a inni programiści wykorzystują je do własnych potrzeb.

    Teraz tworzymy funkcję o nazwie ShortcutInfo, która będzie odczytywała informacje o skrócie, w przypadku tego programu funkcja będzie zwracała jako wynik swojej pracy ścieżkę dostępu do programu do którego prowadzi skrót, czyli przykładowo mamy w Menu Start skrót o nazwie WorPad.lnk, skrót prowadzi do programu C:\Program Files\Windows NT\Accessories\wordpad.exe i właśnie funkcja będzie zwracała tą ścieżkę dostępu, ktoś mógłby się spytać: A po co mi ta ścieżka dostępu? A no po to, że jeżeli chcemy sprawdzić czy skrót prowadzi do istniejącego pliku, to trzeba sprawdzić najpierw do jakiego pliku ten skrót prowadzi, a potem trzeba sprawdzić czy taki plik istnieje, i jeżeli np. plik nie istnieje no to już wiemy, że jest to błędny skrót i to jest przykład na to jak powinien podchodzić informatyk do problemu, ponieważ z doświadczenia wiem, że większość tzw. informatyków stając przed podobnym problemem zaczyna się zastanawiać skąd wziąć funkcję, która od razu odpowie, czy skrót jest błędny czy nie, a programowanie nie na tym polega.
    Tak więc funkcja ShortcutInfo nie odpowie nam, czy jest to błędny skrót czy nie, ale zrobi to kolejna funkcja, ale o tym później. Funkcja ShortcutInfo pobiera dwa argumenty pierwszy lnkFileName jest typu AnsiString i trzeba tutaj podawać ścieżkę dostępu do skrótu (i tylko do skrótu *.lnk), z którego chcemy wydobyć ścieżkę pliku do którego ten skrót prowadzi. Drugi argument FileName jest typu char i tutaj nie można przekazywać funkcji żadnych parametrów, należy jej przekazać wskaźnik do zmiennej typu char, czyli taką zmienną, gdyż funkcja zwróci ścieżkę dostępu do pliku, właśnie do tej zmiennej. Sama funkcja jest typu bool co oznacza, że zwraca wartości typu true jeżeli odczyt informacji ze skrótu zakończył się sukcesem, lub false jeżeli nie udało się wydobyć informacji ze skrótu, gdy np. sam skrót nie istnieje. Powiedziałem tutaj, że funkcja zwraca wartość typu bool, ale to tylko bezpośrednio, ponieważ pośrednio poprzez drugi argument czyli zmienną FileName, funkcja zwraca również ścieżkę dostępu do pliku (element docelowy).

// Plik źródłowy Unit1.cpp
//--------------------------------

#define NO_WIN32_LEAN_AND_MEAN

#include <vcl.h>
#pragma hdrstop

#include "Unit1.h"
#include <shlobj.h>

//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"

TForm1 *Form1;

bool ShortcutInfo(const AnsiString lnkFileName, char *FileName)
{
 HRESULT hr;
 IShellLink *psl;
 WIN32_FIND_DATA wfd;

 IPersistFile *ppf;

 wchar_t *lpw;
 wchar_t *buf;
 bool Result = false;

 buf = (wchar_t *)AllocMem((MAX_PATH + 1) * sizeof(wchar_t));

 try
 {
  if(SUCCEEDED(CoInitialize(NULL)))
  if(SUCCEEDED(CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkA, (void **)(&psl))))
  {
   hr = psl->QueryInterface(IID_IPersistFile,(void **)(&ppf));
   if(SUCCEEDED(hr))
   {
    lpw = StringToWideChar(lnkFileName, buf, MAX_PATH);
    hr = ppf->Load(lpw, STGM_READ);
    if(SUCCEEDED(hr))
    {
     hr = psl->Resolve(NULL, SLR_NO_UI);
     if(SUCCEEDED(hr))
     {
      psl->GetPath(FileName, MAX_PATH, &wfd, SLGP_RAWPATH);
      Result = true;
     }
    }
   }
  }
 }
 __finally
 {
  delete buf;
 }
 return Result;
}
//---------------------------------------------------------------------------

 

    Kolejnym krokiem jest utworzenie funkcji sprawdzającej czy mamy do czynienia z błędnym skrótem czy nie, funkcja TestShortcut korzysta z funkcji zawartych w bibliotece stdio.h dlatego trzeba tą bibliotekę również włączyć do projektu poprzez sekcję include. Funkcja ta pobiera dwa argumenty, pierwszy link jest typu AnsiString i przekazuje on do funkcji ścieżkę dostępu do skrótu, ten argument jest dokładnie taki sam jak pierwszy argument funkcji ShortcutInfo, a to dlatego, że ta funkcja po prostu przekazuje ten argument dalej, czyli najpierw ścieżka dostępu do skrótu będzie przekazywana do funkcji TestShortcut, a następnie funkcja TestShortcut przekaże ten argument do funkcji ShortcutInfo, dzieje się tak dlatego, że to właśnie ta funkcja sprawdza czy skrót jest błędny, a funkcji ShortcutInfo potrzebuje tylko do wyciągnięcia ścieżki dostępu do pliku. Drugi argument funkcji Buf typu char spełnia dokładnie taką samą rolę jak drugi argument funkcji ShortcutInfo czyli pobiera ścieżkę dostępu do pliku z funkcji ShortcutInfo i przekazuje ją funkcji TestShortcut, a ta wykorzystuje go potem do sprawdzenie czy plik istnieje po czym przekazuje go dalej w celach informacyjnych, ale o tym później:


// Plik źródłowy Unit1.cpp
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
}
//---------------------------------------------------------------------------
#include <stdio.h>
bool TestShortcut(String link, char *Buf)
{
 char Win[MAX_PATH];
 char file[MAX_PATH + 1];
 GetWindowsDirectory(Win, MAX_PATH);

 ShortcutInfo(link, file);
 String path = (String)file;
 if(path.SubString(1, 12).UpperCase() == "%SYSTEMROOT%")
 {
  path = (String)Win + "\\" + path.Delete(1, 12);
 }
 if(FileExists(path) || path.IsEmpty())
 {
  sprintf(Buf, "%s", "");
  return true;
 }
 sprintf(Buf, "%s", file);

 return false;
}
//---------------------------------------------------------------------------


Pokrótce wyjaśnię jak działa ta funkcja. Więc już na wstępie mamy zadeklarowane dwie zmienne typu char. Zmienna Win służy do przechowywania ścieżki dostępu do katalogu systemowego Windows, zmienna file służy do przechowywania informacji o pliku do którego prowadzi skrót, informacja ta, co wyjaśniłem wcześniej jest wyciągana ze skrótu poprzez funkcję ShortcutInfo.
Informację o tym gdzie znajduje się katalog systemowy Windows pobiera funkcja GetWindowsDirectory i przekazuje ją w postaci ścieżki dostępu do zmiennej Win. Niektórzy może się zastanawiają po co sprawdzać ścieżkę dostępu do katalogu Windows skoro wiadomo, że jest to C:\Windows, otóż niekoniecznie, Windows 98 musiał być instalowany na dysku C:, ale Windows XP już nie więc może się zdarzyć, że jest zainstalowany na innym dysku i dlatego właśnie potrzebujemy tej informacji. Kolejne pytanie, które się ciśnie na usta to: po co nam ta ścieżka dostępu? Niektóre skróty do plików mogą mieć podane w polu 'Element docelowy', nie pełną ścieżkę dostępu do pliku, lecz ścieżka do katalogu Windows może być podana jako zmienna środowiskowa %SystemRoot% jak ma to np. miejsce w przypadku kalkulatora:

 

 

W dalszej części kodu funkcja wykorzystując informację o tym gdzie znajduje się katalog Windows podstawia pod tą zmienną środowiskową właśnie tą ścieżkę i w efekcie zamiast %SystemRoot%\System32\calc.exe otrzymuje np. c:\Windows\System32\calc.exe, a wydobyciem zmiennej środowiskowej i jej zamianą na pełną ścieżkę dostępu zajmuje się ten fragment kodu:


String path = (String)file;
if(path.SubString(1, 12).UpperCase() == "%SYSTEMROOT%")
{
 path = (String)Win + "\\" + path.Delete(1, 12);
}

Jak widać na tym przykładzie ścieżka wydobyta ze skrótu zostaje obcięta o zmienną środowiskową, a w jej miejsce zostaje podstawiona ścieżka dostępu do katalogu Windows.

Funkcja TestShortcut wydobywa ścieżkę odstępu do pliku, do którego prowadzi skrót wywołując funkcję ShortcutInfo(link, file); jak widać przekazuje ona funkcji ShortcutInfo jako pierwszy argument ścieżkę dostępu do skrótu, którą sama otrzymała poprzez zmienną link, oraz wskaźnik w postaci zmiennej file, który pobierze informację o elemencie docelowym skrótu. Potem zmienna file zostaje przepisana do nowo utworzonej zmiennej path typu AnsiString.

 

Kolejna cześć kodu:

 

if(FileExists(path) || path.IsEmpty())
{
 sprintf(Buf, "%s", "");
 return true;
}
sprintf(Buf, "%s", file);
return false;

 

sprawdza czy element docelowy skrótu istnieje. Konkretnie za sprawdzenie pliku odpowiada funkcja FileExists pobierająca jako jedyny argument ścieżkę dostępu właśnie do tego pliku przechowywaną w zmiennej path. Jeżeli plik istnieje funkcja zwraca wartość true, w przeciwnym razie false. W tej części kodu znajduje się również taki fragment path.IsEmpty(), który sprawdza, czy zmienna path zawiera w ogóle jakąś wartość, jest on potrzebny z tego względu, że jeżeli mamy do czynienia ze skrótem prowadzącym do adresu internetowego np. http://cyfbar.republika.pl/index.html to funkcja  ShortcutInfo nie zwróci ścieżki dostępu do tego adresu, lecz pustą wartość, co przez funkcję FileExists zostało by rozpoznane jako nieistniejący plik i w efekcie program uznałby to za błędny skrót i ten fragment kodu właśnie eliminuje tą nieprawidłowość.

Znaczenie tego fragmentu kodu: if(FileExists(path) || path.IsEmpty()) jest takie: Jeżeli funkcja FileExists zwróci wartość true to znaczy, że plik przekazany funkcji w ścieżce dostępu zmiennej path istnieje, lub jeżeli ścieżka dostępu (zmienna path) jest pusta to wykonaj kod zawarty miedzy nawiasami, czyli wystarczy, że jeden z tych warunków zostanie spełniony a funkcja zakończy swoje działanie wykonując kod zawarty między nawiasami, a tam zmiennej Buf zostaje przekazana pusta wartość (zostaje wyczyszczona z poprzedniej wartości) i funkcja zwraca wartość true informując tym samym program, że jest to prawidłowy skrót. Jeżeli natomiast żaden z tych warunków nie jest spełniony to wykonuje się dalsza cześć kodu, czyli zmiennej Buf zostaje przypisana wartość zmiennej file zawierającej element docelowy badanego skrótu, a to po to żeby użytkownik wiedział do jakiego nieistniejącego pliku prowadzi dany skrót, następnie funkcja zwraca wartość false informując tym samym program, że jest to błędny skrót.

 

To nie jest jeszcze koniec kodowania, teraz trzeba stworzyć kod wyszukujący wszystkie skróty w Menu Start. Posłużę się tutaj funkcją wyszukiwania folderów, podfolderów i plików prezentowaną już w serwisie:


// Plik źródłowy Unit1.cpp
//---------------------------------------------------------------------------

int pos, ile;
void __fastcall TForm1::FindDir(TListView *lista, String Dir, String typ, bool oblicz)
{
 TSearchRec sr;
 if(FindFirst(Dir + "\\*.*", faAnyFile, sr) == 0)
 {
  do
  {
   if(((sr.Attr & faDirectory) > 0) && (sr.Name != ".") && (sr.Name != ".."))
   {
    FindDir(lista, Dir + "\\" + sr.Name, typ, oblicz);
   }
   if((sr.Attr & faDirectory) == 0)
   {
    if(ExtractFileExt(sr.Name).SubString(2, 5) == typ)
    {
     if(!oblicz)
     {
      ProgressBar1->Position += 1;
      char buf[MAX_PATH + 1];
      if(!TestShortcut(Dir + "\\" + sr.Name, buf))
      {
       lista->Items->Add();
       lista->Items->Item[pos]->Caption = Dir + "\\" + sr.Name;
       lista->Items->Item[pos]->SubItems->Add((String)buf);
       pos++;
      }
     }
     else ile++;
    }
   }
   Application->ProcessMessages();
  }
  while(FindNext(sr) == 0);
  FindClose(sr);
 }
}
//---------------------------------------------------------------------------

 

Zanim przejdziemy dalej, trzeba jeszcze dodać deklarację tej funkcji do pliku nagłówkowego (Unit1.h) w sekcji private:

 

// Plik nagłówkowy Unit1.h
//---------------------------------------------------------------------------

#ifndef Unit1H
#define Unit1H

//---------------------------------------------------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <ComCtrls.hpp>

//---------------------------------------------------------------------------
class TForm1 : public TForm
{
 __published: // IDE-managed Components
             TButton *Del;
             TButton *Skan;
             TListView *ListView1;
             TProgressBar *ProgressBar1;
             void __fastcall SkanClick(TObject *Sender);
             void __fastcall DelClick(TObject *Sender);
private: // User declarations
        void __fastcall FindDir(TListView *lista, String Dir, String typ, bool oblicz);
public: // User declarations
        __fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif


Funkcja FindDir może działać w dwóch trybach, pierwszy to policzenie ile skrótów znajduje się w Menu Start, dzieje się to wtedy gdy wywołamy funkcję z czwartym argumentem - oblicz - ustawionym na true, nie ma to większego wpływu na szybkość działania programu, testowałem to u siebie i 600 skrótów funkcja obliczyła w mniej niż sekundę. Ten element jest potrzebny tylko do zobrazowanie na ProgressBar1 postępu sprawdzania błędnych skrótów. W drugim trybie funkcja (czwarty argument otrzymuje wartość false) wylicza ponownie wszystkie skróty, jednak tym razem wywołuje funkcje TestShortcut i przekazując jej jako argumenty ścieżkę dostępu do testowanego skrótu oraz wskaźnik do zmiennej buf przechowującej informację o błędnym elemencie docelowym skrótu. Jako pierwszy argument funkcja FindDir pobiera wskaźnik do obiektu typu TListView, ponieważ to właśnie w tym obiekcie funkcja będzie tworzyła listę błędnych skrótów, drugi argument to ścieżka dostępu do katalogu w którym będą wyszukiwane skróty, trzeci argument to rozszerzenie dla poszukiwanego typu plików i tutaj należy zawsze podawać wartość "lnk", czwarty argument to przełącznik trybu pracy funkcji który opisałem wyżej. Funkcja steruje również paskiem postępu ProgressBar.

 

Teraz w zdarzeniu OnClick dla przycisku Scan wywołamy wszystkie funkcje przeprowadzając tym samym wyszukiwanie błędnych skrótów. Zdarzenie OnClick tworzymy w Obiect Inspector na zakładce Events:


// Plik źródłowy Unit1.cpp
//---------------------------------------------------------------------------
void __fastcall TForm1::SkanClick(TObject *Sender)
{
 Screen->Cursor = crHourGlass;
 Skan->Enabled = false;
 ile = 0;
 char path[255];

 SHGetSpecialFolderPath(NULL, path, CSIDL_COMMON_STARTMENU, 0);
 FindDir(NULL, (String)path, "lnk", true);

 SHGetSpecialFolderPath(NULL, path, CSIDL_STARTMENU, 0);
 FindDir(NULL, (String)path, "lnk", true);

 ProgressBar1->Max = ile;
 ProgressBar1->Position = 0;
 pos = 0;
 ListView1->Items->Clear();

 SHGetSpecialFolderPath(NULL, path, CSIDL_COMMON_STARTMENU, 0);
 FindDir(ListView1, (String)path, "lnk", false);

 SHGetSpecialFolderPath(NULL, path, CSIDL_STARTMENU, 0);
 FindDir(ListView1, (String)path, "lnk", false);
 Skan->Enabled = true;
 Del->Enabled = true;
 Screen->Cursor = crDefault;

 Application->MessageBox(("Zakończono wyszykiwanie.\nZnaleziono " + IntToStr(pos)
  + " błędnych skrótów.").c_str(), "Wyszukiwanie zakończone", MB_OK | MB_ICONINFORMATION);
 ProgressBar1->Position = 0;
}
//---------------------------------------------------------------------------

 

W tym kodzie na uwagę zasługuje funkcja SHGetSpecialFolderPath, która zwraca ścieżkę dostępu do folderów specjalnych, a w tym programie funkcja  zwraca ścieżkę dostępu do katalogu [Menu Start] w którym to właśnie znajdują się skróty. W zdarzeniu OnClick przycisku Scan ta funkcja jest wywoływana czterokrotnie, za pierwszym razem funkcja sprawdza ścieżkę dostępu do katalogu [Menu Start] dla wszystkich użytkowników (All Users\Menu Start), za drugim razem tylko dla bieżącego użytkownika. Po każdym wywołaniu funkcji następuje również wywołanie funkcji FindDir, w dwóch pierwszych przypadkach jest to robione w celu obliczenia ile jest tych skrótów, po czym maksymalna wartość ProgressBar1 zostaje ustawiona na wyliczona liczbę skrótów, następnie funkcje SHGetSpecialFolderPath i FindDir zostają ponownie wywołane dwukrotnie, ale tym razem w celu wyszukania błędnych skrótów. Wynik wyszukiwania zostanie zobrazowany w ListView1, na liście pojawią się ścieżki dostępu do błędnych skrótów z informacją o tym do jakich nieistniejących plików prowadzą.
Zanim przejdę do kolejnego kodu wspomnę jeszcze o argumencie przekazywanym funkcji SHGetSpecialFolderPath, otóż mamy tam takie argumenty:

 

CSIDL_COMMON_STARTMENU       - All Users\Menu Start

CSIDL_STARTMENU              - <nazwa zalogowanego użytkownika>\Menu Start
 

Możliwych argumentów jest więcej i można je znaleźć w poradzie: ścieżki dostępu do katalogów specjalnych, ja wymienię tutaj jednak jeszcze trzy inne i bardzo przydatne:

 

CSIDL_DESKTOP                 - Pulpit

CSIDL_DESKTOPDIRECTORY        - <nazwa zalogowanego użytkownika>\Pulpit

CSIDL_COMMON_DESKTOPDIRECTORY - All Users\Pulpit

 

Teraz tworzymy ostatni element programu, czyli kod usuwający błędne skróty na podstawie zawartości listy ListView. W tym celu tworzymy zdarzeni OnClick dla przycisku Del i umieszczamy w nim taki kod:

 

// Plik źródłowy Unit1.cpp
//---------------------------------------------------------------------------
void __fastcall TForm1::DelClick(TObject *Sender)
{
 Screen->Cursor = crHourGlass;
 Skan->Enabled = false;
 Del->Enabled = false;
 ProgressBar1->Max = pos;
 for(int i = 0; i < ListView1->Items->Count; i++)
 {
  DeleteFile(ListView1->Items->Item[i]->Caption);
  ProgressBar1->Position = i;
  Application->ProcessMessages();
 }
 Skan->Enabled = true;
 ListView1->Items->Clear();
 Screen->Cursor = crDefault;

 Application->MessageBox("Usunięto wszystkie błędne skróty.",
  "Usuwanie zakońcone", MB_OK | MB_ICONINFORMATION);
 ProgressBar1->Position = 0;
}
//---------------------------------------------------------------------------

Tutaj w każdym obiegu pętli for jest sprawdzana lista i jest z niej pobierana ścieżka dostępu do błędnego skrótu, następnie jest ona przekazywana do funkcji DeleteFile jako jej argument, i funkcja ta na tej podstawie usuwa błędny skrót.

Program usuwa tylko skróty które uzna za błędne, wiec gdyby nawet się okazało, że rozpoznał skróty prawidłowe jako błędne, to usunie tylko skrót i nie ma to żadnego wpływy na plik do którego ten skrót prowadzi.

Program nie jest doskonały to tylko przykład, służący do nauki programowania. Do wad programu można zaliczyć np. to iż program nie usuwa katalogów, z których zostały usunięte wszystkie skróty, no i druga wada to, że usuwa błędne skróty tylko z konta All Users i z konta aktualnie zalogowanego użytkownika, czyli nie przeszukuje kont innych użytkowników, chociaż z drugie strony to może nawet lepiej, jednak zawsze można dodać do programu opcję, że administrator może usuwać błędne skróty ze wszystkich kont.

Pliki źródłowe projektu BCB 6, archiwum RAR - rozmiar 258 kb.

Opracował: Cyfrowy Baron