在 Visual Component Framework 中使用委托






3.65/5 (11投票s)
本文介绍如何在 VCF 中使用委托。
- 从 SourceForge 下载 VCF 源代码 (tar.gz) - 10.5 MB
- 从 SourceForge 下载 VCF Windows 安装程序 - 37.2 MB
- 下载 demo1 - 208 KB
- 下载 demo2 - 741 KB
引言
本文将介绍 Visual Component Framework 事件处理和委托类的新变化。Visual Component Framework (VCF) 是一个现代 C++ 框架,旨在易于使用,并简化 Windows 编程,同时还具有跨平台设计。此外,该框架还因能将您的智商提高至少 10 分而获得全球认可。
VCF 中的委托
Visual Component Framework 使用一种可能被称为“委托”的模式来实现框架中的事件处理。任何进行过 .NET GUI 编程的人都熟悉委托的概念,但对于不熟悉的人,我将解释它们在 VCF 中的工作方式。VCF 将委托定义为一个特殊的类,它充当一种函数指针,并且可以挂接多个回调或函数指针。因此,当调用委托时,它会将参数传递给每个回调,并调用每一个回调。换句话说,它类似于 .NET 中的多播委托。
CodeProject 上有许多文章展示了如何在 C++ 中创建此类对象,有些仅是单播委托(即一个委托,一个回调),其他则实现了多播功能(即一个委托,多个回调)。其他文章无疑比我的更复杂;然而,我对 VCF 有一些其他需求,因此我认为有必要自己实现一个解决方案。其中一个需求是能够通过名称存储回调并引用它们。这样做是为了您可以在运行时获取现有的回调并将其连接到委托。这完全可以通过 VCF 的内省功能动态完成。另一个独特的功能是,此处提供的委托可以异步调用,委托的回调将在线程池中执行。我们将在文章后面介绍这一点。
委托基础
使用委托并不难,但它需要使用模板。这是一个例子
void MyFunction( int num ) { System::println( Format("The num is %d") % num ); } void MyFunction2( int num ) { System::println( Format("MyFunction2 called, the num is %d") % num ); } Delegate1<int> myDelegate; myDelegate += MyFunction; myDelegate += MyFunction2; myDelegate(100); myDelegate.invoke( 200 );
您可以(显然)使用类成员函数指针。目前的限制是该类需要直接或间接继承自 VCF::Object
。将来,这个限制可能会被移除。
class MyClass : public Object { public: void myFunc( int num ) { System::println( Format("MyFunction2 called - this: %p, the num is %d") % this % num ); } }; MyClass obj; Delegate1<int> myDelegate; myDelegate += new ClassProcedure1<int,MyClass>(obj,&MyClass::myFunc); myDelegate(100);
添加回调时要注意的一点是:添加回调时,委托会尝试“验证”回调,以确保其函数签名与委托的函数签名匹配。我们之所以这样做,是因为回调可以在运行时动态添加,这是一种简单但有点粗糙的方法,可以确保在以后调用回调时不会出现完全的灾难。
到目前为止,一切顺利,这相当通用,而且一点也不令人费解。现在,让我们看看如何引用回调以供重用。为了实现这一点,我们需要让我们的类直接或间接继承自 ObjectWithCallbacks
。所以,我们最终会得到
class MyClass : public ObjectWithCallbacks { public: void myFunc( int num ) { System::println( Format("MyFunction2 called - this: %p, the num is %d") % this % num ); } };
我们的用法看起来类似
MyClass obj; Delegate1<int> myDelegate; myDelegate += new ClassProcedure1<int,MyClass>(obj, &MyClass::myFunc, "MyClass::myFunc");
请注意传递给 ClassProcedure1
的新参数,它是一个字符串,代表回调的名称。按照惯例,这是 C++ 限定的函数名,但也可以是任何您想要的名称。
我们可以像这样引用它
MyClass obj; Delegate1<int> d1; d1 += new ClassProcedure1<int,MyClass>(obj, &MyClass::myFunc, "MyClass::myFunc"); Delegate1<int> d2; d2 += obj.getCallback( "MyClass::myFunc" ); d1(100); d2(344);
调用 ObjectWithCallbacks::getCallback()
会返回一个有效回调实例(如果不存在具有该名称的对象,则返回 NULL
)。
在以上所有情况下,我们都创建了回调,而没有调用 delete。只要您已将回调添加到“源”类(一个继承自 ObjectWithCallbacks
的类)或已将回调添加到委托,框架就会为您处理回调。在这两种情况下,源类或委托将负责清理和删除回调。
委托的返回值
到目前为止,示例一直显示不返回值的委托。让我们看看有返回值的委托,例如
double someFunc( int x, int y ) { return (double)x / (double)y; } Delegate2R<double,int,int> myDelegate; myDelegate += someFunc;
这指定了一个返回 double 类型并接受两个 int 类型参数的委托。我们可以像以前一样调用它
double someFunc( int x, int y ) { return (double)x / (double)y; } Delegate2R<double,int,int> myDelegate; myDelegate += someFunc; double answer = myDelegate( 230, 345 );
结果显而易见。然而,问题出现了:我们如何处理多个回调?由于 VCF 中的所有委托都是多播委托,因此完全有可能我们会有
double divideIt( int x, int y ) { return (double)x / (double)y; } double multiplyIt( int x, int y ) { return (double)x * (double)y; } double powerOf( int x, int y ) { return pow((double)x , (double)y ); } Delegate2R<double,int,int> myDelegate; myDelegate += divideIt; myDelegate += multiplyIt; myDelegate += powerOf; double answer = myDelegate( 230, 345 );
现在,当我们调用委托时,有三个函数会被调用,但我们似乎只处理一个返回值!我们期望分别得到 0.666...7、79350.0 和 6.253215...e+814 的返回值。
别担心!我们有一个解决方案。返回值的委托(即任何名称为 DelegateRXXX
的委托类,其中 XXX 是它接受的参数数量)都有一个类型为 T
的成员变量,其中 T
是返回类型。因此,在我们上面的示例中,委托有一个成员 std::vector<double>
,它存储了回调的返回值,顺序与回调的调用顺序一致。当我们以我们使用的方式调用方法时,您将获得最后一个被调用回调的值。我们看到的结果应该等于 6.253215...e+814(或类似值),因为 powerOf()
函数将是最后一个被调用的回调。
如果我们想访问所有结果,我们可以这样做
double divideIt( int x, int y ) { return (double)x / (double)y; } double multiplyIt( int x, int y ) { return (double)x * (double)y; } double powerOf( int x, int y ) { return pow((double)x , (double)y ); } Delegate2R<double,int,int> myDelegate; myDelegate += divideIt; myDelegate += multiplyIt; myDelegate += powerOf; myDelegate( 230, 345 ); for (size_t i=0;i<myDelegate.results.size();i++ ) { double answer = myDelegate.results[i]; }
如果返回值是指针,那么指针的责任由函数文档决定。换句话说,委托根本不对指针类型执行任何管理。因此,如果一个函数返回一个类的实例并声明调用者负责管理该实例,那么通过委托进行调用时仍然是这种情况。
异步委托
您可以通过使用 beginInvoke()
方法异步调用委托。这将在一个单独的工作线程(属于框架管理的线程池的一部分)中调用回调。您可以通过回调获得通知(当所有回调都执行完毕时),或者您可以等待并阻塞直到它们全部完成。回调可能在一个线程中一个接一个地执行,或者单个回调本身可能被推送到线程池中的不同线程,所有这些都异步执行。
为了看到一个简单的例子,让我们看看我们之前的委托
void myFunc( int i ) { } Delegate1<int> myDelegate; myDelegate += myFunc; AsyncResult* result = myDelegate.beginInvoke( 100, NULL ); result->wait(); delete result;
这将导致 myFunc()
回调在线程池中的另一个线程中执行。beginInvoke()
方法立即返回,并返回一个新的 AsyncResult
实例。您可以无限期地等待结果,直到所有回调都完成。此时,委托调用完成,您可以删除结果实例。或者,您可以等待特定毫秒数,例如
AsyncResult* result = myDelegate.beginInvoke( 100, NULL ); while ( !result->isCompleted() ) { result->wait(1000); } delete result;
在这里,我们进行“忙碌轮询”,并检查结果是否完成。AsyncResult::isCompleted()
方法将在所有回调执行完毕后返回 true
。在调用此方法之间,我们等待一秒(1000 毫秒)。
另一种调用 beginInvoke
的方式是使用一个特定的回调,该回调在委托完成执行所有回调后调用。
void asyncDelegateDone( AsyncResult* asyncRes ) { delete asyncRes; } CallBack* asynCB = new AsyncCallback(asyncDelegateDone); myDelegate.beginInvoke( 100, asynCB );
当委托完成时,将调用 asyncDelegateDone()
回调。此时,您可以安全地删除 AsyncResult
实例(假设您在其他地方没有使用它)。请注意,AsyncCallback
由您(调用者)管理,因此您需要在完成后删除它。回调始终通过调用回调的 free()
方法来删除,如下所示
CallBack* asynCB = new AsyncCallback(asyncDelegateDone); myDelegate.beginInvoke( 100, asynCB ); //somewhere else in your code asynCB->free(); //destroys the calback.
如果您的委托返回值,那么您可能希望获取回调的结果。您可以通过调用委托上的 endInvoke()
来实现。这将返回最后一个被执行的回调的值。例如
double divideIt( int x, int y ) { return (double)x / (double)y; } double multiplyIt( int x, int y ) { return (double)x * (double)y; } double powerOf( int x, int y ) { return pow((double)x , (double)y ); } Delegate2R<double,int,int> myDelegate; myDelegate += divideIt; myDelegate += multiplyIt; myDelegate += powerOf; AsyncResult* result = myDelegate.beginInvoke( 301, 42, NULL ); result->wait(); double answer = myDelegate.endInvoke( result ); delete result;
您可以通过调用 endInvokeWithResults()
来访问所有返回值。这将返回一个已执行回调的返回值向量。
AsyncResult* result = myDelegate.beginInvoke( 301, 42, NULL ); result->wait(); std::vector<double> results = myDelegate.endInvokeWithResults( result ); for (size_t i=0;i<results.size();i++ ) { double answer = results[i]; } delete result;
如果您希望委托的每个回调都异步执行,则可以这样指定
myDelegate.setRunCallbacksAsynchronously( true ); AsyncResult* result = myDelegate.beginInvoke( 301, 42, NULL ); result->wait();
在前面的代码中,我们添加的三个函数 divideIt()
、multiplyIt()
和 powerOf()
是按照它们被添加到委托的顺序执行的。如果它们是异步运行的,那么执行顺序可能不是它们被添加到委托的顺序。此外,每个函数可能在线程池的不同线程中执行。
幕后花絮
那么,这一切究竟是如何运作的呢?基本思想是使用 VCF 中的 ThreadedFunction
类,它允许您指定一个包装函数的模板类,然后在其他线程的上下文中执行该函数,或者返回一个 Runnable
实例,您可以使用它在您选择的线程上执行。
当调用 beginInvoke()
时,委托首先创建一个 AsyncResult
实例。然后它遍历所有回调。这些回调反过来有一个被调用的虚函数,并创建各种 ThreadedFunction
实例。这些实例中的每一个都存储在 AsyncResult
上,作为将在线程池中运行的 Runnable
集合的一部分。当所有回调都被遍历完后,AsyncResult
将这些项添加到线程池以供执行。此时,beginInvoke()
返回。线程池将接管,并在某个时候,Runnable
实例将被运行,函数将被调用。这将异步发生,并在所有调用完成后完成。
当创建 ThreadedFunction
时,它所做的一件事就是将其参数复制到一个将保存它们的结构中,直到它们准备好使用。当实际函数调用发生时,它使用这些存储/缓存的值。出现的一个问题是如何处理由于其异步性而超出范围的函数参数。例如
//declared globally, or at some higher scope Delegate2R<double,int,int> myDelegate; { AsyncResult* ar = myDelegate.beginInvoke( 100, 100 ); }
在这种情况下,函数参数被指定为 int
,并且将按值复制,因此不会出现问题。当退出调用发生的范围时,堆栈上的局部 int
将消失,但由于进行了完整的按值复制,因此没有问题。现在,让我们做一些修改。
//declared globally, or at some higher scope Delegate2R<double,const int&,int> myDelegate; { AsyncResult* ar = myDelegate.beginInvoke( 100, 100 ); }
现在,我们遇到了一个问题。变量被指定为引用,并且在“复制”时将被保留为引用,因为“缓存”结构将其声明为 const int&
变量。由于我们离开了范围,传递进来的 100 将消失,并且在实际调用函数时我们很可能会得到一个损坏的值。如果我们尝试这样做
//declared globally, or at some higher scope Delegate2R<double,const int&,int> myDelegate; { int foo = 100; AsyncResult* ar = myDelegate.beginInvoke( foo,foo ); }
这样好一些。但是,局部变量“foo
”在函数(们)被调用之前仍然会超出范围。一个可能的解决方案是
//declared globally, or at some higher scope Delegate2R<double,const int&,int> myDelegate; { int foo = 100; AsyncResult* ar = myDelegate.beginInvoke( foo,foo ); ar->wait(); }
现在,beginInvoke()
返回,我们等待 AsyncResult
完成。局部变量在所有函数回调完成之前不会超出范围。另一个解决方案是将您要传递的变量声明在适当的范围内。如果正在执行的回调是类方法,那么您传递的参数可能是类的成员变量。
目前,函数参数*没有*进行任何封送处理。也许将来我会添加对此的支持,以帮助解决此类问题。
委托和 Visual Form 文件
虽然您可以以编程方式使用委托并将它们连接到回调,但您也可以在使用 VCF 的 Visual Form 文件格式设计表单时引用它们。例如,让我们创建一个带有某些 UI 元素的简单表单
MyWindow.vff
object MyWindow : VCF::Window
top = 200
left = 200
height = 110pt
width = 150pt
caption = 'A Window'
container = @hlContainer
object hlContainer : VCF::HorizontalLayoutContainer
numberOfColumns = 2
maxRowHeight = 35
rowSpacerHeight = 10
widths[0] = 80
widths[1] = 80
tweenWidths[0] = 10
end
object lbl1 : VCF::Label
caption = 'First Name'
end
object edt1 : VCF::Label
caption = 'Bob'
end
object lbl2 : VCF::Label
caption = 'Last Name'
end
object edt2 : VCF::TextControl
text = 'Bankmann'
end
object lbl3 : VCF::Label
caption = 'Age'
end
object edt3 : VCF::TextControl
readonly = true
enabled = false
text = '42'
end
object lbl4 : VCF::Label
caption = 'Modify Age'
end
object btn1 : VCF::CommandButton
caption = 'Click Me'
end
end
这会显示类似以下内容
VFF 格式相对简单(至少我认为如此),您可以在这里找到更详细的描述:Visual Form File Format。
我们希望在用户单击标有“Click Me”的命令按钮时执行一个回调方法。由于这是一个 CommandButton
类,它有一个用于按钮单击的委托,称为 ButtonClicked
。该委托接受一个参数,即一个 ButtonEvent
指针。让我们假设我们想将事件处理程序/回调添加到我们的应用程序类。该方法如下所示
class MyApp : public Application { public: //other code removed void clickMe( ButtonEvent* ) { } };
下一步是向我们的应用程序实例添加一个回调。稍后将在 VFF 代码中引用此回调,因此我们需要确保回调实例存在
class MyApp : public Application { public: //other code removed MyApp( int argc, char** argv ) : Application(argc, argv) { addCallback( new ClassProcedure1<ButtonEvent*,MyApp>(this, &MyApp::clickMe), "MyApp::clickMe" ); } void clickMe( ButtonEvent* ) { } };
现在,我们已经添加了事件处理程序,并且可以通过名称引用它。接下来,我们需要调整我们的 VFF,以便利用回调,将其连接到命令按钮的委托
MyWindow.vff
object MyWindow : VCF::Window
//other code removed for clarity
object btn1 : VCF::CommandButton
caption = 'Click Me'
delegates
ButtonClicked = [MyApp@MyApp::clickMe]
end
end
end
现在,我们可以在事件处理程序中添加一些代码
void clickMe( ButtonEvent* ) { Dialog::showMessage( "Hello from clickMe()!" ); }
运行程序时,代码将加载表单,构建 UI,并在到达“delegates”部分时,从应用程序实例获取回调并将其添加到按钮的 ButtonClicked
。
有关连接 VFF 和委托细节的更多信息,请参阅本文:Working with Delegates in Visual Form Files。
关于构建示例的说明
您需要安装最新版本的 VCF(至少 0-9-8 或更高版本)。当前示例包含一个 exe 文件,用于构建 VC6 的每个演示。如果您想使用不同版本的 Visual C++ 构建,则需要手动构建它们,并确保构建静态框架库,而不是动态库。
如果您不确定如何构建框架本身,请参阅:Building the VCF,它解释了使用各种 IDE 和工具链构建框架的基础知识。
结论
欢迎提出有关框架的问题,您可以在此处或在 论坛上发布。如果您对如何改进这些内容有任何建议,我很乐意倾听!