The example of Qt gRPC-Client part with qml based UI

Pick-to: 6.5
Task-number: QTBUG-109598
Change-Id: I670f779fb9a85d02ad69a54dc6adaa50f52a7a71
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Alexey Edelev <alexey.edelev@qt.io>
This commit is contained in:
Tatiana Borisova 2023-01-13 16:31:42 +01:00
parent 613b3de08e
commit 681d104f0d
22 changed files with 792 additions and 2 deletions

View File

@ -12,7 +12,8 @@ project(QtGrpc
)
find_package(Qt6 ${PROJECT_VERSION} CONFIG REQUIRED COMPONENTS BuildInternals Core)
find_package(Qt6 ${PROJECT_VERSION} CONFIG OPTIONAL_COMPONENTS Network Gui Widgets)
find_package(Qt6 ${PROJECT_VERSION} CONFIG OPTIONAL_COMPONENTS Network Gui Widgets Quick
QuickControls2)
# Try to find Qt6::qtprotobufgen and Qt6::qtgrpcgen targets from host tools
# when cross-compiling.

View File

@ -2,3 +2,6 @@ dependencies:
../qtbase:
ref: 360f69b74b5e28ea1cfb0ed1ead624d0323dfe09
required: true
../qtdeclarative:
ref: 0737bb84e2bcf6acafe8e9892179234fd0926bdf
required: false

View File

@ -5,4 +5,7 @@ qt_examples_build_begin(EXTERNAL_BUILD)
add_subdirectory(protobuf)
if(TARGET Qt6::Grpc)
add_subdirectory(grpc)
endif()
qt_examples_build_end()

View File

@ -0,0 +1,9 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
if(TARGET Qt6::Quick
AND TARGET Qt6::QuickControls2
AND TARGET Qt6::qtprotobufgen
AND TARGET Qt6::qtgrpcgen)
qt_internal_add_example(magic8ball)
endif()

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -0,0 +1,55 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
/*!
\example magic8ball
\meta category {Networking}
\ingroup qtgrpc-examples
\title Magic 8 Ball
\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 has the following components:
\list
\li \c magic8ball Qt GRPC client application that includes
the \l {qt_add_protobuf} and \l {qt_add_grpc}
CMake functions for message and service Qt code generation.
\li \c SimpleGrpcServer application that calls C++ gRPC plugin
for generating server code and implementing simple server
logic.
\endlist
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 binds on the \c localhost with port
\c 50051:
\quotefromfile magic8ball/clientservice.cpp
\skipto new QGrpcHttp2Channel
\printuntil m_client->attachChannel(channel);
And sends a request to the server part:
\quotefromfile magic8ball/clientservice.cpp
\skipto void ClientService::sendRequest()
\printuntil }
Click the \uicontrol {Ask question} button to send
the request to the SimpleGrpcServer application.
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 }
After receiving a response the client application shows the answer.
*/

View File

@ -0,0 +1,42 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
Item {
id: root
signal closed()
property alias closingAnimation: closeAnimator
property alias openingAnimation: openAnimator
property alias animationText: result.text
MagicText {
id: result
anchors.centerIn: parent
font.pointSize: text.length > 12 ? 14 : 16
color: "#2E53B6"
ScaleAnimator on scale {
id: openAnimator
target: result
from: 0
to: 1
duration: 2000
running: false
}
ScaleAnimator on scale {
id: closeAnimator
target: result
from: 1
to: 0
duration: 2000
running: false
onStopped: root.closed()
}
}
}

View File

@ -0,0 +1,61 @@
cmake_minimum_required(VERSION 3.16)
project(Magic8Ball LANGUAGES CXX)
if(NOT DEFINED INSTALL_EXAMPLESDIR)
set(INSTALL_EXAMPLESDIR "examples")
endif()
set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/grpc/magic8ball")
find_package(Qt6 REQUIRED COMPONENTS Core Protobuf Grpc Quick)
qt_standard_project_setup()
add_subdirectory(grpc_server_example)
qt_add_executable(magic8ball
main.cpp
)
qt_add_protobuf(magic8ball
PROTO_FILES
proto/exampleservice.proto
)
qt_add_grpc(magic8ball CLIENT
PROTO_FILES
proto/exampleservice.proto
)
set_target_properties(magic8ball PROPERTIES
WIN32_EXECUTABLE TRUE
MACOSX_BUNDLE TRUE
)
qt_add_qml_module(magic8ball
URI qtgrpc.examples.magic8ball
VERSION 1.0
AUTO_RESOURCE_PREFIX
SOURCES
clientservice.h
clientservice.cpp
QML_FILES
"WaitingAnimation.qml"
"AnimatedAnswer.qml"
"MagicText.qml"
"ProgressDot.qml"
"Main.qml"
)
target_link_libraries(magic8ball PRIVATE
Qt6::Core
Qt6::Quick
Qt6::Protobuf
Qt6::Grpc
)
install(TARGETS magic8ball
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)

View File

@ -0,0 +1,14 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
Text {
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
font.family: "Helvetica"
font.pointSize: 16
color: "#264BAF"
style: Text.Sunken
}

View File

@ -0,0 +1,210 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import QtQuick
import QtQuick.Controls
import QtQuick.Shapes
import qtgrpc.examples.magic8ball
ApplicationWindow {
id: root
width: 665
height: width
minimumWidth: width
minimumHeight: height
visible: true
title: qsTr("Magic-8-ball Qt GRPC Example")
property string textAnswer: ""
property string textError: ""
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: 0.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: ClientService
function onMessageRecieved(value) {
root.textAnswer = value
}
function onErrorRecieved(value) {
root.textError = value
}
}
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: 3000
repeat: false
running: false
onTriggered: ClientService.setMessage()
onRunningChanged: {
if (running) {
answer.closingAnimation.start()
ClientService.sendRequest()
} else {
waitingAnimation.runAnimation = false
waitingAnimation.visible = false
answer.animationText = root.textError === "" ? root.textAnswer : root.textError
}
}
}
footer: MagicText {
text: root.textError === "" ? "" : "Please, start server: ../magic8ball/SimpleGrpcServer"
}
}

View File

@ -0,0 +1,12 @@
// 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
}

View File

@ -0,0 +1,54 @@
// 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: 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 (runAnimation)
openning.start()
}
easing.amplitude: 6.0
easing.period: 2.5
}
}

View File

@ -0,0 +1,50 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "clientservice.h"
#include <QGrpcHttp2Channel>
#include <QGrpcInsecureChannelCredentials>
using namespace qtgrpc::examples;
ClientService::ClientService(QObject *parent) :
QObject(parent),
m_client(new ExampleService::Client),
m_response(new AnswerResponse)
{
connect(m_client.get(), &qtgrpc::examples::ExampleService::Client::errorOccurred,
this, &ClientService::errorOccurred);
auto channel = std::shared_ptr<QAbstractGrpcChannel>(new QGrpcHttp2Channel(
QUrl("http://localhost:50051",
QUrl::StrictMode),
QGrpcInsecureChannelCredentials()
| QGrpcInsecureCallCredentials()));
m_client->attachChannel(channel);
}
void ClientService::errorOccurred()
{
qWarning() << "Connection error occurred. Have you started server part:"
" ../magic8ball/SimpleGrpcServer?";
emit errorRecieved("No connection\nto\nserver");
}
void ClientService::sendRequest()
{
// clean error on UI before new request
emit errorRecieved("");
qtgrpc::examples::AnswerRequest request;
request.setMessage("sleep");
m_client->answerMethod(request, m_response.get());
}
void ClientService::setMessage()
{
emit messageRecieved(m_response.get()->message());
}

View File

@ -0,0 +1,36 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef CLIENT_SERVICE_H
#define CLIENT_SERVICE_H
#include <QObject>
#include <QString>
#include <qqmlregistration.h>
#include <memory>
#include "exampleservice.qpb.h"
#include "exampleservice_client.grpc.qpb.h"
class ClientService : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
explicit ClientService(QObject *parent = nullptr);
Q_INVOKABLE void sendRequest();
Q_INVOKABLE void setMessage();
void errorOccurred();
signals:
void messageRecieved(const QString &value);
void errorRecieved(const QString &value);
private:
std::unique_ptr<qtgrpc::examples::ExampleService::Client> m_client;
std::unique_ptr<qtgrpc::examples::AnswerResponse> m_response;
};
#endif // CLIENT_SERVICE_H

View File

@ -0,0 +1,82 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
# 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(WrapgRPCPlugin)
find_package(WrapgRPC)
if(NOT TARGET WrapgRPC::WrapgRPCPlugin OR NOT TARGET WrapProtoc::WrapProtoc
OR NOT TARGET WrapgRPC::WrapLibgRPC)
message(WARNING "Dependencies of QtGrpc test server not found. Skipping.")
return()
endif()
set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/exampleservice.proto")
set(out_dir ${CMAKE_CURRENT_BINARY_DIR})
set(generated_files
"${out_dir}/exampleservice.pb.h" "${out_dir}/exampleservice.pb.cc"
"${out_dir}/exampleservice.grpc.pb.h" "${out_dir}/exampleservice.grpc.pb.cc")
add_custom_command(
OUTPUT ${generated_files}
COMMAND
$<TARGET_FILE:WrapProtoc::WrapProtoc>
ARGS
--grpc_out "${out_dir}"
--cpp_out "${out_dir}"
-I "${CMAKE_CURRENT_LIST_DIR}/../proto/"
--plugin=protoc-gen-grpc=$<TARGET_FILE:WrapgRPC::WrapgRPCPlugin>
"${proto_files}"
WORKING_DIRECTORY ${out_dir}
DEPENDS "${proto_files}"
COMMENT "Generating gRPC ${target} sources..."
COMMAND_EXPAND_LISTS
VERBATIM
)
set_source_files_properties(${generated_files} PROPERTIES GENERATED TRUE)
add_library(ServerRunner_grpc_gen STATIC ${generated_files})
target_include_directories(ServerRunner_grpc_gen
PRIVATE
${out_dir}
WrapgRPC_INCLUDE_PATH
)
target_link_libraries(ServerRunner_grpc_gen
PRIVATE
WrapProtobuf::WrapLibProtobuf
WrapgRPC::WrapLibgRPC
)
add_library(MagicServerRunner
STATIC
serverrunner.cpp
serverrunner.h
)
target_include_directories(MagicServerRunner PRIVATE ${out_dir})
target_link_libraries(MagicServerRunner
PRIVATE
ServerRunner_grpc_gen
WrapgRPC::WrapLibgRPC
Qt6::Core
)
qt_add_executable(SimpleGrpcServer
main.cpp
)
target_link_libraries(SimpleGrpcServer PRIVATE
Qt6::Core
MagicServerRunner
)
install(TARGETS SimpleGrpcServer
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
)

View File

@ -0,0 +1,15 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#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();
}

View File

@ -0,0 +1,81 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#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();
}

View File

@ -0,0 +1,13 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef SERVER_RUNNER_H
#define SERVER_RUNNER_H
class ExampleServer
{
public:
void run();
};
#endif // SERVER_RUNNER_H

View File

@ -0,0 +1,20 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include "clientservice.h"
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
&app, [](){
QCoreApplication::exit(-1);
}, Qt::QueuedConnection);
engine.loadFromModule("qtgrpc.examples.magic8ball", "Main");
return app.exec();
}

View File

@ -0,0 +1,15 @@
syntax = "proto3";
package qtgrpc.examples;
message AnswerRequest {
string message = 1;
}
message AnswerResponse {
string message = 1;
}
service ExampleService {
rpc answerMethod(AnswerRequest) returns (AnswerResponse) {}
}

View File

@ -31,9 +31,12 @@ depends += qtdoc qtcore qtnetwork qtwidgets qtprotobuf qtcmake
{headerdirs,sourcedirs} += ..
url.examples = "https://code.qt.io/cgit/qt/qtgrpc.git/tree/examples/\1?h=$QT_VER"
exampledirs += ../../../examples/grpc
imagedirs += images
imagedirs += images \
../../../examples/grpc/doc/images
navigation.landingpage = "Qt GRPC"
navigation.cppclassespage = "Qt GRPC C++ Classes"

View File

@ -10,3 +10,14 @@
\brief The Qt GRPC module provides support for communicating with gRPC services.
*/
/*!
\group qtgrpc-examples
\ingroup all-examples
\keyword Qt GRPC Examples
\title Qt GRPC Examples
\brief A collection of examples for \l {Qt GRPC C++ Classes}
These examples demonstrate how to generate code using the protobuf and gRPC schemas,
and use it in your projects.
*/