3

I want to create a rectangle with inner shadow in QML, something similar with what Photoshop does:

enter image description here

QML has InnerShadow but I'm unable to achieve this effect. The closest I got was this

import QtQuick 2.0
import QtGraphicalEffects 1.0

Item {
    id: root
    width: 300
    height: 300

    Rectangle {
        id: myRectangle
        anchors.centerIn: parent
        width: 100
        height: 100
        color: "grey"
    }

    InnerShadow {
        anchors.fill: root
        cached: true
        horizontalOffset: 0
        verticalOffset: 0
        radius: 16
        samples: 32
        color: "#b0000000"
        smooth: true
        source: root
    }
}

which is an idea I got from this post. However this example only works if root has a significantly bigger size than myRectangle and I don't want that. I need e.g. a 200x10 square in which shadow is uniformly spread across the edges of the rectangle. I tried all kinds of values for the InnerShadow properties but I couldn't get even close to the effect I want.

Can this be achieved using QML?

Community
  • 1
  • 1
Jacob Krieg
  • 2,834
  • 15
  • 68
  • 140
  • Looks like QML's inner shadow implementation is pretty lame, my guess is "done by a programmer with zero background in graphics design". You could try to achieve the effect with a custom GLSL shader. – dtech Apr 08 '15 at 15:00
  • You just need an offset of two pixels to obtain the correct inner shadow, which is not "significantly bigger" BUT is surely ugly. Combining that with a color for the root does not work? If not, shaders are your only friends. – BaCaRoZzo Apr 08 '15 at 17:29
  • 1
    I repeat myself with a picture since you didn't answer to my previous proposal. What about [something like this](http://i59.tinypic.com/sqgrcx.png)? – BaCaRoZzo Apr 08 '15 at 22:13
  • @BaCaRoZzo How did you achieve that effect? I used `horizontalOffset: 1; verticalOffset: 1` and this is what I achieved: http://i.imgur.com/ltPZT9r.png which is not nearly as good as your result. Can you show the code? – Jacob Krieg Apr 08 '15 at 22:56
  • Solution written. See if it fits your needs. – BaCaRoZzo Apr 08 '15 at 23:24

3 Answers3

2

The "correct" approach - quotation marks needed - to use the effect should be this one:

import QtQuick 2.0
import QtGraphicalEffects 1.0

Item {
   id: root
   width: 300
   height: 300

   Item {
       id: src
       anchors.fill: parent

       Rectangle {
           id: myRectangle
           anchors.centerIn: parent
           width: 100
           height: 100
           color: "grey"
       }
   }

   InnerShadow {
       anchors.fill: src
       cached: true
       horizontalOffset: 0
       verticalOffset: 0
       radius: 16
       samples: 32
       color: "#b0000000"
       smooth: true
       source: src
   }
}

As you can see it is slightly different from the propose solution in the other question. Using this code you still need to left 2 pixels, to have the effect, which results in a white border (or whatever is the background color). The issue can be easily solved by changing the root to be a Rectangle.

Final example solution follows. Clearly you can extract the root component (and related children) and place it in a Component or a different .qml file for later usage.

import QtQuick 2.4
import QtQuick.Window 2.2
import QtGraphicalEffects 1.0

Window {
    width: 200
    height: 20
    visible: true

    Rectangle {     // was Item
        id: root
        anchors.fill: parent
        color: "grey"


        Item {
            id: src
            anchors.fill: parent

            Rectangle {
                id: myRectangle
                anchors.centerIn: parent
                width: root.width - 2
                height: root.height - 2
                color: "lightgrey"
            }
        }

        InnerShadow {
            anchors.fill: src
            cached: true
            horizontalOffset: 0
            verticalOffset: 0
            radius: 16
            samples: 32
            color: "#b0000000"
            smooth: true
            source: src
        }
    }
}

The resulting window for the final code example:

enter image description here

BaCaRoZzo
  • 7,502
  • 6
  • 51
  • 82
  • 1
    Seems like the implementation is illogical. It doesn't work with offset 0 if the source is myRectangle, but mysteriously appears if you add offset. So you have to use a "proxy" item. And it looks like it gets increasingly awkward if you try to do something more complex like apply it to a "compound" item. – dtech Apr 08 '15 at 23:28
  • 1
    Never said I like it...indeed I put the infamous quotation marks around "correct". The original code was proposed as the way to use the effect in a discussion to the mailing list. You can find the link in the other question comments. The implementation MUST be illogical to foster such an odd usage. But, you know, as illogical as it is, we should still find a way. :) – BaCaRoZzo Apr 08 '15 at 23:58
  • 1
    I'd much rather prefer "expected behavior". Which would be the case of an implementation, done by someone who knows how to use say photoshop. Unfortunately, with Qt, development is highly compartmentalized, resulting in many oddities in APIs that are not "all about programming". – dtech Apr 09 '15 at 00:00
  • 1
    @BaCaRoZzo Illogical or not, it works. And till the Qt guys fix the `InnerShadow` (because I don't consider the current behavior to be correct) I don't think there is a better solution. This solution shouldn't keep the Qt guys or the community away from fixing it though...Many thanks! – Jacob Krieg Apr 09 '15 at 07:32
1

The InnerShadow element is like most of the Graphics Effects elements in QML a GLSL shader, and the way it find edges is by looking for a transition between transparent and non-transparent. If you apply the filter to a completely solid graphics element it wouldn't find any edges, and therefore no shadows. This is exactly the same way the Photoshop filter works, it also finds the edge by scanning for edges from transparent to non-transparent (as in the example you supplied). It could have treated the edges of the graphics area as implicit edges, but that would limit is usability considerably. Here's the source for for InnerShadow. Which again uses FastInnerShadow or GaussianInnerShadow depending on the fast attribute.

If you want an implementation that you can just add on to existing elements without having to care about having transitions between transparent and non-transparent edges you could use this:

Rectangle {
    id: myRectangle
    anchors.centerIn: parent
    width: 300
    height: 300
    color: "#AAAAAA"
}
Item {
    id: _innerShadow
    property var shadowSource: myRectangle
    property var color: "#B0000000"
    anchors.fill: shadowSource
    Item {
        id: _shadowMarker
        //width: source.width+1; height: source.height+1
        anchors.fill: parent;
        anchors.margins: -10
        ColorOverlay {
            anchors.fill: _shadowMarker;
            anchors.margins: 10
            source: _innerShadow.shadowSource
            color: "#00000000"
        }
        visible: false
    }
    InnerShadow {
        anchors.fill: _shadowMarker
        cached: true
        horizontalOffset: 0
        verticalOffset: 0
        radius: 16
        samples: 32
        color: _innerShadow.color
        smooth: true
        source: _shadowMarker
    }
}
1337user
  • 56
  • 2
1

Here was my solution

GoodInnerShadow.qml

///
/// Same as InnerShadow QML type, with the following differences
///
/// InnerShadow requires transparent space to be surrounding the 
/// item that you want to make an inner shadow for. GoodInnerShadow
/// does not require this.
///
/// InnerShadow draws the source with the shadow. GoodInnerShadow 
/// draws just the shadow
///
import QtQuick 2.15
import QtGraphicalEffects 1.0

Item {
    id: root

    anchors.centerIn: source
    width: source.width
    height: source.height
    required property var source
    property color color: "#50ffffff"
    property double radius: 12
    property double spread: .8

    Item{
        id: sourceMaskWithPadding
        visible: false

        anchors.centerIn: parent
        width: root.source.width + shadowOfInverse.samples * 2
        height: root.source.height + shadowOfInverse.samples * 2
        OpacityMask {
            id: sourceMask
            anchors.centerIn: parent
            width: root.source.width
            height: root.source.height
            maskSource: root.source
            source: root.source
        }
    }
    
    Rectangle {
        id: coloredRect
        visible: false

        color: root.color
        anchors.fill: sourceMaskWithPadding
    }

    OpacityMask {
        id: sourceInverse
        visible: false

        anchors.fill: coloredRect
        source: coloredRect
        maskSource: sourceMaskWithPadding
        invert: true
    }

    DropShadow {
        id: shadowOfInverse
        visible: false
        
        anchors.fill: sourceInverse
        source: sourceInverse
        radius: root.radius
        samples: radius * 2 + 1
        color: root.color
        spread: root.spread
    }
    
    OpacityMask {
        id: sourceInnerShadow
        anchors.fill: sourceMaskWithPadding
        maskSource: sourceMaskWithPadding
        source: shadowOfInverse
    }

}

Sample Usage

import QtQuick 2.15

Item {
    width: 400
    height: 300

    Rectangle {
        id: myRect
        anchors.centerIn: parent
        width: 300
        height: 100
        color: "lightgray"
    }

    GoodInnerShadow {
        source: myRect
        color: "#aa000000"
        spread: .5
        radius: 16
    }
}

Result

enter image description here