首页
GitHub
实现图像区域选择并裁剪 (2)

# 介绍

接上篇可缩放边的Rectagle用于裁剪图片,实现从已选择的区域截取图像。

# 实现效果

demo

# 可缩放拖拽区域组件

接上篇实现图像区域选择并裁剪 (1)已经实现的区域选择做成独立的组件: SelectionArea.qml

// SelectionArea.qml
import QtQuick 2.12

Rectangle {
    id: selectionRect

    property bool squared: false
    property int rulerSize: 12
    property color rulerColor: "steelblue"
    property int minimumSize: 50
    property int maximumSize: Math.min(parent.width, parent.height)

    color: "#354682B4"
    border {
        width: 2
        color: rulerColor
    }

    MouseArea {
        id: dragMouseArea
        anchors.fill: parent
        drag{
            target: parent
            minimumX: 0
            minimumY: 0
            maximumX: parent.parent.width - parent.width
            maximumY: parent.parent.height - parent.height
            smoothed: true
        }

        onDoubleClicked: {
            parent.destroy() // destroy component
        }
    }

    Repeater {
        id: repeater

        //TODO: optimize this code
        readonly property var actions: {
            "left" : function(x, y) {
                selectionRect.width = selectionRect.width - x
                selectionRect.x = selectionRect.x + x
                if(selectionRect.width < minimumSize)
                    selectionRect.width = minimumSize
                if(selectionRect.width > maximumSize)
                    selectionRect.width = maximumSize

                if(squared) {
                    selectionRect.height = selectionRect.width
                }
            },
            "right" : function(x, y) {
                selectionRect.width = selectionRect.width + x
                if(selectionRect.width < minimumSize)
                    selectionRect.width = minimumSize
                if(selectionRect.width > maximumSize)
                    selectionRect.width = maximumSize

                if(squared) {
                    selectionRect.height = selectionRect.width
                }
            },
            "top" : function(x, y) {
                selectionRect.height = selectionRect.height - y
                selectionRect.y = selectionRect.y + y
                if(selectionRect.height < minimumSize)
                    selectionRect.height = minimumSize
                if(selectionRect.height > maximumSize)
                    selectionRect.height = maximumSize

                if(squared) {
                    selectionRect.width = selectionRect.height
                }
            },
            "bottom" : function(x, y) {
                selectionRect.height = selectionRect.height + y
                if(selectionRect.height < minimumSize)
                    selectionRect.height = minimumSize
                if(selectionRect.height > maximumSize)
                    selectionRect.height = maximumSize

                if(squared) {
                    selectionRect.width = selectionRect.height
                }
            },
            "lt" : function(x, y) {
                repeater.actions["left"](x, y);
                if(!squared) {
                    repeater.actions["top"](x, y);
                }
            },
            "lb" : function(x, y) {
                repeater.actions["left"](x, y);
                if(!squared) {
                    repeater.actions["bottom"](x, y);
                }
            },
            "rt" : function(x, y) {
                repeater.actions["right"](x, y);
                if(!squared) {
                    repeater.actions["top"](x, y);
                }
            },
            "rb" : function(x, y) {
                repeater.actions["right"](x, y);
                if(!squared) {
                    repeater.actions["bottom"](x, y);
                }
            },
        }

        model: [
            // edge rulers
            { horizontal: parent.left, vertical: parent.verticalCenter, axis: Drag.XAxis, callback: "left" },
            { horizontal: parent.right, vertical: parent.verticalCenter, axis: Drag.XAxis, callback: "right" },
            { horizontal: parent.horizontalCenter, vertical: parent.top, axis: Drag.YAxis, callback: "top" },
            { horizontal: parent.horizontalCenter, vertical: parent.bottom, axis: Drag.YAxis, callback: "bottom" },
            // corner rulers
            { horizontal: parent.left, vertical: parent.top, axis: Drag.YAxis | Drag.XAxis, callback: "lt" },
            { horizontal: parent.left, vertical: parent.bottom, axis: Drag.YAxis | Drag.XAxis, callback: "lb" },
            { horizontal: parent.right, vertical: parent.top, axis: Drag.YAxis | Drag.XAxis, callback: "rt" },
            { horizontal: parent.right, vertical: parent.bottom, axis: Drag.YAxis | Drag.XAxis, callback: "rb" },
        ]
        delegate: Rectangle {
            width: rulerSize
            height: rulerSize
            radius: rulerSize
            color: rulerColor
            anchors.horizontalCenter: modelData.horizontal
            anchors.verticalCenter: modelData.vertical

            MouseArea {
                anchors.fill: parent
                drag{ target: parent; axis: modelData.axis }
                onPositionChanged: {
                    if(drag.active) {
                        if(typeof repeater.actions[modelData.callback] === "function")
                            repeater.actions[modelData.callback](mouseX, mouseY)
                    }
                }
            }
        }
    }// Repeater
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145

# 建立裁剪对话框

建立一个裁剪工作的对话框组件ImageCropDialog.qml。对话框上放置一个用于指定大小的Rectangle,作为操作图片的工作viewport(视口),如显示,缩放,旋转等。Image组件的填充模式(fillMode)应设置为Image.Pad以填充整个Image组件,让图片完全显示,并且方便计算裁剪的坐标映射到源图片。

// ImageCropDialog.qml
import QtQuick 2.12
import QtQuick.Controls 2.12

Dialog {
    id: control

    property rect selectedRect
    property alias source: imageItem.source
    property var selection: undefined

    function createSelection()
    {
        if(!selection) {
            // centre in parent
            var ajustWidth = Math.min(parent.width, parent.height) / 2;
            var properties = {
                "width": ajustWidth, "height": ajustWidth,
                "x": (viewport.width - ajustWidth) / 2, "y": (viewport.height - ajustWidth) / 2
            }
            selection = selectionComponent.createObject(viewport, properties)
        }
    }

    title: qsTr("Crop Image Dialog")
    modal: true
    padding: 0
    standardButtons: Dialog.Apply | Dialog.Ok | Dialog.Cancel
    closePolicy: Popup.CloseOnEscape
    onApplied: {
        // mapping parent coordinate to current coordinate
        selectedRect = imageItem.mapFromItem(viewport, selection.x, selection.y,selection.width, selection.height);
    }

    Component {
        id: selectionComponent
        SelectionArea {
            id: selectionRect
            squared: true
        }
    }

    // iamge viewport
    Rectangle {
        id: viewport
        anchors.fill: parent
        color: "black"

        Image {
            id: imageItem

            // zoom in or zoom out
            scale: 1 / (sourceSize.height / parent.height)
            fillMode: Image.Pad
            anchors.centerIn: parent
            onStatusChanged: {
                if(status === Image.Ready) {
                    createSelection();
                }
            }
            onSourceChanged: {
                // reset selection area
                if(selection) {
                    selection.destroy();
                    selection = undefined;
                }
            }

            MouseArea {
                anchors.fill: parent
                onClicked: {
                    if(imageItem.status === Image.Ready) {
                        createSelection();
                    }
                }
            }
        } // Image item
    } // viewport
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

# 建立c++裁剪类

建立一个c++图片帮助类ImageHelper,增加一个裁剪函数crop,非常简单,只传入一个图片文件名和一个裁剪的大小作为参数。并使用jsRegisterSingletonType注册单例对象到QML。

// ImageHelper.h
#ifndef IMAGEHELPER_H
#define IMAGEHELPER_H

#include <QObject>
#include <QCryptographicHash>
#include <QImage>
#include <QDateTime>
#include <QDir>
#include <QUrl>

class ImageHelper : public QObject
{
    Q_OBJECT
public:
    // ...

    // return cropped image filename, return empty string if failed
    Q_INVOKABLE QUrl crop(const QUrl &url, const QRect &rect)
    {
        QUrl resultUrl;
        if(url.isEmpty())
            return resultUrl;
        QString fileName = url.isLocalFile() ? url.toLocalFile() : url.toString();

        QImage original(fileName);
        if(original.isNull())
            return resultUrl;

        // do crop image
        QImage cropped = original.copy(rect);

        // using the current timestamps as the filename
        qint64 ms = QDateTime::currentMSecsSinceEpoch();
        QByteArray md5 = QCryptographicHash::hash(QByteArray().setNum(ms), QCryptographicHash::Md5);

        // save to temp directory
        QDir tempDir = QDir::current();
        if(!tempDir.cd(QStringLiteral("temp")))
        {
            tempDir.mkdir(QStringLiteral("temp"));
            tempDir.cd(QStringLiteral("temp"));
        }
        // no suffix
        QString name = tempDir.path() + "/" + QString(md5.toHex());
        cropped.save(name, "PNG");
        resultUrl = QUrl::fromLocalFile(name);

        return resultUrl;
    }

    // other member functions
    // ...
};

#endif // IMAGEHELPER_H

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 注册单例对象到QML

qmlRegisterSingletonType<ImageHelper>("App", 1, 0, "ImageHelper",
                                    [](QQmlEngine *, QJSEngine *) -> QObject* {
    return new ImageHelper;
});
1
2
3
4

注册的模块名在本例中为App,版本号1.0。注册的对象名ImageHelper,在QML使用ImageHelper访问该对象使用Q_INVOKABL修饰的成员函数或public slot修饰的槽。

# QML中调用方式

在main.qml组件中,组装必要的组件来完成整个程序的功能:从磁盘中打开图片,显示到UI,选择裁剪区域,应用裁剪,显示裁剪结果。

// main.qml
import QtQuick 2.12
import QtQuick.Controls 2.12
import QtQuick.Dialogs 1.3
import App 1.0 // !!import module

ApplicationWindow {
    id: appWindow
    title: qsTr("Window")
    width: 640
    height: 480
    visible: true

    // select a image from disk
    FileDialog {
        id: appImagesSelectDialog

        modality: Qt.WindowModal
        title: qsTr(""Choose a image"")
        selectMultiple: false
        selectFolder: false
        nameFilters: [ "Image files (*.jpeg *.png *.jpg *.gif)", "All files (*)" ]
        selectedNameFilter: nameFilters[0]
        sidebarVisible: true
        onAccepted: {
            // open image crop dialog
            cropDialg.open();
            cropDialg.source = fileUrls[0];
        }
    }

    ImageCropDialog {
        id: cropDialg
        width: parent.width
        height: parent.height
        onApplied: {
            var croppedFile = ImageHelper.crop(source, selectedRect);
            // apply crop result
            source = Qt.resolvedUrl(croppedFile);
        }
        onAccepted: {
            // show result
            image.source = cropDialg.source;
        }
    }

    Column {
        anchors.centerIn: parent
        // show the result image
        Rectangle {
            width: 256; height: 256
            border.color: "blue"; border.width: 1
            Image { id: image; anchors.fill: parent }
        }
        Button {
            text: qsTr("Choose a image")
            onClicked: appImagesSelectDialog.open();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 注册QML类型

最后,在main.cpp中启动应用程序,在加载main.qml之前注册ImageHelper类型到QML,本例子中注册为单例,当然也可以注册为QML组件类型。

qmlRegisterType<ImageHelper>("App", 1, 0, "ImageHelper");
// main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>

// include crop helper
#include "ImageHelper.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    // register image crop helper
    qmlRegisterSingletonType<ImageHelper>("App", 1, 0, "ImageHelper",
                                          [](QQmlEngine *, QJSEngine *) -> QObject* {
        return new ImageHelper;
    });

    QQmlApplicationEngine engine;
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 结束

  • 待续...