Cocoa:将 NSApplication 集成到现有的 c++ 主循环中

Posted

技术标签:

【中文标题】Cocoa:将 NSApplication 集成到现有的 c++ 主循环中【英文标题】:Cocoa: integrate NSApplication into an existing c++ mainloop 【发布时间】:2011-10-07 15:00:48 【问题描述】:

我知道,我不是第一个尝试在 OSX 上将 Cocoa 与现有的 c/c++ 主循环一起使用的人,但我并不是很喜欢迄今为止遇到的解决方案,所以我想出了一个不同的解决方案我想讨论的想法。我发现(在 glut、glfw、SDL 以及我认为的 QT 中)最常见的方法是使用轮询来替换 NSApplications 运行方法并自己处理事件:

nextEventMatchingMask:untilDate:inMode:dequeue:

这有一个很大的缺点是 CPU 永远不会真正空闲,因为你必须一直轮询以检查是否有任何新事件,而且它不是 NSApplications 运行函数中唯一发生的事情,所以它可能会破坏一些如果您使用此替换,请详细说明。

所以我想做的是保持可可 runLoop 完好无损。想象一下,您将在 c++ 中实现自己的计时器方法,这些方法通常会在您的主循环中进行管理和触发(这只是一个示例)。我的想法是将所有循环部分移动到辅助线程(因为据我所知,需要从主线程调用 NSApplication 运行),然后将自定义事件发布到我的 NSApplication 派生版本,该版本在其内部适当地处理它们发送事件:方法。例如,如果我的 c++ 循环中测量的计时器触发,我会向 NSApplication 发布一个自定义事件,该事件反过来运行我的应用程序的 loopFunc() 函数(也位于主线程中),该函数会适当地将事件发送到我的 c++ 事件链中. 首先,您认为这是一个好的解决方案吗? 如果是,你将如何在可可中实现它,我只在 NSEvent Reference 中找到了这个方法来发布自定义 NSApplicationDefined 事件:

otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:

然后使用类似的东西:

[NSApp postEvent:atStart:]

通知 NSApplication。

我宁愿发布一个没有关于窗口的任何信息的事件(在 otherEventWithType 中),我可以直接忽略那部分吗?

然后我想覆盖类似这样的 NSApplications sendEvent 函数:

    - (void)sendEvent:(NSEvent *)event

    //this is my custom event that simply tells NSApplication 
    //that my app needs an update
    if( [event type] == NSApplicationDefined)
    
        myCppAppPtr->loopFunc(); //only iterates once
    
        //transform cocoa events into my own input events
    else if( [event type] == NSLeftMouseDown)
    
             ...
             myCppAppPtr->loopFunc(); //also run the loopFunc to propagate input events
    
        ...

    //dont break the cocoa event chain
    [super sendEvent:event];


很抱歉这篇长文,但这一直困扰着我,因为到目前为止我对这个主题的发现并不满意。这就是我在 NSApplication 中发布和检查自定义事件的方式吗?您认为这是一种无需轮询即可将可可集成到现有运行循环中的有效方法吗?

【问题讨论】:

嗨,以防万一有人想知道我如何解决这个问题:我在派生的 NSApplication 类 sendEvent 函数中获取所有相关事件并将它们转发到我的事件循环。如果我需要我的事件循环运行,我通过 performSelectorOnMainThread 通知 NSApplication,类似于:[theAppNotifier performSelectorOnMainThread:@selector(runMyMainLoop) withObject:nil waitUntilDone:NO];这对我来说是一个很好的解决方案,因为我不必侵入可可主循环。相反,我让它自己运行。 【参考方案1】:

好的,毕竟这花费了我比预期更多的时间,我想概述一下我尝试过的事情,并告诉你我在这些事情上的经历。这有望为人们在未来将 Cocoa 集成到现有主循环中节省大量时间。我在搜索讨论的问题时发现的第一个函数是函数

nextEventMatchingMask:untilDate:inMode:dequeue:

但正如我在问题中所说,我的主要问题是我必须不断轮询新事件,这会浪费相当多的 CPU 时间。 所以我尝试了以下两种方法来简单地让我的 mainloops 更新函数从 NSApplications 主循环中调用:

    向 NSApplication 发布自定义事件,覆盖 NSApplications sendEvent: 函数,只需调用我的 mainloops 更新函数 从那里。类似这样:

    NSEvent* event = [NSEvent otherEventWithType: NSApplicationDefined
                                                         location: NSMakePoint(0,0)
                                                   modifierFlags: 0
                                                       timestamp: 0.0
                                                    windowNumber: 0
                                                         context: nil
                                                         subtype: 0
                                                           data1: 0
                                                           data2: 0];
                    [NSApp postEvent: event atStart: YES];
    
    
    //the send event function of my overwritten NSApplication
       - (void)sendEvent:(NSEvent *)event
    
        //this is my custom event that simply tells NSApplication 
        //that my app needs an update
        if( [event type] == NSApplicationDefined)
        
            myCppAppPtr->loopFunc(); //only iterates once
        
    
    

    这在理论上只是一个好主意,因为如果我的应用更新得非常 快速(例如由于计时器快速触发),整个 可可事件队列变得完全没有响应,因为我添加了 许多自定义事件。 所以不要使用这个...

    将 performSelectorOnMainThread 与 cocoaFunction 一起使用 轮流调用我的更新函数

    [theAppNotifier
    performSelectorOnMainThread:@selector(runMyMainLoop) withObject:nil
    waitUntilDone:NO ];
    

    这好多了,app 和 cocoa EventLoop 非常好 反应灵敏。如果您只是想实现一些简单的事情,我会 建议沿着这条路线走,因为它是最简单的路线 在这里提出。无论如何,我几乎无法控制顺序 这种方法发生的事情(如果你有一个 多线程应用程序),即当我的计时器被触发并且会做一个相当 长期工作,通常他们会在任何新的工作之前重新安排 鼠标/键盘输入可以添加到我的 eventQueue 中,因此会 使整个输入变得迟缓。在窗口上打开垂直同步 由重复计时器绘制的内容足以让这种情况发生。

    毕竟我不得不回到nextEventMatchingMask:untilDate:inMode:dequeue:,经过反复试验,我实际上找到了一种无需不断轮询即可使其工作的方法。我的循环结构是这样的:

    void MyApp::loopFunc()
    
        pollEvents();
        processEventQueue();
        updateWindows();
        idle();
    
    

    其中 pollEvents 和 idle 是重要的函数,基本上我用的是类似这个的。

    void MyApp::pollEvents()
    
        NSEvent * event;
    
        do
        
            event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES];
    
    
                //Convert the cocoa events to something useful here and add them to your own event queue
    
            [NSApp sendEvent: event];
        
        while(event != nil);
    
    

    为了在 idle() 函数中实现阻塞,我这样做了(不确定这是否好,但它似乎工作得很好!):

    void MyApp::idle()
    
        m_bIsIdle = true;
        NSEvent * event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:NO];
        m_bIsIdle = false;
    
    

    这会导致 cocoa 等到有事件发生,如果发生这种情况,则空闲简单地退出并且 loopfunc 再次启动。如果我的一个计时器(我不使用可可计时器)触发,要唤醒空闲功能,我再次使用自定义事件:

    void MyApp::wakeUp()
    
        m_bIsIdle = false;
    
        //this makes sure we wake up cocoas run loop
        NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
        NSEvent* event = [NSEvent otherEventWithType: NSApplicationDefined
                                            location: NSMakePoint(0,0)
                                       modifierFlags: 0
                                           timestamp: 0.0
                                        windowNumber: 0
                                             context: nil
                                             subtype: 0
                                               data1: 0
                                               data2: 0];
        [NSApp postEvent: event atStart: YES];
        [pool release];
    
    

    由于我随后立即清除了整个可可事件队列,因此我没有遇到 第 1 节中描述的相同问题。 但是,这种方法也有一些缺点,因为我认为它并不能完成 [NSApplication run] 在内部所做的所有事情,即应用程序委托这样的事情:

    - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)theApplication
    
          return YES;
    
    

    似乎不起作用,无论如何我可以忍受,因为您可以轻松地检查自己是否刚刚关闭了最后一个窗口。

我知道这个答案很长,但我的旅程也是如此。我希望这可以帮助某人并防止人们犯我犯的错误。

【讨论】:

以上是关于Cocoa:将 NSApplication 集成到现有的 c++ 主循环中的主要内容,如果未能解决你的问题,请参考以下文章

Cocoa:访问事件队列?

让我的 Cocoa 应用程序响应键盘播放/暂停键?

Cocoa NSAlert不会以表格形式显示警报

React Native集成到现有项目(非cocoa pods)

无法连接操作:到 NSApplication 类的目标

与 GLUT 和 OpenGL 集成的 Cocoa 框架