![]() |
Languages »
C / C++ Language »
General
Intermediate
License: The Code Project Open License (CPOL)
Type-safe Signals and Slots in C++: Part 2By ElmueImplements a type-safe signal / slot or event / delegate system in C++ |
VC6, Windows, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
These classes add a highly comfortable signal / slot system to your projects.
void).Combine function must not be static anymore.This article is based on Part 1 (Type-safe C++ Callbacks), but it is not necessary to read Part 1 before reading this article. Signals and slots are callbacks with enhanced features. To use signals and slots, simply copy the files SignalSlot.h, Callback.h and PreProcessor.h to your project and #include "SignalSlot.h".
Every slot needs a function assigned to it that will be called back when an event (=signal) arrives.
class cWorker
{
public:
cSlot <cWorker, void, char*> m_Slot;
cWorker() // constructor
{
m_Slot.AssignFunction(this, OnSlotEvent);
}
private:
void OnSlotEvent(char* Text) // callback function
{
printf("cWorker::OnSlotEvent: %s", Text);
}
};
The first type (cWorker) given to cSlot <...> is the class that contains the callback function. The second type (void) is the return type of the callback function. Then follow the arguments for the callback function (char*). You can use callback functions taking 0, 1, 2, 3, 4 or 5 arguments. If you need more than 5 arguments, you have to expand the callback.h and SignalSlot.h files on your own. However, I recommend passing more than 5 arguments in a structure instead, so the code becomes more readable.
class cMaster
{
cWorker m_Worker;
cSignal <void, char*> m_Signal;
cMaster() // constructor
{
m_Signal.Connect(m_Worker.m_Slot);
}
};
The types given to cSignal <...> are the same as for cSlot <...> except that the first one (cWorker) is missing. It doesn't matter if you connect a signal to a slot or vice versa, so the following lines are identical:
m_Signal.Connect(m_Worker.m_Slot);
m_Worker.m_Slot.Connect(m_Signal);
If they are already connected, further calls to Connect() are ignored. As signals and slots are typesafe, you cannot connect them if the type differs. For example if you would try to connect the above signal of the type < with a slot of the type void, char*><, the result would be a compilation error.int, string, string, string, double, char>
The following line fires the signal:
m_Signal.Fire("Now firing....!");
The signal fires all connected slots and then every slot calls its callback function. The result would be the following output from printf():
cWorker::OnSlotEvent: Now firing....! |
I wrote my own calendar GUI control for my DesktopOrganizer PTBSync (Download) which looks like this:
The weekday names and month names change immediately when the user switches the system language. The colors of the control adapt immediately when the user modifies the Windows colors in Control Panel. To achieve this, I have to catch the Windows messages WM_SYSCOLORCHANGE and WM_SETTINGCHANGE.
However, if you wait inside the control to receive one of these messages, you will never see them because Windows only sends them to the top level windows. This means they will only arrive in the main window of the application (if you don't use MFC).
I use the signal / slot system to notify some controls (like the calendar control) from changes of system settings. The calendar control implements a slot to receive notifications. The corresponding signal is fired in the main window where the Windows messages arrive.
Now I hear you ask, "Why not simply forward the Windows messages to the Calendar control?"
Advantages of signal / slot solution:
MainWindow::WindowProc(). The new control simply calls MainWindow.SignalXYZ.Connect(MySlot) in its constructor. In the above example, the slots have the return type void. However, let's say your slots return an int and you want i_MySignal.Fire(...) to return the sum of the return values of all callback functions that have been called when firing the signal.
That's no problem! What you need is a Combine function, which sums the return values of the slots. For the above example, you would write:
static bool Add(int *Result, int NewValue)
{
(*Result) += NewValue;
return true;
}
MySignal.SetCombineFunction(MAKE_COMBINE_S(&MyClass::Add, int), 0);
The macro MAKE_COMBINE_S(Function, ReturnType) creates a callback to a static Combine function. For member functions, use the macro MAKE_COMBINE_M(Class, Instance, Function, ReturnType). The second argument of Add() is the result of the latest call to cSlot::Fire().
The first argument of Add() is a pointer to the current sum of the previous calls. The second argument to SetCombineFunction(), 0, is an initialization value which is passed with the very first call to Add(). Why the Combine function returns true will be explained later. In the above example, the calls to Add() would look like this:
| *Result (before) | NewValue | *Result (after) | Note |
| 0 | 3 | 3 | after calling the CWorker slot |
| 3 | 11 | 14 | after calling the CMyPort slot |
| 14 | 7 | 21 | after calling the CMyParser slot |
If your slots would return a string instead of int and you want i_Signal.Fire(..) to return the concatenation of all these strings separated by comma, you could write:
static bool Concat(string *Result, string NewValue)
{
if (Result->length()) Result->append(", ");
Result->append(NewValue);
return true;
}
MySignal.SetCombineFunction(MAKE_COMBINE_S(&MyClass::Concat, string),
"Returned from MySignal: ");
.....
string Result = MySignal.Fire(..);
You are absolutely free in writing your specialized Combine function. For example, you could return a bitmask from the slots and OR the bits together in the Combine function to see which operations have been executed successfully. Alternatively, you could use enums to return a status code.
You can have slots of different priority. The ones with higher priority are called first. If one of them has handled the event successfully, it can stop further processing. You can specify in which order a signal calls the connected slots by the order of connecting them. If you write:
MySignal.Connect(Slot1);
MySignal.Connect(Slot2);
MySignal.Connect(Slot3);
MySignal.Fire() will call Slot1 first and then Slot2 and, finally, Slot3. But what if you want to connect a new slot later? You can pass an optional second argument to cSignal::Connect(cSlot &Slot, bool Append=true).
MySignal.Connect(NewSlot, true); // insert new slot at the end
MySignal.Connect(NewSlot, false); // insert new slot at the top
The Combine function offers a second feature: if your Combine function returns false, no more slots will be fired after the current one. This is useful if your slots are specialized to handle only a special type of event. Then the first slot that handled the event successfully can stop processing. For example, your slots return an int and you want no more slots to be called if the sum is greater than 50.
static bool Add(int *Result, int NewValue)
{
(*Result) += NewValue;
return (*Result <= 50);
}
In a very complex project, it is nearly sure that someday you will build a reentrant loop like the following:
Signals and slots are protected against reentrance. A call to cSignal::Fire() or cSlot::Fire() is ignored if it is currently processing a previous call. For this case, you can define a return value which is returned by Fire() on reentrance. This default value has to be passed to the constructor.
cSlot <cClass, tRet [,tArg1,,,tArg5]> Slot(tRet DefaultValue = (tRet) 0)
cSignal <tRet [,tArg1,,,tArg5]> Signal(tRet DefaultValue = (tRet) 0, ....)
If you don't specify a value for DefaultValue, it is set to zero. Attention: not all return types can be set to zero! For example, if tRet is string, the constructor tries to set string DefaultValue=(string)0 which is not possible. Avoid this by writing, for example:
cSlot <cMyClass, string, tArg1, tArg2...> MySlot("Error");
If this slot is called reentrant, Fire() will return a string with "Error." The return type void is a special case. Although the constructor sets void DefaultValue = (void) 0, this does not cause a compiler error because I use a lot of tricks in SignalSlot.h that I will not explain here.
For every signal or slot, you can set a timeout in which further events will be ignored.
Signal.SetMinInterval(2000, false)
This sets the timeout to 2 seconds. After firing the signal, there has to be a pause of at least 2 seconds until the signal can be fired again. In the meantime, Fire() will return the default Timeout value of false.
| Type | Slot | Signal | Set with | Usage |
Combine default |
- | X | SetCombineFunction() |
passed to Combine function with the very first call |
Reentrance default |
X | X | Constructor |
returned from Fire() on reentrance |
Timeout default |
X | X | SetMinInterval() |
returned from Fire() within timeout period |
In complex projects, you will lose the overview of which signal is connected to which slot. For that, cSignal and cSlot offer a debugging function:
bool GetConnectedList(char* Buf, int BufLen)
It outputs all the slots connected to a signal or all the signals connected to a slot by name. If the buffer is too small, GetConnectedList() returns false. To use this functionality, you have to give unique names to your slots and signals. You can name them with an optional argument to the constructor of the Signal and AssignFunction() of the Slot:
cSignal <tRet [,tArg1,,,tArg5]> Signal(tRet DefaultValue=0, char* Name="NoName")
cSlot::AssignFunction(Instance, Function, char* Name="NoName")
Then call GetConnectedList:
char Buffer[5000];
MainSignal.GetConnectedList(Buffer, sizeof(Buffer));
TRACE(Buffer);
This will output, for example:
Signal MainSignal is connected to: PrinterSlot, DataSlot, MessageSlot |
| cSlot | cSignal |
Basic Features |
|
Constructor:cSlot <cClass, tRet [,tArg1,,,tArg5]> Slot(tRet DefaultValue=0) |
Constructor:cSignal <tRet [,tArg1,,,tArg5]> Signal(tRet DefaultValue=0, char* Name="NoName") |
void AssignFunction(Instance, Function, char* Name="NoName") |
- |
void Connect(&Signal, bool Append=true) |
void Connect(&Slot, bool Append=true) |
void Disconnect(&Signal) |
void Disconnect(&Slot) |
void DisconnectAll() |
void DisconnectAll() |
tRet Fire(tArg1,,,tArg5) |
tRet Fire(tArg1,,,tArg5) |
Advanced Features |
|
void SetMinInterval(unsigned int Time, tRet DefaultValue) |
void SetMinInterval(unsigned int Time, tRet DefaultValue) |
| - | void SetCombineFunction(Function, tRet DefaultValue) |
For Debugging |
|
int GetConnectedCount() |
int GetConnectedCount() |
bool GetConnectedList(char* Buf, int BufLen) |
bool GetConnectedList(char* Buf, int BufLen) |
To see how all the stuff works together, download the sample code! It also demonstrates the usage of Timeout and the Combine function.
Boost and QT also offer signal / slot functionality (see Part 1 of the article series). However, the signal slot system by ElmueSoft described in this article has the great advantage that it is tiny, does not need any big libraries or compiler plugins and it is free! You will not want to miss it if you once got used to it!
P.S. From my homepage, you can download free C++ books in compiled HTML format.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 13 Feb 2008 Editor: Genevieve Sovereign |
Copyright 2004 by Elmue Everything else Copyright © CodeProject, 1999-2010 Web21 | Advertise on the Code Project |