head.WriteLine()

Montag, Januar 07, 2008

Windows Forms Support für System.AddIn, Teil 2

Nachdem ich im ersten Teil den grundlegenden Aufbau der WindowsFormsAddInProxy.dll beschrieben habe, soll es nun um die Details der Implementierung gehen. Zum besseren Verständnis der beteiligten Komponenten, hier zunächst einmal der Aufbau der Assembly:

WindowsFormsAddInProxy

Wie ich im ersten Teil bereits erwähnte, ist das Hauptproblem bei der Übertragung von UI-Elementen über AppDomain-Grenzen, dass Windows Forms-Klassen weder serialisierbar sind, noch per Reference übertragen werden können. Daher wird lediglich das Handle der Add-In-Oberfläche übertragen und auf Host-Seite „umgehängt“. Hierfür stellt die Win32-API die SetParent()-Methode zu Verfügung. Sie nimmt das jeweilige Handle, sowie das Handle des gewünschten Elternfensters entgegen. Die Add-In-Oberfläche wird somit zum Kindelement des entsprechenden Host-Controls.

Aus Sicherheitsgründen ist das Umhängen von Handles jedoch nur innerhalb eines Win32-Prozesses möglich. Somit kann diese Technik nicht eingesetzt werden, wenn die Add-Ins in einem separaten Prozess gehosted werden. Für Client-seitige Add-Ins ist die AppDomain-Isolation jedoch vollkommen ausreichend.

Für das Übertragen des Handle ist das IWindowProxy-Interface zuständig, welches einzig die Handle-Eigenschaft definiert. IWindowProxy kann nun im jeweiligen Add-In-Contract verwendet werden, um Oberflächen-Inhalte über die AppDomain-Grenze zu transferieren. Im Beispielprojekt wird IWindowProxy in der GetSurface()-Methode eingesetzt, mit der der Host die Oberfläche des Add-In ermittelt.

Die eigentliche Kommunikation findet jedoch über die Klassen AddInSurface (Add-In-Seite) und WindowProxyPanel (Host-Seite) statt. Letztere bekommt über die SetWindow()-Methode eine WindowProxyBase-Instanz übergeben und „kapert“ daraufhin das zugehörige Fenster.

public void SetWindow(WindowProxyBase window)
{
    if (window != null && window.Handle != IntPtr.Zero)
    {
        SetParent(window.Handle, this.Handle);
    }
}

Weitere Herausforderungen

Somit ist das Hauptproblem schon mal gelöst. Doch nach einem kurzen Test treten weitere Herausforderungen auf. Da das Umhängen des Handles per Win32-Funktion erfolgte, hat die Windows Forms-Infrastruktur keine Information über die Existenz des Controls. Daher werden viele Nachrichten nicht ordnungsgemäß an dieses weiter geleitet. Dies führt zu Problemen in den folgenden Bereichen:

  1. Automatisches Resizing
  2. Vererbung von Font-Einstellungen
  3. Fokussteuerung
  4. Verarbeitung von Accelerator Keys

Zur Lösung dieser Probleme müssen die Nachrichten manuell von WindowProxyPanel an AddInSurface weitergeleitet werden. Die folgende Abbildung zeigt die Kommunikation zwischen den beiden Controls:

HandleCapturing

1. Automatisches Resizing

Das Resizing-Problem ist relativ einfach gelöst. Hierfür muss in WindowProxyPanel lediglich die OnResize()-Methode überschrieben und in ihr die aktuelle Größe über die Win32-Funktion MoveWindow() aktualisiert werden.

protected override void OnResize(EventArgs e)
{
    base.OnResize(e);
    if (m_window != null &&
        m_window.Handle != IntPtr.Zero)
    {
        MoveWindow(m_window.Handle, 0, 0,
            this.Width, this.Height, true);
    }
}

2. Vererbung von Font-Einstellungen

In Windows Forms werden Font- Einstellungen automatisch an die entsprechenden Kindelemente vererbt, wenn diese nicht explizit einen Font festlegen. Diese Vererbungslogik muss ebenfalls manuell nachgebaut werden, da die Windows Forms-Infrastruktur ja nichts von unserem Kindfenster weis.

Dies ist jedoch mit einem einfachen SendMessage()-Aufruf erledigt:

SendMessage(m_window.Handle, WM_SETFONT, this.Font.ToHfont().ToInt32(), 0);

Der Aufruf findet sowohl in der SetWindow()-Methode, als auch in der überschriebenen OnFontChanged()-Methode statt, um auch Font-Änderungen zur Laufzeit entsprechend durchzureichen. In der überschriebenen WndProc()-Methode von AddInSurface wird die WM_SETFONT-Message daraufhin abgefangen und der Font entsprechend gesetzt.

3. Fokussteuerung

Eine richtig dicke Nuss hatte ich mit der Fokussteuerung zu knacken. Zwar kann in der entsprechenden AddInSurface-Ableitung die TabIndex-Eigenschaft der enthaltenen Controls gesetzt werden, die Kontrolle hat hierbei jedoch das hostende Fenster. Und da dieses das Control nicht kennt (da es nicht in seiner Controls-Hierarchie auftaucht) werden die entsprechenden Elemente auch nicht bei Fokusänderungen berücksichtigt.

Zur Lösung ist die WndProc()-Methode sowohl in WindowProxyPanel als auch in AddInSurface überschrieben. Über sie kann direkt in die Win32 Message Loop des Fensters eingegriffen werden. Zum Verarbeitung der Tab-Taste wird die Message WM_GETDLGCODE in WindowProxyPanel abgefangen und manuell an AddInSurface weitergeleitet.

protected override void WndProc(ref Message m)
{
    const int WM_GETDLGCODE = 0x87;

    if (m_window != null &&
        m_window.Handle != IntPtr.Zero)
    {
        if (m.Msg == WM_GETDLGCODE)
        {
            SendMessage(
               m_window.Handle, WM_GETDLGCODE,
               m.WParam.ToInt32(),m.LParam.ToInt32());
            return;
        }
    }
    base.WndProc(ref m);
}

Auf Add-In-Seite wird diese Message wiederum in AddInSurface gefangen und an eine Methode zur Fokussteuerung übergeben:

protected override void WndProc(ref Message m)
{
    const int WM_GETDLGCODE = 0x87;
    const int VK_TAB = 0x09;

    if (m.Msg == WM_GETDLGCODE)
    {
        if (GetAsyncKeyState(VK_TAB) != 0)
        {
            this.TabInto(
               Control.ModifierKeys != Keys.Shift);
        }
    }

    base.WndProc(ref m);
}

Über die Win32-Funktion GetAsyncKeyState() wird hierbei geprüft, ob die Tab-Taste gedrückt wurde. Ist dies der Fall, wird die selbst implementierte TabInto()-Methode aufgerufen, welche für die eigentliche Fokussteuerung zuständig ist. Hierbei wird über Control.MofifierKeys abgefragt, ob zusätzlich die Shift-Taste gedrückt wurde, um eine entsprechende Rückwärtsbewegung des Fokus zu veranlassen.

4. Verarbeitung von Accelerator Key

Accelerator Keys werden ja bekanntlich aktiviert, indem die Alt-Taste zusammen mit einem bestimmten Zeichen gedrückt wird. Sie werden beispielsweise in Labels deklariert um dem Benutzer ein schnelles Springen in das zugehörige Control zu ermöglichen. Auch diese Steuerung ist auf Ebene der Form-Klasse implementiert, sodass hier ebenfalls eine manuelle Implementierung nötig ist. Diese ist in AddInSurface zu finden und ist der Fokussteuerung sehr ähnlich. Doch bevor auf das Drücken von Accelerator Keys reagiert werden kann, müssen diese erst einmal ermittelt werden. Hierfür wurde die OnHandleCreated()-Methode überschrieben, die bei der initialen Erstellung des Control aufgerufen wird.

protected override void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);
    this.InitializeAcceleratorKeys(this);
}

Hier wird die InitializeAcceleratorKeys()-Methode aufgerufen, in der die enthaltenen Controls Collections rekursiv durchlaufen und nach Accelerator Keys durchsucht werden. Die gefundenen Keys werden daraufhin in einem Dictionary gespeichert, welches später ausgewertet werden kann.

Das Abfangen von gedrückten Accelerator Keys erfolgt wie gesagt sehr ähnlich wie bei der Focus-Steuerung. WindowProxyPanel sendet die WM_GETDLGCODE-Message, welche von AddInSurface abgefangen und verarbeitet wird. Der entsprechende Code sieht hierbei wie folgt aus:

protected override void WndProc(ref Message m)
{
    const int WM_GETDLGCODE = 0x87;

    if (m.Msg == WM_GETDLGCODE)
    {
        if (Control.ModifierKeys == Keys.Alt)
        {
            this.ProcessAcceleratorKeys();
        }
    }

    base.WndProc(ref m);
}

In der ProcessAcceleratorKeys()-Methode wird daraufhin das Dictionary der Accelerator Keys durchlaufen und der Fokus entsprechend gesetzt.

Den Source Code von WindowsFormsAddInProxy können Sie inkl. Demoprojekt hier herunter laden. Für Feedback bin ich stets dankbar.

PS: Wenn Sie mehr über System.AddIn erfahren möchten, schauen Sie auf der diesjährigen BASTA! Spring Edition vorbei. Dort führen Dominick Baier und ich in der Session „.NET-Anwendungen sicher und robust durch Add-ins erweitern“ in das Thema ein.

2 Comments:

Kommentar veröffentlichen

<< Home