mirror of https://github.com/qt/qtgrpc.git
Rework magic8ball example
Improve QML layout, GRPC communications, animation logic and documentation. Simplify the GRPC server implementation removing Qt parts. Task-number: QTBUG-129571 Pick-to: 6.8 Change-Id: I8913ca3b52950d950dd5862bd986b222f0e6405e Reviewed-by: Jaime Resano <Jaime.RESANO-AISA@qt.io> Reviewed-by: Alexey Edelev <alexey.edelev@qt.io>
This commit is contained in:
parent
2683203c5d
commit
c761459e59
|
@ -6,36 +6,161 @@ import QtQuick
|
|||
Item {
|
||||
id: root
|
||||
|
||||
signal closed()
|
||||
property string currentAnswerText: ""
|
||||
property string nextAnswerText: ""
|
||||
property bool canRequestAnswer: true
|
||||
|
||||
property alias closingAnimation: closeAnimator
|
||||
property alias openingAnimation: openAnimator
|
||||
property alias animationText: result.text
|
||||
state: "DISABLED"
|
||||
states: [
|
||||
State {
|
||||
name: "DISABLED"
|
||||
PropertyChanges {
|
||||
root.canRequestAnswer: true
|
||||
answerText.visible: false
|
||||
waitingPlaceholder.visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "WAITING"
|
||||
PropertyChanges {
|
||||
root.canRequestAnswer: false
|
||||
answerText.visible: false
|
||||
waitingPlaceholder.visible: true
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "SHOWING"
|
||||
PropertyChanges {
|
||||
root.canRequestAnswer: false
|
||||
answerText.visible: true
|
||||
waitingPlaceholder.visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "PAUSED"
|
||||
PropertyChanges {
|
||||
root.canRequestAnswer: true
|
||||
answerText.visible: true
|
||||
waitingPlaceholder.visible: false
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "HIDING"
|
||||
PropertyChanges {
|
||||
root.canRequestAnswer: false
|
||||
answerText.visible: true
|
||||
waitingPlaceholder.visible: false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "DISABLED,HIDING"
|
||||
to: "WAITING"
|
||||
SequentialAnimation {
|
||||
id: waitingAnimation
|
||||
loops: Animation.Infinite
|
||||
|
||||
ScaleAnimation {
|
||||
target: root
|
||||
mode: "ZoomIn"
|
||||
}
|
||||
|
||||
ScaleAnimation {
|
||||
target: root
|
||||
mode: "ZoomOut"
|
||||
}
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
root.currentAnswerText = root.nextAnswerText;
|
||||
root.nextAnswerText = "";
|
||||
if (!root.currentAnswerText) {
|
||||
root.state = "DISABLED";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "WAITING,HIDING"
|
||||
to: "SHOWING"
|
||||
|
||||
ScaleAnimation {
|
||||
target: root
|
||||
mode: "ZoomIn"
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
root.state = "PAUSED";
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "PAUSED"
|
||||
to: "HIDING"
|
||||
|
||||
ScaleAnimation {
|
||||
target: root
|
||||
mode: "ZoomOut"
|
||||
}
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
if (root.nextAnswerText) {
|
||||
root.currentAnswerText = root.nextAnswerText;
|
||||
root.nextAnswerText = "";
|
||||
root.state = "SHOWING";
|
||||
} else {
|
||||
root.state = "WAITING";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function addAnswer(answer: string): void {
|
||||
root.nextAnswerText = answer;
|
||||
if (root.state == "WAITING") {
|
||||
root.state = "SHOWING";
|
||||
}
|
||||
}
|
||||
|
||||
function startWaiting(): void {
|
||||
if (root.state == "PAUSED") {
|
||||
root.state = "HIDING";
|
||||
return;
|
||||
}
|
||||
root.state = "WAITING";
|
||||
}
|
||||
|
||||
function cancelAnimation(): void {
|
||||
root.state = "DISABLED";
|
||||
}
|
||||
|
||||
MagicText {
|
||||
id: result
|
||||
id: answerText
|
||||
anchors.centerIn: parent
|
||||
|
||||
font.pointSize: text.length > 12 ? 14 : 16
|
||||
font.pointSize: 20
|
||||
color: "#2E53B6"
|
||||
text: root.currentAnswerText
|
||||
}
|
||||
|
||||
ScaleAnimator on scale {
|
||||
id: openAnimator
|
||||
target: result
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 2000
|
||||
running: false
|
||||
}
|
||||
Row {
|
||||
id: waitingPlaceholder
|
||||
anchors.centerIn: parent
|
||||
spacing: 12
|
||||
|
||||
ScaleAnimator on scale {
|
||||
id: closeAnimator
|
||||
target: result
|
||||
from: 1
|
||||
to: 0
|
||||
duration: 2000
|
||||
running: false
|
||||
onStopped: root.closed()
|
||||
Repeater {
|
||||
model: 3
|
||||
Rectangle {
|
||||
width: 11
|
||||
height: width
|
||||
color: "#264BAF"
|
||||
radius: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,6 @@ find_package(Qt6 REQUIRED COMPONENTS
|
|||
|
||||
qt_standard_project_setup()
|
||||
|
||||
add_subdirectory(grpc_server_example)
|
||||
|
||||
qt_add_executable(magic8ball
|
||||
main.cpp
|
||||
)
|
||||
|
@ -50,11 +48,11 @@ qt_add_qml_module(magic8ball
|
|||
VERSION 1.0
|
||||
RESOURCE_PREFIX "/qt/qml"
|
||||
QML_FILES
|
||||
"WaitingAnimation.qml"
|
||||
"AnimatedAnswer.qml"
|
||||
"MagicText.qml"
|
||||
"ProgressDot.qml"
|
||||
"MagicBall.qml"
|
||||
"Main.qml"
|
||||
"ScaleAnimation.qml"
|
||||
)
|
||||
|
||||
target_link_libraries(magic8ball PRIVATE
|
||||
|
@ -71,3 +69,8 @@ install(TARGETS magic8ball
|
|||
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
)
|
||||
|
||||
add_subdirectory(server)
|
||||
if(TARGET magic8ball_server)
|
||||
add_dependencies(magic8ball magic8ball_server)
|
||||
endif()
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Rectangle {
|
||||
function addAnswer(answer: string): void {animatedAnswer.addAnswer(answer)}
|
||||
function startWaiting(): void {animatedAnswer.startWaiting()}
|
||||
function cancelAnimation(): void {animatedAnswer.cancelAnimation()}
|
||||
property alias canRequestAnswer: animatedAnswer.canRequestAnswer
|
||||
|
||||
implicitWidth: 433
|
||||
implicitHeight: width
|
||||
|
||||
color: "black"
|
||||
radius: 300
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop {
|
||||
position: 0.0
|
||||
color: "#4b4b4b"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.33
|
||||
color: "#212121"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1.0
|
||||
color: "#000000"
|
||||
}
|
||||
}
|
||||
|
||||
// Reflection decoration
|
||||
Rectangle {
|
||||
width: 300
|
||||
height: width
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: 6
|
||||
radius: 300
|
||||
|
||||
color: "#bababa"
|
||||
}
|
||||
|
||||
// Ball center
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: 300
|
||||
height: width
|
||||
|
||||
color: "black"
|
||||
border.width: 1.5
|
||||
border.color: "#bababa"
|
||||
radius: 300
|
||||
|
||||
Shape {
|
||||
id: ballTriangle
|
||||
anchors.centerIn: parent
|
||||
width: 250
|
||||
height: 250
|
||||
ShapePath {
|
||||
strokeWidth: 4
|
||||
strokeColor: "#213f94"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
fillGradient: RadialGradient {
|
||||
centerX: ballTriangle.width / 2
|
||||
centerY: ballTriangle.height / 2
|
||||
focalX: centerX
|
||||
focalY: centerY
|
||||
centerRadius: 50
|
||||
focalRadius: 0
|
||||
|
||||
GradientStop {
|
||||
position: 0
|
||||
color: "#1C2F60"
|
||||
}
|
||||
GradientStop {
|
||||
position: 0.5
|
||||
color: "#000547"
|
||||
}
|
||||
GradientStop {
|
||||
position: 1
|
||||
color: "#000324"
|
||||
}
|
||||
}
|
||||
|
||||
startX: 26
|
||||
startY: 68
|
||||
|
||||
PathLine {
|
||||
x: 125
|
||||
y: 230
|
||||
}
|
||||
PathLine {
|
||||
x: 224
|
||||
y: 68
|
||||
}
|
||||
PathLine {
|
||||
x: 26
|
||||
y: 68
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedAnswer {
|
||||
id: animatedAnswer
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,10 +2,12 @@
|
|||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.family: "Helvetica"
|
||||
font.pointSize: 16
|
||||
|
|
|
@ -1,238 +1,119 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import QtQuick
|
||||
import QtGrpc
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Layouts
|
||||
|
||||
import qtgrpc.examples
|
||||
import qtgrpc.examples.magic8ball
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
width: 665
|
||||
height: width
|
||||
|
||||
minimumWidth: width
|
||||
minimumHeight: height
|
||||
property answerRequest answerReq
|
||||
property string errorText: ""
|
||||
property int errorCode: 0
|
||||
|
||||
//! [requestAnswerFunction]
|
||||
function requestAnswer(question: string): void {
|
||||
//! [requestAnswerFunction]
|
||||
root.errorText = "";
|
||||
magicBall.startWaiting();
|
||||
|
||||
//! [requestAnswerFunctionBody]
|
||||
root.answerReq.question = question;
|
||||
grpcClient.answerMethod(root.answerReq, finishCallback, errorCallback, grpcCallOptions);
|
||||
}
|
||||
//! [requestAnswerFunctionBody]
|
||||
|
||||
function finishCallback(response: answerResponse): void {
|
||||
magicBall.addAnswer(response.message);
|
||||
}
|
||||
|
||||
function errorCallback(error): void {
|
||||
// error is received as a JavaScript object, but it is a QGrpcStatus instance
|
||||
magicBall.cancelAnimation();
|
||||
console.log(
|
||||
`Error callback executed. Error message: "${error.message}" Code: ${error.code}`
|
||||
);
|
||||
root.errorText = error.message;
|
||||
root.errorCode = error.code;
|
||||
}
|
||||
|
||||
minimumWidth: rootLayout.implicitWidth + rootLayout.anchors.margins * 2
|
||||
minimumHeight: rootLayout.implicitHeight + rootLayout.anchors.margins * 2
|
||||
|
||||
visible: true
|
||||
title: qsTr("Magic-8-ball Qt GRPC Example")
|
||||
Material.theme: Material.Light
|
||||
font.pointSize: 18
|
||||
|
||||
property string textAnswer: ""
|
||||
property string textError: ""
|
||||
|
||||
property answerRequest _answerReq
|
||||
property answerResponse _answerResp
|
||||
|
||||
property var setResponse: function(value) { root._answerResp = value }
|
||||
property var errorCallback: function() { console.log("Error can be handled also here") }
|
||||
|
||||
MagicText {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 20
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
width: parent.width * 0.9
|
||||
height: parent.height/3
|
||||
|
||||
color: "black"
|
||||
|
||||
text: qsTr("For fortune-telling and seeking advice ask the ball"
|
||||
+ " a yes-no question and press the button.")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: magic8ball
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: 433
|
||||
height: width
|
||||
|
||||
color: "#000000"
|
||||
radius: 300
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
GradientStop { position: 0.0; color: "#4b4b4b" }
|
||||
GradientStop { position: 0.33; color: "#212121" }
|
||||
GradientStop { position: 1.0; color: "#000000" }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
|
||||
width: 244
|
||||
height: width
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.horizontalCenterOffset: 6
|
||||
radius: 300
|
||||
|
||||
color: "#bababa"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: magic8ballCenter
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: 244
|
||||
height: width
|
||||
|
||||
color: "black"
|
||||
border.width: 1.5
|
||||
border.color: "#bababa"
|
||||
radius: 300
|
||||
|
||||
Shape {
|
||||
anchors.centerIn: parent
|
||||
width: 200
|
||||
height: 200
|
||||
ShapePath {
|
||||
strokeWidth: 4
|
||||
strokeColor: "#213f94"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
fillGradient: RadialGradient {
|
||||
centerX: 100
|
||||
centerY: 100
|
||||
focalX: centerX
|
||||
focalY: centerY
|
||||
centerRadius: 50
|
||||
focalRadius: 0
|
||||
|
||||
GradientStop { position: 0; color: "#1C2F60" }
|
||||
GradientStop { position: 0.5; color: "#000547" }
|
||||
GradientStop { position: 1; color: "#000324" }
|
||||
}
|
||||
|
||||
startX: 10
|
||||
startY: 40
|
||||
|
||||
PathLine { x: 100.5; y: 190 }
|
||||
PathLine { x: 188; y: 40 }
|
||||
PathLine { x: 10; y: 40 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WaitingAnimation {
|
||||
id: waitingAnimation
|
||||
anchors.centerIn: parent
|
||||
|
||||
visible: false
|
||||
}
|
||||
|
||||
AnimatedAnswer {
|
||||
id: answer
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: false
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: grpcClient
|
||||
function onErrorOccurred() {
|
||||
root.textError = "No connection\nto\nserver"
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: button
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 30
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
width: 200
|
||||
height: 50
|
||||
radius: 10
|
||||
|
||||
color: handler.pressed ? "#a5a5a5" : "#bebebe"
|
||||
|
||||
MagicText {
|
||||
id: btnText
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: qsTr("Ask question")
|
||||
color: "black"
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: handler
|
||||
|
||||
onTapped: animationTimeout.start()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: answer.closingAnimation
|
||||
function onStopped() {
|
||||
answer.animationText = ""
|
||||
answer.visible = false
|
||||
waitingAnimation.visible = true
|
||||
waitingAnimation.runAnimation = true
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: waitingAnimation
|
||||
function onRunAnimationChanged() {
|
||||
if (!waitingAnimation.runAnimation) {
|
||||
answer.visible = true
|
||||
answer.openingAnimation.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: animationTimeout
|
||||
|
||||
interval: 5000
|
||||
repeat: false
|
||||
running: false
|
||||
onTriggered: root.textAnswer = _answerResp.message
|
||||
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
root.textError = ""
|
||||
answer.closingAnimation.start()
|
||||
root.sendRequest()
|
||||
} else {
|
||||
waitingAnimation.runAnimation = false
|
||||
waitingAnimation.visible = false
|
||||
answer.animationText = root.textError === "" ? root.textAnswer : root.textError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sendRequest()
|
||||
{
|
||||
grpcClient.answerMethod(_answerReq, setResponse, errorCallback)
|
||||
}
|
||||
|
||||
footer: MagicText {
|
||||
text: root.textError === "" ? "" : "Please, start server: ../magic8ball/SimpleGrpcServer"
|
||||
//! [channelOptions]
|
||||
GrpcHttp2Channel {
|
||||
id: grpcChannel
|
||||
hostUri: "http://localhost:50051"
|
||||
// Optionally, you can specify custom channel options here
|
||||
// options: GrpcChannelOptions {}
|
||||
}
|
||||
//! [channelOptions]
|
||||
|
||||
//! [exampleServiceClient]
|
||||
ExampleServiceClient {
|
||||
id: grpcClient
|
||||
channel: grpcChannel.channel
|
||||
}
|
||||
//! [exampleServiceClient]
|
||||
|
||||
GrpcHttp2Channel {
|
||||
id: grpcChannel
|
||||
hostUri: "http://localhost:50051"
|
||||
options: GrpcChannelOptions {
|
||||
//! [callOptions]
|
||||
GrpcCallOptions {
|
||||
id: grpcCallOptions
|
||||
deadlineTimeout: 6000
|
||||
}
|
||||
//! [callOptions]
|
||||
|
||||
ColumnLayout {
|
||||
id: rootLayout
|
||||
anchors.margins: 10
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
|
||||
MagicText {
|
||||
color: "black"
|
||||
text: qsTr("Ask the ball a yes-no question and press the button.")
|
||||
}
|
||||
|
||||
MagicBall {
|
||||
id: magicBall
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: questionInput
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
Layout.minimumWidth: 300
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
placeholderText: qsTr("Type here a question...")
|
||||
}
|
||||
|
||||
Button {
|
||||
onClicked: root.requestAnswer(questionInput.text)
|
||||
enabled: magicBall.canRequestAnswer
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
leftPadding: 16
|
||||
rightPadding: 16
|
||||
text: qsTr("Ask")
|
||||
}
|
||||
|
||||
MagicText {
|
||||
visible: root.errorText
|
||||
text:
|
||||
qsTr("Error: %1\n%2")
|
||||
.arg(root.errorText)
|
||||
.arg(root.errorCode == QtGrpc.StatusCode.Unavailable
|
||||
? qsTr("Please, restart the server")
|
||||
: "")
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
_answerReq.message = "sleep"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
width: 11
|
||||
height: width
|
||||
|
||||
color: "#264BAF"
|
||||
radius: 100
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import QtQuick
|
||||
|
||||
NumberAnimation {
|
||||
property string mode: "ZoomIn"
|
||||
property: "scale"
|
||||
duration: 1000
|
||||
easing.amplitude: 6.0
|
||||
easing.period: 2.5
|
||||
from: mode == "ZoomIn" ? 0.3 : 1.0
|
||||
to: mode == "ZoomIn" ? 1.0 : 0.3
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool runAnimation: false
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: 100
|
||||
height: width
|
||||
|
||||
color: "transparent"
|
||||
|
||||
Row {
|
||||
id: scene
|
||||
anchors.centerIn: parent
|
||||
spacing: 12
|
||||
|
||||
Repeater {
|
||||
model: 4
|
||||
ProgressDot {}
|
||||
}
|
||||
}
|
||||
|
||||
ScaleAnimator on scale {
|
||||
id: openning
|
||||
target: root
|
||||
from: 0.3
|
||||
to: 1
|
||||
duration: 1000
|
||||
running: root.runAnimation
|
||||
onStopped: closing.start()
|
||||
easing.amplitude: 6.0
|
||||
easing.period: 2.5
|
||||
}
|
||||
|
||||
ScaleAnimator on scale {
|
||||
id: closing
|
||||
target: root
|
||||
from: 1
|
||||
to: 0.3
|
||||
duration: 1000
|
||||
running: false
|
||||
onStopped: {
|
||||
if (root.runAnimation)
|
||||
openning.start()
|
||||
}
|
||||
easing.amplitude: 6.0
|
||||
easing.period: 2.5
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -8,19 +8,18 @@
|
|||
\meta tag {network,protobuf,grpc}
|
||||
\title Magic 8 Ball
|
||||
|
||||
\brief Creating a HTTP2 connection between a Qt GRPC client and
|
||||
a C++ gRPC server.
|
||||
\brief Creating a HTTP2 connection between a Qt gRPC client and a C++ gRPC server.
|
||||
|
||||
Magic 8 ball shows an answer it receives from a server:
|
||||
\image answer.webp
|
||||
Magic 8 ball sends a question to a server and displays the received answer:
|
||||
\image magic8ballScreenshot.webp "Magic 8 ball example screenshot"
|
||||
|
||||
Magic 8 ball has the following components:
|
||||
The example code includes the following components:
|
||||
\list
|
||||
\li \c magic8ball Qt GRPC client application that includes
|
||||
\li \c magic8ball Qt gRPC client application that uses
|
||||
the \l {qt_add_protobuf}{qt_add_protobuf()} and
|
||||
\l {qt_add_grpc}{qt_add_grpc()}
|
||||
CMake functions for message and service Qt code generation.
|
||||
\li \c SimpleGrpcServer application that calls C++ gRPC plugin
|
||||
\li \c server application that calls C++ gRPC plugin
|
||||
for generating server code and implementing simple server
|
||||
logic.
|
||||
\endlist
|
||||
|
@ -28,32 +27,37 @@
|
|||
\note you need the C++ gRPC plugin installed.
|
||||
Find details here: \l {Module prerequisites}
|
||||
|
||||
Both components use generated messages from the protobuf schema
|
||||
described in the \c {exampleservice.proto} file:
|
||||
Both components use generated messages from the protobuf schema described in the
|
||||
\c {exampleservice.proto} file:
|
||||
\quotefromfile magic8ball/proto/exampleservice.proto
|
||||
\skipto syntax = "proto3";
|
||||
\printuntil
|
||||
|
||||
The client application connects to the \c localhost with port
|
||||
\c 50051:
|
||||
\quotefromfile magic8ball/Main.qml
|
||||
\skipto id: grpcChannel
|
||||
\printuntil hostUri: "http://localhost:50051"
|
||||
The gRPC client is defined as a QML object \b{which is available after the code is compiled}.
|
||||
\snippet magic8ball/Main.qml exampleServiceClient
|
||||
|
||||
The client service connects to the \c localhost with port \c {50051}, which is specified in the
|
||||
gRPC channel options:
|
||||
\snippet magic8ball/Main.qml channelOptions
|
||||
|
||||
And sends a request to the server part:
|
||||
\quotefromfile magic8ball/Main.qml
|
||||
\skipto function sendRequest()
|
||||
\printuntil }
|
||||
\snippet magic8ball/Main.qml requestAnswerFunction
|
||||
\dots 8
|
||||
\snippet magic8ball/Main.qml requestAnswerFunctionBody
|
||||
|
||||
Click the \uicontrol {Ask question} button to send
|
||||
the request to the SimpleGrpcServer application.
|
||||
\c answerMethod is a gRPC method that the client calls. It has four parameters: the request
|
||||
object, a finish callback function, an error callback function and a
|
||||
\l {GrpcCallOptions} object.
|
||||
|
||||
The SimpleGrpcServer application chooses a random answer from
|
||||
the list of answers and sends the data to the client's port.
|
||||
\quotefromfile magic8ball/grpc_server_example/serverrunner.cpp
|
||||
\skipto Status ExampleServiceServiceImpl::answerMethod
|
||||
\printuntil }
|
||||
Click the \uicontrol {Ask} button to send the request to the magic8ball server.
|
||||
|
||||
After receiving a response the client application shows the answer.
|
||||
\note You have to run the server in parallel with the client application.
|
||||
|
||||
The \c server application chooses a random answer from the list of answers and sends
|
||||
the data to the client's port. It also checks that the request contains a non empty field
|
||||
\c question. If the field is empty, it returns a \c StatusCode::INVALID_ARGUMENT
|
||||
\snippet magic8ball/server/main.cpp answerMethod
|
||||
|
||||
After receiving a response, the client application shows the answer.
|
||||
*/
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "serverrunner.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <memory>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication a(argc, argv);
|
||||
auto server = std::make_unique<ExampleServer>();
|
||||
server->run();
|
||||
return a.exec();
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "serverrunner.h"
|
||||
#include "exampleservice.grpc.pb.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QDebug>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include <grpc++/grpc++.h>
|
||||
#include <memory>
|
||||
#include <array>
|
||||
#include <random>
|
||||
|
||||
namespace {
|
||||
|
||||
using grpc::Server;
|
||||
using grpc::ServerBuilder;
|
||||
using grpc::ServerContext;
|
||||
using grpc::ServerWriter;
|
||||
using grpc::Status;
|
||||
using qtgrpc::examples::AnswerRequest;
|
||||
using qtgrpc::examples::AnswerResponse;
|
||||
using qtgrpc::examples::ExampleService;
|
||||
|
||||
static const std::array<std::string_view, 10> answers = {"Yes",
|
||||
"Yep",
|
||||
"Most\nlikely",
|
||||
"It is\ncertain",
|
||||
"No",
|
||||
"Nope",
|
||||
"Try later",
|
||||
"Are you\nsure?",
|
||||
"Maybe",
|
||||
"Very\ndoubtful"};
|
||||
|
||||
// Generates random index value.
|
||||
static int generateRandomIndex()
|
||||
{
|
||||
static std::uniform_int_distribution<int> dist(0, answers.size() - 1);
|
||||
return dist(*QRandomGenerator::global());
|
||||
}
|
||||
|
||||
// Logic and data behind the server's behavior.
|
||||
class ExampleServiceServiceImpl final : public qtgrpc::examples::ExampleService::Service
|
||||
{
|
||||
grpc::Status answerMethod(grpc::ServerContext *, const AnswerRequest *request,
|
||||
AnswerResponse *response) override;
|
||||
};
|
||||
}
|
||||
|
||||
Status ExampleServiceServiceImpl::answerMethod(grpc::ServerContext *,
|
||||
const AnswerRequest *request,
|
||||
AnswerResponse *response)
|
||||
{
|
||||
if (request->message() == "sleep")
|
||||
QThread::msleep(2000);
|
||||
|
||||
response->set_message(std::string(answers[generateRandomIndex()]));
|
||||
return Status();
|
||||
}
|
||||
|
||||
void ExampleServer::run()
|
||||
{
|
||||
std::string serverUri("127.0.0.1:50051");
|
||||
ExampleServiceServiceImpl service;
|
||||
|
||||
grpc::ServerBuilder builder;
|
||||
builder.AddListeningPort(serverUri, grpc::InsecureServerCredentials());
|
||||
builder.RegisterService(&service);
|
||||
|
||||
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
|
||||
if (!server) {
|
||||
qDebug() << "Creating grpc::Server failed.";
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Server listening on " << serverUri;
|
||||
server->Wait();
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#ifndef SERVER_RUNNER_H
|
||||
#define SERVER_RUNNER_H
|
||||
|
||||
class ExampleServer
|
||||
{
|
||||
public:
|
||||
void run();
|
||||
};
|
||||
|
||||
#endif // SERVER_RUNNER_H
|
|
@ -1,17 +1,16 @@
|
|||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtQml/QQmlApplicationEngine>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QGuiApplication app(argc, argv);
|
||||
QQmlApplicationEngine engine;
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
|
||||
&app, [](){
|
||||
QCoreApplication::exit(-1);
|
||||
}, Qt::QueuedConnection);
|
||||
QObject::connect(
|
||||
&engine, &QQmlApplicationEngine::objectCreationFailed, &app,
|
||||
[]() { QCoreApplication::exit(-1); }, Qt::QueuedConnection);
|
||||
|
||||
engine.loadFromModule("qtgrpc.examples.magic8ball", "Main");
|
||||
return app.exec();
|
||||
|
|
|
@ -5,7 +5,7 @@ syntax = "proto3";
|
|||
package qtgrpc.examples;
|
||||
|
||||
message AnswerRequest {
|
||||
string message = 1;
|
||||
string question = 1;
|
||||
}
|
||||
|
||||
message AnswerResponse {
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# Copyright (C) 2024 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(MagicServerRunner LANGUAGES CXX)
|
||||
project(magic8ball_server LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)
|
||||
|
||||
# Qt6::Grpc module is not used directly in this project. But this allows to find Qt6::Grpc's
|
||||
# dependencies without setting extra cmake module paths.
|
||||
find_package(Qt6 COMPONENTS Grpc)
|
||||
find_package(protobuf)
|
||||
find_package(gRPC)
|
||||
|
||||
if(NOT TARGET gRPC::grpc_cpp_plugin OR NOT TARGET WrapProtoc::WrapProtoc
|
||||
OR NOT TARGET gRPC::grpc++)
|
||||
message(WARNING "Dependencies of QtGrpc test server not found. Skipping.")
|
||||
message(WARNING "Dependencies of QtGrpc magic8ball_server not found. Skipping.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
|
@ -53,44 +50,24 @@ add_custom_command(
|
|||
)
|
||||
|
||||
set_source_files_properties(${generated_files} PROPERTIES GENERATED TRUE)
|
||||
add_library(ServerRunner_grpc_gen STATIC ${generated_files})
|
||||
target_include_directories(ServerRunner_grpc_gen
|
||||
|
||||
qt_add_executable(magic8ball_server
|
||||
${generated_files}
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_include_directories(magic8ball_server
|
||||
PRIVATE
|
||||
${out_dir}
|
||||
)
|
||||
|
||||
target_link_libraries(ServerRunner_grpc_gen
|
||||
target_link_libraries(magic8ball_server
|
||||
PRIVATE
|
||||
protobuf::libprotobuf
|
||||
gRPC::grpc++
|
||||
)
|
||||
|
||||
add_library(MagicServerRunner
|
||||
STATIC
|
||||
serverrunner.cpp
|
||||
serverrunner.h
|
||||
)
|
||||
|
||||
target_include_directories(MagicServerRunner PRIVATE ${out_dir})
|
||||
|
||||
target_link_libraries(MagicServerRunner
|
||||
PRIVATE
|
||||
ServerRunner_grpc_gen
|
||||
protobuf::libprotobuf
|
||||
gRPC::grpc++
|
||||
Qt6::Core
|
||||
)
|
||||
|
||||
qt_add_executable(SimpleGrpcServer
|
||||
main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(SimpleGrpcServer PRIVATE
|
||||
Qt6::Core
|
||||
MagicServerRunner
|
||||
)
|
||||
|
||||
install(TARGETS SimpleGrpcServer
|
||||
install(TARGETS magic8ball_server
|
||||
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "exampleservice.grpc.pb.h"
|
||||
|
||||
#include <grpc++/grpc++.h>
|
||||
|
||||
#include <array>
|
||||
#include <iostream>
|
||||
|
||||
using namespace qtgrpc::examples;
|
||||
|
||||
// Logic and data behind the server's behavior.
|
||||
class ExampleServiceImpl final : public ExampleService::Service
|
||||
{
|
||||
inline static std::array<std::string, 10> answers = {
|
||||
"Yes", "Yep", "Most\nlikely", "It is\ncertain", "No",
|
||||
"Nope", "Try later", "Are you\nsure?", "Maybe", "Very\ndoubtful"
|
||||
};
|
||||
|
||||
std::string getRandomAnswer()
|
||||
{
|
||||
return answers.at(rand() % answers.size());
|
||||
}
|
||||
//! [answerMethod]
|
||||
grpc::Status answerMethod(grpc::ServerContext *, const AnswerRequest *request,
|
||||
AnswerResponse *response) override
|
||||
{
|
||||
if (request->question().empty()) {
|
||||
std::cerr << "Question is empty" << std::endl;
|
||||
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Question is empty");
|
||||
}
|
||||
std::cout << "Received question: " << request->question() << std::endl;
|
||||
|
||||
response->set_message(getRandomAnswer());
|
||||
|
||||
return grpc::Status();
|
||||
};
|
||||
//! [answerMethod]
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
ExampleServiceImpl service;
|
||||
|
||||
grpc::ServerBuilder builder;
|
||||
builder.AddListeningPort("127.0.0.1:50051", grpc::InsecureServerCredentials());
|
||||
builder.RegisterService(&service);
|
||||
|
||||
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
|
||||
if (!server) {
|
||||
std::cout << "Creating server failed." << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::cout << "Server listening on port 50051" << std::endl;
|
||||
server->Wait();
|
||||
|
||||
return 0;
|
||||
}
|
Loading…
Reference in New Issue