Focus

Introduction

Focus 在程式中有很多用途,例如接收鍵盤事件、標示使用者最後操作的元件、開關視窗......等等。 Focus usage

上圖列出了幾個跟 Focus 有關的事情。不過,請想想看圖中有幾個地方有 Focus ? 簡單的答案如下: focus items

一個視窗可以有多個 focus 元件,以及一個 active focus 元件。其中有 active focus 的元件可接收鍵盤事件。

Focus in QML

QML Item 有 focus 可以設定。先看個簡單的範例:

Item {
    width: 800; height: 600
    Rectangle { //left
         x:0; y:0; width: 400; height: 600
         color: focus ? "green" : "white"
         MouseArea {
             anchors.fill: parent
             onClicked: parent.focus = true;
         }
    }
    Rectangle { //right
         x:400; y:0; width: 400; height: 600
         color: focus ? "green" : "white"
         MouseArea {
             anchors.fill: parent
             onClicked: parent.focus = true;
        }
    }
}

color example 程式啟動時,左右都是白色的。滑鼠點擊其中一個方塊時,方塊會變綠色,但最多只有一個方塊會是綠色:點左邊,左邊變綠色右邊變白色。這說明了兩件事情:

  1. 一開始兩個人都沒有 focus
  2. 點擊其中一個方塊後,同時只有一個人有 focus

若再加上鍵盤事件到程式碼內試試:

Item {
    width: 800; height: 600
    Rectangle { //left
         x:0; y:0; width: 400; height: 600
         color: focus ? "green" : "white"
         MouseArea {
             anchors.fill: parent
             onClicked: parent.focus = true;
         }
         Text {
             id: leftText
             font.pixelSize: 30
         }
         Keys.onPressed: { leftText.text = event.text}
    }
    Rectangle { //right
         x:400; y:0; width: 400; height: 600
         color: focus ? "green" : "white"
         MouseArea {
             anchors.fill: parent
             onClicked: parent.focus = true;
         }
         Text {
             id: rightText
             font.pixelSize: 30
         }
         Keys.onPressed: { rightText.text = event.text}
    }
}

Key event

只有綠色的方塊會顯示現在打的字,也就是只有 focus 的地方會收到鍵盤事件。程式剛啟動時,打鍵盤沒有任何反應。從這邊可以知道:

  1. 有 focus 的元件可以收到鍵盤事件
  2. 沒有 focus 的元件收不到鍵盤事件

FocusScope in QML

用前面的程式碼,不管怎麼寫程式都只會有一個 focus。稍微改寫一下:

//WhiteGreenRectangle.qml
Rectangle {
     width: 400; height: 600
     color: focus ? "green" : "white"
     MouseArea {
         anchors.fill: parent
         onClicked: parent.focus = true;
     }
     Text {
         id: txt
         font.pixelSize: 30
     }
     Keys.onPressed: { txt.text = event.text}
}

//main.qml
Item {
    width: 800; height: 600
    Item {
        x:0; y:0; width: 400; height: 600
        WhiteGreenRectangle {
             x:0; y:0; width:200; height: 600
        }
        WhiteGreenRectangle {
             x:200; y:0; width:200; height: 600
        }
    }
    Item {
        x:400; y:0; width: 400; height: 600
        WhiteGreenRectangle {
             x:0; y:0; width:200; height: 600
        }
        WhiteGreenRectangle {
             x:200; y:0; width:200; height: 600
        }
    }
}

只有一個方塊是綠色 畫面上有四個方塊,且永遠只有一個方塊會是綠色的。但從 Introduction 章節內容來看,一個程式可能會有多個 focus 。那要如何讓左邊兩個方塊跟右邊兩個方塊有各自獨立的 focus 呢? 這個時候可以用 QML 的 FocusScope 來將兩邊隔開:

Item {
    width: 800; height: 600
    FocusScope {
        x:0; y:0; width: 400; height: 600
        WhiteGreenRectangle {
             x:0; y:0; width:200; height: 600
        }
        WhiteGreenRectangle {
             x:200; y:0; width:200; height: 600
        }
    }
    FocusScope {
        x:400; y:0; width: 400; height: 600
        WhiteGreenRectangle {
             x:0; y:0; width:200; height: 600
        }
        WhiteGreenRectangle {
             x:200; y:0; width:200; height: 600
        }
    }
}

With FocusScope

這個時候,程式有兩個 focus,左邊兩個方塊有一個 focus 、右邊兩個方塊有一個 focus ,左右兩邊互不干擾。不管左邊兩個方塊怎麼切換,都不影響右邊兩個方塊的 focus。從此可知:

  1. 每個 FocusScope 內都會有一個 focus
  2. 每個 FocusScope 內最多只有一個 focus
  3. 不管 FocusScope 內的 focus 如何切換,都不影響其他 FocusScope 內的 focus 狀態

不過,你可以試試看鍵盤功能。 WhiteGreenRectangle 內的鍵盤功能居然在加上 FocusScope 後消失了!不管怎麼打字, Keys.onPressed 都沒有反應。

activeFocus in QML

QML Item 的 activeFocus 說,有 active focus 的元件可以收到鍵盤事件,另外可用 forceActiveFocus 拿到 active focus。因此,對 WhiteGreenRectangle 做小小的修改:

Rectangle {
    width: 400; height: 600
    color: activeFocus? "yellow": focus ? "green" : "white"
    MouseArea {
        anchors.fill: parent
        onClicked: parent.forceActiveFocus();
    }
    Text {
        id: txt
        font.pixelSize: 30
    }
    Keys.onPressed: { txt.text = event.text}
}

With FocusScope

被點下去的方塊會變成黃色,且可以接收鍵盤事件;原本是黃色的方塊會變成綠色。換句話說,

  1. 有 activeFocus 的元件可以接收鍵盤事件
  2. 拿到 activeFocus 的同時會拿到 focus
  3. 多個 FocusScope 間,只會有一個 activeFocus
  4. 失去 activeFocus 元件,會保留 focus 狀態

如果把 FocusScope 的狀態寫畫出來:

Item {
    width: 800; height: 600
    FocusScope {
        x:0; y:0; width: 400; height: 600
        WhiteGreenRectangle {
             x:0; y:0; width:200; height: 600
        }
        WhiteGreenRectangle {
             x:200; y:0; width:200; height: 600
        }
        Rectangle {
            x: 180; y: 0; width: 40; height: 40
            border.width: 1
            color: parent.activeFocus ? "yellow" : parent.focus ? "green": "white"
        }
    }
    FocusScope {
        x:400; y:0; width: 400; height: 600
        WhiteGreenRectangle {
             x:0; y:0; width:200; height: 600
        }
        WhiteGreenRectangle {
             x:200; y:0; width:200; height: 600
        }
        Rectangle {
            x: 180; y: 0; width: 40; height: 40
            border.width: 1
            color: parent.activeFocus ? "yellow" : parent.focus ? "green": "white"
        }
    }
}

FocusScope focus and active focus

可以觀察到,

  1. FocusScope 內的元件拿到 active focus 時,FocusScope 本身也會拿到 acitve focus
  2. FocusScope 內的元件失去 active focus 時,FocusScope 本身也會失去 active focus & focus

Natural FocusScope

部分 QML 元件本身會有 FocusScope 的效果,舉例來說:

  1. Loader: Loader is a focus scope
  2. ListView (以及其他類似元件): The list view itself is a focus scope

Active Focus in Window

QML Window 不是 Item ,但也有類似 active focus 的概念。

  1. 可用 activeFocusItem 取得 Window 內 active focus 的元件
  2. 可用 requestActivate 讓 Window 取得 active focus。通常滑鼠點擊 Window 時,Window 會自動取得 active focus。

Keyboard Event Propagation

鍵盤事件會從目前有 active focus 的地方,開始往上 (parent) 傳。舉例來說:

Item {
    objectName: "layer1"
    Keys.onPressed: {console.log(objectName)}
    Item {
        objectName: "layer2"
        Keys.onPressed: {console.log(objectName)}
        Item {
            objectName: "layer3.1"
            Keys.onPressed: {console.log(objectName)}
            Item {
                objectName: "layer4"
                Keys.onPressed: {console.log(objectName)}
                Component.onCompleted: forceActiveFocus() //*****
                Item {
                    objectName: "layer5"
                    Keys.onPressed: {console.log(objectName)}
                }
            }
        }
        Item {
            objectName: "layer3.2"
            Keys.onPressed: {console.log(objectName)}
        }
    }
}

在 layer4 的地方有 active focuse,因此按下鍵盤時,看到的 console output 是

qml: layer4
qml: layer3.1
qml: layer2
qml: layer1

下層的 layer5 跟旁邊的 layer3.2 都不會收到鍵盤事件。如果你的程式在 layer2 就要處理完全部的事件,不要讓事件繼續往上傳,就必須在 layer2 accept event

Item {
    objectName: "layer1"
    Keys.onPressed: {console.log(objectName)}
    Item {
        objectName: "layer2"
        Keys.onPressed: {console.log(objectName); event.accepted = true;} //*****
        Item {
            objectName: "layer3.1"
            Keys.onPressed: {console.log(objectName)}
            Item {
                objectName: "layer4"
                Keys.onPressed: {console.log(objectName)}
                Component.onCompleted: forceActiveFocus()
                Item {
                    objectName: "layer5"
                    Keys.onPressed: {console.log(objectName)}
                }
            }
        }
        Item {
            objectName: "layer3.2"
            Keys.onPressed: {console.log(objectName)}
        }
    }
}

此時程式執行的結果就會變成:

qml: layer4
qml: layer3.1
qml: layer2

Use Tab to Change Active Focus

很多程式可以按鍵盤 Tab 來切換 active focus 的元件 Tab to change focus

在 QML 裡面,可以用 activeFocusOnTab 快速完成這件事情:

Item {
    width: 800; height: 600
    FocusScope {
        x:0; y:0; width: 400; height: 600
        WhiteGreenRectangle {
            x:0; y:0; width:200; height: 600; activeFocusOnTab: true
        }
        WhiteGreenRectangle {
            x:200; y:0; width:200; height: 600; activeFocusOnTab: true
        }
        Rectangle {
            x: 180; y: 0; width: 40; height: 40
            border.width: 1
            color: parent.activeFocus ? "yellow" : parent.focus ? "green": "white"
        }
    }
    FocusScope {
        x:400; y:0; width: 400; height: 600
        WhiteGreenRectangle {
            x:0; y:0; width:200; height: 600; activeFocusOnTab: true
        }
        WhiteGreenRectangle {
            x:200; y:0; width:200; height: 600; activeFocusOnTab: true
        }
        Rectangle {
            x: 180; y: 0; width: 40; height: 40
            border.width: 1
            color: parent.activeFocus ? "yellow" : parent.focus ? "green": "white"
        }
    }
}

Tab

Tab 跟 Shift+Tab 都可以用。

KeyNavigation

上圖的程式除了用 Tab 切換外,若加上鍵盤左右鍵切換,可以用 KeyNavigation 來完成。舉例來說,指定每個方塊按下鍵盤右鍵後, focus 該切換到哪裡:

Item {
    width: 800; height: 600
    FocusScope {
        x:0; y:0; width: 400; height: 600
        WhiteGreenRectangle {
            id: rectA
            x:0; y:0; width:200; height: 600; activeFocusOnTab: true
            KeyNavigation.right: rectB
        }
        WhiteGreenRectangle {
            id: rectB
            x:200; y:0; width:200; height: 600; activeFocusOnTab: true
            KeyNavigation.right: rectC
        }
        Rectangle {
            x: 180; y: 0; width: 40; height: 40
            border.width: 1
            color: parent.activeFocus ? "yellow" : parent.focus ? "green": "white"
        }
    }
    FocusScope {
        x:400; y:0; width: 400; height: 600
        WhiteGreenRectangle {
            id: rectC
            x:0; y:0; width:200; height: 600; activeFocusOnTab: true
            KeyNavigation.right: rectD
        }
        WhiteGreenRectangle {
            id: rectD
            x:200; y:0; width:200; height: 600; activeFocusOnTab: true
            KeyNavigation.right: rectA
        }
        Rectangle {
            x: 180; y: 0; width: 40; height: 40
            border.width: 1
            color: parent.activeFocus ? "yellow" : parent.focus ? "green": "white"
        }
    }
}

KeyNavigation

只要寫 KeyNavigation.right ,往左的方向 QML 會自動產生。

Conclusion

程式的 Focus 需要仔細的規劃。舉例來說,滑鼠點擊空白背景處時, input box 打字的 focus 是否需要消失? 如果答案是要消失,那麼空白背景就必須接收 MouseEvent ,並在被點擊時搶走 focus。另外 focus 跟 Key event handler 的處理方式,例如哪邊需要 FocusScope、哪裡要 accept key event、往上傳的階層關係......等等,都會直接影響到 QML 程式碼的結構。這些並不是程式自然會有的行為,需要詳細的規劃。

References

  1. Keyboard Focus in Qt Quick
  2. Qt Quick Examples - Key Interaction

results matching ""

    No results matching ""