【问题标题】:Wait for [NSAlert beginSheetModalForWindow:...];等待 [NSAlert beginSheetModalForWindow:...];
【发布时间】:2009-03-03 01:22:56
【问题描述】:

当我显示这样的 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 要求在警报出现时使用工作表特定于文档。

如何显示警报表并等待响应?

【问题讨论】:

    标签: cocoa macos alerts document-based


    【解决方案1】:

    我们创建了一个category on NSAlert to run alerts synchronously,就像应用程序模式对话框:

    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
    

    【讨论】:

      【解决方案2】:

      这是一个解决问题的 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
      

      【讨论】:

      • 完美!只是使其工作所需的本质。谢谢。
      • @Laurent - 这是一个很好的解决方案,但对于一个问题:有没有办法让工作表只在包含工作表的窗口中暂停事件?似乎 NSApp runModalForWindow 也停止了应用程序其他窗口中的所有事件处理。
      【解决方案3】:

      解决办法是调用

      [NSApp runModalForWindow:alert];
      

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

      【讨论】:

      • 我使用[NSApp stopModalWithCode:returnCode]; 以便runModalForWindow 获得正确的代码。
      • 从 10.9 开始,-beginSheetModalForWindow:completionHandler: 优于使用委托。 Laurent在完成块中的stopModalWithCode:returnCode工作:用户点击,完成块运行,然后由runModalForWindow返回returnCode。
      【解决方案4】:

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

      @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];
      

      请注意,如上所述,可能会出现重入问题,因此在这样做时要小心。

      【讨论】:

        【解决方案5】:

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

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

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

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

        【讨论】:

        • 线程我想我最终是要走的路。对象树最终会变得更大更复杂。
        • 看不到你的应用,显然很难说,但你确定你需要线程吗?我从未遇到过将响应放入回调方法中比线程化应用程序更复杂的情况。
        【解决方案6】:

        斯威夫特 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)
          }
        }
        

        【讨论】:

          【解决方案7】:

          这是我的答案:

          创建一个全局类变量'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;
          }
          

          希望对你有所帮助 干杯 --汉斯

          【讨论】:

            【解决方案8】:

            这是上面 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!!)
                }
            }
            

            【讨论】:

            • 很棒,在 Swift 4 上仍然可以。函数的返回类型现在是 NSApplication.ModalResponse,您现在只需要在最后一行中添加一个感叹号。我们也不需要第 6 行的类型转换。
            【解决方案9】:

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

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

            【讨论】:

              【解决方案10】:

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

              【讨论】:

              • 我刚刚重新阅读了您的问题,我担心“我无法将任务分成两个区域”您说这是不可能的。对不起,如果我的回答没有帮助。
              【解决方案11】:
              - (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;
              }
              

              【讨论】:

              • 感谢您发布答案!虽然代码 sn-p 可以回答这个问题,但添加一些附加信息仍然很棒,比如解释等..
              【解决方案12】:

              你可以使用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
              

              希望对您有所帮助。

              【讨论】:

                猜你喜欢
                • 2015-07-31
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2018-12-09
                • 1970-01-01
                • 2018-05-04
                • 2020-05-09
                相关资源
                最近更新 更多