Mac OS X Cocoa:applicationDidBecomeActive未被触发
我打算完全靠代码来实现一个应用程序(因此这不是对 this 的重复提问,因为那个问题的解决方案是在Xcode中配置nib文件),并且我想在我的应用程序成为主窗口时检测到它。
我正在通过以下方式创建它:
@interface MilskoCocoa : NSObject {
NSApplication *application;
MilskoCocoaWindow *window;
NSRect rect;
MilskoCocoaView *view;
MwLL parent;
}
+ (MilskoCocoa *)newWithParent:(MwLL)parent
x:(int)x
y:(int)y
width:(int)width
height:(int)height;
@end
// ...
@implementation MilskoCocoa
+ (MilskoCocoa *)newWithParent:(MwLL)parent
x:(int)x
y:(int)y
width:(int)width
height:(int)height
handle:(MwLL)r {
MilskoCocoa *c = [MilskoCocoa alloc];
[c retain];
c->application = [NSApplication sharedApplication];
c->rect = rectFlip(NSMakeRect(x, y, width, height));
if (parent == NULL) {
c->window = [[MilskoCocoaWindow alloc]
initWithContentRect:c->rect
styleMask:(NSTitledWindowMask |
NSTexturedBackgroundWindowMask |
NSClosableWindowMask | NSMiniaturizableWindowMask |
NSResizableWindowMask)
backing:NSBackingStoreBuffered
defer:NO];
} else {
double offset = 0;
MilskoCocoaWindow *parentWindow = parent->cocoa.real->window;
offset =
[parentWindow frameRectForContentRect:[parentWindow frame]]
.size.height -
[parentWindow contentRectForFrameRect:[parentWindow frame]].size.height;
c->rect.origin.x += [parentWindow frame].origin.x;
c->rect.origin.y -= [parentWindow frame].origin.y - offset;
c->rect.origin.y -= offset;
c->window =
[[MilskoCocoaWindow alloc] initWithContentRect:c->rect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered
defer:NO];
}
[c->window
setDelegate:[[MilskoCocoaWindowDelegate alloc] initWithWin:c->window]];
[c->window makeKeyAndOrderFront:c->application];
[c->window retain];
if (parent != NULL) {
MilskoCocoa *p = parent->cocoa.real;
double offset = 0;
offset = [p->window frameRectForContentRect:[p->window frame]].size.height -
[p->window contentRectForFrameRect:[p->window frame]].size.height;
[p->window addChildWindow:c->window ordered:NSWindowAbove];
[c->window setHasShadow:MwFALSE];
[c->window setParentWindow:p->window];
c->rect.origin.x += [p->window frame].origin.x;
c->rect.origin.y -= [p->window frame].origin.y - offset;
c->rect.origin.y -= offset;
} else {
[c->application setActivationPolicy:NSApplicationActivationPolicyRegular];
[c->application activateIgnoringOtherApps:true];
[c->window makeFirstResponder:c->view];
[c->window makeKeyAndOrderFront:nil];
}
[c->application setDelegate:[[MilskoCocoaApplicationDelegate alloc]
initWithAppl:c->application]];
[c->application retain];
c->view = [[MilskoCocoaView alloc] initWithFrame:c->rect];
[c->view retain];
[c->window setContentView:c->view];
c->parent = parent;
[c->application finishLaunching];
return c;
}
如代码所示,我也有这个应用程序代理:
@implementation MilskoCocoaApplicationDelegate
- (MilskoCocoaApplicationDelegate *)initWithAppl:(NSApplication *)_appl {
self->appl = _appl;
return self;
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
printf("test\n");
}
@end
我的理解是applicationDidBecomeActive应该会在我的应用程序成为主窗口时被调用(当我在它的任意区域点击,除了标题栏)。然而这并没有发生,我放置的日志也从未被触发。是不是应该调用其他的函数?
值得一提的是,由于我正在移植的库的设计,我并不使用任何回调,相反我的主循环包含手动调用 nextEventMatchingMask,根据产生的NSEvent的类型来执行代码,然后再执行 [NSWindow sendEvent:{event}]。这样做在过去也带来过麻烦,因为相应的回调函数也不会被触发。
编辑:由于在调用 [c->application run] 时它确实会被激活,下面是前述自定义事件循环。
/* Check whether there are pending events. Yes, this is hacky and unreliable, but the rest of the library REALLY doesn't like us just always returning 1 from this function so we have to do Something. */
- (int)pending {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
MwBool isPending = MwFALSE;
if (_forceRender) {
_forceRender = MwFALSE;
[pool release];
return 1;
}
self->lastEvent = [self->window nextEventMatchingMask:NSAnyEventMask
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES];
isPending = self->lastEvent != NULL;
[pool release];
return isPending;
};
/* Actually process the event assuming there are any pending */
- (void)getNextEvent {
[self eventProcess:self->lastEvent];
while (true) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSEvent *ev = [self->window nextEventMatchingMask:NSAnyEventMask
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES];
if (!ev) {
[pool release];
break;
}
[self eventProcess:ev];
[pool release];
}
[self sendClipboardEvent];
[self->application updateWindows];
[self->application runModalForWindow:self->window];
};
- (void)eventProcess:(NSEvent *)ev {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSWindow *win = [ev window];
MwLL h;
MwBool doSendEvent = MwTRUE;
if (!win) {
[pool release];
return;
}
switch ([ev type]) {
/* snip */
}
if (doSendEvent) {
[win sendEvent:ev];
}
[pool release];
}
解决方案
编辑:发帖后我意识到实际需要做的事情,但为了防止有人遇到这个问题时第一种方法不是他们的解决方案,我就把两种方案都保留着。
正确的方案:模态会话
在创建我的窗口时,我需要使用 beginModalSessionForWindow 来创建一个“模态会话”,然后在待处理函数中使用 runModalSession 运行该模态会话。
- (int)pending {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
MwBool isPending = MwFALSE;
self->lastEvent = [self->window nextEventMatchingMask:NSAnyEventMask
untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode
dequeue:YES];
[self->lastEvent retain];
/* only run the modal session if we don't have a pending event, as otherwise
* it consumes a bunch of our events */
if (!self->lastEvent) {
[NSApp runModalSession:self->modalSession];
}
isPending = self->lastEvent != NULL;
[pool release];
return isPending;
};
笨拙、不正确,但可行的方案:重复 [NSApplication run] 调用
我的原始方案是只使用 [NSApplication run],再搭配一个立即停止它的函数,从而以某种方式推动事件,以便让MacOS愿意接受它。然而,这也会导致CPU使用率飙升(除非在每次循环后执行 usleep(1000)),在一些鲜为人知的地方还引发了奇怪的段错误。我发现换一种做法反而解决了更多的问题。
- (int)pending {
[MilskoCocoa performSelectorOnMainThread:@selector(eventCanceller:)
withObject:self
waitUntilDone:NO];
[self->application run];
return 1;
};
+ (void)eventCanceller:(MilskoCocoa *)this {
this->lastEvent = [this->application currentEvent];
[this eventProcess:this->lastEvent];
[[NSApplication sharedApplication] stop:nil];
}