Die Unsitte in WM_CONTEXTMENU keine Tastaturnutzer zu berücksichtigen

Das viele Programmierer nur an die Maus denken, wenn Sie Kontextmenü hören ist schon an meinem letzten Blog Artikel klar geworden. 

Dennoch überraschen mich immer wieder Programm, die zwar korrekt auf die Kontextmenü-Taste (bzw. Umschalt+F10) reagieren, aber dann ein Kontextmenü links oben in der Ecke meines Monitors aufklappen.

OK! Es wurde verstanden, dass es eine WM_CONTEXTMENU Nachricht gibt, dass aber intelligentes Handling für Tastaturbenutzer anders aussieht, als für einen Mausbenutzer wird sehr selten verstanden oder berücksichtigt.

❗ Nehmen wir mal ein Beispiel:
Gegeben sei ein List View Control. Der Benutzer hat den Fokus auf diesem Control und drückt nun die Kontextmenütaste bzw. die Tasten Umschalt+F10.

❓ Frage: Was währe nun ein angemessenes Verhalten?

Wer die Doku gelesen hat wird wissen, dass in diesem Fall als Koordinaten (-1,-1) mit der WM_CONTEXTMENU Nachricht übermittelt werden. Dies ist nun der Grund dafür, dass viele Kontextmenüs nun in der linken oberen Ecke aufklappen, weil viele Entwickler eben nicht lesen (können), oder sich keine Gedanken machen, dass es einen Unterschied macht Mausbenutzer zu sein oder Tastaturbenutzer.

(-1,-1) ist keine geeignete Position für das Kontextmenü!

Nun die Mausposition zu bestimmen, wäre meiner Meinung nach, auch nicht angemessen. Denn der User benutzt diese ja gar nicht, und hat sie evtl. sogar einfach an den Bildschirmrand geschoben. Das Kontextmenü dort aufpoppen zu lassen wäre genauso falsch.

Einzig richtig wäre es den aktuell selektierten Eintrag mit dem Fokus im List View zu bestimmen und das Popupmenü knapp darunter und leicht rechts versetzt davon anzuzeigen. Dem Benutzer wäre der Kontextbezug sofort klar und auch seine Augen müssten nicht erst an eine andere Bildschirmposition wandern um zu erfassen, was nun möglich ist

Im Klartext heißt das für den Entwickler: selektiertes Item suchen (LVM_GETNEXTITEM), dessen Bildschirmposition Position zu bestimmen (LVM_GETITEMRECT) und sich eine gute alternative zu überlegen, wenn das Item außerhalb des sichtbaren Bereiches ist (z.B. Mitte des Controls).

Fazit: Um Kontextmenüs auch mit der Tastatur bedienbar zu machen gehört etwas mehr Grips und Aufwand dazu, als nur eine Popupmenü an einer bestimmten Koordinate anzuzeigen.

Die Unsitte WM_RBUTTONDOWN statt WM_CONTEXTMENU zu verwenden

Ich freue mich jedesmal, wenn ich ein Programm benutze und ein Kontextmenü öffnen will und es öffnet sich nicht.

„Ja da hast Du wohl nicht mit der rechten Maustaste geklickt, sonst würde es sich öffnen!“

werden jetzt einige sagen.
Und ja es stimmt, ich habe nicht mit der rechten Maustaste irgendwohin geklickt sondern die Kontextmenü-Taste rechts unten auf meiner Tastatur benutzt. Und wer es noch nicht wusste Umschalt+F10 löst die selbe Funktion aus.

Falls es also noch jemand nicht bemerkt haben sollte. Microsoft hat in der Windows API direkt eine Nachricht nur für die Behandlung von Kontextmenüs reserviert und die heißt: WM_CONTEXTMENU! Und es sollte jedem Entwickler bereits in Fleisch und Blut übergegangen sein, diese Nachricht und nichts anderes für Kontextmenüs zu verwenden.

Der nette Nebenbonus dieser Nachricht, ist, dass man sich nicht um jedes Fenster alleine kümmern muss. WM_CONTEXTMENU wird an das Elternfenster weitergereicht, wenn ein Kindfenster diese Nachricht nicht behandelt. Das macht es auch einfach für Dialoge einen zentralen Handler zu bauen.

Die Unsitte PostQuitMessage zum Beenden eines Programmes zu verwenden!

Immer wieder lese ich Postings in http://www.c-plusplus.de/forum die es anpreisen ein Programm mit PostQuitMessage zu beenden, genau so unsinnig wie WM_QUIT zu versenden. 

Das ist natürlich Unfug! Sicherlich wird ein Programm durch PostQuitMessage beendet, aber warum?
Weil die Nachrichtenschleife verlassen wird und letzten Endes WinMain verlassen wird. Dies führt dazu, dass die darunter liegenden CRT Routinen irgendwann ExitProcess ausführen. BTW: Würde hier nur ein der CRT einfacher return erfolgen, dann würde der Prozess weiterleben, wenn noch ein einziger anderer Thread aktiv wäre.

Das brutale Verlassen führt aber letzten Endes auch dazu, dass erst ExitProcess brutal alle Fenster aufräumt. D.h. kein Fenster wird normal zerstört, kein WM_DESTROY bzw. WM_NCDESTROY wird empfangen. D.h. alle normalen Prozesse, die dem Aufräumen und Freigeben von Ressourcen dienen, werden außer Kraft gesetzt.
Ja und sicherlich gibt ExitProcess Speicher frei, die der Prozess alloziert hat, auch einige Handles können freigegeben werden, aber nicht alle (z.B. benamte Mutexe und Semaphoren).

Bei einem Mikey Mouse Win32 API Programm mag dies kein Problem sein, denn hier gibt es keine Ressourcen, die Prozessübergreifend ein Leak verursachen würden, oder eine Ressource blockieren würden.
Aber grundsätzlich würde ich es unterlassen. Jede andere Library, die man verwendet, jedes externe Control, dass man einbindet könnte genau auf dieses entscheiden WM_DESTROY angewiesen sein um Ressourcen freizugeben, die ein System blockieren könnten. Solange man nicht 100%ig weiß wie die benutzen Bibliotheken arbeiten, ja nicht einmal detailliert weiß wie COM und die CRT Handles behandeln würde ich grundsätzlich abraten ein Programm einfach mit ExitProcess zu verlassen, genau so wie ich abrate TerminateProcess zu verwenden.

Der richtige Weg ist und bleibt es das/alle Main Window(s) zu zerstören und entsprechend dann (im WM_DESTROY Handler) PostQuitMessage (AfxPostQuitMessage) auszuführen. Durch das Zerstören des Hauptfensters werden natürlich alle enthaltenen Child-Windows mit zerstört. Alle Fenster bekommen damit die Chance hinter sich aufzuräumen und Ressourcen frei zu geben.

PS: Aber solche Unsitten lassen sich kaum ausmerzen. Genauso wenig wie die Unsitte einen HINSTANCE Wert einfach durch Aufruf von GetModuleHandle(NULL) zu bestimmen… Auch eine Unsitte, die wohl niemand mehr ausmerzen wird.

VC-2008: Neues, Breaking changes…

Hier ein kleiner Auszug aus der Liste der Änderungen und Neuigkeiten.
Die Links verweisen in die MSDN wo man alles nachlesen kann.

What’s New in Visual C++ 2008
Da ist nicht sooo viel:

  • Eingeschränkte (read only) Unterstützung des Class Designers.
  • Vista style Guidlines für Dialoge
  • Vista Common Controls in der MFC
  • Die interessanten Sachen wie MFCNext und TR1 kommen noch

Breaking Changes

  • Nachdem in Visual Studio 2005 der Support von Windows  95 entfiel, hat Microsoft nun einen Schlussstrich unter gezogen.
    Im Klartext: Windows 95, Windows 98, Windows ME, und Windows NT werden als Zielplattformen nicht mehr unterstützt.
  • Hier fällt auf, dass die ATL nun zwingend Abhängig von der CRT wird. Wer früher gerne ATL_MIN_CRT verwendet hat um „gar keine“ CRT zu verwenden, der wird feststellen, dass seine Module etwas wachsen.
  • /Wp64 ist deprecated (na endlich)…

Anmerkung:
Wie so oft werden hier die Sachen veröffentlicht, wo man bei Microsoft weiß, dass man Breaking Changes durchgeführt hat. Es gibt oft genug weitere Breaking Changes die man oft erst am eigenen Leib erfahren muss, siehe Attributed ATL.

Weitere Infos zur MFC in VS-2008 und der BCG Pro-Library

Hier noch ein paar Infos zu dem Thema:

Eine Microsoft Bekanntmachung auf den US VC++ Seiten: MFC Update Powered By BCGSoft
http://msdn2.microsoft.com/de-de/visualc/bb892882.aspx

Eine Pressemitteilung von BCG-Soft vom 09.11.2007:
http://www.bcgsoft.com/pressreleases/PR071110.pdf

Fragen und Antworten im BCG-Forum:
http://www.bcgsoft.com/cgi-bin/forum/topic.asp?TOPIC_ID=4476

MFC Updates for Visual Studio 2008 and Beyond: MFCnext

Was ich hier schreibe ist an sich viel zu viel für einen einzigen Blog Artikel, deshalb behalte ich mir hier nur eine kurze Zusammenfassung vor. Kommentieren und diskutieren werde ich dies noch in den nächsten Wochen.

Was ❓
Heute in den C++ Sessions der TechEd 2007 in Barcelona wurde ein Geheimnis gelüftet. Das war uns MVPs schon etwas länger bekannt aber leider war es uns bis heute durch NDA (non-disclosure aggreement) untersagt davon zu berichten.
Microsoft wird der MFC einen neuen gewaltigen Schub geben und seit Jahren die ersten wirklich gravierenden Erweiterungen verpassen. Das Ganze wird unter dem Namen MFCnext laufen.

In diesem Bog will ich nur summarisch aufzählen was alles an Neuem kommen wird:

  • Office 2007 UI Stil. Ribbons und alles was dazu gehört
  • Tabbed MDI
  • Integration neuer Controls (Advanced button, Shell tree and list, Mask edit, Property list)
  • Erweiterter Applikations Assistent
  • Rückwärts kompatibel bis Windows 2000
  • Docking wie wir es von Visual Studio her kennen
  • Visual Styles (Skins)
  • Die Standard Template Library wird um TR1 erweitert (tr1::shared_ptr, tr1:: mem_fn, tr1:: bind, tr1::regex, tr1::tuple, tr1::array, unordered containers hash-based, tr1::type_traits)
  • Microsoft bekennt sich klar und offen zur weiteren Entwicklung und Unterstützung der nativen C++ Programmierung.
  • und vieles andere mehr…

Die MFC wird sich in Anzahl der Klassen und Größe verdoppeln.

Wann kommt das ❓
Das Ganze wird bereits im März 2008 nachgeliefert, in einem Update für VS-2008. Ein Release Kandidat (RC) wird bereits im Dezember 2007 zur Verfügung stehen.  So die bisherige Planung.

Die MFC ist tot… lange lebe MFCnext 😀

Weitere Infos:
Auch in Deutschland bei dieser Veranstaltung werden die Vortargenden aus Barcelona zu sehen und hören sein:
München 15.11.2007: Visual C++ 2008 und danach, Ask the Experts

Noch mehr Infos gibt es auch in dem Blog von Jochen Kalmbach, der live aus Barcelona von der TechEd 2007 berichtet.

Mal ganz schnell sich selbst reingelegt mit Excel und CRecordset::optimizeBulkAdd

Um eine größere Datenmenge in Excel über eine ODBC Verbindung zu erzeugen habe ich einen entsprechenden Recordset geöffnet und Daten massenweise hineingepumpt. Die Tabelle (das Worksheet), das erzeugt wurde enthielt Spalten mit Text-, Numerischen-, und Zeitdaten. Einige der Daten sollten wurden mit NULL Werten erzeugt werden.

Entsprechend den Tipps in der Doku habe ich die zusätzliche Option CRecordset::optimizeBulkAdd zu CRecordset::appendOnly verwendet um das zu beschleunigen.

m_recordSet.Open(CRecordset::snapshot,
                NULL,
                CRecordset::appendOnly|CRecordset::optimizeBulkAdd);

Laut der Doku sollte das erste Statement entsprechend für alle weiteren Operationen, den Dirty-Status setzten. Fein (dachte ich), alle Daten des Recordsets werden gesetzt, entweder auf den entsprechenden Wert oder NULL.

Alle Tests waren „erstmal“ positiv. Die Daten wurden in guter Geschwindigkeit erzeugt.
Leider stellten Kunden dann fest (allerdings Wochen später), dass unter bestimmten Umständen ganze Spalten leer waren. Sobald in der ersten Zeile einmal in einer Spalte NULL ausgegeben wurde, dann wurde auch in allen folgenden Spalten kein Wert eingetragen. Das Ganze, obwohl die Daten korrekt gebunden und als dirty markiert wurden. An das Open Statement dachte ich nicht…

Nach langem Testen nahm ich schon an, einen Bug in dem ODBC Treiber gefunden zu haben. Stutzig wurde ich aber, dass alles prima klappte, wenn ich pure SQL INSERT-Statements mit ExecuteSQL  direkt auslöste, ohne den Recordset zu bemühen. Was nun?

Relativ ratlos wollte ich schon einen Supportfall bei Microsoft öffnen, bis ich den Code noch einmal Schritt für Schritt durchsah und wieder über das das Open Statement stolperte.

Bingo: Sobald ich dieses Flag entfernte arbeitete alles korrekt. Nach meinem Verständnis müsste auch ein NULL Wert ein Wert sei, der gültig ist, solange ich alle Spalten beim ersten INSERT/AddNew angebe. Ich nahm an, dass auch ein Einfügen eines NULL Wertes ein „Dirty“ für die entsprechende Spalte auslöst. Das ist aber nicht so.

Wird – zumindest beim ODBC Treiber für Excel – in dem ersten INSERT eines Datensatzes ein NULL Wert ausgegeben , dann werden auch keine Daten für alle nachfolgenden Zeilen eingefügt. Und das NULL eingefügt wird, geschieht ganz schnell, wenn z.B. ein leeres Textfeld (Länge 0) ausgegeben werden soll. Ob dies auch für Access oder andere Treiber gilt habe ich noch nicht ausprobiert.

Das verschollene Scribble Tutorial…

Immer wieder merke ich, dass man bei vielen Anfragen in den Foren auf den Klassiker verweisen müsste:
Das Scribble Tutorial.

Dieses Tutorial ist offiziell über die MSDN Suche im Internet nicht mehr zu finden. Egal wie man auf den MS-Seiten sucht. Interessant ist, dass es noch genug KB-Artikel gibt die auf dieses Tutorial verweisen. Der Sample Code ist noch vorhanden. Nur das Tutorial ist nicht einfach zu finden.

Aber es existiert noch in einem Ast der MSDN, die die VC++ 6.0 Dokumentation beinhaltet.:
http://msdn2.microsoft.com/en-us/library/aa716528(VS.60).aspx

Leider ist dieser Ast mit der globalen Suchfunktion nicht erreichbar und auch nicht entsprechend indiziert.
Meiner Meinung nach, hätte dieses Tutorial schon längst angepasst werden müssen, weil sich die Wizards komplett seit VS.NET 2002 geändert haben.

BTW: Leider befürchte ich, dass auch dieser VC++ 6.0 Ast der MSDN irgendwann verschwinden wird.

SetFocus versus WM_NEXTDLGCTL

Die meisten Entwickler verwenden SetFocus um in einem Dialog gezielt den Eingabefokus zu versetzen. Aber es gibt ein Problem, dem SetFocus nicht gerecht wird: der Default Button.
Der Default Button wird durch WM_SETDEFID bzw. CDialog::SetDefID gesetzt. SetFocus berücksicht das interne Konzept des Default Buttons nicht.

Wenn man mit der Tab-Taste durch einen Dialog springt und einen Button erwischt, dann wird dieser automatisch zum Default Button. Normalerweise ist das der OK-Schalter, er verliert dann den dicken Rahmen. Drückt man die Eingabe-Taste, dann wird nun der neue Schalter ausgelöst und nicht der OK-Schalter.
Landet der Fokus von einem Button dann bei einem Edit Control, dann wird der OK-Schalter wieder der Default Button und man kann mit der Eingabe-Taste den Dialog beenden.

Wenn nun SetFocus verwendet wird durch eine interne Funktion, dann wird dieser Mechanismus des Dialoges umgangen. Der Default-Button wird evtl. nicht korrekt gesetzt. Es kann sogar soweit kommen, dass es zwei Default-Schalter oder gar keinen mehr gibt. SetFocus führt immer zu Problemen wenn das neue Control oder das bisherige Control, welches den Fokus hatte, ein Button ist. Nur wenn beide Controls keine Button sind kann SetFocus gefahrlos verwendet werden.

Korrekt funktioniert das Ganze nur, wenn statt SetFocus, WM_NEXTDLGCTL verwendet wird, oder die entsprechenden MFC Funktionen, CDialog::NextDlgCtrl bzw. CDialog::GotoDlgCtrl verwendet werden.
Die Nachricht WM_NEXTDLGCTL wird auch intern durch die DefDialogProc behandelt und normalerweise durch IsDialogMessage erzeugt.
Gefahrlos ist auch die Verwendung von SetFocus in WM_INITDIALOG bzw. CDialog::OnInitDialog Handlern, die dann normalerweise mit FALSE, verlassen werden. Nach dieser Funktion sorgt der Dialog Handler, für die korrekte Behandlung der Default Buttons.

Fazit: Man sollte also innerhalb von Dialogen ganz auf SetFocus verzichten sondern nur WM_NEXTDLGCTL  bzw.  CDialog::NextDlgCtrl und CDialog::GotoDlgCtrl verwenden. Konsequenterweise sollte man dann auch in OnInitDialog Handlern auf SetFocus verzichten. ❗

Besonderheiten bei der Ausgabe über Excel via ODBC

Man sollte tunlichst darauf achten, dass bei Excel Export über ODBC alle Spalten mit einem möglichst exakten Datentyp erzeugt werden. D.h. der Faulheit halber sollte man nicht jede Spalte mit dem Typ TEXT erzeugen, sondern eben DOUBLE, NUMERIC, BIT, DATETIME etc. verwenden, die Excel auch unterstützt. In diesem Fall werden nur die TEXT-Daten mit einem Apostroph versehen. (siehe dieser Blogbeitrag)

Lästig ist, dass Excel es nicht schafft DATETIME, DATE und TIME Spalten standardmäßig auch korrekt anzuzeigen. DATETIME-Spalten werden zwar korrekt mit Datum und Uhrzeit befüllt. Das Anzeigeformat wird aber so dämlich gewählt, dass nur das Datum sichtbar ist. Für DATE-Spalten geht das in Ordnung. Bei TIME-Spalten wird sogar noch einfach das Tagesdatum „hinzugedacht“, obwohl nur der Zeitwert übertragen wurde. D.h. man sieht ein Tagesdatum aber nicht die Zeit. Die Zeit wird nur sichtbar beim Ändern des Formtes der Zellen, oder wenn man die Daten einzeln anklickt und in der Bearbeitungsleiste betrachtet.

Trickreich ist auch die Ausgabe in ein DATETIME Feld, wenn man ein Textfeld bindet. In diesem Fall muss zwingend das Format JJJJ-MM-TT für die Ausgabe verwendet werden.

Ein weitere Trick besteht darin, die Spalten zusätzlich mit der Option NULL anzulegen:
CREATE TABLE [Data] ([Field1] TEXT NULL, [Field2] DOUBLE NULL)
andernfalls braucht man sich nicht wundern, wenn es eine Exception gibt beim Speichern eines leeren Strings.