Skip to content

Instantly share code, notes, and snippets.

@usagimaru
Last active December 16, 2023 03:39
Show Gist options
  • Save usagimaru/9d773f9cc1ec0ee3ec1f52dc01c2a78c to your computer and use it in GitHub Desktop.
Save usagimaru/9d773f9cc1ec0ee3ec1f52dc01c2a78c to your computer and use it in GitHub Desktop.
macOSメニューバーの高さをバックグラウンドプロセスから正しく取得するには

macOSメニューバーの高さをバックグラウンドプロセスから正しく取得するには

バックグラウンドに常駐するアプリケーションでメニューバーの高さを計算する必要があった。いくつかの方法を試してみたが、冴えたやり方が存在しない模様。(13.x Ventura)

考慮事項

  • バックグラウンドプロセスからメニューバーの高さを正しく取得したい
  • メニューバーを隠す設定が有効な場合、それを考慮する
  • メニューバーの高さはそもそも固定値ではない
  • ノッチ無しMacでは、どの解像度でもメニューバーの高さは24pt固定(常時表示の場合/単位はpxではないことに注意)
  • ノッチ付きMac (MacBookシリーズ) では、メニューバーの高さが可変的になる
    • 設定解像度によってメニューバーの高さが変化する
  • メニューバーはひとつとは限らない
  • マルチディスプレイ環境
    • 複数個のディスプレイがある環境では、メニューバーを表すウインドウも複数存在する
    • ノッチ付きMacにノッチ無しのディスプレイを接続している環境が考えられる(ディスプレイによってメニューバーの高さが異なる)

前提環境

  • macOS Ventura 13.0
  • Studio Display
  • AppKit

🫠 NSMenuのmenuBarHeightは、アプリケーションがフォアグラウンドにいる時のみ値を返す

アプリケーションがバックグラウンドに移行して、かつメニューバーが自動で隠される設定の場合、うまく値を取れない。

let height = NSApp.mainMenu?.menuBarHeight

🫠 メニューバーの可視状態は、アプリケーションがフォアグラウンドにいる時のみ値を返す

let isVisible = NSMenu.menuBarVisible()

🫠 NSStatusBar.system.thickness は間違った値を返す

NSStatusBar.system.thickness で得られる値はmacOS Ventura時点で22ptで、これは24ptよりも小さい。昔のmacOSではメニューバーの高さは22ptであったので一致していたが、少なくともBig Sur以降ではずれている。

どうやらAppleは「正しい」としているらしい。 feedback-assistant/reports#140

The default value of this property is 20.0. The status bar returned by the system has a thickness of 22 pixels, which corresponds to the thickness of the menu bar. https://developer.apple.com/documentation/appkit/nsstatusbar/1534591-thickness

🤔 CGWindowListCopyWindowInfo() でメニューバーっぽいウインドウを探す

以下のようなコードでデスクトップ上のウインドウ一覧の中から、メニューバーっぽいやつを探す。kCGWindowBoundsでフレームが取れるので、heightがそのままメニューバーの高さになると思われる。

if let windowInfo = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [Dictionary<CFString, AnyObject>] {
	windowInfo.forEach {
		if let windowOwnerName = $0[kCGWindowOwnerName] as? String, windowOwnerName == "Window Server",
		   let windowName = $0[kCGWindowName] as? String, windowName == "Menubar" {
			print("\($0)")
		}
	}
}

絞り込み条件:

  • kCGWindowOwnerName … "Window Server"
  • kCGWindowName … "Menubar"
  • kCGWindowBoundswidth … デスクトップの幅と一致

要注意なのは、確実にメニューバーを絞りきれる保証がないことと、どうやらデスクトップスペースの数だけメニューバーが存在するようで、.optionAllだと1つに絞りきれないこと。

kCGWindowName は、Menubarに限っては画面収録のパーミッションがなくても得られる。

😑 NSScreenのフレームから計算

例えば以下のコードでは 25pt の値を取得できるが、 メニューバーを隠すオプションを有効にすると常に 1pt を返す。 (macOS 13.2.1) 1pt を除外すると、それぞれ 24pt, 0pt となり、メニューバーの高さを表しているように見える。余分な 1pt は境界線分?

メニューバーを自動で隠すオプションを有効にして、カーソルを重ねて表示してもその高さは1ptを返すことに注意。つまり、 このオプションが有効だと本来の高さ値を確認することはできない。 惜しい。😩

一応バックグラウンドからも正しく取れているように見える。(macOS 13.2.1) ノッチ付きMacでは未検証。

if let screen = NSScreen.main {
	// Maybe 25pt, or 1pt
	let menubarHeight = screen.frame.maxY - screen.visibleFrame.maxY
	// Maybe 24pt, or 0pt
	let menubarHeightActual = screen.frame.maxY - screen.visibleFrame.maxY - 1
}

マウスカーソルがいるスクリーンを得るには以下のコードを使う。上記コードの NSScreen.main の代わりにすると、操作中スクリーンを絞ったユースケースに対応できて良いかもしれない。

import Cocoa

extension NSScreen {
	
	class func screenThatUnderMouse() -> NSScreen? {
		NSScreen.screens.first {
			NSMouseInRect(NSEvent.mouseLocation, $0.frame, false)
		}
	}
	
}
if let screen = NSScreen.screenThatUnderMouse() {
	// Maybe 25pt, or 1pt
	let menubarHeight = screen.frame.maxY - screen.visibleFrame.maxY
}

😗 AppleScriptで最前面アプリケーションのメニューバーのサイズを取得

この方法だと、メニューバーを隠すオプションを有効にしていても高さ値を得られる。 今のところこの方法が最も現実的と思われる。

tell application "System Events" to tell (process 1 where frontmost is true)
	set appName to name
	
	tell menu bar 1
		set {menuBarWidth, menuBarHeight} to the size
	end tell
end tell

return {appName, {menuBarWidth, menuBarHeight}}

-- result:
-- {"Script Editor", {2560, 24}}
-- {"Finder", {2560, 24}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment