Eigener OpenWith Dialog

Please note: This article is only available in German.
Den Öffnen mit Dialog von Windows aufzurufen stellt kein Problem dar. Da man diesen aber nicht modifizieren kann, kann es notwendig sein einen solchen Dialog selbst zu schreiben.

In meinem aktuellen Projekt gebe ich dem Benutzer die Möglichkeit sich zwischen dem von Windows ausgewählten Standardprogramm, dem letzten für diese Datei ausgewählten Programm, einer Liste von Möglichkeiten und einer ganz eigenen Wahl zum Öffnen der Datei zu entschieden. Für die Liste wollte ich die Programme verwenden, die Windows normalerweise in dem OpenWith (Öffnen mit...) Dialog einbaut.

Im ersten Teil dieses Artikels werde ich darauf eingehen, wie man den OpenWith Dialog in seiner Anwendung aufrufen kann und wieso dies für meine Applikation keine Alternative darstellt. Dann werde ich auf die Schwierigkeiten bzw. Besonderheiten bei der eigenen Implementation einer solchen Liste eingehen.

1 | Den OpenWith Dialog aufrufen

Um den OpenWith Dialog aus einem C# Programm aufzurufen muss man nur die entsprechende Windows-API callen (in diesem Fall in der "Shell32.dll" beinhaltet). Man kann jeden Dateityp über das Verb "open" öffnen, falls eine Anwendung hinterlegt ist. Ansonsten wird direkt der OpenWith Dialog aufgerufen. Will man den OpenWith Dialog im Falle einer hinterlegten Anwendung erzwingen, so verwendet man statt dessen das Verb "openas". Das nächste Codeschnippsel verdeutlicht dies.

[Serializable]
public struct ShellExecuteInfo
{
    public int Size;
    public uint Mask;
    public IntPtr hwnd;
    public string Verb;
    public string File;
    public string Parameters;
    public string Directory;
    public uint Show;
    public IntPtr InstApp;
    public IntPtr IDList;
    public string Class;
    public IntPtr hkeyClass;
    public uint HotKey;
    public IntPtr Icon;
    public IntPtr Monitor;
}
 
[DllImport("shell32.dll", SetLastError = true)]
extern public static bool ShellExecuteEx(ref ShellExecuteInfo lpExecInfo);
 
public const uint SW_NORMAL = 1;
 
public static bool OpenAs(string file)
{
    ShellExecuteInfo sei = new ShellExecuteInfo();
    sei.Size = Marshal.SizeOf(sei);
    sei.Verb = "openas";
    sei.File = file;
    sei.Show = SW_NORMAL;
    return ShellExecuteEx(ref sei));
}

Dieser Aufbau ist identisch mit denen der meisten Windows-APIs. Wir erstellen zunächst die Strukturen und Datentypen (z.B. Flags/Enumerationen), welche von der API gewünscht werden und sprechen danach die *.dll über den DllImport direkt an. Anschließend basteln wir uns eine Funktion, die einen vorgegebenen Satz von Daten mit einem variablen Satz an die API schickt und das Ergebnis auswertet.

Als Ergebnis sehen wir den OpenWith Dialog von Windows. Wir haben allerdings keine Kontrolle über die Ansicht (könnte ich verkraften), noch Kontrolle über den ausgewählten Prozess (kann ich nicht mehr verkraften). Selbst den Namen des ausgewählten Prozesses zu erhalten ist mehr oder weniger problematisch.

2 | Workaround für diese Lösung

Über diese Lösung erhalten wir keinen direkten Zugriff auf die durch den Dialog gestarteten Anwendung. Dies hat zwei Gründe:

  • Der Dialog läuft in einem seperaten Prozess (rundll32.exe), weshalb wir keine Möglichkeit haben auf die internen Vorgänge im Dialog zuzugreifen.
  • Einige Anwendungen unterstützen die Rückgabe des Prozesshandles nicht. Diese Anwendungen würden den OpenWith Dialog zum Abstürzen bringen oder instabil machen, weshalb das Prozesshandle generell nicht abgefragt wird.

Wir können jedoch einen Workaround basteln. Dieser baut auf der "handle.exe" auf. Die neue Funktion OpenAs(string file) sieht nun folgendermaßen aus:

public static string OpenAs(string file)
{
    ShellExecuteInfo sei = new ShellExecuteInfo();
    sei.Size = Marshal.SizeOf(sei);
    sei.Verb = "openas";
    sei.File = file;
    sei.Show = SW_NORMAL;
    if( ShellExecuteEx(ref sei)) )
    {
        Process.Start("cmd.exe", @"/C D:handle.exe C:my.doc > D:log.txt")
        return File.ReadAllText(@"D:log.txt");
    }
    return string.Empty;
}

Der gezeigte Code ist nur als Beispiel anzusehen. Meiner Meinung nach ist dieser Workaround viel zu umständlich. Er beinhaltet

  • Download / Verwenden der handle.exe
  • Aufrufen dieser Über die Kommandozeile
  • Verwenden einer Pipe zur Umleitung der Ausgabe in ein Textfile
  • Auslesen der Textdatei

Die Ausgabe hat folgende Form (angenommen es wurde WordPad ausgewählt um C:\my.doc zu öffnen):

WORDPAD.EXE pid: 5216 AF0: C:my.doc

Alternativ kann man den Prozess ganz normal starten (über Process) und die Ausgabe direkt in einen String umleiten. Dies wäre sicherlich weniger Umständlich, beinhaltet aber immer noch das Starten einer externen Applikation und das Auslesen von Daten dieser Applikation.

3 | Der eigene OpenWith Dialog

Fangen wir also an unseren eigenen Dialog zu basteln. Das Design und die Steuerelemente können wir selbst bestimmen. Für die Liste wählt man am Besten ein TreeView Control, wobei das natürlich Geschmackssache ist. Als nächstes müssen wir dieses Control mit Daten füllen. Dies bringt uns zur ersten Frage: Woher kommen diese Daten?

Ab Windows Vista gibt es eine neue Möglichkeit diese Daten zu erhalten: Die API SHAssocEnumHandlers. Nachdem ich mit meinen Projekten immer das Ziel einer maximalen Plattformunabhängigkeit verfolge und ich Windows XP als Minimalanforderung bei der Wahl der Windows Version ansehe, kam diese API für mich leider nicht in Frage. Mehr Informationen zur Verwendung dieserj API gibt es unter http://msdn.microsoft.com/en-us/library/bb762109(VS.85).aspx.

Daher stellt sich die Frage woher wir diese Daten noch erhalten können? Fakt ist, dass wir uns die Daten selbst zusammenstellen müssen. Wir finden die Daten in der Registrierung, wie unter http://windowsxp.mvps.org/OpenWith.htm beschrieben. Wir können dabei sehr frei vorgehen, d.h. wir können neben der möglichen OpenWithListe des (speziellen) Dateityps noch weitere OpenWithListen einbinden (z.B. von Textdateien etc.).

RegistryKey reg = Registry.CurrentUser;
string[] keys = new string[] { "Software", "Microsoft", "Windows", "CurrentVersion", "Explorer", "FileExts", Path.GetExtension("beispiel.doc"), "OpenWithList" };
for(int i = 0; i < keys.Length; i++)
{
    string lowerkey = keys[i].ToLower();
    string[] subkeys = reg.GetSubKeyNames();
    bool containskey = false;
    foreach(string subkey in subkeys)
        if(containskey = subkey.ToLower().Equals(lowerkey))
            break;
    if(!containskey)
        return;
    reg = reg.OpenSubKey(lowerkey);
}
string[] values = reg.GetValueNames();
foreach (string value in values)
{
    if (value.Length == 1 && (int)value[0] >= (int)'a' && (int)value[0] <= (int)'z')
        AddProgram(reg.GetValue(value));
}

Der oben gezeigte Code fügt unserem TreeView Einträge hinzu. In diesem Fall werden es Einträge zum Typ *.doc sein. Das eigentliche Hinzufügen von Einträgen wird von der Prozedur AddProgram(string program) erledigt. Der gezeigte Code ist vor Abstürzen gesichert, da er vor dem Öffnen jedes Unterschlüssels eine Abfrage tätigt, ob dieser auch wirklich existiert. Außerdem nimmt er nicht jeden Wert als Programmnamen an, sondern nur die, welche als Schlüsselnamen a,b,c,...,z haben.

Diese Routine alleine nützt uns noch gar nichts. Wir benötigen noch mehr Routinen um neben den Programmanamen (welcher z.B. WordPad.exe wäre) noch den genauen Pfad herauszufinden. Folgende zwei Routinen bauen wir noch in unser Formular ein:

private void AddProgram(object p)
{
    string fullpath = GetFullPath(p.ToString());
    if(fullpath == null)
        fullpath = ShellIcon.GetAssociationPath(p.ToString());
    iL.Images.Add(ShellIcon.GetSmallIcon(fullpath).ToBitmap());
    TreeNode node = new TreeNode(p.ToString(),
                                iL.Images.Count-1, iL.Images.Count-1);
    node.ToolTipText = fullpath;
    treeList.Nodes.Add(node);
}
 
public string GetFullPath(string fileName)
{
    if (File.Exists(fileName))
        return Path.GetFullPath(fileName);
    string values = Environment.GetEnvironmentVariable("PATH");
    string[] paths = values.Split(';');
    foreach (string path in paths)
    {
        string fullPath = Path.Combine(path, fileName);
        if (File.Exists(fullPath))
            return fullPath;
    }
    return null;
}

Die GetFullPath(string filename) Methode hilft uns dabei den vollständigen Pfad für eine Datei herauszufinden. Dazu fragen wir zuerst die Windows-API ob die Datei bereits bekannt ist. Falls nicht könnte es eine Datei sein, deren Pfad in der Windows Umgebungsvariable PATH hinterlegt ist.

Betrachten wir mal die für diese zwei Methoden notwendigen API Aufrufe. Zuerst die notwendigen Enumertionen, Flags und Strukturen:

[StructLayout(LayoutKind.Sequential)]
public struct SHFILEINFO
{
  public IntPtr hIcon;
  public IntPtr iIcon;
  public uint dwAttributes;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
  public string szDisplayName;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
  public string szTypeName;
};
 
[Flags]
enum SHGFI : int
{
  Icon = 0x000000100,
  DisplayName = 0x000000200,
  TypeName = 0x000000400,
  Attributes = 0x000000800,
  IconLocatin = 0x000001000,
  ExeType = 0x000002000,
  SysIconIndex = 0x000004000,
  LinkOverlay = 0x000008000,
  Selected = 0x000010000,
  Attr_Specified = 0x000020000,
  LargeIcon = 0x000000000,
  SmallIcon = 0x000000001,
  OpenIcon = 0x000000002,
  ShellIconize = 0x000000004,
  PIDL = 0x000000008,
  UseFileAttributes = 0x000000010,
  AddOverlays = 0x000000020,
  OverlayIndex = 0x000000040,
}
 
[Flags]
enum AssocF
{
  Init_NoRemapCLSID = 0x1,
  Init_ByExeName = 0x2,
  Open_ByExeName = 0x2,
  Init_DefaultToStar = 0x4,
  Init_DefaultToFolder = 0x8,
  NoUserSettings = 0x10,
  NoTruncate = 0x20,
  Verify = 0x40,
  RemapRunDll = 0x80,
  NoFixUps = 0x100,
  IgnoreBaseClass = 0x200
}
 
enum AssocStr
{
  Command = 1,
  Executable,
  FriendlyDocName,
  FriendlyAppName,
  NoOpen,
  ShellNewValue,
  DDECommand,
  DDEIfExec,
  DDEApplication,
  DDETopic
}

Und nun die zugehörigen DllImport Aufrufe und in C# implementierten Methoden:

[DllImport("shell32.dll")]
public static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi,
  uint cbSizeFileInfo, uint uFlags);
 
[DllImport("User32.dll")]
public static extern int DestroyIcon(IntPtr hIcon);
 
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern uint AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra,
   [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
 
public static Icon GetSmallIcon(string fileName)
{
  SHGFI flag = SHGFI.SmallIcon;
  if (!System.IO.File.Exists(fileName))
    flag |= SHGFI.UseFileAttributes;
  return GetIcon(fileName, flag);
}
 
private static Icon GetIcon(string fileName, SHGFI flags)
{
  SHFILEINFO shinfo = new SHFILEINFO();
  IntPtr hImgSmall = SHGetFileInfo(fileName, 0, ref shinfo, (uint)Marshal.SizeOf(shinfo),
                (uint)(SHGFI.Icon | flags));
  Icon icon = (Icon)System.Drawing.Icon.FromHandle(shinfo.hIcon).Clone();
  DestroyIcon(shinfo.hIcon);
  return icon;
}
 
public static string GetAssociationPath(string appname)
{
  uint pcchOut = 0;  // size of output buffer
  AssocQueryString(AssocF.Open_ByExeName, AssocStr.Executable, appname, null, null,
            ref pcchOut);
  StringBuilder pszOut = new StringBuilder((int)pcchOut);
  AssocQueryString(AssocF.Open_ByExeName, AssocStr.Executable, appname, null, pszOut,
            ref pcchOut);
  return pszOut.ToString();
}

Damit kann man einen eigenen OpenWith Dialog sehr schnell erstellen. Nun verwendet man einfach die ToolTipText Eigenschaft des selektierten Knotens im TreeView (oder wo man auch immer den Pfad des ausgewählten Programms abgespeichert hat) und öffnet den Process mit der Datei als Argument.

Created . Last updated .

References

Sharing is caring!