RESTful API 客户端¶
如何创建RESTful API QML客户端的示例。
此示例展示了如何使用一个虚构的调色板服务创建一个基本的QML RESTful API客户端。该应用程序与选定的服务器进行RESTful通信,以请求和发送数据。REST服务作为一个QML元素提供,其子元素封装了服务器提供的各个JSON数据API。
应用程序功能¶
该示例提供以下基本功能: * 选择要与之通信的服务器 * 列出用户和颜色 * 登录和注销用户 * 修改和创建新颜色
服务器选择¶
在启动时,应用程序会显示调色板服务器用于通信的选项。预定义的选项有:
https://reqres.in
,一个公开可用的REST API测试服务一个基于Qt的REST API服务器示例在
QtHttpServer
一旦选择,RESTful API 客户端会向颜色 API 发出测试 HTTP GET 请求,以检查服务是否可访问。
两个预定义的API选项之间的一个主要区别是,基于Qt的REST API服务器示例是一个有状态的应用程序,允许修改颜色,而reqres.in
是一个无状态的API测试服务。换句话说,当使用reqres.in
后端时,修改颜色不会产生持久的影响。
用户和颜色是服务器端的分页资源。这意味着服务器以称为页面的块提供数据。UI列表反映了这种分页,并在页面上查看数据。
在UI上查看数据是通过标准的QML视图完成的,其中模型是从服务器接收的JSON数据表示的QAbstractListModel派生类。
登录是通过登录弹窗提供的登录功能进行的。在底层,登录会发送一个HTTP POST请求。在接收到成功的响应后,授权令牌会从响应中提取出来,然后用于后续需要令牌的HTTP请求中。
编辑和添加新颜色是在弹出窗口中完成的。请注意,将颜色更改上传到服务器需要用户已登录。
REST实现¶
该示例展示了一种从单个资源元素组合REST服务的方法。在这个示例中,资源是分页的用户和颜色资源以及登录服务。这些资源元素通过基础URL(服务器URL)和共享的网络访问管理器绑定在一起。
REST服务的基础是RestService QML元素,其子项构成了实际的服务。
在实例化时,RestService 元素会遍历其子元素并设置它们以使用相同的网络访问管理器。这样,各个资源共享相同的访问详细信息,例如服务器 URL 和授权令牌。
实际的通信是通过一个实现了某些便利功能的REST访问管理器来完成的,该管理器专门处理HTTP REST API,并有效地处理发送和接收所需的QNetworkRequest
和QNetworkReply
。

# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from PySide6.QtCore import QObject
from PySide6.QtQml import QmlAnonymous
QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1
@QmlAnonymous
class AbstractResource(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.m_manager = None # QRestAccessManager
self.m_api = None # QNetworkRequestFactory
def setAccessManager(self, manager):
self.m_manager = manager
def setServiceApi(self, serviceApi):
self.m_api = serviceApi
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
import sys
from functools import partial
from dataclasses import dataclass
from PySide6.QtCore import Property, Signal, Slot
from PySide6.QtNetwork import QHttpHeaders
from PySide6.QtQml import QmlElement
from abstractresource import AbstractResource
tokenField = "token"
emailField = "email"
idField = "id"
QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1
@QmlElement
class BasicLogin(AbstractResource):
@dataclass
class User:
email: str
token: bytes
id: int
userChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.m_user = None
self.m_loginPath = ""
self.m_logoutPath = ""
self.m_user = None
@Property(str, notify=userChanged)
def user(self):
return self.m_user.email if self.m_user else ""
@Property(bool, notify=userChanged)
def loggedIn(self):
return bool(self.m_user)
@Property(str)
def loginPath(self):
return self.m_loginPath
@loginPath.setter
def loginPath(self, p):
self.m_loginPath = p
@Property(str)
def logoutPath(self):
return self.m_logoutPath
@logoutPath.setter
def logoutPath(self, p):
self.m_logoutPath = p
@Slot("QVariantMap")
def login(self, data):
request = self.m_api.createRequest(self.m_loginPath)
self.m_manager.post(request, data, self, partial(self.loginReply, data))
def loginReply(self, data, reply):
self.m_user = None
if not reply.isSuccess():
print("login: ", reply.errorString(), file=sys.stderr)
(json, error) = reply.readJson()
if json and json.isObject():
json_object = json.object()
token = json_object.get(tokenField)
if token:
email = data[emailField]
token = json_object[tokenField]
id = data[idField]
self.m_user = BasicLogin.User(email, token, id)
headers = QHttpHeaders()
headers.append("token", self.m_user.token if self.m_user else "")
self.m_api.setCommonHeaders(headers)
self.userChanged.emit()
@Slot()
def logout(self):
request = self.m_api.createRequest(self.m_logoutPath)
self.m_manager.post(request, b"", self, self.logoutReply)
def logoutReply(self, reply):
if reply.isSuccess():
self.m_user = None
self.m_api.clearCommonHeaders() # clears 'token' header
self.userChanged.emit()
else:
print("logout: ", reply.errorString(), file=sys.stderr)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
"""PySide6 port of the Qt RESTful API client demo from Qt v6.x"""
import os
import sys
from pathlib import Path
from PySide6.QtCore import QUrl
from PySide6.QtGui import QIcon, QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from basiclogin import BasicLogin # noqa: F401
from paginatedresource import PaginatedResource # noqa: F401
from restservice import RestService # noqa: F401
import rc_colorpaletteclient # noqa: F401
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
QIcon.setThemeName("colorpaletteclient")
engine = QQmlApplicationEngine()
app_dir = Path(__file__).parent
app_dir_url = QUrl.fromLocalFile(os.fspath(app_dir))
engine.addImportPath(os.fspath(app_dir))
engine.loadFromModule("ColorPalette", "Main")
if not engine.rootObjects():
sys.exit(-1)
exit_code = app.exec()
del engine
sys.exit(exit_code)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
import sys
from dataclasses import dataclass
from PySide6.QtCore import (QAbstractListModel, QByteArray,
QUrlQuery, Property, Signal, Slot, Qt)
from PySide6.QtQml import QmlAnonymous, QmlElement
from abstractresource import AbstractResource
QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1
totalPagesField = "total_pages"
currentPageField = "page"
@dataclass
class ColorUser:
id: int
email: str
avatar: str # URL
@QmlElement
class ColorUserModel (QAbstractListModel):
IdRole = Qt.ItemDataRole.UserRole + 1
EmailRole = Qt.ItemDataRole.UserRole + 2
AvatarRole = Qt.ItemDataRole.UserRole + 3
def __init__(self, parent=None):
super().__init__(parent)
self._users = []
def clear(self):
self.set_data([])
def set_data(self, json_list):
if not self._users and not json_list:
return
self.beginResetModel()
self._users.clear()
for e in json_list:
self._users.append(ColorUser(int(e["id"]), e["email"], e["avatar"]))
self.endResetModel()
def roleNames(self):
roles = {
ColorUserModel.IdRole: QByteArray(b'id'),
ColorUserModel.EmailRole: QByteArray(b'email'),
ColorUserModel.AvatarRole: QByteArray(b'avatar')
}
return roles
def rowCount(self, index):
return len(self._users)
def data(self, index, role):
if index.isValid():
d = self._users[index.row()]
if role == ColorUserModel.IdRole:
return d.id
if role == ColorUserModel.EmailRole:
return d.email
if role == ColorUserModel.AvatarRole:
return d.avatar
return None
def avatarForEmail(self, email):
for e in self._users:
if e.email == email:
return e.avatar
return ""
@dataclass
class Color:
id: int
color: str
name: str
pantone_value: str
@QmlElement
class ColorModel (QAbstractListModel):
IdRole = Qt.ItemDataRole.UserRole + 1
ColorRole = Qt.ItemDataRole.UserRole + 2
NameRole = Qt.ItemDataRole.UserRole + 3
PantoneValueRole = Qt.ItemDataRole.UserRole + 4
def __init__(self, parent=None):
super().__init__(parent)
self._colors = []
def clear(self):
self.set_data([])
def set_data(self, json_list):
if not self._colors and not json_list:
return
self.beginResetModel()
self._colors.clear()
for e in json_list:
self._colors.append(Color(int(e["id"]), e["color"],
e["name"], e["pantone_value"]))
self.endResetModel()
def roleNames(self):
roles = {
ColorModel.IdRole: QByteArray(b'color_id'),
ColorModel.ColorRole: QByteArray(b'color'),
ColorModel.NameRole: QByteArray(b'name'),
ColorModel.PantoneValueRole: QByteArray(b'pantone_value')
}
return roles
def rowCount(self, index):
return len(self._colors)
def data(self, index, role):
if index.isValid():
d = self._colors[index.row()]
if role == ColorModel.IdRole:
return d.id
if role == ColorModel.ColorRole:
return d.color
if role == ColorModel.NameRole:
return d.name
if role == ColorModel.PantoneValueRole:
return d.pantone_value
return None
@QmlAnonymous
class PaginatedResource(AbstractResource):
"""This class manages a simple paginated Crud resource,
where the resource is a paginated list of JSON items."""
dataUpdated = Signal()
pageUpdated = Signal()
pagesUpdated = Signal()
def __init__(self, parent=None):
super().__init__(parent)
# The total number of pages as reported by the server responses
self.m_pages = 0
# The default page we request if the user hasn't set otherwise
self.m_currentPage = 1
self.m_path = ""
def _clearModel(self):
pass
def _populateModel(self, json_list):
pass
@Property(str)
def path(self):
return self.m_path
@path.setter
def path(self, p):
self.m_path = p
@Property(int, notify=pagesUpdated)
def pages(self):
return self.m_pages
@Property(int, notify=pageUpdated)
def page(self):
return self.m_currentPage
@page.setter
def page(self, page):
if self.m_currentPage == page or page < 1:
return
self.m_currentPage = page
self.pageUpdated.emit()
self.refreshCurrentPage()
@Slot()
def refreshCurrentPage(self):
query = QUrlQuery()
query.addQueryItem("page", str(self.m_currentPage))
request = self.m_api.createRequest(self.m_path, query)
self.m_manager.get(request, self, self.refreshCurrentPageReply)
def refreshCurrentPageReply(self, reply):
if not reply.isSuccess():
print("PaginatedResource: ", reply.errorString(), file=sys.stderr)
(json, error) = reply.readJson()
if json:
self.refreshRequestFinished(json)
else:
self.refreshRequestFailed()
def refreshRequestFinished(self, json):
json_object = json.object()
self._populateModel(json_object["data"])
self.m_pages = int(json_object[totalPagesField])
self.m_currentPage = int(json_object[currentPageField])
self.pageUpdated.emit()
self.pagesUpdated.emit()
self.dataUpdated.emit()
def refreshRequestFailed(self):
if self.m_currentPage != 1:
# A failed refresh. If we weren't on page 1, try that.
# Last resource on currentPage might have been deleted, causing a failure
self.setPage(1)
else:
# Refresh failed and we we're already on page 1 => clear data
self.m_pages = 0
self.pagesUpdated.emit()
self._clearModel()
self.dataUpdated.emit()
@Slot("QVariantMap", int)
def update(self, data, id):
request = self.m_api.createRequest(f"{self.m_path}/{id}")
self.m_manager.put(request, self, self.updateReply)
def updateReply(self, reply):
if reply.isSuccess():
self.refreshCurrentPage()
@Slot("QVariantMap")
def add(self, data):
request = self.m_api.createRequest(self.m_path)
self.m_manager.post(request, data, self, self.updateReply)
@Slot(int)
def remove(self, id):
request = self.m_api.createRequest(f"{self.m_path}/{id}")
self.m_manager.deleteResource(request, self, self.updateReply)
@QmlElement
class PaginatedColorUsersResource(PaginatedResource):
def __init__(self, parent=None):
super().__init__(parent)
self.m_model = ColorUserModel(self)
@Property(ColorUserModel, constant=True)
def model(self):
return self.m_model
def _clearModel(self):
self.m_model.clear()
def _populateModel(self, json_list):
self.m_model.set_data(json_list)
@Slot(str, result=str)
def avatarForEmail(self, email):
return self.m_model.avatarForEmail(email)
@QmlElement
class PaginatedColorsResource(PaginatedResource):
def __init__(self, parent=None):
super().__init__(parent)
self.m_model = ColorModel(self)
@Property(ColorModel, constant=True)
def model(self):
return self.m_model
def _clearModel(self):
self.m_model.clear()
def _populateModel(self, json_list):
self.m_model.set_data(json_list)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from PySide6.QtCore import Property, Signal, ClassInfo
from PySide6.QtNetwork import (QNetworkAccessManager, QRestAccessManager,
QNetworkRequestFactory, QSslSocket)
from PySide6.QtQml import QmlElement, QPyQmlParserStatus, ListProperty
from abstractresource import AbstractResource
QML_IMPORT_NAME = "ColorPalette"
QML_IMPORT_MAJOR_VERSION = 1
@QmlElement
@ClassInfo(DefaultProperty="resources")
class RestService(QPyQmlParserStatus):
urlChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.m_resources = []
self.m_qnam = QNetworkAccessManager()
self.m_qnam.setAutoDeleteReplies(True)
self.m_manager = QRestAccessManager(self.m_qnam)
self.m_serviceApi = QNetworkRequestFactory()
@Property(str, notify=urlChanged)
def url(self):
return self.m_serviceApi.baseUrl()
@url.setter
def url(self, url):
if self.m_serviceApi.baseUrl() != url:
self.m_serviceApi.setBaseUrl(url)
self.urlChanged.emit()
@Property(bool, constant=True)
def sslSupported(self):
return QSslSocket.supportsSsl()
def classBegin(self):
pass
def componentComplete(self):
for resource in self.m_resources:
resource.setAccessManager(self.m_manager)
resource.setServiceApi(self.m_serviceApi)
def appendResource(self, r):
self.m_resources.append(r)
resources = ListProperty(AbstractResource, appendResource)
<RCC>
<qresource prefix="/qt/qml/ColorPalette">
<file>icons/close.svg</file>
<file>icons/delete.svg</file>
<file>icons/dots.svg</file>
<file>icons/edit.svg</file>
<file>icons/login.svg</file>
<file>icons/logout.svg</file>
<file>icons/ok.svg</file>
<file>icons/plus.svg</file>
<file>icons/qt.png</file>
<file>icons/testserver.png</file>
<file>icons/update.svg</file>
<file>icons/user.svg</file>
<file>icons/userMask.svg</file>
</qresource>
</RCC>
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtExampleStyle
Popup {
id: colorDeleter
padding: 10
modal: true
focus: true
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
signal deleteClicked(int cid)
property int colorId: -1
property string colorName: ""
function maybeDelete(color_id, name) {
colorName = name
colorId = color_id
open()
}
ColumnLayout {
anchors.fill: parent
spacing: 10
Text {
color: "#222222"
text: qsTr("Delete Color?")
font.pixelSize: 16
font.bold: true
}
Text {
color: "#222222"
text: qsTr("Are you sure, you want to delete color") + " \"" + colorDeleter.colorName + "\"?"
font.pixelSize: 12
}
RowLayout {
Layout.fillWidth: true
spacing: 10
Button {
Layout.fillWidth: true
text: qsTr("Cancel")
onClicked: colorDeleter.close()
}
Button {
Layout.fillWidth: true
text: qsTr("Delete")
buttonColor: "#CC1414"
textColor: "#FFFFFF"
onClicked: {
colorDeleter.deleteClicked(colorDeleter.colorId)
colorDeleter.close()
}
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtExampleStyle
Popup {
id: colorEditor
// Popup for adding or updating a color
padding: 10
modal: true
focus: true
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
signal colorAdded(string name, string color, string pantone_value)
signal colorUpdated(string name, string color, string pantone_value, int cid)
property bool newColor: true
property int colorId: -1
property alias currentColor: colordialogButton.buttonColor
function createNewColor() {
newColor = true
colorNameField.text = "cute green"
colorRGBField.text = "#41cd52"
colorPantoneField.text = "PMS 802C"
open()
}
function updateColor(color_id, name, color, pantone_value) {
newColor = false
colorNameField.text = name
currentColor = color
colorPantoneField.text = pantone_value
colorId = color_id
open()
}
ColorDialog {
id: colorDialog
title: qsTr("Choose a color")
onAccepted: {
colorEditor.currentColor = Qt.color(colorDialog.selectedColor)
colorDialog.close()
}
onRejected: {
colorDialog.close()
}
}
ColumnLayout {
anchors.fill: parent
spacing: 10
GridLayout {
columns: 2
rowSpacing: 10
columnSpacing: 10
Label {
text: qsTr("Color Name")
}
TextField {
id: colorNameField
padding: 10
}
Label {
text: qsTr("Pantone Value")
}
TextField {
id: colorPantoneField
padding: 10
}
Label {
text: qsTr("Rgb Value")
}
TextField {
id: colorRGBField
text: colorEditor.currentColor.toString()
readOnly: true
padding: 10
}
}
Button {
id: colordialogButton
Layout.fillWidth: true
Layout.preferredHeight: 30
text: qsTr("Set Color")
textColor: isColorDark(buttonColor) ? "#E6E6E6" : "#191919"
onClicked: colorDialog.open()
function isColorDark(color) {
return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) < 0.5;
}
}
RowLayout {
Layout.fillWidth: true
spacing: 10
Button {
text: qsTr("Cancel")
onClicked: colorEditor.close()
Layout.fillWidth: true
}
Button {
Layout.fillWidth: true
text: colorEditor.newColor ? qsTr("Add") : qsTr("Update")
buttonColor: "#2CDE85"
textColor: "#FFFFFF"
onClicked: {
if (colorEditor.newColor) {
colorEditor.colorAdded(colorNameField.text,
colorRGBField.text,
colorPantoneField.text)
} else {
colorEditor.colorUpdated(colorNameField.text,
colorRGBField.text,
colorPantoneField.text,
colorEditor.colorId)
}
colorEditor.close()
}
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Shapes
import QtExampleStyle
import ColorPalette
Item {
id: root
required property BasicLogin loginService
required property PaginatedColorsResource colors
required property PaginatedColorUsersResource colorViewUsers
ColorDialogEditor {
id: colorPopup
onColorAdded: (colorNameField, colorRGBField, colorPantoneField) => {
root.colors.add({"name" : colorNameField,
"color" : colorRGBField,
"pantone_value" : colorPantoneField})
}
onColorUpdated: (colorNameField, colorRGBField, colorPantoneField, cid) => {
root.colors.update({"name" : colorNameField,
"color" : colorRGBField,
"pantone_value" : colorPantoneField},
cid)
}
}
ColorDialogDelete {
id: colorDeletePopup
onDeleteClicked: (cid) => {
root.colors.remove(cid)
}
}
ColumnLayout {
// The main application layout
anchors.fill :parent
ToolBar {
Layout.fillWidth: true
Layout.minimumHeight: 25 + 4
UserMenu {
id: userMenu
userMenuUsers: root.colorViewUsers
userLoginService: root.loginService
}
RowLayout {
anchors.fill: parent
Text {
text: qsTr("QHTTP Server")
font.pixelSize: 8
color: "#667085"
}
Item { Layout.fillWidth: true }
AbstractButton {
id: loginButton
Layout.preferredWidth: 25
Layout.preferredHeight: 25
Item {
id: userImageCliped
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 25
height: 25
Image {
id: userImage
anchors.fill: parent
source: getCurrentUserImage()
visible: false
function getCurrentUserImage() {
if (root.loginService.loggedIn)
return users.avatarForEmail(loginService.user)
return "qrc:/qt/qml/ColorPalette/icons/user.svg";
}
}
Image {
id: userMask
source: "qrc:/qt/qml/ColorPalette/icons/userMask.svg"
anchors.fill: userImage
anchors.margins: 4
visible: false
}
MultiEffect {
source: userImage
anchors.fill: userImage
maskSource: userMask
maskEnabled: true
}
}
onClicked: {
userMenu.open()
var pos = mapToGlobal(Qt.point(x, y))
pos = userMenu.parent.mapFromGlobal(pos)
userMenu.x = x - userMenu.width + 25 + 3
userMenu.y = y + 25 + 3
}
Shape {
id: bubble
x: -text.width - 25
anchors.margins: 3
preferredRendererType: Shape.CurveRenderer
visible: !root.loginService.loggedIn
ShapePath {
strokeWidth: 0
fillColor: "#667085"
startX: 5; startY: 0
PathLine { x: 5 + text.width + 6; y: 0 }
PathArc { x: 10 + text.width + 6; y: 5; radiusX: 5; radiusY: 5}
// arrow
PathLine { x: 10 + text.width + 6; y: 8 + text.height / 2 - 6 }
PathLine { x: 10 + text.width + 6 + 6; y: 8 + text.height / 2 }
PathLine { x: 10 + text.width + 6; y: 8 + text.height / 2 + 6}
PathLine { x: 10 + text.width + 6; y: 5 + text.height + 6 }
// end arrow
PathArc { x: 5 + text.width + 6; y: 10 + text.height + 6 ; radiusX: 5; radiusY: 5}
PathLine { x: 5; y: 10 + text.height + 6 }
PathArc { x: 0; y: 5 + text.height + 6 ; radiusX: 5; radiusY: 5}
PathLine { x: 0; y: 5 }
PathArc { x: 5; y: 0 ; radiusX: 5; radiusY: 5}
}
Text {
x: 8
y: 8
id: text
color: "white"
text: qsTr("Log in to edit")
font.bold: true
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
}
}
}
Image {
anchors.centerIn: parent
source: "qrc:/qt/qml/ColorPalette/icons/qt.png"
fillMode: Image.PreserveAspectFit
height: 25
}
}
ToolBar {
Layout.fillWidth: true
Layout.minimumHeight: 32
RowLayout {
anchors.fill: parent
Text {
Layout.alignment: Qt.AlignVCenter
text: qsTr("Color Palette")
font.pixelSize: 14
font.bold: true
color: "#667085"
}
Item { Layout.fillWidth: true }
AbstractButton {
Layout.preferredWidth: 25
Layout.preferredHeight: 25
Layout.alignment: Qt.AlignVCenter
Rectangle {
anchors.fill: parent
radius: 4
color: "#192CDE85"
border.color: "#DDE2E8"
border.width: 1
}
Image {
source: UIStyle.iconPath("plus")
fillMode: Image.PreserveAspectFit
anchors.fill: parent
sourceSize.width: width
sourceSize.height: height
}
visible: root.loginService.loggedIn
onClicked: colorPopup.createNewColor()
}
AbstractButton {
Layout.preferredWidth: 25
Layout.preferredHeight: 25
Layout.alignment: Qt.AlignVCenter
Rectangle {
anchors.fill: parent
radius: 4
color: "#192CDE85"
border.color: "#DDE2E8"
border.width: 1
}
Image {
source: UIStyle.iconPath("update")
fillMode: Image.PreserveAspectFit
anchors.fill: parent
sourceSize.width: width
sourceSize.height: height
}
onClicked: {
root.colors.refreshCurrentPage()
root.colorViewUsers.refreshCurrentPage()
}
}
}
}
//! [View and model]
ListView {
id: colorListView
model: root.colors.model
//! [View and model]
footerPositioning: ListView.OverlayFooter
spacing: 15
clip: true
Layout.fillHeight: true
Layout.fillWidth: true
header: Rectangle {
height: 32
width: parent.width
color: "#F0F1F3"
RowLayout {
anchors.fill: parent
component HeaderText : Text {
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Qt.AlignHCenter
font.pixelSize: 12
color: "#667085"
}
HeaderText {
id: headerName
text: qsTr("Color Name")
Layout.preferredWidth: colorListView.width * 0.3
}
HeaderText {
id: headerRgb
text: qsTr("Rgb Value")
Layout.preferredWidth: colorListView.width * 0.25
}
HeaderText {
id: headerPantone
text: qsTr("Pantone Value")
Layout.preferredWidth: colorListView.width * 0.25
}
HeaderText {
id: headerAction
text: qsTr("Action")
Layout.preferredWidth: colorListView.width * 0.2
}
}
}
delegate: Item {
id: colorInfo
required property int color_id
required property string name
required property string color
required property string pantone_value
width: colorListView.width
height: 25
RowLayout {
anchors.fill: parent
anchors.leftMargin: 5
anchors.rightMargin: 5
Rectangle {
id: colorSample
Layout.alignment: Qt.AlignVCenter
implicitWidth: 36
implicitHeight: 21
radius: 6
color: colorInfo.color
}
Text {
Layout.preferredWidth: colorInfo.width * 0.3 - colorSample.width
horizontalAlignment: Qt.AlignLeft
leftPadding: 5
text: colorInfo.name
}
Text {
Layout.preferredWidth: colorInfo.width * 0.25
horizontalAlignment: Qt.AlignHCenter
text: colorInfo.color
}
Text {
Layout.preferredWidth: colorInfo.width * 0.25
horizontalAlignment: Qt.AlignHCenter
text: colorInfo.pantone_value
}
Item {
Layout.maximumHeight: 28
implicitHeight: buttonBox.implicitHeight
implicitWidth: buttonBox.implicitWidth
RowLayout {
id: buttonBox
anchors.fill: parent
ToolButton {
icon.source: UIStyle.iconPath("delete")
enabled: root.loginService.loggedIn
onClicked: colorDeletePopup.maybeDelete(color_id, name)
}
ToolButton {
icon.source: UIStyle.iconPath("edit")
enabled: root.loginService.loggedIn
onClicked: colorPopup.updateColor(color_id, name, color, pantone_value)
}
}
}
}
}
footer: ToolBar {
// Paginate buttons if more than one page
visible: root.colors.pages > 1
implicitWidth: parent.width
RowLayout {
anchors.fill: parent
Item { Layout.fillWidth: true /* spacer */ }
Repeater {
model: root.colors.pages
ToolButton {
text: page
font.bold: root.colors.page === page
required property int index
readonly property int page: (index + 1)
onClicked: root.colors.page = page
}
}
}
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import ColorPalette
Window {
id: window
width: 500
height: 400
visible: true
title: qsTr("Color Palette Client")
enum DataView {
UserView = 0,
ColorView = 1
}
ServerSelection {
id: serverview
anchors.fill: parent
onServerSelected: {colorview.visible = true; serverview.visible = false}
colorResources: colors
restPalette: paletteService
colorUsers: users
}
ColorView {
id: colorview
anchors.fill: parent
visible: false
loginService: colorLogin
colors: colors
colorViewUsers: users
}
//! [RestService QML element]
RestService {
id: paletteService
PaginatedColorUsersResource {
id: users
path: "/api/users"
}
PaginatedColorsResource {
id: colors
path: "/api/unknown"
}
BasicLogin {
id: colorLogin
loginPath: "/api/login"
logoutPath: "/api/logout"
}
}
//! [RestService QML element]
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ColorPalette
import QtExampleStyle
pragma ComponentBehavior: Bound
Item {
id: root
// A popup for selecting the server URL
signal serverSelected()
required property PaginatedColorsResource colorResources
required property PaginatedColorUsersResource colorUsers
required property RestService restPalette
Connections {
target: root.colorResources
// Closes the URL selection popup once we have received data successfully
function onDataUpdated() {
fetchTester.stop()
root.serverSelected()
}
}
ListModel {
id: server
ListElement {
title: qsTr("Public REST API Test Server")
url: "https://reqres.in"
icon: "qrc:/qt/qml/ColorPalette/icons/testserver.png"
}
ListElement {
title: qsTr("Qt-based REST API server")
url: "http://127.0.0.1:49425"
icon: "qrc:/qt/qml/ColorPalette/icons/qt.png"
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 10
Image {
Layout.alignment: Qt.AlignHCenter
source: "qrc:/qt/qml/ColorPalette/icons/qt.png"
fillMode: Image.PreserveAspectFit
Layout.preferredWidth: 20
}
Label {
text: qsTr("Choose a server")
Layout.alignment: Qt.AlignHCenter
font.pixelSize: 24
}
component ServerListDelegate: Rectangle {
id: serverListDelegate
required property string title
required property string url
required property string icon
required property int index
radius: 10
color: "#00000000"
border.color: ListView.view.currentIndex === index ? "#2CDE85" : "#E0E2E7"
border.width: 2
implicitWidth: 180
implicitHeight: 100
Rectangle {
id: img
anchors.left: parent.left
anchors.top: parent.top
anchors.topMargin: 10
anchors.leftMargin: 20
width: 30
height: 30
radius: 200
border. color: "#E7F4EE"
border.width: 5
Image {
anchors.centerIn: parent
source: serverListDelegate.icon
width: 15
height: 15
fillMode: Image.PreserveAspectFit
smooth: true
}
}
Text {
text: parent.url
anchors.left: parent.left
anchors.top: img.bottom
anchors.topMargin: 10
anchors.leftMargin: 20
color: "#667085"
font.pixelSize: 13
}
Text {
text: parent.title
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: 10
color: "#222222"
font.pixelSize: 11
font.bold: true
}
MouseArea {
anchors.fill: parent
onClicked: serverList.currentIndex = serverListDelegate.index;
}
}
ListView {
id: serverList
Layout.alignment: Qt.AlignHCenter
Layout.minimumWidth: 180 * server.count + 20
Layout.minimumHeight: 100
orientation: ListView.Horizontal
model: server
spacing: 20
delegate: ServerListDelegate {}
}
Button {
Layout.alignment: Qt.AlignHCenter
text: restPalette.sslSupported ? qsTr("Connect (SSL)") : qsTr("Connect")
buttonColor: "#2CDE85"
textColor: "#FFFFFF"
onClicked: {
busyIndicatorPopup.title = (serverList.currentItem as ServerListDelegate).title
busyIndicatorPopup.icon = (serverList.currentItem as ServerListDelegate).icon
busyIndicatorPopup.open()
fetchTester.test((serverList.currentItem as ServerListDelegate).url)
}
}
Timer {
id: fetchTester
interval: 2000
function test(url) {
root.restPalette.url = url
root.colorResources.refreshCurrentPage()
root.colorUsers.refreshCurrentPage()
start()
}
onTriggered: busyIndicatorPopup.close()
}
}
onVisibleChanged: {if (!visible) busyIndicatorPopup.close();}
Popup {
id: busyIndicatorPopup
padding: 10
modal: true
focus: true
anchors.centerIn: parent
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property alias title: titleText.text
property alias icon: titleImg.source
ColumnLayout {
id: fetchIndicator
anchors.fill: parent
RowLayout {
Rectangle {
Layout.preferredWidth: 50
Layout.preferredHeight: 50
radius: 200
border. color: "#E7F4EE"
border.width: 5
Image {
id: titleImg
anchors.centerIn: parent
width: 25
height: 25
fillMode: Image.PreserveAspectFit
}
}
Label {
id: titleText
text:""
font.pixelSize: 18
}
}
RowLayout {
Layout.fillWidth: false
Layout.alignment: Qt.AlignHCenter
BusyIndicator {
running: visible
Layout.fillWidth: true
}
Label {
text: qsTr("Testing URL")
font.pixelSize: 18
}
}
Button {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Cancel")
onClicked: {
busyIndicatorPopup.close()
}
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import QtExampleStyle
import ColorPalette
Popup {
id: userMenu
required property BasicLogin userLoginService
required property PaginatedColorUsersResource userMenuUsers
width: 280
height: 270
ColumnLayout {
anchors.fill: parent
ListView {
id: userListView
model: userMenu.userMenuUsers.model
spacing: 5
footerPositioning: ListView.PullBackFooter
clip: true
Layout.fillHeight: true
Layout.fillWidth: true
delegate: Rectangle {
id: userInfo
required property string email
required property string avatar
height: 30
width: userListView.width
readonly property bool logged: (email === loginService.user)
Rectangle {
id: userImageCliped
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: 30
height: 30
Image {
id: userImage
anchors.fill: parent
source: userInfo.avatar
visible: false
}
Image {
id: userMask
source: "qrc:/qt/qml/ColorPalette/icons/userMask.svg"
anchors.fill: userImage
anchors.margins: 4
visible: false
}
MultiEffect {
source: userImage
anchors.fill: userImage
maskSource: userMask
maskEnabled: true
}
}
Text {
id: userMailLabel
anchors.left: userImageCliped.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 5
text: userInfo.email
font.bold: userInfo.logged
}
ToolButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 5
icon.source: UIStyle.iconPath(userInfo.logged
? "logout" : "login")
enabled: userInfo.logged || !userMenu.userLoginService.loggedIn
onClicked: {
if (userInfo.logged) {
userMenu.userLoginService.logout()
} else {
//! [Login]
userMenu.userLoginService.login({"email" : userInfo.email,
"password" : "apassword",
"id" : userInfo.id})
//! [Login]
userMenu.close()
}
}
}
}
footer: ToolBar {
// Paginate buttons if more than one page
visible: userMenu.userMenuUsers.pages > 1
implicitWidth: parent.width
RowLayout {
anchors.fill: parent
Item { Layout.fillWidth: true /* spacer */ }
Repeater {
model: userMenu.userMenuUsers.pages
ToolButton {
text: page
font.bold: userMenu.userMenuUsers.page === page
required property int index
readonly property int page: (index + 1)
onClicked: userMenu.userMenuUsers.page = page
}
}
}
}
}
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Templates as T
T.Button {
id: control
property alias buttonColor: rect.color
property alias textColor: label.color
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding)
leftPadding: 15
rightPadding: 15
topPadding: 10
bottomPadding: 10
background: Rectangle {
id: rect
radius: 8
border.color: "#E0E2E7"
border.width: 1
color: "#FFFFFF"
}
icon.width: 24
icon.height: 24
icon.color: control.palette.buttonText
contentItem: IconLabel {
id: label
spacing: control.spacing
mirrored: control.mirrored
display: control.display
icon: control.icon
text: control.text
font.pixelSize: 14
color: "#667085"
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Templates as T
T.Popup {
id: control
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
implicitContentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
implicitContentHeight + topPadding + bottomPadding)
leftPadding: 15
rightPadding: 15
topPadding: 10
bottomPadding: 10
background: Rectangle {
id: bg
radius: 8
border.color: "#E0E2E7"
border.width: 2
color: "#FFFFFF"
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Templates as T
T.TextField {
id: control
placeholderText: ""
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding)
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
contentHeight + topPadding + bottomPadding)
background: Rectangle {
implicitWidth: 200
implicitHeight: 40
radius: 8
color: control.enabled ? "transparent" : "#353637"
border.color: "#E0E2E7"
}
}
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
pragma Singleton
import QtQuick
QtObject {
id: uiStyle
// Font Sizes
readonly property int fontSizeXXS: 10
readonly property int fontSizeXS: 15
readonly property int fontSizeS: 20
readonly property int fontSizeM: 25
readonly property int fontSizeL: 30
readonly property int fontSizeXL: 35
readonly property int fontSizeXXL: 40
// Color Scheme
// Green
readonly property color colorQtPrimGreen: "#41cd52"
readonly property color colorQtAuxGreen1: "#21be2b"
readonly property color colorQtAuxGreen2: "#17a81a"
function iconPath(baseImagePath) {
return `qrc:/qt/qml/ColorPalette/icons/${baseImagePath}.svg`
}
}
<RCC>
<qresource prefix="/qt/qml/ColorPalette">
<file>icons/close.svg</file>
<file>icons/delete.svg</file>
<file>icons/dots.svg</file>
<file>icons/edit.svg</file>
<file>icons/login.svg</file>
<file>icons/logout.svg</file>
<file>icons/ok.svg</file>
<file>icons/plus.svg</file>
<file>icons/qt.png</file>
<file>icons/testserver.png</file>
<file>icons/update.svg</file>
<file>icons/user.svg</file>
<file>icons/userMask.svg</file>
</qresource>
</RCC>