Wenn man ein Smartphone an einen Windows-Computer anschließt, wird der Inhalt nicht, wie z.B. bei USB-Speichergeräten, in dessen Dateisystem eingebunden, so dass man zum Zugriff auf diese Dateien nicht die alt bekannten Funktionen, wie z.B. FindFirst/FindNext oder FileExists, verwenden kann.
Der Grund dafür ist, dass derartige Geräte über das Media Transport Protocol (MTP) angebunden werden. Um auf die Inhalte zuzugreifen, benötigt man die Funktionen des Windows-Portable-Devices-Interfaces (WPD).
Die Units, die erforderlich sind, um die WPD-Funktionen in eigene Delphi-Programme einbinden zu können, sind leider nicht Bestandteil der System-Bibliotheken. Ich habe daher die folgenden beiden Units erstellt, die eine Verwendung von WPD in eigenen Delphi-Programmen ermöglichen.
Das Resultat dieser Arbeit kann man bei GitHub herunterladen. Dort findet man auch ein Delphi-Demoprogramm für die wichtigsten WPD-Funktionen. Es handelt sich dabei um eine Konvertierung des von Microsoft als Beispiel bereit gestellten Visual-Studio-Programms WpdApiSample.
Mit den Funktionen von Windows-Portable-Devices kann man auf alle Inhalte und Eigenschaften des angeschlossenen Geräts zugreifen.
Wenn es nur darum geht, einzelne Dateien von dem Gerät auf einen anderen Datenträger, z.B. die interne Festplatte, zu kopieren, gibt es aber auch einen deutlich einfacheren Weg. Wenn man den Windows-Explorer öffnet, findet man dort unter Dieser PC auch das angeschlossene Gerät aufgeführt. Klickt man darauf, so werden als dessen Unterverzeichnisse auf der ersten Ebene das Gerät selbst und evtl. daran angeschlossene Speicherkarten angezeigt. Darunter öffnet sich dann die Verzeichnisstruktur in gewohnter Form. Ebenfalls auf bekannte Weise kann man dort Dateien auswählen und an einen anderen Ort kopieren.
Die Funktionen der Windows Shell, wie sie auch vom Explorer benutzt werden, können unter Delphi über die Systembibliotheks-Unit Vcl.Shell.ShellCtrls.pas in eigene Programme eingebunden werden. Auf diesem Weg kann man dann auch auf die Dateien des angeschlossenen Geräts zugreifen. Der Weg dahin ist allerdings etwas trickreich und soll im folgenden beschrieben werden.
Man platziert zunächst auf dem Hauptformular die Komponenten ShellTreeView und ShellListView. Beide werden im Entwurfsmodus über die entsprechende Eigenschaft miteinander gekoppelt. Über ShellTreeView.Path kann im Programm das Anfangsverzeichnis festgelegt werden. Liegt es auf einem herkömmlichen Datenträger (d.h. gehört es in das normale Dateisystem), gibt man hier einfach den Pfad in gewohnter Weise an, z.B. c:\ProgramData\Common Files\. Dies funktioniert allerdings nicht mehr so einfach, wenn es sich um einen Pfad auf dem angeschlossenen Gerät handelt. Die richtige Angabe lässt sich nur durch Probieren ermitteln. Man erstellt dazu ein OnClick-Ereignis für ShellTreeView mit folgenden Programmcode:
procedure TMainForm.ShellTreeViewClick(Sender: TObject); var pn : PWideChar; sa,se : string; n1,n2 : integer; begin with ShellTreeView.SelectedFolder do begin if (fpFileSystem in Properties) then LastPath:=PathName else begin OleCheck(SHGetNameFromIDList(AbsoluteID,SIGDN_DESKTOPABSOLUTEPARSING,pn)); sa:=pn; CoTaskMemFree(pn); pn:=nil; OleCheck(SHGetNameFromIDList(AbsoluteID,SIGDN_DESKTOPABSOLUTEEDITING,pn)); se:=pn; CoTaskMemFree(pn); pn:=nil; n1:=pos('\\\?\',sa); n1:=pos('\',sa,n1+5); if n1>0 then begin n2:=pos('\',se); n2:=pos('\',se,n2+1); LastPath:=copy(sa,1,n1)+copy(se,n2+1,length(se)); end else LastPath:=sa; end; Label1.Caption:=LastPath; end; end;
Erläuterung: Bei herkömmlichen Pfaden ist die Eingenschaft fpFileSystem gesetzt und der Pfad kann direkt übernommen werden. Andernfalls muss man mit der Funktion SHGetNameFromIDList aus dem Wert für AbsoluteID von SelectedFolder zwei unterschiedliche Pfade ermitteln:
Wenn man jetzt versucht, einen der beiden so ermittelten Pfade der Eigenschaft
ShellTreeView.Path zuzuweisen, gibt es in beiden Fällen die Fehlermeldung
Falscher Parameter (0x80070057). Man muss die beiden Pfade kombinieren. Vom Parsing-Namen
benötigt man die ersten beiden Teilpfade, vom anderen den Rest:
::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SD-Karte\DCIM\Camera,
Wenn man dies ShellTreeView.Path zuweist, wird der Pfad richtig erkannt und im
Baum angezeigt.
In ShellListView wird die Eigenschaft MultiSelect aktiviert, so dass man mehrere Dateien auswählen kann. Dann klickt man auf die Schaltfläche bbCopy und führt folgenden Code aus:
procedure TMainForm.bbCopyClick(Sender: TObject); var i,n,j : integer; fileOp : IFileOperation; siSrcList : IShellItemArray; idList : array of PItemIDList; siSrcFile,siDestFolder : IShellItem; begin with ShellListView do if assigned(Selected) then begin n:=Selected.Index; OleCheck(CoCreateInstance(CLSID_FileOperation,nil,CLSCTX_ALL,IFileOperation,fileOp)); if SelCount=1 then with SelectedFolder do begin OleCheck(SHCreateItemFromIDList(AbsoluteID,IShellItem,siSrcFile)); OleCheck(SHCreateItemFromParsingName(PChar(edDestDir.Text),nil,IShellItem,siDestFolder)); OleCheck(fileOp.CopyItem(siSrcFile,siDestFolder,pchar(DisplayName),nil)); OleCheck(fileOp.PerformOperations); end else begin SetLength(idList,SelCount); j:=0; OleCheck(fileOp.SetOperationFlags(FOF_FILESONLY+FOF_NOCONFIRMMKDIR+FOF_NO_CONNECTED_ELEMENTS)); for i:=n to Items.Count-1 do if Items[i].Selected then begin idList[j]:=Folders[i].AbsoluteID; inc(j); end; OleCheck(SHCreateShellItemArrayFromIDLists(SelCount,@idList[0],siSrcList)); OleCheck(SHCreateItemFromParsingName(PChar(edDestDir.Text),nil,IShellItem,siDestFolder)); OleCheck(fileOp.CopyItems(siSrcList,siDestFolder)); OleCheck(fileOp.PerformOperations); idList:=nil; end; end; end;
Erläuterung: Zum Kopieren wird die Funktion IFileOperation verwendet.
Das Zielverzeichnis liegt als gewöhnlicher Pfad vor und wird an SHCreateItemFromParsingName
übergeben, um das zugehörige IShellItem zu erzeugen. Für die Datei
auf dem angeschlossenen Gerät geht das nicht (s.o.). Man kann aber die AbsoluteID
von SelectedFolder im Zusammenhang mit der Funktion SHCreateItemFromIDList
verwenden.
Wurden mehrere Datei ausgewählt, wird zunächst ein Array idList mit den ausgewählten
AbsoluteIDs erzeugt und dann daraus mit SHCreateShellItemArrayFromIDLists eine Liste
der Quell-IShellItems.
Aus der AbsoluteID von dem in ShellListView ausgewählten Eintrag wird mit
SHCreateItemFromIDList das zugehörige IShellItem2 erzeugt. Darüber kann die
Eigenschaft WPD_OBJECT_ID abgefragt werden. Übrigens lassen sich auf diese Weise
auch andere Eigenschaften, wie z.B. WPD_OBJECT_DATE_MODIFIED für die Änderungszeit
der Datei, abfragen.
Die so ermittelte ObjectID kann dann in den Funktionen für Windows Portable Devices
weiter verwendet werden (siehe oben).
procedure TMainForm.ShellListViewClick(Sender: TObject); var si : IShellItem2; pv : TPropVariant; ObjectID : string; begin with ShellListView do if assigned(Selected) then begin ff:=Folders[Selected.Index]; with ff do begin OleCheck(SHCreateItemFromIDList(AbsoluteID,IShellItem2,si)); OleCheck(si.GetProperty(WPD_OBJECT_ID,pv); ObjectID:=pv.pwszVal; PropVariantClear(pv); end; Label1.Caption:=ObjectID; end; end;