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.

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.

Wann Message Reflection nicht funktioniert!

TN062: Message Reflection for Windows Controls beschreibt die vielen schönen Möglichkeiten der Message Reflection.

Aber es gibt eine wichtige Voraussetzung, dass da ganze funktioniert:
Das Parent Window muss auch ein Fenster sein, das mit der MFC erzeugt wurde, oder durch die MFC gesubclassed sein.

Die Reflektion geschieht immer nur durch das Parent Fenster. Wenn dort also eine WM_CTLCOLOR Nachricht das Parent erreicht, dann wird die Nachricht an das Fenster, dass es gesendet weitergeleitet, wenn es eben über einen entsprechenden Reflektion Eintrag für WM_CTLCOLOR verfügt. Das ganze wird erledigt durch die Funktion CWnd::ReflectLastMessage, die die entsprechenden Funktion  CWnd::SendChildNotifyLastMsg im eigentlichen Control wieder aufruft. Diese Funktion ruft wiederum CWnd::OnChildNotify auf, die dann die Reflection Einträge in der Message Map sucht. Ist das Parent nun kein MFC Fenster kann auch niemand die Nachrichten an das sende Fenster zurückleiten.

Hat man also ein Fenster hat, dass durch pure Win32 API Funktionen erzeugt wurde und auch nicht durch die MFC gesubclassed wurde und in diesem Fenster wird ein MFC Fenster als Child erzeugt, dann erreichen dieses Child keine Nachrichten durch die Reflektion.

Command Routing der MFC bei Kontext Menüs mit TrackPopupMenu

Kontext Menüs  gehören zum State of  the Art.

Es ist auch nicht weiter schwer ein Menü zu laden ünd per TrackPopupMenu auf den Bildschirm zu zaubern. Entsprechender Code sieht dann meistens so aus:

void CMyView::OnContextMenu(CWnd* pWnd, CPoint pt)
{
    // Get the menu
    CMenu popups;
    if (!popups.LoadMenu(_T("AGV-IDR_MACRO_EDITOR_CONTEXT")))
        AfxThrowResourceException();
    CMenu* pSubMenu= popups.GetSubMenu(0);
    ASSERT(pSubMenu);
    if (!pSubMenu)
        AfxThrowResourceException();
    pSubMenu->TrackPopupMenu(0,pt.x,pt.y,this);
}

Oft sind in dem Menü einfache IDs von Commands verwendet worden, die irgendwo im View, Dokument, Frame oder der Applikation implementiert sind. Der Programmierer wundert sich nun warum seine Menüpunkte nicht ausgegraut werden. Alle Menüpunkte für alle IDs sind aktiv. Und man wundert sich noch mehr. Manche Commands (außerhalb des Views) werden nicht ausgeführt.

Was läuft hier falsch?

❗ Die Antwort liegt einzig und alleine im Aufruf von TrackPopupMenu! Der letzte Parameter gibt an, an wen die entsprechenden Nachrichten (WM_COMMAND, WM_INITPOPUPMENU und andere) gesendet werden. Naheliegender Weise gibt der Entwickler hier das Fenster an, in dem er das Popup Menü anzeigen will. Hier liegt der Hase im Pfeffer.
Ein normales Fenster und ein View hat keine Verwendung (Handler) für WM_INITPOPUPMENU. Solch ein Handler wäre aber für das Command Routing der MFC notwendig um die Menu-Items ein- und auszuschalten.

Das CMainFrame hat solch einen Handler, und kann damit umgehen. Also einfach das CMainFrame als Zeiger übergeben und … es funktioniert wie erwartet. Warum auch nicht. Die IDs aus dem normalen Menü werden ja auch über genau den selben Weg des Command Routings behandelt.
Die entscheidende Codezeile sieht dann so aus:

pSubMenu->TrackPopupMenu(0,pt.x,pt.y,AfxGetMainWnd());

VC-2005 Features der CRT für Unicode Unterstützung

Die CRT der VC-2005 hat eine perfekte Unterstützung für Unicode Dateien im UTF-8 und UTF-16 Little Endian Format.

Wollte man bisher Unicode Dateien lesen, so mussten diese mit _wfopen(…,L“rb“) geöffnet werden und entsprechende Leseoperationen mussten folgen. Um Unicode und ANSI Dateien zu unterscheiden schrieb man selbst entsprechenden Code, der nach einer BOM (Byte Order Mark) schaut. Beim Schreiben hatte man auch selbst darauf zu achten die BOM entsprechend zu setzen.

Ziemlich unbemerkt (auch von mir 🙂 ), hat die CRT hier eine entscheidene Erweiterung erfahren. Mit dem folgenden Code lässt sich gezielt jede Datei entsprechend öffnen und lesen, sofern Sie über eine korrekte BOM verfügt, bzw. auch eine Datei zum Schreiben öffnen die mit der entsprechenden BOM versehen wird.

FILE *pFile = _tfopen(_T(„myfile.txt“),_T(„rt, ccs=UNICODE“));

Ist die Datei eine ANSI Datei ohne BOM, wird sie entsprechend geöffnet. Unicode Dateien im Typ UTF-8 und UTF-16 little endian werden an der entsprechenden BOM erkannt und entsprechend gelesen.
Über ccs=UTF-8 bzw. ccs=UTF-16LE lässt sich auch gezielt eine entsprechende Unicode Datei ohne BOM öffnen. Eine vorhandene BOM überschreibt allerdings die beim Öffnen angegebenen Formatierung. Fein!

Leider hinkt die MFC diesen wirklich tollen Funktionen der CRT hinterher. Weder die MFC  8.0 aus VS-2005 noch die neue Orcas Version (Stand Beta1) verfügt über eine neue Version der CStdioFile, die diese Funktionen der CRT abbildet.

Glücklicherweise gibt es einen entsprechenden CStdioFile(FILE *) Konstruktor. Dadurch ist es möglich, einfach eine Datei mit _tfopen zu öffnen und den Stream einfach an ein CStdioFile Objekt zu koppeln.
Einziger Schönheitsfehler: CStdioFile::Close muss explizit in diesem Fall aufgerufen werden, d. h. Close wird nicht durch den Destruktor aufgerufen.
Für den MFC Kenner: m_bCloseOnDelete ist FALSE und leider protected. Dieses Flag verhindert, dass Close auch durch den Destruktor aufgerufen wird.

MSDN Dokumentation zu _tfopen/fopen/_wfopen

… und schließlich entdeckte Kolumbus SendDlgItemMessage

Wer hat nicht oft genug solchen Code geschrieben:

GetDlgItem(IDC_MYITEM)->SendMessage(WM_MYMSG,wParam,lParam);
// – oder-
HWND hWnd = ::GetDlgItem(hDlg,IDC_MYITEM);
if (hWnd)
    ::SendMessage(hWnd,WM_MYMSG,wParam,lParam);

Da programmiert man Jahrzente unter Windows und hat SendDlgItemMessage noch niemals gesehen und damit geht es so einfach:

SendDlgItemMessage(IDC_MYITEM,WM_MYMSG,wParam,lParam);
// – oder-
::SendDlgItemMessage(hDlg,IDC_MYITEM,WM_MYMSG,wParam,lParam);

Man lernt nie aus. 😉

MSDN Links: SendDlgItemMessage, CWnd::SendDlgItemMessage

BTW: Netter Seiteneffekt. Die direkte Verwendung von GetDlgItem(…)->SendMessage(…); ist natürlich äußerst unsicher. Im Extremfall kracht es. Charmant, dass SendDlgItemMessage sich hier äußerst robust und korrekt verhält.