As my next task, I'm developing a tab control that has to follow certain design standards. The end result is shown at the end of this post. The easiest way to do this would be just to use images as backgrounds for UI elements. However, the control also has to scale and in most cases the uneven stretching of images would lead to ugliness. Therefore, the whole control had to be developed by using QML design elements, such as Rectangles, Rows or Grids. There were two major issues to be considered: firstly, there is no way to round only certain corners of the rectangle, its either all or none. Secondly, when the tab is selected or moused over, a red line has to be displayed across the top of the tab.
Generally, the tab control is a rectangle which has a row at the top. A tab is a component which is specified in a separate QML file, CustomTab.qml. For now, tabs are hard-coded as child elements of the said row. Other than layout properties, that anchor the tab to a proper position, it also holds some properties that are important for visual display. isSelected defines if the tab is currently selected. If it is, it becomes slightly taller and displays a red line across the top. isHighlighted defines if the mouse is currently over the tab. If it is, a more narrow red line is displayed across the top, and the tab height does not change. The iPosition has three states and defines if the tab is rightmost, leftmost or is in between the tabs. Rightmost tab has the top right corner rounded, leftmost - the left, and the middle tabs are not rounded. When the tab is selected, both corners are rounded regardless of position. All of this logic was implemented in somewhat messy JavaScript functions.
From the design point of view, each tab is a 2x2 Grid element. A rectangular grid covers a rounded rectangle. By making top right or top left elements or both of the grid transparent, I make rounded corners visible, giving the impression of a tab with one or two rounded corners. Top elements of the grid are also used to change the height of the red line by modifying their height.
The most important function is the applyState(). After tab properties such as isSelected or isHighlighted were assigned, the function makes sure that the tab is visualised correctly.
function applyState()
{
if(isSelected)
{
applyHeights(ctlHeight + redLineHeight/2, redLineHeight, ctlHeight + redLineHeight/2)
customTab.color=redColor
}
else if(isHighlighted)
{
applyHeights(ctlHeight, redLineHeight/2, ctlHeight)
customTab.color = redColor
}
else
{
applyHeights(ctlHeight, redLineHeight, ctlHeight)
customTab.color=grayColor
}
if(iPosition == "0")
{
elements.children[0].color = transparentColor;
elements.children[1].color = getCorrectColor();
}
else if(iPosition == "1")
{
elements.children[0].color = getCorrectColor();
elements.children[1].color = getCorrectColor();
}
else if(iPosition == "2")
{
elements.children[0].color = getCorrectColor();
elements.children[1].color = transparentColor;
}
}
If the tab is selected, the height of the grid rows is increased as required, if it is highlighted the height of the top row is adjusted and if none of that, it is returned to the initial state. Next, the grid elements are colored appropriately or set to transparent according to the tab position in the tab control.
function clearState()
{
var j=1;
for(j=0;j<= numTabs-1;j++)
{
children[j].isSelected = false;
children[j].state = "unselected";
children[j].applyState();
}
}
The clearState() function just loops through the children of tabRow, which are individual tabs. It clears the isSelected tag and applies the proper state (see below). Then it applies the tab state, essentially resetting it to initial state.
MouseArea{
hoverEnabled: true
anchors.fill: parent
onClicked: {
parent.parent.clearState();
parent.isSelected = true;
parent.state = "selected"
parent.applyState();
}
onEntered: {
parent.isHighlighted = true;
if(!parent.isSelected)
{
parent.applyState();
}
}
onExited: {
parent.isHighlighted = false;
if(!parent.isSelected)
{
parent.applyState();
}
}
}
Each tab has a MouseArea, which listens to three events: onClicked, onEntered and onExited. Note the usage of hoverEnabled: true, without which the MouseArea would just ignore the last two events mentioned. When the tab is clicked, all tabs are reset first, and then the clicked tab is set to be selected. When the tab is entered or exited, the isHighlighted attribute is toggled and the changes are applied to the tab - but only is it is not already selected. If it is, the event is ignored since the tab should not change if it is already selected, and the mouse is over it.
states: [
State {
name: "selected"
PropertyChanges { target: customTab; anchors.bottomMargin: 0}
},
State {
name: "unselected"
PropertyChanges {target: customTab; anchors.bottomMargin: margin}
}
]
And lastly, the anchor.bottomMargin can not be changed from JavaScript, therefore two states had to be created. Removing the margin when the tab is selected gives the "melding" effect, when the white line between the selected tab and the rest of the control disappears, and appears again when a different tab is selected. The state of the tab gets toggled by the onClicked event of the MouseArea, as shown above.
Finally, the full listing of the code - it is not yet overly huge.
// The main qml file
import QtQuick 1.0
Rectangle {
id: screen
width: 490; height: 400
property int numTabs: 5
property int margin: 2
property string transparentColor : "transparent"
property string redColor: "red"
property string grayColor: "#B7B9BC" // "lightgray"
Rectangle {
id: backRect
radius: 10
width: parent.width
height: parent.height
color: grayColor
anchors.top: parent.top
anchors { leftMargin: 10; bottomMargin: 10; topMargin: 10; rightMargin:10 }
Rectangle{
id: tabsRect
radius: 10
width: parent.width
height: 80
anchors.top: parent.top
Row{
id:tabsRow
width: parent.width
height: parent.height
anchors.fill: parent
function clearState()
{
var j=1;
for(j=0;j<= numTabs-1;j++)
{
children[j].isSelected = false;
children[j].state = "unselected";
children[j].applyState();
}
}
CustomTab{
id: customTab0
ctlWidth: parent.width/numTabs
ctlHeight: parent.height - margin
tabIndex: 0
isSelected: false;
iPosition: 0
anchors { left: parent.left; bottom: parent.bottom; bottomMargin: margin }
}
CustomTab{
id: customTab1
ctlWidth: parent.width/numTabs-margin
ctlHeight: parent.height - margin
tabIndex: 1
isSelected: false;
iPosition: 1
anchors { left: customTab0.right; bottom: parent.bottom; leftMargin: margin; bottomMargin: margin; }
}
CustomTab{
id: customTab2
ctlWidth: parent.width/numTabs-margin
ctlHeight: parent.height - margin
tabIndex: 2
isSelected: false;
iPosition: 1
anchors { left: customTab1.right; bottom: parent.bottom; leftMargin: margin; bottomMargin: margin; }
}
CustomTab{
id: customTab3
ctlWidth: parent.width/numTabs-margin
ctlHeight: parent.height - margin
tabIndex: 3
isSelected: false;
iPosition: 1
anchors { left: customTab2.right; bottom: parent.bottom; leftMargin: margin; bottomMargin: margin; }
}
CustomTab{
id: customTab4
ctlWidth: parent.width/numTabs-margin
ctlHeight: parent.height - margin
tabIndex: 4
isSelected: false;
iPosition: 2
anchors { left: customTab3.right; bottom: parent.bottom; leftMargin: margin; bottomMargin: margin; }
}
}
}
}
}
// The CustomTab.qml file
import QtQuick 1.0
Rectangle {
id: customTab
clip: true
property int ctlWidth;
property int ctlHeight;
property int redLineHeight: 20;
property bool isSelected;
property bool isHighlighted;
property int tabIndex;
property int iPosition; // 0 = left; 1 = middle; 2 = right
width: ctlWidth
height: ctlHeight
color: "#B7B9BC"
radius: 10
function applyHeights(customTabHeight, topHeight, bottomHeight)
{
customTab.height = customTabHeight
elements.children[0].height = topHeight
elements.children[1].height = topHeight
elements.children[2].height = bottomHeight
elements.children[3].height = bottomHeight
}
function clearChildState()
{
applyHeights(ctlHeight, redLineHeight, ctlHeight - redLineHeight)
}
function getCorrectColor()
{
if(isSelected)
{
return transparentColor;
}
else if(isHighlighted)
{
return redColor;
}
else
{
return grayColor;
}
}
function applyState()
{
if(isSelected)
{
applyHeights(ctlHeight + redLineHeight/2, redLineHeight, ctlHeight + redLineHeight/2)
customTab.color=redColor
}
else if(isHighlighted)
{
applyHeights(ctlHeight, redLineHeight/2, ctlHeight)
customTab.color = redColor
}
else
{
applyHeights(ctlHeight, redLineHeight, ctlHeight)
customTab.color=grayColor
}
if(iPosition == "0")
{
elements.children[0].color = transparentColor;
elements.children[1].color = getCorrectColor();
}
else if(iPosition == "1")
{
elements.children[0].color = getCorrectColor();
elements.children[1].color = getCorrectColor();
}
else if(iPosition == "2")
{
elements.children[0].color = getCorrectColor();
elements.children[1].color = transparentColor;
}
}
Component.onCompleted: {
applyState();
}
Grid{
id:elements
width: parent.width
height: parent.height
columns: 2
rows: 2
Rectangle{id: topLeft; color: grayColor; width: parent.width/2; height: redLineHeight;}
Rectangle{id: topRight; color: grayColor; width: parent.width/2; height: redLineHeight;}
Rectangle{id: bottomLeft; color: grayColor; width: parent.width/2; height: parent.height - redLineHeight;}
Rectangle{id: bottomRight; color: grayColor; width: parent.width/2; height: parent.height - redLineHeight;}
}
MouseArea{
hoverEnabled: true
anchors.fill: parent
onClicked: {
parent.parent.clearState();
parent.isSelected = true;
parent.state = "selected"
parent.applyState();
}
onEntered: {
parent.isHighlighted = true;
if(!parent.isSelected)
{
parent.applyState();
}
}
onExited: {
parent.isHighlighted = false;
if(!parent.isSelected)
{
parent.applyState();
}
}
}
states: [
State {
name: "selected"
PropertyChanges { target: customTab; anchors.bottomMargin: 0}
},
State {
name: "unselected"
PropertyChanges {target:customTab; anchors.bottomMargin: margin}
}
]
}
Tabs Just Loaded, None Selected
Tab Selected, Another Tab Highlighted
References
QML StatesQML PropertyChanges ElementAnchor-based Layout in QMLJavaScript Functionsby
Evgeny. Also posted on
my website