アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

フレームなしウィンドウをダブルクリックで全画面化する【Jetpack Compose Desktop】

バージョン情報

plugins {
    kotlin("jvm") version "1.4.30"
    id("org.jetbrains.compose") version "0.3.0-build150"
}

本文

フレームなしウィンドウを作成してウィンドウを移動させるまで【Jetpack Compose Desktop】 の続きになります。
https://matsudamper.hatenablog.com/entry/2021/02/08/025214

フレームなしにしたらフレームと同じ機能を別で実装しないといけないですからね。

Jetpack Compose DesktopというよりはJavaのSwingの話になっています。

  • windowDecoratedInstead という関数名にしました。
  • maximize とか restore を呼びます。
  • mouseEvent.clickCount を発見しました。こちらを使ってダブルクリックを実現しました。秒数とか取らなくて済みました。
@Composable
fun Modifier.windowDecoratedInstead(
    doubleClickEvent: (() -> Unit)? = null
): Modifier {
    val window = LocalAppWindow.current
    val innerDoubleClickEvent = doubleClickEvent ?: {
        if (window.isMaximized) {
            window.restore()
        } else {
            window.maximize()
        }
    }

    return pointerInput(Unit) {
        forEachGesture {
            awaitPointerEventScope {
                val firstEvent = awaitPointerEvent()
                val firstWindowPointer = firstEvent.mouseEvent?.point ?: return@awaitPointerEventScope

                while (true) {
                    val event = awaitPointerEvent()

                    val displayPointer = event.mouseEvent?.locationOnScreen ?: break
                    window.setLocation(
                        (displayPointer.x - firstWindowPointer.x),
                        (displayPointer.y - firstWindowPointer.y),
                    )

                    when (event.mouseEvent?.id) {
                        null,
                        MouseEvent.MOUSE_RELEASED -> {
                            if (firstEvent.mouseEvent?.clickCount == 2) {
                                innerDoubleClickEvent()
                            }
                            break
                        }
                    }
                }
            }
        }
    }
}

不具合

WIndows10 20H2でしか確認していませんが、上記のコードでは全画面ではなくフルスクリーンモードになってしまいます。
同じような問題が発見されていたのでこちらを元に修正しました。
https://stackoverflow.com/questions/7403584/does-jframe-setextendedstatemaximized-both-work-with-undecorated-frames

sun.java2d.SunGraphicsEnvironment のコードを使用しますが、IntelliJからは解決できましたが、コンパイラが認識していなかったので面倒だったのでコードを確認した所、他に依存が無いコードだったのでコーピーして使用しました。
maximizedBounds にタスクバーを除いたサイズを教えてあげると直るようです。

@Composable
fun jFrameBugFix() {
    val window = LocalAppWindow.current
    val listener = remember {
        fun updateMaximizedBounds() {
            val bounds = getUsableBounds(window.window.graphicsConfiguration.device)
            window.window.maximizedBounds = Rectangle(0, 0, bounds.width, bounds.height)
        }
        updateMaximizedBounds()
        PropertyChangeListener {
            updateMaximizedBounds()
        }
    }

    remember {
        object : RememberObserver {
            override fun onAbandoned() {}
            override fun onForgotten() {
                window.window.removePropertyChangeListener(listener)
            }

            override fun onRemembered() {
                window.window.addPropertyChangeListener(listener)
            }
        }
    }
}

// sun.java2d.SunGraphicsEnvironment
private fun getUsableBounds(gd: GraphicsDevice): Rectangle {
    val gc = gd.defaultConfiguration
    val insets = Toolkit.getDefaultToolkit().getScreenInsets(gc)
    val usableBounds = gc.bounds
    usableBounds.x += insets.left
    usableBounds.y += insets.top
    usableBounds.width -= insets.left + insets.right
    usableBounds.height -= insets.top + insets.bottom
    return usableBounds
}

さらなる問題

続いて、上記ページにもありますが、プライマリディスプレイにウィンドウが移動してしまいます。
そのため、最大化を手動でしてあげます。

if (window.isMaximized) {
    window.restore()
} else {
    val device = window.window.graphicsConfiguration.device
    val bounds = getUsableBounds(device)
    val position = getUsableDisplayZeroPoint(device)
    
    window.window.setSize(bounds.width, bounds.height)
    window.window.setLocation(
        position.x,
        position.y,
    )
}

ディスプレイの上側にタスクバーがついている場合も考慮します。

private fun getUsableDisplayZeroPoint(gd: GraphicsDevice) : Point {
    val gc = gd.defaultConfiguration
    val insets = Toolkit.getDefaultToolkit().getScreenInsets(gc)
    val usableBounds = gc.bounds
    return Point(usableBounds.x + insets.left, usableBounds.y + insets.top)
}

さらなる問題(諦め)

私のディスプレイは以下のようになっています。赤い部分がタスクバーです。
f:id:matsudamper:20210208082140p:plain

取得できるInsetはこのようになっています。
1: java.awt.Insets[top=0,left=0,bottom=0,right=56]
2: java.awt.Insets[top=0,left=0,bottom=0,right=67]
3: java.awt.Insets[top=0,left=0,bottom=40,right=0]

ディスプレイ1だけどこにタスクバーを置いても right=56 となってしまうのです。
f:id:matsudamper:20210208081442p:plain

おわりに

Jetpack Composeはモダンですが、結局は昔からあるレガシーな部分に乗っかっているので、大分辛いという印象が植え付けられました。これを直すには直接Win32APIを触るしか無さそうなのかなと思っています。