Source file: /~heha/basteln/PC/oszi/oszi.zip/src/oszi.cpp

//#define OEMRESOURCE
#include "wutils.h"
#include "miniwnd.h"
#include "objekte.h"
#include <stdio.h>
#include "quelle.h"
#include <math.h>	// fabs, log, floor, exp
//#include "inout.h"
//#include "dso.h"

/*Zu verarbeitende Tasten (Idee):
 1..9	Kanalumschaltung (Fokusrechteck)
 A,B	Zeitbasis (Fokusrechteck)
 T	Triggerung (Fokus)
 M	Messung (Fokus)
 Pfeil links-rechts		Zeitbasis
 Shift+Pfeil links-rechts	Triggerverzögerung
 Strg+Pfeil links-rechts	Triggerverzögerung
 Pfeil hoch/runter		Y-Ablenkung (fokussierter Kanal)
 Shift+Pfeil hoch/runter	Y-Position
 Strg+Pfeil hoch/runter		Triggerpegel
 TAB		Fokus weiterschalten
 Shift+TAB	Fokus zurückschalten
 (  c		Kopplung weiterschalten
 (  Shift+C	Kopplung zurückschalten
 (  Strg+C	Kopplungs-Menü
 =		Gleichspannungskopplung
 ~		Wechselspannungskopplung
 _		Massekopplung
 p		Tastkopf weiterschalten
 ( Shift+P	Tastkopf zurückschalten
 ( Strg+P	Tastkopf-Menü
 !		Tastkopf 1:1/10:1
 #		Tastkopf-Dialog
 -		Invertierung
 Leer, Pause	Stop,Start
 Einfg, Enter	Kanal-Trace als Referenz speichern
 Entf		Referenzspeicher löschen
*/
#ifdef WIN32
#define WriteStruct WritePrivateProfileStruct
#define GetStruct GetPrivateProfileStruct
#else
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
extern "C" {	// Suffix X: Windows 3x, Suffix Y: Win9x
BOOL WINAPI WritePrivateProfileStructX(LPCSTR,LPCSTR,LPVOID,UINT,LPCSTR);
BOOL WINAPI GetPrivateProfileStructX(LPCSTR,LPCSTR,LPVOID,UINT,LPCSTR);
BOOL WINAPI CheckMenuRadioItemX(HMENU m,UINT i,UINT j,UINT k,UINT p);
BOOL WINAPI SetMenuDefaultItemX(HMENU m,UINT i,UINT p);
BOOL WINAPI WritePrivateProfileStructY(LPCSTR,LPCSTR,LPVOID,UINT,LPCSTR);
BOOL WINAPI GetPrivateProfileStructY(LPCSTR,LPCSTR,LPVOID,UINT,LPCSTR);
BOOL WINAPI CheckMenuRadioItemY(HMENU m,UINT i,UINT j,UINT k,UINT p);
BOOL WINAPI SetMenuDefaultItemY(HMENU m,UINT i,UINT p);
}
BOOL WINAPI (*WriteStruct)(LPCSTR,LPCSTR,LPVOID,UINT,LPCSTR)	=WritePrivateProfileStructX;
BOOL WINAPI (*GetStruct)(LPCSTR,LPCSTR,LPVOID,UINT,LPCSTR)	=GetPrivateProfileStructX;
BOOL WINAPI (*CheckMenuRadioItem)(HMENU,UINT,UINT,UINT,UINT)	=CheckMenuRadioItemX;
BOOL WINAPI (*SetMenuDefaultItem)(HMENU,UINT,UINT)		=SetMenuDefaultItemX;
#endif

HWND MainWnd;		// Hauptfenster
HMENU MainMenu;
HMENU OsziMenu;		// Der komplett dynamische Menü-Teil
HWND hSetupDlg;		// Nichtmodaler Setup-Dialog
HWND hDisplayDlg;
HWND hKBHand;		// Dialog-Keyboard-Handler (für nichtmodale Dialoge)


#if 0
HWND Tooltip;
#endif
TOOLTIP *tip;
COLORREF CustColors[16];
TCHAR sDecimal[2];	// Dezimaltrennzeichen von Windows
TCHAR sDezimal[2];	// mein Dezimaltrennzeichen ('.', ',' oder '?')
TCHAR sMikro[2];	// Mikro-Zeichen (µ, in Arabien, Russland: u)
TCHAR sKanalFormat[8];	// Format-String für Kanal (Standard: Y%d)
int   iKanalStart;	// Nummer des ersten Kanals (Standard: 1)
TCHAR WindowTitle[64];
TCHAR StdProfile[MAX_PATH];	// mit Pfad, Zusammensetzung in WinMain
TCHAR HelpFileName[]=T("OSZI.HLP");
static const TCHAR sProzentI[]=T("%i,%i,%i,%i,%i");
#define sInt (sProzentI+12)	// wie T("%d")
#define sLeer (sProzentI+14)	// wie T("")
static const TCHAR IniFileName[] =T("OSZI.INI");
static const TCHAR WndClassName[]=T("OSZI");
static const TCHAR HauptSektion[]=T("Elbe");	// Nicht der Fluss aus dem Fichtelgebirge
static const TCHAR S_Position[]  =T("Position");
WORD  wPrefix=MAKEWORD(-4,3);	// piko bis Giga

const int NumZeitMenu=1;	// = Anzahl an Zeitbasen
const int NumTrigMenu=1;	// Anzahl Trigger (stets 1) (grau im Rollbetrieb)
int NumKanalMenu=1;		// Anzahl Kanäle im Menü
// max. MaxKanalMenu+1(!), dann heißt der letzte Eintrag "Weitere Kanäle..."
int MaxKanalMenu;		// =16 von INI-Datei, mindestens 4
HBRUSH EditBrushes[3];		// "wichtigste" Farbe mit höchstem Index
ATOM atom;	// zum Zugriff auf Hintergrundfarbe bei Edit-Feldern (GetProp)
HWND hZeitDlg;
int ZeitDlgCurSel;	// aktuelle Auswahl im Zeitbasis-Dialog
//HWND hTrigDlg;	// Zukunftsmusik
HWND hKanalDlg;

UINT wm_helpmsg;
// Globale Daten zur Interpretation der Sample-Daten (außer Samplerate)
WAVEHDR WaveHdr;	// Zeiger auf ganz viele(?) Sampledaten
			// mit unkonventioneller Belegung der Felder!
#define GetSample st.getsample
//GETSAMPLE GetSample;	// Funktionszeiger zum Sample lesen
#define BlockAlign st.blockalign
//UINT BlockAlign;	// Größe eines Samples für alle Kanäle
float FullScale;	// 1<<bits, wirklich float??
// RECT InvalRect;	// für begrenztes Neuzeichnen

typedef enum {ZUFALL=1,DSO220,SOUND} EDatenquelle;
EDatenquelle Datenquelle;

/*********************************
 ** Weitere allgemeine Routinen **
 *********************************/
/*
BOOL a2f(PCTSTR s, float&z) {
 PTSTR p;
 p=StrChr((PTSTR)s,',');      // Ein Komma durch Punkt ersetzen
 if (p) *p='.';
 return _stscanf(s,T("%f"),&z)==1?TRUE:FALSE;
}

BOOL GetDlgItemFloat(HWND Wnd, UINT id, float&z) {
// Gleitkommazahl (auch mit Komma statt Punkt) vom Dialogelement holen
 TCHAR s[32];
 GetDlgItemText(Wnd,id,s,elemof(s));
 if (a2f(s,z)) return TRUE;
 SetEditFocus(Wnd,id);
 return FALSE;
}

void SetDlgItemFloat(HWND Wnd, UINT id, int stellen, float z) {
// Gleitkommazahl in Dialogelement setzen
 char buf[32];
 sprintf(buf,"%*G",stellen,z); SetDlgItemTextA(Wnd,id,buf);
}
*/

PTSTR AfterTab(PTSTR buf) {
 PTSTR p=StrChr(buf,'\t');
 if (!p) { p=buf+lstrlen(buf); *p='\t'; }
 return ++p;
}

HMENU CopyPopupMenu(HMENU m, UINT inc);	// zur Rekursion

void CopyMenuItem(HMENU sm, UINT si, HMENU dm, UINT di, UINT inc) {
// Menüpunkt vom Menü <sm>, Position <si>, zum Menü <dm> Position <di>
// kopieren, dabei Menü-ID um <inc> erhöhen. Ggf. werden ganze
// Popup-Untermenüs kopiert, siehe CopyPopupMenu().
// Bitmaps (MF_BITMAP) und Checkmark-Bitmaps werden zz. nicht mit kopiert!
 TCHAR buf[64];
 UINT state,id;
 HMENU sub;
 GetMenuString(sm,si,buf,elemof(buf),MF_BYPOSITION);
 sub=GetSubMenu(sm,si);
 state=GetMenuState(sm,si,MF_BYPOSITION);
 if (sub) {
  state&=0xFF;		// nur LOW-Byte ist gültig,
  state|=MF_POPUP;	// "checked" kann es ja trotzdem sein!
  id=(UINT)CopyPopupMenu(sub,inc);
 }else id=GetMenuItemID(sm,si)+inc;
 InsertMenu(dm,di,MF_BYPOSITION|state,id,buf);
}

HMENU CopyPopupMenu(HMENU m, UINT inc) {
// Erstellt vom Popup-Menü <m> eine Kopie mit um <inc> erhöhten Menu-IDs.
 int j=GetMenuItemCount(m);
 HMENU n=CreatePopupMenu();
 for (int i=0; i<j; i++) CopyMenuItem(m,i,n,(UINT)-1,inc);
 return n;
}

void SetMenuPopup(HMENU m, UINT pos, HMENU popup) {
// Verändert NUR das Popup-Menü-Handle; Win32-Version extra!
#ifdef WIN32
 MENUITEMINFO mii;
 mii.cbSize=sizeof(mii);
 mii.fMask=MIIM_SUBMENU;
 mii.fType=MFT_STRING;
 mii.hSubMenu=popup;
 SetMenuItemInfo(m,pos,TRUE,&mii);
#else
 TCHAR s[64];
 GetMenuString(m,pos,s,elemof(s),MF_BYPOSITION);
 ModifyMenu(m,pos,MF_BYPOSITION|MF_POPUP,(UINT)popup,s);
#endif
}

void SetMenuString(HMENU m, UINT pos, PCTSTR s) {
// Verändert NUR den String des Menü-Eintrags; Win32: via SetMenuItemInfo
#ifdef WIN32
 MENUITEMINFO mii;
 mii.cbSize=sizeof(mii);
 mii.fMask=MIIM_TYPE;
 mii.fType=MFT_STRING;
 mii.dwTypeData=(LPTSTR)s;
 SetMenuItemInfo(m,pos,TRUE,&mii);
#else
 UINT id=(UINT)GetSubMenu(m,pos);
 UINT state=GetMenuState(m,pos,MF_BYPOSITION);
 if (id) {
  state&=0xFF;		// nur LOW-Byte ist gültig,
  state|=MF_POPUP;	// "checked" kann es ja trotzdem sein!
 }else id=GetMenuItemID(m,pos);
 ModifyMenu(m,pos,MF_BYPOSITION|state,id,s);
#endif
}

void SetMenuStringAfterTab(HMENU m, UINT pos, PCTSTR a) {
// Setzt den Teilstring hinter dem Tabulator; eigentlich Extra für &->&&?
 TCHAR s[64];
 int l=GetMenuString(m,pos,s,elemof(s),MF_BYPOSITION);
/*
 if (l>=64-1-lstrlen(a))
  return;
 if (s[l]) {
  _asm nop;
 }
 s[l]=0;	// Windows-Bug töten*/
 lstrcpy(AfterTab(s),a);
 SetMenuString(m,pos,s);
}

void AppendAndereToMenu(HMENU m, UINT id) {
// Hängt den String "&andere..." an das (Popup-)Menü an
// Für X-Ablenkung, Samplerate und Y-Ablenkung
 TCHAR s[64];
 LoadString(HInstance,205/*andere...*/,s,elemof(s));
 InsertMenu(m,(UINT)-1,MF_BYPOSITION,id,s);
}

void MenuRadioNextDefault(HMENU m, UINT sub, UINT item) {
// CheckMenuRadioItem für's ganze Menü, sowie nächstes "enabeltes" Item
// zur Vorgabe machen
 m=GetSubMenu(m,sub);
 sub=GetMenuItemCount(m);
 CheckMenuRadioItem(m,0,sub-1,item,MF_BYPOSITION);
 for (UINT i=item;;) {	// Vom nächsten Item an 1x rundherum
  i++;
  if (i>=sub) i=0;
  if (!(GetMenuState(m,i,MF_BYPOSITION)&(MF_DISABLED|MF_GRAYED))) {
   SetMenuDefaultItem(m,i,MF_BYPOSITION);
   break;
  }
  if (i==item) break;
 }
} 

void MenuEnable(HMENU m, UINT pos, UINT mask) {
// Wie EnableMenuGroup; bei Deaktivieren des Häkchens wird das nächste
// aktive Bit eingestellt und WM_COMMAND verschickt
 m=GetSubMenu(m,pos);
 pos=GetMenuItemCount(m);
 EnableMenuGroup(m,0,pos-1,mask,MF_BYPOSITION);
 for (UINT i=0; i<pos; i++) {
  UINT st=GetMenuState(m,i,MF_BYPOSITION);
  if (st&MF_GRAYED && st&MF_CHECKED) {	// Fall vorgefunden
   for (UINT j=i; ; ) {
    j++;
    if (j>=pos) j=0;
    if (!(GetMenuState(m,j,MF_BYPOSITION)&MF_GRAYED)) {
     SendMessage(MainWnd,WM_COMMAND,GetMenuItemID(m,j),0);
     break;
    }
    if (j==i) break;
   }
  }
 }
}

bool FarbAuswahl(HWND w,COLORREF&cr) {
 CHOOSECOLOR cc;
 InitStruct(&cc,sizeof(cc));
 cc.hwndOwner=w;
 cc.rgbResult=cr;
 cc.lpCustColors=CustColors;
 cc.Flags=CC_RGBINIT|CC_SHOWHELP|CC_FULLOPEN;
 if (!ChooseColor(&cc)) return false;
 cr=cc.rgbResult;
 return true;
}

int String2Index(PCTSTR p, PCTSTR n) {
// Ermittelt String-Nummer von <n> in String-Liste <p>, -1 bei keinem Treffer
// p zeigt auf aneinandergekettete nullterminierte Strings, mit Extra-Null am Ende
// Der String-Vergleich erfolgt case-insensitiv (lstrcmpi)
 for (int i=0; *p; p+=lstrlen(p)+1,i++) if (!lstrcmpi(n,p)) return i;
 return -1;
}
PCTSTR Index2String(PCTSTR p, int i) {
// Gegenfunktion, liefert String zum Index, NULL wenn <i> ungültig
 for (;*p;p+=lstrlen(p)+1,i--) if (!i) return p;
 return NULL;
}
int hsz2Index(HSZ*p, int hszlen, HSZ n) {
//Liefert nullbasierten Index des String-Handles <n>
 for (int i=0; i<hszlen; i++,p++) if (!DdeCmpStringHandles(n,*p)) return i;
 return -1;
}
int String2UpDown(PCTSTR s,int sm,int big) {
// Liefert <big> für "++" oder "+?" ohne gedrückte SHIFT-Taste,
// liefert <small> für "+" oder "+?" mit gedrückter SHIFT-Taste,
// bei '-' statt '+' negative Werte, andernfalls 0
 int r=0;
 switch (s[0]) {
  case '+':
  case '-': switch (s[1]) {
   case '?': if (s[2]) break; r=GetKeyState(VK_SHIFT)<0?sm:big; break;
   case 0: r=sm; break;
   default: if (s[0]==s[1] && !s[2]) r=big;
  }
 }
 if (s[0]=='-') r=-r;
 return r;
}
/*
BYTE PointIntoRect(const RECT*rc, POINT*pt) {
// Zieht Punkt ins Rechteck und markiert jede der Bewegung im Rückgabewert
 BYTE r=0;
 if (pt->x< rc->left)  {pt->x=rc->left;   r|=1;}	// links
 if (pt->x>=rc->right) {pt->x=rc->right;  r|=2;}	// rechts
 if (pt->y< rc->top)   {pt->y=rc->top;    r|=4;}	// oben
 if (pt->y>=rc->bottom){pt->y=rc->bottom; r|=8;}	// unten
 return r;
}
*/
COLORREF MixColor(COLORREF c1, COLORREF c2, int f1=128) {
// Farbe c1 und c2 komponentenweise mischen, mit f1 als Gewicht für
// 1. Farbe (0 = nur 2. Farbe, 256 = nur 1. Farbe)
 COLORREF r;
 int f2=256-f1;	// 2. Faktor
#define KOMP(cr) ((PBYTE)&(cr))	// Farbkomponenten als Array[0..2]
 for (int i=0; i<3; i++) KOMP(r)[i]=HIBYTE(KOMP(c1)[i]*f1+KOMP(c2)[i]*f2);
 KOMP(r)[3]=0;
#undef KOMP
 return r;
}

/*******************************************
 ** Spielereien mit dem Hintergrundraster **
 *******************************************/

void _stdcall Line(HDC dc, int x1, int y1, int x2, int y2) {
#ifdef _M_IX86
 Polyline(dc,(POINT*)&x1,2);
#else
 POINT p[2]={{x1,y1},{x2,y2}};
 Polyline(dc,p,2);
#endif
}

void Inval(bool background, LPRECT rcitem) {
//Bildschirm/Doppelpuffer-Bitmap ungültig machen;
//Triple-Puffer-Bitmap nur wenn background=true
//LPRECT für korrekte NULL-Übergabe!
 if (background) DispOpt|=DO_TB_INVAL;
 DispOpt|=DO_DB_INVAL;
 InvalidateRect(MainWnd,rcitem,FALSE);
}


POINT WinBitmapExt={16,16};
#if 0
HBITMAP WinBitmaps[]={
 (HBITMAP)OBM_LFARROW,(HBITMAP)OBM_LFARROWD,(HBITMAP)OBM_LFARROWI,
 (HBITMAP)OBM_RGARROW,(HBITMAP)OBM_RGARROWD,(HBITMAP)OBM_RGARROWI,
 (HBITMAP)OBM_UPARROW,(HBITMAP)OBM_UPARROWD,(HBITMAP)OBM_UPARROWI,
 (HBITMAP)OBM_DNARROW,(HBITMAP)OBM_DNARROWD,(HBITMAP)OBM_DNARROWI,
 (HBITMAP)OBM_CLOSE};

void LoadWinBitmaps(void) {
 int i;
 BITMAP bm;
 for (i=0; i<elemof(WinBitmaps); i++)
   WinBitmaps[i]=LoadBitmap(0,MAKEINTRESOURCE(WinBitmaps[i]));
 GetObject(WinBitmaps[0],sizeof(bm),&bm);
 WinBitmapExt.x=bm.bmWidth;
 WinBitmapExt.y=bm.bmHeight;
}
void FreeWinBitmaps(void) {
 int i;
 for (i=0; i<elemof(WinBitmaps); i++) DeleteObject(WinBitmaps[i]);
}

void PaintWinBitmap(HDC dc,int x, int y, HBITMAP bm) {
 HDC src=CreateCompatibleDC(dc);
 HBITMAP obm=(HBITMAP)SelectObject(src,bm);
 BitBlt(dc,x,y,WinBitmapExt.x,WinBitmapExt.y,src,0,0,SRCCOPY);
 SelectObject(src,obm);
 DeleteDC(src);
}
#endif
void SetBetrag(float&z,float&q) {	// Vorzeichen von <z> behalten, q muss positiv sein!
 z=z<0?-q:q;
}
// Gleitkommazahlen sind mit ihrem Rundungseffekt die blanke Katastrophe!!
static const float Reihe[]={	// von 1n bis 1G, sind 55 Werte
 1E-9F,2E-9F,5E-9F,1E-8F,2E-8F,5E-8F,1E-7F,2E-7F,5E-7F,
 1E-6F,2E-6F,5E-6F,1E-5F,2E-5F,5E-5F,1E-4F,2E-4F,5E-4F,
 1E-3F,2E-3F,5E-3F,1E-2F,2E-2F,5E-2F,1E-1F,2E-1F,5E-1F,
 1E+0F,2E+0F,5E+0F,1E+1F,2E+1F,5E+1F,1E+2F,2E+2F,5E+2F,
 1E+3F,2E+3F,5E+3F,1E+4F,2E+4F,5E+4F,1E+5F,2E+5F,5E+5F,
 1E+6F,2E+6F,5E+6F,1E+7F,2E+7F,5E+7F,1E+8F,2E+8F,5E+8F,
 1E+9F};

int NextFloat(float &v, int updown) {
// Liefert zu gegebener Zahl die nächste oder vorherige im "Raster"
// Ermittelt die nächste (<updown>=1) oder vorhergehende (<updown>=-1)
// Zahl im 1-2-5-Raster, im obigen Raster. Das Vorzeichen wird beibehalten.
// Bei <updown>=0 wird zum nächsten 1-2-5-Raster aufgerundet.
// Liefert Reihen-Index (braucht man kaum!)
 int m;		// Wüste halbieren: Halbwüste: ich trau' mich vorerst nicht!
 float z=(float)fabs(v);
 for (m=0;m<elemof(Reihe);m++) {
  if (z<Reihe[m]) break;	// durchaus mit m=0 möglich, m=größerer Index!
  if (z==Reihe[m]) goto exakt;
 }
 if (updown>0) updown--; // wenn dazwischen: m ist schon "nächster" Index
exakt:
 m+=updown;
 if (m<0) z=0;				// Notbremse 1
 else if (m>=elemof(Reihe)) z=1E38F;	// Notbremse 2
 else z=Reihe[m];
 SetBetrag(v,z);
 return m;
}
// Zweite Version mit Begrenzung
void NextFloat(float &v, int updown, float limit) {
 NextFloat(v,updown);
 if (updown>=0) {
  if (fabs(v)>limit) SetBetrag(v,limit);
 }else{
  if (fabs(v)<limit) SetBetrag(v,limit);
 }
}

static TCHAR ISO_Prefixes[]=T("afpnµm kMGTPE"); // µ ist nicht "const"!
static const double log1000=6.907755278982137;	//log(1000);
TCHAR MakePrefix(float number, float&multiplier, WORD minmax) {
/* Ausgehend von <number> wird ein Präfix ausgewählt,
 * der eine Wiedergabe der physikalischen Größe mit 1..3
 * Vorkommastellen erlaubt.
 * In <multiplier> landet der Wert zum "Behandeln" der Daten vor Ausgabe.
 * das kommt für dieses Problem zupass.
 * Unsauber ist das Problem "Kilogramm", die Basiseinheit ist hier "Gramm",
 * die "Tonne" erfordert Extrawürste beim Aufrufer.
 * Bei bestimmten Zeichensätzen (Kyrillisch, Arabisch) ist die Umsetzung
 * von µ in u bzw. in Symbolschriftart oder besser Unicode erforderlich.
 */
 int i;
 i=number?rndint(log(fabs(number))/log1000-0.5):0;
			//Typecast ohne floor() rundet zu Null: falsch!
 if (i<(signed char)LOBYTE(minmax)) i=0; // Präfix unerwünscht
 if (i>(signed char)HIBYTE(minmax)) i=0;
 multiplier=(float)exp(-i*log1000);
 return ISO_Prefixes[i+6];
}

PTSTR Prefix2Ptr(TCHAR c) {	// Präfix korrigieren und Pointer liefern
 switch (c) {
  case 'µ':
  case 'u': return ISO_Prefixes+4;
  case 0:
  case '\'': c=' ';		// Apostroph zur Unterdrückung des Vorsatzes
 }				// bspw. bei "mol", "at", "atm"
 return StrChr(ISO_Prefixes,c);
}

TCHAR GetDecimal() {	// aktuelles Dezimaltrennzeichen nehmen
 if (*sDezimal!='?') return *sDezimal;
 return *sDecimal;
}

void Float2String(PTSTR s, float v, WORD minmax, PCTSTR Einheit,int precis=3) {
 TCHAR prefix[]=T("\0");	// 2 Zeichen mit Nullen
 TCHAR *p, buf[16];
 float multiplier;
 *prefix=MakePrefix(v,multiplier,minmax);
 if (*prefix==' ') *prefix=0;	// kein String
 _sntprintf(buf,elemof(buf),T("%.*G"),precis,v*multiplier);
 p=StrChr(buf,'.');
 if (p) *p=GetDecimal(); // Ersetzen Punkt durch Komma (nach Systemsteuerung)
 wsprintf(s,T("%s %s%s"),buf,prefix,Einheit);
}

bool String2Float(PCTSTR s, float&z, PTSTR e) {
// Konvertiert Zahl, ggf. Einheitenvorsatz und Einheit, nach <z> und <e>
// Maximale Länge der Einheit (ohne Vorsatz): 3 Zeichen, (Puffergröße=4)
 PTSTR p;
 double v;
 int i,j,k,l;
 p=StrChr((PTSTR)s,GetDecimal());	// Das benutzerdefinierte Zeichen ...
 if (!p) p=StrChr((PTSTR)s,',');	// oder ein Komma ...
 if (p) *p='.';			// durch Punkt ersetzen
 l=lstrlen(s);
 i=_stscanf(s,T("%lf%n %") ELENSTR T("s%n"),&v,&j,e,&k);
 e[ELEN-1]=0;		// zwangsterminieren
 if (i==1 && j==l) {	// nur Zahl gegeben: OK
  *e=0; goto okay;
 }
 if (i==2 && k==l && *e) {	// Zahl und Einheit...
  if (e[1]) {		// Vorsatz möglich? (Nur mit Einheit! Bspw: m=Meter)
   p=Prefix2Ptr(*e);	// Gefahr: min -> Milli-Inch
   if (p) {
    j=int(p-ISO_Prefixes)-6;
    v*=exp(j*log1000);		// mit »float v« gibt's Rundungsfehler!
    lstrcpy(e,e+1);
   }
  }
  okay: z=(float)v;
  return true;
 }
 return false;
}

/*********************************************
 ** Strukturen, die zu Klassen mutierten... **
 *********************************************/

// globale Version fürs Testen...
bool GetNameList(PCTSTR n, PTSTR buf, BYTE*bitnr) {
// Namen mit Nullen trennen, Liste mit Doppelnull anschließen (max. 16 Bytes),
// Wertigkeit zu <bitnr> speichern (Zeiger darf NULL sein, wenn ungewünscht)
// Zur Feststellung von Gruppenzugehörigkeit.
 int i,bit;
 TCHAR hack[32];
 PTSTR p,q;
 lstrcpyn(hack,n,elemof(buf));	// in den Zerhack-Puffer
 n=hack;
 for (;;) {
  bit=0;			// Ohne Angabe Bit=0 setzen
  while (*n==' ') n++;	// Führende Leerzeichen übergehen
  if (!*n) break;		// Stringende erreicht
  p=StrChr(n,' ');
  if (p) *p=0;	// am Leerzeichen zerhacken
  q=StrChr(n,':');
  if (q) {
   *q=0;	// am Doppelpunkt zerhacken
   if (_stscanf(q+1,sInt,&bit)!=1) return false; // falsche Bitnummer
  }
  i=lstrlen(n);
  if ((unsigned)(i-1)>6) return false;	// Name zu kurz oder zu lang
  lstrcpy(buf,n);
  buf+=i+1;			// Ziel vorrücken
  if (bitnr) *bitnr++=(BYTE)bit;
  if (p) n=p+1;			// Quelle vorrücken
  else break;			// Stringende erreicht
 }
 *buf=0;	// Doppel-Null
 return true;
}


int flimit(float f, int u, int o) {	// Runden und begrenzen
 if (f>o) return o;
 if (f<u) return u;
 return rndint(f);
}

int rund(float f) {
 return flimit(f,-30000,30000);
}

ZEIT *zeit;		// notfalls mehrere (Zeitbasen)
TRIGG *trig;		// davon gibt's nur ein Exemplar!
int numkanal;
KANAL *kanal=NULL;	// Kanal-Darstellungs-Objekte (<numkanal> Stück)
QUELLE *quelle=NULL;	// Datenquelle-Objekt (1 Stück)

// Ich komme wohl doch nicht ohne eine Gruppen-Liste aus...
int numgruppe;
GRUPPE *gruppe;		// Gruppen-Objekte (bloß Namen), <numkanal> Stück!

void MacheGruppenListe() {
// aktualisiert <numgruppe> und <gruppe> an Hand der Kanal-Namen:
// Bei nur einem Kanal gibt es gar keine Gruppen,
// ab zwei am Ende stets die Gruppe "Alle Kanäle / Beide Kanäle"
 GRUPPE *gp;
 KANAL *kp,*kj;
 TCHAR buf[32];
 PTSTR p;
 int i,j,k;
 numgruppe=0;
 if (numkanal<=1) return;
 gp=gruppe;	// vorn mit dem Füllen anfangen
 if (!gp) return;	// Puffer noch nicht angefordert (beim Lesen der .INI)
 for (i=0,kp=kanal; i<numkanal-1; i++,kp++) {
  if (!kp->GetNameList(buf,NULL)) continue;
  for (p=buf; *p; p+=lstrlen(p)+1) {
   for (j=i+1; j<numkanal; j++) {
    kj=kanal+j;
    if (!kj->HatName(p)) continue;
    for (k=0; k<numgruppe; k++) {
     if (!lstrcmp(gruppe[k].name,p)) {
      gruppe[k].ref++;
      goto schondrin;
     }
    }
    lstrcpyn(gp->name,p,elemof(gp->name));
    gp->ref=2;
    gp++; numgruppe++;
    if (numgruppe==numkanal) return;
    schondrin:;
   }
  }
 }
 gp->name[0]=0; gp->ref=numkanal;	// Zuletzt "alle/beide Kanäle" anhängen
 numgruppe++;
}
#if 0
bool GRUPPE::SetNulllinie(PTSTR s,PTSTR t) {
 float z,v;
 EINHEIT e;
 if (!String2Float(s,z,e)) return false;
 if (e[0] && lstrcmp(e,einheit)) return false;	// falsche Einheit
 if (!String2Float(t,v,e)) return false;
 if (e[0] && lstrcmp(e,einheit)) return false;	// falsche Einheit
 for (int i=0; i<numkanal; i++) {
  if (kanal[i].HasName(name)) {
   if (!kanal[i].SetNulllinie(z)) return false;
   z+=v;	// nächste Nulllinie
  }
 }
 return true;
}
#endif
static const TCHAR sKopplung1[]=T("DC\0AC\0GND\0");
static const TCHAR sKopplung2[]=T("=~_");	// Kurzform Kopplung
static const TCHAR sTastkopf2[]=T(" !#");	// Kurzform Tastkopf

int TextWidth(HDC dc, PCTSTR s, int slen) {	// Hauptversion mit DC und Länge
 POINT p;
#ifdef WIN32
 GetTextExtentPoint32(dc,s,slen,(PSIZE)&p);
#else
 *(DWORD*)&p=GetTextExtent(dc,s,slen);
#endif
 return p.x;
}
int TextWidth(HDC dc, PCTSTR s, PCTSTR e) {	// Version mit DC und Ende-Zeiger
 return TextWidth(dc,s,int(e-s));
}
int TextWidth(HDC dc, PCTSTR s) {		// Version mit DC und Nullterminierung
 return TextWidth(dc,s,lstrlen(s));
}
int TextWidth(PCTSTR s) {			// Version ohne DC, mit Nullterminierung
 HDC dc=GetDC(MainWnd);
 int len=TextWidth(dc,s);
 ReleaseDC(MainWnd,dc);
 return len;
}

ANZEIGE::ANZEIGE():MINIWND(&Anker[1]) {
 RECT r;
 SetRect(&r,0,0,0,0);
 nagel=new MYBUTTON(this,&r,T("Systemmenü (Strg+Leertaste)"),0,MYBUTTON::EINAUS);
}

void ANZEIGE::Paint(HDC dc) {
 MINIWND::Paint(dc);
 SetBkColor(dc,state&STA_SELECTED?MixColor(AuswahlFarbe,BackColor,0x20):BackColor);
 SelectFont(dc,GetStockFont(SYSTEM_FONT));
}

bool ANZEIGE::RelayMsg(UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
  case WM_SIZE: {
   nagel->Inval();
   SetRect(&nagel->rcitem,rcitem.left-WinBitmapExt.x,
     rcitem.top,rcitem.left,rcitem.top+WinBitmapExt.y);
   nagel->Moved();
// Eigentlich: bei unten liegenden Fenstern Systemmenü nach unten ausrichten;
// bei RTL-System an rechte Kante setzen, niemals außerhalb des Fensters
// sowie Test auf "herausfallende" Fenster!
  }break;
  case WM_MOUSEMOVE: {
   MINIWND::RelayMsg(Msg,wParam,lParam);
   hitkode=0;
   if (!(state&0x20)) return false;
   int x=MAKEPOINTS(lParam).x-rcitem.left;
   for (int i=0; i<elemof(grenzen); i++)
     if (x<grenzen[i]) {hitkode=(BYTE)(HK_PRI+i); break;}
  }return false;
 }return MINIWND::RelayMsg(Msg,wParam,lParam);
}

void ANZEIGE::setTextColor(HDC dc, COLORREF farbe) {
 SetTextColor(dc,state&0x20?farbe:MixColor(farbe,BackColor));
}

void KANALANZEIGE::Update(BYTE was) {
 if (was&(WAS_YVOR|WAS_YINV|WAS_YABL|WAS_YKOP)) {
  TCHAR buf[32];
  knopf[0]->Enable(k->div<k->max);	// Rück-Verbindung...
  knopf[1]->Enable(k->min<k->div);
  k->GetVar(2,buf);	// Ablenkung
  wnsprintf(string1,elemof(string1),T("%c %s = %s %c"),
    sTastkopf2[k->GetTastkopfIndex()],
    k->name,
    buf,
    sKopplung2[k->kopplung]);
  HDC dc=GetDC(MainWnd);
  int spclen2=TextWidth(dc,sTastkopf2,1)/2;	// Breite halbes Leerzeichen
  grenzen[0]=TextWidth(dc,string1,1)+spclen2;	// Tastkopf-Bereich
  PTSTR p=StrChr(string1,'=');
  grenzen[1]=TextWidth(dc,string1,p);	// Kanalname-Bereich
  p+=2; if (*p=='-') p++;
  grenzen[2]=TextWidth(dc,string1,p);	// Vorzeichen-Bereich
  p=StrChr(p,0)-2;
  grenzen[3]=TextWidth(dc,string1,p);	// Ablenkungs-Bereich
  p+=2;
  grenzen[4]=TextWidth(dc,string1,p);	// Kopplungs-Bereich
  ReleaseDC(MainWnd,dc);
  int diff=rcitem.left+grenzen[4]-rcitem.right;
  if (diff) {
   rcitem.right+=diff;
   Moved();				// "heißes" Rechteck nachführen!
   knopf[0]->Schieb(diff,0,true);
   knopf[1]->Schieb(diff,0,true);
  }
 }
 if (was&(WAS_YVOR|WAS_YINV|WAS_YABL|WAS_YKOP|WAS_FARBE)) Inval();
}

KANALANZEIGE::KANALANZEIGE(KANAL*k) {
 this->k=k;
 grenzen[4]=16;
 string1[0]=0;
 RECT r;
// M_CREATESTRUCT mcs;	// SCHEUSSLICH!!
// mcs.p=&Anker[1];	// Hintergrund
 SetKnopfRect(&rcitem,-1);	// Hier: Initiale Position
#ifdef WIN32
 hint=LPSTR_TEXTCALLBACK;
#endif
 id=k->idhigh;
 style=0;		// vorerst nicht verschiebbar!
// MINIWND::Init(&mcs); // Problem: Der Defaultkonstruktor wurde schon gerufen
 SetKnopfRect(&r,0);
 knopf[0]=new MYBUTTON(this,&r,MAKEINTRESOURCE(30)/*T("Größerer Koeffizient  (Pfeil runter)")*/,
   k->idhigh+IDC_YABL+elemof(Reihe)+1,MYBUTTON::TIEFER);
 SetKnopfRect(&r,1);
 knopf[1]=new MYBUTTON(this,&r,MAKEINTRESOURCE(31)/*T("Kleinerer Koeffizient  (Pfeil rauf)")*/,
   k->idhigh+IDC_YABL+elemof(Reihe)+2,MYBUTTON::HOEHER);
}
void KANALANZEIGE::Paint(HDC dc) {
 ANZEIGE::Paint(dc);
 setTextColor(dc,k->farbe);
 ExtTextOut(dc,rcitem.left,rcitem.top,ETO_OPAQUE,&rcitem,
   string1,lstrlen(string1),0);
}
bool KANALANZEIGE::RelayMsg(UINT Msg,WPARAM wParam,LPARAM lParam) {
 switch (Msg) {
#if 0
  case WM_NOTIFY: {
    static const TCHAR Text[]=T("Tastteiler\0Kanal/Gruppe\0")
      T("Invertierung\0Ablenkkoeffizient (pro Teilstrich)\0Kopplung\0");
   if (!hitkode) break;
#define ttt ((LPTOOLTIPTEXT)lParam)
   if (ttt->hdr.code==TTN_NEEDTEXT)
   ttt->lpszText=(LPTSTR)Index2String(Text,hitkode&0x3F);
#undef ttt
  }break;
#endif
  case WM_SIZE: {
   Inval(); SetKnopfRect(&rcitem,-1); Moved();
   knopf[0]->Inval(); SetKnopfRect(&knopf[0]->rcitem,0); knopf[0]->Moved();
   knopf[1]->Inval(); SetKnopfRect(&knopf[1]->rcitem,1); knopf[1]->Moved();
  }break;
  case WM_MOUSEMOVE: {
   TCHAR s[256];	// Aua, jedes MouseMove!
   LoadString(HInstance,24,s,elemof(s));
   tip->SetTip(&rcitem,Index2String(s,hitkode&0x3F));
  }break;
  case WM_RBUTTONDOWN: {
   HMENU m=0;
   switch (hitkode) {
    case HK_PRI+0: m=GetSubMenu(k->submenu,SUB_YVOR); break;
    case HK_PRI+3: m=GetSubMenu(k->submenu,SUB_YABL); break;
    case HK_PRI+4: m=GetSubMenu(k->submenu,SUB_YKOP); break;
   }
   if (m) ShowPopupMenu(m,MAKEPOINTS(lParam).x,MAKEPOINTS(lParam).y);
  }break;
 }
 return ANZEIGE::RelayMsg(Msg,wParam,lParam);
}

void KANALANZEIGE::SetKnopfRect(RECT*r,int idx) {
 if (idx<0) {	// Eigenes Rechteck
  r->left=k->kn*160+20;
  r->top =ClientExt.y-20;
  r->right=r->left+grenzen[4];
  r->bottom=r->top+WinBitmapExt.y;
  return;
 }
 int x=rcitem.right;
 int y=rcitem.top;
 SetRect(r,x,y,x+WinBitmapExt.x,y+WinBitmapExt.y);
 if (!idx) return;
 OffsetRect(r,WinBitmapExt.x,0);
}

static int _fastcall GetSampleDc(KANAL*k,LPSTR p) {
 return GetSample(p,k->maske);
}
static int _fastcall GetSampleAc(KANAL*k,LPSTR p) {
 int y=GetSample(p,k->maske)-GET_SUB(k->sub);
 k->sub+=y;		// Nachkommastellen in <sub> aufheben!
 return y;
}
static int _fastcall GetSampleGnd(KANAL*,LPSTR) {
 return 0;
}

void KANAL::Konstruktor(int kn) {
 this->kn=kn;
 idhigh=kn<<10;
 wnsprintf(name,elemof(name),sKanalFormat,kn+iKanalStart);
 div=0;		// "ungültig" für LadeVorgabe(), SetAblenkung()
 fTastkopf=0;	// "ungültig" für LadeVorgabe(), SetTastkopf()
 kopplung=0;
 getsample=GetSampleDc;
 poly=NULL/*new POINT[zeit.samples]*/;
 if (kn<MaxKanalMenu) {
  submenu=GetSubMenu(OsziMenu,NumZeitMenu+NumTrigMenu);
  if (kn) submenu=CopyPopupMenu(submenu,idhigh);
  TCHAR buf[64],KanalGruppe[32],*pg=KanalGruppe;
  LoadString(HInstance,208,KanalGruppe,elemof(KanalGruppe)); // "Kanal %s\0Gruppe %s"
  if (kn>=numkanal) {
   pg+=lstrlen(pg)+1;	// auf "Gruppe %s"
   wnsprintf(buf,elemof(buf),pg,name);
  }else{
   wnsprintf(buf,elemof(buf),pg,name);
   if (kn<10) InsertAmp(buf,elemof(buf),-1);	// bei 2stelligen Zahlen ohne "&"
  }
  (kn?InsertMenu:ModifyMenu)(OsziMenu,NumZeitMenu+NumTrigMenu+kn,MF_BYPOSITION|MF_POPUP,(UINT)submenu,buf);
  if (kn) NumKanalMenu++;
 }else if (kn==MaxKanalMenu) {
  TCHAR s[32];
  LoadString(HInstance,212/*"Weitere Kanäle..."*/,s,elemof(s));
  InsertMenu(OsziMenu,NumZeitMenu+NumTrigMenu+kn,MF_BYPOSITION,IDC_YWEITER,s);
  NumKanalMenu++;
 }
 nulllinie=0;
 farbe=kn?0x008080L:0x000080L;	// gelb? // rot? Besser wäre "ungültig"
 masse=new MASSE(this);
 anzeige=new KANALANZEIGE(this);	// Verlinkung herstellen
 LadeVorgabe();
 InfoNeu(0xFF);
}

void KANAL::Destruktor() {
 RetteVorgabe();
 if (poly) delete poly; poly=NULL;
 delete anzeige;
 delete masse;
 if (kn) DeleteMenu(OsziMenu,NumZeitMenu+NumTrigMenu+kn,MF_BYPOSITION);
}

void ZEITANZEIGE::Paint(HDC dc) {
 ANZEIGE::Paint(dc);
 setTextColor(dc,z->farbe);
 ExtTextOut(dc,rcitem.left,rcitem.top,ETO_OPAQUE|ETO_CLIPPED,&rcitem,
   string1,lstrlen(string1),NULL);
 TCHAR buf[16];
 z->GetVar(1,buf);
 ExtTextOut(dc,rcitem.left,rcitem.bottom,ETO_OPAQUE,NULL,buf,lstrlen(buf),NULL);
}

void TRIGGERANZEIGE::Paint(HDC dc) {
 ANZEIGE::Paint(dc);
 setTextColor(dc,t->farbe);
 ExtTextOut(dc,rcitem.left,rcitem.top,ETO_OPAQUE|ETO_CLIPPED,&rcitem,
   string1,lstrlen(string1),NULL);
}

void ZEITANZEIGE::Update(BYTE was) {
 if (was&(WAS_RATE|WAS_XABLENK)) {
  z->GetVar(0,string1);
  knopf[0]->Enable(z->div<z->max);	// Rück-Verbindung...
  knopf[1]->Enable(z->min<z->div);
  int diff=rcitem.left+TextWidth(string1)-rcitem.right;
  if (diff) {
   rcitem.right+=diff;
   Moved();
   knopf[0]->Schieb(diff,0,true);
   knopf[1]->Schieb(diff,0,true);
  }
 }
 if (was&(WAS_RATE|WAS_XABLENK|WAS_FARBE)) Inval();
}
const TCHAR sTrigModus[]=T("Auto\0Normal\0Single\0");
const TCHAR sTrigFlanke[]=T("+-");
const TCHAR sTrigKopplung[]=T("DC\0AC\0HF\0LF\0TVH\0TVL\0NULL\0PAT\0");

void TRIGGERANZEIGE::Update(BYTE was) {
 if (was&(WAS_TQUELLE|WAS_TFLANKE|WAS_TKOPPLUNG|WAS_TPEGEL|WAS_TPRETRIG)) {
  TCHAR buf[32];
  KANAL *k=t->k;
  PCTSTR kop=Index2String(sTrigKopplung,t->kopplung);
  TRIG tr;	// DEBUG!
  tr.what=0;
  ::quelle->RelayMsg(Q_SETTRIG,&tr);
  Float2String(buf,k->div*t->pegel,k->praefixe,k->einheit);
  wnsprintf(string1,elemof(string1),T("%c%s: %s %s (%d%%)"),
    sTrigFlanke[t->flanke],k->name,buf,
    kop,t->pretrig);
// Anklick-Bereiche festlegen
  HDC dc=GetDC(MainWnd);
  int spclen2=TextWidth(dc,sTastkopf2,1)/2;	// Breite halbes Leerzeichen
  grenzen[0]=TextWidth(dc,string1,1)+spclen2;	// Flanken-Bereich
  PTSTR p=StrChr(string1,':');
  grenzen[1]=TextWidth(dc,string1,p);	// Kanalname-Bereich
  p=StrChr(p,'(');			// Pegel-Bereich
  grenzen[2]=TextWidth(dc,string1,p-lstrlen(kop)-1);
  grenzen[3]=TextWidth(dc,string1,p);	// Kopplungs-Bereich
  grenzen[4]=TextWidth(dc,string1);	// Prätrigger-Bereich (ganzer String)
  ReleaseDC(MainWnd,dc);
  int diff=rcitem.left+grenzen[4]-rcitem.right;
  if (diff) {
   rcitem.right+=diff; Moved();
   knopf[0]->Schieb(diff,0,true);
  }
 }
 if (was&(WAS_TQUELLE|WAS_TFLANKE|WAS_TKOPPLUNG|WAS_TPEGEL|WAS_TPRETRIG|
   WAS_FARBE)) Inval();
}

ZEITANZEIGE::ZEITANZEIGE(ZEIT*z) {
 this->z=z;
 RECT r;
// M_CREATESTRUCT mcs;
// mcs.p=&Anker[1];	// Hintergrund
 SetRect(&rcitem,20,10,100,10+WinBitmapExt.y);
 hint=T("Zeitbasis");
// MINIWND::Init(&mcs);
 SetRect(&r,0,0,WinBitmapExt.x,WinBitmapExt.y);
 OffsetRect(&r,rcitem.right,rcitem.top);
 knopf[0]=new MYBUTTON(this,&r,MAKEINTRESOURCE(35)/*T("Größere Zeitbasis  (Pfeil links)")*/,
   IDC_XABLENK+elemof(Reihe)+1,MYBUTTON::ENGER);
 OffsetRect(&r,WinBitmapExt.x,0);
 knopf[1]=new MYBUTTON(this,&r,MAKEINTRESOURCE(36)/*T("Kleinere Zeitbasis  (Pfeil rechts)")*/,
   IDC_XABLENK+elemof(Reihe)+2,MYBUTTON::WEITER);
 Update(0xFF);
}

TRIGGERANZEIGE::TRIGGERANZEIGE(TRIGG*t) {
 this->t=t;
 RECT r;
// M_CREATESTRUCT mcs;
// mcs.p=&Anker[1];	// Hintergrund
 SetRect(&rcitem,120,10,200,10+WinBitmapExt.y);
#ifdef WIN32
 hint=LPSTR_TEXTCALLBACK;
#endif
 style=0;
// MINIWND::Init(&mcs);
 SetRect(&r,0,0,WinBitmapExt.x,WinBitmapExt.y);
 OffsetRect(&r,rcitem.right,rcitem.top);
 knopf[0]=new MYBUTTON(this,&r,MAKEINTRESOURCE(37)/*T("Start/Stopp (Leertaste)")*/,
   0x229,MYBUTTON::PAUSE);
 Update(0xFF);
}

bool ZEITANZEIGE::RelayMsg(UINT Msg, WPARAM wParam, LPARAM lParam) {
 switch (Msg) {
  case WM_MOUSEMOVE: {
   TCHAR s[256];	// Aua, jedes MouseMove!
   LoadString(HInstance,16,s,elemof(s));
   tip->SetTip(&rcitem,Index2String(s,hitkode&0x3F));
  }break;
  case WM_RBUTTONDOWN: {
   if (!(state&0x20)) break;
   ShowPopupMenu(GetSubMenu(z->submenu,SUB_XABLENK),
     MAKEPOINTS(lParam).x,MAKEPOINTS(lParam).y);
  }break;
 }
 return ANZEIGE::RelayMsg(Msg,wParam,lParam);
}
bool TRIGGERANZEIGE::RelayMsg(UINT Msg,WPARAM wParam,LPARAM lParam) {
 switch (Msg) {
#if 0
  case WM_NOTIFY: {
    static const TCHAR Text[]=T("Flanke\0Quelle\0Pegel\0Kopplung\0Prätrigger\0");
   if (!hitkode) break;
#define ttt ((LPTOOLTIPTEXT)lParam)
   if (ttt->hdr.code==TTN_NEEDTEXT)
   ttt->lpszText=(PTSTR)Index2String(Text,hitkode&0x3F);
#undef ttt
  }break;
#endif
  case WM_MOUSEMOVE: {
   TCHAR s[256];	// Aua, jedes MouseMove!
   LoadString(HInstance,38,s,elemof(s));
   tip->SetTip(&rcitem,s,(LPCTSTR)Index2String(s,(hitkode&0x3F)+1));
  }break;
  case WM_RBUTTONDOWN: {
   HMENU m=0;
   switch (hitkode) {
    case HK_PRI+0: m=GetSubMenu(t->submenu,SUB_TFLANKE); break;
    case HK_PRI+1: m=GetSubMenu(t->submenu,SUB_TQUELLE); break;
    case HK_PRI+3: m=GetSubMenu(t->submenu,SUB_TKOPPLUNG); break;
   }
   if (m) ShowPopupMenu(m,MAKEPOINTS(lParam).x,MAKEPOINTS(lParam).y);
  }break;
 }
 return ANZEIGE::RelayMsg(Msg,wParam,lParam);
}


#ifndef WIN32
BOOL WINAPI CheckMenuRadioItemX(HMENU m,UINT i,UINT j,UINT k,UINT p) {
 for (; i<=j; i++) {
  CheckMenuItem(m,i,p|(i==k?
    MFT_RADIOCHECK|MF_CHECKED:
    MFT_RADIOCHECK|MF_UNCHECKED));
 }
 return TRUE;
}
BOOL WINAPI SetMenuDefaultItemX(HMENU,UINT,UINT) {
 return TRUE;
};	//vorerst leer, geht nur mit OwnerDraw zu lösen
#endif

/**********************
 ** Hardware-Zugriff **
 **********************/
/* Mal-Routinen: Gitternetz, Kurven, Fadenkreuz, alles zusammen */

void KANAL::MaleKurve(HDC dc) {
 COLORREF f=farbe;
 int o;
 if (DispOpt&DO_XOR) {
  f^=BackColor;
  o=SetROP2(dc,R2_XORPEN);
 }
 if (DispOpt&DO_LINE) {
  HPEN KPen,OldPen;

  KPen=CreatePen(PS_SOLID,1,f);
  OldPen=(HPEN)SelectObject(dc,KPen);
  Polyline(dc,poly,polycount);
  Polyline(dc,poly+polycount,polyc2);
  SelectObject(dc,OldPen);
  DeleteObject(KPen);
 }else{
  POINT *pp;
  int i;
  for (i=polycount+polyc2, pp=poly; i>0; i--,pp++) {
   SetPixel(dc,pp->x,pp->y,f);
  }
 }
 if (DispOpt&DO_XOR) SetROP2(dc,o);
}

size_t log2phys(size_t a) {
// Wandelt einen logischen Sampledaten-Index in WaveHdr in einen physikalischen.
// Dabei muss nur der Prätrigger-Bereich beachtet werden
// BEI ROLLBETRIEB alles.
// if (WaveHdr.dwFlags&WHDR_BEGINLOOP) {
  if (a<WaveHdr.reserved) {
   size_t A=WaveHdr.reserved-WaveHdr.dwLoops;
   if (a<A) a+=WaveHdr.dwLoops;
   else a-=A;
  }
// }
 return a;
}
LPSTR log2addr(size_t a) {
// Wandelt logischen Sampleraten-Index in Adresse
// (FAR reicht zum Zugriff, wenn Sample-Breite eine Potenz von 2 ist.)
 return (LPSTR)((char huge*)WaveHdr.lpData+log2phys(a));
}
/*
LPSTR idx2addr(DWORD index,BYTE byteoffset) {
 return log2addr(index*BlockAlign+byteoffset);
}
*/
void KANAL::CalcGraf() {
// fehlt noch:
// * Reduktion auf notwendige Punktzahl (2049..4096)
// * nur Aktualisierungsbereich
// * X/Y-Betrieb
 int *pp;
 int i,x;
 size_t j;
 float pixprosample=step.x/zeit->rate/zeit->div;
 float pixprolsb=-voltprolsb/div*step.y;	// Minus weil oben positiv
 float pixmitte=Mitte.y-(nulllinie-voltoffset/div)*step.y;
 if (!WaveHdr.lpData) return;
 pp=(int*)poly;
 if (!pp) return;
// Neue Samples:
 x=0;
 j=byteoffset;
 for (i=0; j<WaveHdr.dwBytesRecorded; i++,x++) {
  *pp=Rand.left+rund(x*pixprosample); pp++;
  *pp=rund(getsample(this,log2addr(j))*pixprolsb+pixmitte); pp++;
  j+=BlockAlign;
 }
 polycount=i;
// Alte Samples:
 j=WaveHdr.dwUser;
 x=(int)(j/BlockAlign);
 j+=byteoffset;
 for (i=0; j<WaveHdr.dwBufferLength; i++,x++) {
  *pp=Rand.left+rund(x*pixprosample); pp++;
  *pp=rund(getsample(this,log2addr(j))*pixprolsb+pixmitte); pp++;
  j+=BlockAlign;
 }
 polyc2=i;
}

/********************************************
 ** Das Drama mit den veränderlichen Menüs **
 ********************************************/

void StringUndZahl(PTSTR buf, UINT id, float z, WORD iso, PCTSTR einheit) {
 LoadString(HInstance,id,buf,64);
 Float2String(AfterTab(buf),z,iso,einheit);
}

void Markiere(HMENU m,UINT idc,float z) {
// Im Menü mit gestuften Gleitkommazahlen fuhrwerken...
 for (int idx=0; idx<elemof(Reihe); idx++) {
  UINT flags= Reihe[idx]==z ? MFT_RADIOCHECK|MF_CHECKED : MF_UNCHECKED;
  CheckMenuItem(m,idx+idc,flags);
 }
}
void Markiere(HMENU m, UINT pos, UINT idc, float z) {
// Dito für Popup-Menüs
 Markiere(GetSubMenu(m,pos),idc,z);
}
/*
void ZEIT::LadeXAblenkString(PTSTR buf) {
 if (flags&OF_XY) StringUndZahl(buf,203,rate,MAKEWORD(0,3),T("Sa/s"));
 else StringUndZahl(buf,202,div,MAKEWORD(-4,0),T("s/div"));
}*/

bool ZEIT::SetAblenkung(PCTSTR s) {
 float z;
 EINHEIT e;
 z=div;
 int i=String2UpDown(s,1,1);	// Hier: nur große Schritte, vorerst
 if (i>0) NextFloat(z,i,max);
 else if (i<0) NextFloat(z,i,min);
 else if (!String2Float(s,z,e)) z=0;
 else if (e[0] && lstrcmp(e,T("s"))) z=0; // falsche Einheit
 return SetAblenkung(z);
}
bool ZEIT::SetAblenkung(float z) {
// Bei ungültigem <div> erfolgt eine Initialisierung zumindest mit <min>!
// liefert NUR "false" bei versuchtem Übergang von "gültig" zu "ungültig"!
 if (z<=0) {
  if (div>0) return false;	// Kleiner/gleich Null darf es nicht sein!
  z=min;			// Initialbedingung und keine .INI
 }
 if (div==z) return true;	// Keine Änderung
 div=z;
 SetRate(samples/(div*Kaestel.x));
 return InfoNeu(WAS_XABLENK);		// setzt die Samplerate
}
bool ZEIT::SetRate(PCTSTR s) {
 float z;
 EINHEIT e;
 if (!String2Float(s,z,e)) z=0;
 else if (e[0] && lstrcmp(e,T("Sa/s")) && lstrcmp(e,T("Hz"))) z=0;
 return SetRate(z);
}
bool ZEIT::SetRate(float z) {
 if (z<=0) {
  if (rate>0) return false;
  z=maxrate;
 }
 if (rate==z) return true;
 quelle->RelayMsg(Q_SETRATE,&z);
 if (rate==z) return true;	// immer noch unverändert!
 rate=z;
 if (z<minrate || z>maxrate) LadeListenVonRateMinMax();
	// Neue Grenzen! Bei Soundkarten passiert so etwas
 return InfoNeu(WAS_RATE);
}

bool ZEIT::SetAnfang(PCTSTR s) {
 float z;
 EINHEIT e;
 if (!String2Float(s,z,e)) return false;
 if (e[0] && lstrcmp(e,T("s"))) return false;
 // Hier fehlt noch eine Umrechnung von <div> oder <%>
 return SetAnfang(z);
}
bool ZEIT::SetAnfang(float z) {
 anfang=z;
 // Hier fehlen Bereichstests!
 return InfoNeu(WAS_XANF);
}

bool ZEIT::SetSamples(PCTSTR s) {
 DWORD sa;
 if (_stscanf(s,T("%lu"),&sa)!=1) return false;
 return SetSamples(sa);
}
bool ZEIT::SetSamples(DWORD sa) {
 samples=sa;
 // weiter: Speicher reallozieren usw!
 return InfoNeu(WAS_SAMPLES);
}

bool String2Farbe(PCTSTR s, COLORREF &c) {
 bool swap=false;
 if (!s) return false;
 if (s[0]=='#') s++,swap=true;	// HTML-Syntax
 if (_stscanf(s,T("%lx"),&c)!=1) return false;
 if (swap) {
  ((PBYTE)&c)[3]=((PBYTE)&c)[2];	// Farbangabe BGR (Windows) statt RGB (HTML)
  ((PBYTE)&c)[2]=((PBYTE)&c)[0];
  ((PBYTE)&c)[0]=((PBYTE)&c)[3];
  ((PBYTE)&c)[3]=0;
 }
 return true;
}
bool Farbe2String(PTSTR s, COLORREF &c) {
 if (!s) return false;	// HTML-kompatibel konvertieren
 _sntprintf(s,32,T("#%02X%02X%02X"),((PBYTE)&c)[0],((PBYTE)&c)[1],((PBYTE)&c)[2]);
 return true;
}
/* DOPPELTER KODE: Identisch mit KANAL::SetFarbe */
bool ZEIT::SetFarbe(PCTSTR s) {
 COLORREF c;
 return (bool)(String2Farbe(s,c) && SetFarbe(c));
}
bool ZEIT::SetFarbe(COLORREF c) {
 if (c==BackColor) return false;
 if (c==GridColor) return false;
 if (farbe==c) return true;	// Kurzschluss wenn gleich
 farbe=c;
 return InfoNeu(WAS_FARBE);	// FEHLT: Paletteneintrag beschaffen
}

static TCHAR sZeitVar[]=T("Ablenkung\0Rate\0Abtastwerte\0Farbe\0");
static HSZ hszZeitVar[3];

bool ZEIT::SetVar(int i, PCTSTR s) {
 switch (i) {
  case 0: return SetAblenkung(s);
  case 1: return kn?SetAnfang(s):SetRate(s);
  case 2: return SetSamples(s);
  case 3: return SetFarbe(s);
 }
 return false;
}
bool ZEIT::SetVar(PCTSTR n, PCTSTR s) {
 return SetVar(String2Index(sZeitVar,n),s);
}
bool ZEIT::SetVar(HSZ hsz,PCTSTR s) {
 return SetVar(hsz2Index(hszZeitVar,elemof(hszZeitVar),hsz),s);
}

bool ZEIT::GetVar(int i, PTSTR s) {
 switch (i) {
  case 0: Float2String(s,div,MAKEWORD(-4,0),T("s"),5); break;
  case 1: if (kn) Float2String(s,anfang,MAKEWORD(-4,0),T("s"),5);
	  else Float2String(s,rate,MAKEWORD(0,3),T("Sa/s"),5);
          break;
  case 2: wsprintf(s,T("%lu"),samples); break;
  case 3: Farbe2String(s,farbe); break;
  default: return false;
 }
 return true;
}
bool ZEIT::GetVar(PCTSTR n, PTSTR s) {
 return GetVar(String2Index(sZeitVar,n),s);
}
bool ZEIT::GetVar(HSZ hsz,PTSTR s) {
 return GetVar(hsz2Index(hszZeitVar,elemof(hszZeitVar),hsz),s);
}
/* DREIFACHER KODE: Identisch mit KANAL::SetFarbe */
bool TRIGG::SetFarbe(PCTSTR s) {
 COLORREF c;
 return (bool)(String2Farbe(s,c) && SetFarbe(c));
}
bool TRIGG::SetFarbe(COLORREF c) {
 if (c==BackColor) return false;
 if (c==GridColor) return false;
 if (farbe==c) return true;	// Kurzschluss wenn gleich
 farbe=c;
 return InfoNeu(WAS_FARBE);		// FEHLT: Paletteneintrag beschaffen
}

static TCHAR sTriggerVar[]=T("Flanke\0Quelle\0Pegel\0Kopplung\0Prätrigger\0Farbe\0");
static HSZ hszTriggerVar[6];

bool TRIGG::SetVar(int i, PCTSTR s) {
 switch (i) {
  case 0: return SetFlanke(s);
  case 1: return SetQuelle(s);
  case 2: return SetPegel(s);
  case 3: return SetKopplung(s);
  case 4: return SetPretrig(s);
  case 5: return SetFarbe(s);
 }
 return false;
}
bool TRIGG::SetVar(PCTSTR n, PCTSTR s) {
 return SetVar(String2Index(sTriggerVar,n),s);
}
bool TRIGG::SetVar(HSZ hsz,PCTSTR s) {
 return SetVar(hsz2Index(hszTriggerVar,elemof(hszTriggerVar),hsz),s);
}

bool TRIGG::GetVar(int i, PTSTR s) {
 switch (i) {
  case 0: wsprintf(s,T("%c"),sTrigFlanke[flanke]); break;
  case 1: wsprintf(s,sInt,quelle); break;	// hier: numerisch
  case 2: Float2String(s,pegel,0,T("div")); break;	// hier: in "div"
  case 3: lstrcpy(s,Index2String(sTrigKopplung,kopplung)); break;
  case 4: wsprintf(s,T("%d %%"),pretrig); break;
  case 5: Farbe2String(s,farbe); break;
  default: return false;
 }
 return true;
}
bool TRIGG::GetVar(PCTSTR n, PTSTR s) {
 return GetVar(String2Index(sTriggerVar,n),s);
}
bool TRIGG::GetVar(HSZ hsz,PTSTR s) {
 return GetVar(hsz2Index(hszTriggerVar,elemof(hszTriggerVar),hsz),s);
}

void ZEIT::LadeListenVonRateMinMax() {
 SYSINFO si;
 quelle->RelayMsg(Q_GETSYSINFO,&si);
 minrate=si.rateminmax[0];
 maxrate=si.rateminmax[1];
 LadeRateListe();
 min=10/maxrate;	// wären 10 Samples pro div, bissel wenig!?
 max=(float)si.depth/Kaestel.x/minrate;
 if (si.flags&Q_CONTINUOUS) max=100;	// 100 Sekunden pro Teilstrich??
 LadeAblenkListe();
}

void ZEIT::LadeVorgabe() {
 samples=1000;	// Harte, zurzeit idiotische Vorgabe!!
 LadeListenVonRateMinMax();
 PCTSTR p; int i;
 for (p=sZeitVar,i=0; *p; p+=lstrlen(p)+1,i++) {
  TCHAR buf[32];
  GetString(name,p,sLeer,buf,elemof(buf));
  SetVar(i,buf);
 }
// InfoNeu(WAS_XABLENK|WAS_RATE);
}

#define WM_INFONEU (WM_USER+1234) // wParam=WAS-Maske, lParam=this-Strukturptr.
bool ZEIT::InfoNeu(BYTE was) {
 if (submenu) {
  TCHAR s1[32],s2[32];
  if (was&(WAS_XABLENK|WAS_RATE)) {
   TCHAR buf[64];
   Float2String(s1,div,MAKEWORD(-4,0),T("s"));
   if (kn) {
    Float2String(s2,anfang,MAKEWORD(-4,0),T("s"));
   }else{
    Float2String(s2,rate,MAKEWORD(0,3),T("Sa"),5);	// Langform
   }
   wnsprintf(buf,elemof(buf),T("%s @ %s"),s1,s2);
   SetMenuStringAfterTab(OsziMenu,kn,buf);
  }
  if (was&WAS_XABLENK) {
   Markiere(submenu,SUB_XABLENK,IDC_XABLENK,div);
   lstrcat(s1,T("/div"));
   SetMenuStringAfterTab(submenu,SUB_XABLENK,s1);
//   if (!(was&WAS_RATE)) SetRate(samples/(div*Kaestel.x)); // Rekursionsgefahr?
  }
  if (was&WAS_RATE) {
   Markiere(submenu,SUB_RATE,IDC_RATE,rate);
   lstrcat(s2,T("/s"));
   SetMenuStringAfterTab(submenu,SUB_RATE,s2);
  }
  if (was&WAS_FARBE) {
   Farbe2String(s1,farbe);
   SetMenuStringAfterTab(submenu,SUB_XFARBE,s1);
  }
 }
 if (anzeige) anzeige->Update(was);
 if (hZeitDlg && (unsigned)ZeitDlgCurSel==kn)
   PostMessage(hZeitDlg,WM_INFONEU,was,(LPARAM)this);
 return true;	// liefert stets true als bequeme Rückgabe für SetXxx()
}

bool TRIGG::InfoNeu(BYTE was) {
 if (submenu) {
  if (was&WAS_TMODUS) {
   MenuRadioNextDefault(submenu,SUB_TMODUS,modus);
   SetMenuStringAfterTab(submenu,SUB_TMODUS,Index2String(sTrigModus,modus));
  }
  if (was&WAS_TQUELLE) {
   TRIGINFO ti;
   ::quelle->RelayMsg(Q_GETTRIGINFO,&ti);
   MenuEnable(submenu,SUB_TFLANKE,ti.edges);
   MenuEnable(submenu,SUB_TKOPPLUNG,ti.couplings);
   MenuRadioNextDefault(submenu,SUB_TQUELLE,quelle);
   SetMenuStringAfterTab(submenu,SUB_TQUELLE,k->name);
  }
  if (was&WAS_TFLANKE) {
   MenuRadioNextDefault(submenu,SUB_TFLANKE,flanke);
   SetMenuStringAfterTab(submenu,SUB_TFLANKE,flanke?T("v"):T("^"));
  }
  if (was&WAS_TKOPPLUNG) {
   MenuRadioNextDefault(submenu,SUB_TKOPPLUNG,kopplung);
   SetMenuStringAfterTab(submenu,SUB_TKOPPLUNG,Index2String(sTrigKopplung,kopplung));
  }
  if (was&(WAS_TMODUS|WAS_TQUELLE|WAS_TFLANKE|WAS_TKOPPLUNG)) {
   TCHAR s[32];
   wnsprintf(s,elemof(s),T("%s %c%s %s"),
     Index2String(sTrigModus,modus),
     flanke?'-':'+',
     k->name,
     Index2String(sTrigKopplung,kopplung));
   SetMenuStringAfterTab(OsziMenu,NumZeitMenu,s);
  }
  if (was&WAS_TPEGEL) {
   TCHAR s[32];
   GetVar(2,s);
   SetMenuStringAfterTab(submenu,SUB_TPEGEL,s);
  }
  if (was&WAS_TPRETRIG) {
   TCHAR s[32];
   GetVar(4,s);
   SetMenuStringAfterTab(submenu,SUB_TPRETRIG,s);
  }
  if (was&WAS_FARBE) {
   TCHAR s[32];
   Farbe2String(s,farbe);
   SetMenuStringAfterTab(submenu,SUB_TFARBE,s);
  }
 }
 if (anzeige) anzeige->Update(was);
 if (kreuz) kreuz->Update(was);
 return true;	// liefert stets true als bequeme Rückgabe für SetXxx()
}
void TRIGG::LadeVorgabe() {
 TRIGINFO ti;
 ::quelle->RelayMsg(Q_GETTRIGINFO,&ti);
 quelle=0;
// FEHLT: Menüs erstellen (gehört eher zum Konstruktor!)
 PCTSTR p; int i;
 for (p=sTriggerVar,i=0; *p; p+=lstrlen(p)+1,i++) {
  TCHAR buf[32];
  GetString(name,p,sLeer,buf,elemof(buf));
  SetVar(i,buf);
 }
 InfoNeu(0xFF);	// alles
}
bool ZEIT::RetteVorgabe() {
 bool r=true;
 PCTSTR p; int i;
 for (p=sZeitVar,i=0; *p; p+=lstrlen(p)+1,i++) {
  TCHAR buf[32];
  GetVar(i,buf);
  r=bool(WriteString(name,p,buf)!=0 && r);
 }
 return r;
}
bool TRIGG::RetteVorgabe() {
 bool r=true;
 PCTSTR p; int i;	// Eigentlich ein Iterator fällig!
 for (p=sTriggerVar,i=0; *p; p+=lstrlen(p)+1,i++) {
  TCHAR buf[32];
  GetVar(i,buf);
  r=bool(WriteString(name,p,buf)!=0 && r);
 }
 return r;
}

void ZEIT::Konstruktor(int kn) {
 this->kn=kn;
 idhigh=kn<<10;
 div=0;		// ungültiger Wert
 rate=0;	// noch ein ungültiger Wert
 wnsprintf(name,elemof(name),T("%c"),'A'+kn);
 submenu=GetSubMenu(OsziMenu,0);
 if (kn) submenu=CopyPopupMenu(submenu,idhigh);
 TCHAR sZeitbasis[32],buf[32];
 LoadString(HInstance,202,sZeitbasis,elemof(sZeitbasis));
 wnsprintf(buf,elemof(buf),sZeitbasis,name);	// Name der Zeitbasis einsetzen
 if (kn<10) InsertAmp(buf,elemof(buf),-1);
 (kn?InsertMenu:ModifyMenu)(OsziMenu,kn,MF_BYPOSITION|MF_POPUP,(UINT)submenu,buf);
 anzeige=new ZEITANZEIGE(this);	// Verlinkung herstellen
 LadeVorgabe();
}

void TRIGG::LadeQuelleMenu() {
 TCHAR s[64];
 SYSINFO si;
 ::quelle->RelayMsg(Q_GETSYSINFO,&si);
 HMENU m=CreatePopupMenu();
 TCHAR fmt[32];
 LoadString(HInstance,208/*"Kanal %s"*/,fmt,elemof(fmt));
 for (int i=0; i<si.numtrig; i++) {
  if (i<si.numchan) {
   if (i>MaxKanalMenu) continue;	// keinen Menüeintrag erzeugen!
   if (i==MaxKanalMenu) LoadString(HInstance,212/*"&Weitere Kanäle..."*/,s,elemof(s));
   else{
    wnsprintf(s,elemof(s),fmt,::kanal[i].name);
    if (i<10) InsertAmp(s,elemof(s),-1);	// vor letztes (Zahl-)Zeichen
   }
  }else LoadString(HInstance,204/*"&Extern"*/,s,elemof(s));
  InsertMenu(m,(UINT)-1,MF_BYPOSITION,IDC_TQUELLE+(i<<10),s);
 }
 SetMenuPopup(submenu,SUB_TQUELLE,m);
}

void TRIGG::Konstruktor(int) {
 wnsprintf(name,elemof(name),T("%c"),'T');
 submenu=GetSubMenu(OsziMenu,NumZeitMenu);
 modus=TM_AUTO;
 quelle=0;
 LadeQuelleMenu();
 flanke=TF_STEIG;
 kopplung=TK_DC;
 SetQuelle(0);		// besser: vom Oszi holen!
 pretrig=0;
 pegel=0;
 LadeVorgabe();
 anzeige=new TRIGGERANZEIGE(this);	// Verlinkung herstellen
 kreuz=new KREUZ(this);
 InfoNeu(0xFF);
}

void ZEIT::Destruktor() {
 RetteVorgabe();
 delete anzeige;
 if (kn) DeleteMenu(OsziMenu,kn,MF_BYPOSITION);
}

void TRIGG::Destruktor() {
 RetteVorgabe();
 if (quelle>=numkanal) delete k;
 delete kreuz;
 delete anzeige;
}

void ZEIT::LadeAblenkListe() {
// Aufrufen bei LadeXVorgabe
 HMENU m;
 m=CreatePopupMenu();
 float z;
 float e;
 int idx;
 TCHAR buf[64];
 z=min;
 e=max;
 for (idx=NextFloat(z,0); z<=e; idx=NextFloat(z,1)) {
  Float2String(buf,z,MAKEWORD(-4,0),T("s"),5);	// von ps bis s
  InsertMenu(m,0,MF_BYPOSITION|MFT_RADIOCHECK,IDC_XABLENK+idx+idhigh,buf);	// Rückwärts einsortieren!
 }
 AppendAndereToMenu(m,elemof(Reihe)+IDC_XABLENK);
 SetMenuPopup(submenu,SUB_XABLENK,m);
}

void AppendRateToMenu(HMENU m, UINT id, float z) {
// Hängt Samplerate <z> an Popup-Menü <m> mit ID <id> an
// Hilfsfunktion für ZEIT::LadeRateListe
 TCHAR buf[32];
 Float2String(buf,z,MAKEWORD(0,3),T("Sa"),5);	// hier: Kurz-Einheit!
 InsertMenu(m,(UINT)-1,MF_BYPOSITION|MFT_RADIOCHECK,id,buf);	// Vorwärts sortieren
}

void ZEIT::LadeRateListe() {
// Erzeugt Popup-Menü mit Sampleraten, inklusive absolut minimaler
// und maximaler, soweit unterschiedlich von der 1-2-5-Reihe
// Aufrufen bei ZEIT::LadeVorgabe und wenn sich (plötzlich)
// minimale und maximale Samplerate ändert(e)
 HMENU m;
 int idx;
 float z;
 m=CreatePopupMenu();
 z=minrate;
 idx=NextFloat(z,0);
 if (z!=minrate) AppendRateToMenu(m,IDC_RATE+elemof(Reihe)+3+idhigh,minrate);
 for (; z<=maxrate; idx=NextFloat(z,1)) {
  AppendRateToMenu(m,IDC_RATE+idx+idhigh,z);
  if (z==maxrate) goto keinmax;
 }
 if (minrate!=maxrate) AppendRateToMenu(m,IDC_RATE+elemof(Reihe)+4+idhigh,maxrate);
keinmax:
 AppendAndereToMenu(m,IDC_RATE+elemof(Reihe)+idhigh);
 SetMenuPopup(submenu,SUB_RATE,m);
}

bool TRIGG::SetModus(MODUS m) {
 if (m>TM_SINGLE) return false;
 if (modus==TM_AUTO) KillTimer(MainWnd,3);
 modus=m;
 if (modus==TM_AUTO) SetTimer(MainWnd,3,500,NULL);
 return InfoNeu(WAS_TMODUS);
}
bool TRIGG::SetModus(PCTSTR s) {
 int m=String2Index(sTrigModus,s);
 if (m<0 && _stscanf(s,sInt,&m)!=1) return false;
 return SetModus((MODUS)m);
}
bool TRIGG::SetFlanke(FLANKE f) {
 if ((unsigned)f>=2) return false;
 if (flanke==f) return true;
 flanke=f;
 TRIG t;
 t.what=TRIG_EDGE;
 t.edge=(BYTE)f;
 ::quelle->RelayMsg(Q_SETTRIG,&t);	// kann simuliert werden...???
 return InfoNeu(WAS_TFLANKE);
}
bool TRIGG::SetFlanke(PCTSTR s) {
 int fl;
 if (!s) return false;
 if (!s[1]) {
  if (*s==sTrigFlanke[0]) {fl=0; goto habs;}
  if (*s==sTrigFlanke[1]) {fl=1; goto habs;}
 }
 if (_stscanf(s,sInt,&fl)!=1) return false;
habs:
 return SetFlanke((FLANKE)fl);
}
bool TRIGG::SetQuelle(int q) {
 if (quelle!=q) {
  TRIG t;
  t.what=TRIG_SOURCE;
  t.source=(BYTE)q;
  ::quelle->RelayMsg(Q_SETTRIG,&t);
 }
 if (quelle>=numkanal) delete k;	// Dummy-Kanal löschen
 quelle=q;
 if (quelle<numkanal) k=kanal+q; else{
  k=new KANAL;				// Dummy-Kanal (nicht in Liste)
  lstrcpy(k->name,T("Ext"));
  k->div=1;
  k->praefixe=0;
  lstrcpy(k->einheit,T("V"));
  k->nulllinie=0;
  k->farbe=farbe;
  SetPegel(0.0);
 }
 return InfoNeu(WAS_TQUELLE);
}
bool TRIGG::SetQuelle(PCTSTR s) {	// in div oder der Einheit des Kanals
 int q=numkanal;
 if (!lstrcmpi(s,T("Ext"))) goto habs;
 if (kanal) for (q=0; q<numkanal; q++)
   if (!lstrcmpi(s,kanal[q].name)) goto habs;
 if (_stscanf(s,sInt,&q)!=1) return false;
habs:
 return SetQuelle(q);
}
bool TRIGG::SetPegel(float p) {	// in <div>
 if (abs(rndint(p-0.5))>2*Kaestel.y) return false;
 if (fabs(p)<1E-3) p=0;	// Sollte sicher Null sein
 if (!k) return false;
 TRIG t;
 t.what=TRIG_LEVEL;
 t.level=rund(p*k->div/k->voltprolsb);
// SoftTrigger sofort setzen! Kontinuierliche Datenquellen brauchen's nicht 
 ::quelle->RelayMsg(Q_SETTRIG,&t);
 pegel=t.level*k->voltprolsb/k->div;	// RÜCK-BERECHNUNG!
 if (pegel==p) return true;
 return InfoNeu(WAS_TPEGEL);
}
bool TRIGG::SetPegel(PCTSTR s) {	// Sonderfälle: +, ++, +?
 float p;
 EINHEIT e;
 int i=String2UpDown(s,1,10);
 if (i) p=pegel+i*0.1F;
 else{
  String2Float(s,p,e);
  if (*e) {
   if (!lstrcmp(e,k->einheit)) {
    p/=k->div;		// Volt / Volt/div = div
   }else if (lstrcmpi(e,T("div"))) return false;
  }
 }
 return SetPegel(p);
}
bool TRIGG::SetKopplung(KOPPLUNG k) {
 if ((unsigned)k>7) return false;
 TRIG t;
 t.what=TRIG_COUPLING;
 t.coupling=(BYTE)k;
 ::quelle->RelayMsg(Q_SETTRIG,&t);	// Simulation??
 kopplung=k;
 return InfoNeu(WAS_TKOPPLUNG);
}
bool TRIGG::SetKopplung(PCTSTR s) {
 int k;
 if (!s) return false;
 k=String2Index(sTrigKopplung,s);
 if (k<0 && _stscanf(s,sInt,&k)!=1) return false;
 return SetKopplung((KOPPLUNG)k);
}
bool TRIGG::SetPretrig(int pt) {
 if (pt<-100) return false;
 if (pt>100) return false;
 TRIG t;
 t.what=TRIG_PRE;
 t.pre=MulDiv(pt,(int)zeit->samples,100);
 ::quelle->RelayMsg(Q_SETTRIG,&t);
 pretrig=pt;
 return InfoNeu(WAS_TPRETRIG);
}
bool TRIGG::SetPretrig(PCTSTR s) {
 int pt;
 float t;
 EINHEIT e;
 pt=String2UpDown(s,1,10);
 if (pt) pt+=pretrig;
 else{
  if (!String2Float(s,t,e)) return false;
  pt=rund(t);	// Wenn ohne Einheit, dann in Prozent
  if (*e) {
   if (!lstrcmp(e,T("s"))) {
    pt=rund(t*100/zeit->div/Kaestel.x);
   }else if (!lstrcmpi(e,T("div"))) {
    pt=rund(t*100/Kaestel.x);
   }else if (lstrcmp(e,T("%"))) return false;
  }
 }
 return SetPretrig(pt);
}

void ZeigeSimul(HMENU m, UINT idx, BYTE echt) {
 TCHAR s[64],*p;
 GetMenuString(m,idx,s,elemof(s),MF_BYPOSITION);
 p=AfterTab(s);
 if (echt) *--p=0;	// ohne Simulation
 else LoadString(HInstance,213,p,int(s+elemof(s)-p));	//"simuliert"
 SetMenuString(m,idx,s);
}
/****************************************************************************
 ** Wegen der fehlenden with-Anweisung ersparen Objekte in C++ tatsächlich **
 ** Schreibarbeit; deshalb fortan Y-bezogenes als Memberfunktionen	   **
 ****************************************************************************/
static TCHAR sKanalVar[]=T("Name\0Tastkopf\0Ablenkung\0Kopplung\0Nulllinie\0Farbe\0");
static HSZ hszKanalVar[6];

void KANAL::LadeVorgabe() {
 CHANINFO ci;
 ci.ch=(BYTE)kn;
 quelle->RelayMsg(Q_GETCHANINFO,&ci);
 min=ci.voltminmax[0]*5;	// minimal: 5 Stufen pro div Auflösung
 max=ci.voltminmax[1]*FullScale*numkanal;	// maximal: alle Traces geradeso
 byteoffset=ci.byteoffset;
 maske=ci.mask;
 if (submenu) {
  HMENU m=GetSubMenu(submenu,SUB_YKOP);
// DC-Kopplung kann nicht aus AC simuliert werden!
  EnableMenuItem(m,0,
    ci.couplings&CHANINFO_DC?MF_ENABLED|MF_BYPOSITION:MF_GRAYED|MF_BYPOSITION);
  ZeigeSimul(m,1,(BYTE)(ci.couplings&CHANINFO_AC));
  ZeigeSimul(m,2,(BYTE)(ci.couplings&CHANINFO_GND));
 }

 CHAN c;
 c.ch=(BYTE)kn;
 c.what=0;	// Nur lesen
 quelle->RelayMsg(Q_SETCHAN,&c);
 kopplung=c.coupling;
 voltprolsb=c.volt;
 voltoffset=c.dcoffset;
// div=chan.volt*fullscale/fTastkopf/Kaestel.y;
// NextFloat(div,0);

 TCHAR buf[32];
 PCTSTR p;
 int i;
 for (p=sKanalVar,i=0; *p; p+=lstrlen(p)+1,i++) {
  GetString(name,p,sLeer,buf,elemof(buf));
  SetVar(i,buf);
 }
 idhigh=kn<<10;
 if (poly) delete poly;
 poly=new POINT[(int)zeit->samples];
}

bool KANAL::RetteVorgabe() {
 bool r=true;
 int i;
 PCTSTR p;
 for (p=sKanalVar,i=0; *p; p+=lstrlen(p)+1,i++) {
  TCHAR buf[32];
  GetVar(i,buf);
  r=bool(WriteString(name,p,buf)!=0 && r);
 }
 return r;
}

void KANAL::LadeAblenkListe() {
// AUFRUFEN bei Änderung von Datenquelle oder Tastteiler, oder Vorzeichen!
 HMENU m=CreatePopupMenu();
 float z=min/*fTastkopf*/;
 float e=max/*fTastkopf*/;
 int idx;
 TCHAR buf[64];
 for (idx=NextFloat(z,0); z<=e; idx=NextFloat(z,+1)) {
  Float2String(buf,div<0?-z:z,praefixe,einheit);
  InsertMenu(m,0,MF_BYPOSITION|MFT_RADIOCHECK,idx+IDC_YABL+idhigh,buf);
 }
 SetMenuPopup(submenu,SUB_YABL,m);
}

int KANAL::GetTastkopfIndex() {	// fürs Menü usw.
 if (lstrcmp(einheit,T("V"))) return 2;
 if (fTastkopf==1) return 0;
 if (fTastkopf==10) return 1;
 return 2;
}

bool KANAL::SetNamen(PCTSTR s) {
 TCHAR buf[32];
 int l=lstrlen(s);
 if (l>=elemof(sNamen)) return false;	// zu lang
 if (!lstrcmp(s,sNamen)) return true;	// Gleich geblieben
 if (!::GetNameList(s,buf,NULL)) return false;	// ungültige Syntax
 lstrcpy(sNamen,s);
 return InfoNeu(WAS_YNAM);
}

bool KANAL::SetTastkopf(PCTSTR s) {
 float z,n;
 EINHEIT e;
 int i,j,k,l=lstrlen(s);
 if (l>=elemof(sTastkopf)) goto falsch;	// zu lang zum Speichern
 lstrcpy(e,T("V"));
 e[ELEN-1]=0;	// zwangsterminieren
 i=_stscanf(s,T("%f:%f%n %") ELENSTR T("[^ -\x40[-`{-\x7F]/V%n"),&z,&n,&j,e,&k);
 if (i==2 && j==l) goto ok;
 if (i==3 && k==l) goto ok;
// 2. Versuch der Interpretation
 i=_stscanf(s,T("%f%") ELENSTR T("[^ -@[-`{-\x7F]:%fV%n"),&z,e,&n,&k);
 if (i==3 && k==l) {
ok:
  if (!n || !z || !e[0]) goto falsch;
  lstrcpy(einheit,e);
  praefixe=wPrefix;	// vom Setup (normalerweise p..G)
// Die Begrenzung der Vorsätze ist einheiten-abhängig, etwa:
// keine Kilosekunden (max=0), Megagramm (max=1, Tonne keine hübsche
// Alternative, denn es gibt keine Kilotonne, außer bei Sprengstoff),
// weder Milli- noch Kilomol (min=max=0), ebenso bei (Bruttoregister)Tonne usw.
  if (!e[1]) switch (e[0]) {
   case 's': ((BYTE*)&praefixe)[1]=0; break;	// Sekunden (keine Kilosek.)
   case 'm':					// Meter (keine Megameter)
   case 'g': ((BYTE*)&praefixe)[1]=1; break;	// Gramm (keine Megagramm)
   case 't': praefixe=0;			// Tonne (keine Vorsätze)
  }
  z/=n;			// rechenwirksames Tastkopf-Verhältnis (10:1->10)
  n=fTastkopf;		// vorheriges Tastkopf-Verhältnis retten
  fTastkopf=z;		// neues Tastkopf-Verhältnis setzen
  if (n) {
   z/=n;		// z/n = Verhältnis beider (1:1->10:1 -> z=10)
   min*=z; max*=z;	// Grenzen für (sinnvolle) Koeffizienten ändern
   SetAblenkung(div*z);	// "div" ändern, da voltprolsb konstant bleiben soll
  }
  lstrcpy(sTastkopf,s);
  return InfoNeu(WAS_YVOR);
 }
falsch:
 if (!fTastkopf) SetTastkopf(T("1:1"));	// jaja, Rekursion!
 return false;	// 2 Zahlen müssen es mindestens sein!
}

// Kopplungs-Strings in Zahl 0..2 umwandeln
bool IsKopplung1(PCTSTR s, UINT&v) {
 int k=String2Index(sKopplung1,s);
 if (k<0) return false;
 v=k;
 return true;
}
bool IsKopplung2(PCTSTR s, UINT&v) {
 if (!s[0]) return false;
 if (s[1]) return false;
 PCTSTR p=StrChr(sKopplung2,s[0]);
 if (!p) return false;
 v=UINT(p-sKopplung2);
 return true;
}

bool KANAL::SetAblenkung(PCTSTR s) {
// Bei ungültigem <div> erfolgt auf jeden Fall eine Initialisierung
 float z=0;
 EINHEIT e;
 int l=lstrlen(s);
 UINT k=kopplung;
 if (l) {
  IsKopplung2(s+--l,k);	// SCHIFFBRUCH BEI DBCS!!!
  z=div;
  int i=String2UpDown(s,1,1);
  if (i>0) NextFloat(z,i,max/*fTastkopf*/);
  else if (i<0) NextFloat(z,i,min/*fTastkopf*/);
  else if (!String2Float(s,z,e)) z=0;
  else if (e[0] && lstrcmp(e,einheit)) z=0;	// falsche Einheit
 }
 SetKopplung(k);
 return SetAblenkung(z);
}

bool KANAL::SetAblenkung(float z) {
// Negative Zahlen führen entsprechend zur Invertierung
 CHAN chan;
 BYTE was=WAS_YABL;
 if (!z) {
  if (div) return false;	// Null darf es nicht werden!
  z=max;			// max = Linksanschlag
 }
 if (div==z) return true;
 if (div*z<0) was|=WAS_YINV;	// Invertierung dazu
 div=z;
 chan.what=CHAN_VOLT;
 chan.ch=(BYTE)kn;
 chan.volt=z*Kaestel.y/fTastkopf/FullScale;
 quelle->RelayMsg(Q_SETCHAN,&chan);
 voltprolsb=chan.volt;	// rücklesen, zur Skalierung
 voltoffset=chan.dcoffset;
 if (trig && trig->k==this) trig->SetPegel(trig->pegel);
 return InfoNeu(was);
}

bool KANAL::SetNulllinie(PCTSTR s) {
// wird vom Dialog und von DDEPOKE aufgerufen
 float z=nulllinie;
 EINHEIT e;
 int i=String2UpDown(s,1,1);
 if (i) z+=i; else{
  if (!String2Float(s,z,e)) return false;
  if (*e) {
   if (!lstrcmp(e,einheit)) z/=div;
   else if (lstrcmpi(e,T("div"))) return false;	// falsche Einheit
  }
 }
 return SetNulllinie(z);	// in div
}

bool KANAL::SetNulllinie(float z) {
 CHAN c;
 if (nulllinie==z) return true;
 nulllinie=z;
 c.what=CHAN_DCOFFSET;
 c.ch=(BYTE)kn;
 c.dcoffset=z*div;
 quelle->RelayMsg(Q_SETCHAN,&c); // Falls die Quelle einen Offset beherrscht...
 voltoffset=c.dcoffset;		// rücklesen
 if (trig && trig->k==this && trig->kreuz) trig->kreuz->Update(WAS_TPEGEL);
 return InfoNeu(WAS_YNUL);
}

bool KANAL::SetKopplung(PCTSTR s) {
 UINT k;
 if (!s) return false;
 if (!IsKopplung1(s,k) && !IsKopplung2(s,k)) return false;
 return SetKopplung(k);
}

bool KANAL::SetKopplung(UINT k) {
 if (k>=3) return false;	// Etwas anderes als DC,AC,GND gibt es nicht
 if (kopplung==k) return true;	// (Noch nicht!)
 CHAN c;
 c.ch=(BYTE)kn;
 c.what=(BYTE)CHAN_COUPLING;
 c.coupling=(BYTE)(k);
 if (!quelle->RelayMsg(Q_SETCHAN,&c)) return false;
 kopplung=k;
 if (k==CHAN_DC && c.coupling==CHAN_AC) kopplung=CHAN_AC;
	// DC-Kopplung kann nicht simuliert werden! Menüpunkt ist grau.
 getsample=GetSampleDc;
 if (k==CHAN_AC && c.coupling==CHAN_DC) getsample=GetSampleAc;
	// Simulation der AC-Kopplung mittels GetSampleAc
 if (k==CHAN_GND && c.coupling!=CHAN_GND)
  getsample=GetSampleGnd;
	// Simulation der GND-Kopplung durch brutales Null-Liefern
 return InfoNeu(WAS_YKOP);
}	// Andere Kopplungen, wie Klemmung+, Klemmung- usw. sind denkbar.

bool KANAL::SetFarbe(PCTSTR s) {
 COLORREF c;
 return (bool)(String2Farbe(s,c) && SetFarbe(c));
}
bool KANAL::SetFarbe(COLORREF c) {
 if (c==BackColor) return false;
 if (c==GridColor) return false;
 if (farbe==c) return true;	// Kurzschluss wenn gleich
 farbe=c;
 return InfoNeu(WAS_FARBE);	// FEHLT: Stift, Pinsel, Paletteneinträge beschaffen
}

bool GRUPPE::SetVar(int i, PCTSTR s) {
 bool r=true;
 for (int k=0; k<numkanal; k++)
   if (kanal[k].HatName(name)) r=bool(kanal[k].SetVar(i,s) && r);
 return r;
}
bool GRUPPE::GetVar(int i, PTSTR s) {
// Die Extrawurst für die Offsets steht hier noch aus!
 bool r=true;
 bool first=true;
 TCHAR buf[32];
 for (int k=0; k<numkanal; k++) if (kanal[k].HatName(name)) {
  r=bool(kanal[k].GetVar(i,buf) && r);
  if (first) {
   lstrcpy(s,buf);
   first=false;
  }else if (lstrcmp(s,buf)) {
   lstrcat(s,T("?"));	// Ungleiche Werte: markieren
   return r;		// Schleife verlassen
  }
 }
 return r;
}

bool KANAL::SetVar(int i, PCTSTR s) {
 switch (i) {
  case 0: return SetNamen(s);
  case 1: return SetTastkopf(s);
  case 2: return SetAblenkung(s);
  case 3: return SetKopplung(s);
  case 4: return SetNulllinie(s);
  case 5: return SetFarbe(s);
 }
 return false;
}
bool KANAL::SetVar(PCTSTR n, PCTSTR s) {
 return SetVar(String2Index(sKanalVar,n),s);
}
bool KANAL::SetVar(HSZ hsz,PCTSTR s) {
 return SetVar(hsz2Index(hszKanalVar,elemof(hszKanalVar),hsz),s);
}

bool KANAL::GetVar(int i, PTSTR s) {
 switch (i) {
  case 0: lstrcpy(s,sNamen); break;
  case 1: lstrcpy(s,sTastkopf); break;
  case 2: Float2String(s,div,praefixe,einheit); break;
  case 3: lstrcpy(s,Index2String(sKopplung1,kopplung)); break;
  case 4: Float2String(s,nulllinie,0,T("div")); break;
  case 5: Farbe2String(s,farbe); break;
  default: return false;
 }
 return true;
}
bool KANAL::GetVar(PCTSTR n, PTSTR s) {
 return GetVar(String2Index(sKanalVar,n),s);
}
bool KANAL::GetVar(HSZ hsz,PTSTR s) {
 return GetVar(hsz2Index(hszKanalVar,elemof(hszKanalVar),hsz),s);
}

void KANAL::GetInfoString(PTSTR s) {
// ASCII-Info-String zusammenstellen
 TCHAR buf[32];
// Hier: <div> invertieren bei _echter_ Inversion
 Float2String(buf,div,praefixe,einheit);
 wnsprintf(s,elemof(s),T("%c %s %c"),
   sTastkopf2[GetTastkopfIndex()],
   buf,
   sKopplung2[kopplung]);
}

bool KANAL::InfoNeu(BYTE was) {
 TCHAR buf[64];
 HMENU m;		// Untermenü
 if (submenu) {		// Kanäle >16 oder so bekommen kein Untermenü...
  if (was&(WAS_YVOR|WAS_YINV)) {
   LadeAblenkListe();
  }
  if (was&WAS_YVOR) {
   int j=GetTastkopfIndex();
   m=GetSubMenu(submenu,SUB_YVOR);
   CheckMenuRadioItem(m,0,2,j,MF_BYPOSITION);
   if (j<2) j^=1;	// bei User-Tastkopf bleibt die Vorgabe benutzerdefiniert
   SetMenuDefaultItem(m,j,MF_BYPOSITION);
   SetMenuStringAfterTab(submenu,SUB_YVOR,sTastkopf);
  }
  if (was&WAS_YINV) {	// zieht stets WAS_YABL nach sich
   bool neg=bool(div<0);
   CheckMenuItem(submenu,SUB_YINV,
     neg?MF_BYPOSITION|MF_CHECKED:MF_BYPOSITION|MF_UNCHECKED);
   SetMenuStringAfterTab(submenu,SUB_YINV,neg?T("--"):T("+"));
  }
  if (was&WAS_YABL) {
   Markiere(submenu,SUB_YABL,IDC_YABL+idhigh,div);
   Float2String(buf,div,praefixe,einheit);
   lstrcat(buf,T("/div"));
   SetMenuStringAfterTab(submenu,SUB_YABL,buf);
  }
  if (was&WAS_YKOP) {
   m=GetSubMenu(submenu,SUB_YKOP);
   CheckMenuRadioItem(m,0,2,kopplung,MF_BYPOSITION);
   SetMenuDefaultItem(m,GetMenuState(m,0,MF_BYPOSITION)==MF_GRAYED?kopplung==1?2:1:
   kopplung==0?1:0,MF_BYPOSITION);
   SetMenuStringAfterTab(submenu,SUB_YKOP,Index2String(sKopplung1,kopplung));
  }
  if (was&WAS_YNUL) {
   CalcGraf(); Inval(false);
  }
  if (was&(WAS_YVOR|WAS_YABL|WAS_YKOP)) {
   GetInfoString(buf);
   SetMenuStringAfterTab(OsziMenu,NumZeitMenu+NumTrigMenu+kn,buf);
  }
  if (was&WAS_YNUL) {
   Float2String(buf,nulllinie,0,T("div"));
   SetMenuStringAfterTab(submenu,SUB_YNUL,buf);
  }
  if (was&WAS_YNAM) {
   SetMenuStringAfterTab(submenu,SUB_YNAM,sNamen);
  }
  if (was&WAS_FARBE) {
   Farbe2String(buf,farbe);
   SetMenuStringAfterTab(submenu,SUB_YFARBE,buf);
  }
 }
 if (was&WAS_YNAM) MacheGruppenListe();
 if (masse) masse->Update(was);
 if (anzeige) anzeige->Update(was);
// Zu tun: hKanalDlg, DDEADV aktualisieren
 return true;
}

bool KANAL::HatName(PCTSTR n) {		// Testet, ob Name enthalten ist
// Liefert <true> auch bei Kanalbezeichnung (bspw. "Y1")
// sowie wenn n=NULL (bedeutet hier "alle Kanäle")
 TCHAR buf[32],*p;
 if (!n) return true;
 if (!*n) return true;
 if (!lstrcmpi(n,name)) return true;
 GetNameList(buf,NULL);
 for (p=buf; *p; p+=lstrlen(p)+1) {
  if (!lstrcmpi(p,n)) return true;
 }
 return false;
}

/****************************************************
 ** Dialoge (erst modusbehaftete, dann modusfreie) **
 ****************************************************/
static int LoadWinPos(HWND Wnd, PCTSTR key) {
// Liefert den Sichtbarkeitsstatus, auch bei Wnd=0, zum Wieder-Öffnen des Dialogs
// Lädt Position und, wenn gegeben und WS_THICKFRAME, auch die Größe
 TCHAR buf[32];
 WINDOWPLACEMENT wp;
 RECT r;
 int showCmd;
 InitStruct(&wp,sizeof(wp));
 if (Wnd) GetWindowPlacement(Wnd,&wp);
 GetString(S_Position,key,sLeer,buf,elemof(buf));
 switch (_stscanf(buf,sProzentI,&r.left,&r.top,&r.right,&r.bottom,&showCmd)) {
  case 2: r.right=wp.showCmd; nobreak;
  case 3: {	// feste Fenstergröße
   showCmd=r.right;
   r.right=r.left+wp.rcNormalPosition.right-wp.rcNormalPosition.left;
   r.bottom=r.top+wp.rcNormalPosition.bottom-wp.rcNormalPosition.top;
  }break;
  case 4: showCmd=wp.showCmd; nobreak;
  case 5: if (!Wnd || GetWindowStyle(Wnd)&WS_THICKFRAME) break;
  default: return SW_HIDE;	// Alles andere ist Fehler!
 }	// (auch einem Fenster ohne WS_THICKFRAME die Größe setzen zu wollen)
 if (Wnd) {
  MoveRectIntoFullScreen(&r);	// Bei Win32 angeblich nicht notwendig
  CopyRect(&wp.rcNormalPosition,&r);
//  if (showCmd) wp.showCmd=showCmd;	// nie "hidden" setzen
  SetWindowPlacement(Wnd,&wp);
 }
 return showCmd;
}

static void SaveWinPos(HWND Wnd, PCTSTR key, bool state=false) {
// Setzt den Sichtbarkeitsstatus auf SW_HIDE wenn state=false ist
// Speichert Position -- sowie Größe wenn Fenster WS_THICKFRAME hat
 WINDOWPLACEMENT wp;
 TCHAR buf[32],*p=buf;
 if (!Wnd) return;	// Fenster muss noch vorhanden sein!
 InitStruct(&wp,sizeof(wp));
 GetWindowPlacement(Wnd,&wp);
 if (!state) wp.showCmd=0;
 p+=wsprintf(p,sProzentI+9,wp.rcNormalPosition.left,wp.rcNormalPosition.top);
 if (GetWindowStyle(Wnd)&WS_THICKFRAME) p+=wsprintf(p,sProzentI+8,
   wp.rcNormalPosition.right,wp.rcNormalPosition.bottom);
 if (wp.showCmd) wsprintf(p,sProzentI+11,wp.showCmd);
 WriteString(S_Position,key,buf);
}

static INT_PTR CALLBACK AboutDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM){
 switch (Msg) {
  case WM_INITDIALOG: {
   LoadWinPos(Wnd,T("Über"));
  }return TRUE;

  case WM_COMMAND: switch (LOWORD(wParam)){
   case 1:
   case 2:
   SaveWinPos(Wnd,T("Über"));
   EndDialog(Wnd,wParam);
  }break;
 }
 return FALSE;
}


bool ScanXY(PCTSTR s, PPOINT p) {
// sscanf auf String der Form %dx%d ausführen und in p speichern,
// dabei werden beide Koordinaten auf 2..100 begrenzt
// Liefert false bei Fehler
 int x,y;
 if (_stscanf(s,T("%dx%d"),&x,&y)==2 && 2<=x && x<=100 && 2<=y && y<=100) {
  p->x=x; p->y=y; return true;
 }
 return false;
}

void PrintXY(PTSTR s, const POINT *p) {
 _sntprintf(s,32,T("%dx%d"),p->x,p->y);
}

bool ScanPr(PCTSTR s, WORD &pr) {
 TCHAR a,e;
 PTSTR pa,pe;
 if (_stscanf(s,T("%c..%c"),&a,&e)!=2) return false;
 pa=Prefix2Ptr(a); if (!pa) return false;
 pe=Prefix2Ptr(e); if (!pe) return false;
 if (pa>pe) return false;
 pr=MAKEWORD(pa-ISO_Prefixes-6,pe-ISO_Prefixes-6);
 return true;
}

void PrintPr(PTSTR s, WORD pr) {
 _sntprintf(s,32,T("%c..%c"),ISO_Prefixes[(signed char)LOBYTE(pr)+6],
   ISO_Prefixes[(signed char)HIBYTE(pr)+6]);
}
/**********************************
 ** Allgemeine Dialog-Behandlung **
 **********************************/

#define WM_CHECKDIALOG	(WM_USER+2)	// konsequente Fortsetzung vom
#define WM_TAKEDIALOG	(WM_USER+3)	// WM_INITDIALOG-Konzept, oder?
// In beiden F„llen ist lParam=DWL_USER=Parameter von ...Dialog...Param
// WM_CHECKDIALOG muss im Fehlerfall mindestens eins von beiden tun:
// LOWORD(lParam) auf die fehlerhafte Fenster-ID setzen,
// HIWORD(lParam) auf die String-ID f�r MessageBox setzen
// und damit MyDlgHandler aufrufen - oder DWL_DLGRESULT setzen
#define IDAPPLY		3
#define IDAUTOAPPLY	4
#define IDTIMEDAPPLY	5
#define IDHELP		9

#define PROP_DDEADV	1	// DDEADVISE aktiv (normal gelb)
#define PROP_DIVERS	2	// Verschiedene Werte in Gruppe (normal grau)
#define PROP_ERROR	4	// Fehler (normal rot)
void ChangeProp(HWND Wnd, UINT and, UINT xor) {
 UINT o=(UINT)GetProp(Wnd,MAKEINTATOM(atom));
 UINT n=(o&and)^xor;
 if (o==n) return;
 if (n) SetProp(Wnd,MAKEINTATOM(atom),(HANDLE)n);
 else RemoveProp(Wnd,MAKEINTATOM(atom));
 InvalidateRect(Wnd,NULL,TRUE);
}
void ChangeProp(HWND Wnd, UINT id, UINT and, UINT xor) {
 ChangeProp(GetDlgItem(Wnd,id),and,xor);
}

#define WM_SHOWITEMS	(WM_USER+100)
#define AutoUpdate_id	3
#define AutoUpdate_ms	500

#define EN_STEPUP (EN_VSCROLL+0x10)
#define EN_STEPDOWN (EN_STEPUP+1)
WNDPROC DefEditProc;
LRESULT CALLBACK EditHook(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 UINT dir=0;
 switch (Msg) {
  case WM_KEYDOWN: switch (wParam) {
   case VK_UP: dir=EN_STEPUP; break;
   case VK_DOWN: dir=EN_STEPDOWN; break;
  }break;
  case WM_VSCROLL: switch (LOWORD(wParam)) {
   case SB_LINEUP: dir=EN_STEPUP; break;
   case SB_LINEDOWN: dir=EN_STEPDOWN; break;
  }break;
  case WM_DESTROY: ChangeProp(Wnd,0,0); break;
 }
 if (dir) {
  SendMessage(CONTROLPARAMS3(WM_COMMAND,Wnd,dir));
  return 0;
 }
 return CallWindowProc(DefEditProc,Wnd,Msg,wParam,lParam);
}
// Standard-Dialogprozedur für meine Dialoge (modal oder moduslos):
// * Position aus INI-Datei speichern/wiederherstellen
// * WM_ACTIVATE, WM_HELP behandeln
// * alle einzeiligen EDITs subklassifizieren für UpDown
BOOL MyDlgHandler(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam,
  UINT id, HWND *wp, PTSTR PosKey) {
 switch (Msg) {
  case WM_INITDIALOG: {
   SetWindowLongPtr(Wnd,DWLP_USER,lParam);	// Compiler beruhigen (nur Win32)
   if (PosKey) LoadWinPos(Wnd,PosKey);
   for (HWND w=GetFirstChild(Wnd); w; w=GetNextSibling(w)) {
    TCHAR s[8];
    GetClassName(w,s,elemof(s));
    if (!lstrcmpi(s,T("EDIT"))) DefEditProc=SubclassWindow(w,EditHook);
   }
  }return TRUE;				// Fokus setzen _lassen_

  case WM_ACTIVATE: if (wp) hKBHand=wParam?Wnd:0; break;

  case WM_HELP: {
   WinHelp(Wnd,HelpFileName,HELP_CONTEXTPOPUP,
     MAKELONG(id,((LPCHELPINFO)lParam)->iCtrlId));
  }break;

#ifdef WIN32
  case WM_CTLCOLOREDIT: {
#else
  case WM_CTLCOLOR: if (HIWORD(lParam)==CTLCOLOR_EDIT){
#endif
   int i=(int)GetProp(Wnd,MAKEINTATOM(atom));
   if (i) {
    SetBkMode((HDC)wParam,TRANSPARENT);
    return (BOOL)EditBrushes[bsf(i)];	// Nummer des höchsten Bits!
   }
  }return TRUE;

  case WM_TIMER: if (wParam==AutoUpdate_id) {
   KillTimer(Wnd,wParam);
   SendMessage(Wnd,WM_COMMAND,IDTIMEDAPPLY,0);
  }break;
  case WM_CHECKDIALOG: SetWindowLongPtr(Wnd,DWLP_MSGRESULT,lParam); nobreak;
  case WM_TAKEDIALOG: return TRUE;	// kein DefDlgProc

  case WM_COMMAND: {
   switch (LOWORD(wParam)){
    case IDHELP: WinHelp(Wnd,HelpFileName,HELP_CONTEXT,id); break;
    case IDAUTOAPPLY: {
     BOOL state=IsDlgButtonChecked(Wnd,IDAUTOAPPLY)-1;
     EnableDlgItem(Wnd,IDAPPLY,state);
     if (state) break;
    }nobreak;
    case IDTIMEDAPPLY:
    case IDAPPLY:
    case IDOK: {
     lParam=SendMessage(Wnd,WM_CHECKDIALOG,0,GetWindowLongPtr(Wnd,DWLP_USER));
     if (lParam) {
      if (LOWORD(wParam)<=IDAPPLY) {	// Ohne explizite Aufforderung nicht meckern
       if (LOWORD(lParam)) SetEditFocus(Wnd,LOWORD(lParam));
       if (HIWORD(lParam)) MBox(Wnd,HIWORD(lParam),MB_OK|MB_ICONEXCLAMATION);
      } 
      break;	// BUG: Nicht immer Edit!
     }
     SendMessage(Wnd,WM_TAKEDIALOG,0,GetWindowLongPtr(Wnd,DWLP_USER));
     if (LOWORD(wParam)!=IDOK) break;
    }
    case IDCANCEL: {
     if (PosKey) SaveWinPos(Wnd,PosKey);
     if (wp) {
      *wp=0;
      DestroyWindow(Wnd);
     }else{
      EndDialog(Wnd,LOWORD(wParam));
     }
    }break;
   }
   switch (GET_WM_COMMAND_CMD(wParam,lParam)) {
    case CBN_EDITCHANGE: {
     TCHAR s[12];
     GetClassName((HWND)lParam,s,elemof(s));
     if (lstrcmpi(s,T("COMBOBOX"))) break;
    }nobreak;
    case EN_CHANGE: {
     ChangeProp((HWND)lParam,~PROP_ERROR,0);	// bei Änderung nicht rot!?
     if (IsDlgButtonChecked(Wnd,4) 
     && !SetTimer(Wnd,AutoUpdate_id,AutoUpdate_ms,NULL))
       PostMessage(Wnd,WM_TIMER,AutoUpdate_id,0);
    }break;
   }
  }
 }
 return FALSE;
}

void FarbRechteck(HDC dc,LPCRECT rc,COLORREF f) {
// Zeichnet (für Dialogboxen) umrandete Rechecke mit der Farbe f ausgefüllt
// Umrandung ist schwarz; weiß bei dunkler Farbe. Inhalt zweigeteilt.
 HPEN p;
 HBRUSH br;
 int m;
 RECT r;
 CopyRect(&r,rc);
 m=(r.left+r.right)>>1;
// 1. Pattern-Farbe (links)
 r.right=m;
 br=CreateSolidBrush(f);
 FillRect(dc,&r,br);
 DeleteBrush(br);
 r.right=rc->right;
// 2. Einheitliche Farbe (rechts)
 r.left=m;
 br=CreateSolidBrush(GetNearestColor(dc,f));
 FillRect(dc,&r,br);
 DeleteBrush(br);
 r.left=rc->left;
// 3. Rand (schwarz oder weiß [keine Farbkomponente >=128])
 p=SelectPen(dc,GetStockPen(f&0x808080L?BLACK_PEN:WHITE_PEN));
 br=SelectBrush(dc,GetStockBrush(HOLLOW_BRUSH));	// nur Rand
 Rectangle(dc,r.left,r.top,r.right,r.bottom);
 SelectBrush(dc,br);
 SelectPen(dc,p);
}

static INT_PTR CALLBACK DisplayDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam){
 switch (Msg) {
  case WM_INITDIALOG: {
   TCHAR s[32];
   SetCheckboxGroup(Wnd,101,108,DispOpt);
   PrintXY(s,&Kaestel);
   SetDlgItemText(Wnd,120,s);
   PrintXY(s,&SubTick);
   SetDlgItemText(Wnd,121,s);
#ifndef WIN32
   KillTimer(Wnd,3);	// In 16-bit-Windows erforderlich?
#endif
  }break;

  case WM_TIMER: {
   TCHAR s[32];
   KillTimer(Wnd,wParam);
   if (GetModifiedEditItemText(Wnd,120,s,elemof(s))) {
    if (ScanXY(s,&Kaestel)) {
     wmSize(ClientExt.x,ClientExt.y);
    }else MessageBeep(MB_ICONEXCLAMATION);
   }
   if (GetModifiedEditItemText(Wnd,121,s,elemof(s))) {
    if (ScanXY(s,&SubTick)) {
     wmSize(ClientExt.x,ClientExt.y);
    }else MessageBeep(MB_ICONEXCLAMATION);
   }
  }break;

  case WM_COMMAND: switch (LOWORD(wParam)){
   case 101:
   case 102:
   case 103:
   case 104:
   case 105:
   case 108: {
    SetDispOpt(DispOpt^(1<<(wParam-101)),0);
   }break;
   case 120:
   case 121: {
    if (GET_WM_COMMAND_CMD(wParam,lParam)==EN_CHANGE) SetTimer(Wnd,3,500,NULL);
   }break;
   case 122: {
    Kaestel.x=10; Kaestel.y=8;
    SubTick.x=5;  SubTick.y=5;
    DisplayDlgProc(Wnd,WM_INITDIALOG,0,0);
    wmSize(ClientExt.x,ClientExt.y);
   }break;
   case 111: if (FarbAuswahl(Wnd,GridColor)) SetDispOpt(DispOpt,DO_GRID); break;
   case 112: if (FarbAuswahl(Wnd,BackColor)) SetDispOpt(DispOpt,DO_BACK); break;
   case 1:
    DisplayDlgProc(Wnd,WM_TIMER,3,0);	// evtl. Editfelder übernehmen
  }break;
 }
 return MyDlgHandler(Wnd,Msg,wParam,lParam,112,&hDisplayDlg,T("Anzeige"));
}

// Grundlegende Einstellungen: Kanalnamen-Präfix, Zählweise (MODAL)
// Bonus: Ist eine Einstellung von Zählweise und Dezimaltrenner
// per Modifikation der OSZI.INI gemacht worden, dann ist kein RadioButton
// aktiv, und beim Übernehmen wird, wenn nichts gecheckt, dann auch
// nicht verändert. Wenn also bspw. Oktal-Zählung, Start ab 200,
// 5 reservierte Stellen und ein „ oder TEN (Punkt in der Mitte für Chinesen)
// als Dezimaltrenner bevorzugt werden, dann bitteschön!
static INT_PTR CALLBACK GrundDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam){
 static const TCHAR sPunktKomma[]=T("?.,");
 switch (Msg) {
  case WM_INITDIALOG: {
   TCHAR s[32];
   {PTSTR p;
    lstrcpy(s,sKanalFormat); p=StrChr(s,'%');
    if (p) {
     *p++=0;	// Präfix von Formatanweisung trennen
     if (*p=='0') {
      CheckDlgButton(Wnd,107,TRUE);	// Führende Null (bei 1 Stelle)
      p+=2;
     }
     switch (*p) {
      case 'd':
      case 'i':
      case 'u': CheckDlgButton(Wnd,104,TRUE); break;	// dezimal zählen
      case 'x':
      case 'X': CheckDlgButton(Wnd,105,TRUE); break;	// hexadezimal zählen
     }
    }
   }
   SetDlgItemText(Wnd,101,s);	// Präfix
   if ((unsigned)iKanalStart<2) CheckDlgButton(Wnd,102+iKanalStart,TRUE);
   SetDlgItemText(Wnd,111,sMikro);
   SetDlgItemInt(Wnd,112,MaxKanalMenu,FALSE);
   GetDlgItemText(Wnd,120,s,elemof(s));
   TCHAR buf[32];
   _sntprintf(buf,elemof(buf),s,*sDecimal);
   SetDlgItemText(Wnd,120,buf);
   {PCTSTR p;
    p=StrChr(sPunktKomma,*sDezimal);
    if (p) CheckDlgButton(Wnd,120+int(p-sPunktKomma),TRUE);
   }
   CheckDlgButton(Wnd,123,TRUE);	// Ausrichtung "von links nach rechts"
   PrintPr(s,wPrefix);
   SetDlgItemText(Wnd,125,s);
  }break;
  case WM_CHECKDIALOG: {	// Zwangweise zusammenstellen, bei fehlenden Angaben Vorgabe
   lParam=0;
   TCHAR *p=sKanalFormat+GetDlgItemText(Wnd,101,sKanalFormat,4);
   *p++='%';
   if (IsDlgButtonChecked(Wnd,107)) {*p++='0'; *p++='2';}
   *p++=IsDlgButtonChecked(Wnd,105) ? 'X' : 'd';
   *p=0;
   iKanalStart=1-IsDlgButtonChecked(Wnd,102);	// ohne Angabe "ab 1" wählen
   TCHAR m[2],s[8];
   GetDlgItemText(Wnd,111,m,elemof(m));
   if (*m!='u' && (unsigned)*m<0x80) {lParam=111; break;}
   UINT i=GetDlgItemInt(Wnd,112,NULL,FALSE);
   if (i<4) {lParam=112; break;}
   i=GetRadioCheck(Wnd,120,122);
   if ((int)i>=0) *sDezimal=sPunktKomma[i];	// des Users Wunsch ist des Users Himmelreich!
   GetDlgItemText(Wnd,125,s,elemof(s)); WORD pr;
   if (!ScanPr(s,pr)) {lParam=125; break;}
   *sMikro=ISO_Prefixes[4]=*m;	// Nur u oder Sonderzeichen akzeptieren!
   MaxKanalMenu=i;
   wPrefix=pr;
  }break;
 }
 return MyDlgHandler(Wnd,Msg,wParam,lParam,110,NULL,T("Grundlage"));
}
/********************************
 ** Ein Super-Zeitbasis-Dialog **
 ********************************/
INT_PTR CALLBACK ZeitDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam){
 switch (Msg) {
  case WM_INITDIALOG: {
   int i;
   HWND w=GetDlgItem(Wnd,100);
   for (i=0; i<NumZeitMenu; i++) {
    ComboBox_AddString(w,zeit[i].name);   
   }
   ComboBox_SetCurSel(w,ZeitDlgCurSel);
   PostMessage(Wnd,WM_SHOWITEMS,0,0);
   PostMessage(Wnd,WM_INFONEU,0xFF,(LPARAM)(zeit+ZeitDlgCurSel));
  }break;
  case WM_SHOWITEMS: {
   TCHAR s[64];
   LoadString(HInstance,44,s,elemof(s));
   SetWindowText(GetPrevSibling(GetDlgItem(Wnd,102)),
     Index2String(s,ZeitDlgCurSel?1:0));
   EnableDlgItem(Wnd,108,NumZeitMenu<4);	// Max. 4 Zeitbasen
   EnableDlgItem(Wnd,109,ZeitDlgCurSel);	// "A" kann nicht gelöscht werden
  }break;
  case WM_INFONEU: {
   TCHAR buf[32];
#define was ((BYTE)wParam)
#define z ((ZEIT*)lParam)
   if (was&WAS_XABLENK) {
    z->GetVar(0,buf);
    SetDlgItemText(Wnd,101,buf);	// PROP fehlt noch!
   }
   if (was&(WAS_RATE|WAS_XANF)) {
    z->GetVar(1,buf);
    SetDlgItemText(Wnd,102,buf);
   }
   if (was&WAS_SAMPLES) {
    z->GetVar(2,buf);
    SetDlgItemText(Wnd,103,buf);
   }
   if (was&WAS_FARBE) InvalidateRect(GetDlgItem(Wnd,112),NULL,TRUE);
#undef z
#undef was   
  }break;
  case WM_DRAWITEM: {	// Farbe für Zeitbasis
#define dis ((LPDRAWITEMSTRUCT)lParam)
   FarbRechteck(dis->hDC,&dis->rcItem,zeit[ZeitDlgCurSel].farbe);
#undef dis
  }break;
  case WM_COMMAND: switch(LOWORD(wParam)) {
   case 100: switch (GET_WM_COMMAND_CMD(wParam,lParam)) {
    case CBN_SELCHANGE: {
     ZeitDlgCurSel=ComboBox_GetCurSel((HWND)lParam);
     PostMessage(Wnd,WM_SHOWITEMS,0,0);
     PostMessage(Wnd,WM_INFONEU,0xFF,(LPARAM)(zeit+ZeitDlgCurSel));
    }
   }break;
   case 101: switch (GET_WM_COMMAND_CMD(wParam,lParam)) {
    case EN_STEPUP:   zeit[ZeitDlgCurSel].SetAblenkung(T("++")); break;
    case EN_STEPDOWN: zeit[ZeitDlgCurSel].SetAblenkung(T("--")); break;
   }break;
   case 108: { // weitere Zeitbasis
    // keine Ahnung, wie weiter!
   }break;
   case 109: { // Zeitbasis löschen
   }break;
   case 110: zeit[ZeitDlgCurSel].FarbAuswahl(Wnd); break;
  }break;
  case WM_CHECKDIALOG: {
   lParam=0;
   TCHAR s[32];
   GetDlgItemText(Wnd,101,s,elemof(s));
   if (!zeit->SetAblenkung(s)) lParam=MAKELONG(101,201);
  }
 }
 return MyDlgHandler(Wnd,Msg,wParam,lParam,IDC_XABLENK,&hZeitDlg,T("ZeitDlg"));
}

/***************************************************
 ** Der Super-Dialog für alle Kanal-Einstellungen **
 ***************************************************/

bool GetEditText(int zero_idx,PTSTR buf) {
 HWND w=GetDlgItem(hKanalDlg,101+zero_idx);
 if (!GetModifiedEditText(w,buf,32)) return false;
 ChangeProp(w,~PROP_ERROR,0);
 InvalidateRect(w,NULL,TRUE);
 return true;
}
void SetEditText(int zero_idx,PTSTR buf) {
// Wird auch bei DDEPOKE aufgerufen!
 HWND w=GetDlgItem(hKanalDlg,101+zero_idx);
 ChangeProp(w,~PROP_ERROR,0);
 SetWindowText(w,buf);
 Edit_SetModify(w,FALSE);
 KillTimer(hKanalDlg,AutoUpdate_id);
}
void LadeKanalComboBox() {
 HWND hCombo=GetDlgItem(hKanalDlg,100);	// Combobox
 TCHAR buf[32],KanalGruppe[32],*pg=KanalGruppe;
 KANAL *k;
 GRUPPE *g;
 int i,item;
 item=ComboBox_GetCurSel(hCombo);
 ComboBox_ResetContent(hCombo);
 LoadString(HInstance,208,KanalGruppe,elemof(KanalGruppe)); // "Kanal %s\0Gruppe %s"
 for (i=0,k=kanal; i<numkanal; i++,k++) {
  _sntprintf(buf,elemof(buf),pg,k->name);
  if (k->sNamen[0]) {
   lstrcat(buf,T(": "));
   lstrcat(buf,k->sNamen);
  }
  ComboBox_AddString(hCombo,buf);
 }
 if (numkanal>=2
 && !IsWindowVisible(GetDlgItem(hKanalDlg,106))) { // Knopf "Gruppen zeigen"
  pg+=lstrlen(pg)+1;		// auf "Gruppe %s"
  for (i=0,g=gruppe; i<numgruppe; i++,g++) {
   if (g->name[0]) {
    _sntprintf(buf,elemof(buf),pg,g->name);	// "Gruppe <gruppenname>"
   }else{			// "Alle Kanäle" / "Beide Kanäle"
    LoadString(HInstance,numkanal==2?207:206,buf,elemof(buf));
   }
   ComboBox_AddString(hCombo,buf);
  }
 }
 ComboBox_SetCurSel(hCombo,item);
}
#define WM_SETKANAL	(WM_USER+101)	// lParam=Kanal
int KanalDlgCurSel;
INT_PTR CALLBACK KanalDlgProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam) {
 int i;
 switch (Msg) {
  case WM_INITDIALOG: {
   hKanalDlg=Wnd;
//   for (i=102; i<=105; i++)
//     DefEditProc=SubclassWindow(GetDlgItem(Wnd,i),EditHook);
   LadeKanalComboBox();
   PostMessage(Wnd,WM_SETKANAL,0,lParam);
  }break;
  case WM_SHOWITEMS: {	//HIBYTE(wParam)=zu schaltende Fenster,
			//LOBYTE(wParam)=Sichtbarkeits-Zustände
   HWND w;		//Bit0=Name, Bit1=Tastkopf, Bit2=Ablenkung,
   int show;		//Bit3=DC-Pegel, Bit4=DC-Stufung, Bit5=Gruppen-Knopf
   for (i=101; i<=108; i++) if (HIBYTE(LOWORD(wParam))&1) {
    w=GetDlgItem(Wnd,i);
    show=LOBYTE(LOWORD(wParam))&1?SW_SHOW:SW_HIDE;
    ShowWindow(w,show);
    if (i<=105) ShowWindow(GetPrevSibling(w),show);	// Beschriftung
    switch (i) {
     case 103:		// das /div hinter dem Ablekkoeffizienten
     case 108: ShowWindow(GetNextSibling(w),show);
    }
    wParam>>=1;
   }
   if (HIBYTE(LOWORD(wParam))&1)	// HIER STIMMT WAS NICHT!
     ShowWindow(GetDlgItem(Wnd,5),LOBYTE(LOWORD(wParam))&1?SW_SHOW:SW_HIDE);
  }break;
  case WM_SETKANAL: {
   SendMessage(Wnd,WM_SHOWITEMS,HIWORD(lParam),0);
   HWND hCombo=GetDlgItem(Wnd,100);
   if (ComboBox_GetCurSel(hCombo)!=LOBYTE(LOWORD(lParam))) {
    for (i=101; i<=105; i++) {
     if ((BOOL)SendDlgItemMessage(Wnd,i,EM_GETMODIFY,0,0)) {
      TCHAR name[32];
      ComboBox_GetLBText(hCombo,ComboBox_GetCurSel(hCombo),name);
      switch (MBox(Wnd,209,MB_YESNOCANCEL|MB_ICONQUESTION,(LPSTR)name)) {
       case IDOK: SendMessage(Wnd,WM_COMMAND,3,0); nobreak; // vorerst!
       case IDCANCEL: return FALSE;
      }
      break;
     }
    }
    i=LOBYTE(LOWORD(lParam));
    ComboBox_SetCurSel(hCombo,i);
    if (HIBYTE(LOWORD(lParam))) SetEditFocus(Wnd,100+HIBYTE(LOWORD(lParam)));
    goto set_vars;
   }
  }break;

  case WM_DRAWITEM: {
#define dis ((LPDRAWITEMSTRUCT)lParam)
   if (KanalDlgCurSel<numkanal)
     FarbRechteck(dis->hDC,&dis->rcItem,kanal[KanalDlgCurSel].farbe);
#undef dis
  }break;

  case WM_COMMAND: switch (LOWORD(wParam)) {
   case 100: switch (GET_WM_COMMAND_CMD(wParam,lParam)) {
    case CBN_SELCHANGE: {
     KanalDlgCurSel=ComboBox_GetCurSel((HWND)lParam);
     if (KanalDlgCurSel<0) break;	// eigentlich: alle Items ausschalten!
     SendMessage(Wnd,WM_SHOWITEMS,
       KanalDlgCurSel>=numkanal?0x5150:0x5101,0);	// Staffelung
// jetzt noch die übrigen Werte setzen...
set_vars:
     TCHAR s[32];
     if (KanalDlgCurSel<numkanal) {
      KANAL *k=kanal+KanalDlgCurSel;
      k->GetVar(0,s); SetDlgItemText(Wnd,101,s);
      k->GetVar(1,s); SetDlgItemText(Wnd,102,s);
      k->GetVar(2,s); SetDlgItemText(Wnd,103,s);
//      wsprintf(s+lstrlen(s),T(" %c"),T("=~_")[k->kopplung]);
      k->GetVar(4,s); SetDlgItemText(Wnd,104,s);
     }else{
      GRUPPE *g=gruppe+KanalDlgCurSel-numkanal;
      g->GetVar(1,s); SetDlgItemText(Wnd,102,s);
      g->GetVar(2,s); SetDlgItemText(Wnd,103,s);
//      wsprintf(s+lstrlen(s),T(" %c"),T("=~_")[k->kopplung]);
      g->GetVar(4,s); SetDlgItemText(Wnd,104,s);
     }
    }break;
   }break;
   case 101:
   case 102:
   case 103:
   case 104:
   case 105: switch (GET_WM_COMMAND_CMD(wParam,lParam)) {
//    case EN_CHANGE: {
//     ChangeProp((HWND)lParam,~PROP_DIVERS,0);	// bei Änderung nicht grau!?
//     if (IsDlgButtonChecked(Wnd,4)) SetTimer(Wnd,AutoUpdate_id,AutoUpdate_ms,NULL);
//    }break;
    case EN_STEPUP:
    case EN_STEPDOWN: switch (LOWORD(wParam)){
     case 103: {
      TCHAR buf[32];
      UINT kn=(UINT)SendDlgItemMessage(Wnd,100,CB_GETCURSEL,0,0);
      if (!GetEditText(2,buf) || kanal[kn].SetAblenkung(buf))
	SendMessage(MainWnd,WM_COMMAND,
	(kn<<10)+IDC_YABL+elemof(Reihe)+
	GET_WM_COMMAND_CMD(wParam,lParam)-EN_STEPUP+1,0);
     }break;
    }break;
   }break;
   case 106: {
//    ShowWindow((HWND)lParam,SW_HIDE);	// Knopf verschwindet
    SendMessage(Wnd,WM_SHOWITEMS,0x2F0E,0);	// aber noch keine Staffelung
    LadeKanalComboBox();
   }break;
   case 110: if (KanalDlgCurSel<numkanal) kanal[KanalDlgCurSel].FarbAuswahl(Wnd); break;
  }break;
  case WM_CHECKDIALOG: {
   TCHAR buf[32];
   lParam=0;
   i=(int)SendDlgItemMessage(Wnd,100,CB_GETCURSEL,0,0);
   KANAL *k;
   if ((unsigned)i<(unsigned)numkanal) k=kanal+i;
   if (GetEditText(0,buf)
   && !k->SetVar(0,buf)) {
    lParam=MAKELONG(101,201);
    ChangeProp(Wnd,101,~PROP_ERROR,PROP_ERROR);
   }
   if (GetEditText(1,buf)
   && !k->SetVar(1,buf)) {
    lParam=MAKELONG(102,201);
    ChangeProp(Wnd,102,~PROP_ERROR,PROP_ERROR);
   }
   if (GetEditText(2,buf)
   && !k->SetVar(2,buf)) {
    lParam=MAKELONG(103,201);
    ChangeProp(Wnd,103,~PROP_ERROR,PROP_ERROR);
   }
   if (GetEditText(3,buf)
   && !k->SetVar(4/*!!*/,buf)) {
    lParam=MAKELONG(104,201);
    ChangeProp(Wnd,104,~PROP_ERROR,PROP_ERROR);
   }
  }break;
 }		// switch
 return MyDlgHandler(Wnd,Msg,wParam,lParam,IDC_YABL,&hKanalDlg,T("KanalDlg"));
}

void StartDlg(HWND&Wnd, UINT DlgID, DLGPROC DlgProc, LPARAM lParam) {
 if (!Wnd) Wnd=CreateDialogParam(
   HInstance,MAKEINTRESOURCE(DlgID),MainWnd,DlgProc,lParam);
 else{
  SetActiveWindow(Wnd);
  SendMessage(Wnd,WM_SETKANAL,0,lParam);
 }
}

void StartKanalDlg(LPARAM lParam) {
/* Bit-Belegung von lParam:
 7:0	Kanal- oder Gruppennummer
 15:8	Zu fokussierende Edit-Zeile: 1=Namen, 2=Tastkopf,3=Ablenkung,4=Offset
 16	Sichtbarkeit von NAMEN-Eingabezeile
 17	Sichtbarkeit von TASTKOPF-Eingabezeile usw.
 18	ABLENKUNG
 19	OFFSET
 20	STUFUNG (nur bei Gruppe)
 21	GRUPPEN-Knopf
 24	Schalten NAMEN-Sichtbarkeit usw.*/
 StartDlg(hKanalDlg,IDC_YABL,KanalDlgProc,lParam);
}

void CreateEditBrushes() {
 static DWORD XOR[]={0x7F0000L,0x1F1F1FL,0x3F3F00L};	// gelb,grau,rot
 for (int i=0; i<elemof(EditBrushes); i++) {
  EditBrushes[i]=CreateSolidBrush(GetSysColor(COLOR_WINDOW)^XOR[i]);
 }
}
void DeleteEditBrushes() {
 for (int i=0; i<elemof(EditBrushes); i++) DeleteBrush(EditBrushes[i]);
}
/*
static void LoadWinPos(void) {
 WINDOWPLACEMENT wp;
 TCHAR buf[32];
 InitStruct(&wp,sizeof(wp));
 GetWindowPlacement(MainWnd,&wp);
 GetString(S_Position,T("Oszi"),sLeer,buf,elemof(buf));
 if (_stscanf(buf,T("%d,%d,%d,%d"),
   &wp.rcNormalPosition.left,
   &wp.rcNormalPosition.top,
   &wp.rcNormalPosition.right,
   &wp.rcNormalPosition.bottom)!=4) return;
 MoveRectIntoFullScreen(&wp.rcNormalPosition);
 SetWindowPlacement(MainWnd,&wp);
}

static void SaveWinPos(void) {
 WINDOWPLACEMENT wp;
 TCHAR buf[32];
 InitStruct(&wp,sizeof(wp));
 GetWindowPlacement(MainWnd,&wp);
 wvsprintf(buf,T("%d,%d,%d,%d"),(va_list)&wp.rcNormalPosition);
 WriteString(S_Position,T("Oszi"),buf);
}
*/
UINT Bilder;
enum RUNNING {T_STOP,T_WAIT,T_AUTO} Running;

void ZeigBildFrequenz() {	// wird aller 1 Sekunde aufgerufen
 TCHAR buf[64];
 static const TCHAR sRunning[]=T(
   "%s - angehalten\0%s - wartet auf Trigger mit %d Hz\0%s - Bildfrequenz: %d Hz\0");
 _sntprintf(buf,elemof(buf),Index2String(sRunning,Running),WindowTitle,Bilder);
 SetWindowText(MainWnd,buf);
 Bilder=0;
}

void SetStopRun(RUNNING neu) {
 if (neu==Running) return;
 if (Running==T_WAIT && trig->modus==TRIGG::TM_SINGLE) {
  FlashWindow(MainWnd,TRUE);	// Trigger-Blitz
  FlashWindow(MainWnd,FALSE);
 }
 Running=neu;
 if (trig && trig->anzeige && trig->anzeige->knopf[0]) {
  trig->anzeige->knopf[0]->bild=neu?MYBUTTON::PAUSE:MYBUTTON::PLAY;
  trig->anzeige->knopf[0]->Inval();
 }
 quelle->RelayMsg(Running ? Q_ARM : Q_UNARM, NULL);
 ZeigBildFrequenz();
}

void ToggleStopRun(void) {
 SetStopRun(Running?T_STOP:T_WAIT);	// nicht korrekt wenn eh' ohne Trigger!
}

bool Idle(void) {
 if (!quelle) return true;
 if (!Running) return true;
 if (!trig) return true;
// if (!quelle->buffer) return true;
// quelle->state&=~Q_FULL;
// Sofern keine alten Daten (also alles "neu"), alles auf alt setzen lassen
// und Trigger suchen mittels dwFlags=0 (InQueue-Bit löschen)
 if (WaveHdr.dwUser==WaveHdr.dwBufferLength) {
  WaveHdr.dwFlags=0;		// Neu initialisieren
  WaveHdr.dwBytesRecorded=0;	// Null neue Daten
  WaveHdr.dwUser=0;		// Alles alte Daten (PROBLEM MIT PRÄTRIGGER-DATEN!!!)
// Software-Trigger initialisieren
  if (st.pre>(long)WaveHdr.dwBufferLength) st.pre=WaveHdr.dwBufferLength;
  WaveHdr.reserved=st.pre;		// begrenzen!
  st.Reset();
  if (trig->modus==TRIGG::TM_AUTO) SetTimer(MainWnd,3,500,NULL);
 }
 if (!FindTrigger(/*TimeOut:100*/))	// keine neuen Daten?
   return WaveHdr.dwFlags&WHDR_PREPARED ? false : true;
// Sobald Trigger entdeckt, ggf. Fenstertitel umändern
 if (WaveHdr.dwBytesRecorded>WaveHdr.reserved) SetStopRun(T_AUTO);
// Sofern SINGLE, wenn Puffer voll, Aufnahme abschalten
 if (trig->modus==TRIGG::TM_SINGLE
   && WaveHdr.dwBytesRecorded==WaveHdr.dwBufferLength) SetStopRun(T_STOP);
 for (int i=0; i<numkanal; i++) kanal[i].CalcGraf();
 Inval(false);
 return false;
}
/*
void KANAL::SetAnzeigen(BYTE and, BYTE xor) {
// if (masse) masse->SetState(masse->state&and^xor);
// if (::trig && ::trig->k==this && ::trig->kreuz)
//   ::trig->kreuz->SetState(::trig->kreuz->state&and^xor);
 if (anzeige) anzeige->SetState(and,xor);
}
*/
int aktkanal;	// aktiver Kanal für Tastatureingabe
KANAL *aktk;
int aktzeit;	// aktive Zeitbasis für Tastatureingabe
ZEIT *aktz;
void SetAktKanal(int a) {
 if (a<0) a=numkanal-1;
 else if (a>=numkanal) a=0;
 aktkanal=a;
 if (aktk && aktk->anzeige) aktk->anzeige->SetState(~STA_SELECTED,0);
 aktk=kanal+a;
 if (aktk && aktk->anzeige) aktk->anzeige->SetState(~STA_SELECTED,STA_SELECTED);
}
void SetAktZeit(int a) {
 if (a<0) a=NumZeitMenu-1;
 else if (a>=NumZeitMenu) a=0;	// Ungültiges <a> erzeugt fehlende Zeitbasen - oder?
 aktzeit=a;	// Zurzeit nur eine Zeitbasis!
 if (aktz && aktz->anzeige) aktz->anzeige->SetState(~STA_SELECTED,0);
 aktz=zeit+a;
 if (aktz && aktz->anzeige) aktz->anzeige->SetState(~STA_SELECTED,STA_SELECTED);
}

void SetDatenquelle(EDatenquelle neu) {
 SetStopRun(T_STOP);
 if (Datenquelle==neu) return;
 if (Datenquelle>0) {	// falls bereits initialisiert
  CheckMenuItem(MainMenu,120+Datenquelle-1,MF_UNCHECKED);
 }
 if (trig) {trig->Destruktor(); delete trig; trig=0;}
 if (quelle) {
  if (numkanal) quelle->RelayMsg(Q_DONE,0);	// macht selber "unarm"
  delete quelle; quelle=NULL;
 }
 if (kanal) {
  for (int i=numkanal-1; i>=0; i--) kanal[i].Destruktor();
  delete[]kanal; kanal=NULL;
 }
 aktk=NULL;
 if (gruppe) {delete[]gruppe; gruppe=NULL;}	// Gruppen sind "dumm"!
 if (zeit) {zeit->Destruktor(); delete zeit; zeit=0;}
 aktz=NULL;
 if (WaveHdr.lpData) { GlobalFree(WaveHdr.lpData); WaveHdr.lpData=NULL;}
 WaveHdr.lpNext=NULL;
 numkanal=0;
 Datenquelle=neu;
 if (!Datenquelle) return;
 CheckMenuItem(MainMenu,120+Datenquelle-1,MF_CHECKED);
 switch (Datenquelle) {
  case ZUFALL: quelle=new Q_ZUFALL; break;
  case DSO220: quelle=new Q_DSO220; break;
  case SOUND:  quelle=new Q_SOUND;  break;
 }
 if (!quelle->RelayMsg(Q_INIT,0)) return;
 SYSINFO si;
 quelle->RelayMsg(Q_GETSYSINFO,&si);
 numkanal=si.numchan;
 if (!numkanal) return;
 GetSample=si.getsample;	// die wichtigsten Daten herauskopieren
 BlockAlign=si.blockalign;
 FullScale=(float)(1L<<si.bits);
 zeit=new ZEIT;		// Wie zum Array machen??
 zeit->Konstruktor(0);
 WaveHdr.dwBufferLength=1000L*BlockAlign;
 WaveHdr.lpData=(LPSTR)GlobalAlloc(GMEM_FIXED,WaveHdr.dwBufferLength);
 WaveHdr.dwBytesRecorded=0;		// keine neuen Daten
 WaveHdr.dwUser=WaveHdr.dwBufferLength;	// keine alten Daten
 WaveHdr.dwFlags=0;			// keine Statusbits
 kanal=new KANAL[numkanal];
 for (int i=0; i<numkanal; i++) kanal[i].Konstruktor(i);
 gruppe=new GRUPPE[numkanal];
 MacheGruppenListe();
 trig=new TRIGG;
 trig->Konstruktor(0);
 SetAktKanal(aktkanal);		// Zeiger neu setzen
 SetAktZeit(aktzeit);
 SetStopRun(T_WAIT);
}

BYTE HandleKeyDownUp(BYTE down, WPARAM key) {
// Zentrale Verwaltung der Hotkeys, die zur besseren Visualisierung die
// Anzeige-Schaltflächen mit betätigen. Liefert Flags: MK_SHIFT und MK_CONTROL
 BYTE mk=0;
 ANZEIGE *a=NULL;
 int idx=0;
 if (GetKeyState(VK_SHIFT)<0)  mk|=MK_SHIFT;
 if (GetKeyState(VK_CONTROL)<0)mk|=MK_CONTROL;
 switch (key) {
  case VK_SHIFT: {	// Massesymbol hervorheben
   if (!(mk&MK_CONTROL) && aktk && aktk->masse)
     aktk->masse->SetState((BYTE)~STA_SELECTED,(BYTE)(down?STA_SELECTED:0));
  }break;
  case VK_CONTROL: {	// Triggersymbol hervorheben
   if (trig && trig->kreuz)
     trig->kreuz->SetState((BYTE)~STA_SELECTED,(BYTE)(down?STA_SELECTED:0));
  }break;
  case VK_TAB: {
   if (down) SetAktKanal(aktkanal+(mk&MK_SHIFT?-1:+1));
  }break;
  case VK_APPS:
  case VK_SPACE: {
   if (mk&MK_CONTROL) {
    if (down && trig && trig->kreuz) ShowPopupMenu(trig->submenu,trig->kreuz->mitte.x,trig->kreuz->mitte.y);
    break;
   }
   if (mk&MK_SHIFT) {
    if (down && aktk && aktk->masse) ShowPopupMenu(aktk->submenu,aktk->masse->mitte.x,aktk->masse->mitte.y);
    break;
   }
   if (key==VK_APPS) break;
  }nobreak;
  case VK_PAUSE: {
   if (trig) a=trig->anzeige;	// Wenn PortTalk fehlt, gibt's keinen Trigger
  }break;
  case VK_LEFT: goto A;
  case VK_RIGHT: idx=1; A:{
   if (mk&MK_CONTROL) break;
   a=aktz->anzeige;
  }break;
  case VK_SUBTRACT:
  case VK_DOWN: goto B;
  case VK_ADD:
  case VK_UP: idx=1; B:{
   if (mk&MK_CONTROL) break;
   if (aktk) a=aktk->anzeige;
  }break;
 }
 if (a) {			// Anzeige existiert
  MYBUTTON *b=a->knopf[idx];
  if (b && !(b->state&2)) {	// Knopf existiert, und nicht disabled
   b->SetState(down);
   UpdateWindow(MainWnd);	// rcitem-Aktualisierung vorziehen
  }
 }
 return mk;
}

static void LoadConfig(void) {
 TCHAR buf[16];
// Globales
 GetString(HauptSektion,T("KanalFormat"),T("Y%d,1"),buf,elemof(buf));
 _stscanf(buf,T("%7[^,],%d"),sKanalFormat,&iKanalStart); sKanalFormat[7]=0;
 GetStruct(HauptSektion,T("Farben"),CustColors,sizeof(CustColors),StdProfile);
 GetString(HauptSektion,T("Mikro"),T("µ"),sMikro,elemof(sMikro));
 ISO_Prefixes[4]=*sMikro;
 GetString(HauptSektion,T("Dezimaltrenner"),T("?"),sDezimal,elemof(sDezimal));
 GetString(HauptSektion,T("Hintergrund"),T("#FFFFFF"),buf,elemof(buf));
 String2Farbe(buf,BackColor);
 GetString(HauptSektion,T("Gitternetz"),T("#008000"),buf,elemof(buf));
 String2Farbe(buf,GridColor);
 MaxKanalMenu=GetInt(HauptSektion,T("MaxKanalMenü"),16);
 if (MaxKanalMenu<4) MaxKanalMenu=4;
 if (GetString(HauptSektion,T("Kästel"),sLeer,buf,elemof(buf)))
   ScanXY(buf,&Kaestel);
 if (GetString(HauptSektion,T("SubTick"),sLeer,buf,elemof(buf)))
   ScanXY(buf,&SubTick);
 if (GetString(HauptSektion,T("Vorsätze"),sLeer,buf,elemof(buf)))
   ScanPr(buf,wPrefix);
// Hintergrund
 SetDispOpt(GetInt(HauptSektion,T("Anzeige"),
   DO_GRID|DO_DB|DO_LINE|DO_TICK|DO_CROSS|DO_BACK),0);
 gitter=new GITTER;	// Konstruktor wird gerufen
// Trigger
// Kanäle
 SetDatenquelle((EDatenquelle)GetInt(HauptSektion,T("Datenquelle"),1));
}
static void SaveConfig(void) {
 TCHAR buf[16];
 _sntprintf(buf,elemof(buf),sInt,Datenquelle);
 WriteString(HauptSektion,T("Datenquelle"),buf);
 WriteStruct(HauptSektion,T("Farben"),CustColors,sizeof(CustColors),StdProfile);
 WriteString(HauptSektion,T("Mikro"),sMikro);
 WriteString(HauptSektion,T("Dezimaltrenner"),sDezimal);
 _sntprintf(buf,elemof(buf),T("%s,%d"),sKanalFormat,iKanalStart);
 WriteString(HauptSektion,T("KanalFormat"),buf);
 Farbe2String(buf,BackColor);
 WriteString(HauptSektion,T("Hintergrund"),buf);
 Farbe2String(buf,GridColor);
 WriteString(HauptSektion,T("Gitternetz"),buf);
 _sntprintf(buf,elemof(buf),T("%u"),DispOpt);
 WriteString(HauptSektion,T("Anzeige"),buf);
 _sntprintf(buf,elemof(buf),sInt,MaxKanalMenu);
 WriteString(HauptSektion,T("MaxKanalMenü"),buf);
 PrintXY(buf,&Kaestel); WriteString(HauptSektion,T("Kästel"),buf);
 PrintXY(buf,&SubTick); WriteString(HauptSektion,T("SubTick"),buf);
 PrintPr(buf, wPrefix); WriteString(HauptSektion,T("Vorsätze"),buf);
}

void SendAlleNachricht(UINT Msg, WPARAM wParam, LPARAM lParam) {
 if (Anker[0].sub) Anker[0].sub->RelayMsg(Msg,wParam,lParam);
 if (Anker[1].sub) Anker[1].sub->RelayMsg(Msg,wParam,lParam);
 if (Anker[2].sub) Anker[2].sub->RelayMsg(Msg,wParam,lParam);
}

static MINIWND *HoverWnd;	// Fenster, das Mausnachrichten erhält
static WPARAM ButtonState;	// Momentaner Maustastenstatus MK_xxx

void SendMausNachricht(UINT Msg, WPARAM wParam, LPARAM lParam) {
 if (HoverWnd) HoverWnd->RelayMsg(Msg,wParam,lParam);
}

void CheckHover(LPARAM lParam) {
 if (ButtonState) return;// HOVER nicht bei gedrückter Maustaste wechseln
 MINIWND *p=MINIWND::MiniWndFromPoint(MAKEPOINTS(lParam));
 if (p==HoverWnd) return;
 SendMausNachricht(WM_MOUSELEAVE,0,lParam);	// Tschüss sagen
 HoverWnd=p;
 SendMausNachricht(WM_MOUSEHOVER,0,lParam);	// Hallo sagen
}

void MausNachricht(UINT Msg, WPARAM wParam, LPARAM lParam) {
// Maus-Verarbeitung, Eintritts- und Austritts-Feststellung
 switch (Msg) {
  case WM_NCHITTEST:
  case WM_MOUSEWHEEL: {	// Diese beiden kommen in Bildschirmkoordinaten
   ScreenToClientS(MainWnd,(POINTS*)&lParam);
  }break;
  case WM_MOUSEMOVE: {
   if (wParam&(MK_LBUTTON|MK_RBUTTON|MK_MBUTTON))
     SetCapture(MainWnd);	// Hier System-Mausfang setzen
   else ReleaseCapture();
  }break;
  case WM_SETCURSOR: SendMausNachricht(Msg,wParam,lParam); return;
  default: if (Msg<WM_MOUSEFIRST) return;
	   if (Msg>WM_MOUSELAST)  return;
 }
 WPARAM bsc=ButtonState;	// um nicht 2x CheckHover aufrufen zu müssen...
 CheckHover(lParam);		// mein lParam=POINTS!
 if (WM_MOUSEFIRST<=Msg && Msg<=WM_MOUSELAST)
   ButtonState=wParam&(MK_LBUTTON|MK_RBUTTON|MK_MBUTTON);
 bsc^=ButtonState;		// Änderungs-Bits gesetzt
 SendMausNachricht(Msg,wParam,lParam);
 if (bsc) CheckHover(lParam);	// alles in Client-Koordinaten!
}

LRESULT CALLBACK MainWndProc(HWND Wnd, UINT Msg, WPARAM wParam, LPARAM lParam){
#if 0
 if (WM_MOUSEFIRST<=Msg && Msg<=WM_MOUSELAST && Tooltip) {
  MSG msg;
  msg.hwnd=Wnd;
  msg.message=Msg;
  msg.wParam=wParam;
  msg.lParam=lParam;
  SendMessage(Tooltip,TTM_RELAYEVENT,0,(LPARAM)&msg);
 }
#endif
 MausNachricht(Msg,wParam,lParam);
 switch (Msg) {
  case WM_CREATE: {
   MainWnd=Wnd;
   MainMenu=GetMenu(Wnd);
//   LoadWinBitmaps();
   atom=GlobalAddAtom(T("Oszi-Edit-BackColor"));
   CreateEditBrushes();
   SendMessage(Wnd,WM_WININICHANGE,0,0);
#if 0
   Tooltip=CreateWindow(TOOLTIPS_CLASS,NULL,TTS_ALWAYSTIP,0,0,0,0,
     0,0,HInstance,NULL);
#endif
   if (waveInGetNumDevs()) EnableMenuItem(MainMenu,122,MF_ENABLED);
   // eigentlich ein Fall für ConfigChanged... oder?
   PostMessage(Wnd,WM_USER+10,LoadWinPos(Wnd,T("Oszi")),0);
   OsziMenu=GetSubMenu(MainMenu,2);
   tip=new TOOLTIP;
   LoadConfig();
   if (LoadWinPos(0,T("Anzeige"))) PostMessage(Wnd,WM_COMMAND,112,0);
   if (LoadWinPos(0,T("Grundlage"))) PostMessage(Wnd,WM_COMMAND,112,0);
   if (LoadWinPos(0,T("ZeitDlg"))) StartDlg(hZeitDlg,IDC_XABLENK,
     ZeitDlgProc,MAKELONG(MAKEWORD(0,1),0x0303));
   if (LoadWinPos(0,T("KanalDlg"))) StartDlg(hKanalDlg,IDC_YABL,
     KanalDlgProc,MAKELONG(MAKEWORD(0,1),0xFFFF));
//   SetTimer(Wnd,1,20,NULL);
   SetTimer(Wnd,2,1000,NULL);	// zur Berechnung der Bilder pro Sekunde
//   static TRACKMOUSEEVENT tme={sizeof(tme),TME_HOVER|TME_LEAVE,0,HOVER_DEFAULT};
//   tme.hwndTrack=Wnd;
//   _TrackMouseEvent(&tme);
   wm_helpmsg=RegisterWindowMessage(HELPMSGSTRING);
  }break;

  case WM_USER+10: {
   if (wParam) ShowWindow(Wnd,(int)wParam);	// macht in WM_CREATE Murks!!
  }break;

  case WM_PAINT:{
   PAINTSTRUCT ps;
   BeginPaint(Wnd,&ps);
   if (DispOpt&DO_DB) {
    if (DispOpt&DO_DB_INVAL) BltAlles(bltdc[0]);
    BitBlt(ps.hdc,0,0,ClientExt.x,ClientExt.y,bltdc[0],0,0,SRCCOPY);
   }else{
    BltAlles(ps.hdc);
   }
   EndPaint(Wnd,&ps);
   Bilder++;
  }return 0;

  case WM_TIMER: {
   lParam=GetMessagePos();
   ScreenToClientS(Wnd,(LPPOINTS)&lParam);
   if ((LOWORD(lParam)>=(unsigned)ClientExt.x || HIWORD(lParam)>=(unsigned)ClientExt.y)
     && KillTimer(Wnd,222)) tip->SetParent(Anker+2);
//   SendMessage(Wnd,WM_MOUSEMOVE,ButtonState,lParam);
//DAMIT LÄSST SICH KEIN MENÜ BEDIENEN!
//   MausNachricht(WM_MOUSEMOVE,ButtonState,GetMessagePos());
   switch (wParam) {
    case 1: Idle(); break;
    case 2: ZeigBildFrequenz(); break;
    case 3: {
     KillTimer(Wnd,wParam);
     WaveHdr.dwFlags|=WHDR_ENDLOOP;	// ausbrechen lassen
     if ((long)WaveHdr.reserved<0) WaveHdr.reserved=0;
    }break;
   }
   SendAlleNachricht(Msg,wParam,lParam);
  }return 0;	// besser: gerichtet mittels wParam als MINIWND-Zeiger

  case WM_NCMOUSEMOVE: {
   if (KillTimer(Wnd,222)) tip->SetParent(Anker+2);
  }break;

  case WM_MOUSEMOVE:{
   HDC dc=GetDC(Wnd);
   if (DispOpt&DO_DB) Fadenkreuz();
   Fadenkreuz(dc);
   lastmouse.x=GET_X_LPARAM(lParam);
   lastmouse.y=GET_Y_LPARAM(lParam);
   Fadenkreuz(dc);
   if (DispOpt&DO_DB) Fadenkreuz();
   ReleaseDC(Wnd,dc);
  }return 0;

//  case WM_NCHITTEST: _asm int 3; break;

  case WM_SIZE: {
   wmSize((short)LOWORD(lParam),(short)HIWORD(lParam));
   SetDispOpt(DispOpt,DO_DB|DO_TB); // (beide) Puffer neu allokieren lassen!
   for (int i=0; i<numkanal; i++) kanal[i].CalcGraf();
   Inval(false);
   SendAlleNachricht(Msg,wParam,lParam);
  }return 0;

  case WM_WININICHANGE: {
   GetProfileString(T("intl"),T("sDecimal"),T("."),sDecimal,elemof(sDecimal));
   Inval(true);
  }return 0;

  case WM_SYSCOLORCHANGE: {
   DeleteEditBrushes();
   CreateEditBrushes();
   SendAlleNachricht(Msg,wParam,lParam);
  }return 0;

  case WM_SYSKEYDOWN: switch (wParam) {
   case VK_RETURN: {
    ShowWindow(Wnd,IsZoomed(Wnd)?SW_SHOWNORMAL:SW_MAXIMIZE);
   }break;
   case 'X': SendMessage(Wnd,WM_CLOSE,0,0); break;
  }break;
  case WM_KEYDOWN: {
   BYTE mk=HandleKeyDownUp(1,wParam);
   if (mk&MK_CONTROL) switch (wParam) {
    case VK_UP:    trig->SetPegel(T("+?")); break;
    case VK_DOWN:  trig->SetPegel(T("-?")); break;
    case VK_LEFT:  trig->SetPretrig(T("-?")); break;
    case VK_RIGHT: trig->SetPretrig(T("+?")); break;
   }else switch (wParam) {
    case VK_SPACE:
    case VK_PAUSE: {
     if (HIBYTE(HIWORD(lParam))&0x40) break;	// ohne Autorepeat!
     ToggleStopRun();
    }break;
    case VK_ADD:
    case VK_UP:    if (mk&MK_SHIFT) aktk->SetNulllinie(T("++"));
		   else aktk->SetAblenkung(T("--")); break;
    case VK_SUBTRACT:
    case VK_DOWN:  if (mk&MK_SHIFT) aktk->SetNulllinie(T("--"));
		   else aktk->SetAblenkung(T("++")); break;
    case VK_LEFT:  aktz->SetAblenkung(T("+?")); break;
    case VK_RIGHT: aktz->SetAblenkung(T("-?")); break;
   }
  }break;
  case WM_KEYUP: HandleKeyDownUp(0,wParam); break;
  case WM_CHAR: switch ((TCHAR)wParam) {
   case '0':
   case '1':
   case '2':
   case '3':
   case '4':
   case '5':
   case '6':
   case '7':
   case '8':
   case '9': SetAktKanal(((TCHAR)wParam-'0'-iKanalStart+10)%10); break;
   case 'a':
   case 'b':
   case 'c':
   case 'd': SetAktZeit((TCHAR)wParam-'a'); break;
   case '=': aktk->SetKopplung((UINT)0); break;
   case '~': aktk->SetKopplung(1); break;
   case '_': aktk->SetKopplung(2); break;
   case '!': aktk->SetTastkopf(aktk->GetTastkopfIndex()?T("1:1"):T("10:1")); break;
   case '/': aktk->SetAblenkung(-aktk->div); break;
  }return 0;

  case WM_COMMAND: {
   UINT id=wParam&0x3FF;	// Menü-ID (10 bit)
   UINT kn=LOWORD(wParam)>>10;	// Kanalnummer (6 bit)
   KANAL *k=::kanal+kn;
   ZEIT  *z=::zeit +kn;
   UINT j;
   if ((j=id-IDC_XABLENK)<elemof(Reihe)) {
    z->SetAblenkung(Reihe[j]);
   }else if ((j=id-IDC_RATE)<elemof(Reihe)) {
    z->SetRate(Reihe[j]);
   }else if ((j=id-IDC_YABL)<elemof(Reihe)) {
    k->SetAblenkung(Reihe[j]);
   }else switch (id) { // <kn> enthält Kanalnummer
    case IDC_XABLENK+elemof(Reihe): StartDlg(hZeitDlg,IDC_XABLENK,ZeitDlgProc,
      MAKELONG(MAKEWORD(kn,1),0x0301)); break;
    case IDC_XABLENK+elemof(Reihe)+1: z->SetAblenkung(T("++")); break;
    case IDC_XABLENK+elemof(Reihe)+2: z->SetAblenkung(T("--")); break;
    case IDC_RATE+elemof(Reihe):
    case IDC_XANFANG: StartDlg(hZeitDlg,IDC_XABLENK,ZeitDlgProc,
      MAKELONG(MAKEWORD(kn,2),0x0302)); break;
    case IDC_XFARBE: z->FarbAuswahl(Wnd); break;
    case IDC_YVOR+0: k->SetTastkopf(T("1:1")); break;
    case IDC_YVOR+1: k->SetTastkopf(T("10:1")); break;
    case IDC_YVOR+2: StartKanalDlg(MAKELONG(MAKEWORD(kn,2),0xFF22U)); break;
    case IDC_YKOP+0:	// DC
    case IDC_YKOP+1:	// AC
    case IDC_YKOP+2: {	// GND
     k->SetKopplung(id-IDC_YKOP);
    }break;
    case IDC_YINV: k->SetAblenkung(-k->div); break;
    case IDC_YNAME: StartKanalDlg(MAKELONG(MAKEWORD(kn,1),0xFF21U)); break;
    case IDC_YFARBE: k->FarbAuswahl(Wnd); break;
    case IDC_YABL+elemof(Reihe):
     StartKanalDlg(MAKELONG(MAKEWORD(kn,3),0xFF24U)); break;
    case IDC_YABL+elemof(Reihe)+1: k->SetAblenkung(T("++")); break;
    case IDC_YABL+elemof(Reihe)+2: k->SetAblenkung(T("--")); break;
    case 109: SendMessage(Wnd,WM_CLOSE,0,0); break;
    case 110: DialogBox(HInstance,MAKEINTRESOURCE(110),Wnd,GrundDlgProc); break;
    case 112: StartDlg(hDisplayDlg,112,DisplayDlgProc,0); break;
    case 113: {
     DLGINFO di;
     di.parent=Wnd;
     di.kbHand=&hKBHand;
     quelle->RelayMsg(Q_SETUPDLG,&di);
    }break;
    case 120:
    case 121:
    case 122: SetDatenquelle(EDatenquelle(LOWORD(wParam)-120+1)); break;
    case IDC_TMODUS:	trig->SetModus((TRIGG::MODUS)kn); break;
    case IDC_TQUELLE:	trig->SetQuelle(kn); break;
    case IDC_TFLANKE:	trig->SetFlanke((TRIGG::FLANKE)kn); break;
    case IDC_TKOPPLUNG:	trig->SetKopplung((TRIGG::KOPPLUNG)kn); break;
    case IDC_TFARBE:	trig->FarbAuswahl(Wnd); break;
    case 0x229: ToggleStopRun(); break;
    case 191: WinHelp(Wnd,HelpFileName,HELP_INDEX,0); break;
    case 192: WinHelp(Wnd,HelpFileName,HELP_CONTEXT,192); break;
    case 199: DialogBox(HInstance,MAKEINTRESOURCE(199),Wnd,AboutDlgProc); break;
   }
  }return 0;

  case WM_CLOSE:
  case WM_ENDSESSION: {
   if (hDisplayDlg)SaveWinPos(hDisplayDlg,T("Anzeige"),true);
   if (hZeitDlg)   SaveWinPos(hZeitDlg,   T("ZeitDlg"),true);
   if (hKanalDlg)  SaveWinPos(hKanalDlg, T("KanalDlg"),true);
   SaveConfig();
   SaveWinPos(Wnd,T("Oszi"),true);
   WriteString(NULL,NULL,NULL);		// Flush
   WinHelp(Wnd,HelpFileName,HELP_QUIT,0);
  }break;

  case WM_DESTROY: {
   SetDatenquelle(EDatenquelle(0));
   SetDispOpt(0,0);
   DeleteEditBrushes();
   GlobalDeleteAtom(atom);
//   FreeWinBitmaps();
   KillTimer(Wnd,1);
   PostQuitMessage(0);
  }return 0;
 }
 if (Msg==wm_helpmsg) WinHelp(Wnd,HelpFileName,HELP_CONTEXT,88);
 return DefWindowProc(Wnd,Msg,wParam,lParam);
}

void CALLBACK WinMainCRTStartup() {
 static WNDCLASS wc={
   CS_DBLCLKS|CS_HREDRAW|CS_VREDRAW|CS_BYTEALIGNCLIENT,
   MainWndProc,0,0,0,0,0,0,MAKEINTRESOURCE(100),WndClassName};

 LoadString(HInstance,200,WindowTitle,elemof(WindowTitle));
 MBoxTitle=WindowTitle;	// Zeiger für <wutils> setzen

 HInstance=GetModuleHandle(NULL);
 {OSVERSIONINFO vi;
  InitStruct(&vi,sizeof(vi));
  if (!GetVersionEx(&vi)) {
#ifdef UNICODE
   CHAR buf[256];
   LoadStringA(HInstance,210,buf,elemof(buf));
   MessageBoxA(0,buf,NULL,MB_OK|MB_ICONSTOP);	// Zwang: ANSI-Version
#endif
   return;
  }
  if (vi.dwPlatformId==VER_PLATFORM_WIN32s)
    MBox(0,210,MB_OK|MB_ICONASTERISK);		// Empfehlung: 16bit
 }

 InitCommonControls();
 GetModuleFileName(HInstance,StdProfile,elemof(StdProfile));
 lstrcpy(GetFileNamePtr(StdProfile),(PTSTR)IniFileName);

 wc.hInstance=HInstance;
 wc.hIcon=LoadIcon(HInstance,MAKEINTRESOURCE(100));
 RegisterClass(&wc);

 CreateWindowEx(WS_EX_ACCEPTFILES,WndClassName,MBoxTitle,
   WS_VISIBLE|WS_OVERLAPPEDWINDOW,
   CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,
   0,0,HInstance,NULL);

 MSG msg;
 for(;;){
  while (PeekMessage(&msg,0,0,0,PM_REMOVE)) {
   if (msg.message==WM_QUIT) goto raus;
   if (hKBHand && IsDialogMessage(hKBHand,&msg)) continue;
   TranslateMessage(&msg);
   DispatchMessage(&msg);
  }
  if (Idle()) WaitMessage();
 }
raus:
#ifndef WIN32
 UnhookWindowsHookEx(MessageHook);
#endif
 ExitProcess((UINT)msg.wParam);
}

Detected encoding: ANSI (CP1252)4
Wrong umlauts? - Assume file is ANSI (CP1252) encoded