21

当我显示这样的 NSAlert 时,我会立即得到响应:

int response;
NSAlert *alert = [NSAlert alertWithMessageText:... ...];
response = [alert runModal];

问题是这是应用程序模式,我的应用程序是基于文档的。我使用工作表在当前文档的窗口中显示警报,如下所示:

int response;
NSAlert *alert = [NSAlert alertWithMessageText:... ...];
[alert beginSheetModalForWindow:aWindow
                  modalDelegate:self
                 didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
                    contextInfo:&response];

//elsewhere
- (void) alertDidEnd:(NSAlert *) alert returnCode:(int) returnCode contextInfo:(int *) contextInfo
{
    *contextInfo = returnCode;
}

唯一的问题是它会beginSheetModalForWindow:立即返回,因此我无法可靠地向用户提问并等待回复。如果我可以将任务分成两个区域,这没什么大不了的,但我不能。

我有一个循环处理大约 40 个不同的对象(在树中)。如果一个对象失败,我希望显示警报并询问用户是继续还是中止(在当前分支继续处理),但由于我的应用程序是基于文档的,Apple Human Interface Guidelines 要求在警报出现时使用工作表特定于文档。

如何显示警报表并等待回复?

4

12 回答 12

14

我们创建了一个类别NSAlert来同步运行警报,就像应用程序模式对话框一样:

NSInteger result;

// Run the alert as a sheet on the main window
result = [alert runModalSheet];

// Run the alert as a sheet on some other window
result = [alert runModalSheetForWindow:window];

该代码可通过GitHub获得,为了完整起见,下面发布了当前版本。


头文件NSAlert+SynchronousSheet.h

#import <Cocoa/Cocoa.h>


@interface NSAlert (SynchronousSheet)

-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow;
-(NSInteger) runModalSheet;

@end

实施文件NSAlert+SynchronousSheet.m

#import "NSAlert+SynchronousSheet.h"


// Private methods -- use prefixes to avoid collisions with Apple's methods
@interface NSAlert ()
-(IBAction) BE_stopSynchronousSheet:(id)sender;   // hide sheet & stop modal
-(void) BE_beginSheetModalForWindow:(NSWindow *)aWindow;
@end


@implementation NSAlert (SynchronousSheet)

-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow {
    // Set ourselves as the target for button clicks
    for (NSButton *button in [self buttons]) {
        [button setTarget:self];
        [button setAction:@selector(BE_stopSynchronousSheet:)];
    }

    // Bring up the sheet and wait until stopSynchronousSheet is triggered by a button click
    [self performSelectorOnMainThread:@selector(BE_beginSheetModalForWindow:) withObject:aWindow waitUntilDone:YES];
    NSInteger modalCode = [NSApp runModalForWindow:[self window]];

    // This is called only after stopSynchronousSheet is called (that is,
    // one of the buttons is clicked)
    [NSApp performSelectorOnMainThread:@selector(endSheet:) withObject:[self window] waitUntilDone:YES];

    // Remove the sheet from the screen
    [[self window] performSelectorOnMainThread:@selector(orderOut:) withObject:self waitUntilDone:YES];

    return modalCode;
}

-(NSInteger) runModalSheet {
    return [self runModalSheetForWindow:[NSApp mainWindow]];
}


#pragma mark Private methods

-(IBAction) BE_stopSynchronousSheet:(id)sender {
    // See which of the buttons was clicked
    NSUInteger clickedButtonIndex = [[self buttons] indexOfObject:sender];

    // Be consistent with Apple's documentation (see NSAlert's addButtonWithTitle) so that
    // the fourth button is numbered NSAlertThirdButtonReturn + 1, and so on
    NSInteger modalCode = 0;
    if (clickedButtonIndex == NSAlertFirstButtonReturn)
        modalCode = NSAlertFirstButtonReturn;
    else if (clickedButtonIndex == NSAlertSecondButtonReturn)
        modalCode = NSAlertSecondButtonReturn;
    else if (clickedButtonIndex == NSAlertThirdButtonReturn)
        modalCode = NSAlertThirdButtonReturn;
    else
        modalCode = NSAlertThirdButtonReturn + (clickedButtonIndex - 2);

    [NSApp stopModalWithCode:modalCode];
}

-(void) BE_beginSheetModalForWindow:(NSWindow *)aWindow {
    [self beginSheetModalForWindow:aWindow modalDelegate:nil didEndSelector:nil contextInfo:nil];
}

@end
于 2011-06-10T16:31:36.217 回答
9

这是解决问题的 NSAlert 类别(正如 Philipp 建议的,Frederick 提出并由 Laurent P. 改进的解决方案:我使用代码块而不是委托,因此再次简化)。

@implementation NSAlert (Cat)

-(NSInteger) runModalSheetForWindow:(NSWindow *)aWindow
{
    [self beginSheetModalForWindow:aWindow completionHandler:^(NSModalResponse returnCode)
        { [NSApp stopModalWithCode:returnCode]; } ];
    NSInteger modalCode = [NSApp runModalForWindow:[self window]];
    return modalCode;
}

-(NSInteger) runModalSheet {
    return [self runModalSheetForWindow:[NSApp mainWindow]];
}

@end
于 2014-05-21T10:54:15.167 回答
8

解决方法是调用

[NSApp runModalForWindow:alert];

在 beginSheetModalForWindow 之后。此外,您需要实现一个委托来捕获“对话框已关闭”操作,并调用 [NSApp stopModal] 作为响应。

于 2010-04-06T18:37:54.677 回答
5

万一有人来找这个(我做过),我用以下方法解决了这个问题:

@interface AlertSync: NSObject {
    NSInteger returnCode;
}

- (id) initWithAlert: (NSAlert*) alert asSheetForWindow: (NSWindow*) window;
- (NSInteger) run;

@end

@implementation AlertSync
- (id) initWithAlert: (NSAlert*) alert asSheetForWindow: (NSWindow*) window {
    self = [super init];

    [alert beginSheetModalForWindow: window
           modalDelegate: self didEndSelector: @selector(alertDidEnd:returnCode:) contextInfo: NULL];

    return self;
}

- (NSInteger) run {
    [[NSApplication sharedApplication] run];
    return returnCode;
}

- (void) alertDidEnd: (NSAlert*) alert returnCode: (NSInteger) aReturnCode {
    returnCode = aReturnCode;
    [[NSApplication sharedApplication] stopModal];
}
@end

然后同步运行 NSAlert 很简单:

AlertSync* sync = [[AlertSync alloc] initWithAlert: alert asSheetForWindow: window];
int returnCode = [sync run];
[sync release];

请注意,正如所讨论的那样,可能会出现重入问题,因此在这样做时要小心。

于 2010-01-25T01:33:17.623 回答
3

不幸的是,您在这里无能为力。您基本上必须做出决定:重新架构您的应用程序,以便它可以以异步方式处理对象,或者使用未经批准、已弃用的架构来呈现应用程序模式警报。

在不了解有关您的实际设计以及如何处理这些对象的任何信息的情况下,很难提供任何进一步的信息。不过,在我的脑海中,有几个想法可能是:

  • 在通过某种运行循环信号或队列与主线程通信的另一个线程中处理对象。如果窗口的对象树被中断,它会向主线程发出它被中断的信号,并等待来自主线程的信号,其中包含有关要做什么的信息(继续这个分支或中止)。然后主线程显示文档模式窗口,并在用户选择要做什么后向进程线程发出信号。

但是,对于您需要的东西,这可能真的过于复杂了。在这种情况下,我的建议是使用已弃用的用法,但这实际上取决于您的用户要求。

于 2009-03-03T02:29:21.027 回答
2

斯威夫特 5:

extension NSAlert {

  /// Runs this alert as a sheet.
  /// - Parameter sheetWindow: Parent window for the sheet.
  func runSheetModal(for sheetWindow: NSWindow) -> NSApplication.ModalResponse {
    beginSheetModal(for: sheetWindow, completionHandler: NSApp.stopModal(withCode:))
    return NSApp.runModal(for: sheetWindow)
  }
}
于 2020-02-04T13:23:50.543 回答
1

这是我的答案:

创建一个全局类变量 'NSInteger alertReturnStatus'

- (void)alertDidEndSheet:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
    [[sheet window] orderOut:self];
    // make the returnCode publicly available after closing the sheet
    alertReturnStatus = returnCode;
}


- (BOOL)testSomething
{

    if(2 != 3) {

        // Init the return value
        alertReturnStatus = -1;

        NSAlert *alert = [[[NSAlert alloc] init] autorelease];
        [alert addButtonWithTitle:@"OK"];
        [alert addButtonWithTitle:@"Cancel"];
        [alert setMessageText:NSLocalizedString(@"Warning", @"warning")];
        [alert setInformativeText:@"Press OK for OK"];
        [alert setAlertStyle:NSWarningAlertStyle];
        [alert setShowsHelp:NO];
        [alert setShowsSuppressionButton:NO];

        [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(alertDidEndSheet:returnCode:contextInfo:) contextInfo:nil];

        // wait for the sheet
        NSModalSession session = [NSApp beginModalSessionForWindow:[alert window]];
        for (;;) {
            // alertReturnStatus will be set in alertDidEndSheet:returnCode:contextInfo:
            if(alertReturnStatus != -1)
                break;

            // Execute code on DefaultRunLoop
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                                     beforeDate:[NSDate distantFuture]];

            // Break the run loop if sheet was closed
            if ([NSApp runModalSession:session] != NSRunContinuesResponse 
                || ![[alert window] isVisible]) 
                break;

            // Execute code on DefaultRunLoop
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode 
                                     beforeDate:[NSDate distantFuture]];

        }
        [NSApp endModalSession:session];
        [NSApp endSheet:[alert window]];

        // Check the returnCode by using the global variable alertReturnStatus
        if(alertReturnStatus == NSAlertFirstButtonReturn) {
            return YES;
        }

        return NO;
    }
    return YES;
}

希望它会有所帮助,干杯——汉斯

于 2010-08-18T15:48:24.307 回答
1

这是上面的 Laurent 等人的版本,翻译成 Xcode 6.4 的 Swift 1.2(截至今天的最新工作版本)并在我的应用程序中进行了测试。感谢所有为这项工作做出贡献的人!Apple 的标准文档没有给我任何关于如何进行的线索,至少在我能找到的任何地方都没有。

对我来说还有一个谜团:为什么我必须在 final 函数中使用双感叹号。NSApplication.mainWindow 应该只是一个可选的 NSWindow (NSWindow?),对吧?但是编译器给出了显示的错误,直到我使用第二个'!'。

extension NSAlert {
    func runModalSheetForWindow( aWindow: NSWindow ) -> Int {
        self.beginSheetModalForWindow(aWindow) { returnCode in
            NSApp.stopModalWithCode(returnCode)
        }
        let modalCode = NSApp.runModalForWindow(self.window as! NSWindow)
        return modalCode
    }

    func runModalSheet() -> Int {
        // Swift 1.2 gives the following error if only using one '!' below:
        // Value of optional type 'NSWindow?' not unwrapped; did you mean to use '!' or '?'?
        return runModalSheetForWindow(NSApp.mainWindow!!)
    }
}
于 2015-07-28T21:23:16.150 回答
0

与 Windows 不同,我不相信有办法阻止模式对话框。输入(例如用户单击按钮)将在您的主线程上处理,因此无法阻止。

对于您的任务,您要么必须将消息向上传递,然后从中断的地方继续。

于 2009-03-03T02:03:36.643 回答
0

当一个对象失败时,停止处理树中的对象,记下哪个对象失败(假设有订单,您可以从上次中断的地方继续),然后扔掉工作表。当用户关闭工作表时,让该didEndSelector:方法从它离开的对象重新开始处理,或者不处理,具体取决于returnCode.

于 2009-03-03T13:09:10.443 回答
0
- (bool) windowShouldClose: (id) sender
 {// printf("windowShouldClose..........\n");
  NSAlert *alert=[[NSAlert alloc ]init];
  [alert setMessageText:@"save file before closing?"];
  [alert setInformativeText:@"voorkom verlies van laatste wijzigingen"];
  [alert addButtonWithTitle:@"save"];
  [alert addButtonWithTitle:@"Quit"];
  [alert addButtonWithTitle:@"cancel"];
  [alert beginSheetModalForWindow: _window modalDelegate: self
              didEndSelector: @selector(alertDidEnd: returnCode: contextInfo:)
                 contextInfo: nil];
  return false;
}
于 2012-10-06T07:56:46.243 回答
0

您可以使用dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);

NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:@"alertMessage"];
[alert addButtonWithTitle:@"Cancel"];
[alert addButtonWithTitle:@"Ok"];

dispatch_async(dispatch_get_main_queue(), ^{
    [alert beginSheetModalForWindow:progressController.window completionHandler:^(NSModalResponse returnCode) {
         if (returnCode == NSAlertSecondButtonReturn) {
             // do something when the user clicks Ok

         } else {
             // do something when the user clicks Cancel
         }

         dispatch_group_leave(group);
     }];
});

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

//you can continue your code here

希望有帮助。

于 2018-09-24T13:07:08.133 回答