やりたいこと
- Unityで、Mac OS X 向けアプリケーションを作る。
- 背景が透過されたウィンドウに描画する。
- 非アクティブ時でも最前面に来るようにする。
- 非アクティブ時はタイトルバーなどを隠し、アクティブ時のみタイトルバーなどを描画する。
- Objective-Cではなく、Swiftで書く。
つまり、こういうものを作る。
タイトルバーを消せるようになりました。ウィンドウがアクティブのときだけタイトルバーが出るので、移動とかリサイズとかした上で好きな場所に透過して置いておけます。かわいい。最高。 #SiroTalk #SiroArt pic.twitter.com/BM5an08rmy
— かりばぁ (@KRiver1) May 23, 2018
前回までのあらすじ
この記事は単独でも読めるようになっているが、一応前回記事のリンクを貼っておく。
前回は背景透過処理と最前面処理をObjective-Cで書いたが、コピペして少しいじっただけで、あまり内容までは踏み込まなかった。
今回はSwiftで再実装することで内容を理解し、よりわかりやすくナウいコーディングを目指す。
また、追加処理としてウィンドウのアクティブイベントを検知し、タイトルバーの可視属性を変化させてみる。
最終的なSwiftコード
せっかちな人のために、先にコードの全文を載せよう。
//
// transparent.swift
// TransparentInSwift
//
// Created by kriver1 on 2018/05/23.
// Copyright © 2018年 kriver1. All rights reserved.
//
import Foundation
import Cocoa
// from: http://tatsudoya.blog.fc2.com/blog-entry-244.html
// see: https://qiita.com/mybdesign/items/fe3e390741799c1814ad
public class NativeWindowManager : NSObject {
// style mask
// see: https://developer.apple.com/documentation/appkit/nswindow.stylemask
private static let styleMask: NSWindow.StyleMask = [.closable, .titled, .resizable]
/// Initialize Unity window and set it to transparent and front.
public static func initializeTransparent() -> Void {
// get the window used by Unity (frontmost window object)
let unityWindow: NSWindow = NSApp.orderedWindows[0]
// step 1: set the Unity window transparent
transparentizeWindow(window: unityWindow)
// step 2: set the Unity view transparent
transparentizeContentView(window: unityWindow)
// step 3: make the window permanently front
frontizeWindow(window: unityWindow)
// step 4: observe notification
// see: https://qiita.com/mono0926/items/754c5d2dbe431542c75e
let center = NotificationCenter.default
center.addObserver(forName: Notification.Name.NSWindowDidBecomeMain, object: nil, queue: nil, using: becomeMainListener(notification:))
center.addObserver(forName: Notification.Name.NSWindowDidResignMain, object: nil, queue: nil, using: resignMainListener(notification:))
}
/// Set the window transparent.
///
/// - Parameters:
/// - window: window to be transparent
/// - mask: window style (title bar, closable, or something else)
private static func transparentizeWindow(window: NSWindow) -> Void {
// set its style mask
window.styleMask = styleMask
// make it transparent
window.backgroundColor = NSColor.clear
window.isOpaque = false
// remove its shadow
window.hasShadow = false
}
/// Set the view transparent.
///
/// - Parameter window: its content view is transparentized.
private static func transparentizeContentView(window: NSWindow) -> Void {
// if content view is nil, then do nothing
if let view: NSView = window.contentView {
// make it layer-backed
// see: https://blog.fenrir-inc.com/jp/2011/07/nsview_uiview.html
view.wantsLayer = true
// make its layer transparent
view.layer?.backgroundColor = CGColor.clear
view.layer?.isOpaque = false
}
}
/// Make the window permanently front.
/// see: https://qiita.com/ocadaruma/items/790e96245c99e7af42a3
///
/// - Parameter window: window to be permanently front
private static func frontizeWindow(window: NSWindow) -> Void {
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.level = NSFloatingWindowLevel
}
/// A listener for become main notification.
///
/// - Parameter notification: Notification
@objc private static func becomeMainListener(notification: Notification) -> Void {
if let window = getWindowFromNotification(notification: notification) {
toggleBorderAppearance(window: window, isShow: true)
}
}
/// A listener for resign main notification.
///
/// - Parameter notification: Notification
@objc private static func resignMainListener(notification: Notification) -> Void {
if let window = getWindowFromNotification(notification: notification) {
toggleBorderAppearance(window: window, isShow: false)
}
}
/// Safely get a window object from a notification object.
///
/// - Parameter notification: Notification
private static func getWindowFromNotification(notification: Notification) -> NSWindow? {
if let window = (notification.object as? NSWindow) {
return window
} else {
return nil
}
}
/// Hide or show the border of the given window.
///
/// - Parameters:
/// - window: a window to show/hide
/// - isShow: boolean value to indicate show or hide
private static func toggleBorderAppearance(window: NSWindow, isShow: Bool) {
window.styleMask = isShow ? styleMask : [.borderless]
window.titlebarAppearsTransparent = !isShow
window.titleVisibility = isShow ? .visible : .hidden
}
}
前回のプログラムの修正
前回のプログラムでは、このサイトにある通り、新規ウィンドウを作成してそれを透過することで透過ウィンドウを実現していた。
しかし、その後いくつかトライアンドエラーを繰り返した結果、実はこの工程はいらなかったことが発覚した。単にUnityのウィンドウを透過させる処理を書けば良かったのである。
前回のプログラムから、newWindow
に関する処理をすべて削除してもそのまま動作する。
これでコードがかなりきれいになった。
Objective-CからSwiftへ
Objective-Cで書かれたネイティブコードをSwiftに移植するのは、コード量が少なければ全然難しくない。基本的に、Objective-Cで書かれたコードをルールに従って一対一対応させていくだけで移植が完了する。
例えば、以下のようなアクセスを考える。
[window setBackgroundColor:[NSColor clearColor]];
これはSwiftに直すと以下のようになる。
window.backgroundColor = NSColor.clear
[a b]
と書いていたアクセスを、他の言語と同じようにa.b
で書くことができて、とても気持ちがいい。
Objective-CからSwiftに移行するメリットはたくさんあるが、例えばnil
を安全に扱えるなどが挙げられる。
Swiftではnil
になりうる型はOptional
としてnil
にならない型と厳密に区別されるので、変数がnil
かどうかビクビク震えながらコードを書く必要がなくなる(あるいは、関数を呼ぶたびにnil
かどうかを判定しなくてよくなる)。
さて、Objective-CからSwiftにするときは、Unityから見えるインタフェースをObjective-Cで書かなければならない。とはいえこれは簡単で、次のようなファイルを作ればよい。
import Foundation
public class Hoge : NSObject {
public static func huga() -> Void {
...
}
}
#import <Foundation/Foundation.h>
#import "[Your-Awesome-Project-Name]-Swift.h"
extern "C" {
void _ex_callHugaOfHoge() {
[Hoge huga];
}
}
using UnityEngine;
public class callHoge : MonoBehaviour
{
#if UNITY_STANDALONE_OSX
[DllImport("[Your-Awesome-Project-Name]")]
private static extern void _ex_callHugaOfHoge();
#endif
// Use this for initialization
[RuntimeInitializeOnLoadMethod]
static void doStuff()
{
#if UNITY_STANDALONE_OSX
_ex_callHugaOfHoge();
#endif
}
}
UnityのC# codeがplugin bundle内のObjective-C++ codeを読み込み、Objective-C++ codeが同bundle内のSwift codeを参照する。この書き方によって、実装をすべてSwiftに任せることができる。
ちなみに、
Library not loaded: @rpath/libswiftAppKit.dylib
というエラーメッセージがUnity側で出る場合は、ここにある通りXCodeの設定で標準ライブラリをbundleに埋め込むことで解決する。
(リンク先は「これでは解決しなかった」というissueだが、僕の場合はこれで解決した。)
アクティブ・非アクティブを検知する
AppKitにはNotificationCenter
なるものがあり、こいつにobserver
を登録することでイベントを監視することができる(イベントが発火したときに呼んでもらうコールバックを登録することができる)。
この記事がわかりやすかった。
https://qiita.com/mono0926/items/754c5d2dbe431542c75e
コールバックが発火したかどうかを目で見たいときは、コールバックとしてメッセージを出すようなコードを書けばよい。
こういう感じでプッシュ通知が出せるので、printf
的な気持ちで使える。
import Foundation
import Cocoa
public class CallBackManager : NSObject {
public static func CallBackRegister() -> Void {
let center = NotificationCenter.default
center.addObserver(forName: Notification.Name.NSWindowDidBecomeMain, object: nil, queue: nil, using: becomeMainListener(notification:))
}
@objc private static func becomeMainListener(notification: Notification) -> Void {
var pushNotification = NSUserNotification()
pushNotification.title = "OK I called!"
pushNotification.informativeText = "Notified with: \(notification) "
pushNotification.soundName = NSUserNotificationDefaultSoundName
NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification(pushNotification)
}
}
Unityのビルド済みウィンドウをアクティブにして、プッシュ通知が来れば成功だ。
先述のコードのうち、下記の部分がこの処理に該当する。
import Foundation
import Cocoa
...
public class NativeWindowManager : NSObject {
...
/// Initialize Unity window and set it to transparent and front.
public static func initializeTransparent() -> Void {
...
// step 4: observe notification
// see: https://qiita.com/mono0926/items/754c5d2dbe431542c75e
let center = NotificationCenter.default
center.addObserver(forName: Notification.Name.NSWindowDidBecomeMain, object: nil, queue: nil, using: becomeMainListener(notification:))
center.addObserver(forName: Notification.Name.NSWindowDidResignMain, object: nil, queue: nil, using: resignMainListener(notification:))
}
...
/// A listener for become main notification.
///
/// - Parameter notification: Notification
@objc private static func becomeMainListener(notification: Notification) -> Void {
if let window = getWindowFromNotification(notification: notification) {
toggleBorderAppearance(window: window, isShow: true)
}
}
/// A listener for resign main notification.
///
/// - Parameter notification: Notification
@objc private static func resignMainListener(notification: Notification) -> Void {
if let window = getWindowFromNotification(notification: notification) {
toggleBorderAppearance(window: window, isShow: false)
}
}
/// Safely get a window object from a notification object.
///
/// - Parameter notification: Notification
private static func getWindowFromNotification(notification: Notification) -> NSWindow? {
if let window = (notification.object as? NSWindow) {
return window
} else {
return nil
}
}
/// Hide or show the border of the given window.
///
/// - Parameters:
/// - window: a window to show/hide
/// - isShow: boolean value to indicate show or hide
private static func toggleBorderAppearance(window: NSWindow, isShow: Bool) {
window.styleMask = isShow ? styleMask : [.borderless]
window.titlebarAppearsTransparent = !isShow
window.titleVisibility = isShow ? .visible : .hidden
}
}
まとめ
途中詰まるところはいくつかあったものの、上記のような流れで最終的に透過&最前面&タイトルバー非表示なウィンドウを作ることができた。
みなさんがデスクトップに好きな女の子を召喚するときの助けになれば幸いである。
宣伝
技術的な話を除いた一部始終をはてなブログにまとめたので、ぜひ読んで下さい。
デスクトップに神降ろし - ブログ村