Skip to content

Mac の tray アプリを作る

Mac の tray アプリを作るためには、以下の手順が必要です。

設定

build.gradle.kts に以下のように追記します。

kotlin
plugins {
    kotlin("multiplatform") version "2.0.20"
}

kotlin {
    macosArm64("native") {
        binaries {
            executable()
        }
    }

    sourceSets {
        commonMain {
            dependencies {
            }
        }
    }
}

実装

kotlin
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCAction
import platform.AppKit.NSAlert
import platform.AppKit.NSAlertFirstButtonReturn
import platform.AppKit.NSAlertSecondButtonReturn
import platform.AppKit.NSApplication
import platform.AppKit.NSApplicationDelegateProtocol
import platform.AppKit.NSMenu
import platform.AppKit.NSMenuItem
import platform.AppKit.NSStatusBar
import platform.AppKit.NSStatusItem
import platform.AppKit.NSVariableStatusItemLength
import platform.Foundation.NSNotification
import platform.Foundation.NSSelectorFromString
import platform.darwin.NSObject

class TrayIconHandler {
    // keep these properties as fields to avoid being garbage collected.
    private lateinit var statusItem: NSStatusItem
    private lateinit var appDelegate: NSApplicationDelegateProtocol

    @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
    fun createAppDelegate(): NSApplicationDelegateProtocol {
        appDelegate =
            object : NSObject(), NSApplicationDelegateProtocol {
                override fun applicationDidFinishLaunching(notification: NSNotification) {
                    statusItem = NSStatusBar.systemStatusBar.statusItemWithLength(NSVariableStatusItemLength)
                    statusItem.button?.title = "Hello"
                    val menu =
                        NSMenu().apply {
                            addItem(
                                NSMenuItem(
                                    "Show dialog",
                                    action = NSSelectorFromString("showDialog"),
                                    keyEquivalent = "s",
                                ),
                            )
                            addItem(
                                NSMenuItem(
                                    "Quit",
                                    action = NSSelectorFromString("terminate:"),
                                    keyEquivalent = "q",
                                ),
                            )
                        }
                    statusItem.menu = menu
                }

                @ObjCAction
                fun showDialog() {
                    println("Show dialog")
                    val alert =
                        NSAlert().apply {
                            messageText = "Alert Dialog"
                            informativeText = "This is an example of an alert dialog in Kotlin/Native for macOS."
                            addButtonWithTitle("OK")
                            addButtonWithTitle("Cancel")
                        }
                    val response = alert.runModal()
                    when (response) {
                        NSAlertFirstButtonReturn -> println("OK clicked")
                        NSAlertSecondButtonReturn -> println("Cancel clicked")
                    }
                }
            }
        return appDelegate
    }
}

fun main() {
    val trayIconHandler = TrayIconHandler()
    val app = NSApplication.sharedApplication()
    app.delegate = trayIconHandler.createAppDelegate()
    app.run()
}

注意点

通常の kotlin native アプリケーションと同様に ./gradlew runDebugExecutableNative とすると起動しない。

shell
../../gradlew nativeBinaries
./build/bin/native/debugExecutable/03-trayicon.kexe

とすると、メニューに出る。

NSStatusItem と NSApplicationDelegateProtocol が解放されるとメニューアイテムがすっと消えるという謎挙動に悩まされることになる。 なので、解放されない場所に保持しておく必要があることに注意してください。

発展

  • notification を送るようにすることも可能だが、その場合は .app 形式にする必要がある。
  • dialog を表示させることも可能なのだが、なぜか出るときと出ないときがあって謎。。
  • (このへんは、mac 自体のデバッグ力がもう少したかまらないとキツいかも)