Source file: /~heha/mb-iwp/Triggerkarte/c.zip/src/Relais1.c

/* Relais-Steuerung mit 3x Trigger-Abfrage und 3x Trigger-Ausgängen
 * Zum Programmierstil siehe http://www.tu-chemnitz.de/~heha/hs_freeware/mein_msvc.htm
 * h#s 12/06 - 04/07
Zu tun:
 * "maximiert" mit speichern
 * "Alle LEDs an/aus auch in Liste mit MMB/RMB
+070524	Kommandozeilenargument = Dateiname, Registrierung von .relay
+070529	Geladener Dateiname in Titelzeile, mit Modifikationsanzeige
+070619	5 ToolInfos, "Direkthilfe", Fragezeichen in Titelleiste, graue „ausgewählte Zeile“
-070704	Breite für 5 statt 4 Spalten speichern, verschwindender Dateiname, ExecName-Bug
 */

#define WIN32_LEAN_AND_MEAN
#define _WIN32_WINNT 0x0501	// WM_THEMECHANGED aktivieren
#include <windows.h>
#include <windowsx.h>	// Makros
#include <commdlg.h>	// Datei öffnen/speichern
#include <commctrl.h>	// Listenfenster für die Zeitschritte
#include <shellapi.h>	// HDROP
#include <shlwapi.h>	// nützliche Funktionen
#include <shlobj.h>	// PathIsExe
#include <uxtheme.h>	// für Fragezeichen-Knopf (Kontexthilfe)
#include <tmschema.h>	// noch mehr XP-Müll

#define elemof(x) (sizeof(x)/sizeof((x)[0]))
#define T(x) TEXT(x)
#define nobreak
typedef enum {false,true} bool;	// nicht für C++!

//Leider hat Win95 mit IE 3.0 ein reduziertes Angebot der
//shlwapi.dll, kein StrCatBuff, kein wnsprintf, kein wvnsprintf,
//daher Ersetzung durch „unsichere“ Funktionen aus kernel32.dll
//Weiterhin kennt Win95 die Schrift "MS Shell Dlg 2" nicht...
#ifdef WIN95
# undef  StrCatBuff
# define StrCatBuff(d,s,l) lstrcat(d,s)
# define wnsprintf1(d,l,t,a1)			wsprintf(d,t,a1)
# define wnsprintf2(d,l,t,a1,a2)		wsprintf(d,t,a1,a2)
# define wnsprintf3(d,l,t,a1,a2,a3)		wsprintf(d,t,a1,a2,a3)
# define wnsprintf6(d,l,t,a1,a2,a3,a4,a5,a6)	wsprintf(d,t,a1,a2,a3,a4,a5,a6)
# undef  wvnsprintf
# define wvnsprintf(d,l,t,a) wvsprintf(d,t,a)
#else
# define wnsprintf1 wnsprintf
# define wnsprintf2 wnsprintf
# define wnsprintf3 wnsprintf
# define wnsprintf6 wnsprintf
#endif

#ifdef DEBUG
# define _debug(x) DebugPrintf x
void _cdecl DebugPrintf(LPCTSTR s,...) {
 TCHAR buf[256];
 wvnsprintf(buf,elemof(buf),s,(va_list)(&s+1));
 OutputDebugString(buf);
}
#else
# define _debug(x)
#endif

#ifdef WIN32
# define Send_WM_Command(ToWnd,CtlId,NotifyCode,FromWnd)\
  SendMessage(ToWnd,WM_COMMAND,MAKELONG(CtlId,NotifyCode),(LPARAM)FromWnd);
#else	// Win16
# define Send_WM_Command(ToWnd,CtlId,NotifyCode,FromWnd)\
  SendMessage(ToWnd,WM_COMMAND,CtlId,MAKELONG((UINT)FromWnd,NotifyCode));
#endif

// Deklaration der zwei Einsprünge in (statisch gebundene) InpOut32.dll
void WINAPI Out32(WORD,BYTE);
BYTE WINAPI Inp32(WORD);

/*****************************************
 * Globale Variablen und Typdefinitionen *
 *****************************************/
WORD gLptBase=0x378;	// Voreinstellung

HINSTANCE ghInstance;
HWND ghMainWnd;
HWND ghListWnd;
HWND ghStatusWnd;
BYTE gCurLptState[3];	// Momentaner LED-Zustand
bool gUseStbBsyAck=true;// für Sundox-Relaiskarte (BSY und ACK entfallen für Trigger)
bool gDoBeep=true;	// Piepser bei jedem Schritt
bool gModified=false;	// Inhalt von gFileName modifiziert?
UINT gEditErrors;	// Bit pro Editfenster für Fehler (Hintergrund rot)
TCHAR gFileName[MAX_PATH];	// Zuletzt geladene Datei
POINT gMinTrackSize;	// minimale Fenstergröße
const TCHAR gHelpFileName[]=T("Relais1.hlp");
static HWND gToolTip;

typedef struct{
 BYTE hour,min,sec,hsec;	// Kompakte Zeitangabe (ohne Tag, Wochentag o.ä.)
}MYTIME,*PMYTIME;

typedef struct{
 BYTE sourcebit,polarity;	// 1. Byte: Bit-Nummer, 2. Byte: H-Aktivität
}MYTRIGGER,*PMYTRIGGER;

// Diese Struktur "klebt" an jeder Zeile der ListView.
// Deshalb unterhält dieses Programm keine eigene verkettete Liste
typedef struct{
 BYTE Flags;	// ENUM: (UHRZEIT, DAUER, TRIGGER; BEGINLOOP, ENDLOOP, RELATIVZEIT)
 BYTE LoopCount;// Runden-Anzahl (0=unendlich, nur für äußere Schleife)
 BYTE RelState;	// Gewünschter Relais-Zustand (Datenport)
 BYTE TrigOut;	// Trigger-Ausgang-Zustand (Steuerport), Optokoppler
 union{
  MYTIME Uhrzeit;
  MYTIME Dauer;
  MYTRIGGER Trigger;
 };
 LPTSTR ExecFileName;
}ITEM,*PITEM;

#define LOADITEMSIZE 8	// ExecFileName nicht (per GetPrivateProfileStruct) laden
enum {UHRZEIT=1, DAUER, TRIGGER, TYPEMASK=0x0F,
  BEGINLOOP=0x10, ENDLOOP=0x20, RELATIVZEIT=0x40};

// GDI-Verwaltung
static const COLORREF gBrushColors[]={
 RGB(  0,128,0), RGB(  0,255,0),
 RGB(128,  0,0), RGB(255,  0,0),
 RGB(128,128,0), RGB(255,255,0)};
static const TCHAR gSmallFontName[]=T("Small Fonts");

union{
 struct{
  HBRUSH hbr[elemof(gBrushColors)];	// {dunkel-hell}{grün-rot-gelb}
  HBRUSH hbrError;	// rot (Fehler im Editfenster)
  HFONT hSmallFont;	// zur ziffernmäßigen Beschriftung
 };
 HGDIOBJ hgdiobj[elemof(gBrushColors)+1];
}gGdiObj;

//Schleifen-Verwaltung
#define MAXDEPTH 6	// Verschachtelungstiefe für Schleifen

typedef struct {
 int info;		// Bit0=Trigger, Bit1=unendlich
 int line;		// Start- bzw. Fehler-Zeilennummer (0-basiert)
 int depth;
 int remainings[MAXDEPTH+1];	// Verbleibende Iterationen
 int backjumps[MAXDEPTH+1];	// Rücksprünge
}LOOPINFO,*PLOOPINFO;

enum {		// info-Bits:
 INFO_LongerThan=1,	// Bit0 = Trigger, Zeit länger als angegeben
 INFO_Infinite=2,	// Bit1 = unendlich (äußere Schleife)
 INFO_ReenterLoop=4,	// Bit2 = Schleifen-Wiederholung (Schleifenzähler nicht init.)
 INFO_BreakInfinity=8,	// Bit3 = nicht endlos "loopen" (zur Zeitberechnung!)
 INFO_Overflow=0x10,	// Fehler: zu viele Verschachtelungsebenen
 INFO_Underflow=0x20,	// Fehler: Stackunterlauf
 INFO_InnerInfinity=0x30,// Fehler: innere Endlosschleife
 INFO_MissingEnd=0x40,	// Fehler: fehlendes ENDLOOP am Ende
 INFO_NoItem=0x50,	// Fehler: fehlendes Item oder Typ
 INFO_ErrorMask=0xF0,
 INFO_ErrorShift=4};

struct{
// int ListLine;	// aktuelle Zeile in der Liste (die Relais werden danach(!) übernommen)
 DWORD settimer_ms;
 DWORD start_ms;	// Startzeitpunkt, zur Laufzeitanzeige
 LOOPINFO loopinfo;	// zur Schleifen-Verwaltung
#define ListLine loopinfo.line
}gSched;	// "Zeitplaner"

PITEM gpSelectedItem;
TCHAR StdMBoxTitle[64];

/****************
 * aus WUTILS.C *
 ****************/

//Win32-typische Strukturen mit DWORD-Ausrichtung initialisieren
void _fastcall InitStruct(LPVOID p, UINT len) {
 *((LPUINT)p)=len; len/=sizeof(UINT); len--;
 if (len) do *++((LPUINT)p)=0; while (--len);
}

void _fastcall CopyUINT(UINT*d, const UINT*s, UINT len) {
 if (len) do *d++=*s++; while (--len);
}

int _cdecl MBox(HWND Wnd, LPCTSTR Text, UINT Type, ...) {
 TCHAR buf[256],buf2[256];
 if (!HIWORD(Text)) {
  LoadString(ghInstance,(UINT)(DWORD_PTR)Text,buf2,elemof(buf2));
  Text=buf2;
 }
 wvnsprintf(buf,elemof(buf),Text,(va_list)(&Type+1));
 return MessageBox(Wnd,buf,StdMBoxTitle,Type);
}

void WMMenuSelect(UINT id, UINT flags) {
 HINSTANCE hCommCtrl;
 TCHAR s[256];
 SendMessage(ghStatusWnd,SB_SIMPLE,flags!=0xFFFF,0);
 if (flags==0xFFFF) return;
 s[0]=0;
 if (flags&MF_SYSMENU) {
  id&=0x8FF0;
  if (flags&MF_POPUP) id=0x8FF0;
  hCommCtrl=LoadLibrary(T("comctl32.dll"));	// Da sind sie!
  LoadString(hCommCtrl,id,s,sizeof(s));
  FreeLibrary(hCommCtrl);
 }else if (flags&MF_POPUP) {}
 else LoadString(ghInstance,id,s,elemof(s));
 SendMessage(ghStatusWnd,SB_SETTEXT,255|SBT_NOBORDERS,(LPARAM)(LPTSTR)s);
}

// Wie Windows NT auf allen Windows-Versionen (da fehlt ein shlwapi.Beep?)
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;
 }
}

DWORD MyTimeToMs(PMYTIME time) {
 return (((time->hour*60+time->min)*60+time->sec)*100+time->hsec)*10;
}

#define MS_PER_DAY (24UL*60*60*1000)

DWORD NowToMs(void) {
 SYSTEMTIME st;
 GetLocalTime(&st);
 return ((st.wHour*60UL+st.wMinute)*60+st.wSecond)*1000+st.wMilliseconds;
}

int TimeInterval(PMYTIME time, PTSTR buf, UINT buflen) {
// Benutzt Windows-Funktion zur "hübschen" Darstellung einer Zeitspanne
 DWORD time_ms=MyTimeToMs(time);	// Millisekunden (Hundertstel unwichtig)
 return StrFromTimeInterval(buf,buflen,time_ms,6);
}

int TimeAbsolute(PMYTIME time, PTSTR buf, UINT buflen) {
// Benutzt Windows-Funktion für (einigermaßen) lokal-angepasste Uhrzeit-Darstellung
 SYSTEMTIME st;
 st.wHour=time->hour;
 st.wMinute=time->min;
 st.wSecond=time->sec;
 st.wMilliseconds=time->hsec*10;
 return GetTimeFormat(LOCALE_USER_DEFAULT,TIME_FORCE24HOURFORMAT,&st,NULL,buf,buflen)-1;
}

/************************************************
 * LED-Darstellung (etwa wie LptChk, WinDriver) *
 ************************************************/

void MakeGdiObj(void) {
 int i;
 for (i=0; i<elemof(gBrushColors); i++) {
  gGdiObj.hbr[i]=CreateSolidBrush(gBrushColors[i]);
 }
 gGdiObj.hbrError=CreateSolidBrush(RGB(255,128,128));
 gGdiObj.hSmallFont=CreateFont(-7,0,0,0,0,0,0,0,0,0,0,0,0,gSmallFontName);
}

void KillGdiObj(void) {
 int i;
 for (i=0; i<elemof(gGdiObj.hgdiobj); i++) {
  DeleteObject(gGdiObj.hgdiobj[i]);
 }
}

void DrawOneLed(HDC dc, LPCRECT pr, BYTE FarbIndex, TCHAR number) {
 RECT r;
 COLORREF ocolor;
 int obkmode;
// Schwarzen Hintergrund malen
 HBRUSH obrush=SelectBrush(dc,GetStockBrush(BLACK_BRUSH));
 HPEN open=SelectPen(dc,GetStockPen(NULL_PEN));
 CopyRect(&r,pr);
 PatBlt(dc,r.left,r.top,r.right-r.left,r.bottom-r.top,PATCOPY);
// Kreis (LED) malen
 SelectBrush(dc,gGdiObj.hbr[FarbIndex]);
 InflateRect(&r,-1,-1);
 Ellipse(dc,r.left,r.top,r.right,r.bottom);
// Beschriften
 ocolor=SetTextColor(dc,FarbIndex&1?RGB(0,0,0):RGB(255,255,255));
 obkmode=SetBkMode(dc,TRANSPARENT);
// ExtTextOut(dc,r.left,r.top,&r,&number,1,NULL);
 DrawText(dc,&number,1,&r,DT_CENTER|DT_NOPREFIX|DT_SINGLELINE|DT_VCENTER);
 SetBkMode(dc,obkmode);
 SetTextColor(dc,ocolor);
// GDI aufräumen
 SelectBrush(dc,obrush);
 SelectPen(dc,open);
}

// Button besitzergezeichnet, zum direkten Schalten
void LedButtonHandleDraw(LPDRAWITEMSTRUCT dis,BYTE leds,BYTE numbits,BYTE FarbIndex) {
 RECT r;
 TCHAR c;
 CopyRect(&r,&dis->rcItem);
 if (dis->itemAction&(ODA_DRAWENTIRE|ODA_SELECT)) {
  c=T('1');
  do{
   r.right=r.left+(r.bottom-r.top);	// Höhe=Breite (eigentlich: Aspectratio einrechnen!!)
   DrawOneLed(dis->hDC,&r,FarbIndex+(leds&1),c);
   r.left=r.right;
   leds>>=1;
   c++;
  }while(--numbits);
  r.left=dis->rcItem.left;	// restaurieren
 }else{
  r.right=r.left+(r.bottom-r.top)*numbits;	// rechte Seite eingrenzen
 }
 if (dis->itemState&ODS_FOCUS || dis->itemAction==ODA_FOCUS) {
  DrawFocusRect(dis->hDC,&r);
 }
// Den Rest des Knopfes mit Hintergrundfarbe auffüllen, weil EraseBkgnd abgeschaltet wurde
 SelectBrush(dis->hDC,(HBRUSH)SendMessage(ghMainWnd,WM_CTLCOLORDLG,(WPARAM)dis->hDC,(LPARAM)ghMainWnd));
 PatBlt(dis->hDC,r.right,r.top,dis->rcItem.right-r.right,r.bottom-r.top,PATCOPY);
}

#define BN_BITALL0 0x1000	// Kode für "alles AUS"
#define BN_BIT0HIT 0x1001	// darauf die Bitnummer addiert
#define BN_BITALL1 0x1009	// Kode für "alles EIN"
WNDPROC gOldButtonProc;

// Zur Steuerung der LED-Anzeigen per Maus und Tatstatur
// An das Elternfenster (Dialog) wird WM_COMMAND mit
// BN_BIT0HIT .. BN_BIT7HIT gesendet.
LRESULT WINAPI LedButtonSubclassProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
// Tastaturbedienung
  case WM_KEYDOWN: {
   wParam-='0';
   if (wParam<10) {
    Send_WM_Command(GetParent(Wnd),GetWindowID(Wnd),BN_BITALL0+wParam,Wnd);
   }
  }break;
// Mausbedienung
  case WM_LBUTTONDOWN:
  case WM_LBUTTONDBLCLK: {
   RECT r;
   GetWindowRect(Wnd,&r);
   wParam=(WPARAM)(GET_X_LPARAM(lParam)/(r.bottom-r.top));
   if (wParam<8) {
    Send_WM_Command(GetParent(Wnd),GetWindowID(Wnd),BN_BIT0HIT+wParam,Wnd);
   }
  }break;
// Alles AUS mit rechter Maustaste
  case WM_RBUTTONDOWN:
  case WM_RBUTTONDBLCLK: {
   Send_WM_Command(GetParent(Wnd),GetWindowID(Wnd),BN_BITALL0,Wnd);
  }break;
// alles EIN mit mittlerer Maustaste (sofern vorhanden)
  case WM_MBUTTONDOWN:
  case WM_MBUTTONDBLCLK: {
   Send_WM_Command(GetParent(Wnd),GetWindowID(Wnd),BN_BITALL1,Wnd);
  }break;
// Flackern vermeiden (nur Knöpfe mit BS_OWNERDRAW flackern per se!)
  case WM_ERASEBKGND: return TRUE;
 }
 return CallWindowProc(gOldButtonProc,Wnd,Msg,wParam,lParam);
}

BOOL InvalidateItem(UINT id, PRECT pRect, BOOL bErase) {
 return InvalidateRect(GetDlgItem(ghMainWnd,id),pRect,bErase);
}

void SetModified(bool newstate) {
 if (!gModified || !newstate) {
  TCHAR s[MAX_PATH];
  gModified=newstate;
  if (*gFileName) {
   wnsprintf3(s,elemof(s),T("%s %c %s"),
     PathFindFileName(gFileName),gModified?'*':'-',StdMBoxTitle);
  }else{
   wnsprintf2(s,elemof(s),T("%s %c"),
     StdMBoxTitle,gModified?'*':0);
  }
  SetWindowText(ghMainWnd,s);
 }
}

/*********************************
 * Listbox und Besitzerzeichnung *
 *********************************/
#define SELECTED (-2)

int ListGetSelected(void) {
 return ListView_GetNextItem(ghListWnd,-1,LVNI_SELECTED);
}

// "Copy-Konstruktor", mit pSrc==NULL ist's ein "Default-Konstruktor"
PITEM NewItem(PITEM pSrc) {
 PITEM pItem=LocalAlloc(LPTR,sizeof(ITEM));
 if (pItem && pSrc) {
  CopyUINT((UINT*)pItem,(UINT*)pSrc,(sizeof(ITEM)+sizeof(UINT)-1)/sizeof(UINT));
  if (pSrc->ExecFileName) pItem->ExecFileName=StrDup(pSrc->ExecFileName);
 }
 return pItem;
}

// "Destruktor" - danach ist Zeiger ungültig
void DeleteItem(PITEM pItem) {
 if (pItem->ExecFileName) LocalFree(pItem->ExecFileName);
 LocalFree(pItem);
}

WNDPROC gOldLvProc;
 
LRESULT WINAPI LvSubclassProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
// Tastaturbedienung (LVN_KEYDOWN hatte Seiteneffekte!)
  case WM_KEYDOWN: {
   bool redraw=false;
   unsigned k=(unsigned)wParam;
   if (k!=VK_INSERT && !gpSelectedItem) break;	// nichts tun
   switch (k) {
    case VK_INSERT: {
     SendMessage(ghMainWnd,WM_COMMAND,121,0);	// Knopf "Neue Zeile"
    }return 0;

    case VK_DELETE: {
     SendMessage(ghMainWnd,WM_COMMAND,122,0);	// Knopf "Löschen"
    } return 0;

    case VK_DOWN: if (GetKeyState(VK_CONTROL)<0) {
     SendMessage(ghMainWnd,WM_COMMAND,123,0); return 0;	// Knopf "Item runter"
    }break;

    case VK_UP: if (GetKeyState(VK_CONTROL)<0) {
     SendMessage(ghMainWnd,WM_COMMAND,124,0); return 0;	// Knopf "Item rauf"
    }break;

    case '0': gpSelectedItem->RelState=0; redraw=true; break;
    case '9': gpSelectedItem->RelState=0xFF; redraw=true; break;
    default: {
     k-='1';     if (k<8) {gpSelectedItem->RelState^=1<<k; redraw=true; }
     k-='A'-'1'; if (k<8) {gpSelectedItem->TrigOut^=1<<k; redraw=true; }
    }
   }
   if (redraw) {
    int idx=ListGetSelected();
    ListView_RedrawItems(Wnd,idx,idx);
   }
  }break;
 }
 return CallWindowProc(gOldLvProc,Wnd,Msg,wParam,lParam);
}

#define ITEMHEIGHT 16
 
// Bereitstellen des Strings für Spalte 0 
int LineColumn0(PITEM pItem, PTSTR buf, UINT buflen) {
 TCHAR buf2[40],tem[40];
 int i;
 LoadString(ghInstance,pItem->Flags&TYPEMASK,tem,elemof(tem));	//UHRZEIT/DAUER/TRIGGER
 switch (pItem->Flags&TYPEMASK) {
  case UHRZEIT: {
   i=TimeAbsolute(&pItem->Uhrzeit,buf2,elemof(buf2));
   if (pItem->Flags&RELATIVZEIT) {
    LoadString(ghInstance,17/* (r)*/,buf2+i,elemof(buf2)-i);	// r-Suffix anhängen
   }
  }return wnsprintf1(buf,buflen,tem,buf2);
  case DAUER: {
   TimeInterval(&pItem->Dauer,buf2,elemof(buf2));
  }return wnsprintf1(buf,buflen,tem,buf2);
  case TRIGGER:
    return wnsprintf2(buf,buflen,tem,
    pItem->Trigger.sourcebit,
    pItem->Trigger.polarity?'H':'L');
 }
 return 0;
}

// Bereitstellen des Strings für Spalte 1
int LineColumn1(PITEM pItem, PTSTR buf, UINT buflen) {
 int ret=0;
 if (!buflen) return ret;
 buf[0]=0;
 if (!(pItem->Flags&(BEGINLOOP|ENDLOOP))) return ret;
 ret=LoadString(ghInstance,4+((pItem->Flags&(BEGINLOOP|ENDLOOP))>>4)-1,
   buf,buflen);	// Start(%d)/Ende/Schleife(%d)
 if (pItem->Flags&BEGINLOOP) {
  ret+=pItem->LoopCount
    ? wnsprintf1(buf+ret,buflen-ret,T(" (%ux)"),pItem->LoopCount)
    : LoadString(ghInstance,39/*endlos*/,buf+ret,buflen-ret);
 }
 return ret;
}

// ListView besitzergezeichnet
// Auch das besitzergezeichnete ListView flackert per se.
void HandleDrawList(LPDRAWITEMSTRUCT dis) {
 RECT r;
 TCHAR c;
 BYTE b;
 TCHAR buf[40];
 COLORREF ocolor,obkcolor;	// Gemerkte Farben
 int n,j;
 if (dis->itemAction&(ODA_DRAWENTIRE|ODA_SELECT) && dis->itemData) {
  if (dis->itemID==(unsigned)gSched.ListLine) {
// etwas gelber machen; falls gelb, dann weiß u.ä.
   obkcolor=SetBkColor(dis->hDC,RGB(0,0,255)^GetBkColor(dis->hDC));
   ocolor=SetTextColor(dis->hDC,GetSysColor(COLOR_INFOTEXT));
  }else if (dis->itemState&ODS_SELECTED) {	// Selektion zeichnen
   if (dis->itemState&ODS_FOCUS) {
    obkcolor=SetBkColor(dis->hDC,GetSysColor(COLOR_HIGHLIGHT));
    ocolor=SetTextColor(dis->hDC,GetSysColor(COLOR_HIGHLIGHTTEXT));
   }else{
    obkcolor=SetBkColor(dis->hDC,GetSysColor(COLOR_BTNFACE));
    ocolor=GetTextColor(dis->hDC);
   }
  }
//GetSubItemRect vom SubItem 0 funktioniert nicht, liefert ganze Breite, deshalb...
  ListView_GetSubItemRect(ghListWnd,dis->itemID,1,LVIR_BOUNDS,&r);	// mittlere Spalte (Schleifen)
  n=LineColumn1((PITEM)dis->itemData,buf,elemof(buf));
  ExtTextOut(dis->hDC,r.left+2,r.top+2,ETO_OPAQUE|ETO_CLIPPED,&r,buf,n,NULL);
  j=r.left;
  ListView_GetItemRect(ghListWnd,dis->itemID,&r,LVIR_BOUNDS);	// linke Spalte (Zeiten)
  r.right=j;
  n=LineColumn0((PITEM)dis->itemData,buf,elemof(buf));
  ExtTextOut(dis->hDC,r.left+2,r.top+2,ETO_OPAQUE|ETO_CLIPPED,&r,buf,n,NULL);
  if (dis->itemState&ODS_SELECTED || dis->itemID==(unsigned)gSched.ListLine) {
   SetBkColor(dis->hDC,obkcolor);
   SetTextColor(dis->hDC,ocolor);
  }
  if (((PITEM)dis->itemData)->ExecFileName) {
   ListView_GetSubItemRect(ghListWnd,dis->itemID,4,LVIR_BOUNDS,&r);
   ExtTextOut(dis->hDC,r.left+2,r.top+2,ETO_OPAQUE|ETO_CLIPPED,&r,
     ((PITEM)dis->itemData)->ExecFileName,
     lstrlen(((PITEM)dis->itemData)->ExecFileName),NULL);
  }
  ListView_GetSubItemRect(ghListWnd,dis->itemID,2,LVIR_BOUNDS,&r);	// Relais
  j=r.right;
  if (dis->itemAction&ODA_DRAWENTIRE) {	// nicht bei SELECTED neu zeichnen
   b=((PITEM)dis->itemData)->RelState;
   for (c=T('1'); c<='8'; c++) {
    r.right=r.left+(r.bottom-r.top);	// Höhe=Breite (eigentlich: Aspectratio einrechnen!!)
    if (r.right>j) r.right=j;
    DrawOneLed(dis->hDC,&r,b&1,c);
    r.left=r.right;
    b>>=1;		// nächstes Bit
   }
   ListView_GetSubItemRect(ghListWnd,dis->itemID,3,LVIR_BOUNDS,&r);	// Triggerausgänge
   j=r.right;
   b=((PITEM)dis->itemData)->TrigOut;
   for (c=T('1'); c<=T('3'); c++) {
    r.right=r.left+(r.bottom-r.top);
    if (r.right>j) r.right=j;
    DrawOneLed(dis->hDC,&r,4+(b&1),c);
    r.left=r.right;
    b>>=1;		// nächstes Bit
   }
  }
 }
 if (dis->itemState&ODS_FOCUS || dis->itemAction==ODA_FOCUS) {
  ListView_GetSubItemRect(ghListWnd,dis->itemID,0,LVIR_BOUNDS,&r);
  j=r.left;
  ListView_GetSubItemRect(ghListWnd,dis->itemID,1,LVIR_BOUNDS,&r);
  r.left=j;
  DrawFocusRect(dis->hDC,&r);
 }
 _debug((T("DrawItem Item=%i Action=%u State=%u\n"),
    dis->itemID,dis->itemAction,dis->itemState));
}

int ListCreateColumn(int Spalte, UINT StringId, int Breite) {
 LVCOLUMN lvc;
 TCHAR s[64];
 LoadString(ghInstance,StringId,s,elemof(s));
 lvc.mask=LVCF_FMT|LVCF_WIDTH|LVCF_TEXT|LVCF_SUBITEM;
 lvc.fmt=LVCFMT_LEFT;
 lvc.cx=Breite;
 lvc.pszText=s;
 lvc.iSubItem=Spalte;
 return ListView_InsertColumn(ghListWnd,Spalte,&lvc);
}

// Neues Element bei Index einfügen (Rest rutscht nach hinten)
int ListInsertItem(int idx, PITEM pItem, UINT state) {
 LVITEM lvi;
 if (state&LVIS_SELECTED && GetFocus()==ghListWnd) state|=LVIS_FOCUSED;
 lvi.mask=LVIF_PARAM|LVIF_STATE;
 lvi.iItem=idx;
 lvi.iSubItem=0;	//??notwendig??
 lvi.state=state;	// sollte übrige Selektionen entfernen...
 lvi.stateMask=LVIS_SELECTED|LVIS_FOCUSED;
 lvi.lParam=(LPARAM)pItem;
 return ListView_InsertItem(ghListWnd,&lvi);
}

// von Index den lParam holen und zum Zeiger casten
PITEM ListGetItem(int iLine) {
 LVITEM lvi;
 if (iLine==SELECTED) iLine=ListGetSelected();
 lvi.mask=LVIF_PARAM;
 lvi.iItem=iLine;
 lvi.iSubItem=0;
 if (!ListView_GetItem(ghListWnd,&lvi)) return NULL;
 return (PITEM)lvi.lParam;
}

void ListInvalidateItem(int iLine) {
 RECT r;
 if (iLine==SELECTED) {
  iLine=ListGetSelected();
  SetModified(true);
 }
 if (iLine<0) return;
 ListView_GetItemRect(ghListWnd,iLine,&r,LVIR_BOUNDS);
 InvalidateRect(ghListWnd,&r,TRUE);
}

/***********************************************
 * Relais + Trigger schalten, Trigger abfragen *
 ***********************************************/
 
// Strobe ausgeben, sofern erforderlich
// (alternativ könnte man das Parallelport auf Auto-Strobe schalten...)
void MakeStrobe(void) {
 if (!gUseStbBsyAck) return;
 Out32(gLptBase+2,gCurLptState[2]^0x01);// STB=L
 Out32(gLptBase+2,gCurLptState[2]);	// STB=H
}
 
// Parallelport-Spiegelbytes initialisieren
void InitLptState(void) {
 gCurLptState[0]=Inp32(gLptBase+0);	// Zustände Port-Leitungen
 gCurLptState[1]=Inp32(gLptBase+1);	// Zustände Trigger-Eingang
 gCurLptState[2]=Inp32(gLptBase+2)&~0x20; // DIR=OUT festnageln
 MakeStrobe();				// in Flipflop schreiben
}
 
// Relais-Zustände erfragen
BYTE GetCurRelState(void) {
 return gCurLptState[0];
}

// Relais-Zustände setzen und Anzeige nachführen
void SetCurRelState(BYTE NewState) {
 if (NewState!=GetCurRelState()) {
  gCurLptState[0]=NewState;
  Out32(gLptBase+0,gCurLptState[0]);
  MakeStrobe();
  InvalidateRect(GetDlgItem(ghMainWnd,80),NULL,FALSE);
 }
}

// Trigger-Ausgänge erfragen
BYTE GetCurTrigOutState(void) {
 return ((gCurLptState[2]^0x0B)>>1)&0x07;
}

// Trigger-Ausgänge setzen und Anzeige nachführen
void SetCurTrigOutState(BYTE NewState) {
// im Fall von gUseStbBusyAck beinhaltet NewState 3 Bits,
// sonst 4 Bits? Zunächst stets 3 Bits, rechts ausgerichtet.
 NewState&=7;		// nur 3 Bits
 if (NewState!=GetCurTrigOutState()) {
  NewState<<=1;
  NewState^=0x0A;	// Bit 3 und 1 invertieren (wegen Parallelport)
  gCurLptState[2]=0xC0|NewState;	// 1100nnn0
  Out32(gLptBase+2,gCurLptState[2]);
  InvalidateItem(84,NULL,FALSE);
 }
}

// Trigger-Eingänge erfragen und Anzeige nachführen
BYTE GetCurTrigInState(void) {
 BYTE ret=Inp32(gLptBase+1);
 if (ret!=gCurLptState[1]) {
  gCurLptState[1]=ret;
  InvalidateItem(83,NULL,FALSE);
 }
 ret=(gCurLptState[1]^0x80)>>3;		// 5 Bits
 if (gUseStbBsyAck) ret&=0x07;		// 3 Bits
 return ret;
}

/************
 * Logdatei *
 ************/
static TCHAR LogFileName[MAX_PATH];		// Log-Dateiname
static HANDLE hLog=INVALID_HANDLE_VALUE;	// Log-Dateihandle
 
void LogClose(void) {
 if (hLog!=INVALID_HANDLE_VALUE) {
  CloseHandle(hLog);
  hLog=INVALID_HANDLE_VALUE;
 }
}

void LogOpen(void) {
 DWORD cb;
 LogClose();
 if (!LogFileName[0]) return;
 hLog=CreateFile(LogFileName,
   FILE_APPEND_DATA,
   FILE_SHARE_READ|FILE_SHARE_DELETE,
   NULL,
   OPEN_ALWAYS,
   FILE_ATTRIBUTE_NORMAL|FILE_FLAG_WRITE_THROUGH,
   0);
 if (hLog==INVALID_HANDLE_VALUE) {
  MBox(ghMainWnd,MAKEINTRESOURCE(23)/*Log-Datei*/,MB_OK,LogFileName);
  return;
 }
 if (SetFilePointer(hLog,0,NULL,FILE_END)) {
  static const TCHAR NewLine[]=T("\r\n");
  WriteFile(hLog,NewLine,2,&cb,NULL);
 }else{
#ifdef UNICODE
  static const TCHAR UnicodeIntroducer[1]={0xFEFF};	// zur Erkennung von Unicode und Endian
  WriteFile(hLog,UnicodeIntroducer,sizeof(UnicodeIntroducer),&cb,NULL);
#endif
 }
}

void LogWrite(UINT MsgIndex) {
 int i;
 BYTE b,m;
 DWORD cb;
 TCHAR line[128];
 TCHAR format[128],date[32],time[32],relais[9],trigger_in[4],trigger_out[4];

 if (hLog==INVALID_HANDLE_VALUE) return;	// nicht loggen
 LoadString(ghInstance,MsgIndex,format,elemof(format));
 GetDateFormat(LOCALE_USER_DEFAULT,DATE_SHORTDATE,NULL,NULL,date,elemof(date));
 GetTimeFormat(LOCALE_USER_DEFAULT,TIME_FORCE24HOURFORMAT,NULL,NULL,time,elemof(time));

 b=GetCurRelState();
 for (i=0,m=1; i<8; i++,m<<=1) relais[i]=m&b?'1':'0';
 relais[i]=0;

 b=GetCurTrigInState();
 for (i=0,m=1; i<3; i++,m<<=1) trigger_in[i]=m&b?'1':'0';
 trigger_in[i]=0;

 b=GetCurTrigOutState();
 for (i=0,m=1; i<3; i++,m<<=1) trigger_out[i]=m&b?'1':'0';
 trigger_out[i]=0;

 cb=wnsprintf6(line,elemof(line),format,date,time,
   gSched.ListLine+1,relais,trigger_in,trigger_out);
 if (!WriteFile(hLog,line,cb,&cb,NULL) || !cb) MBox(ghMainWnd,MAKEINTRESOURCE(30),MB_OK);
 FlushFileBuffers(hLog);	// aus Angst, sollten die Flags bei CreateFile() schon erledigen
}

// Je nach Vorhandensein eines Log-Dateinamens Menü-Häkchen mitführen
void LogSetMenuCheck(void) {
 CheckMenuRadioItem(GetMenu(ghMainWnd),1003,1004,LogFileName[0]?1003:1004,MF_BYCOMMAND);
}

/**********************************
 * Zeitgesteuerter Programmablauf *
 **********************************/

//Behandlung von BEGINLOOP zusammen mit <LoopInfo>:
// * Setzt Felder in <LoopInfo> entsprechend
// * Wertet "ReenterLoop" aus
//<false> bei Fehler
bool HandleBeginLoop(PITEM pItem, PLOOPINFO pLoopInfo) {
 if (pItem->Flags&BEGINLOOP) {
  if (!(pLoopInfo->info&INFO_ReenterLoop)) {
   if (pLoopInfo->depth==MAXDEPTH) {
    pLoopInfo->info|=INFO_Overflow;	// Überlauf
    return false;
   }
   pLoopInfo->backjumps[pLoopInfo->depth]=pLoopInfo->line;
   pLoopInfo->remainings[pLoopInfo->depth]=pItem->LoopCount;
  }
  pLoopInfo->depth++;
 }
 return true;	// OK
}

//Behandlung von ENDLOOP zusammen mit <LoopInfo>:
// * Dekrementiert Felder in <LoopInfo> entsprechend
// * Setzt LoopInfo.line auf nächste oder zu wiederholende Zeilennummer
// * Bei Schleifenwiederholung wird "ReenterLoop" gesetzt
//<false> bei Fehler
bool HandleEndLoop(PITEM pItem, PLOOPINFO pLoopInfo) {
 pLoopInfo->info&=~INFO_ReenterLoop;
 pLoopInfo->line++;			// im Normalfall nächste Zeile
 if (pItem->Flags&ENDLOOP) {
  if (pLoopInfo->depth==0) {
   pLoopInfo->info|=INFO_Underflow;	// Unterlauf
   return false;
  }
  pLoopInfo->depth--;
  if (!pLoopInfo->remainings[pLoopInfo->depth]) {
   pLoopInfo->info|=0x02;		// Endlosschleife
   if (pLoopInfo->depth) {
    pLoopInfo->info|=INFO_InnerInfinity;	// innere Endlosschleife
    return false;
   }
   if (pLoopInfo->info&INFO_BreakInfinity) return true;
  }else if (!--pLoopInfo->remainings[pLoopInfo->depth]) return true;
  pLoopInfo->line=pLoopInfo->backjumps[pLoopInfo->depth];
  pLoopInfo->info|=INFO_ReenterLoop;	//Iteration
 }//ENDLOOP
 return true;
}

// gleichzeitig Test auf korrekte Schleifen-Konstruktion
DWORD EstimateRunningTime(PLOOPINFO pLoopInfo) {
 int k=ListView_GetItemCount(ghListWnd);
 PITEM pItem;
 DWORD sum=0;
 DWORD now=NowToMs();
 DWORD aim;
 pLoopInfo->info|=INFO_BreakInfinity;
 for (; pLoopInfo->line<k; ) {
  pItem=ListGetItem(pLoopInfo->line);
  if (!pItem) {
   pLoopInfo->info|=INFO_NoItem;	// interner Fehler
   return 0;
  }
  if (!HandleBeginLoop(pItem,pLoopInfo)) return 0;
  switch (pItem->Flags&TYPEMASK) {
   case UHRZEIT: {
    aim=MyTimeToMs(&pItem->Uhrzeit);	// Ziel
    if (pItem->Flags&RELATIVZEIT) {
     while (aim<sum) aim+=MS_PER_DAY;	// Ziel nächster Tag
     sum=aim;				// Summation ist hier faktisch außer Kraft!
    }else{
     if (aim<now+sum) aim+=MS_PER_DAY;	// Ziel nächster Tag
     sum+=(aim-now);			// Differenz = Dauer
    }
   }break;
   case DAUER: {
    sum+=MyTimeToMs(&pItem->Dauer);	// Dauer
   }break;
   case TRIGGER: {
    pLoopInfo->info|=INFO_LongerThan;	// Zeit ist länger als Rückgabewert
   }break;
   default: {
    pLoopInfo->info|=INFO_NoItem;	// leere Zeile
   }return 0;
  }//switch
  if (!HandleEndLoop(pItem,pLoopInfo)) return 0;
 }//for
 if (pLoopInfo->depth){
  pLoopInfo->info|=INFO_MissingEnd;	// offene Schleife
  return 0;
 }
 return sum;
}

BOOL EnableItem(UINT id, BOOL state) {
 HWND Wnd=GetDlgItem(ghMainWnd,id);
// Element mit Fokus disablen ist tödlich - ohne Maus
 if (!state && GetFocus()==Wnd) {
  HWND w=Wnd;	// Das nächste "enabelte" Element suchen
  do w=GetNextSibling(Wnd); while (w && !IsWindowEnabled(w));
  if (w) SetFocus(w);
 }
 return EnableWindow(Wnd,state);
}

void SchedStart(void) {
 gSched.ListLine=0;
 gSched.start_ms=NowToMs();
 gSched.loopinfo.info=gSched.loopinfo.depth=0;	// alle Flags AUS
 EnableItem(92,TRUE);	// Stopp-Knopf EIN
 EnableItem(91,FALSE);	// Starten-Knopf AUS (Fokus rutscht auf "Stopp")
 LogOpen();
 LogWrite(27/*gestartet*/);
}

void SchedStop(bool bAbort) {
 int PrevItem=gSched.ListLine;
 gSched.ListLine=-1;
 ListInvalidateItem(PrevItem);		// Zeile wieder weiß machen
 EnableItem(91,TRUE);	// Starten-Knopf EIN
 EnableItem(92,FALSE);	// Stopp-Knopf AUS (Fokus rutscht auf "Schritt")
 LogWrite(bAbort?29:28);
 LogClose();
}

bool SchedCalcWait(void) {
// Dauer bis zum zeitlichen Ende des Schritts berechnen, Minimum für Trigger
 PITEM pItem=ListGetItem(gSched.ListLine);
 if (pItem) {
  if (!HandleBeginLoop(pItem,&gSched.loopinfo)) {
   SchedStop(true);
   return FALSE;
  }
//  gSched.loopinfo.info|=INFO_ReenterLoop;	// nicht noch einmal setzen!
// (Auch dann nicht, wenn nach Systemzeitänderung SchedCalcWait nochmal aufgerufen wird)
  switch (pItem->Flags&TYPEMASK) {
   case UHRZEIT: {
    DWORD Then=MyTimeToMs(&pItem->Uhrzeit);
    DWORD Now=NowToMs();
    if (pItem->Flags&RELATIVZEIT) Now-=gSched.start_ms;
    if (Then<Now) Then+=MS_PER_DAY;	// am nächsten Tag
    gSched.settimer_ms=Then-=Now;
   }break;
   case DAUER: gSched.settimer_ms=MyTimeToMs(&pItem->Uhrzeit); break;
   case TRIGGER: gSched.settimer_ms=USER_TIMER_MINIMUM; break;
  }
 }else{	// offenbar Listenende erreicht
  SchedStop(false);		// Programm-Ende
 }
 return pItem!=NULL;
}

bool IsTriggered(PITEM pItem) {
 switch (pItem->Flags&TYPEMASK) {
  case TRIGGER: {
   BYTE TriggerInput=GetCurTrigInState();
   BYTE mask=1<<(pItem->Trigger.sourcebit-1);
   mask&=TriggerInput;
   if (pItem->Trigger.polarity) mask=!mask;	// wenn H-aktiv: logisch negieren
   if (mask) return false;	// nicht getriggert
  }break;
 }
 return true;
}

DWORD CALLBACK AsyncBeepThread(LPVOID Data) {
 return MyBeep(LOWORD(Data),HIWORD(Data));
}

void AsyncBeep(DWORD dwFreq, DWORD dwDuration) {
 CreateThread(NULL,0,AsyncBeepThread,(LPVOID)(DWORD_PTR)MAKELONG(dwFreq,dwDuration),0,&dwFreq);
}

BOOL SchedHandleNext(BOOL ByButton) {
 PITEM pItem=ListGetItem(gSched.ListLine);
 int PrevItem=gSched.ListLine;
 if (!pItem) {	// Problem!!
  KillTimer(ghMainWnd,1);
  return FALSE;
 }
 if (!ByButton && !IsTriggered(pItem)) return TRUE;	// weiter TIMERn
 KillTimer(ghMainWnd,1);
 SetCurRelState(pItem->RelState);
 SetCurTrigOutState(pItem->TrigOut);
 if (pItem->ExecFileName) {
  TCHAR FileName[MAX_PATH],Parameters[MAX_PATH];
  lstrcpyn(FileName,pItem->ExecFileName,elemof(FileName));
  lstrcpy(Parameters,PathGetArgs(FileName));
  PathRemoveArgs(FileName);
  PathUnquoteSpaces(FileName);
  if ((DWORD_PTR)ShellExecute(ghMainWnd,NULL,FileName,Parameters,NULL,SW_SHOWNORMAL)<=32) {
   Beep(200,500);
  }
 }
 if (!ByButton && gDoBeep) AsyncBeep(400,200);	// Geräusch (besser kleines .WAV: Relais-Klicken)
 LogWrite(24/*langer Formatstring*/);

 if (!HandleEndLoop(pItem,&gSched.loopinfo)) {
  SchedStop(true);
  return FALSE;
 }
 ListInvalidateItem(PrevItem);		// Zeile wieder weiß machen
 if (!SchedCalcWait()) return FALSE;
 ListInvalidateItem(gSched.ListLine);	// Zeile gelb machen
 SetTimer(ghMainWnd,1,gSched.settimer_ms,NULL);
 return TRUE;
}

/***********************
 * Laden und Speichern *
 ***********************/

// Lädt Zeit-Daten aus INI-ähnlicher Datei in die Liste
bool LoadFile(LPCTSTR FileName) {
 int i;
 bool ret=false;
 gpSelectedItem=NULL;
 ListView_DeleteAllItems(ghListWnd);
 for (i=0; ; i++) {
  TCHAR Nummer[8],buf[MAX_PATH];
  PITEM pItem;
  wsprintf(Nummer,T("%i"),i);
  pItem=NewItem(NULL);
  if (!GetPrivateProfileStruct(T("Liste"),Nummer,
    pItem,LOADITEMSIZE,FileName)) {
   DeleteItem(pItem);	// Ende der Liste
   break;
  }
  if (GetPrivateProfileString(T("Exec"),Nummer,NULL,buf,elemof(buf),FileName)) {
   pItem->ExecFileName=StrDup(buf);
  }
  ListInsertItem(i,pItem,0);
  ret=true;
 }
 i=GetPrivateProfileInt(T("Liste"),T("Select"),0,FileName);
 ListView_SetItemState(ghListWnd,i,LVIS_SELECTED,LVIS_SELECTED);
 if (ret) SetModified(false);
 return ret;
}

// Speichert Zeit-Daten aus Liste in INI-ähnliche Datei, mit Flush
bool SaveFile(LPCTSTR FileName) {
 int i;
 TCHAR Nummer[8];
 bool ret=true;
 WritePrivateProfileString(T("Liste"),NULL,NULL,FileName);	// leeren
 for (i=0; ; i++) {
  LVITEM lvi;
  PITEM pItem;
  lvi.mask=LVIF_PARAM;
  lvi.iItem=i;
  if (!ListView_GetItem(ghListWnd,&lvi)) break;	// lParam (Datenzeiger) holen
  pItem=(PITEM)lvi.lParam;
  wsprintf(Nummer,T("%i"),i);
  if (!WritePrivateProfileStruct(T("Liste"),Nummer,
    pItem,LOADITEMSIZE,FileName)) {
   ret=false;
  }
  if (!WritePrivateProfileString(T("Exec"),Nummer,
    pItem->ExecFileName,FileName)) ret=false;	// setzen oder löschen
 }
 wsprintf(Nummer,T("%i"),ListGetSelected());
 WritePrivateProfileString(T("Liste"),T("Select"),Nummer,FileName);
 WritePrivateProfileString(NULL,NULL,NULL,FileName);	// Flush (XP liefert immer FALSE)
 if (ret) SetModified(false);
 return ret;
}

void GetDefaultsFileName(PTSTR Name, UINT len) {
 PTSTR p;
 GetModuleFileName(0,Name,len);
 p=PathFindFileName(Name);
 len-=(UINT)(p-Name);
 lstrcpyn(p,T("Relais1.ini"),len);
}

// alles mögliche per WritePrivateProfileString() schreiben
// Sonderfall Format == NULL: Value = String (svw. LPCTSTR)
// Sonderfall Format == Value == NULL: Eintrag löschen
// Achtung: Format == "%s": Value = Zeiger auf LPCTSTR!
BOOL vWriteProfile(LPCTSTR FileName, LPCTSTR Section, LPCTSTR Key, LPCTSTR Format, va_list Value) {
 TCHAR buf[MAX_PATH];
 if (Format) {
  wvnsprintf(buf,elemof(buf),Format,Value);
  Value=(va_list)buf;
 }
 return WritePrivateProfileString(Section,Key,(LPCTSTR)Value,FileName);
}

BOOL _cdecl WriteProfile(LPCTSTR FileName, LPCTSTR Section, LPCTSTR Key, LPCTSTR Format, ...) {
 return vWriteProfile(FileName,Section,Key,Format,(va_list)(&Format+1));
}

void SaveDefaults(void) {
 TCHAR FileName[MAX_PATH];
 WINDOWPLACEMENT wp;
 int Widths[5],i;

 GetDefaultsFileName(FileName,elemof(FileName));
 InitStruct(&wp,sizeof(wp));
 GetWindowPlacement(ghMainWnd,&wp);
 vWriteProfile(FileName,T("MainWnd"),T("WinPos"),T("%i,%i,%i,%i"),(va_list)&wp.rcNormalPosition);
 for (i=0; i<5; i++) Widths[i]=ListView_GetColumnWidth(ghListWnd,i);
 vWriteProfile(FileName,T("MainWnd"),T("Widths"),T("%i,%i,%i,%i,%i"),(va_list)Widths);
 vWriteProfile(FileName,T("MainWnd"),T("LogFile"),NULL,(va_list)(*LogFileName?LogFileName:NULL));
 WriteProfile(FileName,T("MainWnd"),T("LptBase"),T("0x%X"),gLptBase);
 WriteProfile(FileName,T("MainWnd"),T("UseStbBsyAck"),T("%u"),gUseStbBsyAck);
 WriteProfile(FileName,T("MainWnd"),T("DoBeep"),T("%u"),gDoBeep);
//070402: Letzten Schaltzustand der Ausgänge merken
 WriteProfile(FileName,T("MainWnd"),T("LptState"),T("0x%X"),
   (GetCurTrigOutState()<<8)|GetCurRelState());
 vWriteProfile(FileName,T("MainWnd"),T("FileName"),NULL,(va_list)(*gFileName?gFileName:NULL));
 WriteProfile(FileName,T("MainWnd"),T("Modified"),T("%u"),gModified);
 if (!SaveFile(FileName)) MBox(ghMainWnd,MAKEINTRESOURCE(37)/*Fehler*/,MB_OK,FileName);
}

#define SIZE_SIZEBY 0xAFFE

// Kommaseparierte Integer-Liste in Array wandeln
int ScanInts(LPCTSTR s, int ints[], int elems) {
 int i=0;
 if (elems) do{
  *ints++=StrToInt(s);
  i++;
  s=StrChr(s,',');
  if (!s) break;
  s++;	// hinter Komma
 }while (--elems);
 return i;	// Anzahl gewandelter Elemente
}

// Vorgaben aus "relais1.ini" laden sowie Listen-Inhalt aus AlternateFileName (Kommandozeile)
void LoadDefaults(LPCTSTR AlternateFileName) {
 TCHAR FileName[MAX_PATH];
 TCHAR buf[MAX_PATH];
 int val;
 WINDOWPLACEMENT wp;
 POINT newsize;
 int Widths[5],i;
 InitStruct(&wp,sizeof(wp));
 GetWindowPlacement(ghMainWnd,&wp);
 gMinTrackSize.x=wp.rcNormalPosition.right-wp.rcNormalPosition.left;
 gMinTrackSize.y=wp.rcNormalPosition.bottom-wp.rcNormalPosition.top;
 
 GetDefaultsFileName(FileName,elemof(FileName));
 if (AlternateFileName && *AlternateFileName) {
  lstrcpyn(gFileName,AlternateFileName,elemof(gFileName));
  PathUnquoteSpaces(gFileName);
 }else{
  lstrcpyn(gFileName,FileName,elemof(gFileName));
  AlternateFileName=NULL;
 }
 if (LoadFile(gFileName)) {
// Fensterposition und -größe lesen
  if (GetPrivateProfileString(T("MainWnd"),T("WinPos"),
    T(""),buf,elemof(buf),FileName)
  && ScanInts(buf,&wp.rcNormalPosition.left,4)==4) {
// SetWindowPlacement ist Ursache für verschwindenden Fragezeichen-Knopf!
//   SetWindowPlacement(ghMainWnd,&wp);
// Deshalb hier: SetWindowPos
   SetWindowPos(ghMainWnd,0,
     wp.rcNormalPosition.left,
     wp.rcNormalPosition.top,
     wp.rcNormalPosition.right-wp.rcNormalPosition.left,
     wp.rcNormalPosition.bottom-wp.rcNormalPosition.top,
     SWP_NOZORDER);
// Fenster in Bildschirm ziehen...
   SendMessage(ghMainWnd,DM_REPOSITION,0,0);
   newsize.x=wp.rcNormalPosition.right-wp.rcNormalPosition.left;
   newsize.y=wp.rcNormalPosition.bottom-wp.rcNormalPosition.top;
   SendMessage(ghMainWnd,WM_SIZE,SIZE_SIZEBY,
     MAKELONG(newsize.x-gMinTrackSize.x,newsize.y-gMinTrackSize.y));
  }
  if (GetPrivateProfileString(T("MainWnd"),T("Widths"),
    T(""),buf,elemof(buf),FileName)
  && ScanInts(buf,Widths,5)==5) {
   for (i=0; i<5; i++) ListView_SetColumnWidth(ghListWnd,i,Widths[i]);
  }
  GetPrivateProfileString(T("MainWnd"),T("LogFile"),
    T(""),LogFileName,elemof(LogFileName),FileName);
  if (GetPrivateProfileString(T("MainWnd"),T("LptBase"),
    T(""),buf,elemof(buf),FileName)
  && StrToIntEx(buf,STIF_SUPPORT_HEX,&val)
  && val>0x100
  && val<=0xFFFF) gLptBase=(WORD)val;
  gUseStbBsyAck=GetPrivateProfileInt(T("MainWnd"),T("UseStbBsyAck"),
   1,FileName);
  InitLptState();
  gDoBeep=GetPrivateProfileInt(T("MainWnd"),T("DoBeep"),
   1,FileName);
  if (GetPrivateProfileString(T("MainWnd"),T("LptState"),
    T(""),buf,elemof(buf),FileName)
  && StrToIntEx(buf,STIF_SUPPORT_HEX,&val)
  && (unsigned)val<0x800) {
   SetCurRelState((BYTE)val);
   SetCurTrigOutState((BYTE)(val>>8));
  }
  if (!AlternateFileName) {
   GetPrivateProfileString(T("MainWnd"),T("FileName"),
     T(""),gFileName,elemof(gFileName),FileName);
   SetModified(GetPrivateProfileInt(T("MainWnd"),T("Modified"),0,FileName));
  }
 }else{
  PITEM pItem;
  InitLptState();
  pItem=NewItem(NULL);
  pItem->Flags=UHRZEIT;
  pItem->Uhrzeit.hour=12;
  pItem->RelState=0x5A;
  ListInsertItem(0,pItem,0);
  pItem=NewItem(NULL);
  pItem->Flags=DAUER;
  pItem->Dauer.sec=3;
  pItem->RelState=0xFF;
  ListInsertItem(1,pItem,0);
 }
}

void RegisterFileTypes(void) {		// verknüpft ".relay" mit diesem Programm
 DWORD l;
 TCHAR ExeName[MAX_PATH];
 l=GetModuleFileName(0,ExeName,elemof(ExeName)-5);
 lstrcpy(ExeName+l,T(" \"%1\"")); l+=5;
 RegSetValue(HKEY_CLASSES_ROOT,T(".relay"),REG_SZ,T("Relais-Steuerung"),16*sizeof(TCHAR));
 RegSetValue(HKEY_CLASSES_ROOT,T(".relay\\shell\\open\\command"),REG_SZ,ExeName,l*sizeof(TCHAR));
}

/************************
 * Dialog-Unterstützung *
 ************************/
 
void EnableWindows(HWND FirstWnd, UINT NumWnd, BOOL state) {
 for(;NumWnd;NumWnd--) {
  EnableWindow(FirstWnd,state);
  FirstWnd=GetNextSibling(FirstWnd);
 }
}

void EnableDlgItems(PITEM pItem) {
// Uhrzeit- oder Dauer-Dialogelemente
 BYTE type=pItem->Flags&TYPEMASK;
 EnableWindows(GetDlgItem(ghMainWnd,154),8,type==UHRZEIT || type==DAUER);
// Trigger-bezogene Dialogelemente
 EnableWindows(GetDlgItem(ghMainWnd,158),4,type==TRIGGER);
 EnableItem(53,type==UHRZEIT);
 EnableItem(72,pItem->Flags&BEGINLOOP?TRUE:FALSE);
}

void SetItemInt_TwoChar(UINT idEdit, BYTE n) {
 TCHAR buf[4];
 wsprintf(buf,T("%02u"),n);
 SetDlgItemText(ghMainWnd,idEdit,buf);	// generiert EN_CHANGE
 KillTimer(ghMainWnd,idEdit);		// Validation abbrechen
}

void FillEdits(PITEM pItem) {
 gEditErrors=0;				// Annahme: keine Fehler
 if (pItem) {
  EnableWindows(GetDlgItem(ghMainWnd,49),24,TRUE);	// alles EIN (flackert!?)
  CheckRadioButton(ghMainWnd,50,52,50+(pItem->Flags&TYPEMASK)-UHRZEIT);
  CheckDlgButton(ghMainWnd,53,pItem->Flags&RELATIVZEIT?1:0);
  SetItemInt_TwoChar(54,pItem->Uhrzeit.hour);
  SetItemInt_TwoChar(55,pItem->Uhrzeit.min);
  SetItemInt_TwoChar(56,pItem->Uhrzeit.sec);
  SetItemInt_TwoChar(57,pItem->Uhrzeit.hsec);
  SetDlgItemInt(ghMainWnd,58,pItem->Trigger.sourcebit,FALSE);
  KillTimer(ghMainWnd,58);		// Validation abbrechen
  CheckRadioButton(ghMainWnd,60,61,60+pItem->Trigger.polarity);
  CheckDlgButton(ghMainWnd,70,pItem->Flags&BEGINLOOP?1:0);
  CheckDlgButton(ghMainWnd,71,pItem->Flags&ENDLOOP?1:0);
  SetDlgItemInt(ghMainWnd,72,pItem->LoopCount,FALSE);
  SetDlgItemText(ghMainWnd,73,pItem->ExecFileName?pItem->ExecFileName:T(""));
  EnableDlgItems(pItem);
  KillTimer(ghMainWnd,72);
  KillTimer(ghMainWnd,73);
 }else{
  EnableWindows(GetDlgItem(ghMainWnd,49),24,FALSE);	// alles GRAU
 }
}

void UpdateStatusLine(void) {
 SYSTEMTIME st;
 int slen;
 long timediff;
 TCHAR buf[80],buf2[80];
// 1. Uhrzeit-Anzeige aktualisieren
 slen=LoadString(ghInstance,15/*Uhrzeit:*/,buf,elemof(buf));
 GetLocalTime(&st);
 GetTimeFormat(LOCALE_USER_DEFAULT,TIME_FORCE24HOURFORMAT,&st,NULL,
   buf+slen,elemof(buf)-slen);	// anhängen lassen
 SendMessage(ghStatusWnd,SB_GETTEXT,0,(LPARAM)buf2);
 if (lstrcmp(buf,buf2))	// nur bei Änderung Text setzen
   SendMessage(ghStatusWnd,SB_SETTEXT,0,(LPARAM)buf);
// 2. Laufzeit-Anzeige aktualisieren: momentane Laufzeit
 if (gSched.ListLine>=0) {
  slen=LoadString(ghInstance,32/*Laufzeit:*/,buf,elemof(buf));
  timediff=NowToMs()-gSched.start_ms;
  if (timediff<0) timediff+=MS_PER_DAY;
  StrFromTimeInterval(buf+slen,elemof(buf)-slen,timediff,6);
 }else{	// oder ermittelte Gesamtlaufzeit
  LOOPINFO li;
  slen=LoadString(ghInstance,16/*Gesamtlaufzeit:*/,buf,elemof(buf));
  li.line=li.depth=li.info=0;
  timediff=EstimateRunningTime(&li);
  if (li.info&INFO_ErrorMask) {
   slen+=LoadString(ghInstance,12/*Fehler: */,buf+slen,elemof(buf)-slen);
   slen+=LoadString(ghInstance,18+((li.info&INFO_ErrorMask)>>INFO_ErrorShift)-1,
     buf+slen,elemof(buf)-slen);
  }else{
   if (li.info&INFO_LongerThan) {
    slen+=LoadString(ghInstance,14/*mehr als*/,buf+slen,elemof(buf)-slen);
   }
   slen+=StrFromTimeInterval(buf+slen,elemof(buf)-slen,timediff,6);
   if (li.info&INFO_Infinite) {
    slen+=LoadString(ghInstance,13/* (pro Runde)*/,buf+slen,elemof(buf)-slen);
   }
  }
 }
 SendMessage(ghStatusWnd,SB_GETTEXT,1,(LPARAM)buf2);
 if (lstrcmp(buf,buf2))	// nur bei Änderung Text setzen
   SendMessage(ghStatusWnd,SB_SETTEXT,1,(LPARAM)buf);
}

int LoadFilterString(HINSTANCE hInstance, UINT uID, LPTSTR lpBuffer, int nBufferMax) {
 int ret;
 ret=LoadString(hInstance,uID,lpBuffer,nBufferMax);
 if (ret) do{
  lpBuffer=StrChr(lpBuffer,'|');
  if (lpBuffer) *lpBuffer++=0;
 }while (lpBuffer);
 return ret;
}

/******************
 * Scroll-Ziffern *
 ******************/
#define EN_QUERYMIN 0x1001	// unbeantwortet: 0
#define EN_QUERYMAX 0x1002	// per WM_COMMAND
#define EN_QUERYPAGE 0x1003	// unbeantwortet (0): 10
#define EN_QUERYWIDTH 0x1004	// unbeantwortet: 0

static void HandleScroll(HWND Wnd, int dir) {
 HWND Parent=GetParent(Wnd);
 UINT id=GetWindowLong(Wnd,GWL_ID);
 BOOL ok;
 int j,k;
 int i=GetDlgItemInt(Parent,id,&ok,TRUE);
 if (!ok) {
  MessageBeep(MB_ICONEXCLAMATION);
  return;	// keine Zahl
 }
 if (dir==-10 || dir==10) {
  j=(int)SendMessage(Parent,WM_COMMAND,MAKELONG(id,EN_QUERYPAGE),(LPARAM)Wnd);
  if (j) dir=dir/10*j;
 }
 j=(int)SendMessage(Parent,WM_COMMAND,MAKELONG(id,dir>0?EN_QUERYMAX:EN_QUERYMIN),
   (LPARAM)Wnd);
 k=i;
 i+=dir;
 if (dir>0){
  if (i>j) i=j;	// auf Maximum begrenzen
 }else{
  if (i<j) i=j;	// auf Minimum begrenzen
 }
 if (i==k) {
  MessageBeep(MB_ICONEXCLAMATION);
  return;	// Anschlag erreicht
 }
 j=(int)SendMessage(Parent,WM_COMMAND,MAKELONG(id,EN_QUERYWIDTH),(LPARAM)Wnd);
 if (j) {	// hier: nur 2 erlaubt (allgemein: schwierig)
  TCHAR s[8];
  wnsprintf1(s,elemof(s),T("%02u"),i);
  SetWindowText(Wnd,s);
 }else{
  SetDlgItemInt(Parent,id,i,TRUE);
 }
}

static WNDPROC DefEditProc;

static LRESULT CALLBACK UpDownEditProc(HWND Wnd,UINT Msg,WPARAM wParam,LPARAM lParam) {
 switch (Msg) {
  case WM_VSCROLL: switch (LOWORD(wParam)) {
   case SB_LINEUP:	HandleScroll(Wnd,1); break;
   case SB_LINEDOWN:	HandleScroll(Wnd,-1); break;
   case SB_PAGEUP:	HandleScroll(Wnd,10); break;
   case SB_PAGEDOWN:	HandleScroll(Wnd,-10); break;
  }break;
  case WM_KEYDOWN: switch (wParam) {
   case VK_UP:		HandleScroll(Wnd,1); return 0;	// Taste verschlucken
   case VK_DOWN:	HandleScroll(Wnd,-1); return 0;
   case VK_PRIOR:	HandleScroll(Wnd,10); return 0;
   case VK_NEXT:	HandleScroll(Wnd,-10); return 0;
  }break;
 }
 return CallWindowProc(DefEditProc,Wnd,Msg,wParam,lParam);
}

void SubclassForUpDown(HWND Wnd, int id) {
 DefEditProc=SubclassWindow(GetDlgItem(Wnd,id),UpDownEditProc);
}

/*********
 * Hilfe *
 *********/

// ToolInfoText an textlose Fenster "ankleben", der Text kommt mittels
// GetWindowText aus der Ressource. Nur für's Hauptfenster!
static void AddToolToImageButton(UINT id) {
 TOOLINFO ti;
 TCHAR buf[64];
 ti.cbSize=sizeof(ti);
 ti.uFlags=TTF_IDISHWND|TTF_SUBCLASS;
 ti.hwnd=ghMainWnd;
 (HWND)ti.uId=GetDlgItem(ghMainWnd,id);
 ti.lpszText=buf;
 GetWindowText((HWND)ti.uId,buf,elemof(buf));
 SendMessage(gToolTip,TTM_ADDTOOL,0,(LPARAM)&ti);
}

static void SendWmHelp(HWND Wnd, HWND Child, int MouseX, int MouseY) {
 HELPINFO hi;
 hi.cbSize=sizeof(hi);			// restliches HELPINFO auffüllen
 hi.iContextType=HELPINFO_WINDOW;
 hi.iCtrlId=GetDlgCtrlID(Child);
 hi.hItemHandle=Child;
 hi.dwContextId=GetWindowContextHelpId(Child);
 hi.MousePos.x=MouseX;
 hi.MousePos.y=MouseY;
 SendMessage(Wnd,WM_HELP,0,(LPARAM)&hi);
}

// Bei WM_CONTEXTMENU Menü mit "Direkthilfe" einblenden,
// bei dessen Anwahl WM_HELP generieren
bool HandleContextMenu(HWND Wnd, HWND Child, LPARAM lParam) {
 HMENU m;
 TCHAR s[64];
 if (Child==Wnd) return false;
 m=CreatePopupMenu();
 LoadString(ghInstance,40/*Direkthilfe*/,s,elemof(s));
 AppendMenu(m,0,101,s);
 if ((DWORD)lParam==(DWORD)-1) {	// per Tastatur
  RECT r;
  GetWindowRect(Child,&r);
  lParam=MAKELONG(r.left,r.bottom);
 }
 if (TrackPopupMenu(m,TPM_LEFTALIGN|TPM_TOPALIGN|TPM_RIGHTBUTTON|TPM_RETURNCMD,
   GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam),0,Wnd,NULL)==101) {
  SendWmHelp(Wnd,Child,GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam));
 }
 DestroyMenu(m);
 return true;
}

static void GetQuestionButtonRect(HWND Wnd, LPRECT r) {
 NONCLIENTMETRICS ncm;
 ncm.cbSize=sizeof(ncm);
 SystemParametersInfo(SPI_GETNONCLIENTMETRICS,sizeof(ncm),&ncm,0);
 GetWindowRect(Wnd,r);		// hier: X-Ausdehnung ermitteln
 r->left=r->right-r->left
   -GetSystemMetrics(SM_CYFRAME)
   -ncm.iCaptionHeight*4
   +1;
 r->right=r->left+ncm.iCaptionHeight-2;
 r->top=GetSystemMetrics(SM_CXFRAME)+2;
 r->bottom=r->top+ncm.iCaptionHeight-4;
}

static BOOL NcInQuestionRect(HWND Wnd, POINT p) {
 RECT r;
 GetWindowRect(Wnd,&r);
 p.x-=r.left;
 p.y-=r.top;
 GetQuestionButtonRect(Wnd,&r);
 return PtInRect(&r,p);
}

static BOOL InQuestionRect(HWND Wnd, POINT p) {
 ClientToScreen(Wnd,&p);
 return NcInQuestionRect(Wnd,p);
}

static void DrawQuestionButton(HWND Wnd, bool Pushed, bool Hot) {
 HDC dc=GetWindowDC(Wnd);
 RECT r;
 HTHEME th=0;
 HANDLE hLib;
 GetQuestionButtonRect(Wnd,&r);
 hLib=LoadLibraryA("uxtheme.dll");
 if (hLib) {
  th=(HTHEME(_stdcall*)(HWND,LPCWSTR))GetProcAddress(hLib,"OpenThemeData")(Wnd,L"WINDOW");
 }
 if (th) {	// Teletubbie-Optik
  HRESULT (_stdcall*pDrawThemeBackground)(HTHEME,HDC,int,int,const RECT*,const RECT*);
  (FARPROC)pDrawThemeBackground=GetProcAddress(hLib,"DrawThemeBackground");
  pDrawThemeBackground(th,dc,WP_HELPBUTTON,Pushed?HBS_PUSHED:Hot?HBS_HOT:HBS_NORMAL,&r,NULL);
  (HRESULT(_stdcall*)(HTHEME))GetProcAddress(hLib,"CloseThemeData")(th);
 }else{
  DrawFrameControl(dc,&r,DFC_CAPTION,Pushed?DFCS_CAPTIONHELP|DFCS_PUSHED:DFCS_CAPTIONHELP);
 }
 if (hLib) FreeLibrary(hLib);
 ReleaseDC(Wnd,dc);
}

static void PointHelpModalLoop(HWND Wnd) {
 MSG Msg;
 bool DoExit=false;
 SetCursor(LoadCursor(0,IDC_HELP));

 while (GetMessage(&Msg,0,0,0)) {
  switch (Msg.message) {
   case WM_KEYDOWN:
   switch (Msg.wParam) {
    case VK_TAB:
    case VK_ESCAPE:
    case VK_F1: DoExit=true; break;
   }break;
   case WM_LBUTTONDOWN: {
    POINT p={GET_X_LPARAM(Msg.lParam),GET_Y_LPARAM(Msg.lParam)};
    HWND Child=ChildWindowFromPointEx(Wnd,p,CWP_SKIPINVISIBLE|CWP_SKIPTRANSPARENT);
    ClientToScreen(Wnd,&p);
    SendWmHelp(Wnd,Child,p.x,p.y);
   }nobreak;
   case WM_MBUTTONDOWN:
   case WM_RBUTTONDOWN:
   case WM_SYSKEYDOWN: DoExit=true; break;
  }
  TranslateMessage(&Msg);
  DispatchMessage(&Msg);
  if (DoExit) break;
 }
}

static void QuestionModalLoop(HWND Wnd) {
 MSG Msg;
 bool Pushed=true;
 SetCapture(Wnd);
 DrawQuestionButton(Wnd,true,false);

 while (GetMessage(&Msg,0,0,0)) {
  switch (Msg.message) {
   case WM_MOUSEMOVE: {
    POINT p={GET_X_LPARAM(Msg.lParam),GET_Y_LPARAM(Msg.lParam)};
    bool InQuest=InQuestionRect(Wnd,p)!=0;
    if (Pushed ^ InQuest) {
     Pushed=InQuest;
     DrawQuestionButton(Wnd,Pushed,false);
    }
   }break;
   case WM_LBUTTONUP: {
    if (Pushed) {
     PointHelpModalLoop(Wnd);	// nächste modale Schleife
     DrawQuestionButton(Wnd,false,false);
    }
    ReleaseCapture();
   }return;			// modale Schleife verlassen
  }
  DispatchMessage(&Msg);
 }
}

// Filtert diverse NC-Nachrichten zur Darstellung und Behandlung des Hilfe-Fragezeichen-Knopfes
// Nur für Dialoge! Hier: Nicht für normale Fenster geeignet.
// Die Dialogprozedur muss TRUE zurückgeben, wenn diese Routine TRUE liefert.
BOOL HandleDlgNcMessagesForHelp(HWND Wnd,UINT Msg,WPARAM wParam,LPARAM lParam) {
 switch (Msg) {

  case WM_NCPAINT:
  case WM_NCACTIVATE: {
   SetWindowLong(Wnd,DWL_MSGRESULT,(LONG)DefWindowProc(Wnd,Msg,wParam,lParam));
   DrawQuestionButton(Wnd,false,false);
  }return TRUE;

  case WM_NCHITTEST: {
// 1. Verhindert das Verschieben des Fensters beim Knopf drücken
// 2. Liefert wParam==HTHELP bei WM_NCLBUTTONDOWN
   POINT p={GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam)};
   if (NcInQuestionRect(Wnd,p))
     return SetDlgMsgResult(Wnd,WM_NCHITTEST,HTHELP);
  }break;

  case WM_NCMOUSEMOVE: {	// Hot-Tracking (nur Teletubbie-Optik)
   volatile static bool Hot;
   if (Hot!=(wParam==HTHELP)) {
    Hot=!Hot;
    DrawQuestionButton(Wnd,false,Hot);
   }
  }break;

  case WM_NCLBUTTONDOWN:
  case WM_NCLBUTTONDBLCLK: {
   if (wParam==HTHELP) {QuestionModalLoop(Wnd); return TRUE;}	// Ereignis schlucken!!
  }break;
  
  case 0xAE:		// Als Ergebnis bleibt der Knopf beim inaktiven Fenster hervorgehoben
  case 0xAF: /*MessageBeep(0);*/ return TRUE;	// unbekannte XP-Nachrichten
			// scheinen die Titelleiste aufzuhellen (Kontrast reduzieren)

 }
 return FALSE;
}

/***********************
 * 2 Dialog-Prozeduren *
 ***********************/
 
BOOL CALLBACK SetupDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
  case WM_INITDIALOG: {
   const static WORD DefLpt[3]={0x378,0x278,0x3BC};
   HWND hCombo=GetDlgItem(Wnd,101);
   TCHAR buf[16];
   int i;
   wsprintf(buf,T("%Xh"),gLptBase);
   SetWindowText(hCombo,buf);	// Eingabezeile belegen
   for (i=0; i<3; i++) {
    wsprintf(buf,T("%Xh (LPT%u)"),DefLpt[i],i+1);
    ComboBox_AddString(hCombo,buf);
    if (DefLpt[i]==gLptBase) ComboBox_SetCurSel(hCombo,i);
   }	// Auswahl einer Voreinstellung, überschreibt Eingabezeile
   CheckDlgButton(Wnd,102,gUseStbBsyAck);
   CheckDlgButton(Wnd,103,gDoBeep);
  }return TRUE;
  
  case WM_COMMAND: switch (LOWORD(wParam)) {
   case IDOK: {
    TCHAR buf[8];
    int value;
    buf[0]='0'; buf[1]='x';
    GetDlgItemText(Wnd,101,buf+2,elemof(buf)-2);
    if (!StrToIntEx(buf,STIF_SUPPORT_HEX,&value) || value<0x100) {
     Wnd=GetDlgItem(Wnd,101);
     SetFocus(Wnd);
     ComboBox_SetEditSel(Wnd,0,-1);	// bei Fehler
     break;
    }
    gLptBase=(WORD)value;
    gUseStbBsyAck=(bool)IsDlgButtonChecked(Wnd,102);
    gDoBeep=(bool)IsDlgButtonChecked(Wnd,103);
   }nobreak;
   case IDCANCEL: {
    EndDialog(Wnd,wParam);
   }break;
  }break;

  case WM_HELP: {
   LPHELPINFO hi=(LPHELPINFO)lParam;
   DWORD ids[4];
   ids[0]=hi->iCtrlId;
   ids[1]=MAKELONG(hi->iCtrlId,1101);	// High-Teil = Ressourcen-ID des Dialogs
   ids[2]=ids[3]=0;
   WinHelp(hi->hItemHandle,gHelpFileName,HELP_WM_HELP,(DWORD_PTR)ids);
  }break;
  
  case WM_CONTEXTMENU: if (HandleContextMenu(Wnd,(HWND)wParam,lParam)) return TRUE; break;
 }
 return FALSE;
}

BOOL CALLBACK MainDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
if (HandleDlgNcMessagesForHelp(Wnd,Msg,wParam,lParam)) return TRUE;
 switch (Msg) {
  case WM_INITDIALOG: {
   const int StatusParts[]={100,450,-1};
   const int ListViewWidths[]={150,80,8*ITEMHEIGHT+2,3*ITEMHEIGHT+2,200};
   UINT id;
   ghMainWnd=Wnd;
   GetWindowText(Wnd,StdMBoxTitle,elemof(StdMBoxTitle));
   ghListWnd=GetDlgItem(Wnd,120);	// ListView
   ghStatusWnd=GetDlgItem(Wnd,102);	// Statuszeile
   for (id=54; id<=58; id++) SubclassForUpDown(Wnd,id);	// Auf/Ab-Steuerung dranhängen
   SubclassForUpDown(Wnd,72);
   gSched.ListLine=-1;			// angehalten
   gToolTip=CreateWindowEx(WS_EX_TOPMOST,TOOLTIPS_CLASS,NULL,
     WS_POPUP|TTS_NOPREFIX|TTS_ALWAYSTIP|TTS_BALLOON,0,0,0,0,Wnd,0,ghInstance,NULL);
   for (id=121; id<=124; id++) {	// vier bebilderte Knöpfe
    SendDlgItemMessage(Wnd,id,BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadImage(
      ghInstance,(LPCTSTR)id,IMAGE_BITMAP,16,15,LR_LOADMAP3DCOLORS));
    AddToolToImageButton(id);
   }
   MakeGdiObj();
   gOldButtonProc=SubclassWindow(GetDlgItem(Wnd,80/*LED Data*/),LedButtonSubclassProc);
   AddToolToImageButton(80);
   SubclassWindow(GetDlgItem(Wnd,83/*LED Status*/),LedButtonSubclassProc);
   AddToolToImageButton(83);
   SubclassWindow(GetDlgItem(Wnd,84/*LED Control*/),LedButtonSubclassProc);
   AddToolToImageButton(84);
   gOldLvProc=SubclassWindow(ghListWnd,LvSubclassProc);
//die Fensterunterklasse für die Status-LEDs ist nur für das Abbiegen von WM_ERASEBKGND.
   for (id=0; id<elemof(ListViewWidths); id++) {
    ListCreateColumn(id,7+id/*Zeit oder Trigger*/,ListViewWidths[id]);
   }
   
   SendMessage(ghStatusWnd,SB_SETPARTS,elemof(StatusParts),(LPARAM)(LPINT)StatusParts);
   SetTimer(Wnd,2,200,NULL);	// zur Aktualisierung der Statuszeile und der Trigger-LEDs
   
   RegisterFileTypes();		// verknüpft ".relay" mit diesem Programm
   LoadDefaults(PathGetArgs((PTSTR)lParam));	// ruft InitLptState auf - nach(!) dem Einlesen der Portadresse
   LogSetMenuCheck();
  }return TRUE;
  
  case WM_MEASUREITEM: switch (wParam) {
   case 120: ((LPMEASUREITEMSTRUCT)lParam)->itemHeight=ITEMHEIGHT;
  }break;

  case WM_DRAWITEM: switch (wParam) {
   case 80: LedButtonHandleDraw((LPDRAWITEMSTRUCT)lParam,GetCurRelState(),8,0); break;
   case 83: LedButtonHandleDraw((LPDRAWITEMSTRUCT)lParam,GetCurTrigInState(),3,2); break;
   case 84: LedButtonHandleDraw((LPDRAWITEMSTRUCT)lParam,GetCurTrigOutState(),3,4); break;
   case 120: HandleDrawList((LPDRAWITEMSTRUCT)lParam); break;
  }break;
  
  case WM_NOTIFY: switch (wParam) {

   case 120: switch (((LPNMHDR)lParam)->code) {
   
    case NM_SETFOCUS: {
     int i=ListGetSelected();
     if (i<0 && ListView_GetItemCount(ghListWnd)) i=0;
     if (i>=0) ListView_SetItemState(ghListWnd,i,LVIS_FOCUSED,LVIS_FOCUSED);
    }break;
    
    case NM_KILLFOCUS: {
     int i=ListView_GetNextItem(ghListWnd,-1,LVNI_FOCUSED);
     if (i>=0) ListView_SetItemState(ghListWnd,i,0,LVIS_FOCUSED);
    }break;
    
    case LVN_ITEMCHANGED: {
     LPNMLISTVIEW pnlv=(LPNMLISTVIEW)lParam;
     PITEM pItem;
     if (!(pnlv->uNewState&LVIS_SELECTED)) {
      _debug((T("unselektiert\n")));
      break;
     }
     pItem=ListGetItem(pnlv->iItem);
     if (pItem) gpSelectedItem=pItem;
     if (!gpSelectedItem) _debug((T("Murks2\n")));
     FillEdits(gpSelectedItem);
    }break;
   
    case NM_CLICK:
    case NM_DBLCLK: {	// LEDs schalten mit Maus
     LPNMITEMACTIVATE ia=(LPNMITEMACTIVATE)lParam;
     if (ia->iItem>=0 && ia->iSubItem>=2) {
      RECT r;
      int LedNummer;
      ListView_GetSubItemRect(ghListWnd,ia->iItem,ia->iSubItem,LVIR_BOUNDS,&r);
      LedNummer=(ia->ptAction.x-r.left)/(r.bottom-r.top);
      if ((unsigned)LedNummer<=7) {
       PITEM pItem=ListGetItem(ia->iItem);
       LedNummer=1<<LedNummer;	// Bitmaske daraus machen
       switch (ia->iSubItem) {
        case 2: pItem->RelState^=(BYTE)LedNummer; break;
        case 3: pItem->TrigOut ^=(BYTE)LedNummer; break;
       }
       ListView_RedrawItems(ia->hdr.hwndFrom,ia->iItem,ia->iItem);
      }
     }
     if (ia->iItem<0) {	// freier Bereich: neues Item erzeugen!
      int i;
      PITEM pItem=NewItem(gpSelectedItem);
      if (!pItem) break;	// Notbremse
      i=ListView_GetItemCount(ghListWnd);	// hinter das letzte Element
      ListInsertItem(i,pItem,LVIS_SELECTED);	// "selected" erzeugen
     }
    }break;
     
    case LVN_DELETEITEM: {	// anhängige Item-Struktur löschen
     LPNMLISTVIEW lv=(LPNMLISTVIEW)lParam;
     PITEM pItem=(PITEM)lv->lParam;
// Wenn gleich selektierte Struktur, dann nicht löschen (Zeile wird nur verschoben)
     if (pItem != gpSelectedItem) DeleteItem(pItem);
    }break;
   }
  }break;
  
// Könnte sein, dass die Dialogprozedur das bereits tut...  
  case WM_SYSCOLORCHANGE: {
   SendMessage(ghListWnd,Msg,wParam,lParam);
  }break;

  case WM_THEMECHANGED: {
   UINT id;
   for (id=121; id<=124; id++) {	// vier bebilderte Knöpfe
    DeleteBitmap(SendDlgItemMessage(Wnd,id,BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadImage(
      ghInstance,(LPCTSTR)id,IMAGE_BITMAP,16,15,LR_LOADMAP3DCOLORS)));
   }
  }break;
  
  case WM_DROPFILES: {
   HDROP hDrop=(HDROP)wParam;
   UINT nDrop=DragQueryFile(hDrop,(UINT)-1,NULL,0);
   if (nDrop!=1) MBox(Wnd,MAKEINTRESOURCE(31),MB_OK|MB_ICONEXCLAMATION,nDrop);
   DragQueryFile(hDrop,0,gFileName,elemof(gFileName));
   DragFinish(hDrop);
   LoadFile(gFileName);
  }break;
  
  case WM_COMMAND: switch (LOWORD(wParam)) {
//Menü:
   case 1001: {	// Datei laden
    OPENFILENAME ofn;
    TCHAR FilterBuf[256];
    InitStruct(&ofn,sizeof(ofn));
    ofn.hwndOwner=Wnd;
    ofn.lpstrFilter=FilterBuf;
    ofn.lpstrFile=gFileName;
    ofn.nMaxFile=elemof(gFileName);
    LoadFilterString(ghInstance,34,FilterBuf,elemof(FilterBuf));
    ofn.Flags=OFN_FILEMUSTEXIST|OFN_HIDEREADONLY;
    ofn.lpstrDefExt=T("relay");
    if (GetOpenFileName(&ofn)) LoadFile(gFileName);
   }break;
   
   case 1002: {	// Datei speichern
    OPENFILENAME ofn;
    TCHAR FilterBuf[256];
    InitStruct(&ofn,sizeof(ofn));
    ofn.hwndOwner=Wnd;
    ofn.lpstrFilter=FilterBuf;
    ofn.lpstrFile=gFileName;
    ofn.nMaxFile=elemof(gFileName);
    LoadFilterString(ghInstance,34,FilterBuf,elemof(FilterBuf));
    ofn.Flags=OFN_PATHMUSTEXIST|OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT;
    ofn.lpstrDefExt=T("relay");
    if (GetSaveFileName(&ofn) && !SaveFile(gFileName))
      MBox(Wnd,MAKEINTRESOURCE(36),MB_OK);
   }break;
   
   case 1003: {	// Log-Datei
    OPENFILENAME ofn;
    TCHAR FilterBuf[256];
    InitStruct(&ofn,sizeof(ofn));
    ofn.hwndOwner=Wnd;
    ofn.lpstrFilter=FilterBuf;
    ofn.lpstrFile=LogFileName;
    ofn.nMaxFile=elemof(LogFileName);
    LoadFilterString(ghInstance,35,FilterBuf,elemof(FilterBuf));
    ofn.Flags=OFN_PATHMUSTEXIST|OFN_HIDEREADONLY;
    ofn.lpstrDefExt=T("log");
    if (GetSaveFileName(&ofn)) {
     LogSetMenuCheck();
     if (hLog!=INVALID_HANDLE_VALUE) LogOpen();	// altes Log schließen, neues öffnen
    }
   }break;
   
   case 1004: {	// keine Log-Datei
    LogFileName[0]=0;
    LogSetMenuCheck();
   }break;
   
   case 1005: {	// Vorgabe speichern
    SaveDefaults();
   }break;
   
   case 1009: {	// Beenden
    SendMessage(Wnd,WM_CLOSE,0,0);
   }break;

   case 1101: {	// Einstellungen
    DialogBox(ghInstance,MAKEINTRESOURCE(1101),Wnd,SetupDlgProc);
   }break;
   
   case 1901: {	// Hilfe
    WinHelp(Wnd,gHelpFileName,HELP_CONTENTS,0);
   }break;
   
   case 1902: {	// Kontexthilfe
    SetCapture(Wnd);
    PointHelpModalLoop(Wnd);
    ReleaseCapture();
   }break;
   
   case 1909: {	// Über
    MBox(Wnd,MAKEINTRESOURCE(26),MB_OK);
   }break;

//Dialogelemente:
   case 50:	// Umschalten auf "Uhrzeit"
   case 51:	// Umschalten auf "Dauer"
   case 52: {	// Umschalten auf "Trigger"
    if (!gpSelectedItem) break;
    gpSelectedItem->Flags&=~TYPEMASK;
    gpSelectedItem->Flags|=(BYTE)(UHRZEIT+wParam-50);
    EnableDlgItems(gpSelectedItem);
    SendMessage(Wnd,WM_TIMER,wParam==52?58:54,0);
    ListInvalidateItem(SELECTED);
   }break;
   
   case 53: {	// Auswahlfeld "relative Uhrzeit"
    if (!gpSelectedItem) break;
    if (IsDlgButtonChecked(Wnd,53)) gpSelectedItem->Flags|=RELATIVZEIT;
    else gpSelectedItem->Flags&=~RELATIVZEIT;
    ListInvalidateItem(SELECTED);
   }break;
   
   case 70:
   case 71: {
    if (!gpSelectedItem) break;
    if (IsDlgButtonChecked(Wnd,70)) gpSelectedItem->Flags|=BEGINLOOP;
    else gpSelectedItem->Flags&=~BEGINLOOP;
    if (IsDlgButtonChecked(Wnd,71)) gpSelectedItem->Flags|=ENDLOOP;
    else gpSelectedItem->Flags&=~ENDLOOP;
    EnableDlgItems(gpSelectedItem);
    ListInvalidateItem(SELECTED);
   }break;
   
   case 54:
   case 55:
   case 56:
   case 57:
   case 58:
   case 72:
   case 73: switch (GET_WM_COMMAND_CMD(wParam,lParam)) {
    case EN_CHANGE: SetTimer(Wnd,LOWORD(wParam),200,NULL); break;
    case EN_QUERYMIN: if (LOWORD(wParam)==58) return SetDlgMsgResult(Wnd,WM_COMMAND,1); break;
    case EN_QUERYMAX: {
     BYTE max;
     switch (LOWORD(wParam)) {
      case 54:			// Stunden
      case 72: max=255; break;	// Schleifenzahl
      case 55:			// Minuten
      case 56: max=59; break;	// Sekunden
      case 57: max=99; break;	// Hundertstel
      case 58: max=3; break;	// Trigger-Eingang
     }
     return SetDlgMsgResult(Wnd,WM_COMMAND,max);
    }break;
    case EN_QUERYWIDTH: switch (LOWORD(wParam)) {
     case 54:
     case 55:
     case 56:
     case 57: return SetDlgMsgResult(Wnd,WM_COMMAND,2);
    }break;
   }break;
   
   case 60:	// wenn L
   case 61: {	// wenn H
    if (!gpSelectedItem) break;
    gpSelectedItem->Trigger.polarity=(BYTE)(wParam-60);
    ListInvalidateItem(SELECTED);
   }break;
   
   case 74: {	// Knopf "Dateiauswahl"
    OPENFILENAME ofn;
    TCHAR FilterBuf[256];
    TCHAR FileName[MAX_PATH];
    TCHAR Parameters[MAX_PATH];
    InitStruct(&ofn,sizeof(ofn));
    ofn.hwndOwner=Wnd;
    ofn.lpstrFilter=FilterBuf;
    ofn.lpstrFile=FileName;
    ofn.nMaxFile=elemof(FileName);
    ofn.Flags=OFN_FILEMUSTEXIST|OFN_HIDEREADONLY;
    GetDlgItemText(Wnd,73,FileName,elemof(FileName));
    lstrcpy(Parameters,PathGetArgs(FileName));	// Argumente retten
    PathRemoveArgs(FileName);
    PathUnquoteSpaces(FileName);
    LoadFilterString(ghInstance,33,FilterBuf,sizeof(FilterBuf));
    if (GetOpenFileName(&ofn)) {
#ifdef UNICODE    
     if (PathIsExe(FileName)) {
#else
     WCHAR uName[MAX_PATH];
     MultiByteToWideChar(CP_ACP,0,FileName,-1,uName,elemof(uName));
     if (PathIsExe(uName)) {
#endif
      PathQuoteSpaces(FileName);
      if (Parameters[0]) {
       StrCatBuff(FileName,T(" "),elemof(FileName));
       StrCatBuff(FileName,Parameters,elemof(FileName));
      }
     }
     SetDlgItemText(Wnd,73,FileName);
    }
   }break;
   
   case 80: {	// "Knopf" mit 8 LEDs, liefert BN_BIT0HIT..BN_BIT7HIT
    unsigned NumLed=GET_WM_COMMAND_CMD(wParam,lParam)-BN_BIT0HIT;
		// Der folgende Vergleich filtert vor allem BN_CLICKED aus!
    if (NumLed<8) SetCurRelState(GetCurRelState()^(1<<NumLed));
    else if (NumLed==BN_BITALL0-BN_BIT0HIT) SetCurRelState(0x00);
    else if (NumLed==BN_BITALL1-BN_BIT0HIT) SetCurRelState(0xFF);
   }break;
   
   case 81: {	// alle EIN
    SetCurRelState(0xFF);
   }break;
   
   case 82: {	// alle AUS
    SetCurRelState(0);
   }break;
   
   case 84: {	// "Knopf" mit 3 LEDs, liefert BN_BIT0HIT..BN_BIT2HIT
    unsigned NumLed=GET_WM_COMMAND_CMD(wParam,lParam)-BN_BIT0HIT;
    if (NumLed<3) SetCurTrigOutState(GetCurTrigOutState()^(1<<NumLed));
    else if (NumLed==BN_BITALL0-BN_BIT0HIT) SetCurTrigOutState(0x00);
    else if (NumLed==BN_BITALL1-BN_BIT0HIT) SetCurTrigOutState(0xFF);
   }break;

   case 91: {	// Starten
    LOOPINFO li;
    li.line=li.depth=li.info=0;
    EstimateRunningTime(&li);
    if (li.info&INFO_ErrorMask) {
     TCHAR s[256];
     LoadString(ghInstance,18+((li.info&INFO_ErrorMask)>>INFO_ErrorShift)-1,s,elemof(s));
     if (MBox(Wnd,MAKEINTRESOURCE(38),MB_YESNO|MB_DEFBUTTON2,s)!=IDYES) break;
    }
    SchedStart();
    if (SchedCalcWait()) {	// kann stoppen, wenn keine Zeile da!
     ListInvalidateItem(gSched.ListLine);
     SetTimer(Wnd,1,gSched.settimer_ms,NULL);
    }
   }break;
   
   case 92: {	// Anhalten
    KillTimer(Wnd,1);
    SchedStop(true);
   }break;
   
   case 93: {	// Schritt
    if (gSched.ListLine<0) SendMessage(Wnd,WM_COMMAND,91,0);
    else SchedHandleNext(TRUE);
   }break;

   case 120: {
#ifdef DEBUG
    Beep(400,100);	// Seit wann liefert eine ListView ein WM_COMMAND?
#endif
   }break;
   
   case 121: {	// Neue Zeile
    int i;
    PITEM pItem=NewItem(gpSelectedItem);
    if (!pItem) break;	// Notbremse
    i=ListGetSelected();
    if (i<0) i=0;
    ListInsertItem(i,pItem,LVIS_SELECTED);	// "selected" erzeugen
   }break;
   
   case 122: {	// Zeile löschen
    int i=ListGetSelected();
    int k=ListView_GetItemCount(ghListWnd);
    if (i<0) {
     MessageBeep(MB_ICONHAND);
     break;	// nichts selektiert: nichts löschen
    }
    gpSelectedItem=NULL;
    ListView_DeleteItem(ghListWnd,i);	// LVN_DELETEITEM löscht den Speicher
    k--;	// Eine Zeile weniger
    if (k) {	// Noch mindestens eine Zeile vorhanden?
     if (i==k) i--;	// Letzte Zeile gelöscht: vorherige (sonst nächste) markieren
     ListView_SetItemState(ghListWnd,i,LVIS_SELECTED,LVIS_SELECTED);
    }else FillEdits(NULL);	// alle Eingabefelder grau
   }break;
   
   case 123: {	// Zeile runter
    int i=ListGetSelected();
    int k=ListView_GetItemCount(ghListWnd);
    if (!gpSelectedItem || gpSelectedItem!=ListGetItem(i)) {
     _debug((T("Murks! i=%d k=%d %p %p\n"),i,k,gpSelectedItem,ListGetItem(i)));
     break;
    }
    if (i<0) {
     MessageBeep(MB_ICONHAND);
     break;	// nichts selektiert: nichts schieben
    }
    if (i+1==k) break;	// schon letztes Element, nicht schieben
    ListView_DeleteItem(ghListWnd,i);
    i++;
    ListInsertItem(i,gpSelectedItem,LVIS_SELECTED);
   }break;
   
   case 124: {	// Zeile hoch
    int i=ListGetSelected();
    int k=ListView_GetItemCount(ghListWnd);
    if (i<0) {
     MessageBeep(MB_ICONHAND);
     break;	// nichts selektiert: nichts schieben
    }
    if (i==0) break;	// schon erstes Element, nicht schieben
    ListView_DeleteItem(ghListWnd,i);
    i--;
    ListInsertItem(i,gpSelectedItem,LVIS_SELECTED);
   }break;
  }break;
  
  case WM_TIMECHANGE: {
// Falls Programm läuft und gerade auf "Uhrzeit" wartet, Timer umstellen!
  }break;
  
  case WM_TIMER: switch (wParam) {
// Nicht schlecht wäre auch das "Überleben" eines Reboot-Vorgangs!
   case 1:
    SchedHandleNext(FALSE); break;
   
   case 2: {
    UpdateStatusLine();
    GetCurTrigInState();	// rote LEDs bei Änderung aktualisieren
   }break;
   
// Editfenster-Validierung und Übernahme in die Liste
   case 54:
   case 55:
   case 56:
   case 57:
   case 58:
   case 72: {
    UINT u;
    BOOL b;
    UINT bits1,bits2;
    KillTimer(Wnd,wParam);	// nicht-zyklisch!
    u=GetDlgItemInt(Wnd,wParam,&b,FALSE);
    if (b) switch (wParam) {	// begrenzen
     case 54: if (u>=7*24)b=FALSE; break; // <1 Woche (<256)
     case 55:				  // <60 Minuten
     case 56: if (u>=60)  b=FALSE; break; // <60 Sekunden
     case 57: if (u>=100) b=FALSE; break; // <100 Hundertstel
     case 58: if (u<1||u>3)b=FALSE; break; // max. Eingänge 1..3 am Parallelport
     case 72: if (u>255)  b=FALSE; break;
    }
    bits1=1<<(wParam-54);	// Fehler-Bit ermitteln
    bits2=gEditErrors;
    if (b) {
     PITEM pItem=ListGetItem(SELECTED);
     gEditErrors&=~bits1;
     if (!pItem) break;		// sollte nicht passieren (Notbremse)
     switch (wParam) {
      case 54: pItem->Uhrzeit.hour=(BYTE)u; break;
      case 55: pItem->Uhrzeit.min =(BYTE)u; break;
      case 56: pItem->Uhrzeit.sec =(BYTE)u; break;
      case 57: pItem->Uhrzeit.hsec=(BYTE)u; break;
      case 58: pItem->Trigger.sourcebit=(BYTE)u; break;
      case 72: pItem->LoopCount   =(BYTE)u; break;
     }
     ListInvalidateItem(SELECTED);
    }else gEditErrors|=bits1;
    if (gEditErrors!=bits2) InvalidateItem(wParam,NULL,TRUE);
   }break;
   case 73: {	// Ausführungs-Dateiname und -Parameter (immer "fehlerfrei")
    PITEM pItem=ListGetItem(SELECTED);
    UINT BufChr;
    KillTimer(Wnd,wParam);		// einmalig
    if (!pItem) break;			// Notbremse
    if (pItem->ExecFileName) {
     LocalFree(pItem->ExecFileName);	// alten Text freigeben
     pItem->ExecFileName=NULL;
    }
    BufChr=SendDlgItemMessage(Wnd,wParam,WM_GETTEXTLENGTH,0,0);
    if (BufChr) {	// bei leerem String NULL-Zeiger belassen
     BufChr++;		// für "\0"
     pItem->ExecFileName=LocalAlloc(LPTR,BufChr*sizeof(TCHAR));
     if (!pItem->ExecFileName) break;	// Notbremse
     GetDlgItemText(Wnd,wParam,pItem->ExecFileName,BufChr);	// neuen Text laden
    }
    ListInvalidateItem(SELECTED);
   }break;
  }break;
  
  case WM_CTLCOLOREDIT: {
   int id=GetDlgCtrlID((HWND)lParam)-54;
   if ((unsigned)id>=sizeof(gEditErrors)*8) break;
   if (!(gEditErrors&(1<<id))) break;
   SetBkMode((HDC)wParam,TRANSPARENT);
   SetBkColor((HDC)wParam,RGB(255,128,128));
  }return (BOOL)gGdiObj.hbrError;
  
  case WM_SIZE: {
   static LPARAM PrevSize;	// Client-Größe!
   if (wParam==SIZE_MINIMIZED) break;
   if (ghStatusWnd && wParam!=SIZE_SIZEBY) SendMessage(ghStatusWnd,Msg,wParam,lParam);
   if (PrevSize || wParam==SIZE_SIZEBY) {	// zweites (oder späteres) WM_SIZE: Items nachführen
    RECT r;
    HDWP dwp=BeginDeferWindowPos(9);
    int i;
// Bits: 0=SchiebeX, 1=SchiebeY, 2=SizeX, 3=SizeY
    static const BYTE MoveInfo[9]={1,1,1,1,0,0x0C,2,2,2};
    HWND w=GetDlgItem(Wnd,121);	// Erstes zu verschiebendes Fenster
    BYTE info;
    SIZE SizeChange;
    if (wParam==SIZE_SIZEBY) PrevSize=0;
    SizeChange.cx=GET_X_LPARAM(lParam)-GET_X_LPARAM(PrevSize);
    SizeChange.cy=GET_Y_LPARAM(lParam)-GET_Y_LPARAM(PrevSize);
    for (i=0; i<9; i++) {
     info=MoveInfo[i];
     GetWindowRect(w,&r);
     r.right-=r.left;	// = Breite
     r.bottom-=r.top;	// = Höhe
     ScreenToClient(Wnd,(LPPOINT)&r);	// .left und .top in Clientkoordinaten
     if (info&1) r.left  +=SizeChange.cx;
     if (info&2) r.top   +=SizeChange.cy;
     if (info&4) r.right +=SizeChange.cx;
     if (info&8) r.bottom+=SizeChange.cy;
     dwp=DeferWindowPos(dwp,w,0,r.left,r.top,r.right,r.bottom,
       SWP_DRAWFRAME|SWP_NOZORDER);
     w=GetNextSibling(w);
    }
    EndDeferWindowPos(dwp);
   } // gMinTrackSize wird bereits von LoadSettings() gesetzt
   if (wParam!=SIZE_SIZEBY) PrevSize=lParam;
   else {
    RECT r;
    GetClientRect(Wnd,&r);
    PrevSize=MAKELONG(r.right-r.left,r.bottom-r.top);
   }
  }break;
  
  case WM_GETMINMAXINFO: {
   ((LPMINMAXINFO)lParam)->ptMinTrackSize=gMinTrackSize;
  }break;
  
  case WM_MENUSELECT: {
#ifdef WIN32
 WMMenuSelect(HIWORD(wParam)&MF_POPUP
 ?(UINT)GetSubMenu((HMENU)lParam,LOWORD(wParam))
 :LOWORD(wParam),HIWORD(wParam));
#else
 WMMenuSelect(wParam,LOWORD(lParam));
#endif
  }break;
  
  case WM_HELP: {
   LPHELPINFO hi=(LPHELPINFO)lParam;
   DWORD ids[4];
   ids[0]=hi->iCtrlId;
   ids[1]=MAKELONG(hi->iCtrlId,100);	// High-Teil = Ressourcen-ID des Dialogs
   ids[2]=ids[3]=0;
   WinHelp(hi->hItemHandle,gHelpFileName,HELP_WM_HELP,(DWORD)ids);
	// HELP_WM_HELP positioniert besser als HELP_CONTEXTPOPUP
  }break;
  
  case WM_CONTEXTMENU: if (HandleContextMenu(Wnd,(HWND)wParam,lParam)) return TRUE; break;  
  
  case WM_QUERYENDSESSION: {
   if (gSched.ListLine>=0) switch (MBox(Wnd,
     MAKEINTRESOURCE(25),MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2)) {
    case IDNO: /*SetWindowLong(Wnd,DWL_MSGRESULT,0);*/ return TRUE;
   }		// Windows nicht beenden!
  }break;
  
  case WM_ENDSESSION: {
   SaveDefaults();
  }break;
  
  case WM_CLOSE: {
   if (!SendMessage(Wnd,WM_QUERYENDSESSION,0,0)) return TRUE;	// kein EndDialog
   SendMessage(Wnd,WM_ENDSESSION,1,0);
   WinHelp(Wnd,gHelpFileName,HELP_QUIT,0);
   EndDialog(Wnd,0);
  }break;
  
  case WM_DESTROY: {
   KillGdiObj();
  }break;
 }
 return FALSE;
}

/******************************
 * Hauptprogramm (lächerlich) *
 ******************************/
 
int _stdcall WinMainCRTStartup(void) {
 WNDCLASS wc={
  CS_HREDRAW|CS_VREDRAW,
  DefDlgProc,
  0,	// ClsExtra
  DLGWINDOWEXTRA,
  ghInstance=GetModuleHandle(NULL),	// hInstance
  LoadIcon(ghInstance,MAKEINTRESOURCE(100)),	// hIcon
  LoadCursor(0,IDC_ARROW),	// hCursor
  (HBRUSH)(COLOR_WINDOW+1),
  NULL,
  T("Relais1")};	// Klassenname (in Übereinstimmung mit Dialog-Ressource 100)

 RegisterClass(&wc);
 InitCommonControls();
 ExitProcess(DialogBoxParam(0,MAKEINTRESOURCE(100),0,MainDlgProc,(LPARAM)GetCommandLine()));
}
Detected encoding: ASCII (7 bit)8