I am trying to add zoom controls to my image. I tried to start as simple as possible, but anything I try ends up clipping the image.
The image is loaded in a part of the screen, and I want to place it in a Flickable
, to be able to zoom and pan. I will add pinch and mouse wheel later, for now I have buttons to set various scale values.
The problem: when zoomed, the image is shifted and clipped. The shift is bothersome, because the zoomed image being bigger than the container, why show the empty space ? But the clipping is much worse, and I don't understand it. Seems the shift is as big as the clip (maybe?).
I tried to change the transformOrigin
, not good. I tried to use transform
instead of scale
, but I saw nothing happen.
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.0
import QtQuick.Window 2.2
Window {
width: 1015; height: 550; visible: true
Item { // A bunch of buttons to easily test several zoom values and layout is similar
id: leftSideContainer; width: 100; height: parent.height
ColumnLayout {
anchors.fill: parent; Layout.alignment: Qt.AlignCenter;
Layout.preferredHeight: 40; Layout.fillWidth: true;
Button { text: "50 %"; onClicked: imageContainer.imageScale = 0.5; }
Button { text: "80 %"; onClicked: imageContainer.imageScale = 0.8; }
Button { text: "100 %"; onClicked: imageContainer.imageScale = 1; }
Button { text: "110 %"; onClicked: imageContainer.imageScale = 1.1; }
Button { text: "120 %"; onClicked: imageContainer.imageScale = 1.2; }
Button { text: "150 %"; onClicked: imageContainer.imageScale = 1.5; }
Button { text: "200 %"; onClicked: imageContainer.imageScale = 2; }
Button { text: "400 %"; onClicked: imageContainer.imageScale = 4; }
Button { text: "landscape"; onClicked: { paintedCanvasImage.source = ""; paintedCanvasImage.source = "/examples/landscape.jpg"; } }
Button { text: "portrait"; onClicked: { paintedCanvasImage.source = ""; paintedCanvasImage.source = "/examples/portrait.jpg"; } }
Button { text: "square"; onClicked: { paintedCanvasImage.source = ""; paintedCanvasImage.source = "/examples/square.jpg"; } }
}
}
Item { // Using separate variable and item a bit like my code
id: imageContainer; property var imageScale: 1;
anchors { left: leftSideContainer.right; right: parent.right;
top: parent.top; bottom: parent.bottom; }
Rectangle { // Easy to show background compared to actual image, no other use
anchors.fill: parent; color: "lightgreen"; }
onImageScaleChanged: paintedCanvasImage.scale = imageScale;
Flickable { // I want to pan
id: flickImage
anchors.fill: parent
contentWidth: paintedCanvasImage.paintedWidth * paintedCanvasImage.scale
contentHeight: paintedCanvasImage.paintedHeight * paintedCanvasImage.scale
//contentX: ??? I have no idea
clip: true // required
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar { active: true; policy: ScrollBar.AsNeeded; }
ScrollBar.horizontal: ScrollBar { active: true; policy: ScrollBar.AsNeeded; }
Image {
id: paintedCanvasImage
width: imageContainer.width; height: imageContainer.height
source: "/examples/landscape.jpg" // Trying square, landscape or portrait
fillMode: Image.PreserveAspectFit // required
transformOrigin: Item.TopLeft // With this, weirdly the vertical fits correctly, horizontal only shifted/clipped
}
}
}
}
Sample run: I will show 100, and 200% top-left and bottom-right, to show clipping and empty area. Note that with transformOrigin: Item.TopLeft
the image fits vertically (if the container is more landscape-y than the image, or horizontally otherwise) - but is shifted by some amount that I don't know, to the right:
I cannot figure out what the transformOrigin
should be, or how to use transform
instead of scale
, or what the flickable's origin must be.
Changing contentX
I thought would fix it, but it doesn't - something about the transform must be needed.
Platform: windows as well as embedded linux;
Qt version: 5.12
There are a few mistakes in your code that are causing the problem.
First, the clipping issue is due to the absence of transformOrigin: Item.TopLeft
, which causes the origin to be the center by default and makes your image scale in negative x and y.
The shifting issue is caused by the paintedWidth
value, which, according to the documentation, can be less than the image item.
Additionally, using fillMode: Image.PreserveAspectFit
will always result in empty spaces around your image if its ratio doesn't fit your container
.
For the solution, I made a few more changes so that you can easily use the wheel or pinch handler.
In the following code, I added a fitSize
property which stores the initial size of the image, and it also calculates the aspect ratio using the sourceSize
of the image. I also removed the scaling part of the image to be able to use flickable.resizeContent
.
By multiplying fitSize
with iscale
, you can get new sizes.
It may not be best practice, but it works well, and I think you could also use a PinchHandler
instead of WheelHandler
.
import QtQuick 2.12
import QtQuick.Controls 2.12
Page {
id: root
implicitWidth: 550
implicitHeight: 300
contentItem: Control {
id: container
property real iscale: 1
property size fitSize: {
const ss = image.sourceSize;
const ratio = ss.width / ss.height;
return ratio < width/height ? Qt.size(height * ratio, height) : Qt.size(width, width / ratio);
}
contentItem: Flickable {
id: flickable
clip: true
leftMargin: Math.max(0, width - contentWidth) / 2 // Centering the content
topMargin: Math.max(0, height - contentHeight) / 2
contentWidth: container.fitSize.width
contentHeight: container.fitSize.height
rebound: Transition {}
boundsBehavior: Flickable.StopAtBounds
ScrollBar.vertical: ScrollBar {}
ScrollBar.horizontal: ScrollBar {}
Image {
id: image
width: flickable.contentWidth
height: flickable.contentHeight
source: "path/to/image.jpg"
onSourceChanged: container.iscale = 1
MouseArea {
hoverEnabled: true
anchors.fill: parent
onWheel: function(e) {
const { width, height } = container.fitSize;
const mousePos = Qt.point(mouseX, mouseY);
container.iscale += e.angleDelta.y / 1200;
flickable.resizeContent(width * container.iscale, height * container.iscale, mousePos);
flickable.returnToBounds();
}
}
}
}
background: Rectangle { color: "#121314" }
}
}