Dem Leser sollte Win32-Programmierung bekannt sein, wenn nicht, dann bitte den „Petzold“ lesen!
Ich bin mit Windows-Programmierung in Pascal groß geworden,
und habe Objektbibliotheken zu hassen gelernt, weil sie eben nur so tun,
als ob sie Komplexität verbergen könnten. Deswegen wird der Leser bei
mir kein einziges
MFC-Programm finden.
Auch wenn es nach außen so (modern)
aussehen möge, schon die Dateigröße verrät, dass es kein MFC sein kann.
| mit Laufzeitbibliothek | ohne Laufzeitbibliothek | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Minimale Kodegröße | 20 KB | 2,5 KB | ||||||||||||
| Kopfdateien | #include <stdafx.h> oder ähnliches | #define WIN32_LEAN_AND_MEAN #include <windows.h> // Windows eben #include <windowsx.h> // Nützliche Makros #include <shlwapi.h> // String-Funktionen #include <comdlg32.h> // Datei-öffnen-Dialog #include <comctl32.h> // erweiterte Dialogelemente ... | ||||||||||||
| Eintrittspunkt | WinMain
|
Konstanten im Kodesegment (/GF) ersparen initialisierte statische Variablen.
(Alle nicht initialisierten statischen Variablen werden vom EXE-Loader
stets mit Null initialisiert, muss man wissen.
Es ist ohnehin unschöner Programmierstil, mit Nicht-Null initialisierte
statische Variablen nicht const zu setzen.
Bei der Verwendung von
unicows.lib bzw.
libunicows.lib
kommt man um solche Variablen nicht herum.)
Den Erfolg der Maßnahme kann man mittels dumpbin /headers
prüfen: das Datensegment (.data) muss in der EXE-Datei leer sein.
SECTION HEADER #3
.data name
6F4 virtual size
8000 virtual address (00408000 to 004086F3)
0 size of raw data
0 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
Read Write
Ohne Standardbibliothek kann man nicht „portabel“ im Sinne des ANSI-Standards
programmieren. Aber kein Windows-Programm ist jemals ANSI-konform.
Es gibt kein main, kein printf. (Wohin denn?)
Muss ich weiter argumentieren? (Ich rede hier nicht von Konsolen-Programmen.)
Also, Portabibiltät ist etwas für den Elfenbeinturm (Informatiker, Lehre)
oder für Unix (Linux - da kommt man nicht herum!).
Für Windows: da sage ich einfach Tschüss und winke-winke!
Jetzt muss man nur noch wissen, was man statt der gewohnten Funktionen
verwenden muss. Ganz nebenbei wird das Programm unicode-fähig, ohne jedesmal
den TCHAR-Präfix (bspw. _tprintf statt printf)
verwenden zu müssen (was ja auch nicht ganz portabel ist).
Hier eine kurze Übersicht
| mit Laufzeitbibliothek | ohne Laufzeitbibliothek | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Kommandozeilen-| via | strchr zerstückeltPathGetArgs einsetzen!
String- | strlen | |||||||||||||||||||||||||||||||||||||||||||||||||||||
Hier könnte man noch beliebig fortsetzen.
Bei Einsatz von Gleitkomma bei Ein- und Ausgabe ist es besser,
ein funktionierendes sprintf() und sscanf()
zu haben, diese beschafft man sich wie folgt:
EXTERN_C int (_cdecl*_stprintf)(PTSTR,int,PTSTR,...);
EXTERN_C int (_cdecl*_stscanf)(PTSTR,PTSTR,...);
#ifdef UNICODE
# define __IMP__STPRINTF "_snwprintf"
# define __IMP__STSCANF "swscanf"
#else
# define __IMP__STPRINTF "_snprintf"
# define __IMP__STSCANF "sscanf"
#endif
// irgendwo in der Programminitialisierung:
HINSTANCE hLibMSVCRT=LoadLibraryA("MSVCRT.DLL");
(FARPROC)_stprintf=GetProcAddress(hLibMSVCRT,__IMP__STPRINTF);
(FARPROC)_stscanf=GetProcAddress(hLibMSVCRT,__IMP__STSCANF);
Oftmals will der Linker eine Funktion __ftol oder __ftol2_sse. Siehe hier:
long _declspec(naked) _cdecl _ftol(void){ _asm{
push eax
fistp dword ptr [esp]
fwait
pop eax
ret
}}
shlwapi.dll war da schon ein kleiner
Durchbruch, lieferte diese doch endlich die fehlenden String-Funktionen nach.
Immer noch fehlen Funktionen für Gleitkomma-
Das fehlende MSVC enthält eher einen RC-Zerstörer als einen -Editor.
Deshalb ein
Mehrsprachen-Ressourcen scheinen nie ordentlich zu funktionieren.
So beschweren sich Amerikaner, dass meine zweisprachigen (englisch+deutsch)
Programme selbst unter XP überraschend in Deutsch erscheinen.
Ich habe hierzu keine gute Lösung.
Vielleicht Windows Commander zum Vorbild nehmen und nur Texte übersetzen?
Das Problem hierbei sind Dialoge im Rechts-nach-Links-Stil (arabisch).
Damit hat es Microsoft wieder mal geschafft, gestandene Programmierer
so richtig zu ärgern. Und alte Programme alt aussehen zu lassen.
Für alle, die nicht wissen, worum es geht:
Gewöhnliche, nicht speziell für Windows XP geschriebene Programme bekommen
bei aktivierter „Teletubbie-Optik“ zwar einen schicken Fensterrahmen
(über Geschmack lässt sich streiten), aber das Dialog-Innere sieht altbacken
aus.
Es sei denn, man legt neben die Echse eine „dateiname.exe.manifest“-Datei
mit magischem Inhalt. Oder steckt diese in die Ressource:
Typ:
Zeile in projektname.RC-Datei
Der Inhalt der Manifest-Datei ist immer der gleiche:
Was auch immer Microsoft mit Manifest-Dateien eigentlich wollte (irgendwas
mit C# und Microsoft Forms, ich habe es nicht verstanden), ich stecke jetzt immer
eine Manifest-Datei ins Projekt und stecke sie in die Ressource.
Damit sind die Echsen etwas größer, sehen aber modern aus.
Für jemanden, der Teletubbie-Optik nie eingeschaltet hat
(oder wie ich Windows 2000 oder noch älter benutzt), nur sinnlose Luft.
Besonders unverständlich für mich ist die Auswirkung der Namespace-Elemente (
Der Schalter
Bei einer 64-bit-DLL kann man sich das Beschaffen des Einsprungpunktes
zur Laufzeit ersparen, da es 64-bit-Editionen von Windows erst ab Windows XP gibt.
Eigenschaftsseiten verlangen ein besonderes Vorgehen:
Zu bedenken ist, dass das Hervorheben bestimmter Dialogelemente durch Farbe,
insbesondere bei Checkboxen, nicht mehr mittels
Inzwischen ist mir der Sinn von Manifests (zumindest der
SetWorldTransform) und bei GDI+ für exakte Positionierung
kantengeglätteter Figuren äußerst nützlich ist.
Kopfdateien und defines
WIN32_LEAN_AND_MEANlässt einigen unnötigen Windows-3.0-Ramsch weg.
Nicht verwenden bei Verwendung von GDIplus!
STRICTSchaltet die strenge Typprüfung auch bei Win16 ein.
Wichtig für den klassischen Bug bei ReleaseDC: Man konnte
HWND und HDC vertauschen, und irgendwann (viel später)
krachte es im GDI, weil die Handles alle waren.
<windowsx.h>Nützliche Makros, ersparen eine Unmenge
SendMessage, und man kann kombinierte Win16/Win32-Quelltexte schreiben
Makros
T("string")wie TEXT("string"),
erzeugt je nach UNICODE "string" oder L"string"
elemof(array)liefert Anzahl der Elemente eines Arrays
(Feldes) bekannter Länge, bspw. die Anzahl der Zeichen eines TCHAR-Feldes
nobreakexpandiert zu nichts und dient zur Kennzeichnung
absichtlich „vergessener“ break bei switch + case
Funktionen
InitStructFüllt eine Struktur mit Nullen, aber das erste UINT
mit der Längenangabe. Zweckmäßig für eine große Zahl von Windows-Funktionen.
EnableDlgItemVermählung von EnableWindow und GetDlgItem
ShowDlgItemVermählung von ShowWindow und GetDlgItem
GetRadioCheckDas Gegenteil von CheckDlgButton
SetCheckboxGroupBitfeld in eine Reihe von Auswahlfeldern setzen
GetCheckboxGroupBitfeld aus einer Reihe von Auswahlfeldern lesen
SetEditFocussollte aufgerufen werden, wenn bei WM_COMMAND - IDOK
ein fehlerhaftes Eingabefeld vorgefunden wird: der Text wird markiert
MoveRectIntoRect
MoveRectIntoFullScreen(Fenster-)Rechteck
in ein anderes (Bildschirm-)Rechteck schieben, zur Verhinderung deplatzierter Fenster.
(Wer kennt nicht die Programme mit klassischen Bugs?
Win3.1 CLOCK.EXE, um mal anzufangen, WinAmp…)
LimitInteger-Zahl zwischen zwei Grenzen eingrenzen
iitrafoInteger-Zahl „transformieren“, bspw. Messwert nach Zweipunktabgleich
bsf,bsr„bit scan forward“, „bit scan backward“,
so heißen die Assemblerbefehle, die hier nachgebildet werden:
nieder- bzw. höchstwertigstes Bit suchen
vMBox, MBoxMessageBox mit String aus Resource und statischem Titel
GetFileNameExtZeiger auf Datei-Erweiterung liefern,
funktioniert auch bei Unix-Dateien wie .login (keine Erweiterung!)
PathFindExtension() ist leider unbrauchbar.
typeof-Makro kann ich leider nicht nachliefern…
Ressourcen - ein ewiges Drama mit MSVC
AWK-Skript,
diese Dateien wieder menschenlesbar zu machen.
Für meine Web-Seiten, und für kleinere .RC-Dateien:
nice_rc.zip
Manifest-Dateien und -Ressourcen
Marx hat das Manifest erfunden, dachte ich so(?) - und Microsoft erfindet
es einfach noch einmal. In Form von verballhorntem XML.
Wie damals schon beim Übergang von 16-bit-Programmen nach Windows95.
Als Heerscharen von Programmieren damit beschäftigt waren,
OFN_LONGNAMES – äh, 0x200000L – in die Flags bei
GetOpenFileName nachzureichen.
Und DS_3DLOOK – äh, 0x0004 – in jede
Dialogresource. Und so weiter.
RT_MANIFEST = 24 (nicht als Text stehen lassen!),
ID: CREATEPROCESS_MANIFEST_RESOURCE_ID = 1.
Womit die Echse zwangsläufig größer wird.
Eigentlich nur für ein einziges Bit: „Mach mich schick.“
1 24 projektname.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
Den sonst üblichen Programmnamen und die Programmbeschreibung kann man weglassen.
Mit processorArchitecture="*" hält man sich 64-bit-Probleme vom Hals.
xmlns).
Kann man den Quatsch vielleicht weglassen, oder spinnt dann irgendeins der Microsoft-Betriebssysteme?
Besonderheiten ab Vista
Ab Windows Vista und 7 sind noch weitere must-have-Einträge
hinzu gekommen. Macht zusammen 1 Kilo Manifest trotz dieser dichten Packung.
Für gesittete Programme (die keine Admin-Rechte benötigen) ist am besten folgendes
(habe ich teilweise vom Total Commander abgeguckt):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"><security><requestedPrivileges><requestedExecutionLevel level="asInvoker" uiAccess="false"/></requestedPrivileges></security></trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"><application><supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/><supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/></application></compatibility>
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"><asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"><dpiAware>true</dpiAware></asmv3:windowsSettings></asmv3:application>
<dependency><dependentAssembly><assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" /></dependentAssembly></dependency></assembly>
Der beliebte Registry-Schlüssel HKEY_LOCAL_MACHINE ist praktisch tabu.
Dieser Schlüssel wird für Programme mit normalen Benutzerrechten weg-virtualisiert,
und die Daten gelangen an eine unschöne, schwer aufzufindende Stelle.
Für Ordnung in der Registrierung sollte man daher alle Einstellungen schön brav
nach HKEY_CURRENT_USER speichern.
Maschinenspezifische Einstellungen wie etwa die Portadresse einer
Schnittstellenkarte lassen sich faktisch nur sehr umständlich
(in einer INI-Datei; das Programme-Verzeichnis kann aber schreibgeschützt sein) speichern,
für Ordnung und Einfachheit tue ich's ebenfalls zu HKEY_CURRENT_USER.
Diese Einstellungen sind
„umziehend“,
was bspw. für Portadressen Quatsch ist.
Hier hilft eventuell das (missbräuchliche?) Speichern nach HKEY_CLASSES_ROOT
als benutzerspezifische, aber nicht „umziehende“ Daten.
Manifest-Ressourcen für DLLs, genauer: Eigenschaftsseiten-Lieferanten („Property Sheet Provider“)
Im konkreten Fall ging es darum, eine Erweiterung für MMC.EXE,
genauer, dem Gerätemanager
(Aufruf einfach mit devmgmt.msc)
modern aussehen zu lassen.
Laut Microsoft-Dokumentation muss man wie folgt vorgehen:
Lässt man auch nur einen Schritt weg, bleibt alles altbacken.
Warum eigentlich dieses Mehr an Aufwand? Ist mir total unverständlich.
RT_MANIFEST-Ressource mit dem numerischen
Bezeichner ISOLATIONAWARE_MANIFEST_RESOURCE_ID = 2 an
#define ISOLATION_AWARE_ENABLED 1
Alt und neu zusammen in einem Fenster sieht eben nicht toll aus!
Man beachte vor allem die unterschiedlichen Knöpfe
ISOLATION_AWARE_ENABLED bewirkt ein Aufblähen
des Kodes und den Zwang zur Laufzeitbibliothek!
Dies wird seitens Microsoft über *.inl-Dateien erreicht,
das sind Kopfdateien mit einer Menge Inline-Code.
Dieser Inline-Code ist absichtlich kryptisch gehalten
und damit kaum lesbar!
Außerdem werden so unsinnige Sachen gemacht wie UNICOWS.DLL
zu laden.
UNICOWS.DLL ist die Hauptdatei für Unicode-Unterstützung
unter Windows 98/Me, aber diese Systeme haben mit Manifest-Ressourcen
eh' nichts am Hut!ISOLATION_AWARE_ENABLED ist also für mich
völlig inakzeptabel, und so habe ich mich so lange durch die
kryptischen *.inl-Dateien gewurstelt,
bis nur noch der wirklich benötigte Kode übrig blieb.
Das ist nun erfreulich wenig.
Einschalten der Teletubbie-Optik
Irgendwie braucht man dazu immer einen Aktivierungskontext hActCtx.
Beschaffen des Aktivierungskontextes
HANDLE hActCtx; // „Aktivierungskontext“, das Ding mit der Teletubbie-Optik
BOOL APIENTRY _DllMainCRTStartup(HANDLE hModule, DWORD reason, LPVOID lpReserved) {
switch (reason) {
case DLL_PROCESS_ATTACH: {
ACTIVATION_CONTEXT_BASIC_INFORMATION actCtxBasicInfo;
BOOL (WINAPI*pQueryActCtxW)(DWORD,HANDLE,PVOID,ULONG,PVOID,SIZE_T,SIZE_T*);
HANDLE hKernel;
hInst=(HINSTANCE)hModule;
DisableThreadLibraryCalls(hModule);
InitCommonControls();
hKernel=GetModuleHandle(T("KERNEL32.dll"));
(FARPROC)pQueryActCtxW=GetProcAddress(hKernel,"QueryActCtxW");
if (pQueryActCtxW
&& pQueryActCtxW( // ziemlich undokumentiert
QUERY_ACTCTX_FLAG_ACTCTX_IS_ADDRESS|QUERY_ACTCTX_FLAG_NO_ADDREF,
&hActCtx, // irgendeine Adresse
NULL,
ActivationContextBasicInformation,
&actCtxBasicInfo,
sizeof(actCtxBasicInfo),
NULL)
&& actCtxBasicInfo.hActCtx!=INVALID_HANDLE_VALUE)
hActCtx=actCtxBasicInfo.hActCtx; // <hActCtx> nie freigeben, da ohne AddRef!
}break;
}
return TRUE;
}
Der dynamische Einsprungpunkt via GetProcAddress() wird benötigt,
damit die DLL auch unter Windows 2000 ladbar ist.
Der Trick bei Win16, mit
Wo (und ob?) UndefDynLink
zur Laufzeit zur vergleichen, klappt bei Win32 nicht.InitCommonControls() aufgerufen wird, ist irrelevant.
Beim Debuggen sieht man, dass InitCommonControls() zu nichts weiter als einem
ret-Befehl führt.
(Ja, gibt es! Wenn auch nur in englisch. Die meisten kennen das erst ab Windows Vista.)
Aktivieren der Teletubbie-Optik
Normalerweise kann man die Aktivierung mittels ActivateActCtx()
während des Programmlaufs bewirken.
Das klappt aber nicht mit Eigenschaftsseiten („Property Sheets“)!
Da verreckt irgendwo MMC.exe, der Gerätemanager.
HPROPSHEETPAGE hpage;
PROPSHEETPAGE page;
… // die normale Initialisierung
page.dwFlags = … |PSP_USEFUSIONCONTEXT;
page.hActCtx = hActCtx; // Die erweiterte PROPSHEETPAGE-Struktur steckt in der Kopfdatei des XP-SDKs oder -DDKs
hpage = CreatePropertySheetPage(&page);
Die übrigen Fenster, wie Unter-Dialoge oder Messageboxen
bekommen automatisch Teletubbie-Optik, da braucht man sich nicht mehr
zu kümmern.
Keine Wrapper mit ActivateActCtx() und DeactivateActCtx().
Und, das ist sehr nett, keine weiteren dynamischen Einsprungpunkte.
SetTextColor()
innerhalb von WM_CTLCOLORSTATIC möglich ist.
Die einfachste Lösung ist es, derartige Dialogelemente mittels
SetWindowTheme() in ihr altes Aussehen zurückzuverwandeln.
<dependency>-Abschnitt) etwas klar geworden:
WINDOWS\SYSTEM32)
und mindestens eine „neue“ für XP-Stil (in – festhalten –
WINDOWS\WinSxS\x86_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.0.0_x-ww_1382d70a
Damit wollte man wohl das Problem der
DLL-Hölle angreifen.
CreateProcess()
und LoadLibrary()) entscheidet
anhand der Manifest-Ressource oder -Datei, die in bzw. neben der
zu ladenden .EXE/.DLL liegt, welche Version von COMCTL32.dll geladen und dynamisch gelinkt wird.
Dies kann man im modernen Dependency Walker sehen,
der auch die Pfade ausspuckt.
CREATEPROCESS_MANIFEST_RESOURCE_ID.
PSP_USEFUSIONCONTEXT und Member hActCtx nicht gesetzt).
hActCtx).
Wahrscheinlich können DLLs Teletubbie-Optik benutzen, auch wenn das
Wirtsprogramm ohne ausgestattet ist; dazu muss CreateActCtx() aufgerufen werden.
Wenn das Wirtsprogramm (hier: MMC.exe) mit Teletubbie-Optik
ausgestattet ist, darf CreateActCtx() nicht
aufgerufen werden (Absturz!!).
Stattdessen muss QueryActCtxW() mit undokumentierten
Parametern und Schaltern aufgerufen werden.
Henrik Haftmann, 09.03.2012