65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Visual Component Framework 中使用委托

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.65/5 (11投票s)

2008年6月17日

BSD

12分钟阅读

viewsIcon

26747

downloadIcon

271

本文介绍如何在 VCF 中使用委托。

引言

本文将介绍 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 和工具链构建框架的基础知识。

结论

欢迎提出有关框架的问题,您可以在此处或在 论坛上发布。如果您对如何改进这些内容有任何建议,我很乐意倾听!

© . All rights reserved.