Source file: /~heha/hs/spe.zip/src/SPE.cpp

/* Serial Printer Emulator:
 * emulates a serial printer on a selectable COM port
 * (port settings are adjustable too),
 * and redirects it to a Windows printer (even selectable)
 *
 * Emulation eines seriellen Druckers:
 * emuliert einen Drucker an einem auswählbaren COM-Port
 * (Anschlusseinstellungen sind ebenfalls auswählbar),
 * und leitet den Datenstrom zu einem (auch auswählbaren) Windows-Drucker
 *
 * Mit einem (käuflichen) Parallel-zu-Seriell-Umsetzer kann man so auch
 * Druckausgaben von Parallelports auffangen.
 * Für COM-Port-lose Rechner funktionieren auch USB-Seriell-Konverter.
 *
 * Siehe http://www.tu-chemnitz.de/~heha/hs/mein_msvc.htm

+140430	Voreinstellbare DIP-Schalter für ESC/P, noch ohne Dialog
	ESC "R"-Voreinstellung je nach LocaleInfo (einige europäische Länder)
	SetupDi-basierte COM-Port-Auflistung (>COM32)
+150818	Dialog mit UpDownControl; Sound-Test beim Rechtsklick auf "..."-Knopf
-150818	Dialog mit Ausgabedatei- oder Ausgabeprozess-Auswahl
*150818	Konfigurations-Speicherort HKCU statt HKLM; x64-Echse; Signierung
*150819	setjmp+longjmp statt Catch+Throw; SEH geht auch aber frisst mehr Platz.
	C++-Exceptions gehen nicht (mit msvcrt-light.lib) zu übersetzen.
-221008	Restrukturierung DCB laden und speichern
	(fehlendes GetCommDCB() kann zu Fehlern führen)
	Keine Codesignatur mehr da eigenes Zertifikat abgelaufen
	ShellExecute statt sndPlaySound wenn Endung != WAV

TODO:
 Voreinstellungs-Dialog (crlf-Handling, Seitengröße usw.) für ESC/P
 Grafikausgabe (sofern Bedarf besteht und Testdatei vorliegt)

 */
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <windowsx.h>
#include <commctrl.h>
#include <commdlg.h>
#include <shlwapi.h>
#include <mmsystem.h>
#include <setupapi.h>
#include <devguid.h>
#include <winspool.h>
#include <shellapi.h>
#include "escp.h"

#define T(x) TEXT(x)		// Schreibfaulheit...
#define elemof(x) (sizeof(x)/sizeof(*(x)))	// Elemente eines Arrays
#define nobreak			// expliziter Durchlauf bei switch/case

#if (_MSC_VER <= 1200)	// missing common control definitions (for split button)
struct NMBCDROPDOWN{
 NMHDR   hdr;
 RECT    rcButton;
};
#define BCN_DROPDOWN	0xFFFFFB20
#define BS_SPLITBUTTON	0x0000000C
#endif

void _fastcall InitStruct(LPVOID p, UINT len) {
 LPUINT pp=(LPUINT)p;
 *pp=len; len/=sizeof(UINT); len--;
 if (len) do *++pp=0; while (--len);
}

TCHAR MBoxTitle[64];

int  vMBox(HWND Wnd, UINT id, UINT style, va_list arglist) {
 TCHAR buf1[256],buf2[1024];
 LoadString(0,id,buf1,elemof(buf1));
 wvnsprintf(buf2,elemof(buf2),buf1,arglist);
 return MessageBox(Wnd,buf2,MBoxTitle,style);
}

int _cdecl MBox(HWND Wnd, UINT id, UINT style,...) {
 return vMBox(Wnd,id,style,(va_list)(&style+1));
}

static TCHAR ModeString[64]=T("COM1 9600,n,8,1");	// idiotischerweise kommt Parität vor Datenbits
/* Wirkung der Suffixe ",p" (Hardware-Protokoll), ",x" (XON/XOFF-Protokoll)
   und keiner (ohne Protokoll) bei BuildCommDCB(ModeString,&DCB):
Suffix		",p"	",x"	ohne
fOutxCtsFlow	1	0	0
fOutxDsrFlow	1	0	0
fDtrControl	2	1	1
fRtsControl	2	1	1
fOutX		0	1	0
fInX		0	1	0
*/
struct{
 WORD TimeOutMs;	// TimeOut zur Druckjob-Beendigung in ms
 BYTE PrintFilter;	// 0==RAW, 1==ESC/P, 2==ESC/P2, 3==Dateiausgabe, 4==Standardeingabe für Prozess
 BYTE keepdef;		// Veränderungen durch Druckersteuerkommandos persistent halten
			// 1 = Zwischen Einzelaufträgen
			// 2 = Zusätzlich zwischen Neustarts von SPE.exe (also Reboots)
 POINTS PageOffset;	// Verschiebung der Druckdaten auf dem Papier in mm
 DEFAULTS def;		// DIP-Schalter (Mäuseklavier), siehe <escp.h>
}Config;
COMMTIMEOUTS to;
static TCHAR StartSound[MAX_PATH],EndSound[MAX_PATH];
static TCHAR PrinterName[MAX_PATH];

//Hilfsfunktion
static LONG RegQueryValueExL(HKEY hKey,LPCTSTR Tag,PVOID pValue,DWORD Len) {
 return RegQueryValueEx(hKey,Tag,NULL,NULL,(LPBYTE)pValue,&Len);
}

static TCHAR Key0[]=T("Software\\h#s\\SPE");
static TCHAR Key1[]=T("Port");
static TCHAR Key2[]=T("PrinterName");
static TCHAR Key3[]=T("StartSound");
static TCHAR Key4[]=T("EndSound");
static TCHAR Key5[]=T("Config");

static void LoadSettings() {
 HKEY hKey;
// Wenns mit Laden schief geht, wenigstens sinnvoll initialisieren
 Config.TimeOutMs=5000;
// Schlüssel-Zweig öffnen
 if (RegOpenKeyEx(HKEY_CURRENT_USER,Key0,0,KEY_QUERY_VALUE,&hKey)) return;
 RegQueryValueExL(hKey,Key1,ModeString,sizeof ModeString);
 RegQueryValueExL(hKey,Key2,PrinterName,sizeof PrinterName);
 RegQueryValueExL(hKey,Key3,StartSound,sizeof StartSound);
 RegQueryValueExL(hKey,Key4,EndSound,sizeof EndSound);
 RegQueryValueExL(hKey,Key5,&Config,sizeof(Config));
 RegCloseKey(hKey);
}

//Hilfsfunktion
static LONG RegSetValueExSZ(HKEY hKey,LPCTSTR Tag,LPCTSTR Value) {
 if (!Value || !*Value) return RegDeleteValue(hKey,Tag);
 else return RegSetValueEx(hKey,Tag,0,REG_SZ,
   (PBYTE)Value,(lstrlen(Value)+1)*sizeof(TCHAR));
}

static void SaveSettings() {
 HKEY hKey;
 DWORD dispo;
// Schlüssel-Zweig öffnen
 if (RegCreateKeyEx(HKEY_CURRENT_USER,Key0,0,
   NULL,REG_OPTION_NON_VOLATILE,KEY_SET_VALUE,NULL,&hKey,&dispo)) return;
// eine Beschreibung schreiben (damit der Anwender weiß, worum es geht)
 RegSetValueExSZ(hKey,0,MBoxTitle);
 RegSetValueExSZ(hKey,Key1,ModeString);
 RegSetValueExSZ(hKey,Key2,PrinterName);
 RegSetValueExSZ(hKey,Key3,StartSound);
 RegSetValueExSZ(hKey,Key4,EndSound);
 RegSetValueEx(hKey,Key5,0,REG_BINARY,(PBYTE)&Config,sizeof(Config));
 RegCloseKey(hKey);
}

static void EnabOffsets(HWND Wnd) {
 BOOL ena=(unsigned)(Config.PrintFilter-1)<2;
 EnableWindow(GetDlgItem(Wnd,22),ena);
 EnableWindow(GetDlgItem(Wnd,122),ena);
 EnableWindow(GetDlgItem(Wnd,23),ena);
 EnableWindow(GetDlgItem(Wnd,123),ena);
 int show=Config.PrintFilter<3?SW_SHOW:SW_HIDE;
 ShowWindow(GetDlgItem(Wnd,13),show);	// Combobox
 show^=SW_SHOW^SW_HIDE;			// umkehren
 ShowWindow(GetDlgItem(Wnd,24),show);	// Edit
 ShowWindow(GetDlgItem(Wnd,25),show);	// Button
 TCHAR s[64],*sp=s;
 s[LoadString(0,6,s,elemof(s)-1)+1]=0;
 for (show=2;show<Config.PrintFilter;sp+=lstrlen(sp)+1) show++;
 SetDlgItemText(Wnd,21,sp);
}

static WNDPROC DefButtonProc;
static void DoStartSound();
static void DoEndSound();

// Ungefähr das Verhalten eines SplitButtons nachmachen, für Windows XP.
// Optisch wird nichts verändert.
static LONG_PTR CALLBACK NotifyProc(HWND Wnd, UINT msg, WPARAM wParam, LPARAM lParam) {
 switch (msg) {
  case WM_GETDLGCODE: return CallWindowProc(DefButtonProc,Wnd,msg,wParam,lParam)|DLGC_WANTARROWS;
  case WM_KEYDOWN: if (wParam!=VK_DOWN) break; else nobreak; // seltsamerweise geht auch Cursor Hoch!
  case WM_CONTEXTMENU: {
   NMBCDROPDOWN h;
   h.hdr.hwndFrom=Wnd;
   h.hdr.idFrom=GetDlgCtrlID(Wnd);
   h.hdr.code=BCN_DROPDOWN;
   GetWindowRect(Wnd,&h.rcButton);
   MapWindowPoints(0,Wnd,(POINT*)&h.rcButton,2);
   SendMessage(GetParent(Wnd),WM_NOTIFY,h.hdr.idFrom,(LPARAM)&h);
  }return 0;
 }
 return CallWindowProc(DefButtonProc,Wnd,msg,wParam,lParam);
}

static void MkSplitButton(HWND Wnd) {
 if (LOBYTE(GetVersion())>=6) {
  DWORD style=GetWindowLong(Wnd,GWL_STYLE);
  style|=BS_SPLITBUTTON;
  SetWindowLong(Wnd,GWL_STYLE,style);
 }else{
  DefButtonProc=SubclassWindow(Wnd,NotifyProc);
 }
}

static TCHAR stPortName[]=T("PortName");

static INT_PTR CALLBACK SetupDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
  case WM_INITDIALOG: {
   TCHAR s[256],*sp;
   HWND w;
   CreateUpDownControl(WS_VISIBLE|WS_CHILD|WS_BORDER|UDS_ALIGNRIGHT|UDS_ARROWKEYS|UDS_HOTTRACK|UDS_NOTHOUSANDS|UDS_SETBUDDYINT,
     0,0,0,0,Wnd,112,0,GetDlgItem(Wnd,12),1000,0,Config.TimeOutMs/1000);
   SetDlgItemText(Wnd,16,StartSound);
   SetDlgItemText(Wnd,18,EndSound);
   s[LoadString(0,5,s,elemof(s)-1)+1]=0;
   w=GetDlgItem(Wnd,20);	// Combobox
   for(sp=s;*sp;sp+=lstrlen(sp)+1) {
    ComboBox_AddString(w,sp);	// Combobox füllen
   }
   ComboBox_SetCurSel(w,Config.PrintFilter);
   CreateUpDownControl(WS_VISIBLE|WS_CHILD|WS_BORDER|UDS_ALIGNRIGHT|UDS_ARROWKEYS|UDS_HOTTRACK|UDS_NOTHOUSANDS|UDS_SETBUDDYINT,
     0,0,0,0,Wnd,122,0,GetDlgItem(Wnd,22),500,-500,Config.PageOffset.x);
   CreateUpDownControl(WS_VISIBLE|WS_CHILD|WS_BORDER|UDS_ALIGNRIGHT|UDS_ARROWKEYS|UDS_HOTTRACK|UDS_NOTHOUSANDS|UDS_SETBUDDYINT,
     0,0,0,0,Wnd,123,0,GetDlgItem(Wnd,23),500,-500,Config.PageOffset.y);
   SetDlgItemText(Wnd,24,PrinterName);
   MkSplitButton(GetDlgItem(Wnd,17));
   MkSplitButton(GetDlgItem(Wnd,19));
   EnabOffsets(Wnd);
   SendMessage(Wnd,WM_TIMER,1,0);
  }return TRUE;

  case WM_DEVICECHANGE: SetTimer(Wnd,1,2500,NULL); break;

  case WM_TIMER: if (wParam==1) {
   KillTimer(Wnd,wParam);
// serielle Schnittstellen (neu) listen (bei jedem WM_DEVICECHANGE bspw. für USB)
   HWND hCombo=GetDlgItem(Wnd,10);
   ComboBox_ResetContent(hCombo);
   HANDLE devs=SetupDiGetClassDevs(&GUID_DEVCLASS_PORTS,NULL,0,DIGCF_PRESENT);
   if (devs!=INVALID_HANDLE_VALUE) {
    DWORD ComNr=StrToInt(ModeString+3);	// Einsbasierte Ist-Portnummer
    SP_DEVINFO_DATA devInfo;
    devInfo.cbSize=sizeof devInfo;
    for (DWORD i=0; SetupDiEnumDeviceInfo(devs,i,&devInfo); i++) {
     HKEY hKey;
     TCHAR s[16];
     *s=0;
     if ((hKey=SetupDiOpenDevRegKey(devs,&devInfo,DICS_FLAG_GLOBAL,0,DIREG_DEV,KEY_READ))
       ==INVALID_HANDLE_VALUE) continue;
     RegQueryValueExL(hKey,stPortName,s,sizeof s);
     RegCloseKey(hKey);
     if (*s=='C') {		// Fehlschläge und LPTx ausfiltern
      int idx=ComboBox_AddString(hCombo,PTSTR(s));
      DWORD num=StrToInt(s+3);	// auch hier: einsbasiert
      ComboBox_SetItemData(hCombo,idx,num);
      if (num==ComNr) ComboBox_SetCurSel(hCombo,idx);
     }
    }
    SetupDiDestroyDeviceInfoList(devs);
   }
// Drucker (neu) listen (richtig bei WM_DEVICECHANGE?)
   DWORD NeedBytes=0,ArrLen,LastError;
   PRINTER_INFO_5 *pPI=NULL;
   hCombo=GetDlgItem(Wnd,13);
   ComboBox_ResetContent(hCombo);
   do{	// Endlosschleife, um dem Multitasksystem wechselnde Drucker zu erlauben
    LastError=0;
    if (EnumPrinters(PRINTER_ENUM_LOCAL|PRINTER_ENUM_CONNECTIONS,
      NULL,5,(LPBYTE)pPI,NeedBytes,&NeedBytes,&ArrLen)) break;
    LastError=GetLastError();
    if (pPI) LocalFree(pPI);
    pPI=(PRINTER_INFO_5*)LocalAlloc(LPTR,NeedBytes);
   }while(pPI && LastError==ERROR_INSUFFICIENT_BUFFER);
   if (!pPI || LastError) break;
   for(UINT i=0; i<ArrLen; i++) {
    int idx=ComboBox_AddString(hCombo,pPI[i].pPrinterName);
    if (!lstrcmpi(pPI[i].pPrinterName,PrinterName))
      ComboBox_SetCurSel(hCombo,idx);
   }
   LocalFree(pPI);
  }break;
  
  case WM_NOTIFY: {
   NMHDR*h=(NMHDR*)lParam;
   if (h->code==BCN_DROPDOWN) {
    NMBCDROPDOWN*h=(NMBCDROPDOWN*)lParam;
    HMENU m=CreatePopupMenu();
    TCHAR s[32];
    LoadString(0,9,s,elemof(s));
    AppendMenu(m,MF_STRING,2,s);
    MapWindowPoints(h->hdr.hwndFrom,0,(POINT*)&h->rcButton,2);
    if (TrackPopupMenu(m,TPM_LEFTALIGN|TPM_TOPALIGN|TPM_RETURNCMD|TPM_RIGHTBUTTON,
      h->rcButton.left,h->rcButton.bottom,0,Wnd,NULL)==2) switch (h->hdr.idFrom) {
     case 17: {
      GetDlgItemText(Wnd,16,StartSound,elemof(StartSound));
      DoStartSound();
     }break;
     case 19: {
      GetDlgItemText(Wnd,18,EndSound,elemof(EndSound));
      DoEndSound();
     }break;
    }
    DestroyMenu(m);
   }
  }break;

  case WM_COMMAND: switch (LOBYTE(wParam)) {
   case 10: switch (GET_WM_COMMAND_CMD(wParam,lParam)) {
    case CBN_SELCHANGE: {
     TCHAR buf[elemof(ModeString)], *rest=StrChr(ModeString,' ');
     if (rest) ++rest;
     wnsprintf(buf,elemof(buf),T("COM%u %s"),
       ComboBox_GetItemData((HWND)lParam,ComboBox_GetCurSel((HWND)lParam)),
       rest);
     StrCpyN(ModeString,buf,elemof(ModeString));
    }break;
   }break;

   case 11: {
    TCHAR ComName[8];
    wnsprintf(ComName,elemof(ComName),T("COM%u"),StrToInt(ModeString+3));
    COMMCONFIG cc;
    InitStruct(&cc,sizeof cc);
    cc.dcb.DCBlength=sizeof cc.dcb;
    BuildCommDCB(ModeString,&cc.dcb);
    if (CommConfigDialog(ComName,Wnd,&cc)) {
     static const TCHAR*stopbits[]={T("1"),T("1.5"),T("2")};
     const TCHAR*proto=T("");
     if (cc.dcb.fInX) proto=T(",x");
     else if (cc.dcb.fOutxDsrFlow) proto=T(",p");
     wnsprintf(ModeString,elemof(ModeString),
       T("%s %u,%c,%u,%s%s"),
       ComName,
       cc.dcb.BaudRate,
       "noems"[cc.dcb.Parity],		// no/odd/even/mark/space
       cc.dcb.ByteSize,
       stopbits[cc.dcb.StopBits],	// 1.5 gibt's praktisch nie
       proto
     );
    }
   }break;

   case 13: switch (GET_WM_COMMAND_CMD(wParam,lParam)) {
    case CBN_SELCHANGE: {
     ComboBox_GetLBText((HWND)lParam,ComboBox_GetCurSel((HWND)lParam),PrinterName);
    }break;
   }break;

   case 17:
   case 19:	// Dateiauswahl für WAV-Dateien
   case 25: {	// Dateiauswahl für PRN-Datei oder ausführbare Datei
    int strid=3;
    OPENFILENAME ofn;
    TCHAR FileName[MAX_PATH];
    TCHAR Filter[256];
    wParam--;
    InitStruct(&ofn,sizeof(ofn));
    ofn.lpstrFile=FileName;
    ofn.nMaxFile=elemof(FileName);
    ofn.lpstrFilter=Filter;
    ofn.Flags=OFN_HIDEREADONLY|OFN_FILEMUSTEXIST;
    if (wParam==24) {
     strid=Config.PrintFilter-3+7;
     if (Config.PrintFilter==3) ofn.Flags=OFN_HIDEREADONLY|OFN_PATHMUSTEXIST;
    }
    Filter[LoadString(0,strid,Filter,elemof(Filter)-1)+1]=0;
    GetDlgItemText(Wnd,(int)wParam,FileName,elemof(FileName));
    if ((ofn.Flags&OFN_FILEMUSTEXIST?GetOpenFileName:GetSaveFileName)(&ofn)) {
     SetDlgItemText(Wnd,(int)wParam,FileName);
    }
   }break;

   case 20: switch (GET_WM_COMMAND_CMD(wParam,lParam)) { // Druckerfilter
    case CBN_SELCHANGE: {
     Config.PrintFilter=ComboBox_GetCurSel((HWND)lParam);
     EnabOffsets(Wnd);	// Dialog verändern
    }break;
   }break;

   case IDOK: {
    BOOL ok;
    UINT id;
    HWND hWndItem=GetDlgItem(Wnd,10);
    if (ComboBox_GetCurSel(hWndItem)<0) {
     SetFocus(hWndItem);
     break;	// nicht OK!
    }
    if ((int)Config.PrintFilter<0) {
     SetFocus(GetDlgItem(Wnd,20));
     break;	// nicht OK!
    }
    if (Config.PrintFilter<3) {
     hWndItem=GetDlgItem(Wnd,13);
     if (ComboBox_GetCurSel(hWndItem)<0) {
      SetFocus(hWndItem);
      break;	// nicht OK!
     }
    }else GetDlgItemText(Wnd,24,PrinterName,elemof(PrinterName));
    id=22;
    Config.PageOffset.x=GetDlgItemInt(Wnd,id,&ok,TRUE);
    if (!ok) goto editfail;	// nicht OK!
    id=23;
    Config.PageOffset.y=GetDlgItemInt(Wnd,id,&ok,TRUE);
    if (!ok) goto editfail;	// nicht OK!
    id=12;
    Config.TimeOutMs=GetDlgItemInt(Wnd,id,&ok,FALSE)*1000;
    if (!Config.TimeOutMs) {	// Fehler, muss >0 sein
editfail:
     hWndItem=GetDlgItem(Wnd,id);
     Edit_SetSel(hWndItem,0,-1);
     SetFocus(hWndItem);
     break;	// kein EndDialog()
    }
    GetDlgItemText(Wnd,16,StartSound,elemof(StartSound));
    GetDlgItemText(Wnd,18,EndSound,elemof(EndSound));
   }nobreak;
   case IDCANCEL: {
    EndDialog(Wnd,LOWORD(wParam));
   }break;
  }break;
 }
 return FALSE;
}

// Wie Windows NT auf allen Windows-Versionen (da fehlt ein shlwapi.Beep?)
#ifdef _M_IX86
static BOOL MyBeep(DWORD dwFreq, DWORD dwDuration) {
 if ((long)GetVersion()>=0) return Beep(dwFreq,dwDuration);
 else{	// Win32s, Win9x/Me
  if (dwFreq<1843200L/0x10000/2) return FALSE;
  dwFreq=1843200L/2/dwFreq;	// Teilerwert
  _asm{
	cli
	mov	al,0xB6	// Timer2: Rechteckgenerator, 16bit
	out	0x43,al
	mov	eax,dwFreq
	out	0x42,al	// Timer2: Endwert Low-Teil setzen
	xchg	ah,al
	out	0x42,al	// Timer2: Endwert High-Teil setzen
	in	al,0x61
	or	al,0x03
	out	0x61,al	// Ein- und Ausgang des Timer2 freischalten
	sti
  }
  Sleep(dwDuration);
  _asm{
	cli
	in	al,0x61
	and	al,~0x03
	out	0x61,al	// Ein- und Ausgang des Timer2 sperren
	sti
  }
  return TRUE;
 }
}
#else
#define MyBeep Beep
#endif

static DWORD WINAPI TwoBeep(void*p) {
 return MyBeep(LOWORD(p),200) && MyBeep(HIWORD(p),200);
}

static DWORD WINAPI Exec(void*p) {
 const TCHAR*filename=(const TCHAR*)p;
 return DWORD(ShellExecute(0,0,filename,0,0,SW_SHOWDEFAULT));
}

static bool DoSound(const TCHAR*filename) {
 if (!filename) return false;
 if (!*filename) return false;
// Der Returnwert von sndPlaySound ist immer true auch wenn's keine Audiodatei ist!
// Daher Endung angucken
 if (!StrCmpI(PathFindExtension(filename),T(".wav"))
 && sndPlaySound(filename,SND_ASYNC)) return true;
// ShellExecute kann muddeln. Daher über gesonderten Thread starten
 CreateThread(0,0,Exec,(void*)filename,0,0);
 return true;
}

static void DoStartSound() {
 if (!DoSound(StartSound)) CreateThread(0,0,TwoBeep,(void*)MAKELONG(300,400),0,0);
// Nicht beim Piepsen blockieren, sofort (parallel) Drucker öffnen
}

static void DoEndSound() {
 if (!DoSound(EndSound)) CreateThread(0,0,TwoBeep,(void*)MAKELONG(400,300),0,0);
}

// Enthält der Dateiname einen Stern,
// wird dieser durch eine Dezimalzahl ersetzt und eine neue Datei erzeugt.
// Wenn nicht, wird die Datei zum Anhängen geöffnet.
static HANDLE CreateUniqueOrAppend(PTSTR filename) {
 //_tcsftime()...
 HANDLE ret=0;
 PTSTR p=StrChr(filename,'*');
 if (p) {
  *p=0;					// String dort zerhacken
  for (DWORD i=1;;i++) {
   TCHAR fname[MAX_PATH];
   wnsprintf(fname,elemof(fname),T("%s%u%s"),filename,i,p+1);
   ret=CreateFile(fname,GENERIC_WRITE,FILE_SHARE_READ,NULL,CREATE_NEW,0,0);
   if (ret!=INVALID_HANDLE_VALUE) break;		// Neue Datei erfolgreich erzeugt
   ret=0;
   if (GetLastError()!=ERROR_ALREADY_EXISTS) break;	// sonstige Fehler brechen ab
  }
  *p='*';				// zurück-ersetzen
 }else{
  ret=CreateFile(filename,GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS,0,0);
  if (ret==INVALID_HANDLE_VALUE) ret=0;
  else if (GetLastError()==ERROR_ALREADY_EXISTS) SetFilePointer(ret,0,NULL,FILE_END);
 }
 return ret;
}

int TransferJob(HANDLE hCom) {
#ifndef _DEBUG
 to.ReadTotalTimeoutConstant=0;
 SetCommTimeouts(hCom,&to);
// zunächst unendlich blockieren, bis das erste Zeichen kommt
 DWORD ev;
 ClearCommError(hCom,&ev,NULL);
 SetCommMask(hCom,EV_RXCHAR);
 if (!WaitCommEvent(hCom,&ev,NULL)) return -1;
 SetCommMask(hCom,0);
#endif
 DoStartSound();
#ifndef _DEBUG
// fortan per TimeOut blockieren, um das letzte Zeichen des Druckjobs abzuschätzen
 to.ReadTotalTimeoutConstant=Config.TimeOutMs;
 SetCommTimeouts(hCom,&to);
#endif
 switch (Config.PrintFilter) {
  case 4: {
   STARTUPINFO si;
   PROCESS_INFORMATION pi;
   InitStruct(&si,sizeof(si));
   si.dwFlags=STARTF_USESTDHANDLES;
   si.hStdInput=hCom;
   CreateProcess(NULL,PrinterName,NULL,NULL,TRUE,0,NULL,NULL,&si,&pi);
   WaitForSingleObject(pi.hProcess,INFINITE);
   DoEndSound();
  }break;

  case 3: {	// Dateiausgabe
   HANDLE hFile=CreateUniqueOrAppend(PrinterName);
   if (!hFile) return -2;
   BYTE buffer[1024];
   DWORD br,bw;
   while (ReadFile(hCom,buffer,sizeof(buffer),&br,NULL) && br) {
    if (!WriteFile(hFile,buffer,br,&bw,NULL) || br!=bw) {
     CloseHandle(hFile);
     return -2;
    }
   }
   DoEndSound();
   CloseHandle(hFile);
  }break;

  case 1:
  case 2: {	// Ausgabe mit ESC/P-Interpretation an (GDI-)Drucker
   HDC dc=CreateDC((int)GetVersion()>=0?T("WINSPOOL"):NULL,PrinterName,NULL,NULL);
   if (!dc) goto Ende0;
   DOCINFO di;
   InitStruct(&di,sizeof(di));
   di.lpszDocName=MBoxTitle;
   StartDoc(dc,&di);
   if (!Config.def.ly) ESCP::Default(&Config.def);	// Zeilenabstand Null nicht zulassen!
   ESCP escp;
   escp.Init(&Config.def);			// Vorgaben einsetzen
   escp.Offset.x=MulDiv(Config.PageOffset.x,21600,254);
   escp.Offset.y=MulDiv(Config.PageOffset.y,21600,254);
   escp.mode=Config.PrintFilter;
   escp.f=hCom;
   escp.dc=dc;
   escp.Filter();
   if (Config.keepdef) {
    escp.Save(&Config.def);			// Vorgaben retten
    if (Config.keepdef&2) SaveSettings();	// persistent sogar über Reboots
   }
   EndDoc(dc);
   DeleteDC(dc);
   DoEndSound();
  }break;
  
  case 0: {
   HANDLE hPrinter;
   if (!OpenPrinter(PrinterName,&hPrinter,NULL)) goto Ende0;
   DOC_INFO_1 di;
   InitStruct(&di,sizeof(di));
   di.pDocName=MBoxTitle;
// di.pOutputFile=T("d:\\ttt.prn");	// Test!!
   di.pDatatype=T("RAW");
   if (!StartDocPrinter(hPrinter,1,(LPBYTE)&di)) goto Ende1;
   if (!StartPagePrinter(hPrinter)) goto Ende2;
   BYTE buffer[1024];
   DWORD br,bw;
   while (ReadFile(hCom,buffer,sizeof(buffer),&br,NULL) && br) {
    if (!WritePrinter(hPrinter,buffer,br,&bw) || br!=bw) goto Ende3;
   }
   DoEndSound();
   if (!EndPagePrinter(hPrinter)) goto Ende2;
   if (!EndDocPrinter(hPrinter)) goto Ende1;
   if (!ClosePrinter(hPrinter)) goto Ende0;
   return 0;
Ende3: EndPagePrinter(hPrinter);
Ende2: EndDocPrinter(hPrinter);
Ende1: ClosePrinter(hPrinter);
  }
Ende0: return -2;
 }
 return 0;
}

int WinMainCRTStartup() {
 HANDLE hCom;
 LPTSTR Args;
 Args=PathGetArgs(GetCommandLine());

 LoadString(0,0,MBoxTitle,elemof(MBoxTitle));
// Vorgaben laden
 LoadSettings();
// Benutzerabfrage (bei Notwendigkeit oder Kommandozeilenschalter)
 if (!StrToInt(ModeString+3) || !PrinterName[0] || *Args) {
rep1:
  if (DialogBox(0,MAKEINTRESOURCE(1),0,SetupDlgProc)!=IDOK) ExitProcess(0);
  SaveSettings();
 }
#ifdef _DEBUG
 static TCHAR ComName[]=T("Draft.prn");
#else
 TCHAR ComName[12];
// serielle Schnittstelle öffnen
 wnsprintf(ComName,elemof(ComName),T("\\\\.\\COM%u"),StrToInt(ModeString+3));
#endif
 hCom=CreateFile(ComName,GENERIC_READ|GENERIC_WRITE,0,NULL,OPEN_EXISTING,0,0);
 if (hCom==INVALID_HANDLE_VALUE) switch (MBox(0,4,MB_RETRYCANCEL,StrToInt(ModeString+3))) {
  case IDRETRY: goto rep1;
  default: ExitProcess(3);
 }
#ifndef _DEBUG
 DCB dcb;
 dcb.DCBlength=sizeof dcb;
 GetCommState(hCom,&dcb);
 BuildCommDCB(ModeString,&dcb);
// COMx [baud=b] [parity=p] [data=d] [stop=s] [to={on|off}]
//	[xon={on|off}] [odsr={on|off}] [octs={on|off}] [dtr={on|off|hs}]
//	[rts={on|off|hs|tg}] [idsr={on|off}]
// oder
// COMx baud,parity,data,stop [,[xp]]
 SetCommState(hCom,&dcb);
#endif
 for (;;) switch (TransferJob(hCom)) {
#ifdef _DEBUG
  case 0: ExitProcess(0);	// Debug: Keine Wiederholungen
#endif
  case -1: switch (MBox(0,1,MB_RETRYCANCEL,StrToInt(ModeString+3))) {
   case IDRETRY: CloseHandle(hCom); goto rep1;
  }ExitProcess(1);
  case -2: switch (MBox(0,2,MB_RETRYCANCEL,PrinterName)) {
   case IDRETRY: CloseHandle(hCom); goto rep1;
  }ExitProcess(2);
 }
// Dieses Programm kann nur gekillt werden.
 return 0;
}

/*******************
 * Hilfsfunktionen *
 *******************/

#ifdef _M_IX86
#if (_MSC_VER>=1400)
#define _alloca_probe _alloca_probe_16
#endif
extern "C" void _declspec(naked) _cdecl _alloca_probe() {
 _asm{	dec	eax
	and	al,0xFC
	sub	esp,eax			// make room
	jmp	dword ptr[esp+eax]	// fetch return address and jump
 }
}
#elif defined(_M_AMD64)
extern "C" void __chkstk() {}		// As all allocations are << 4KByte, stack checking is never needed.
#endif
Detected encoding: ANSI (CP1252)4
Wrong umlauts? - Assume file is ANSI (CP1252) encoded