Какао: интегрировать NSApplication в существующий основной цикл C++

Я знаю, что я не первый, кто пытается использовать Cocoa на OSX вместе с существующим основным циклом c/ C++, но мне не очень нравятся решения, с которыми я сталкивался до сих пор, поэтому у меня возникла другая идея: Я хотел бы обсудить. Наиболее распространенный способ, который я нашел (в glut, glfw, SDL, а также QT, я думаю), это использовать опрос для замены метода запуска NSApplications и обрабатывать события самостоятельно следующим образом:

nextEventMatchingMask:untilDate:inMode:dequeue:

Это имеет большой недостаток, заключающийся в том, что процессор никогда не находится в режиме ожидания, так как вы должны опрашивать все время, чтобы проверить, есть ли какие-либо новые события, кроме того, это не единственная вещь, происходящая внутри функции запуска NSApplications, поэтому она может сломать некоторые детали, если вы используйте эту замену.

Так что я хотел бы сохранить неповрежденный цикл запуска какао. Представьте, что у вас есть собственные методы таймера, реализованные в C++, которые обычно управляются и запускаются внутри вашего основного цикла (это только небольшая часть в качестве примера). Моя идея состояла бы в том, чтобы переместить все мои циклические части во вторичный поток (так как запуск NSApplication должен вызываться из основного потока, насколько мне известно), а затем публиковать пользовательские события в моей производной версии NSApplication, которая соответствующим образом обрабатывает их внутри своего потока. sendEvent: метод. Например, если мои таймеры были измерены в моем цикле C++, я бы отправил пользовательское событие в NSApplication, которое, в свою очередь, запускает функцию loopFunc() моего приложения (также находящейся в mainthread), которая соответствующим образом отправляет события по моей цепочке событий C++., Итак, прежде всего, вы думаете, это будет хорошим решением? Если да, как бы вы реализовали это в какао, я нашел этот метод только в NSEvent Reference для публикации пользовательских событий NSApplicationDefined:

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

а затем использовать что-то вроде:

[NSApp postEvent:atStart:]

уведомить NSApplication.

Я бы предпочел опубликовать событие без какой-либо информации об окне (в otherEventWithType), могу ли я просто игнорировать эту часть?

Тогда я бы хотел перезаписать функцию sendEvent NSApplications, подобную этой:

    - (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, и думаете ли вы, что это правильный подход для интеграции какао в существующий цикл запуска без опроса?

1 ответ

Решение

Ладно, после всего этого у меня ушло больше времени, чем я ожидал, и я хотел бы рассказать о том, что я пробовал, и рассказать, какой у меня был опыт с ними. Это, мы надеемся, спасет людей, пытающихся интегрировать Какао в существующий основной цикл много времени в будущем. Первой функцией, которую я нашел при поиске обсуждаемого вопроса, была функция

nextEventMatchingMask:untilDate:inMode:dequeue:

но, как я уже сказал в этом вопросе, моей главной проблемой было то, что мне приходилось постоянно опрашивать новые события, которые бы тратили довольно много времени процессора. Поэтому я попробовал следующие два метода, чтобы просто позволить моей функции обновления mainloops вызываться из mainloop NSApplications:

  1. Опубликовать пользовательское событие в NSApplication, перезаписать NSApplicationssendEvent: функцию и просто вызвать мою функцию обновления 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
        }
    }
    

    Это было только хорошей идеей в теории, потому что, если мое приложение обновлялось очень быстро (например, из-за быстрого срабатывания таймера), вся очередь событий какао полностью перестала отвечать, потому что я добавил так много пользовательских событий. Так что не используйте это...

  2. Используйте executeSelectorOnMainThread с функцией Cocoa, которая, в свою очередь, вызывает мою функцию обновления.

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

    Это было намного лучше, приложение и какао EventLoop были очень отзывчивыми. Если вы только пытаетесь достичь чего-то простого, я бы порекомендовал вам пойти по этому пути, так как он самый простой из предложенных здесь. В любом случае, у меня было очень мало контроля над порядком вещей, происходящих с этим подходом (это важно, если у вас есть многопоточное приложение), т.е. когда мои таймеры срабатывали и выполняли довольно долгую работу, часто они перепланировали перед любой новой мышью / ввод с клавиатуры может быть добавлен к моему eventQueue и, таким образом, весь ввод будет медленным. Включите вертикальную синхронизацию на окне, которое было нарисовано повторяющимся таймером, было достаточно, чтобы это произошло.

  3. В конце концов я должен был вернуться 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;
    }
    

    это заставляет какао ждать, пока не произойдет событие, если это произойдет, простоя просто выйдет, и 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;
    }
    

    кажется, не работает, в любом случае, я могу с этим смириться, так как вы можете легко проверить, закрыто ли последнее окно.

Я знаю, что этот ответ довольно длинный, но так же и мой путь к нему. Я надеюсь, что это помогает кому-то и мешает людям делать ошибки, которые я сделал.

Другие вопросы по тегам