【问题标题】:(OS X) Detecting when front app goes into fullscreen mode(OS X) 检测前端应用程序何时进入全屏模式
【发布时间】:2014-05-27 18:47:42
【问题描述】:

我正在编写一个“UIElement”应用程序,它在屏幕一侧显示一个状态窗口,类似于 Dock。

现在,当一个程序占据整个屏幕时,我需要隐藏我的状态窗口,就像 Dock 一样。

我有什么选择来检测这个和相反的事件?

我喜欢避免通过定时事件进行轮询,也不能使用未记录的技巧(例如建议的here

什么不起作用:

  • kEventAppSystemUIModeChanged 事件注册一个 Carbon 事件处理程序是不够的 - 它可以检测 VLC 的全屏模式,但不适用于在其右上角使用新的全屏小部件的现代 Cocoa 应用程序窗户。

  • 1234563小部件。
  • 使用CGDisplayRegisterReconfigurationCallback 监控屏幕配置的更改不起作用,因为这些全屏模式没有任何回调。

【问题讨论】:

  • 这是一个有趣的问题。我有一个基于grabbing the onscreen window list 的想法,并在 NSWorkspace 的活动空间更改通知触发时检查桌面是否存在,但我现在没有时间测试它。如果您喜欢冒险,并且在我回到它之前没有其他人提出更好的东西,请随意将其用作起点。
  • 你窗口的collectionBehavior 是什么?我认为省略NSWindowCollectionBehaviorFullScreenAuxiliary 意味着您的窗口会自动隐藏在全屏空间中。
  • @KenThomases - 我想观察的不是我自己的应用程序,而是其他应用程序。我的应用是纯后台应用。
  • 你说你有一个状态窗口,所以你的应用程序不是后台的。 (UIElement 与仅背景不同。)我在询问该状态窗口的 collectionBehavior
  • 也与工作代码相关stackoverflow.com/a/15895398/231917

标签: macos


【解决方案1】:

根据@Chuck 的建议,我想出了一个可行的解决方案,但可能并非万无一失。

该解决方案基于 10.7 的 new fullscreen mode for windows 将这些窗口移动到新的屏幕空间的假设。因此,我们订阅活动空间更改的通知。在那个通知处理程序中,我们检查窗口列表以检测是否包含菜单栏。如果不是,则可能意味着我们处于全屏空间中。

根据 Chuck 的想法,检查“菜单栏”窗口是否存在是我能想到的最好的测试。不过,我不太喜欢它,因为它对内部管理窗口的命名和存在做出了假设。

这是 AppDelegate.m 中的测试代码,其中还包括对其他应用程序范围的全屏模式的测试:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSApplication *app = [NSApplication sharedApplication];

    // Observe full screen mode from apps setting SystemUIMode
    // or invoking 'setPresentationOptions'
    [app addObserver:self
          forKeyPath:@"currentSystemPresentationOptions"
             options:NSKeyValueObservingOptionNew
             context:NULL];

    // Observe full screen mode from apps using a separate space
    // (i.e. those providing the fullscreen widget at the right
    // of their window title bar).
    [[[NSWorkspace sharedWorkspace] notificationCenter]
        addObserverForName:NSWorkspaceActiveSpaceDidChangeNotification
        object:NULL queue:NULL
        usingBlock:^(NSNotification *note)
        {
            // The active space changed.
            // Now we need to detect if this is a fullscreen space.
            // Let's look at the windows...
            NSArray *windows = CFBridgingRelease(CGWindowListCopyWindowInfo
                        (kCGWindowListOptionOnScreenOnly, kCGNullWindowID));
            //NSLog(@"active space change: %@", windows);

            // We detect full screen spaces by checking if there's a menubar
            // in the window list.
            // If not, we assume it's in fullscreen mode.
            BOOL hasMenubar = NO;
            for (NSDictionary *d in windows) {
                if ([d[@"kCGWindowOwnerName"] isEqualToString:@"Window Server"]
                 && [d[@"kCGWindowName"] isEqualToString:@"Menubar"]) {
                    hasMenubar = YES;
                    break;
                }
            }
            NSLog(@"fullscreen: %@", hasMenubar ? @"No" : @"Yes");
        }
     ];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([keyPath isEqual:@"currentSystemPresentationOptions"]) {
        NSLog(@"currentSystemPresentationOptions: %@", [change objectForKey:NSKeyValueChangeNewKey]); // a value of 4 indicates fullscreen mode
    }
}

【讨论】:

    【解决方案2】:

    由于我之前的回答不适用于检测应用程序之间的全屏模式,因此我做了一些实验。从 Thomas Tempelmann 提出的检查菜单栏是否存在的解决方案开始,我发现了一种我认为可能更可靠的变体。

    检查菜单栏的问题在于,在全屏模式下,您可以将鼠标光标移动到屏幕顶部以显示菜单栏,但您仍处于全屏模式。我对CGWindow的信息做了一些爬取,发现当我进入全屏时,有一个名为"Fullscreen Backdrop"的窗口属于"Dock",而在非全屏模式时它不存在。

    这是在 Xcode 游乐场的 Catalina (10.15.6) 上,所以它应该在一个真实的应用程序中进行测试,并且在 Big Sur(或任何当前的操作系统,当您阅读本文时)。

    这是代码(在 Swift 中...更容易快速测试)

    func isFullScreen() -> Bool
    {
        guard let windows = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) else {
            return false
        }
    
        for window in windows as NSArray
        {
            guard let winInfo = window as? NSDictionary else { continue }
            
            if winInfo["kCGWindowOwnerName"] as? String == "Dock",
               winInfo["kCGWindowName"] as? String == "Fullscreen Backdrop"
            {
                return true
            }
        }
        
        return false
    }
    

    【讨论】:

      【解决方案3】:

      编辑注意:不幸的是,这个答案没有提供在不同应用程序中检测全屏的解决方案,这是 OP 所要求的。我离开它是因为它确实回答了在同一个应用程序中全屏检测的问题 - 例如,在需要知道自动更新显式添加的“进入全屏”菜单项的 keyEquivalents 和标题的通用库中比 Apple 自动添加的菜单项。

      虽然这个问题现在已经很老了,但我最近不得不在 Swift 中检测全屏模式。虽然它不像在 NSWindow 中查询一些标志那么简单,但正如我们所希望的那样,自 macOS 10.7 以来就有一个简单可靠的解决方案。

      当一个窗口即将进入全屏模式时NSWindow发送willEnterFullScreenNotification通知,当它即将退出全屏模式时,它发送willExitFullScreenNotification。因此,您为这些通知添加了一个观察者。在下面的代码中,我使用它们来设置一个全局布尔标志。

      import Cocoa
      
      /*
       Since notification closures can be run concurrently, we need to guard against
       races on the Boolean flag.  We could use DispatchSemaphore, but it's kind
       over-kill for guarding a simple read/write to a boolean variable.
       os_unfair_lock is appropriate for nanosecond-level contention.  If the wait
       could be milliseconds or longer, DispatchSemaphore is the thing to use.
       
       This extension is just to make using it easier and safer to use.
       */
      extension os_unfair_lock
      {
          mutating func withLock<R>(block: () throws -> R) rethrows -> R
          {
              os_unfair_lock_lock(&self)
              defer { os_unfair_lock_unlock(&self) }
              
              return try block()
          }
      }
      
      fileprivate var fullScreenLock = os_unfair_lock()
      public fileprivate(set) var isFullScreen: Bool = false
      
      // Call this function in the app delegate's applicationDidFinishLaunching method
      func initializeFullScreenDetection()
      {
          _ = NotificationCenter.default.addObserver(
              forName: NSWindow.willEnterFullScreenNotification,
              object: nil,
              queue: nil)
          { _ in
              fullScreenLock.withLock  { isFullScreen = true }
          }
          _ = NotificationCenter.default.addObserver(
              forName: NSWindow.willExitFullScreenNotification,
              object: nil,
              queue: nil)
          { _ in
              fullScreenLock.withLock  { isFullScreen = false }
          }
      }
      

      由于观察者闭包可以同时运行,我使用os_unfair_lock 来保护_isFullScreen 属性上的竞争。你可以使用DispatchSemaphore,虽然它只是保护一个布尔标志有点重。回到第一次提出问题时,OSSpinLock 将是等效的,但自 10.12 以来已被弃用。

      只需确保在您的应用程序委托的applicationDidFinishLaunching() 方法中调用initializeFullScreenDetection()

      【讨论】:

      • 感谢这里展示了如何使用os_unfair_lock。也不错的 Swift 包装器。
      • 遗憾的是,您的解决方案对我所要求的具体问题没有帮助:我想检测 diffierent 应用程序的窗口(当时是前端应用程序) ) 全屏显示。您建议使用的事件仅适用于您自己的应用程序中的窗口。所以,这个答案虽然做得很好,但不适合这个问题。或者,如果您的应用程序是 LSUIElement=YES(没有 Dock 图标)并且另一个应用程序进入全屏模式,您能看到它工作吗?
      • 啊,在另一篇阅读文章中,我确实看到您提出的问题比我回答的问题更广泛。我的错。我的解决方案不适用于您的情况。我需要它的原因是为了在不使用组合的情况下处理 SwiftUI 中的主菜单(栏)的库,因此它存在于全屏应用程序中。很遗憾,对于您的问题,我没有直接的答案,因此必须考虑一下。
      • @Thomas Tempelmann,经过一些实验,我想出了一个可能更可靠的解决方案变体。它涉及检查“Dock”拥有的“全屏背景”窗口。我再次在 Swift 中完成了此操作,并使用该代码发布了答案,但您的解决方案的 Objective-C 代码可以轻松修改。
      猜你喜欢
      • 1970-01-01
      • 2011-10-31
      • 2013-09-30
      • 1970-01-01
      • 1970-01-01
      • 2010-11-16
      • 1970-01-01
      • 2013-05-21
      • 1970-01-01
      相关资源
      最近更新 更多