Hallo zusammen,
ich habe das Problem, daß der Callback auf eine Memberfunktion der
Dialogklasse nicht funktioniert, wenn der Callback-Aufruf in der vom
Timerhandler aufgerufenen Funktion steht. Der Aufruf an sich
funktioniert aber definitiv außerhalb des Timerhandlers und auch der
Timerhandler selbst wird nachweislich abgearbeitet !
Zum Code:
Aus der Dialogklasse "CTabDialog" wird mit der Methode
"CModulTime::StartModulTime" ein Timer gestartet, der dann zyklisch den
Callback "call()" auf die Member-Funktion der Dialogklasse machen soll.
fp ist ein Objekt der Klasse FunctionPointer aus der mbed
Microcontroller Library, hier schon mal verlinkt in:
Beitrag "Re: C++ member Methode als Callback-Funktion übergeben"
(Mit der Klasse sind auch Callbacks auf nicht-statische Memberfunktionen
möglich).
Ich nehme an, daß es irgendwie am Callback-Aufruf der statischen
Timerfunktion liegt, habe aber keinen Schimmer, was genau das Problem
ist. Das ganze kostet mich jetzt schon paar Stunden, ohne wirklich
weiter zu kommen. Ich verwende VC++2008.
Wenn vielleicht jemand einen sachdienlichen Hinweis liefern könnte, was
zu dem genannten Verhalten führt, wäre ich sehr dankbar!
MFG
Andy
1
//aus Dialogklasse CTabDialog aufgerufen zur Übergabe Funkt.Pointer+this und Starten des Timers
Da das Timer-Konzept in Windows keine vernünftige Möglichkeit bietet,
Benutzerdaten (im konkreten Fall ein Pointer auf das CModulTime-Objekt)
an das Timer-Callback (hier TimerFunc) zu übertragen, scheint es gängige
Praxis zu sein, dafür das Timer-Event (zweites Argument in SetTimer und
drittes Argument in der Callback-Funktion) zu missbrauchen.
Das ist zwar gruselig, passt aber ganz gut zu Microsoft, denn die Doku
dazu und Windows insgesamt sind nicht weniger gruselig. So wird der
Benutzer über die Bedeutung der vier Argumente der Callback-Funktion
völlig im Unklaren gelassen:
https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-timerproc
Anmerkungen: Das & vor TimerFunc in deinem Code ist überflüssig, da
TimerFunc (ohne Argumentliste) bereits ein Pointer ist. Evtl. kann auch
das (TIMERPROC) weggelassen werden, da TimerFunc ja bereits die passende
Signatur hat und deswegen kein Cast erforderlich sein sollte.
Disclaimer: Ich kann nicht für die Richtigkeit des oben Geschriebenen
garantieren, dass ich das als Windows-Laie auch nur zusammengegoogelt
habe ;-)
J. S. schrieb:> Benutzt der Timer Threads? Dann funktioniert es nicht weil du nicht GUI> Funktionen aus anderen Threads heraus aufrufen darfst.
Ja, das sollte auch noch geklärt werden. In der MS-Doku steht zwar
nichts zu diesem Thema, aber das muss nichts heißen ;-)
Hallo zusammen,
Danke für das Feedback!
Ich benutze hier keine Threads.
CModulTime ist auch keine MFC-Klasse, ich vermute, man spricht dann beim
"Timereinsatz" von der WIN-API, korrekt? Verglichen mit dem
Timerhandling innerhalb der MFC via WM_ONTIMER erscheint mir das alles
auch etwas kompliziert bzw. nicht gut dokumentiert...
Muß hier aber nochmal erwähnen, daß ich absolut kein Programmierexperte
bin.
Ich schaue mir verschiedene Codeschnipsel im Netz zu einem Thema an,
versuche die zu verstehen und dann in meinen Code einzubauen.
MFG
Andy
Der Hinweis von Yalu jedenfalls ist schon recht zutreffend.
Die Win32-API-Funktion SetTimer() kann für alle möglichen Dinge
verwendet werden, neben ihrer eigentlichen Aufgabe, Windows-Messages an
irgendwelche Fenster zu senden, kann sie auch eine Callbackfunktion
aufrufen.
Wenn diese Funktion eine Memberfunktion einer C++-Klasse ist, muss
diese Funktion als static deklariert werden, denn sie wird ohne
impliziten This-Pointer aufgerufen.
Damit man aus dieser statischen Memberfunktion heraus wiederum andere
Memberfunktionen der zugehörigen Klasse heraus aufrufen kann, muss ein
This-Pointer herbeigeschafft werden, der auf die zugehörige
Objektinstanz verweist.
Daher muss der Win32-API-Funktion der This-Pointer der Objektinstanz
als opaker Parameter übergeben werden, damit der Callbackmechanismus
beim Aufruf der Callbackfunktion diesen Parameter auch entsprechend
weiterreicht.
Wenn nicht mit Windows-Fensterfunktionen gearbeitet wird, ist diese
Funktion insgesamt eher sehr ungeeignet, um das Ziel zu erreichen, da
sind die Multimedia-Timer-Funktionen deutlich besser geeignet, da sie
außerhalb des Messagehandlings funktionieren.
Kern ist hier timeSetEvent() und hier insbesondere der Parameter dwUser
(der als der eben beschriebene opake Pointer missbraucht werden kann).
Dieser Parameter ist auch in der Funktionssignatur der verwendeten
Callbackfunktion (LPTIMECALLBACK) beschrieben.
Wenn aber die entsprechende Anwendung eine GUI-Oberfläche hat, mithin
also Fensterfunktionen und Messagehandler verwendet, ist es wenig
sinnvoll, mit Timer-Callbacks zu arbeiten. Da ist der seit über dreißig
Jahren etablierte Weg mit WM_TIMER der deutlich bessere.
Multimedia-Timer werden hier beschrieben:
https://learn.microsoft.com/en-us/windows/win32/multimedia/multimedia-timersAndy W. schrieb:> Ich schaue mir verschiedene Codeschnipsel im Netz zu einem Thema an,> versuche die zu verstehen und dann in meinen Code einzubauen.
Ich halte das für keinen zielführenden Weg, da diese Codeschnipsel oft
völlig unterschiedliche Programmierkonzepte vermischen und so nur
unnötige Verwirrung und Komplexität schaffen.
Die Memberfunktion muss nicht static sein, das wird ja von der fp Klasse
behandelt und die bekommt im Konstruktor auch die Instanz mit.
Im verlinkten Thread ist im letzten Post die Alternative mit der C++
Standard Lib gezeigt, das würde ich unter Windows eher verwenden. Aber
es wird nicht das Problem lösen.
J. S. schrieb:> Die Memberfunktion muss nicht static sein, das wird ja von der fp Klasse> behandelt und die bekommt im Konstruktor auch die Instanz mit.
Hier nicht, denn SetTimer() weiß nichts davon. Der wird ein
Funktionspointer übergeben, und der wird ohne this-Pointer aufgerufen,
und damit muss die hier verwendete Funktion static sein.
Was da im oben zu sehenden Codeschnipsel mit "fp" veranstaltet wird, hat
mit dem verwendeten Callback-Mechanismus nichts zu tun.
Wie ich schon schrieb, wenn man irgendwelche irgendwo im Internet
gefundenen Codefragmente zusammenkopiert, ist das seltenst zielführend.
Hat leider keine Änderung gebracht.
Dann ist mir noch aufgefallen, daß der Timerhandler
"TestCallBackToTabDgViaTimer" aufgerufen wird, egal welcher der 4
Parameter der "TimerFunc" gecastet wird. Steht in allen vieren der
gleiche Zahlenwert (=Adresse)?
Ich kann nachvollziehen, daß Ihr bei der Aufgabenstellung andere Wege
empfehlt, das umzusetzen.
Ich wollte es halt nur verstehen.
Wenn ich das nicht zum Laufen bekomme, muß ich eh versuchen, eine andere
Lösung zu finden.
MFG
Andy
Andy W. schrieb:> Steht in allen vieren der> gleiche Zahlenwert (=Adresse)?
Die Adresse von TestCallBackToTabDgViaTimer steht nicht im Objekt, auf
das der This-Pointer verweist.
Das wäre überflüssig; denn wenn Du zwei Instanzen der Klasse anlegst,
bleibt die Funktion, die aufgerufen wird, dieselbe, nur die Daten, die
zur Objektinstanz gehören (d.h. etwaige Membervariablen) unterscheiden
sich.
Dein Beispielcode knallt nur deswegen nicht, weil
TestCallBackToTabDgViaTimer den this-Pointer nicht derefenziert.
Ich nehme stark an, daß "fp" keine Membervariable ist, sondern irgendwo
außerhalb Deines Objektes existiert.
Harald K. schrieb:> Dein Beispielcode knallt nur deswegen nicht, weil> TestCallBackToTabDgViaTimer den this-Pointer nicht derefenziert.
Der Cast von unnamedParam3 löst eine Exception aus, die vom
Windows-System abgefangen und ignoriert wird.
Ich weiß nicht warum du meinst das unnamedParam1 bis 4 irgendwas
sinnvolles enthalten sollten.
Die Doku zu SetTimer sagt für deinen Fall, am besten so aufrufen:
SetTimer(NULL, 0, [elapseTime], [Timerproc]);
Timerproc muß die Signatur void Timerproc(HWND,UINT,UINT_PTR,DWORD)
haben, also ein static Member wäre ok.
Es wird angeraten, die Exceptionunterdrückung auszuschalten:
"UOI_TIMERPROC_EXCEPTION_SUPPRESSION flag to false through the
SetUserObjectInformationW function, otherwise the application could
behave unpredictably and could be vulnerable to security exploits. For
more info, see SetUserObjectInformationW."
Alles in
https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer
nachlesbar.
CModuleTime::TimerFunc muss aber schon static sein, sonst würde der
Compiler meckern. Und fp ist Instanz einer speziellen FunctionPointer
Klasse, kein nackter Pointer, also auch ok.
Wenn Beep() ausgeführt wird ist es ja soweit ok, nur das GUI mag es
nicht. Wird in den Debugausgaben etwas gemeldet?
Andy W. schrieb:> So, habe den Timeraufruf angepasst.> Hat leider keine Änderung gebracht.
Schade.
Andy W. schrieb:> void CModulTime::TestCallBackToTabDgViaTimer()> {> Beep(500,100);//###wird ausgeführt> fp.call(); //###funktioniert nicht! Warum?> }
Was heißt eigentlich "funktioniert nicht" genau? Wird eine Fehlermeldung
ausgegeben, hängt sich das Programm auf, oder sagt eine Stimme im
Hintergrund "Ätsch, funktioniert nicht"?
Wenn du nach fp.call() einen weiteren Beep()-Aufruf (am besten mit
anderer Frequenz) einfügst, wird dieser dann ausgeführt? Falls nicht,
bedeutet das, dass das Programm in fp.call() steckenbleibt.
> Dann ist mir noch aufgefallen, daß der Timerhandler> "TestCallBackToTabDgViaTimer" aufgerufen wird, egal welcher der 4> Parameter der "TimerFunc" gecastet wird. Steht in allen vieren der> gleiche Zahlenwert (=Adresse)?
Für den bloßen Aufruf von Beep() wird die Objektadresse gar nicht
benötigt, deswegen funktioniert es bis zu diesem Punkt auch mit einer
falschen Adresse.
Sie wird aber für fp.call() benötigt, falls fp eine nichtstatische
Member-Variable von CModulTime ist.
Du solltest den Wert von unnamedParam3 mal ausgeben. Wenn ich
die MS-Logik richtig verstanden habe, sollte er der Adresse des
CModulTime-Objekts entsprechen, die du als zweites Argument an
SetTimer() übergeben hast.
In deiner ursprünglichen Version hast du statt der Objektadresse den
Wert 123 an SetTimer() übergeben, dessen Dereferenzierung natürlich ein
unvorhersehbares Verhalten erklären würde. Deswegen dachte ich, dass
mein obiger Vorschlag eigentlich gar nicht so schlecht sein sollte ;-)
Das sind gute Fragen :-)
Yalu X. schrieb:> Was heißt eigentlich "funktioniert nicht" genau? Wird eine Fehlermeldung> ausgegeben, hängt sich das Programm auf, oder sagt eine Stimme im> Hintergrund "Ätsch, funktioniert nicht"?
Das Programm stürzt nicht ab, die Methode
CTabDialog::*CallBackFunctionTabDlg wird nur nicht aufgerufen (gebe dort
AfxMessageBox testweise aus)
Yalu X. schrieb:> Wenn du nach fp.call() einen weiteren Beep()-Aufruf (am besten mit> anderer Frequenz) einfügst, wird dieser dann ausgeführt? Falls nicht,> bedeutet das, dass das Programm in fp.call() steckenbleibt.
Genau das hatte ich schon gemacht, der zweite Beep() danach kommt nicht
mehr, d.h. das fp.Call() bleibt stecken.
Yalu X. schrieb:> In deiner ursprünglichen Version hast du statt der Objektadresse den> Wert 123 an SetTimer() übergeben
Das war mein Fehler, irgendwie hatte ich aus der MFC-Welt in Erinnerung,
daß dort eine eindeutige ID für den Timer übergeben werden muss.
Yalu X. schrieb:> Du solltest den Wert von unnamedParam3 mal ausgeben. Wenn ich> die MS-Logik richtig verstanden habe, sollte er der Adresse des> CModulTime-Objekts entsprechen, die du als zweites Argument an> SetTimer() übergeben hast.
this Zeiger an SetTimer 2.Argument übergeben 0x2f4ef10
die unnamedParam1 bis 4 in der Timerfunktion: 0, 0x113, 0x6ce9,
0x2a16729b
Die Adresse des this-Zeigers sehe ich nicht, Kommentar siehe unten.
J. S. schrieb:> Wird in den Debugausgaben etwas gemeldet?
Mit jeder Ausführung des Timerhandlers gibt es folgenden Fehler
Eine Ausnahme (erste Chance) bei 0x00b37776 in MyBus.exe: 0xC0000005:
Zugriffsverletzung beim Lesen an Position 0x00006cf1.
Offensichtlich enthält der 3.Parameter (bzw. alle 4) keine gültige
Adresse. Hatte jetzt eigentlich die Adresse des this-Zeigers erwartet...
Yalu X. schrieb:> SetTimer(NULL, reinterpret_cast<UINT_PTR>(this), 5000,> (TIMERPROC)TimerFunc);
Damit das so funktioniert wie gedacht, muss als erstes Argument ein
Window-Handle übergeben werden (Microsoft will es so):
https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer
Ich habe das unter Wine erfolgreich getestet (Code s. Anhang). Um nicht
die Mbed-Bibliothek herunterladen zu müssen, habe ich std::function
anstelle von mbed::FunctionPointer verwendet.
Hallo zusammen,
was für eine schwere Geburt - jetzt funktioniert es endlich!
Wird das gültige Handle im 1.Argument von SetTimer übergeben, bekomme
ich im 3 Parameter der Timerfunktion den this-Zeiger auch zurück. :-)
Zusammenfassend kann man sagen - es fehlten 2 Dinge:
Beim Aufruf von SetTimer
1) im ersten Parameter ein gültiges Handle des übergeordneten Dialogs
2) im zweiten Parameter der this-Zeiger der aufrufenden Klasse
Danke an alle, die sich bei der Problemlösung beteiligt haben, besonders
natürlich Yalu für das Nachvollziehen! Ich denke, sein Codebeispiel ist
sehr nützlich für jeden, der erstmalig so etwas umsetzen will/muss.
Euch einen schönen Start in die Woche!
MFG
Andy