# Wednesday, November 16, 2005

Ich habe bereits in Ersatzteilkatalog - Bildnavigation mit GDI+ erzählt, dass ich versuche in Visual Foxpro 9 mit GDI+ direkt auf VFP Formen zu zeichnen. Inzwischen funktioniert das eigentlich ganz gut. Einige Probleme waren noch zu lösen und den Bildaufbau habe ich noch optimiert.

In Craig Boyd's Artikelserie in FoxTalk 2.0 wird eine komplette Form mit GDI+ bearbeitet. In unserer SPCat Applikation ist aber nur ein Bereich der Form für die Darstellung der Bilder genutzt. Der restliche Bereich der Form soll weiterhin von Foxpro selbst gezeichnet werden. Deswegen wollte ich die ganze Bilddarstellung in eine Container Klasse verpacken und die eigentliche Form so wenig wie möglich verändern.

Die eigentlichen GDI+ Zeichenroutinen stelle ich nicht vor. Es geht mir jetzt mehr darum wie ich diese Routinen mit den Mechanismen von VFP verheiratet habe, damit Foxpro und GDI+ Elemente auf einer Form dargestellt werden. Foxpro weiß ja nichts davon, dass man ihm mit eigenen Routinen in die Bildschirmdarstellung pfuscht. Deswegen wird es bei jedem Update des Bildschirminhalts einfach seine eigenen Elemente zeichnen. Mit BINDEVENT hat man die Möglichkeit in die Nachrichten von Windows an das Foxpro Fenster zum Neuzeichnen des Inhalts einzugreifen. Hiermit werden die relevanten Events auf die eigene Prozedur WinEventHandler umgeleitet:

#DEFINE WM_ERASEBKGND 0x0014
#DEFINE WM_PAINT 0x000F

=BINDEVENT(THISFORM.hwnd, WM_ERASEBKGND, THIS, 'WinEventHandler')
=BINDEVENT(THISFORM.hwnd, WM_PAINT, THIS, "WinEventHandler")

Wir brauchen auch noch zwei Aufrufe in die Windows API:

DECLARE LONG CallWindowProc IN User32 ; INTEGER lpPrevWndFunc, LONG HWND, ; INTEGER Msg ,INTEGER wParam, ; INTEGER lParam
DECLARE INTEGER GetUpdateRect IN User32 LONG HWND, STRING @lpRect, INTEGER bErase

CallWindowProc ruft die Routine auf, die normalerweise den WM_PAINT Event behandelt. Also in diesem Fall die interne Foxpro Funktion, die dessen Elemente zeichnet.
GetUpdateRect liefert das Rechteck innerhalb dessen etwas zu zeichnen ist. Damit kann man den Einsatz der eigenen Zeichenroutinen auf die benötigten Fälle reduzieren. Dieses Rechteck ist als folgendes Struct definiert:

typedef struct _RECT { 
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT, *PRECT;

In der WinEventHandler Prozedur werden die Windows Nachrichten bearbeitet:

PROCEDURE WinEventHandler
LPARAMETERS HWND, msg, wParam, LPARAM

#DEFINE WM_ERASEBKGND 0x0014
#DEFINE WM_PAINT 0x000F

DO CASE
    CASE msg = WM_PAINT
        WITH THIS
            IF .autodraw
                LOCAL lpRect,lnUpdate
                lpRect=SPACE(16)
                * Das Rechteck holen, in dem gezeichnet werden muss.
                lnUpdate=GetUpdateRect(HWND, @lpRect, 0)
                * Dieser Aufruf führt dazu, dass VFP alles zeichnt, wofür es zuständig ist.
                CallWindowProc(.WinProcAddress, HWND, msg, wParam, lParam)
                IF lnUpdate=1
                    * In hitdetection() wird geprüft, ob dieses Control im zu zeichnenden Rechteck liegt.
                    IF .hitdetection(lpRect)
                        .drawdb()
                    ENDIF
                ENDIF
            ELSE
                * Hier ist nur VFP für das Zeichnen verantwortlich.
                CallWindowProc(.WinProcAddress, HWND, msg, wParam, lParam)
            ENDIF
        ENDWITH
    CASE msg = WM_ERASEBKGND
        RETURN 1
ENDCASE

Die Nachricht zum Löschen des Hintergrunds WM_ERASEBKGND wird komplett ignoriert. Mit WM_PAINT treffen alle relevanten Zeichnungsanforderungen ein. Mit der autodraw Eigenschaft kann ich die eigenen GDI+ Routinen an- und ausschalten. Das ist notwendig, da das Control auch unsichtbar sein kann. Dann dürfen natürlich meine GDI+ Routinen nicht auf der Form zeichnen. Bei der Behandlung des WM_PAINT Events darf zuerst Foxpro seine Aufgabe erledigen. Danach teste ich, ob der Container im zu zeichnenden Bereich liegt und starte entsprechende meine Zeichenroutine. Diese Reihenfolge ist wichtig, damit am Ende auch meine Ausgabe zu sehen ist. Sonst würde ja Foxpro den tatsächlichen Container darüber malen.

Damit kann ich jetzt grundsätzlich auf die Fläche des Containers zeichnen und zusätzlich auf der Form weitere Foxpro Elemente anzeigen. Ich möchte aber den Container und damit die GDI+ Zeichnung unsichtbar machen können. Genau genommen möchte ich an der Stelle des Containers auch andere Objekte anzeigen können. Das läuft aber auch auf einen unsichtbaren Container hinaus.
Natürlich kann ich nicht einfach die Visible Eigenschaft auf .F. stellen. Dann würde zwar Foxpro den leeren Container nicht mehr zeichnen, aber meine Routinen würden munter weiter zeichnen. Über visible_assign werden bei Änderungen der Sichtbarkeit auch meine Zeichenroutinen mit umgestellt.

Es hat sich aber gezeigt, dass VFP seltsamerweise Teile des Containers zeichnet wenn Visible auf .T. gesetzt, obwohl der Container bereits sichtbar ist. Das führt leider zu einem Flackern. Da ich ja selbst erst nach Foxpro zeichne und damit darunterliegende Objekte kurz aufblitzen. Also wird dieser Fall auch in visible_assign abgefangen:

PROCEDURE visible_assign
LPARAMETERS vNewVal
WITH THIS
    IF m.vNewVal
        IF .Visible
            * Wenn das Control schon sichtbar ist, nicht nochmal auf sichtbar schalten.
            * Andernfalls würde VFP das Control zeichnen und es flackert.
            .clearborder()
        ELSE
            * Die eigene GDI+ Zeichenroutine anschalten.
            .redraw()
            .Visible=.T.
        ENDIF
    ELSE
        * Die eigene GDI+ Zeichenroutine abschalten.
        .autodraw=.F.
        .Visible=.F.
    ENDIF
ENDWITH

Es kann sein, dass das Bild, das mit GDI+ gezeichnet wird, kleiner als der Container ist. In WM_PAINT Routine wird aus Performance Gründen nur dieser Teil gezeichnet. Damit das Rest des Containers in der Hintergrundfarbe erscheint, lösche ich diesen Bereich von Hand mit clearborder(). Da bei einem Wechsel des Bildes auch die Visible Eigenschaft auf .T. gesetzt wird, ist das die einzige Stelle, wo dieser Rand manuell gelöscht werden muß.

Insgesamt bin ich mit dem Ergebnis jetzt sehr zufrieden. Leider feuert aus mir unbekannten Gründen unregelmässig ein WM_PAINT Event, der offensichtlich grundlos die ganze Form neu zeichnen lässt. Das führt zu einem Flackern. Insbesondere auf langsameren Rechner kann das stören. Trotzdem bin ich beeindruckt, was mit VFP 9, GDI+ und ein paar Tricks möglich ist.

Wednesday, November 16, 2005 8:55:56 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 
# Friday, November 11, 2005

Es kommt ja öfters vor, dass man schnell mal einen Screenshot machen will: Den neuesten Dialog per E-Mail verschicken oder die unerwartete Fehlermeldung dem Entwickler zukommen lassen.
Dafür gibt es diverse kleine Helferlein. Zur Not gibt es auch noch Alt+PrtScn wenn es ganz minimalistisch sein soll.

Aber oft ist dann der Weg zu einer sinnvoll benannten Bild-Datei doch eine größere Unterbrechung als geplant. Wieder ein Hot-Key, den man genau dann vergessen hat. Welche Taste war jetzt für den ganzen Bildschirm, welche für das aktive Fenster? Und wenn dann das Fenster nicht rechteckig ist, wie machen wir das mit dem Rand?

Kenny Kerr hat uns mit Window Clippings 1.0 um ein weiteres Tool bereichert. Wenn man auf das Toolbar Icon klickt, wird der ganze Bildschirm grau, man wählt das gewünschte Fenster aus und es wird hell. Mit einem Doppelklick landet das Fenster wahlweise im Clipboard und in einem History Verzeichnis. Mit der rechten Maustasten kann man die Aktion auswählen. Der Dateiname wird dabei aus Namen des Fensters abgeleitet.

Besonderen Wert hat der Autor dabei auf die Transparenz gelegt. Der Bereich des erzeugten rechteckigen Bildes, der nicht zum Fenster gehört, wird mit transparent gespeichert.
Die Unterstützung für OneNote ist für mich im Moment nicht wichtig. Mit diesem elektronischen Notizbuch bin ich nicht warm geworden.

Ein kleines Tool, daß seinen Platz in meiner Sammlung gefunden hat.
Friday, November 11, 2005 2:48:44 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 
# Monday, November 07, 2005

Jetzt beginnen die lustigen Spielchen mit dem neuen .NET 2.0 Framework. Ich möchte ein internes Tool auf die neue Version umstellen. Beim Kompilieren kommen nur noch Warnings, keine Errors, also mal fröhlich F5 gedrückt, um die Applikation im Debugger zu starten. Haste wohl gedacht. Visual Studio 2005 überrascht mit folgender Meldung:

Error while trying to run project: Unable to start debugging.
The binding handle is invalid.

Na prima! Was ist da kaputt? Im Microsoft Forum habe ich diesen Thread gefunden: Unable to debug: The binding handle is invalid
Damit konnte ich das Problem lösen. Wobei mich aber die Lösung schon sehr irritiert hat:

Vor einiger Zeit bin ich mal die Dienste (Services) auf meinem Rechner durchgegangen und habe alles ausgeschaltet, was ich wirklich nicht gebraucht habe. Wofür brauche ich auf meinem Desktop Rechner zum Beispiel die Terminal Services? Fast User Switching will ich nicht und Remote Zugriff gibt es auch nicht. Also stand der Autostarttyp (Startup type) auf Disabled. Jetzt lerne ich, dass man deswegen nicht mit Visual Studio 2005 lokal debuggen kann.

Was da intern alles abläuft, möchte ich gar nicht wissen. Brrr. Wenn der Terminal Services Dienst auf Manual steht, dann startet der Dienst, sobald VS 2005 startet. Danach geht auch das Debugging.

Wichtig dabei ist noch, das man den Rechner neu starten muss, nachdem man den Autostarttyp geändert hat

Zusammengefasst:
  1. Visual Studio 2005 will nicht debuggen.
  2. Control Panel -> Administrative Tools -> Services -> Terminal Services -> Startup type -> Manual.
  3. Reboot.
  4. Visual Studio 2005 ist zufrieden, ich bin verwundert.

Monday, November 07, 2005 5:14:57 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [1]  | 
# Friday, November 04, 2005

Mit .NET 1.1 ist nicht direkt möglich eine kontextsensitive HTML Hilfe (CHM-Datei) mit einer ganzzahligen Topic ID aufzurufen. Die HelpNavigator Aufzählung definiert die verschiedenen Möglichkeiten eine bestimme Stelle in der Hilfe anzuspringen. Leider ist keine Topic ID dabei, obwohl die HTML Hilfe diese unterstützen würde.
Eine kurze Recherche hatte damals eine  Lösung geliefert. Das FrameWork gibt den HelpNavigator Wert als ganze Zahl an die eigentlich Hilfe Darstellung weiter. Ein Wert von 15 erlaubt eine ganzzahlige Topic ID. Ein Cast auf HelpNavigator und siehe da, es geht.

Die Lösung sah dann im Wesentlichen so aus:

public static void ShowHelpTopic(System.Windows.Forms.Control parent, int topicID)
{
    string helpFile = @"c:\\help.chm";
    System.Windows.Forms.Help.ShowHelp(parent, helpFile, (System.Windows.Forms.HelpNavigator)15, topicID);
}

Jetzt, da das .NET Framework 2.0 erschienen ist, habe ich getestet, ob die Applikation direkt unter dieser Version funktioniert. Also ohne ein .Net 1.1 vorauszusetzen. Es hat eigentlich alles funktioniert, bis auf die kontextsensitive Hilfe. In Version 2.0 erlaubt HelpNavigator nämlich eine Topic ID. Der übergebene Parameter ist dann aber kein ganze Zahl, sondern die ID als Zeichenkette.
Da die Applikation auch weiterhin unter .Net 1.1 kompilierbar sein soll, kann die Topic ID nicht direkt aus der HelpNavigater Aufzählung verwendet werden. Also verwende ich wieder die Entsprechung dieses Wertes als ganze Zahl. Über eine Fallunterscheidung werden dann in Abhängigkeit von dem verwendeten .NET Framework die passenden Parameter übergeben. Damit funktioniert die HTML Hilfe auch unter .Net 2.0 kontextsensitiv.

Vereinfacht sieht die neue Methode dann so aus:

public static void ShowHelpTopic(System.Windows.Forms.Control parent, int topicID)
{
    string helpFile = @"c:\\help.chm";

    if (System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion().Substring(1,3) == "1.1")
    {
        // .NET 1.1
        System.Windows.Forms.Help.ShowHelp(parent, helpFile, (System.Windows.Forms.HelpNavigator)15, topicID);
    }
    else
    {
        // .NET 2.0
        System.Windows.Forms.Help.ShowHelp(parent, helpFile, (System.Windows.Forms.HelpNavigator)(-2147483641), topicID.ToString());
    }
}

Mit dieser Erweiterung können unsere Applikationen jetzt kontextsensitive HTML Hilfe unter beiden .NET Versionen darstellen.
Als nächstes werde ich damit beginnen unser LinkEdit, eine Applikation zum Setzen von HotSpots für SPCat, auf .NET 2.0 umzustellen. Dann kann ich ganz einfach nur HelpNavigator.TopicID verwenden. Wie langweilig :).

Friday, November 04, 2005 2:53:37 PM (W. Europe Standard Time, UTC+01:00)  #
  Disclaimer  |  Comments [0]  | 
# Thursday, October 27, 2005

Kein Vergleich der Funktionalität, sondern der Performance und des Speicherbedarfs von Microsoft Office 2003 im Vergleich zu Open Office 2.0:

Performance analysis of OpenOffice and MS Office by ZDNet's George Ou

Au Backe, da sieht Open Office 2.0 aber richtig schlecht aus. Es braucht deutlich länger und verwendet mehr Speicher.

Vielleicht findet sich ja die Zeit, daß ich selber mal damit rumspiele. Technische Dokumentation und Open Source geistert bei uns schon länger als Thema rum. Unter dem Gesichtspunkt könnte Open Office interessante Möglichkeiten bieten.

Admin | TD
Thursday, October 27, 2005 2:27:58 PM (W. Europe Daylight Time, UTC+02:00)  #
  Disclaimer  |  Comments [1]  | 
# Wednesday, October 26, 2005

Die meisten Zeichnungen, die im SPCat verwendet werden sollen, liegen als Schwarz-Weiß-Grafik im Tiff-Format vor. Um diese Grafiken aufzubereiten habe ich in C# eine .NET Applikation "TiffSPCat" geschrieben. Unter anderem werden dabei Gif-Grafiken für die Bildschirmdarstellung erzeugt. Da in der Ausgangs-Grafik keine Farben vorkommen, werden die Gif-Grafiken mit einer festen Palette von 256 Graustufen erzeugt.
Inzwischen ist TiffSPCat deutlich flexibler und erlaubt auch die Konvertierung anderer Pixel-Formate. Diese müssen nicht notwendig in Schwarz-Weiß oder als Graustufen vorliegen. Also kann ich nicht mit einer Graustufen-Palette bei den Gifs arbeiten.
Der naheliegende Weg wäre also, einfach im System.Drawing.Imaging Namensraum den Gif Encoder auszuwählen und damit auf der erzeugten Bitmap die Save Methode aufzurufen. GDI+ wird es schon richten.
Bei Ausgangs-Grafiken mit wenigen Farben, also einer typischen technischen Zeichnung, sieht das Ergebnis passabel aus und ich war zunächst zufrieden.
Leider störte dann bei der weiter Verwendung der Gif-Grafiken deren Transparenz. Bei den Grafiken mit der selbst erzeugten, festen Graustufen-Palette waren die Gif-Grafiken nicht transparent. Also doch weiter in den Eingeweiden von GDI+ und dem .NET Wrapper System.Drawing.Imaging stöbern.

Als erstes fällt auf, daß immer eine Standard-Palette mit 256 Farben verwendet wird. Die Palette kann man in der GDI+ FAQ Creating Transparent GIF Images von Bob Powell schön sehen. Es gibt keine Möglichkeit über zusätzliche Parameter eine optimierte, adaptive Palette erzeugen zu lassen. Schade, also von Hand die Palette erzeugen. Aber erstmal Google fragen, keine Räder neu erfinden.
Das Beispiel aus diesem MSDN Artikel Optimizing Color Quantization for ASP.NET Images quantisiert mit einem Octree Algorithmus die Echtfarben auf eine optimierte Palette. Mit ein paar kleinen Veränderungen konnte ich den Quelltext in TiffSPCat einbauen. Die erzeugten Paletten sehen gleich viel sinnvoller und die dazugehörigen Gif-Grafiken gleich viel besser aus.

Trotzdem blieb eine Farbe in dem erzeugten Gif transparent. Jetzt habe ich aber Kontrolle über die verwendete Palette. Der letzte Schritt klärt sich dann mit diesem Knowledge Base Artikel KB319061, How to save a .gif file with a new color table by using Visual C# .NET. Die erste Farbe in der Palette mit einem Alpha Wert gleich Null, wird transparent. Also alle Alpha Werte auf 255 setzen:

// Palette von der Bitmap holen.
ColorPalette pal = this.activeBitmap.Palette;

// Alpha auf 255 setzen.
for(int i = 0; i < pal.Entries.Length; i++)
{
Color col = pal.Entries[i];
pal.Entries[i] = Color.FromArgb(255, col.R, col.G, col.B);
}

// Palette zuweisen.
this.activeBitmap.Palette = pal;

Die fertige Bitmap im PixelFormat.Format8bppIndexed und allen Alpha Werten ungleich Null wird dann mit dem Gif Encoder von GDI+ ohne Transparenz gespeichert.

Wednesday, October 26, 2005 11:18:36 AM (W. Europe Daylight Time, UTC+02:00)  #
  Disclaimer  |  Comments [0]  | 
# Tuesday, October 18, 2005

In unserem Ersatzteilkatalog SPCat ist die Navigation in der Hierarchie der Ersatzteile sehr wichtig. Unter anderem besteht die Möglichkeit über graphische Links in einer Baugruppenzeichnung zu entsprechenden Unterteilen zu  springen. Diese Links in der Zeichnung werden zur Laufzeit in die Graphik gezeichnet.

Der aktuelle SPCat 6.5 ist mit Visual Foxpro 8 programmiert. Bis zu dieser Version ist Visual Foxpro  sehr limitiert in den Möglichkeiten zur Modifikation und Darstellung von Graphiken. Deswegen wurde die Darstellung der verlinkten Zeichnungen über eine ActiveX Komponente realisiert. Diese Komponente ist in Visual Basic 6 programmiert.

Mit Visual Foxpro 9 ist der Zugriff auf GDI+, der Windows Graphik Schnittstelle, sinnvoll möglich. Damit können modernere Bildformate geladen werden, unter anderem auch PNG Bilder. Alle grundlegenden graphischen Funktionen stehen zur Verfügung. Es ist aber nicht einfach möglich das Ergebnis dieser Manipulationen zusammen mit anderen Visual Foxpro Bildschirmelement darzustellen. Zu grundlegend anders ist die Art, wie Visual Foxpro seine Ausgabe erzeugt.

Craig Boyd hat einen sehr hilfreichen Artikel "GDI+ on VFP 9 Forms: Solving the Paint Problem" in FoxTalk 2.0 veröffentlicht. Die Lösung besteht im Wesentlichen darin, die Windows Ereignisse abzufangen und mit GDI+ das erzeugte Bild innerhalb von Visual Foxpro selbst zu zeichnen. Der Nachteil ist, daß in häufig aufgerufenen Ereignissen die eigene Logik zusätzlich mitläuft. Deswegen wird der zu zeichnenende Ausschnitt des Bildes mit allen Links zwischengespeichert und nur dieses Ergebnis gezeichnet (double buffer). Lediglich sich ändernde Teile, wie der Hovereffekt und Tooltips werden neu erzeugt.
Der Vorteil liegt darin, daß man die volle Kontrolle über die Ausgabe hat und Effekte möglich sind, die bisher weder Visual Foxpro noch reines Visual Basic 6 konnte.

Das Ergebnis sieht besser als die bisherige Lösung aus und eine weitere externe Abhängigkeit ist aus unserem elektronischen Ersatzteilkatalog verbannt. Ich bin jedesmal froh, wenn ich VB 6 Ballast abwerfen kann.

Tuesday, October 18, 2005 1:40:36 PM (W. Europe Daylight Time, UTC+02:00)  #
  Disclaimer  |  Comments [0]  | 
# Thursday, October 13, 2005

Ich habe mich entschieden, dieses Blog in deutsch zu schreiben. Mein Englisch ist nicht gut genug Zusammenhänge sinnvoll darzustellen. Außerdem sind die meisten unserer (potentiellen) Kunden auch aus dem deutschsprachigem Raum.
Vielleicht wird der eine oder andere Eintrag zusätzlich als englische Version erscheinen.
Thursday, October 13, 2005 12:53:57 PM (W. Europe Daylight Time, UTC+02:00)  #
  Disclaimer  |  Comments [0]  | 
# Wednesday, October 12, 2005

Hallo,

mein Name ist Christian Deger. Ich bin hier bei der CPTec GmbH im weitesten Sinne für die IT zuständig. Da gehört auch mal die Installation einer Blog-Software dazu. Das ist hiermit erledigt.

Die administrativen Aufgaben innerhalb von CPTec dürfte aber niemanden interessieren. Deswegen soll es in diesem Blog eher um die Themen gehen, die mit unserer Hauptaufgabe, der technischen Dokumentation zu tun haben:

  • Einsatz von Redaktionssystemen (u.a. Word und Framemaker)
  • Einsatz von Grafiktools (u.a. Corel und Adobe)
  • Technische Dokumentation im Internet
  • Content Management
  • Software Hilfe
  • Elektronische Ersatzteilkataloge
Über die Jahre hat es sich ergeben, daß wir auch für unsere Kunden Webseiten erstellen und diese zum Teil auch bei uns hosten. Dafür haben wir eigene Tools geschrieben. Auch andere interne Aufgaben lösen wir gerne mit eigener Software. Letztes Beispiel ist unser dateibasiertes Backup mit Unterstützung für externe Festplatten: CPTec Backup.

Auch dieses Blog hat einen Standard-Disclaimer "spreche nicht für die ganze Firma". Ich bin bezüglich keines Themas der Pressesprecher von CPTec. Ich schreibe hier im Sinne und im Auftrag von CPTec, aber ohne direkte Kontrolle. Im Zweifelsfall hat mein Arbeitgeber das letzte Wort und ich werde zurückrudern.
Dennoch soll es wirklich um Dinge gehen, die mit meinem Job zu tun haben. So gesehen gehört dieses Blog jetzt zu meinem Job.


Wednesday, October 12, 2005 7:46:50 PM (W. Europe Daylight Time, UTC+02:00)  #
  Disclaimer  |  Comments [0]  |