Focus
Introduction
Focus 在程式中有很多用途,例如接收鍵盤事件、標示使用者最後操作的元件、開關視窗......等等。
上圖列出了幾個跟 Focus 有關的事情。不過,請想想看圖中有幾個地方有 Focus ? 簡單的答案如下:
一個視窗可以有多個 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;
}
}
}
程式啟動時,左右都是白色的。滑鼠點擊其中一個方塊時,方塊會變綠色,但最多只有一個方塊會是綠色:點左邊,左邊變綠色右邊變白色。這說明了兩件事情:
- 一開始兩個人都沒有 focus
- 點擊其中一個方塊後,同時只有一個人有 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}
}
}
只有綠色的方塊會顯示現在打的字,也就是只有 focus 的地方會收到鍵盤事件。程式剛啟動時,打鍵盤沒有任何反應。從這邊可以知道:
- 有 focus 的元件可以收到鍵盤事件
- 沒有 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
}
}
}
這個時候,程式有兩個 focus,左邊兩個方塊有一個 focus 、右邊兩個方塊有一個 focus ,左右兩邊互不干擾。不管左邊兩個方塊怎麼切換,都不影響右邊兩個方塊的 focus。從此可知:
- 每個 FocusScope 內都會有一個 focus
- 每個 FocusScope 內最多只有一個 focus
- 不管 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}
}
被點下去的方塊會變成黃色,且可以接收鍵盤事件;原本是黃色的方塊會變成綠色。換句話說,
- 有 activeFocus 的元件可以接收鍵盤事件
- 拿到 activeFocus 的同時會拿到 focus
- 多個 FocusScope 間,只會有一個 activeFocus
- 失去 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 內的元件拿到 active focus 時,FocusScope 本身也會拿到 acitve focus
- FocusScope 內的元件失去 active focus 時,FocusScope 本身也會失去 active focus & focus
Natural FocusScope
部分 QML 元件本身會有 FocusScope 的效果,舉例來說:
Active Focus in Window
QML Window 不是 Item ,但也有類似 active focus 的概念。
- 可用 activeFocusItem 取得 Window 內 active focus 的元件
- 可用 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 的元件
在 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 跟 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.right
,往左的方向 QML 會自動產生。
Conclusion
程式的 Focus 需要仔細的規劃。舉例來說,滑鼠點擊空白背景處時, input box 打字的 focus 是否需要消失? 如果答案是要消失,那麼空白背景就必須接收 MouseEvent ,並在被點擊時搶走 focus。另外 focus 跟 Key event handler 的處理方式,例如哪邊需要 FocusScope、哪裡要 accept key event、往上傳的階層關係......等等,都會直接影響到 QML 程式碼的結構。這些並不是程式自然會有的行為,需要詳細的規劃。