luffy
luffy

Reputation: 2388

Textfield with an icon and placeholder text

is there a way to get a textfield to display the placeholder text next to an icon like in the picture below.

I tried doing it by setting the padding, but that only works for the actual text not the placeholder text.

import QtQuick
import QtQuick.Controls

TextField {
    property alias iconSource: image.source

    selectByMouse: true
    leftPadding: iconSource.toString() ? 32 : 16

    Image {
        id: image
        anchors.verticalCenter: parent.verticalCenter
        width: parent.height * 0.5
        height: width
        sourceSize.width: width
        sourceSize.height: height
    }
}

It looks ok if the textfield is focused. If it is out of focus, the text is overlapped by the icon (see password field):

enter image description here

But I want it to look like this:

enter image description here

Upvotes: 2

Views: 151

Answers (3)

smr
smr

Reputation: 1004

The TextField in Material style uses Material.textFieldHorizontalPadding (source code). And since textFieldHorizontalPadding is read-only and there is no alias defined for placeholder, you cannot change the placeholder's x position directly.

There are two options here:

  • You can either copy and paste the source code from material/TextField.qml and modify it, as shown below:

    T.TextField {
        // ...
    +   property real holderPadding: control.Material.textFieldHorizontalPadding
        // ...
        FloatingPlaceholderText {
            // ...
    +       x: control.holderPadding
    -       x: control.Material.textFieldHorizontalPadding
            // ...
        }
    }
    
  • Or, you can access the placeholder using the children property (or visibleChildren).

    The current placeHolder object is located at visibleChildren[0]. By using Binding, you can bind the placeholder's x position to a custom property (e.g., holderPadding). You can then change that property based on the TextField's activeFocus. The changes can be seen in the following code:

    TextField {
        id: input
        property alias iconSource: image.source
    
        property real spacing: 5
        property real holderPadding: activeFocus ? Material.textFieldHorizontalPadding : leftPadding
    
        Binding { target: input.visibleChildren[0]; property: 'x'; value: input.holderPadding }
        Behavior on holderPadding { NumberAnimation {} }
    
        selectByMouse: true
        height: 40; width: 250
        placeholderText: 'placeholder'
        leftPadding: image.x + image.width + spacing
    
        Image {
            id: image
            x: 10; y: (parent.height - height) / 2
            height: width; width: parent.height * 0.5
            source: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 483.1 482.7'%3E%3Cpath fill='%235f6469' d='M93 364.4c88.9-67.4 208-67 296.8 0 105-124.6 15.1-316.6-148.4-316.1C78.2 47.6-11 239.7 93 364.4zm148.4-99c-75.2 1.5-112.9-92.4-59.7-144.2 31.5-32.5 88.8-33.1 120.1 0 52.2 51.8 15.1 147-60.4 144.2zm0 217.2a239.3 239.3 0 0 1-222-147.2C-48.1 176.9 68.9.4 241.4 0A241.7 241.7 0 0 1 464 335.4c-36.9 87.7-126.3 148.9-222.6 147.2zm0-48.2a180 180 0 0 0 112.2-36.2A190.4 190.4 0 0 0 241.4 362c-41-.2-79.7 13-112.2 36.2 32.3 23.9 71 37.2 112.2 36.2zm0-217.2a35.1 35.1 0 0 0 36.2-36.2 35.1 35.1 0 0 0-36.2-36.2 35.3 35.3 0 0 0-36.2 36.2 35.1 35.1 0 0 0 36.2 36.2z'/%3E%3C/svg%3E"
            sourceSize{ width: 50; height: 50 }
        }
    }
    

Note
There are also some issues if you want to use it with RTL, which you can fix by adding some conditions.

Upvotes: 2

Stephen Quan
Stephen Quan

Reputation: 26214

The pattern I follow is

Frame {
     RowLayout {
         /* IconButton */
         TextField { }
         /* IconButton */
     }
}

To make this work, I make use of the fact that:

  1. Most components have a background property that can be:
  • be turned off, e.g. background: Item { }
  • be turned on and customized, e.g. background: Rectangle { radius: height / 2 }
  1. Most components have an icon property that can be set to a SVG icon

Butting it altogether we can complete the component as follows:

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
    background: Rectangle { color: "#848895" }
    Frame {
        width: parent.width
        background: Rectangle { radius: height / 2 }
        RowLayout {
            width: parent.width
            IconButton { icon.source: "lock.svg" }
            TextField {
                Layout.fillWidth: true
                background: Item { }
                placeholderText: "Password"
            }
            IconButton { icon.source: "view.svg" }
        }
    }
}

// IconButton.qml
import QtQuick
import QtQuick.Controls
Item {
    width: 32
    height: 32
    clip: true
    property alias icon: btn.icon
    property alias button: btn
    Button {
        id: btn
        anchors.centerIn: parent
        background: Item { }
        icon.width: 32
        icon.height: 32
        icon.color: "grey"
    }
}

// user.svg : https://raw.githubusercontent.com/Esri/calcite-ui-icons/master/icons/user-32.svg
// lock.svg : https://raw.githubusercontent.com/Esri/calcite-ui-icons/master/icons/lock-32.svg
// view.svg : https://raw.githubusercontent.com/Esri/calcite-ui-icons/master/icons/view-visible-32.svg

You can Try it Online!

Upvotes: 0

Atmo
Atmo

Reputation: 3967

What you are trying to achieve is documented on the Customizing Qt Quick Controls help page: you need to set the background property of your TextField to some rectangle and bind that rectangle's radius.

Below is a demonstration of that. I am going to assume you defined your applications colors in a qtquickcontrols2.conf file such as this one:

[Controls]
Style=Universal

[Universal]
Accent="#0267ff"

[Material]
Theme=Light
Accent=Blue
Primary=Gray

Your control becomes somethings in the lines of:

TextField {
    id: textField
    verticalAlignment: Text.AlignVCenter
    leftPadding: image.x + image.width + 2
    placeholderText: qsTr("User name")
    background: Rectangle {
        anchors.fill: parent
        border.color: textField.activeFocus ? Material.accent : Material.primary
        border.width: 1
        radius: textField.height / 2
    }
    Image {
        id: image
        x: (textField.height - width) / 2 /* Set the center of the image to be the same as the center of the rounded border */
        y: 5
        width: textField.height - 10
        height: textField.height - 10
        source: "../account_circle.svg"
        fillMode: Image.PreserveAspectFit
    }
}

which renders like so (without and with focus respectively):
User field without focus User field with focus


With Material style

Since your screenshot seem to show the placeholder text moves when the field has active focus, you must have noticed the above code does not force the border to open up for it to fit.

Solving this issue requires a lot more work, because of the following items:

  • The background must be changed to a Shape as there is not way to open a Rectangle.
  • The placeholder text moves slightly above the border, meaning we must play with insets and margins.
  • The opening of the path is done in an animation which, to the best I can see, has default duration / easing curve, but if you feel it does not open fast enough in the solution below, you will have to adjust it.
  • How much the path must open is supposed to be calculated but I have hardcoded a number in the solution below (calculating the length in pixels of a text was not the topic of the question anyway; I assume you know how to do it or will look for past SO questions on the topic).

RoundedBorderShape.qml

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Shapes 1.8

Shape {
    property int placeholderLeft: 0
    property int placeholderLength: 0
    property color fill: "transparent"
    property real pathExtraLength: parent.activeFocus ? 0.0 : placeholderLength / 2

    anchors.fill: parent
    anchors.bottomMargin: -parent.topInset
    /* Add layer properties for antialiasing */
    layer {
        enabled: true
        samples: 4
    }
    HoverHandler { id: hoverHandler }
    ShapePath {
        id: shapePath
        fillColor: fill
        strokeColor: parent.activeFocus ? Material.accent : (hoverHandler.hovered ? "black" : Material.primary)
        strokeWidth: parent.activeFocus ? 1.5 : 0.75
        startX: placeholderLeft + placeholderLength - pathExtraLength
        startY: parent.topInset + 1
        Behavior on startX {
            NumberAnimation {
                duration: 250
            }
        }
        PathLine {
            x: width - (height - parent.topInset) / 2 + 1
            y: parent.topInset + 1
        }
        PathArc {
            x: width - (height - parent.topInset) / 2 + 1
            y: height - parent.topInset - 2
            radiusY: height / 2 - parent.topInset - 2
            radiusX: radiusY
        }
        PathLine {
            x: (height - parent.topInset) / 2 - 1
            y: height - parent.topInset - 2
        }
        PathArc {
            x: (height - parent.topInset) / 2 - 1
            y: parent.topInset + 1
            radiusY: height / 2 - parent.topInset - 2
            radiusX: radiusY
        }
        PathLine {
            x: placeholderLeft + pathExtraLength
            y: parent.topInset + 1
            Behavior on x {
                NumberAnimation {
                    duration: 250
                }
            }
        }
    }
}

The text field becomes (you may have to force the size to something that matches the Material style):

TextField {
    id: textField
    topInset: 5
    bottomInset: -5
    verticalAlignment: Text.AlignVCenter
    leftPadding: image.x + image.width + 2
    placeholderText: qsTr("User name")

    background: RoundedBorderShape {
        placeholderLeft: 0.8 * textField.height
        placeholderLength: 75 /* Hardcoded, to be replaced by actual text length in pixels */
    }
    Image {
        id: image
        x: (textField.height - width) / 2 /* Set the center of the image to be the same as the center of the rounded border */
        y: 5 + textField.topInset
        width: height
        height: textField.height - textField.topInset - 10
        source: "../account_circle.svg"
        fillMode: Image.PreserveAspectFit
    }
}

The rendering, next to another text field rendered natively by QML, becomes: Rounded Material text field

(Animations are in fact smoother, the above gif has limited FPS).

PS: I did not demonstrate it but I added a property called "fill" to let you turn the field background color to white, as per your screenshot.

Upvotes: 1

Related Questions