16. Okt 2023
Lesedauer 7 Min.
Windows Document History
Ein eigenes Steuerelement für Verlaufsdaten, Teil 6
Nicht nur Webbrowser zeichnen Verlaufsdaten auf, sondern auch Windows.

Das Benutzersteuerelement HistoryCtl stellt alle angewählten URL-Verlaufsdaten der Webbrowser Edge, Google Chrome sowie dem in der vorangegangenen Serie aufgebauten Steuerelement BrowserCtl in einer geordneten Strukturansicht zusammen. Den bislang letzten Stand der Arbeiten finden Sie unter [1] und dort wiederum Verweise auf die vorangegangenen Folgen der Serie. Im nächsten Schritt sollen auch die von Windows aufgezeichneten Verlaufsdaten angebunden werden. Sie werden an unterschiedlichen Stellen des Systems verwaltet und sind mitunter verborgen. Fündig werden Sie sowohl im Windows-Dateisystem als auch in der Systemregistrierung (Registry). Weniger bekannt sind die Aufzeichnungsfunktionen des Windows Explorers. Dieser legt Daten über sogenannte Shell Bags in der Registry ab. Beim Auslesen dieser Informationen hilft ein in C++ definiertes API. Um dieses in VB.NET nutzen zu können, sind alle API-Funktionen, Enums, Datenstrukturen, Eigenschaften, Schnittstellen sowie COM-Objektanbindungen zunächst in die Syntax von Visual Basic .NET zu überführen. Danach lässt sich die Funktionalität des API im Verlaufsdatensteuerelement nutzen. Die zu implementierende Gesamtfunktionalität sehen Sie in Bild 1.

Gesamtfunktionalität des benutzerdefinierten Verlaufsdatensteuerelements (Bild 1)
Autor
Den Recent-Ordner auswerten
Windows verwaltet die jeweils zuletzt angewählten Ordner und/oder Dateien eines bestimmten Benutzers im Recent-Ordner unter C:\Users\Benutzername\AppData\Roaming\Microsoft\Windows\Recent. Für jeden Benutzer werden folglich unterschiedliche Ergebnisse zurückgegeben.Zunächst sollen Enumerationen, Datenstrukturen und Methoden realisiert werden, die das Auslesen der besagten Informationen vereinfachen und bei der Anzeige im Verlaufsdatensteuerelement helfen. Erinnern Sie sich zunächst daran, dass alle Informationen durch eindeutige Symbole gekennzeichnet werden. So lassen sich Browser- und Systemverlaufsdaten leicht unterscheiden. Die Symbole werden über das Steuerelement ImgLst verwaltet, das in den Entwurfsbereich des Benutzersteuerelements platziert und mit den benötigten Icons gefüllt wurde (Bild 2). Innerhalb der Quelltexte werden die Icons per Index an die Eigenschaften ImageIndex und SelectedImageIndex übergeben.
Icons im Bildsammlungs-Editor (Bild 2)
Autor
Bei den angewählten Elementen wird zwischen Ordnern (Folder) und Dateien (File) unterschieden.Die Typen selbst werden über die Enumeration eLinkType zusammengefasst. Kann kein Verbindungstyp zugeordnet werden, wird diesem der Typ NoFolderFile zugewiesen.
Enum eLinkType
Folder
File
NoFolderFile
End Enum
Alle Daten zu angewählten Ordnern und/oder Dateien werden im Recent-Ordner über Verknüpfungsdateien (Link Files) verwaltet. Diese enthalten alle wichtigen Daten, um später auf den tatsächlichen Pfad oder die tatsächliche Datei zugreifen zu können.Um die Informationen zu bündeln, wird zuerst die Datenstruktur RecentLinkFileInfo definiert. Sie fasst den Namen (Name), die Verknüpfungsdatei (LinkFileName), den Ordner (Folder), das Arbeitsverzeichnis (WorkingFolder), eine Beschreibung (Description), das zugeordnete Bildsymbol (IconLocation), den aktiven Stil (WindowsStyle), das Anlagedatum (CreationDate) sowie den Verknüpfungstyp (eLinkType) zusammen.
Structure RecentLinkFileInfo
Dim Name As String
Dim LinkFileName As String
Dim Folder As String
Dim WorkingFolder As String
Dim Description As String
Dim IconLocation As String
Dim WindowStyle As String
Dim CreationDate As Date
Dim Type As eLinkType
End Structure
Mit der Funktion GetLastRecentFilesAndFolders können Sie die zehn zuletzt geöffneten Ordner und Dateien auslesen (Listing 1). Die Funktion fasst die ermittelten Einträge zusammen und liefert diese als Datenfeld des Typs RecentLinkFileInfo zurück. Funktionsintern wird das Datenfeld unter dem Namen LnkInfos eingerichtet und auf Nothing gesetzt. Die Zählvariable Counter wird auf -1 gesetzt. Danach wird der aktuelle Benutzername (ActualUser) ermittelt und damit der zugehörige Recent-Ordner (DataFolder) bestimmt. Nur wenn das Verzeichnis existiert, werden die darin enthaltenen lnk-Dateien mit Directory.GetFiles ausgelesen und über eine For-Each-Schleife verarbeitet. Für jedes verarbeitete Element wird der Zähler Counter erhöht und das Datenfeld mit einer ReDim-Preserve-Anweisung neu dimensioniert. Die Verknüpfungsinformationen werden über die Funktion GetLinkFileInfos unter Angabe der verarbeiteten Verknüpfungsdatei ermittelt und an das neue Datenelement LnkInfos(Counter) übergeben. Sind alle Einträge verarbeitet, wird das Datenfeld LnkInfos mit Array.Sort aufsteigend nach Datum sortiert. Die Sortierroutine CompareRecentLinkFileInfos wird dabei als Adresse übergeben. Mit Array.Reverse wird die Sortierung umgekehrt (absteigende Sortierung) und die Liste an das aufrufende Programm übergeben.
Listing 1: Zuletzt geöffnete Ordner und Dateien ermitteln
Function GetLastRecentFilesAndFolders() As _ RecentLinkFileInfo()
Dim LnkInfos As RecentLinkFileInfo() = Nothing
Dim Counter As Integer = -1
' bezogen auf angemeldeten Benutzer
Dim ActualUser As String = Environment.UserName
' Datenverzeichnis mit den Informationen
Dim DataFolder As String = "C:\Users\" &
ActualUser & "\AppData\Roaming\Microsoft\ Windows\Recent"
If Directory.Exists(DataFolder) Then
' Verweise sind als Verknüpfungen gespeichert
Dim RecentFiles As String() = Directory.GetFiles(
DataFolder, "*.lnk", SearchOption.AllDirectories)
For Each rFile In RecentFiles
Counter = Counter + 1
ReDim Preserve LnkInfos(Counter)
LnkInfos(Counter) = GetLinkFileInfo(rFile)
Next
End If
' Datenfeld nach Datum sortieren (aufsteigend)
Array.Sort(LnkInfos, AddressOf CompareRecentLinkFileInfos)
Array.Reverse(LnkInfos) ' Sortierung umkehren
Return LnkInfos
End Function
Das Auswerten der Informationen in einer Verknüpfungsdatei erledigt die Funktion GetLinkFileInfo. Dieser wird der Dateiname mit Pfad über den Parameter LinkFileWithPath übergeben. Das Ergebnis wird als RecentLinkFileInfo zurückgegeben. In der Funktion werden die Verknüpfungsinformationen über die neu instanzierte Variable LnkInfo (Typ RecentLinkFileInfo) verwaltet.Die Informationen werden dabei durch den Windows Scripting Host ausgelesen verarbeitet. Folglich ist ein entsprechender Verweis in der Steuerelementbibliothek ExtendedControlsLib erforderlich (Bild 3).

Den Windows Scripting Host per COM-Verweis anbinden (Bild 3)
Autor
Nach der Verweiserstellung können Sie die Funktion codieren. Zuerst instanzieren Sie ein neues Objekt des Typs IWshRuntimeLibrary.WshShell, das hier der Variablen wShell zugeordnet wird. Mit wShell.CreateShortcut legen Sie dann unter Angabe der bestehenden Verknüpfungsdatei LinkFileWithPath ein ShortCut-Objekt an (Typ IWshShortcut) und füllen dieses mit den zugeordneten Daten. Die Daten werden dann schrittweise in die Datenstruktur LnkInfo (Typ RecentLinkFileInfo) kopiert. Lediglich das Anlagedatum ist nicht direkt verfügbar. Es wird über die Datei und ein zugehöriges FileInfo-Objekt ermittelt. Beim Anlagedatum werden Datum und Zeit des letzten Zugriffs vermerkt. Ob ein Eintrag als Ordner oder Datei gekennzeichnet wird, ist abhängig davon, ob das jeweilige Element noch existiert oder nicht. Existiert es nicht mehr, wird der Verknüpfungstyp eLinkType.NoFolderFile zugeordnet (Listing 2).
Listing 2: Verknüpfungsinfos auslesen
Function GetLinkFileInfo(ByVal LinkFileWithPath _ As String) As RecentLinkFileInfo
Dim LnkInfo As New RecentLinkFileInfo
Try
Dim wShell As IWshShell = New IWshRuntimeLibrary.WshShell()
Dim wLink As IWshShortcut = DirectCast(
wShell.CreateShortcut(LinkFileWithPath), IWshShortcut)
With wLink
LnkInfo.Name = .FullName.Substring(
.FullName.LastIndexOf("\") + 1).Replace(
".lnk", "")
LnkInfo.LinkFileName = .FullName
LnkInfo.Folder = .TargetPath
LnkInfo.WorkingFolder = .WorkingDirectory
LnkInfo.Description = .Description
LnkInfo.IconLocation = .IconLocation
LnkInfo.WindowStyle = .WindowStyle
Dim fi As New FileInfo(LinkFileWithPath)
LnkInfo.CreationDate = fi.LastWriteTime
LnkInfo.Type = eLinkType.NoFolderFile
If .TargetPath <> "" Then
If Directory.Exists(.TargetPath) Then
LnkInfo.Type = eLinkType.Folder
ElseIf IO.File.Exists(.TargetPath) Then
LnkInfo.Type = eLinkType.File
Else
LnkInfo.Type = eLinkType.NoFolderFile
End If
End If
Return LnkInfo
End With
Catch ex As Exception
Return Nothing
End Try
End Function
Die Gültigkeit jedes Elements ist nachzuweisen, denn nicht jeder Link muss noch gültig sein. So könnte etwa ein externes Laufwerk, auf das der Link zeigt, getrennt worden sein.Für den Datumsvergleich wird die Methode CompareRecentLinkFileInfos (Strukturvergleichsdelegat für Datenstrukturen mit Datumselement) definiert, der zwei Informationen zu den Verknüpfungsinfos des Typs RecentLinkFileInfo übergeben werden. Diese Methode vergleicht die Anlagedaten CreationDate via CompareTo und liefert das Ergebnis als Zahlenwert zurück.
Private Function CompareRecentLinkFileInfos(
ByVal LnkFileInfo1 As RecentLinkFileInfo,
ByVal LnkFileInfo2 As RecentLinkFileInfo) As Integer
Return LnkFileInfo1.CreationDate.CompareTo(
LnkFileInfo2.CreationDate)
End Function
Infos im Recent-Ordner anzeigen
Nachdem die Informationen des Recent-Ordners ausgelesen sind, können Sie diese zeitbasiert in das Verlaufsdatensteuerelement übernehmen. Dafür nutzen Sie die Methode ShowFilesAndFolders, der Sie den Hauptknoten DateTimeNode für die Datumsausgaben als Parameter übergeben. Die Methode ermittelt zuerst die zuletzt geöffneten Ordner und Dateien über die Funktion GetLastRecentFilesAndFolders und übernimmt die Daten in das Datenfeld rlfi. Die Elemente des Typs RecentLinkFileInfo werden dann per For-Each-Schleife durchlaufen und bezogen auf das letzte Schreibdatum datumsbasiert eingeordnet. Die Datumsknoten werden im kurzen Datumsformat verwendet und bei Nichtexistenz mit CreateNewDateNode zuerst angelegt. Mit der Methode DateNodeExist prüfen Sie, ob ein Datumsknoten existiert. Wenn ja, wird er mit GetDateNode ermittelt. Der Zielknoten wird über die Variable DestinationNode vom Typ HistoryTreeNode verwaltet und um neue, mit einem typspezifischen Symbol gekennzeichnete Eintragsknoten (NewEntryNode) erweitert (Listing 3). Eine exemplarische Inhaltsausgabe zeigt Bild 4.Listing 3: Zuletzt geöffnete Ordner und Dateien ausgeben (Teil 1)
Sub ShowFilesAndFolders( ByVal DateTimeNode As HistoryTreeNode)
Dim rlfi As RecentLinkFileInfo() =
GetLastRecentFilesAndFolders()
For Each Entry As RecentLinkFileInfo In rlfi
With Entry
Dim EntryDate As String =
.CreationDate.ToShortDateString
If .Folder <> "" Then
' Zielknoten wählen/anlegen
Dim DestinationDateNode As HistoryTreeNode = Nothing
If DateNodeExist( DateTimeNode, EntryDate) = False Then
DestinationDateNode =
CreateNewDateNode(DateTimeNode, EntryDate)
Else
DestinationDateNode =
GetDateNode(DateTimeNode, EntryDate)
End If
Dim NewEntryNode As New HistoryTreeNode
If .Type = eLinkType.Folder Then ' Ordner
With NewEntryNode
.Text = Entry.Name
.ImageIndex = 3
.SelectedImageIndex = 3
Dim di As New DirectoryInfo(Entry.Folder)
.Description = "Gültiger Verlaufseintrag [Ordner]"
.NodeDate = di.LastAccessTime.ToShortDateString
.NodeTime = di.LastAccessTime.ToShortTimeString
.NodeType = "Verzeichnis"
.UrlOrFileOrFolder = Entry.Folder
End With
ElseIf .Type = eLinkType.File Then ' Datei
With NewEntryNode
.Text = Entry.Name
.ImageIndex = 4
.SelectedImageIndex = 4
.Tag = Entry.Folder
Dim fi As New FileInfo(Entry.Folder)
.Description = "Gültiger Verlaufseintrag [Datei]"
.NodeDate = fi.LastAccessTime.ToShortDateString
.NodeTime = fi.LastAccessTime.ToShortTimeString
.NodeType = "Datei"
.UrlOrFileOrFolder = Entry.Folder
End With
ElseIf .Type = eLinkType.NoFolderFile Then
With NewEntryNode
.Text = Entry.Name
.ImageIndex = 8
.SelectedImageIndex = 8
.Tag = Entry.Folder
Dim fDateTime As Date = Nothing
Dim fType As eNameType = TypeOfName(Entry.Folder)
Dim fTypeName As String = Nothing
If fType = eNameType.Folder Then
Dim di As New DirectoryInfo( Entry.Folder)
fDateTime = di.LastAccessTime
fTypeName = "Verzeichnis"
ElseIf fType = eNameType.File Then
Dim fi As New FileInfo(Entry.Folder)
fDateTime = fi.LastAccessTime
fTypeName = "Datei"
Else
fTypeName = "unbekannt"
fDateTime = DateTime.Now
End If
.Description = "Derzeit ungültiger Verlaufseintrag [Ordner/Datei]"
.NodeDate = fDateTime.ToShortDateString
.NodeTime = fDateTime.ToShortTimeString
.NodeType = fTypeName
.UrlOrFileOrFolder = Entry.Folder
End With
End If
If ShowNoAccessableFilesAndFolders And
.Type = eLinkType.NoFolderFile Or .Type = eLinkType.Folder Or
.Type = eLinkType.File Then
If NewHistoryNodeAlreadyExists(
DestinationDateNode, NewEntryNode) = False Then
DestinationDateNode.Nodes.Add( NewEntryNode)
End If
End If
End If
End With
Next
End Sub

Die zuletzt geöffneten Ordner im Verlaufssteuerelement (Bild 4)
Autor
Im Rahmen der Anzeige wird die Existenz der anzuzeigenden Elemente erneut geprüft. Die Typen werden durch die Enumeration eNameType vordefiniert.
Enum eNameType
Folder
File
None
End Enum
Die Funktion TypeOfName ermittelt anhand des übergebenen Suchpfads FileOrFolderName, ob es sich um einen Ordner, eine Datei oder ein nicht existierendes Element handelt.
Function TypeOfName(
ByVal FileOrFolderName As String) As eNameType
Dim Result As eNameType = eNameType.None
If IO.File.Exists(FileOrFolderName) Then
Result = eNameType.File
ElseIf Directory.Exists(FileOrFolderName) Then
Result = eNameType.Folder
End If
Return Result
End Function
Registry-Verlaufsdaten ermitteln
Neben den Verlaufsdaten im Recent-Ordner des aktuellen Benutzers finden sich weitere Verlaufsdaten in der Registry. Diese sind binär verschlüsselt und deshalb nur mit viel Aufwand auszulesen und anzuzeigen. Wie die binäre Entschlüsselung und Dateninterpretation erfolgt, soll an dieser Stelle detailliert beschrieben werden.Im ersten Schritt wird dafür eine Datenstruktur definiert, welche die übergeordneten Informationen zu den Binäreinträgen verwaltet. Diese Einträge sind mit Verknüpfungsdateien von Windows verbunden, die als Strukturelement LinkFileInfo des Typs RecentLinkFileInfo mitverwaltet werden. Ansonsten werden ein Zähler für die interne binäre Datenverwaltung (Counter), ein Indexwert (IndexValue), ein Indexname (IndexName), ein Dokumentname (DocName), eine Verknüpfungsdatei (LinkFileName), eine binäre Zeichenkette (BinaryString) sowie die letzte Zugriffszeit (LastAccessTime) in die Datenstruktur aufgenommen.
Public Structure RecentDocFolderInfos
Dim Counter As Integer
Dim IndexValue As UInt32
Dim IndexName As String
Dim DocName As String
Dim LinkFileName As String
Dim BinaryString As String
Dim LastAccessTime As Date
Dim LinkFileInfo As RecentLinkFileInfo
End Structure
Nun lokalisieren Sie die Daten in der Registry. Sie werden benutzerspezifisch im Zweig Computer\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer
\RecentDocs verwaltet. Die vorhandenen Einträge sind zwar nicht geordnet, aber fortlaufend nummeriert. Die Daten sind binär verschlüsselt. Im Registrierschlüssel MRUListEx werden – ebenfalls binär verschlüsselt – die zuletzt angewählten Einträge zusammengestellt, und zwar in der Reihenfolge des letzten Zugriffs, vergleiche Bild 5.
\RecentDocs verwaltet. Die vorhandenen Einträge sind zwar nicht geordnet, aber fortlaufend nummeriert. Die Daten sind binär verschlüsselt. Im Registrierschlüssel MRUListEx werden – ebenfalls binär verschlüsselt – die zuletzt angewählten Einträge zusammengestellt, und zwar in der Reihenfolge des letzten Zugriffs, vergleiche Bild 5.

Binär verschlüsselte Informationen zu den zuletzt geöffneten Ordnern und Dateien (Bild 5)
Autor
Mit diesem Grundwissen können Sie sich nun der eigentlichen Analyse zuwenden.Jetzt werden die MRU-Dokumente analysiert (MRU = Most Recently Used). Als String-Parameter CurrentUserSubKey wird der Registrierzweig an die Funktion übergeben. Funktionsergebnis ist ein Ganzzahldatenfeld, hinter dem sich die Schlüsselnamen der Einträge verbergen (Listing 4).
Listing 4: Binäre MRU-Listen auswerten
Function GetMRUListEx(
ByVal CurrentUserSubKey As String) As Integer()
Dim Counter As Integer = -1
Dim Elements As Integer() = Nothing
Dim RecentDocs_UserKey As RegistryKey =
CurrentUser.OpenSubKey(CurrentUserSubKey, True)
If RecentDocs_UserKey IsNot Nothing Then
Dim EntryList As Byte() =
RecentDocs_UserKey.GetValue( "MRUListEx", Nothing)
If EntryList IsNot Nothing Then
Dim Entries As Integer = EntryList.Length / 4
Dim StartIndex As Integer = 0
For x = 0 To Entries - 1
Counter = Counter + 1
Dim FourBytes(0 To 3) As Byte
Array.Copy(EntryList, StartIndex, FourBytes, 0, 4)
Dim Index As UInt32 =
BitConverter.ToUInt32(FourBytes, 0)
If Index < 4294967295 Then
ReDim Preserve Elements(Counter)
Elements(Counter) = Index
End If
StartIndex = StartIndex + 4
Next
End If
End If
Return Elements
End Function
Nachdem der Registrierschlüssel CurrentUserSubKey mit CurrentUser.OpenSubKey geöffnet wurde, wird der binäre Wert zum Schlüsselnamen MRUListEx in das Byte-Datenfeld EntryList übernommen, um dann die detaillierte Analyse des Datenfelds zu starten. Über die MRU-Liste werden die Nummern für die Reihenfolge bestimmt, wobei jede einzelne Nummer in vier Bytes verschlüsselt ist (DWord). Die Anzahl der Einträge ist die Byte-Anzahl geteilt durch den Wert 4. Die letzten vier Byte (FFFF) markieren das Ende der Auflistung. Anschließend werden alle Einträge per For-Schleife durchlaufen. Die aktuell für einen Eintrag benötigten Bytes werden mit Array.Copy aus dem gesamten Byte-Datenfeld in das Byte-Datenfeld FourBytes kopiert.Die abgespaltenen Bytes werden dann in eine Ganzzahl des Typs UInt32 umgewandelt und in die Variable Index übernommen. Wird dieser Wert später wieder in eine Zeichenkette konvertiert, entspricht er dem Schlüsselnamen des Eintrags. Jedes neue Element wird dem Datenfeld Elements per Anweisung Redim Preserve ohne Wertverlust angefügt, Bei jedem Wert wird geprüft, ob die Ende-Kennung (FFFF = 4294967295) bereits erreicht wurde. In diesem Fall wird die Verarbeitung abgebrochen. Das Datenfeld mit allen Indexwerten wird letztlich an das aufrufende Programm zurückgegeben.Nicht alle Einträge, die sich im Registrierschlüssel befinden, werden auch in der MRUListEx-Zusammenstellung aufgeführt. Aus diesem Grund wird die Funktion GetNonMRUListExElements definiert, welche die nicht berücksichtigten Einträge offenlegt. Wie das umgesetzt wird, erfahren Sie in der kommenden Ausgabe der dotnetpro. Dann wird auch gezeigt, wie Sie die in der Registry vermerkten Daten zu den zuletzt geöffneten Programmen auslesen und anzeigen.
Fussnoten
- Andreas Maslo, User History Management, dotnetpro 10/2023, Seite 134 ff., http://www.dotnetpro.de/A2310BasicInstinct