├── .gitignore
├── EasyQPainter.pro
├── LICENSE
├── README.md
├── img
├── demo
│ ├── 1.png
│ ├── 10.png
│ ├── 11.png
│ ├── 2.png
│ ├── 3.png
│ ├── 4.png
│ ├── 5.png
│ ├── 6.png
│ ├── 7.png
│ ├── 8.png
│ └── 9.png
├── heart.png
├── hehua.png
├── icon.ico
└── img.qrc
└── src
├── GlobalDef.h
├── MainWindow.cpp
├── MainWindow.h
├── MainWindow.ui
├── main.cpp
├── src.pri
├── tab3d
├── Cube3D.cpp
├── Cube3D.h
├── Tab3D.cpp
├── Tab3D.h
├── Tab3D.ui
├── Windmill3D.cpp
├── Windmill3D.h
└── tab3d.pri
├── tabbasic
├── BasicComposition.cpp
├── BasicComposition.h
├── BasicCurve.cpp
├── BasicCurve.h
├── BasicImage.cpp
├── BasicImage.h
├── PenColor.cpp
├── PenColor.h
├── PenStyle.cpp
├── PenStyle.h
├── TabBasic.cpp
├── TabBasic.h
├── TabBasic.ui
├── TextPath.cpp
├── TextPath.h
└── tabbasic.pri
├── tabchart
├── PieView.cpp
├── PieView.h
├── TabChart.cpp
├── TabChart.h
├── TabChart.ui
├── XYAxis.cpp
├── XYAxis.h
├── XYView.cpp
├── XYView.h
└── tabchart.pri
└── tabdraw
├── CalcDegree.cpp
├── CalcDegree.h
├── CalcPos.cpp
├── CalcPos.h
├── DrawAnimation.cpp
├── DrawAnimation.h
├── DrawBezier.cpp
├── DrawBezier.h
├── DrawCircle.cpp
├── DrawCircle.h
├── LedLattice.cpp
├── LedLattice.h
├── PlanetSystem.cpp
├── PlanetSystem.h
├── SimpleSelection.cpp
├── SimpleSelection.h
├── SineWave.cpp
├── SineWave.h
├── TabDraw.cpp
├── TabDraw.h
├── TabDraw.ui
├── TaiJi.cpp
├── TaiJi.h
└── tabdraw.pri
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pro.user
2 | /bin
3 |
--------------------------------------------------------------------------------
/EasyQPainter.pro:
--------------------------------------------------------------------------------
1 | QT += core gui widgets concurrent
2 |
3 | CONFIG += c++11 utf8_source
4 |
5 | DEFINES += QT_DEPRECATED_WARNINGS
6 |
7 | win32{
8 | DEFINES += NOMINMAX
9 | RC_ICONS = $$PWD/img/icon.ico
10 | }
11 |
12 | DESTDIR = $$PWD/bin
13 |
14 | INCLUDEPATH += $$PWD/src
15 | include($$PWD/src/src.pri)
16 |
17 | DISTFILES += \
18 | LICENSE \
19 | README.md
20 |
21 | RESOURCES += \
22 | img/img.qrc
23 |
24 | # Default rules for deployment.
25 | qnx: target.path = /tmp/$${TARGET}/bin
26 | else: unix:!android: target.path = /opt/$${TARGET}/bin
27 | !isEmpty(target.path): INSTALLS += target
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2025 龚建波
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EasyQPainter
2 |
3 | Various operation practices of Qt QPainter.(Qt QPainter 的各种操作实践)
4 |
5 | I named this demo "What is QPainter".(我给这个 Demo 命名为 "何为 QPainter")
6 |
7 | # Environment (开发环境)
8 |
9 | (2025-01-19)Win10/Win11 64bit + Qt5.15.2/Qt6.8.1 + MSVC2019/MSVC2022 64bit
10 |
11 | # Note (备注)
12 |
13 | ### version compatible(版本兼容 2022-06-26)
14 |
15 | There might be some incompatibility between different QT version header directives, please adjust includes based on your QT SDK version. (版本之间可能有些接口不兼容,或者头文件变动,请自行调整)
16 | > 如:低版本升级到 Qt5.15 上需要显式引入 QPainterPath;QWheelEvent pos() 需改用 position(),delta() 需改用 angleDelta();QFontMetrics width() 需改用 horizontalAdvance() 等...
17 |
18 | # Demo Show (展示)
19 |
20 | 
21 |
22 | 
23 |
24 | 
25 |
26 | 
27 |
28 | 
29 |
30 | 
31 |
32 | 
33 |
34 | 
35 |
36 | 
37 |
38 | 
39 |
40 | 
41 |
--------------------------------------------------------------------------------
/img/demo/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/1.png
--------------------------------------------------------------------------------
/img/demo/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/10.png
--------------------------------------------------------------------------------
/img/demo/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/11.png
--------------------------------------------------------------------------------
/img/demo/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/2.png
--------------------------------------------------------------------------------
/img/demo/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/3.png
--------------------------------------------------------------------------------
/img/demo/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/4.png
--------------------------------------------------------------------------------
/img/demo/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/5.png
--------------------------------------------------------------------------------
/img/demo/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/6.png
--------------------------------------------------------------------------------
/img/demo/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/7.png
--------------------------------------------------------------------------------
/img/demo/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/8.png
--------------------------------------------------------------------------------
/img/demo/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/demo/9.png
--------------------------------------------------------------------------------
/img/heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/heart.png
--------------------------------------------------------------------------------
/img/hehua.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/hehua.png
--------------------------------------------------------------------------------
/img/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gongjianbo/EasyQPainter/8cf0f02cec3c0e539e31fbee3f6b4b9187fae4d8/img/icon.ico
--------------------------------------------------------------------------------
/img/img.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | hehua.png
4 | icon.ico
5 | heart.png
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/GlobalDef.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
4 | #define GetTextWidth(fm, text) fm.width(text)
5 | #else
6 | #define GetTextWidth(fm, text) fm.horizontalAdvance(text)
7 | #endif
8 | #define GetTextHeight(fm) fm.height()
9 |
10 | #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
11 | #define GetMousePos(evt) evt->pos()
12 | #define GetMouseDelta(evt) evt->delta()
13 | #else
14 | #define GetMousePos(evt) evt->position().toPoint()
15 | #define GetMouseDelta(evt) evt->angleDelta().y()
16 | #endif
17 |
--------------------------------------------------------------------------------
/src/MainWindow.cpp:
--------------------------------------------------------------------------------
1 | #include "MainWindow.h"
2 | #include "ui_MainWindow.h"
3 | #include
4 |
5 | MainWindow::MainWindow(QWidget *parent)
6 | : QMainWindow{parent}
7 | , ui{new Ui::MainWindow}
8 | {
9 | ui->setupUi(this);
10 | setWindowIcon(QIcon(":/icon.ico"));
11 | ui->tabWidget->setCurrentIndex(0);
12 | }
13 |
14 | MainWindow::~MainWindow()
15 | {
16 | delete ui;
17 | }
18 |
--------------------------------------------------------------------------------
/src/MainWindow.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 |
4 | namespace Ui {
5 | class MainWindow;
6 | }
7 |
8 | // 主窗口,包含各分组页面
9 | class MainWindow : public QMainWindow
10 | {
11 | Q_OBJECT
12 | public:
13 | explicit MainWindow(QWidget *parent = nullptr);
14 | ~MainWindow();
15 |
16 | private:
17 | Ui::MainWindow *ui;
18 | };
19 |
--------------------------------------------------------------------------------
/src/MainWindow.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 |
7 | 0
8 | 0
9 | 800
10 | 600
11 |
12 |
13 |
14 | Easy QPainter (By: GongJianBo 1992)
15 |
16 |
17 |
18 | -
19 |
20 |
21 | 0
22 |
23 |
24 |
25 | Basic
26 |
27 |
28 |
29 |
30 | Draw
31 |
32 |
33 |
34 |
35 | 3D
36 |
37 |
38 |
39 |
40 | Chart
41 |
42 |
43 |
44 |
45 |
46 |
47 |
57 |
58 |
59 |
60 |
61 | TabBasic
62 | QWidget
63 |
64 | 1
65 |
66 |
67 | TabDraw
68 | QWidget
69 |
70 | 1
71 |
72 |
73 | Tab3D
74 | QWidget
75 |
76 | 1
77 |
78 |
79 | TabChart
80 | QWidget
81 |
82 | 1
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/src/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include "MainWindow.h"
3 |
4 | int main(int argc, char *argv[])
5 | {
6 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
7 | QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
8 | QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
9 | #endif
10 | QApplication app(argc, argv);
11 | MainWindow window;
12 | window.resize(800, 600);
13 | window.show();
14 | return app.exec();
15 | }
16 |
--------------------------------------------------------------------------------
/src/src.pri:
--------------------------------------------------------------------------------
1 | HEADERS += \
2 | $$PWD/GlobalDef.h \
3 | $$PWD/MainWindow.h
4 |
5 | SOURCES += \
6 | $$PWD/MainWindow.cpp \
7 | $$PWD/main.cpp
8 |
9 | FORMS += \
10 | $$PWD/MainWindow.ui
11 |
12 | INCLUDEPATH += $$PWD/tabbasic
13 | include($$PWD/tabbasic/tabbasic.pri)
14 | INCLUDEPATH += $$PWD/tabdraw
15 | include($$PWD/tabdraw/tabdraw.pri)
16 | INCLUDEPATH += $$PWD/tab3d
17 | include($$PWD/tab3d/tab3d.pri)
18 | INCLUDEPATH += $$PWD/tabchart
19 | include($$PWD/tabchart/tabchart.pri)
20 |
--------------------------------------------------------------------------------
/src/tab3d/Cube3D.cpp:
--------------------------------------------------------------------------------
1 | #include "Cube3D.h"
2 | #include "GlobalDef.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | Cube3D::Cube3D(QWidget *parent)
9 | : QWidget(parent)
10 | {
11 | // 7------------------4
12 | // / / |
13 | // 3------------------0 |
14 | // | | |
15 | // | | |
16 | // | | |
17 | // | | |
18 | // | 6 | 5
19 | // | | /
20 | // 2------------------1
21 | // 立方体前后四个顶点,从右上角开始顺时针
22 | vertexArr = QVector{
23 | QVector3D{1, 1, 1},
24 | QVector3D{1, -1, 1},
25 | QVector3D{-1, -1, 1},
26 | QVector3D{-1, 1, 1},
27 |
28 | QVector3D{1, 1, -1},
29 | QVector3D{1, -1, -1},
30 | QVector3D{-1, -1, -1},
31 | QVector3D{-1, 1, -1}};
32 |
33 | // 六个面,一个面包含四个顶点
34 | elementArr = QVector>{
35 | {0, 1, 2, 3},
36 | {4, 5, 6, 7},
37 | {0, 4, 5, 1},
38 | {1, 5, 6, 2},
39 | {2, 6, 7, 3},
40 | {3, 7, 4, 0}};
41 |
42 | // Widget 默认没有焦点,此处设置为点击时获取焦点
43 | setFocusPolicy(Qt::ClickFocus);
44 | }
45 |
46 | void Cube3D::paintEvent(QPaintEvent *event)
47 | {
48 | Q_UNUSED(event)
49 | QPainter painter(this);
50 | // 先画一个白底黑框
51 | painter.fillRect(this->rect(), Qt::white);
52 | QPen pen(Qt::black);
53 | painter.setPen(pen);
54 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); // 右下角会超出范围
55 |
56 | // 思路,找到 z 值最高的顶点,然后绘制该顶点相邻的面
57 | // 根据 z 值计算,近大远小
58 | // (此外,Qt 是屏幕坐标系,原点在左上角)
59 |
60 | // 矩形边框参考大小
61 | const int cube_width = (width() > height() ? height() : width()) / 4;
62 |
63 | // 投影矩阵
64 | QMatrix4x4 perspective_mat;
65 | perspective_mat.perspective(projectionFovy, 1.0f, 0.1f, 100.0f);
66 | // 观察矩阵
67 | QMatrix4x4 view_mat;
68 | view_mat.translate(0.0f, 0.0f, -5.0f);
69 | view_mat.rotate(rotationQuat);
70 |
71 | // 计算顶点变换后坐标,包含 z 值 max 点就是正交表面可见的,
72 | // 再计算下远小近大的透视投影效果齐活了
73 | QList vertex_list; // 和矩阵运算后的顶点
74 | QList vertex_max_list; // z 最大值列表(z 值可能重复),内容为 vertexArr 的下标
75 | float vertex_max_value; // 顶点列表 z 最大值
76 | // 根据旋转矩阵计算每个顶点
77 | for (int i = 0; i < vertexArr.count(); i++)
78 | {
79 | // 以物体中心为原点旋转
80 | // 相乘顺序和 glsl 一致
81 | QVector3D vertex = perspective_mat * view_mat * vertexArr.at(i);
82 | vertex.setZ(-vertex.z());
83 | vertex.setY(-vertex.y());
84 | vertex_list.push_back(vertex);
85 | // 找出 z 值 max 的顶点
86 | if (i == 0)
87 | {
88 | vertex_max_list.push_back(0);
89 | vertex_max_value = vertex.z();
90 | }
91 | else
92 | {
93 | if (vertex.z() > vertex_max_value)
94 | {
95 | // 找最大的 z 值
96 | vertex_max_list.clear();
97 | vertex_max_list.push_back(i);
98 | vertex_max_value = vertex.z();
99 | }
100 | else if (abs(vertex.z() - vertex_max_value) < (1E-7))
101 | {
102 | // 和最大 z 值相等的也添加到列表
103 | vertex_max_list.push_back(i);
104 | }
105 | }
106 | }
107 |
108 | // 把原点移到中间来,方便绘制
109 | painter.save();
110 | painter.translate(width() / 2, height() / 2);
111 | // 绘制 front 和 back 六个面,先计算路径再绘制
112 | QList element_path_list; // 每个面路径
113 | QList element_z_values; // 每个面中心点的z值
114 | QList element_z_points; // 每个面中心点在平面对应 xy 值
115 | QList element_front_list; // elementArr 中表面的 index
116 | // 计算每个表面
117 | for (int i = 0; i < elementArr.count(); i++)
118 | {
119 | // 每个面四个顶点
120 | const QVector3D &vt0 = vertex_list.at(elementArr.at(i).at(0));
121 | const QVector3D &vt1 = vertex_list.at(elementArr.at(i).at(1));
122 | const QVector3D &vt2 = vertex_list.at(elementArr.at(i).at(2));
123 | const QVector3D &vt3 = vertex_list.at(elementArr.at(i).at(3));
124 |
125 | // 单个面的路径,面根据大小等比放大
126 | QPainterPath element_path;
127 | element_path.moveTo(getPoint(vt0, cube_width));
128 | element_path.lineTo(getPoint(vt1, cube_width));
129 | element_path.lineTo(getPoint(vt2, cube_width));
130 | element_path.lineTo(getPoint(vt3, cube_width));
131 | element_path.closeSubpath();
132 |
133 | // 包含 zmax 点的就是正交表面可见的
134 | bool is_front = true;
135 | for (int vertex_index : vertex_max_list)
136 | {
137 | if (!elementArr.at(i).contains(vertex_index))
138 | {
139 | is_front = false;
140 | break;
141 | }
142 | }
143 | if (is_front)
144 | {
145 | element_front_list.push_back(i);
146 | }
147 | element_path_list.push_back(element_path);
148 | // 对角线中间点作为面的 z
149 | element_z_values.push_back((vt0.z() + vt2.z()) / 2);
150 | // 对角线中间点
151 | element_z_points.push_back((getPoint(vt0, cube_width) + getPoint(vt2, cube_width)) / 2);
152 | }
153 |
154 | // 远小近大,还要把包含 max 但是被近大遮盖的去掉
155 | QList element_front_remove;
156 | for (int i = 0; i < element_front_list.count(); i++)
157 | {
158 | for (int j = 0; j < element_front_list.count(); j++)
159 | {
160 | if (i == j)
161 | continue;
162 | const int index_i = element_front_list.at(i);
163 | const int index_j = element_front_list.at(j);
164 | if (element_z_values.at(index_i) > element_z_values.at(index_j) && element_path_list.at(index_i).contains(element_z_points.at(index_j)))
165 | {
166 | element_front_remove.push_back(index_j);
167 | }
168 | }
169 | }
170 | for (int index : element_front_remove)
171 | {
172 | element_front_list.removeOne(index);
173 | }
174 |
175 | // 根据计算好的路径绘制
176 | painter.setRenderHint(QPainter::Antialiasing, true);
177 | // 画表面
178 | for (auto index : element_front_list)
179 | {
180 | painter.fillPath(element_path_list.at(index), Qt::green);
181 | }
182 | // 画被遮盖面的边框虚线
183 | painter.setPen(QPen(Qt::white, 1, Qt::DashLine));
184 | for (int i = 0; i < element_path_list.count(); i++)
185 | {
186 | if (element_front_list.contains(i))
187 | continue;
188 | painter.drawPath(element_path_list.at(i));
189 | }
190 | // 画表面边框
191 | painter.setPen(QPen(Qt::black, 2));
192 | for (auto index : element_front_list)
193 | {
194 | painter.drawPath(element_path_list.at(index));
195 | }
196 | painter.restore();
197 |
198 | painter.drawText(20, 30, "Drag Moving");
199 | }
200 |
201 | void Cube3D::mousePressEvent(QMouseEvent *event)
202 | {
203 | mousePressed = true;
204 | mousePos = event->pos();
205 | QWidget::mousePressEvent(event);
206 | }
207 |
208 | void Cube3D::mouseMoveEvent(QMouseEvent *event)
209 | {
210 | if (mousePressed)
211 | {
212 | QVector2D diff = QVector2D(event->pos()) - QVector2D(mousePos);
213 | mousePos = event->pos();
214 | QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized();
215 | rotationAxis = (rotationAxis + n).normalized();
216 | // 不能对换乘的顺序
217 | rotationQuat = QQuaternion::fromAxisAndAngle(rotationAxis, 2.0f) * rotationQuat;
218 |
219 | update();
220 | }
221 | QWidget::mouseMoveEvent(event);
222 | }
223 |
224 | void Cube3D::mouseReleaseEvent(QMouseEvent *event)
225 | {
226 | mousePressed = false;
227 | QWidget::mouseReleaseEvent(event);
228 | }
229 |
230 | void Cube3D::wheelEvent(QWheelEvent *event)
231 | {
232 | event->accept();
233 | // const QPoint pos = GetMousePos(event);
234 | const int delta = GetMouseDelta(event);
235 | // fovy 越小,模型看起来越大
236 | if (delta < 0)
237 | {
238 | // 鼠标向下滑动为-,这里作为 zoom out
239 | projectionFovy += 0.5f;
240 | if (projectionFovy > 90)
241 | projectionFovy = 90;
242 | }
243 | else
244 | {
245 | // 鼠标向上滑动为+,这里作为zoom in
246 | projectionFovy -= 0.5f;
247 | if (projectionFovy < 1)
248 | projectionFovy = 1;
249 | }
250 | update();
251 | }
252 |
253 | QPointF Cube3D::getPoint(const QVector3D &vt, int w) const
254 | {
255 | // 可以用 z 来手动计算远小近大,也可以矩阵运算
256 | // const float z_offset=vt.z()*0.1;
257 | // return QPointF{ vt.x()*w*(1+z_offset), vt.y()*w*(1+z_offset) };
258 | return QPointF{vt.x() * w, vt.y() * w};
259 | }
260 |
--------------------------------------------------------------------------------
/src/tab3d/Cube3D.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | // 绘制一个立方体盒子
10 | class Cube3D : public QWidget
11 | {
12 | Q_OBJECT
13 | public:
14 | explicit Cube3D(QWidget *parent = nullptr);
15 |
16 | protected:
17 | void paintEvent(QPaintEvent *event) override;
18 | void mousePressEvent(QMouseEvent *event) override;
19 | void mouseMoveEvent(QMouseEvent *event) override;
20 | void mouseReleaseEvent(QMouseEvent *event) override;
21 | void wheelEvent(QWheelEvent *event) override;
22 |
23 | QPointF getPoint(const QVector3D &vt, int w) const;
24 |
25 | private:
26 | // 立方体八个顶点
27 | QVector vertexArr;
28 | // 立方体六个面
29 | QVector> elementArr;
30 | // 观察矩阵旋转
31 | QVector3D rotationAxis;
32 | QQuaternion rotationQuat;
33 | // 透视投影的fovy参数,视野范围
34 | float projectionFovy{30.0f};
35 |
36 | // 鼠标位置
37 | QPoint mousePos;
38 | // 鼠标按下标志位
39 | bool mousePressed{false};
40 | };
41 |
--------------------------------------------------------------------------------
/src/tab3d/Tab3D.cpp:
--------------------------------------------------------------------------------
1 | #include "Tab3D.h"
2 | #include "ui_Tab3D.h"
3 |
4 | Tab3D::Tab3D(QWidget *parent)
5 | : QWidget(parent)
6 | , ui(new Ui::Tab3D)
7 | {
8 | ui->setupUi(this);
9 | ui->tabWidget->setCurrentIndex(0);
10 | }
11 |
12 | Tab3D::~Tab3D()
13 | {
14 | delete ui;
15 | }
16 |
--------------------------------------------------------------------------------
/src/tab3d/Tab3D.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 |
4 | namespace Ui{
5 | class Tab3D;
6 | }
7 |
8 | // 简单的立体效果
9 | class Tab3D : public QWidget
10 | {
11 | Q_OBJECT
12 | public:
13 | explicit Tab3D(QWidget *parent = nullptr);
14 | ~Tab3D();
15 |
16 | private:
17 | Ui::Tab3D *ui;
18 | };
19 |
--------------------------------------------------------------------------------
/src/tab3d/Tab3D.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Tab3D
4 |
5 |
6 |
7 | 0
8 | 0
9 | 512
10 | 353
11 |
12 |
13 |
14 | Form
15 |
16 |
17 | -
18 |
19 |
20 | 1
21 |
22 |
23 |
24 | Cube
25 |
26 |
27 |
28 |
29 | Windmill
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Cube3D
39 | QWidget
40 |
41 | 1
42 |
43 |
44 | Windmill3D
45 | QWidget
46 |
47 | 1
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/tab3d/Windmill3D.cpp:
--------------------------------------------------------------------------------
1 | #include "Windmill3D.h"
2 | #include "GlobalDef.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | WindMeta::WindMeta(const QList &vtx, const QColor &clr)
14 | : vertex(vtx), color(clr)
15 | {
16 | }
17 |
18 | WindItem::WindItem(const QVector3D &pos, const QVector3D &rotate,
19 | const QList> &metas,
20 | const QList> &subs,
21 | const QVector3D &factor)
22 | : position(pos)
23 | , rotation(rotate)
24 | , surfaceMetas(metas)
25 | , subItems(subs)
26 | , animationFactor(factor)
27 | {
28 | }
29 |
30 | QList>
31 | WindItem::calcSurfaceMetas(const QVector3D &position, const QQuaternion &rotation, float step, float fovy)
32 | {
33 | QVector3D cur_position = position + this->position;
34 | // 这里没验证,因为目前只为 0,可能有误
35 | QQuaternion cur_rotation = QQuaternion::fromEulerAngles(this->rotation) * rotation;
36 | // 平移做裁剪,缩放拉近距离
37 | QMatrix4x4 perspective_mat;
38 | perspective_mat.scale(100);
39 | perspective_mat.perspective(fovy, 1.0f, 0.1f, 2000.0f);
40 | QMatrix4x4 view_mat;
41 | view_mat.translate(0.0f, 0.0f, -1000.0f);
42 | // 先跟随父节点转动和位移,再以自身的转动步进进行转动
43 | QMatrix4x4 model_mat;
44 | model_mat.rotate(cur_rotation);
45 | model_mat.translate(cur_position);
46 | model_mat.rotate(QQuaternion::fromEulerAngles(step * this->animationFactor));
47 | for (QSharedPointer meta : qAsConst(surfaceMetas))
48 | {
49 | QPainterPath path;
50 | double z;
51 | bool is_first = true;
52 | for (const QVector3D &vertex : qAsConst(meta->vertex))
53 | {
54 | // 相乘顺序和 glsl 一致
55 | QVector3D calc_vertex= perspective_mat * view_mat * model_mat * vertex;
56 | calc_vertex.setY(-calc_vertex.y());
57 | calc_vertex.setZ(-calc_vertex.z());
58 | // qDebug()<path = path;
77 | meta->z = z;
78 | }
79 | cur_rotation *= QQuaternion::fromEulerAngles(step * this->animationFactor);
80 | QList> surface_metas = surfaceMetas;
81 | for (QSharedPointer item : qAsConst(subItems))
82 | {
83 | surface_metas += item->calcSurfaceMetas(cur_position, cur_rotation, step, fovy);
84 | }
85 |
86 | return surface_metas;
87 | }
88 |
89 | Windmill3D::Windmill3D(QWidget *parent)
90 | : QWidget(parent)
91 | {
92 | initWindmill();
93 | // 异步处理结束,获取结果并刷新窗口
94 | connect(&watcher, &QFutureWatcher::finished, [this]() {
95 | image = watcher.result();
96 | update();
97 | });
98 |
99 | // 之前使用的 QTime,现在替换为 QElapsedTimer
100 | // QTime fpsTime = QTime::currentTime();
101 | fpsTime.start();
102 |
103 | // 定时旋转风车
104 | connect(&timer, &QTimer::timeout, this, [this]() {
105 | animationStep += 2.0f;
106 | drawImage(width(), height());
107 | });
108 | }
109 |
110 | Windmill3D::~Windmill3D()
111 | {
112 | if (!watcher.isFinished())
113 | watcher.waitForFinished();
114 | }
115 |
116 | void Windmill3D::showEvent(QShowEvent *event)
117 | {
118 | timer.start(50);
119 | QWidget::showEvent(event);
120 | }
121 |
122 | void Windmill3D::hideEvent(QHideEvent *event)
123 | {
124 | timer.stop();
125 | QWidget::hideEvent(event);
126 | }
127 |
128 | void Windmill3D::paintEvent(QPaintEvent *event)
129 | {
130 | event->accept();
131 | QPainter painter(this);
132 | painter.fillRect(this->rect(), Qt::black);
133 |
134 | if (image.size().isValid())
135 | painter.drawImage(0, 0, image);
136 |
137 | // fps 统计
138 | if (fpsTime.elapsed() > 1000)
139 | {
140 | fpsTime.restart();
141 | fpsCounter = fpsTemp;
142 | fpsTemp = 0;
143 | }
144 | else
145 | {
146 | fpsTemp++;
147 | }
148 | painter.setPen(QPen(Qt::white));
149 | painter.drawText(10, 30, "FPS:" + QString::number(fpsCounter));
150 | painter.drawText(10, 50, "Drag Moving ... ...");
151 | }
152 |
153 | void Windmill3D::mousePressEvent(QMouseEvent *event)
154 | {
155 | mousePressed = true;
156 | mousePos = event->pos();
157 | QWidget::mousePressEvent(event);
158 | }
159 |
160 | void Windmill3D::mouseMoveEvent(QMouseEvent *event)
161 | {
162 | if (mousePressed)
163 | {
164 | QVector2D diff = QVector2D(event->pos()) - QVector2D(mousePos);
165 | mousePos = event->pos();
166 | QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized();
167 | rotationAxis = (rotationAxis + n).normalized();
168 | // 不能对换乘的顺序
169 | rotationQuat = QQuaternion::fromAxisAndAngle(rotationAxis, 2.0f) * rotationQuat;
170 |
171 | // update();
172 | drawImage(width(), height());
173 | }
174 | QWidget::mouseMoveEvent(event);
175 | }
176 |
177 | void Windmill3D::mouseReleaseEvent(QMouseEvent *event)
178 | {
179 | mousePressed = false;
180 | QWidget::mouseReleaseEvent(event);
181 | }
182 |
183 | void Windmill3D::wheelEvent(QWheelEvent *event)
184 | {
185 | event->accept();
186 | // const QPoint pos = GetMousePos(event);
187 | const int delta = GetMouseDelta(event);
188 | // fovy 越小,模型看起来越大
189 | if (delta < 0)
190 | {
191 | // 鼠标向下滑动为-,这里作为zoom out
192 | projectionFovy += 0.5f;
193 | if (projectionFovy > 90)
194 | projectionFovy = 90;
195 | }
196 | else
197 | {
198 | // 鼠标向上滑动为+,这里作为zoom in
199 | projectionFovy -= 0.5f;
200 | if (projectionFovy < 1)
201 | projectionFovy = 1;
202 | }
203 | // update();
204 | drawImage(width(), height());
205 | }
206 |
207 | void Windmill3D::resizeEvent(QResizeEvent *event)
208 | {
209 | if (event->size().isValid())
210 | {
211 | const int width = event->size().width();
212 | const int height = event->size().height();
213 | drawImage(width, height);
214 | }
215 | QWidget::resizeEvent(event);
216 | }
217 |
218 | void Windmill3D::initWindmill()
219 | {
220 | // 参照荷兰风车,逆时针旋转,帆布往塔身一侧倾斜
221 | // 四个扇叶
222 | WindMeta *sub_fan1 = new WindMeta{{QVector3D(0, 0, 0), QVector3D(-250, -250, 0),
223 | QVector3D(-300, -200, -10), QVector3D(-100, 0, -10)},
224 | QColor(110, 250, 250, 200)};
225 | WindMeta *sub_fan2 = new WindMeta{{QVector3D(0, 0, 0), QVector3D(-250, 250, 0),
226 | QVector3D(-200, 300, -10), QVector3D(0, 100, -10)},
227 | QColor(130, 250, 250, 200)};
228 | WindMeta *sub_fan3 = new WindMeta{{QVector3D(0, 0, 0), QVector3D(250, 250, 0),
229 | QVector3D(300, 200, -10), QVector3D(100, 0, -10)},
230 | QColor(110, 250, 250, 200)};
231 | WindMeta *sub_fan4 = new WindMeta{{QVector3D(0, 0, 0), QVector3D(250, -250, 0),
232 | QVector3D(200, -300, -10), QVector3D(0, -100, -10)},
233 | QColor(130, 250, 250, 200)};
234 | auto sub_fanmetas = QList>{QSharedPointer(sub_fan1),
235 | QSharedPointer(sub_fan2),
236 | QSharedPointer(sub_fan3),
237 | QSharedPointer(sub_fan4)};
238 | auto sub_fansubs = QList>{};
239 | WindItem *sub_fanitem = new WindItem{
240 | QVector3D(0, 400, 150), // 相对位置,y400 放到顶部,z150 贴在墙上
241 | QVector3D(0, 0, 0), // 相对方向
242 | sub_fanmetas,
243 | sub_fansubs,
244 | QVector3D(0, 0, 1)}; // 给 z 加了动画因子,即扇叶在 xy 平面转
245 |
246 | // 风车主干,共 9 个面,顶部尖塔4 + 主干4 + 底面
247 | // 顶部4
248 | WindMeta *sub_main1 = new WindMeta{{QVector3D(100, 400, 100), QVector3D(-100, 400, 100), QVector3D(0, 500, 0)},
249 | QColor(250, 0, 0)};
250 | WindMeta *sub_main2 = new WindMeta{{QVector3D(-100, 400, 100), QVector3D(-100, 400, -100), QVector3D(0, 500, 0)},
251 | QColor(0, 250, 0)};
252 | WindMeta *sub_main3 = new WindMeta{{QVector3D(-100, 400, -100), QVector3D(100, 400, -100), QVector3D(0, 500, 0)},
253 | QColor(0, 0, 250)};
254 | WindMeta *sub_main4 = new WindMeta{{QVector3D(100, 400, -100), QVector3D(100, 400, 100), QVector3D(0, 500, 0)},
255 | QColor(250, 250, 0)};
256 | // 主体4
257 | WindMeta *sub_main5 = new WindMeta{{QVector3D(100, 400, 100), QVector3D(-100, 400, 100),
258 | QVector3D(-120, 0, 120), QVector3D(120, 0, 120)},
259 | QColor(205, 150, 100)};
260 | WindMeta *sub_main6 = new WindMeta{{QVector3D(-100, 400, 100), QVector3D(-100, 400, -100),
261 | QVector3D(-120, 0, -120), QVector3D(-120, 0, 120)},
262 | QColor(220, 150, 100)};
263 | WindMeta *sub_main7 = new WindMeta{{QVector3D(-100, 400, -100), QVector3D(100, 400, -100),
264 | QVector3D(120, 0, -120), QVector3D(-120, 0, -120)},
265 | QColor(235, 150, 100)};
266 | WindMeta *sub_main8 = new WindMeta{{QVector3D(100, 400, -100), QVector3D(100, 400, 100),
267 | QVector3D(120, 0, 120), QVector3D(120, 0, -120)},
268 | QColor(250, 150, 100)};
269 | // 底部1
270 | WindMeta *sub_main9 = new WindMeta{{QVector3D(-120, 0, 120), QVector3D(-120, 0, -120),
271 | QVector3D(120, 0, -120), QVector3D(120, 0, 120)},
272 | QColor(200, 150, 0)};
273 |
274 | auto sub_mainmetas = QList>{QSharedPointer(sub_main1),
275 | QSharedPointer(sub_main2),
276 | QSharedPointer(sub_main3),
277 | QSharedPointer(sub_main4),
278 | QSharedPointer(sub_main5),
279 | QSharedPointer(sub_main6),
280 | QSharedPointer(sub_main7),
281 | QSharedPointer(sub_main8),
282 | QSharedPointer(sub_main9)};
283 | auto sub_mainsubs = QList>{QSharedPointer(sub_fanitem)};
284 | WindItem *sub_mainitem = new WindItem{
285 | QVector3D(0, 0, 0), // 相对位置
286 | QVector3D(0, 0, 0), // 相对方向
287 | sub_mainmetas,
288 | sub_mainsubs};
289 |
290 | // 根节点,一个平面,(平面用半透明是为了穿模时看起来没那么别扭)
291 | WindMeta *root_meta = new WindMeta{{QVector3D(-200, 0, 200), QVector3D(200, 0, 200),
292 | QVector3D(200, 0, -200), QVector3D(-200, 0, -200)},
293 | QColor(255, 255, 255, 100)};
294 | auto root_metas = QList>{QSharedPointer(root_meta)};
295 | auto root_subs = QList>{QSharedPointer(sub_mainitem)};
296 | rootItem = WindItem{
297 | QVector3D(0, -300, 0), // 相对位置,y 轴 -300 相当于放到了底部
298 | QVector3D(0, 0, 0), // 相对方向
299 | root_metas,
300 | root_subs,
301 | QVector3D(0, -0.1f, 0)}; // 给 y 加了动画因子,即柱子在 xz 平面转
302 | }
303 |
304 | void Windmill3D::drawImage(int width, int height)
305 | {
306 | if (width > 10 && height > 10 && watcher.isFinished())
307 | {
308 | QQuaternion rotate = rotationQuat;
309 | float step = animationStep;
310 | float fovy = projectionFovy;
311 |
312 | // 多线程绘制到 image 上,绘制完后返回 image 并绘制到窗口上
313 | QFuture futures = QtConcurrent::run([this, width, height, rotate, step, fovy]() {
314 | QImage img(width, height, QImage::Format_ARGB32);
315 | img.fill(Qt::transparent);
316 | QPainter painter(&img);
317 | if (!painter.isActive())
318 | return img;
319 | painter.fillRect(img.rect(), Qt::black);
320 |
321 | // painter.save();
322 | // 坐标原点移动到中心
323 | painter.translate(width / 2, height / 2);
324 | // 抗锯齿
325 | painter.setRenderHint(QPainter::Antialiasing);
326 |
327 | // 计算所有的图元顶点路径
328 | QList> surface_metas = rootItem.calcSurfaceMetas(
329 | QVector3D(0, 0, 0), rotate, step, fovy);
330 | // 根据 z 轴排序
331 | std::sort(surface_metas.begin(), surface_metas.end(),
332 | [](const QSharedPointer &left, const QSharedPointer &right) {
333 | return left->z < right->z;
334 | });
335 | // 根据 z 值从远处开始绘制图元路径
336 | for (QSharedPointer meta : qAsConst(surface_metas))
337 | {
338 | painter.fillPath(meta->path, meta->color);
339 | }
340 |
341 | // painter.restore();
342 | return img;
343 | });
344 | watcher.setFuture(futures);
345 | }
346 | }
347 |
--------------------------------------------------------------------------------
/src/tab3d/Windmill3D.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 |
16 | // 图元结构体
17 | struct WindMeta
18 | {
19 | // 顶点,可以是任意个
20 | QList vertex;
21 | // 颜色
22 | QColor color;
23 | // QBrush brush;
24 | // 图元顶点中 z 值最小者,单独作为成员便于排序
25 | double z;
26 | // 根据定点计算出的路径,便于绘制
27 | QPainterPath path;
28 |
29 | // 构造函数
30 | WindMeta(const QList &vtx, const QColor &clr);
31 | };
32 |
33 | // 物体实体结构体
34 | struct WindItem
35 | {
36 | // 相对于场景或者父节点的坐标位置
37 | QVector3D position;
38 | // 相对于场景或者父节点的方向
39 | QVector3D rotation;
40 | // 包含的图元
41 | QList> surfaceMetas;
42 | // 子节点物体列表
43 | QList> subItems;
44 | // 旋转动画因子(根据全局的定时器步进值计算对应分量动画效果)
45 | QVector3D animationFactor;
46 |
47 | // 构造函数
48 | WindItem(const QVector3D &pos = QVector3D(0, 0, 0),
49 | const QVector3D &rotate = QVector3D(0, 0, 0),
50 | const QList> &metas = QList>(),
51 | const QList> &subs = QList>(),
52 | const QVector3D &factor = QVector3D(0, 0, 0));
53 |
54 | // 根据当前位置和角度计算出顶点列表
55 | // position 取出后直接叠加到顶点的坐标上: vertex + position + this->position
56 | // rotation 目前只计算了 x 和 y 的旋转,作用于 item 的顶点上,目前只能旋转 item
57 | // step 为定时器动画的步进,每个 item 根据自身的动画因子成员来计算
58 | QList>
59 | calcSurfaceMetas(const QVector3D &position, const QQuaternion &rotation, float step, float fovy);
60 | };
61 |
62 | // 绘制一个 3D 风车
63 | // (目前按大块平面来计算堆叠顺序效果不太好,两个物体交叉时会有一部分被覆盖)
64 | class Windmill3D : public QWidget
65 | {
66 | Q_OBJECT
67 | public:
68 | explicit Windmill3D(QWidget *parent = nullptr);
69 | ~Windmill3D();
70 |
71 | protected:
72 | // 显示时才启动定时动画
73 | void showEvent(QShowEvent *event) override;
74 | void hideEvent(QHideEvent *event) override;
75 | // 绘制
76 | void paintEvent(QPaintEvent *event) override;
77 | // 鼠标操作
78 | void mousePressEvent(QMouseEvent *event) override;
79 | void mouseMoveEvent(QMouseEvent *event) override;
80 | void mouseReleaseEvent(QMouseEvent *event) override;
81 | void wheelEvent(QWheelEvent *event) override;
82 | // 改变窗口大小
83 | void resizeEvent(QResizeEvent *event) override;
84 |
85 | private:
86 | // 初始化操作
87 | void initWindmill();
88 | // 绘制
89 | void drawImage(int width, int height);
90 |
91 | private:
92 | // 根实体(这个变量绘制时在线程访问)
93 | WindItem rootItem;
94 | // FPS 统计,paintEvent 累计 temp,达到一秒后赋值给 counter
95 | int fpsCounter{0};
96 | int fpsTemp{0};
97 | // FPS 计时
98 | QElapsedTimer fpsTime;
99 |
100 | // 鼠标位置
101 | QPoint mousePos;
102 | // 鼠标按下标志位
103 | bool mousePressed{false};
104 |
105 | // 定时动画
106 | QTimer timer;
107 | // 定时器旋转步进值
108 | float animationStep{0.0f};
109 | // 观察矩阵旋转
110 | QVector3D rotationAxis;
111 | QQuaternion rotationQuat;
112 | // 透视投影的 fovy 参数,视野范围
113 | float projectionFovy{30.0f};
114 |
115 | // 多线程异步 watcher
116 | QFutureWatcher watcher;
117 | // 绘制好的 image
118 | QImage image;
119 | };
120 |
--------------------------------------------------------------------------------
/src/tab3d/tab3d.pri:
--------------------------------------------------------------------------------
1 | FORMS += \
2 | $$PWD/Tab3D.ui
3 |
4 | HEADERS += \
5 | $$PWD/Cube3D.h \
6 | $$PWD/Tab3D.h \
7 | $$PWD/Windmill3D.h
8 |
9 | SOURCES += \
10 | $$PWD/Cube3D.cpp \
11 | $$PWD/Tab3D.cpp \
12 | $$PWD/Windmill3D.cpp
13 |
--------------------------------------------------------------------------------
/src/tabbasic/BasicComposition.cpp:
--------------------------------------------------------------------------------
1 | #include "BasicComposition.h"
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 | #include
16 |
17 | // 和 GL 枚举的对应:qt-everywhere-src-5.15.2\qtbase\src\gui\opengl\qopenglpaintengine.cpp
18 | #if 0
19 | switch(q->state()->composition_mode) {
20 | case QPainter::CompositionMode_SourceOver:
21 | funcs.glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
22 | break;
23 | case QPainter::CompositionMode_DestinationOver:
24 | funcs.glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_ONE);
25 | break;
26 | case QPainter::CompositionMode_Clear:
27 | funcs.glBlendFunc(GL_ZERO, GL_ZERO);
28 | break;
29 | case QPainter::CompositionMode_Source:
30 | funcs.glBlendFunc(GL_ONE, GL_ZERO);
31 | break;
32 | case QPainter::CompositionMode_Destination:
33 | funcs.glBlendFunc(GL_ZERO, GL_ONE);
34 | break;
35 | case QPainter::CompositionMode_SourceIn:
36 | funcs.glBlendFunc(GL_DST_ALPHA, GL_ZERO);
37 | break;
38 | case QPainter::CompositionMode_DestinationIn:
39 | funcs.glBlendFunc(GL_ZERO, GL_SRC_ALPHA);
40 | break;
41 | case QPainter::CompositionMode_SourceOut:
42 | funcs.glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_ZERO);
43 | break;
44 | case QPainter::CompositionMode_DestinationOut:
45 | funcs.glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_ALPHA);
46 | break;
47 | case QPainter::CompositionMode_SourceAtop:
48 | funcs.glBlendFunc(GL_DST_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
49 | break;
50 | case QPainter::CompositionMode_DestinationAtop:
51 | funcs.glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_SRC_ALPHA);
52 | break;
53 | case QPainter::CompositionMode_Xor:
54 | funcs.glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
55 | break;
56 | case QPainter::CompositionMode_Plus:
57 | funcs.glBlendFunc(GL_ONE, GL_ONE);
58 | break;
59 | case QPainter::CompositionMode_Multiply:
60 | funcs.glBlendEquation(GL_MULTIPLY_KHR);
61 | break;
62 | case QPainter::CompositionMode_Screen:
63 | funcs.glBlendEquation(GL_SCREEN_KHR);
64 | break;
65 | case QPainter::CompositionMode_Overlay:
66 | funcs.glBlendEquation(GL_OVERLAY_KHR);
67 | break;
68 | case QPainter::CompositionMode_Darken:
69 | funcs.glBlendEquation(GL_DARKEN_KHR);
70 | break;
71 | case QPainter::CompositionMode_Lighten:
72 | funcs.glBlendEquation(GL_LIGHTEN_KHR);
73 | break;
74 | case QPainter::CompositionMode_ColorDodge:
75 | funcs.glBlendEquation(GL_COLORDODGE_KHR);
76 | break;
77 | case QPainter::CompositionMode_ColorBurn:
78 | funcs.glBlendEquation(GL_COLORBURN_KHR);
79 | break;
80 | case QPainter::CompositionMode_HardLight:
81 | funcs.glBlendEquation(GL_HARDLIGHT_KHR);
82 | break;
83 | case QPainter::CompositionMode_SoftLight:
84 | funcs.glBlendEquation(GL_SOFTLIGHT_KHR);
85 | break;
86 | case QPainter::CompositionMode_Difference:
87 | funcs.glBlendEquation(GL_DIFFERENCE_KHR);
88 | break;
89 | case QPainter::CompositionMode_Exclusion:
90 | funcs.glBlendEquation(GL_EXCLUSION_KHR);
91 | break;
92 | default:
93 | qWarning("Unsupported composition mode");
94 | break;
95 | }
96 | #endif
97 |
98 | BasicComposition::BasicComposition(QWidget *parent)
99 | : QWidget(parent)
100 | {
101 | // 下拉框选择混合方式
102 | QMetaEnum me = QMetaEnum::fromType();
103 | QStringList items;
104 | for (int i = 0; i <= (int)Exclusion; i++)
105 | {
106 | items.push_back(me.valueToKey(i));
107 | }
108 | QComboBox *item_box = new QComboBox(this);
109 | item_box->addItems(items);
110 | item_box->move(100, 10);
111 | QLabel *item_lab = new QLabel("Composition", this);
112 | item_lab->move(10, 10);
113 | connect(item_box, QOverload::of(&QComboBox::currentIndexChanged), [=](int index) {
114 | circleComposition = index;
115 | update();
116 | });
117 |
118 | // 滑动条选择颜色
119 | QSlider *hue_slider = new QSlider(Qt::Horizontal, this);
120 | hue_slider->setRange(0, 359);
121 | hue_slider->setValue(circleHue);
122 | hue_slider->move(100, 50);
123 | QLabel *hue_lab = new QLabel("Color(HSV-Hue)", this);
124 | hue_lab->move(10, 50);
125 | connect(hue_slider, &QSlider::valueChanged, [=](int value) {
126 | circleHue = value;
127 | update();
128 | });
129 |
130 | // 滑动条选择透明度
131 | QSlider *alpha_slider = new QSlider(Qt::Horizontal, this);
132 | alpha_slider->setRange(0, 255);
133 | alpha_slider->setValue(circleAlpha);
134 | alpha_slider->move(100, 90);
135 | QLabel *alpha_lab = new QLabel("Color(Alpha)", this);
136 | alpha_lab->move(10, 90);
137 | connect(alpha_slider, &QSlider::valueChanged, [=](int value) {
138 | circleAlpha = value;
139 | update();
140 | });
141 | }
142 |
143 | void BasicComposition::paintEvent(QPaintEvent *event)
144 | {
145 | event->accept();
146 | {
147 | QPainter p(this);
148 | // 灰白相间的格子,用于查看半透明
149 | int rect_size = 50;
150 | bool h_white = true;
151 | bool w_white = true;
152 | for (int h = 0; h < height(); h += rect_size)
153 | {
154 | w_white = h_white;
155 | for (int w = 0; w < width(); w += rect_size)
156 | {
157 | p.fillRect(w, h, rect_size, rect_size, w_white ? Qt::white : Qt::gray);
158 | w_white = !w_white;
159 | }
160 | h_white = !h_white;
161 | }
162 | }
163 |
164 | if (canvas.isNull() || dest.isNull())
165 | return;
166 | // 把图案画到 canvas,然后叠加 source 色块
167 | {
168 | // 如果用 QPainter 清除,需要 CompositionMode_Clear
169 | // 默认的 CompositionMode_SourceOver 会混合历史数据
170 | canvas.fill(Qt::transparent);
171 | QPainter p(&canvas);
172 | p.setPen(Qt::NoPen);
173 | p.drawImage(0, 0, dest);
174 | // 应用设置的 Comsposition
175 | p.setCompositionMode((QPainter::CompositionMode)circleComposition);
176 | // 色块
177 | p.setBrush(QColor::fromHsv(circleHue, 255, 255, circleAlpha));
178 | p.setRenderHint(QPainter::Antialiasing);
179 | p.drawEllipse(circlePos, circleRadius, circleRadius);
180 | }
181 |
182 | // 把 canvas 画到 widget
183 | {
184 | QPainter p(this);
185 | p.drawImage(0, 0, canvas);
186 | }
187 | }
188 |
189 | void BasicComposition::resizeEvent(QResizeEvent *event)
190 | {
191 | QWidget::resizeEvent(event);
192 | if (!event->size().isValid())
193 | return;
194 | canvas = QImage(event->size(), QImage::Format_ARGB32_Premultiplied);
195 | dest = QImage(event->size(), QImage::Format_ARGB32_Premultiplied);
196 | dest.fill(Qt::transparent);
197 |
198 | QPainter p(&dest);
199 | p.setPen(Qt::NoPen);
200 | const int w = dest.width();
201 | const int h = dest.height();
202 |
203 | // 右侧 - 渐变的色条
204 | // 竖向颜色渐变 + 横向半透明渐变,参考示例 composition
205 | p.save();
206 | QLinearGradient rect_gradient(0, 0, 0, h);
207 | rect_gradient.setColorAt(0, Qt::red);
208 | rect_gradient.setColorAt(.17, Qt::yellow);
209 | rect_gradient.setColorAt(.33, Qt::green);
210 | rect_gradient.setColorAt(.50, Qt::cyan);
211 | rect_gradient.setColorAt(.66, Qt::blue);
212 | rect_gradient.setColorAt(.81, Qt::magenta);
213 | rect_gradient.setColorAt(1, Qt::red);
214 | p.setBrush(rect_gradient);
215 | // 画在右边
216 | p.drawRect(w / 2, 0, w / 2, h);
217 |
218 | QLinearGradient alpha_gradient(0, 0, w, 0);
219 | alpha_gradient.setColorAt(0.2, Qt::white);
220 | alpha_gradient.setColorAt(0.5, Qt::transparent);
221 | alpha_gradient.setColorAt(0.8, Qt::white);
222 |
223 | p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
224 | p.setBrush(alpha_gradient);
225 | p.drawRect(0, 0, w, h);
226 | p.restore();
227 |
228 | // 左侧 - 渐变的刺球
229 | // 准备一个正方形区域
230 | p.save();
231 | QRect area;
232 | if (w > h) {
233 | area = QRect(0, 0, h, h);
234 | } else {
235 | area = QRect(0, (h - w) / 2, w, w);
236 | }
237 | int r = area.width() / 2 - 20;
238 | if (r < 50) {
239 | r = 50;
240 | }
241 | // 移动到正方形区域中心
242 | p.translate(area.center());
243 | // 准备渐变
244 | QLinearGradient r_gradient(0, 0, 0, r);
245 | r_gradient.setColorAt(0, QColor(0, 0, 255, 255));
246 | r_gradient.setColorAt(1, QColor(0, 255, 255, 127));
247 | p.setBrush(r_gradient);
248 | // 准备三角
249 | QPainterPath r_path;
250 | r_path.moveTo(0, r);
251 | r_path.lineTo(-20, 0);
252 | r_path.lineTo(20, 0);
253 | r_path.closeSubpath();
254 | const int r_angle = 18;
255 | p.setRenderHint(QPainter::Antialiasing);
256 | for (int i = 0; i < 360; i += r_angle)
257 | {
258 | p.drawPath(r_path);
259 | p.rotate(r_angle);
260 | }
261 | p.restore();
262 | }
263 |
264 | void BasicComposition::mousePressEvent(QMouseEvent *event)
265 | {
266 | event->accept();
267 | QPoint pos = event->pos();
268 | // 右键点击重置位置
269 | if (event->button() & Qt::RightButton) {
270 | circlePos = pos;
271 | update();
272 | return;
273 | }
274 | QPoint diff = circlePos - pos;
275 | // 距离圆心小于半径就拖动
276 | if (std::hypot(diff.x(), diff.y()) <= circleRadius) {
277 | dragDiff = diff;
278 | dragFlag = true;
279 | }
280 | }
281 |
282 | void BasicComposition::mouseMoveEvent(QMouseEvent *event)
283 | {
284 | event->accept();
285 | if (dragFlag) {
286 | circlePos = dragDiff + event->pos();
287 | update();
288 | }
289 | }
290 |
291 | void BasicComposition::mouseReleaseEvent(QMouseEvent *event)
292 | {
293 | event->accept();
294 | if (dragFlag) {
295 | circlePos = dragDiff + event->pos();
296 | // 超出范围重置
297 | if (!canvas.rect().contains(circlePos)) {
298 | circlePos = QPoint(0, 0);
299 | }
300 | update();
301 | }
302 | dragFlag = false;
303 | }
304 |
--------------------------------------------------------------------------------
/src/tabbasic/BasicComposition.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | // 颜色混合方式
6 | // Qt 示例中有个比较有参考性的例子:Composition
7 | class BasicComposition : public QWidget
8 | {
9 | Q_OBJECT
10 | public:
11 | // 对应 QPainter::CompositionMode
12 | // 源码实现:qt-everywhere-src-5.15.2\qtbase\src\gui\painting\qdrawhelper_mips_dsp.cpp
13 | enum QtComposition {
14 | // 这是默认模式,源的 alpha 用于混合目标顶部的像素
15 | SourceOver,
16 | // 目标的 alpha 用于将其混合到源像素之上,此模式与 SourceOver 相反
17 | DestinationOver,
18 | // 清除,设置为完全透明
19 | Clear,
20 | // 输出是源像素,这意味着基本的复制操作,当源像素不透明时,与 SourceOver 相同
21 | Source,
22 | // 输出是目标像素,这意味着混合没有效果,此模式与 Source 相反
23 | Destination,
24 | // 输出是源,其中 alpha 减少目标的 alpha
25 | SourceIn,
26 | // 输出是目标,其中 alpha 值被源的 alpha 值减少,此模式与 SourceIn 相反
27 | DestinationIn,
28 | // 输出是源,其中 alpha 被目标的倒数减小
29 | SourceOut,
30 | // 输出是目标,其中 alpha 被源的倒数减小,此模式与 SourceOut 相反
31 | DestinationOut,
32 | // 源像素在目标像素的顶部混合,源像素的 alpha 值减去目标像素的 alpha
33 | SourceAtop,
34 | // 目标像素在源之上混合,目标像素的 Alpha 会减小目标像素的 Alpha,此模式与 SourceAtop 相反
35 | DestinationAtop,
36 | // 源的 alpha 随目标 alpha 的倒数而减小,与目标合并,其 alpha 由源 alpha 的倒数减小,此 Xor 与按位 Xor 不同。
37 | Xor,
38 |
39 | // svg 1.2 blend modes
40 |
41 | // 源像素和目标像素的 alpha 像素和颜色相加
42 | Plus,
43 | // 输出是源颜色乘以目标,将颜色与白色相乘会使颜色保持不变,而将颜色与黑色相乘会产生黑色
44 | Multiply,
45 | // 源颜色和目标颜色将反转,然后相乘,用白色筛选颜色会产生白色,而用黑色筛选颜色会保留颜色不变
46 | Screen,
47 | // 根据目标颜色乘以或屏蔽颜色,目标颜色与源颜色混合,以反映目标的明暗
48 | Overlay,
49 | // 选择源颜色和目标颜色的较深色
50 | Darken,
51 | // 选择源颜色和目标颜色的较浅色
52 | Lighten,
53 | // 目标颜色变亮以反映源颜色,黑色源颜色使目标颜色保持不变
54 | ColorDodge,
55 | // 目标颜色变暗以反映源颜色,白色源颜色使目标颜色保持不变
56 | ColorBurn,
57 | // 根据源颜色乘以或屏蔽颜色,光源颜色将使目标颜色变亮,而深光源颜色将使目标颜色变暗
58 | HardLight,
59 | // 根据源颜色使颜色变暗或变亮,类似于 HardLight
60 | SoftLight,
61 | // 从较浅的颜色中减去较深的颜色,用白色绘画会反转目标颜色,而用黑色绘画会使目标颜色保持不变
62 | Difference,
63 | // 与 Difference 相似,但对比度较低,用白色绘画会反转目标颜色,而用黑色绘画会使目标颜色保持不变
64 | Exclusion
65 | };
66 | Q_ENUM(QtComposition)
67 | public:
68 | explicit BasicComposition(QWidget *parent = nullptr);
69 |
70 | protected:
71 | void paintEvent(QPaintEvent *event) override;
72 | void resizeEvent(QResizeEvent *event) override;
73 | void mousePressEvent(QMouseEvent *event) override;
74 | void mouseMoveEvent(QMouseEvent *event) override;
75 | void mouseReleaseEvent(QMouseEvent *event) override;
76 |
77 | private:
78 | // 因为半透明效果不好在 QWidget 上展示,所以用一个 QImage 当画布,最后再画到界面
79 | QImage canvas;
80 | // Composition 的 destination 图案
81 | QImage dest;
82 | // Composition 的 source 用一个圆形的色块
83 | // 色块的位置
84 | QPoint circlePos{0, 0};
85 | // 色块的半径
86 | int circleRadius{100};
87 | // 色块透明度[0,255]
88 | int circleAlpha{255};
89 | // 色块颜色HSV的hue[0,359],saturation和value固定255
90 | int circleHue{0};
91 | // 当前混合方式选择
92 | int circleComposition{SourceOver};
93 | // 是否在拖拽
94 | bool dragFlag{false};
95 | // 拖拽相对位置
96 | QPoint dragDiff{0, 0};
97 | };
98 |
--------------------------------------------------------------------------------
/src/tabbasic/BasicCurve.cpp:
--------------------------------------------------------------------------------
1 | #include "BasicCurve.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | BasicCurve::BasicCurve(QWidget *parent)
11 | : QWidget(parent)
12 | {
13 | }
14 |
15 | void BasicCurve::paintEvent(QPaintEvent *event)
16 | {
17 | event->accept();
18 | QPainter painter(this);
19 | // 先画一个白底黑框
20 | painter.fillRect(this->rect(), Qt::white);
21 | QPen pen(Qt::black);
22 | painter.setPen(pen);
23 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); // 右下角会超出范围
24 |
25 | // 设置
26 | painter.setRenderHint(QPainter::Antialiasing, true); // 抗锯齿
27 | painter.setPen(QPen(Qt::black, 1));
28 |
29 | const int line_count = 5;
30 | const double item_height = height() / line_count;
31 | const double item_width = width() / 10.0;
32 | QList point_list;
33 | // 之所以手动填充点,是为了展示一些典型的曲线
34 | point_list.push_back(QPointF(item_width * 0.5, item_height * 0.95));
35 | point_list.push_back(QPointF(item_width * 0.75, item_height * 0.05));
36 | point_list.push_back(QPointF(item_width * 1.0, item_height * 0.5));
37 | point_list.push_back(QPointF(item_width * 2.5, item_height * 0.6));
38 | point_list.push_back(QPointF(item_width * 3.5, item_height * 0.95));
39 | point_list.push_back(QPointF(item_width * 4.5, item_height * 0.05));
40 | point_list.push_back(QPointF(item_width * 5.5, item_height * 0.95));
41 | point_list.push_back(QPointF(item_width * 6.5, item_height * 0.15));
42 | point_list.push_back(QPointF(item_width * 7.5, item_height * 0.5));
43 | // 这里制造一个方形,目测三次贝塞尔地效果更好
44 | point_list.push_back(QPointF(item_width * 7.51, item_height * 0.8));
45 | point_list.push_back(QPointF(item_width * 9.49, item_height * 0.8));
46 | point_list.push_back(QPointF(item_width * 9.5, item_height * 0.15));
47 |
48 | // 1- 直线连接
49 | QPainterPath path_1;
50 | path_1.moveTo(point_list.at(0));
51 | for (int i = 1; i < point_list.count(); i++)
52 | {
53 | path_1.lineTo(point_list.at(i));
54 | }
55 | painter.drawPath(path_1);
56 | for (const QPointF &pt : point_list)
57 | {
58 | painter.drawEllipse(pt, 5, 5); // 画圆是为了便于观察是否过采样点
59 | }
60 | painter.drawText(10, item_height / 2, "折线 lineTo");
61 |
62 | // 2- 二次贝塞尔
63 | painter.translate(0, item_height);
64 | QPainterPath path_2;
65 | path_2.moveTo(point_list.at(0));
66 | for (int i = 1; i < point_list.count(); i++)
67 | {
68 | // 控制点计算,x 取两个点 x 中间,y 取两者最大 y,但由于绘制时 y 是反过来的,所以取最小的
69 | const double ctrl_x = (point_list.at(i - 1).x() + point_list.at(i).x()) / 2.0;
70 | const double ctrl_y = point_list.at(i - 1).y() < point_list.at(i).y()
71 | ? point_list.at(i - 1).y()
72 | : point_list.at(i).y();
73 | painter.drawEllipse(QPointF(ctrl_x, ctrl_y), 3, 3); // 画出控制点
74 | path_2.quadTo(QPointF(ctrl_x, ctrl_y), point_list.at(i));
75 | }
76 | painter.drawPath(path_2);
77 | for (const QPointF &pt : point_list)
78 | {
79 | painter.drawEllipse(pt, 5, 5); // 画圆是为了便于观察是否过采样点
80 | }
81 | painter.drawText(10, item_height / 2, "二次贝塞尔 quadTo");
82 |
83 | // 3- 三次贝塞尔
84 | painter.translate(0, item_height);
85 | QPainterPath path_3;
86 | path_3.moveTo(point_list.at(0));
87 | for (int i = 1; i < point_list.count(); i++)
88 | {
89 | // 控制点计算,x 取两个点 x 中间,y 分别取一次
90 | const double ctrl_x = (point_list.at(i - 1).x() + point_list.at(i).x()) / 2.0;
91 | const double ctrl_y1 = point_list.at(i - 1).y();
92 | const double ctrl_y2 = point_list.at(i).y();
93 |
94 | painter.drawEllipse(QPointF(ctrl_x, ctrl_y1), 3, 3); // 画出控制点
95 | painter.drawEllipse(QPointF(ctrl_x, ctrl_y2), 3, 3); // 画出控制点
96 | // 下面这个效果差不多,分别取 1x 2y
97 | // painter.drawEllipse(QPointF(point_list.at(i).x(),ctrl_y1),3,3); // 画出控制点
98 | // painter.drawEllipse(QPointF(point_list.at(i-1).x(),ctrl_y2),3,3); // 画出控制点
99 | path_3.cubicTo(QPointF(ctrl_x, ctrl_y1), QPointF(ctrl_x, ctrl_y2), point_list.at(i));
100 | }
101 | painter.drawPath(path_3);
102 | for (const QPointF &pt : point_list)
103 | {
104 | painter.drawEllipse(pt, 5, 5); // 画圆是为了便于观察是否过采样点
105 | }
106 | painter.drawText(10, item_height / 2, "三次贝塞尔 cubicTo");
107 |
108 | // 4- 自定义曲线
109 | painter.translate(0, item_height);
110 | const QVector point_temp = point_list.toVector();
111 | const QVector points4 = point_temp;
112 | // 使用时需要判断 controlPoints4 size >= 2
113 | const QVector controlPoints4 = calculateControlPoints(point_temp);
114 | QPainterPath path_4;
115 | path_4.moveTo(points4.at(0));
116 | for (int i = 0; i < points4.size() - 1; i++)
117 | {
118 | const QPointF &point = points4.at(i + 1);
119 | path_4.cubicTo(controlPoints4[2 * i], controlPoints4[2 * i + 1], point);
120 | }
121 | painter.drawPath(path_4);
122 | for (const QPointF &pt : point_list)
123 | {
124 | painter.drawEllipse(pt, 5, 5); // 画圆是为了便于观察是否过采样点
125 | }
126 | painter.drawText(10, item_height / 2, "QtCharts SplineChartItem");
127 |
128 | // 5- 自定义曲线
129 | painter.translate(0, item_height);
130 | QPainterPath path_5 = generateSmoothCurve(point_list);
131 | painter.drawPath(path_5);
132 | for (const QPointF &pt : point_list)
133 | {
134 | painter.drawEllipse(pt, 5, 5); // 画圆是为了便于观察是否过采样点
135 | }
136 | painter.drawText(10, item_height / 2, "公孙二狗");
137 | }
138 |
139 | QVector BasicCurve::calculateControlPoints(const QVector &points)
140 | {
141 | // Calculates control points which are needed by QPainterPath.cubicTo function to draw the cubic Bezier cureve between two points.
142 | QVector controlPoints;
143 | controlPoints.resize(points.count() * 2 - 2);
144 |
145 | int n = points.count() - 1;
146 |
147 | if (n == 1)
148 | {
149 | // for n==1
150 | controlPoints[0].setX((2 * points[0].x() + points[1].x()) / 3);
151 | controlPoints[0].setY((2 * points[0].y() + points[1].y()) / 3);
152 | controlPoints[1].setX(2 * controlPoints[0].x() - points[0].x());
153 | controlPoints[1].setY(2 * controlPoints[0].y() - points[0].y());
154 | return controlPoints;
155 | }
156 |
157 | // Calculate first Bezier control points
158 | // Set of equations for P0 to Pn points.
159 | //
160 | // | 2 1 0 0 ... 0 0 0 ... 0 0 0 | | P1_1 | | P0 + 2 * P1 |
161 | // | 1 4 1 0 ... 0 0 0 ... 0 0 0 | | P1_2 | | 4 * P1 + 2 * P2 |
162 | // | 0 1 4 1 ... 0 0 0 ... 0 0 0 | | P1_3 | | 4 * P2 + 2 * P3 |
163 | // | . . . . . . . . . . . . | | ... | | ... |
164 | // | 0 0 0 0 ... 1 4 1 ... 0 0 0 | * | P1_i | = | 4 * P(i-1) + 2 * Pi |
165 | // | . . . . . . . . . . . . | | ... | | ... |
166 | // | 0 0 0 0 0 0 0 0 ... 1 4 1 | | P1_(n-1)| | 4 * P(n-2) + 2 * P(n-1) |
167 | // | 0 0 0 0 0 0 0 0 ... 0 2 7 | | P1_n | | 8 * P(n-1) + Pn |
168 | //
169 | QVector vector;
170 | vector.resize(n);
171 |
172 | vector[0] = points[0].x() + 2 * points[1].x();
173 |
174 | for (int i = 1; i < n - 1; ++i)
175 | vector[i] = 4 * points[i].x() + 2 * points[i + 1].x();
176 |
177 | vector[n - 1] = (8 * points[n - 1].x() + points[n].x()) / 2.0;
178 |
179 | QVector xControl = firstControlPoints(vector);
180 |
181 | vector[0] = points[0].y() + 2 * points[1].y();
182 |
183 | for (int i = 1; i < n - 1; ++i)
184 | vector[i] = 4 * points[i].y() + 2 * points[i + 1].y();
185 |
186 | vector[n - 1] = (8 * points[n - 1].y() + points[n].y()) / 2.0;
187 |
188 | QVector yControl = firstControlPoints(vector);
189 |
190 | for (int i = 0, j = 0; i < n; ++i, ++j)
191 | {
192 |
193 | controlPoints[j].setX(xControl[i]);
194 | controlPoints[j].setY(yControl[i]);
195 |
196 | j++;
197 |
198 | if (i < n - 1)
199 | {
200 | controlPoints[j].setX(2 * points[i + 1].x() - xControl[i + 1]);
201 | controlPoints[j].setY(2 * points[i + 1].y() - yControl[i + 1]);
202 | }
203 | else
204 | {
205 | controlPoints[j].setX((points[n].x() + xControl[n - 1]) / 2);
206 | controlPoints[j].setY((points[n].y() + yControl[n - 1]) / 2);
207 | }
208 | }
209 | return controlPoints;
210 | }
211 |
212 | QVector BasicCurve::firstControlPoints(const QVector &vector)
213 | {
214 | QVector result;
215 |
216 | int count = vector.count();
217 | result.resize(count);
218 | result[0] = vector[0] / 2.0;
219 |
220 | QVector temp;
221 | temp.resize(count);
222 | temp[0] = 0;
223 |
224 | qreal b = 2.0;
225 |
226 | for (int i = 1; i < count; i++)
227 | {
228 | temp[i] = 1 / b;
229 | b = (i < count - 1 ? 4.0 : 3.5) - temp[i];
230 | result[i] = (vector[i] - result[i - 1]) / b;
231 | }
232 |
233 | for (int i = 1; i < count; i++)
234 | result[count - i - 1] -= temp[count - i] * result[count - i];
235 |
236 | return result;
237 | }
238 |
239 | QPainterPath BasicCurve::generateSmoothCurve(QList points, bool closed, double tension, int numberOfSegments)
240 | {
241 | QList ps;
242 |
243 | foreach (QPointF p, points)
244 | {
245 | ps << p.x() << p.y();
246 | }
247 |
248 | return BasicCurve::generateSmoothCurve(ps, closed, tension, numberOfSegments);
249 | }
250 |
251 | QPainterPath BasicCurve::generateSmoothCurve(QList points, bool closed, double tension, int numberOfSegments)
252 | {
253 | QList ps(points); // clone array so we don't change the original points
254 | QList result; // generated smooth curve coordinates
255 | double x, y;
256 | double t1x, t2x, t1y, t2y;
257 | double c1, c2, c3, c4;
258 | double st;
259 |
260 | // The algorithm require a previous and next point to the actual point array.
261 | // Check if we will draw closed or open curve.
262 | // If closed, copy end points to beginning and first points to end
263 | // If open, duplicate first points to befinning, end points to end
264 | if (closed)
265 | {
266 | ps.prepend(points[points.length() - 1]);
267 | ps.prepend(points[points.length() - 2]);
268 | ps.prepend(points[points.length() - 1]);
269 | ps.prepend(points[points.length() - 2]);
270 | ps.append(points[0]);
271 | ps.append(points[1]);
272 | }
273 | else
274 | {
275 | ps.prepend(points[1]); // copy 1st point and insert at beginning
276 | ps.prepend(points[0]);
277 | ps.append(points[points.length() - 2]); // copy last point and append
278 | ps.append(points[points.length() - 1]);
279 | }
280 |
281 | // 1. loop goes through point array
282 | // 2. loop goes through each segment between the 2 points + 1e point before and after
283 | for (int i = 2; i < (ps.length() - 4); i += 2)
284 | {
285 | // calculate tension vectors
286 | t1x = (ps[i + 2] - ps[i - 2]) * tension;
287 | t2x = (ps[i + 4] - ps[i - 0]) * tension;
288 | t1y = (ps[i + 3] - ps[i - 1]) * tension;
289 | t2y = (ps[i + 5] - ps[i + 1]) * tension;
290 |
291 | for (int t = 0; t <= numberOfSegments; t++)
292 | {
293 | // calculate step
294 | st = (double)t / (double)numberOfSegments;
295 |
296 | // calculate cardinals
297 | c1 = 2 * qPow(st, 3) - 3 * qPow(st, 2) + 1;
298 | c2 = -2 * qPow(st, 3) + 3 * qPow(st, 2);
299 | c3 = qPow(st, 3) - 2 * qPow(st, 2) + st;
300 | c4 = qPow(st, 3) - qPow(st, 2);
301 |
302 | // calculate x and y cords with common control vectors
303 | x = c1 * ps[i] + c2 * ps[i + 2] + c3 * t1x + c4 * t2x;
304 | y = c1 * ps[i + 1] + c2 * ps[i + 3] + c3 * t1y + c4 * t2y;
305 |
306 | // store points in array
307 | result << x << y;
308 | }
309 | }
310 |
311 | // 使用的平滑曲线的坐标创建 QPainterPath
312 | QPainterPath path;
313 | path.moveTo(result[0], result[1]);
314 | for (int i = 2; i < result.length() - 2; i += 2)
315 | {
316 | path.lineTo(result[i + 0], result[i + 1]);
317 | }
318 |
319 | if (closed)
320 | {
321 | path.closeSubpath();
322 | }
323 |
324 | return path;
325 | }
326 |
--------------------------------------------------------------------------------
/src/tabbasic/BasicCurve.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 |
4 | // 绘制曲线
5 | class BasicCurve : public QWidget
6 | {
7 | Q_OBJECT
8 | public:
9 | explicit BasicCurve(QWidget *parent = nullptr);
10 |
11 | protected:
12 | void paintEvent(QPaintEvent *event) override;
13 |
14 | private:
15 | // 【1】 下面函数来自 QtCharts SplineChartItem
16 | // 计算 QPainterPath.cubicTo 所需的控制点
17 | // Calculates control points which are needed by QPainterPath.cubicTo function to draw the cubic Bezier cureve between two points.
18 | QVector calculateControlPoints(const QVector &points);
19 | QVector firstControlPoints(const QVector &vector);
20 |
21 | // 【2】 下面函数来自公孙二狗博客:http://qtdebug.com/qt-smooth-curve-1/
22 | // 他参考自 https://stackoverflow.com/questions/7054272/how-to-draw-smooth-curve-through-n-points-using-javascript-html5-canvas
23 | // 重载函数
24 | QPainterPath generateSmoothCurve(QList points, bool closed = false, double tension = 0.5, int numberOfSegments = 16);
25 |
26 | /**
27 | * @brief 使用传入的曲线顶点坐标创建平滑曲线。
28 | *
29 | * @param points 曲线顶点坐标数组,
30 | * points[i+0] 是第 i 个点的 x 坐标,
31 | * points[i+1] 是第 i 个点的 y 坐标
32 | * @param closed 曲线是否封闭,默认不封闭
33 | * @param tension 密集程度,默认为 0.5
34 | * @param numberOfSegments 平滑曲线 2 个顶点间的线段数,默认为 16
35 | * @return 平滑曲线的 QPainterPath
36 | */
37 | QPainterPath generateSmoothCurve(QList points, bool closed = false, double tension = 0.5, int numberOfSegments = 16);
38 | };
39 |
--------------------------------------------------------------------------------
/src/tabbasic/BasicImage.cpp:
--------------------------------------------------------------------------------
1 | #include "BasicImage.h"
2 | #include
3 | #include
4 | #include
5 |
6 | BasicImage::BasicImage(QWidget *parent)
7 | : QWidget(parent)
8 | {
9 | imgCache = QImage(":/hehua.png");
10 | imgCache.convertTo(QImage::Format_ARGB32);
11 | maskCache = QImage(":/heart.png");
12 | maskCache.convertTo(QImage::Format_ARGB32);
13 |
14 | // 定时器用来做动态效果
15 | connect(&timer, &QTimer::timeout, [this] {
16 | timeOffset++;
17 | update();
18 | });
19 | }
20 |
21 | void BasicImage::showEvent(QShowEvent *event)
22 | {
23 | timer.start(100);
24 | QWidget::showEvent(event);
25 | }
26 |
27 | void BasicImage::hideEvent(QHideEvent *event)
28 | {
29 | timer.stop();
30 | QWidget::hideEvent(event);
31 | }
32 |
33 | void BasicImage::paintEvent(QPaintEvent *event)
34 | {
35 | Q_UNUSED(event)
36 | QPainter painter(this);
37 | // 先画一个白底黑框
38 | painter.fillRect(this->rect(), Qt::white);
39 | painter.setPen(QPen(Qt::black, 1, Qt::DashLine));
40 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); // 右下角会超出范围
41 |
42 | // 红色笔
43 | painter.setPen(Qt::red);
44 | // 中间变量
45 | QImage img_a;
46 |
47 | // drawImage 是以左上角为起点,drawText 是以左下角为起点
48 | // 缩放
49 | // 平滑缩放效果会更好一点:scaled(w, h, Qt::IgnoreAspectRatio, Qt::FastTransformation)
50 | painter.drawImage(10 + 210 * 0, 30, imgCache.scaled(100, 100));
51 | painter.drawImage(QRectF(10 + 210 * 0 + 100, 30 + 100, 100, 100),
52 | imgCache.scaled(100, 100));
53 | painter.drawText(10, 20, "scaled");
54 | // 镜像,可以横项和纵向反转
55 | painter.drawImage(10 + 210 * 1, 30, imgCache.mirrored(true, true));
56 | painter.drawText(10 + 210 * 1, 20, "mirrored");
57 |
58 | // 红蓝分量交换
59 | painter.drawImage(10 + 210 * 2, 30, imgCache.rgbSwapped());
60 | painter.drawText(10 + 210 * 2, 20, "rgbSwapped");
61 |
62 | // mask
63 | img_a = maskCache.scaled(200, 200);
64 | {
65 | QPainter p(&img_a);
66 | // 因为图片是放大过的,所以没法
67 | // p.setRenderHint(QPainter::SmoothPixmapTransform);
68 | // 源像素混合在目标的顶部,源像素的 alpha 减去目标像素的 alpha。
69 | p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
70 | p.drawImage(0, 0, imgCache);
71 | }
72 | painter.drawImage(10 + 210 * 3, 30, img_a);
73 | painter.drawText(10 + 210 * 3, 20, "mask");
74 | // 其他
75 | // 根据给定的颜色值创建并返回此图像的蒙版
76 | // QImage QImage::createMaskFromColor(QRgb color, Qt::MaskMode mode = Qt::MaskInColor) const
77 | // 函数创建并返回此图像的 1-bpp 启发式掩码
78 | // QImage QImage::createHeuristicMask(bool clipTight = true) const
79 | // 从此图像中的 alpha 缓冲区构建并返回一个 1-bpp 掩码
80 | // QImage QImage::createMaskFromColor(QRgb color, Qt::MaskMode mode = Qt::MaskInColor) const
81 |
82 | // 灰度图
83 | // 灰度值, ARGB32 格式:(0xAARRGGBB).
84 | img_a = imgCache.scaled(100, 100);
85 | for (int row = 0; row < img_a.height(); row++)
86 | {
87 | // 整行数据, typedef unsigned int QRgb;
88 | QRgb *line_data = (QRgb *)img_a.scanLine(row);
89 | int calc_gray;
90 | for (int col = 0; col < img_a.width(); col++)
91 | {
92 | // 多种计算方式,效果略有差别
93 | // Gray = R*0.299 + G*0.587 + B*0.114
94 | // Gray = (R*19595 + G*38469 + B*7472) >> 16
95 | // Gray = (R * 11 + G * 16 + B * 5)/32;(qGray)
96 | // calc_gray = qGray(line_data[col]);
97 | calc_gray = qGray(line_data[col]);
98 | // 渐变效果,0 是黑色,255 白色,可以改变大于小于
99 | if ((timeOffset * 5 % 255) > calc_gray)
100 | {
101 | line_data[col] = qRgb(calc_gray, calc_gray, calc_gray);
102 | }
103 | }
104 | }
105 | painter.drawImage(10 + 210 * 4, 30, img_a);
106 | //
107 | img_a = imgCache.scaled(100, 100).convertToFormat(QImage::Format_Grayscale8);
108 | painter.drawImage(10 + 210 * 4 + 100, 30 + 100, img_a);
109 | painter.drawText(10 + 210 * 4, 20, "gray");
110 | // 填充为某个颜色
111 | img_a = maskCache.copy();
112 | {
113 | QPainter p(&img_a);
114 | p.setCompositionMode(QPainter::CompositionMode_SourceIn);
115 | p.fillRect(img_a.rect(), Qt::green); // 原谅绿
116 | }
117 | painter.drawImage(10 + 210 * 5, 30, img_a);
118 | img_a = maskCache.copy();
119 | {
120 | QPainter p(&img_a);
121 | p.setCompositionMode(QPainter::CompositionMode_SourceIn);
122 | p.fillRect(img_a.rect(), Qt::red); // 原谅绿
123 | }
124 | painter.drawImage(10 + 210 * 5 + 100, 30 + 100, img_a);
125 | painter.drawText(10 + 210 * 5, 20, "colorImage");
126 |
127 | // 转换矩阵
128 | QTransform mat_a; // 3*3 矩阵,貌似用 QMatrix 也可以
129 | mat_a.scale(0.5, 0.5); // 缩放
130 | img_a = imgCache.transformed(mat_a, Qt::SmoothTransformation);
131 | painter.drawImage(10, 310, img_a);
132 | painter.drawRect(QRect(QPoint(10, 310), img_a.rect().size()));
133 | mat_a.rotate(timeOffset * 5 % 360); //旋转
134 | img_a = imgCache.transformed(mat_a, Qt::SmoothTransformation);
135 | painter.drawImage(10, 310, img_a);
136 | painter.drawRect(QRect(QPoint(10, 310), img_a.rect().size()));
137 | painter.drawText(10, 300, "transform rotate");
138 | // 可以发现,用矩阵转换的话就没法像转换 painter 坐标那样获取到矩形的实际边了
139 | // 一般我用 painter 的 rotate
140 |
141 | // 转换矩阵
142 | QTransform mat_c; // 矩阵
143 | mat_c.scale(0.5, 0.5); // 缩放
144 | img_a = imgCache.transformed(mat_c, Qt::SmoothTransformation);
145 | painter.drawImage(200, 310, img_a);
146 | painter.drawRect(QRect(QPoint(200, 310), img_a.rect().size()));
147 | // mat_c.translate(0.5,0.5); // 没效果
148 | // 剪切变形,菱形的效果,为 0 时在原地,为 1 就拉伸了一倍的长/宽
149 | mat_c.shear((timeOffset % 100) / 100.0, 0.1);
150 | img_a = imgCache.transformed(mat_c, Qt::SmoothTransformation);
151 | painter.drawImage(200, 310, img_a);
152 | painter.drawRect(QRect(QPoint(200, 310), img_a.rect().size()));
153 | painter.drawText(200, 300, "transform shear");
154 |
155 | // 亮度调节
156 | // (暖色调加减红绿,冷色调加减蓝)
157 | img_a = imgCache.copy();
158 | int brightness_offset = 100;
159 | int red, green, blue;
160 | for (int row = 0; row < img_a.height(); row++)
161 | {
162 | // 整行数据, typedef unsigned int QRgb;
163 | QRgb *line_data = (QRgb *)img_a.scanLine(row);
164 | for (int col = 0; col < img_a.width(); col++)
165 | {
166 | red = qRed(line_data[col]) + brightness_offset;
167 | red = (red < 0x00) ? 0x00 : (red > 0xff) ? 0xff
168 | : red;
169 | green = qGreen(line_data[col]) + brightness_offset;
170 | green = (green < 0x00) ? 0x00 : (green > 0xff) ? 0xff
171 | : green;
172 | blue = qBlue(line_data[col]) + brightness_offset;
173 | blue = (blue < 0x00) ? 0x00 : (blue > 0xff) ? 0xff
174 | : blue;
175 | line_data[col] = qRgba(red, green, blue, qAlpha(line_data[col]));
176 | }
177 | }
178 | painter.drawImage(10 + 210 * 2, 310, img_a);
179 | painter.drawText(10 + 210 * 2, 300, "brightness");
180 |
181 | // 对比度
182 | img_a = imgCache.copy();
183 | int contrast_offset = 50;
184 | for (int row = 0; row < img_a.height(); row++)
185 | {
186 | // 整行数据, typedef unsigned int QRgb;
187 | QRgb *line_data = (QRgb *)img_a.scanLine(row);
188 | for (int col = 0; col < img_a.width(); col++)
189 | {
190 | float param = 0;
191 | if (contrast_offset > 0 && contrast_offset < 100)
192 | {
193 | param = 1 / (1 - contrast_offset / 100.0) - 1;
194 | }
195 | else
196 | {
197 | param = contrast_offset / 100.0;
198 | }
199 | red = qRed(line_data[col]);
200 | red = red + (red - 127) * param;
201 | red = (red < 0x00) ? 0x00 : (red > 0xff) ? 0xff
202 | : red;
203 | green = qGreen(line_data[col]);
204 | green = green + (green - 127) * param;
205 | green = (green < 0x00) ? 0x00 : (green > 0xff) ? 0xff
206 | : green;
207 | blue = qBlue(line_data[col]);
208 | blue = blue + (blue - 127) * param;
209 | blue = (blue < 0x00) ? 0x00 : (blue > 0xff) ? 0xff
210 | : blue;
211 | line_data[col] = qRgba(red, green, blue, qAlpha(line_data[col]));
212 | }
213 | }
214 | painter.drawImage(10 + 210 * 3, 310, img_a);
215 | painter.drawText(10 + 210 * 3, 300, "contrast");
216 |
217 | // 饱和度
218 | // HSL 格式表示色彩 - hue(色相), saturation(饱和度), lightness(明度)
219 | img_a = imgCache.copy();
220 | int hue, saturation, lightness;
221 | for (int row = 0; row < img_a.height(); row++)
222 | {
223 | // 整行数据, typedef unsigned int QRgb;
224 | QRgb *line_data = (QRgb *)img_a.scanLine(row);
225 | for (int col = 0; col < img_a.width(); col++)
226 | {
227 | QColor color = QColor(line_data[col]).toHsl();
228 | hue = color.hue();
229 | saturation = color.saturation() + 200;
230 | lightness = color.lightness();
231 | saturation = qBound(0, saturation, 255);
232 | color.setHsl(hue, saturation, lightness);
233 | line_data[col] = color.rgba();
234 | }
235 | }
236 | painter.drawImage(10 + 210 * 4, 310, img_a);
237 | painter.drawText(10 + 210 * 4, 300, "saturation");
238 | }
239 |
--------------------------------------------------------------------------------
/src/tabbasic/BasicImage.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | // 图片绘制相关
8 | // 有趣的链接:
9 | // https://www.cnblogs.com/aslistener/articles/4478303.html
10 | // https://www.cnblogs.com/swarmbees/p/5722882.html
11 | // https://blog.csdn.net/u013015629/article/details/54669608
12 | class BasicImage : public QWidget
13 | {
14 | Q_OBJECT
15 | public:
16 | explicit BasicImage(QWidget *parent = nullptr);
17 |
18 | protected:
19 | // 显示时才启动定时动画
20 | void showEvent(QShowEvent *event) override;
21 | void hideEvent(QHideEvent *event) override;
22 | // 绘制
23 | void paintEvent(QPaintEvent *event) override;
24 |
25 | private:
26 | // 定时器动画
27 | QTimer timer;
28 | int timeOffset{0};
29 | // 待绘制的图 - 荷花 200*200
30 | QImage imgCache;
31 | // 遮罩样式 - 心形 100*100
32 | QImage maskCache;
33 | };
34 |
--------------------------------------------------------------------------------
/src/tabbasic/PenColor.cpp:
--------------------------------------------------------------------------------
1 | #include "PenColor.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 |
9 | PenColor::PenColor(QWidget *parent)
10 | : QWidget(parent)
11 | {
12 | // 定时移动
13 | connect(&timer, &QTimer::timeout, this, [this]() {
14 | // 使 offset 一直在 [-1, 1) 区间内
15 | angleOffset += angleStep;
16 | if (angleOffset > 1.0 - angleStep / 2)
17 | angleOffset = -1.0;
18 | update();
19 | });
20 | }
21 |
22 | void PenColor::showEvent(QShowEvent *event)
23 | {
24 | timer.start(50);
25 | QWidget::showEvent(event);
26 | }
27 |
28 | void PenColor::hideEvent(QHideEvent *event)
29 | {
30 | timer.stop();
31 | QWidget::hideEvent(event);
32 | }
33 |
34 | void PenColor::paintEvent(QPaintEvent *event)
35 | {
36 | Q_UNUSED(event)
37 | QPainter painter(this);
38 | // 先画一个黑底白框
39 | painter.fillRect(this->rect(), Qt::black);
40 | painter.setPen(QPen(Qt::white, 1, Qt::DashLine));
41 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); // 右下角会超出范围
42 |
43 | // 【1】 sin 正弦曲线
44 | // 计算路径
45 | QPainterPath line_path;
46 | double angle = angleOffset;
47 | line_path.moveTo(0, height() / 2 - height() / 3 * sin(angle * 3.14159265));
48 | QList x_pos; // 竖线横坐标
49 | for (int i = 0; i < width(); i++, angle += angleStep)
50 | {
51 | if (angle > 1.0 - angleStep / 2)
52 | {
53 | angle = -1.0;
54 | }
55 | // 0.5 时正弦在正半轴峰顶
56 | if (abs(angle - 0.5) < 1E-7)
57 | {
58 | x_pos.push_back(i);
59 | }
60 | // sin 值域为 [-1, 1],这里相当于把 y 值下移并放大
61 | // 因为 Qt 是屏幕坐标系,所以又把 y 取反了再加的偏移
62 | line_path.lineTo(i, height() / 2 - height() / 3 * sin(angle * 3.14159265));
63 | }
64 |
65 | // 先绘制渐变底色
66 | QPainterPath area_path = line_path;
67 | area_path.lineTo(width(), height());
68 | area_path.lineTo(0, height());
69 | area_path.closeSubpath();
70 | QLinearGradient gradient(0, 0, 0, height());
71 | gradient.setColorAt(0.0, QColor(0, 255, 0, 150));
72 | gradient.setColorAt(1.0, QColor(0, 255, 0, 0));
73 | painter.fillPath(area_path, gradient);
74 |
75 | // 绘制路径
76 | painter.save();
77 | // 划分为三个区域依次绘制
78 | painter.setRenderHint(QPainter::Antialiasing);
79 | //
80 | painter.setClipRect(0, 0, width(), height() / 3);
81 | painter.setPen(QPen(Qt::cyan, 2));
82 | painter.drawPath(line_path);
83 | //
84 | painter.setClipRect(0, height() / 3, width(), height() / 3);
85 | painter.setPen(QPen(Qt::blue, 2));
86 | painter.drawPath(line_path);
87 | //
88 | painter.setClipRect(0, height() / 3 * 2, width(), height() / 3);
89 | painter.setPen(QPen(Qt::yellow, 2));
90 | painter.drawPath(line_path);
91 | painter.restore();
92 |
93 | // 画两条分割线在上面
94 | painter.setPen(QPen(Qt::white, 1, Qt::DashLine));
95 | painter.drawLine(0, height() / 3, width(), height() / 3);
96 | painter.drawLine(0, height() / 3 * 2, width(), height() / 3 * 2);
97 | // 绘制竖线
98 | for (int i : x_pos)
99 | {
100 | painter.drawLine(i, 0, i, height());
101 | }
102 |
103 | // 【2】 雷达扫描
104 | // 黑底圆盘
105 | const QRect scan_rect(10, 10, 100, 100); // 100*100
106 | const int scan_radius = scan_rect.width() / 2;
107 | painter.save();
108 | painter.translate(scan_rect.center());
109 | QPainterPath ellipse_path;
110 | ellipse_path.addEllipse(QPoint(0, 0), scan_radius, scan_radius);
111 | painter.fillPath(ellipse_path, Qt::black);
112 | painter.setRenderHint(QPainter::Antialiasing, true);
113 | // 旋转扫描
114 | QConicalGradient conical_gradient(QPoint(0, 0), scan_radius);
115 | conical_gradient.setAngle(-angleOffset * 180.0);
116 | conical_gradient.setColorAt(0.0, Qt::green);
117 | conical_gradient.setColorAt(0.4, Qt::transparent);
118 | conical_gradient.setColorAt(1.0, Qt::transparent);
119 | painter.fillPath(ellipse_path, conical_gradient);
120 | // painter.setBrush(conical_gradient);
121 | // painter.setPen(Qt::NoPen);
122 | // painter.drawPie(QRect(-scan_radius,-scan_radius,2*scan_radius,2*scan_radius),
123 | // -angleOffset*180.0*16,360*0.5*16);
124 | // 加一条线
125 | painter.setPen(QPen(Qt::cyan, 2));
126 | painter.drawLine(QPoint(0, 0),
127 | QPoint(scan_radius * cos(angleOffset * 3.14159265),
128 | scan_radius * sin(angleOffset * 3.14159265)));
129 | // 白色框压在上面
130 | painter.setPen(QPen(Qt::white, 2));
131 | painter.drawEllipse(QPoint(0, 0), scan_radius, scan_radius);
132 | // 十字,1px 直线用不抗锯齿
133 | painter.setRenderHint(QPainter::Antialiasing, false);
134 | painter.setPen(QPen(Qt::white, 1, Qt::DashLine));
135 | painter.drawLine(-scan_radius, 0, scan_radius, 0);
136 | painter.drawLine(0, -scan_radius, 0, scan_radius);
137 | painter.restore();
138 | }
139 |
--------------------------------------------------------------------------------
/src/tabbasic/PenColor.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | // 展示画笔/画刷颜色的使用
6 | class PenColor : public QWidget
7 | {
8 | Q_OBJECT
9 | public:
10 | explicit PenColor(QWidget *parent = nullptr);
11 |
12 | protected:
13 | // 显示时才启动定时动画
14 | void showEvent(QShowEvent *event) override;
15 | void hideEvent(QHideEvent *event) override;
16 | // 绘制
17 | void paintEvent(QPaintEvent *event) override;
18 |
19 | private:
20 | // 当前角度偏移
21 | double angleOffset{0.0};
22 | // 角度步进
23 | double angleStep{0.01};
24 |
25 | // 定时移动
26 | QTimer timer;
27 | };
28 |
--------------------------------------------------------------------------------
/src/tabbasic/PenStyle.cpp:
--------------------------------------------------------------------------------
1 | #include "PenStyle.h"
2 |
3 | #include
4 | #include
5 |
6 | PenStyle::PenStyle(QWidget *parent)
7 | : QWidget(parent)
8 | {
9 | // 累计 dash 总长
10 | for (int item : qAsConst(customDash))
11 | {
12 | dashCount += item;
13 | }
14 |
15 | // 定时移动虚线偏移,制作蚂蚁线效果
16 | connect(&timer, &QTimer::timeout, this, [this]() {
17 | // 虚线移动
18 | ++dashOffset;
19 | // 模以 dash 总长度,防止越界等
20 | dashOffset %= dashCount;
21 | update();
22 | });
23 | }
24 |
25 | void PenStyle::showEvent(QShowEvent *event)
26 | {
27 | timer.start(150);
28 | QWidget::showEvent(event);
29 | }
30 |
31 | void PenStyle::hideEvent(QHideEvent *event)
32 | {
33 | timer.stop();
34 | QWidget::hideEvent(event);
35 | }
36 |
37 | void PenStyle::paintEvent(QPaintEvent *event)
38 | {
39 | Q_UNUSED(event)
40 | QPainter painter(this);
41 | // 先画一个白底黑框
42 | painter.fillRect(this->rect(), Qt::white);
43 | QPen pen(Qt::black);
44 | // SquareCap(默认)方形线端,覆盖端点(在端点延伸了半个线宽)
45 | // FlatCap 方形线端,不覆盖端点
46 | // RoundCap 圆角线端,覆盖端点
47 | pen.setCapStyle(Qt::SquareCap);
48 | // BevelJoin(默认)两线相交处截平
49 | // MiterJoin 两线相交处填充三角延申
50 | // RoundJoin 相交处圆角
51 | pen.setJoinStyle(Qt::MiterJoin);
52 | painter.setPen(pen);
53 | // 右下角会超出范围
54 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1));
55 |
56 | // 准备路径
57 | // item 是元素可视大小, rect 是元素整体整体占位大小
58 | const int rect_width = this->width() / 3; //(3*2 个圆)
59 | const int rect_height = this->height() / 2;
60 | const int center_x = rect_width / 2;
61 | const int center_y = rect_height / 2;
62 | // 取最短边为边长为宽度(3*2 个圆)
63 | const int item_width = ((this->width() / 3 < this->height() / 2)
64 | ? this->width() / 3
65 | : this->height() / 2) - 10;
66 | // item 在 rect 的位置
67 | const int item_left = (rect_width - item_width) / 2;
68 | const int item_top = (rect_height - item_width) / 2;
69 | // (一个圆加一个方框)
70 | QPainterPath path;
71 | path.addRect(item_left, item_top, item_width, item_width);
72 | // path.addEllipse(item_left, item_top, item_width-10, item_width-10);
73 | path.addEllipse(QPoint(rect_width / 2, rect_height / 2),
74 | item_width / 2 - 5, item_width / 2 - 5);
75 |
76 | // 依次试用 pen style 枚举值
77 | pen.setColor(Qt::darkBlue); // 原谅色
78 | // (如果线宽是奇数,直线抗锯齿会模糊,这是 Qt 的 bug)
79 | pen.setWidth(4); // 线宽
80 | // 不开抗锯齿曲线有锯齿
81 | painter.setRenderHint(QPainter::Antialiasing);
82 | // 保存位置,等会儿换行用 restore 恢复,然后下移画第二行
83 | painter.save();
84 |
85 | // Qt::NoPen 啥都没有
86 | // Qt::SolidLine 实线,默认值
87 | pen.setStyle(Qt::SolidLine);
88 | painter.setPen(pen);
89 | painter.drawPath(path);
90 | painter.drawText(item_left + 10, item_top + 20, "SolidLine");
91 |
92 | // Qt::DashLine 虚线
93 | pen.setStyle(Qt::DashLine);
94 | painter.setPen(pen);
95 | painter.translate(rect_width, 0); // 右移
96 | painter.drawPath(path);
97 | painter.drawText(item_left + 10, item_top + 20, "DashLine");
98 |
99 | // Qt::DotLine 点线
100 | pen.setStyle(Qt::DotLine);
101 | painter.setPen(pen);
102 | painter.translate(rect_width, 0); // 右移
103 | painter.drawPath(path);
104 | painter.drawText(item_left + 10, item_top + 20, "DotLine");
105 |
106 | // 恢复位置后画准备第二行
107 | painter.restore();
108 | painter.save();
109 |
110 | // Qt::DashDotLine 混合
111 | pen.setStyle(Qt::DashDotLine);
112 | painter.setPen(pen);
113 | painter.translate(0, rect_height); // 下移
114 | painter.drawPath(path);
115 | painter.drawText(item_left + 10, item_top + 20, "DashDotLine");
116 |
117 | // Qt::DashDotDotLine 混合
118 | pen.setStyle(Qt::DashDotDotLine);
119 | painter.setPen(pen);
120 | painter.translate(rect_width, 0); // 右移
121 | painter.drawPath(path);
122 | painter.drawText(item_left + 10, item_top + 20, "DashDotDotLine");
123 |
124 | // Qt::CustomDashLine
125 | // 除了要设置 pen style 为 Qt::CustomDashLine 外,
126 | // 还需要调用 setDashPattern 来描述自定义虚线的样子
127 | pen.setStyle(Qt::CustomDashLine);
128 | // 参数奇数为线长度,偶数为线间隔(绘制的时候他好像没把线宽考虑进去)
129 | pen.setDashPattern(customDash);
130 | // 定时移动偏移,蚂蚁线效果
131 | pen.setDashOffset(dashOffset);
132 | painter.setPen(pen);
133 | painter.translate(rect_width, 0); //右移
134 | painter.drawPath(path);
135 | painter.drawText(item_left + 10, item_top + 20, "CustomDashLine");
136 |
137 | // 恢复位置准备画 cap 和 join 示意
138 | painter.restore();
139 | const int line_width = 10;
140 | path = QPainterPath(); // 低版本没有 clear
141 | path.moveTo(center_x - line_width * 4, center_y - line_width * 2);
142 | path.lineTo(center_x - line_width * 1, center_y - line_width * 2);
143 | path.lineTo(center_x - line_width * 1, center_y);
144 | path.lineTo(center_x - line_width * 4, center_y);
145 | path.lineTo(center_x - line_width * 4, center_y + line_width * 2);
146 | path.lineTo(center_x - line_width * 1, center_y + line_width * 2);
147 | path.moveTo(center_x + line_width * 4, center_y);
148 | path.lineTo(center_x + line_width * 1, center_y);
149 | path.lineTo(center_x + line_width * 2.5, center_y - line_width * 2);
150 | path.lineTo(center_x + line_width * 2.5, center_y + line_width * 2);
151 | pen.setStyle(Qt::SolidLine); // 默认值
152 | pen.setCapStyle(Qt::SquareCap); // 默认值
153 | pen.setJoinStyle(Qt::BevelJoin); // 默认值
154 | pen.setColor(Qt::red);
155 | pen.setWidth(line_width);
156 | // 保存位置,等会儿换行用 restore 恢复,然后下移画第二行
157 | painter.save();
158 |
159 | // 第一行 CapStyle 端点样式
160 | pen.setCapStyle(Qt::SquareCap);
161 | painter.setPen(pen);
162 | painter.drawPath(path);
163 | painter.drawText(center_x - 20, center_y + line_width * 4, "SquareCap");
164 |
165 | pen.setCapStyle(Qt::FlatCap);
166 | painter.setPen(pen);
167 | painter.translate(rect_width, 0); // 右移
168 | painter.drawPath(path);
169 | painter.drawText(center_x - 20, center_y + line_width * 4, "FlatCap");
170 |
171 | pen.setCapStyle(Qt::RoundCap);
172 | painter.setPen(pen);
173 | painter.translate(rect_width, 0); // 右移
174 | painter.drawPath(path);
175 | painter.drawText(center_x - 20, center_y + line_width * 4, "RoundCap");
176 |
177 | // 恢复位置后画准备第二行
178 | painter.restore();
179 | pen.setStyle(Qt::SolidLine); // 默认值
180 | pen.setCapStyle(Qt::SquareCap); // 默认值
181 | pen.setJoinStyle(Qt::BevelJoin); // 默认值
182 | pen.setColor(Qt::green);
183 | painter.save();
184 |
185 | // 第二行画 JoinStyle 交点样式
186 | pen.setJoinStyle(Qt::BevelJoin);
187 | painter.setPen(pen);
188 | painter.translate(0, rect_height); // 下移
189 | painter.drawPath(path);
190 | painter.drawText(center_x - 20, center_y + line_width * 4, "BevelJoin");
191 |
192 | pen.setJoinStyle(Qt::MiterJoin);
193 | painter.setPen(pen);
194 | painter.translate(rect_width, 0); // 右移
195 | painter.drawPath(path);
196 | painter.drawText(center_x - 20, center_y + line_width * 4, "MiterJoin");
197 |
198 | pen.setJoinStyle(Qt::RoundJoin);
199 | painter.setPen(pen);
200 | painter.translate(rect_width, 0); // 右移
201 | painter.drawPath(path);
202 | painter.drawText(center_x - 20, center_y + line_width * 4, "RoundJoin");
203 | painter.restore();
204 | }
205 |
--------------------------------------------------------------------------------
/src/tabbasic/PenStyle.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 | #include
5 |
6 | // 展示画笔样式的使用
7 | class PenStyle : public QWidget
8 | {
9 | Q_OBJECT
10 | public:
11 | explicit PenStyle(QWidget *parent = nullptr);
12 |
13 | protected:
14 | // 显示时才启动定时动画
15 | void showEvent(QShowEvent *event) override;
16 | void hideEvent(QHideEvent *event) override;
17 | // 绘制
18 | void paintEvent(QPaintEvent *event) override;
19 |
20 | private:
21 | // CustomDashLine 时使用,奇数为线长,偶数为间隔,绘制时循环使用
22 | QVector customDash{2, 3, 4, 5, 6, 7};
23 | // customDash 的累计长度
24 | int dashCount{0};
25 |
26 | // 虚线定时移动实现蚂蚁线
27 | QTimer timer;
28 | // 蚂蚁线偏移
29 | int dashOffset{0};
30 | };
31 |
--------------------------------------------------------------------------------
/src/tabbasic/TabBasic.cpp:
--------------------------------------------------------------------------------
1 | #include "TabBasic.h"
2 | #include "ui_TabBasic.h"
3 |
4 | TabBasic::TabBasic(QWidget *parent)
5 | : QWidget{parent}
6 | , ui{new Ui::TabBasic}
7 | {
8 | ui->setupUi(this);
9 | ui->tabWidget->setCurrentIndex(0);
10 | }
11 |
12 | TabBasic::~TabBasic()
13 | {
14 | delete ui;
15 | }
16 |
--------------------------------------------------------------------------------
/src/tabbasic/TabBasic.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 |
4 | namespace Ui{
5 | class TabBasic;
6 | }
7 |
8 | // 一些基本的接口效果演示
9 | class TabBasic : public QWidget
10 | {
11 | Q_OBJECT
12 | public:
13 | explicit TabBasic(QWidget *parent = nullptr);
14 | ~TabBasic();
15 |
16 | private:
17 | Ui::TabBasic *ui;
18 | };
19 |
--------------------------------------------------------------------------------
/src/tabbasic/TabBasic.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | TabBasic
4 |
5 |
6 |
7 | 0
8 | 0
9 | 475
10 | 351
11 |
12 |
13 |
14 | Form
15 |
16 |
17 | -
18 |
19 |
20 | Qt::NoFocus
21 |
22 |
23 | 0
24 |
25 |
26 |
27 | PenStyle
28 |
29 |
30 |
31 |
32 | PenColor
33 |
34 |
35 |
36 |
37 | TextPath
38 |
39 |
40 |
41 |
42 | Curve
43 |
44 |
45 |
46 |
47 | Image
48 |
49 |
50 |
51 |
52 | Composition
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | PenStyle
62 | QWidget
63 |
64 | 1
65 |
66 |
67 | PenColor
68 | QWidget
69 |
70 | 1
71 |
72 |
73 | TextPath
74 | QWidget
75 |
76 | 1
77 |
78 |
79 | BasicCurve
80 | QWidget
81 |
82 | 1
83 |
84 |
85 | BasicImage
86 | QWidget
87 |
88 | 1
89 |
90 |
91 | BasicComposition
92 | QWidget
93 |
94 | 1
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/tabbasic/TextPath.cpp:
--------------------------------------------------------------------------------
1 | #include "TextPath.h"
2 | #include "GlobalDef.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | TextPath::TextPath(QWidget *parent)
12 | : QWidget(parent)
13 | {
14 | // 定时移动
15 | connect(&timer, &QTimer::timeout, this, [=]() {
16 | // 左侧文字区域宽高变化
17 | if (areaAdd)
18 | {
19 | areaWidth += 2;
20 | if (areaWidth > 400)
21 | {
22 | areaAdd = false;
23 | areaWidth = 400;
24 | }
25 | }
26 | else
27 | {
28 | areaWidth -= 2;
29 | if (areaWidth < 100)
30 | {
31 | areaAdd = true;
32 | areaWidth = 100;
33 | }
34 | }
35 | // 底部文本从左往右循环滚动
36 | textOffset_1 += 3;
37 | if (textOffset_1 > textWidth_1 + labelWidth_1)
38 | {
39 | textOffset_1 = 0;
40 | }
41 | // 底部文本从右往左循环滚动
42 | textOffset_2 += 3;
43 | if (textOffset_2 > textWidth_2 + labelWidth_2)
44 | {
45 | textOffset_2 = 0;
46 | }
47 | update();
48 | });
49 | }
50 |
51 | void TextPath::showEvent(QShowEvent *event)
52 | {
53 | timer.start(50);
54 | QWidget::showEvent(event);
55 | }
56 |
57 | void TextPath::hideEvent(QHideEvent *event)
58 | {
59 | timer.stop();
60 | QWidget::hideEvent(event);
61 | }
62 |
63 | void TextPath::paintEvent(QPaintEvent *event)
64 | {
65 | event->accept();
66 | QPainter painter(this);
67 | // 先画一个白底黑框
68 | painter.fillRect(this->rect(), Qt::white);
69 | QPen pen(Qt::black);
70 | painter.setPen(pen);
71 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); // 右下角会超出范围
72 | // 窗口最短边
73 | const int border_width = width() > height() ? height() : width();
74 |
75 | // 1. 中心绘制一个 Qt 文字
76 | QColor qt_color(200, 250, 200); // 淡淡的原谅色
77 | QFont qt_font("Microsoft YaHei", border_width / 3);
78 | QPen qt_pen(qt_color, border_width / 20 + 1);
79 | painter.setFont(qt_font);
80 | painter.setPen(qt_pen);
81 | const QString qt_str = "Qt";
82 | // 文本的 pos 是在左下角
83 | const int qt_str_width = GetTextWidth(painter.fontMetrics(), qt_str);
84 | const int qt_str_height = GetTextHeight(painter.fontMetrics());
85 | // ascent 从基线到字符延伸到的最高位置的距离
86 | // descent 从基线到最低点字符延伸到的距离
87 | const int qt_str_descent = painter.fontMetrics().descent();
88 | const int qt_left = (width() - qt_str_width) / 2;
89 | const int qt_bottom = (height() + qt_str_height) / 2;
90 | const int qt_bottom_real = (height() + qt_str_height) / 2 - qt_str_descent;
91 | const QRect qt_rect = QRect(QPoint(qt_left, qt_bottom - qt_str_height),
92 | QPoint(qt_left + qt_str_width, qt_bottom));
93 | // 绘制文字的时候,如果使用 rect 指定位置,那么就在矩形中间绘制,可以指定位置 flags
94 | // 如果使用 xy 指定位置,会受到基线等影响,上下没对齐,要计算基线的偏移
95 | // painter.drawText(qt_rect,qt_str);
96 | // painter.drawText(qt_left,qt_bottom_real,qt_str);
97 | QPainterPath qt_str_path;
98 | qt_str_path.addText(qt_left, qt_bottom_real, qt_font, qt_str);
99 | // 画路径
100 | painter.setPen(QPen(qt_color, 2, Qt::DashLine));
101 | painter.setRenderHint(QPainter::Antialiasing);
102 | painter.drawPath(qt_str_path);
103 | painter.drawRect(qt_rect);
104 |
105 | // 2. 左上角绘制文字
106 | const QString str_1 = "一切都是没有结局的开始";
107 | const QString str_2 = "一切都是稍纵即逝的追寻";
108 | const QFont str_font("Microsoft YaHei", 28);
109 | const int str_height = GetTextHeight(QFontMetrics(str_font));
110 | QPainterPath str_path;
111 | // 暂时用的固定值坐标
112 | str_path.addText(20, str_height, str_font, str_1);
113 | str_path.addText(20, str_height * 2, str_font, str_2);
114 | painter.setPen(QPen(Qt::green, 0.5));
115 | QLinearGradient str_gradient(0, 0, 0, str_height * 2);
116 | // 用渐变色来实现文字填充一半的效果,中间部分填充透明色
117 | // 百分比用值计算好点,我这里手动调的固定值
118 | str_gradient.setColorAt(0.0, Qt::green);
119 | str_gradient.setColorAt(0.35, Qt::green);
120 | str_gradient.setColorAt(0.351, Qt::transparent);
121 | str_gradient.setColorAt(0.85, Qt::transparent);
122 | str_gradient.setColorAt(0.851, Qt::green);
123 | str_gradient.setColorAt(1.0, Qt::green);
124 | painter.drawPath(str_path);
125 | painter.fillPath(str_path, str_gradient);
126 |
127 | // 3. 左侧文字
128 | QRect left_area(0, 0, areaWidth, 420 - areaWidth);
129 | left_area.moveTo(20, ((height() - left_area.height()) / 2));
130 | painter.setPen(QPen(QColor(255, 0, 0), 2));
131 | painter.drawRect(left_area);
132 | QFont ft;
133 | ft.setFamily("SimSun");
134 | ft.setPixelSize(left_area.height());
135 | QFontMetrics fm(ft);
136 | const QString text = "Hello!龚建波1992";
137 | double scale = left_area.width() / double(GetTextWidth(fm, text) + 0.1) * 100;
138 | // stretch 100就是百分之百
139 | // 最小拉伸因子为 1,最大拉伸因子为 4000
140 | // BUG:当因子小于100,但是dpi有缩放,没法正常的绘制缩窄的文字
141 | ft.setStretch(scale);
142 | QFontMetrics fm2(ft);
143 | if (prevWidth > 0)
144 | {
145 | // 拉伸之后 Qt 计算可能有点问题
146 | // 有些字号更大了,但是拉伸后反而变窄了
147 | // 所以保持上次的字体大小和宽度,异常则使用上次的字体
148 | // 使之增加时不能小于上次,减小时不能大于上次
149 | if ((areaAdd && GetTextWidth(fm2, text) < prevWidth) || (!areaAdd && GetTextWidth(fm2, text) > prevWidth))
150 | {
151 | ft.setPixelSize(prevSize);
152 | ft.setStretch(prevStretch);
153 | }
154 | }
155 | // qDebug()< 0)
177 | {
178 | textWidth_1 = scroll_width1;
179 | textOffset_1 = 0;
180 | }
181 | else
182 | {
183 | // 彩色文本
184 | QLinearGradient gradient(textOffset_1 - textWidth_1, 0,
185 | textOffset_1, 0);
186 | gradient.setColorAt(0, QColor(Qt::red));
187 | gradient.setColorAt(0.33, QColor(Qt::yellow));
188 | gradient.setColorAt(0.66, QColor(Qt::green));
189 | gradient.setColorAt(1, QColor(Qt::blue));
190 | painter.setPen(QPen(QBrush(gradient), 2));
191 | painter.drawText(textOffset_1 - textWidth_1, scroll_y1, scroll_text1);
192 | }
193 |
194 | // 5. 底部滚动的文字2
195 | const QString scroll_text2 = "我觉得你看我时很远,你看云时很近。";
196 | const int scroll_width2 = GetTextWidth(painter.fontMetrics(), scroll_text2);
197 | // const int scroll_height2 = painter.fontMetrics().capHeight();
198 | const int scroll_y2 = height() - 10; // Qt 文本起点在左下角
199 | // 文本宽度改变,重新滚动(包括设置了文本,修改了字体大小等)
200 | if (textWidth_2 != scroll_width2 && scroll_width2 > 0)
201 | {
202 | textWidth_2 = scroll_width2;
203 | textOffset_2 = 0;
204 | }
205 | else
206 | {
207 | // 彩色文本
208 | QLinearGradient gradient(labelWidth_2 - textOffset_2, 0,
209 | labelWidth_2 - textOffset_2 + textWidth_2, 0);
210 | gradient.setColorAt(0, QColor(Qt::red));
211 | gradient.setColorAt(0.33, QColor(Qt::yellow));
212 | gradient.setColorAt(0.66, QColor(Qt::green));
213 | gradient.setColorAt(1, QColor(Qt::blue));
214 | painter.setPen(QPen(QBrush(gradient), 2));
215 | painter.drawText(labelWidth_2 - textOffset_2, scroll_y2, scroll_text2);
216 | }
217 | }
218 |
219 | void TextPath::resizeEvent(QResizeEvent *event)
220 | {
221 | const int old_width = event->oldSize().width();
222 | const int new_width = event->size().width();
223 | // 从左往右,比之前的更小,重新滚动
224 | if (new_width > 10)
225 | {
226 | labelWidth_1 = new_width;
227 | if (new_width < old_width)
228 | {
229 | textOffset_1 = 0;
230 | }
231 | }
232 | // 从右往左,比之前的更小,重新滚动
233 | if (new_width > 10)
234 | {
235 | labelWidth_2 = new_width;
236 | if (new_width < old_width)
237 | {
238 | textOffset_2 = 0;
239 | }
240 | }
241 | QWidget::resizeEvent(event);
242 | }
243 |
--------------------------------------------------------------------------------
/src/tabbasic/TextPath.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | // 展示文字路径
6 | class TextPath : public QWidget
7 | {
8 | Q_OBJECT
9 | public:
10 | explicit TextPath(QWidget *parent = nullptr);
11 |
12 | protected:
13 | // 显示时才启动定时动画
14 | void showEvent(QShowEvent *event) override;
15 | void hideEvent(QHideEvent *event) override;
16 | // 绘制
17 | void paintEvent(QPaintEvent *event) override;
18 | // 窗口改变大小,可能需要重新计算
19 | void resizeEvent(QResizeEvent *event) override;
20 |
21 | private:
22 | // 左侧文字区域宽度
23 | int areaWidth{350};
24 | // 控制宽度变大还是变小
25 | bool areaAdd{false};
26 | // 拉伸之后 Qt 计算可能有点问题
27 | // 有些字号更大了,但是拉伸后反而变窄了
28 | // 所以保持上次的字体大小和宽度,异常则使用上次的字体
29 | // 使之增加时不能小于上次,减小时不能大于上次
30 | // 上次文本宽
31 | int prevWidth{0};
32 | // 上次字号
33 | int prevSize{0};
34 | // 上次拉伸因子
35 | int prevStretch{0};
36 |
37 | // 文本移动偏移量
38 | int textOffset_1{0};
39 | // 文本绘制宽度
40 | int textWidth_1{1};
41 | // 文本绘制区域宽度
42 | int labelWidth_1{1};
43 |
44 | // 文本移动偏移量
45 | int textOffset_2{0};
46 | // 文本绘制宽度
47 | int textWidth_2{1};
48 | // 文本绘制区域宽度
49 | int labelWidth_2{1};
50 |
51 | // 定时动画
52 | QTimer timer;
53 | };
54 |
--------------------------------------------------------------------------------
/src/tabbasic/tabbasic.pri:
--------------------------------------------------------------------------------
1 | FORMS += \
2 | $$PWD/TabBasic.ui
3 |
4 | HEADERS += \
5 | $$PWD/BasicComposition.h \
6 | $$PWD/BasicCurve.h \
7 | $$PWD/BasicImage.h \
8 | $$PWD/PenColor.h \
9 | $$PWD/PenStyle.h \
10 | $$PWD/TabBasic.h \
11 | $$PWD/TextPath.h
12 |
13 | SOURCES += \
14 | $$PWD/BasicComposition.cpp \
15 | $$PWD/BasicCurve.cpp \
16 | $$PWD/BasicImage.cpp \
17 | $$PWD/PenColor.cpp \
18 | $$PWD/PenStyle.cpp \
19 | $$PWD/TabBasic.cpp \
20 | $$PWD/TextPath.cpp
21 |
--------------------------------------------------------------------------------
/src/tabchart/PieView.cpp:
--------------------------------------------------------------------------------
1 | #include "PieView.h"
2 | #include "GlobalDef.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | PieView::PieView(QWidget *parent)
12 | : QWidget(parent)
13 | {
14 | setMouseTracking(true);
15 |
16 | // 添加测试数据
17 | appendSlice(PieSlice("1 gong", 10));
18 | appendSlice(PieSlice("2 jian", 5));
19 | appendSlice(PieSlice("3 bo", 13));
20 | appendSlice(PieSlice("4 1992", 2));
21 | }
22 |
23 | PieView::~PieView()
24 | {
25 |
26 | }
27 |
28 | void PieView::appendSlice(const PieSlice &slice)
29 | {
30 | // 去重
31 | for (const PieSlice &item : qAsConst(sliceList))
32 | {
33 | if(item.name == slice.name)
34 | return;
35 | }
36 | sliceList.append(slice);
37 | sliceValueCount += slice.value;
38 | // 重新排序-降序
39 | std::sort(sliceList.begin(), sliceList.end(),
40 | [](const PieSlice &left, const PieSlice &right)->bool {
41 | return left.value > right.value;
42 | });
43 | double start_angle = 0; // 起始角度 temp,用于累加
44 | int h_value = -1000; // 色度 temp,用于相近色计算
45 | for (PieSlice &item : sliceList) {
46 | item.percentage = item.value / sliceValueCount; // 计算百分比
47 | item.startAngle = start_angle; // 起始角
48 | item.angleSpan = item.percentage * 360; // 占的角度值
49 | start_angle += item.angleSpan;
50 | // 此处用的随机颜色,也可以通过传入一个颜色列表来取对应颜色
51 | int new_h_value = std::rand() % 360;
52 | // 本来想算一个不相近的颜色,但是 0 附近和 359 附近颜色也接近
53 | // 并且,没有考虑整体的颜色独立性
54 | while(qAbs(new_h_value - h_value) < 60){
55 | new_h_value = std::rand() % 360;
56 | }
57 | // Hue 色度 [0, 359], Lightness 亮度 [0, 255], Saturation 饱和度 [0, 255]
58 | item.color = QColor::fromHsl(new_h_value, 220, 80);
59 | h_value = new_h_value; // 用于下次计算色度相近
60 | }
61 | // 刷新 ui
62 | hoveredFlag = false;
63 | hoveredIndex = -1;
64 | update();
65 | }
66 |
67 | void PieView::clearSlice()
68 | {
69 | sliceList.clear();
70 | sliceValueCount = 0.0;
71 | // 刷新 ui
72 | hoveredFlag = false;
73 | hoveredIndex = -1;
74 | update();
75 | }
76 |
77 | void PieView::paintEvent(QPaintEvent *event)
78 | {
79 | event->accept();
80 | QPainter painter(this);
81 | painter.setRenderHint(QPainter::Antialiasing);
82 |
83 | // 绘制扇形
84 | painter.save();
85 | painter.translate(rect().center()); // 中心点移动到 pie 的中心点
86 | painter.setPen(QColor(255, 255, 255));
87 | int index = 0;
88 | for (const PieSlice &item : qAsConst(sliceList))
89 | {
90 | // 半径随 index 递减样式
91 | int slice_radius = (pieRadius - index * 5);
92 | // hover 选中时半径突出一点
93 | if (hoveredFlag && index == hoveredIndex) {
94 | slice_radius += 10;
95 | }
96 | // slice 扇形路径
97 | {
98 | QPainterPath path;
99 | path.moveTo(QPointF(0, 0));
100 | const QRectF pie_rect = QRectF(-slice_radius, -slice_radius,
101 | slice_radius * 2, slice_radius * 2);
102 | path.arcTo(pie_rect, item.startAngle, item.angleSpan);
103 | path.lineTo(QPointF(0, 0));
104 | painter.fillPath(path, QBrush(item.color.lighter()));
105 | }
106 | if (slice_radius > 20) {
107 | QPainterPath path;
108 | path.moveTo(QPointF(0, 0));
109 | const QRectF pie_rect = QRectF(-(slice_radius - 20), -(slice_radius - 20),
110 | (slice_radius - 20) * 2, (slice_radius - 20) * 2);
111 | path.arcTo(pie_rect, item.startAngle, item.angleSpan);
112 | path.lineTo(QPointF(0, 0));
113 | painter.fillPath(path, QBrush(item.color));
114 | }
115 | // 根据扇形中心点绘制文本
116 | const QString text_percent = QString::number(item.percentage * 100, 'f', 2) + "%";
117 | const double text_angle = item.startAngle + item.angleSpan / 2; // span 中心
118 | const int text_height = GetTextHeight(painter.fontMetrics()) + 2; // 加行间隔2
119 | const int text_namewidth = GetTextWidth(painter.fontMetrics(), item.name); // 名称 str 宽度
120 | const int text_percentwidth = GetTextWidth(painter.fontMetrics(), text_percent); // 值 str 宽度
121 | const double text_x = slice_radius * 0.6 * std::cos(text_angle / 180 * M_PI); // 文本中心点
122 | const double text_y = -slice_radius * 0.6 * std::sin(text_angle / 180 * M_PI); // 文本中心点
123 |
124 | // y 轴是上负下正,所以加减操作反过来了
125 | painter.drawText(text_x - text_namewidth / 2, text_y, item.name);
126 | painter.drawText(text_x - text_percentwidth / 2, text_y + text_height, text_percent);
127 | ++index;
128 | }
129 | painter.restore();
130 |
131 | // 绘制 hover 选中 slice 的 tip
132 | if (hoveredFlag && sliceList.count() > hoveredIndex && 0 <= hoveredIndex) {
133 | const int rect_margin = 5; // 矩形边距
134 | const PieSlice &item = sliceList.at(hoveredIndex);
135 | const QString str_name = QString("name:%1").arg(item.name);
136 | const int name_width = GetTextWidth(painter.fontMetrics(), str_name) + rect_margin * 2;
137 | const QString str_value = QString("value:%1(%2%)")
138 | .arg(QString::number(item.value, 'f', 0))
139 | .arg(QString::number(item.percentage * 100, 'f', 2));
140 | const int text_height = GetTextHeight(painter.fontMetrics());
141 | const int value_width = GetTextWidth(painter.fontMetrics(), str_value) + rect_margin * 2;
142 | const int rect_height = text_height * 2 + rect_margin * 2 + 2; // 两行 + 间隔2
143 | const int rect_width = name_width > value_width ? name_width : value_width;
144 | // 左上角坐标,避免超出范围所以要判断并 set
145 | QPointF top_left(mousePos.x() - rect_width, mousePos.y() - rect_height);
146 | if(top_left.x() < pieRect.x())
147 | top_left.setX(pieRect.x());
148 | if(top_left.y() < pieRect.y())
149 | top_left.setY(pieRect.y());
150 | // 半透明矩形背景,可以 fillpath 绘制圆角矩形
151 | painter.fillRect(QRectF(top_left.x(), top_left.y(), rect_width, rect_height),
152 | QBrush(QColor(150, 150, 150, 120)));
153 | painter.setPen(QColor(255, 255, 255)); // 绘制文本,这里没有设置字体,请自行设置
154 | painter.drawText(top_left.x() + rect_margin,
155 | top_left.y() + rect_margin + text_height,
156 | str_name);
157 | painter.drawText(top_left.x() + rect_margin,
158 | top_left.y() + rect_margin + text_height * 2 + 2,
159 | str_value);
160 | }
161 | }
162 |
163 | void PieView::resizeEvent(QResizeEvent *event)
164 | {
165 | QWidget::resizeEvent(event);
166 | pieRadius = (width() > height() ? height() : width()) / 2 - pieMargin;
167 | QRect pie_rect = QRect(0, 0, pieRadius * 2, pieRadius * 2);
168 | pie_rect.moveCenter(rect().center());
169 | pieRect = pie_rect;
170 | }
171 |
172 | void PieView::mouseMoveEvent(QMouseEvent *event)
173 | {
174 | QWidget::mouseMoveEvent(event);
175 | mousePos = event->pos();
176 | // 不在范围内则清除 hover 标志
177 | if (!pieRect.contains(mousePos)) {
178 | hoveredFlag = false;
179 | hoveredIndex = -1;
180 | update();
181 | return;
182 | }
183 |
184 | // 计算当前所在角度
185 | const double arc_tan = qAtan2(mousePos.y() - pieRect.center().y(),
186 | mousePos.x() - pieRect.center().x());
187 | // aten2 结果是以右侧为 0 点,顺时针半圆为正,逆时针半圆为负,单位是弧度?
188 | // 需要转换为值正北为 0 点,顺时针增长,单位转为角度
189 | double arc_pos = arc_tan * 180 / M_PI;
190 | if (arc_pos < 0) {
191 | arc_pos = -arc_pos;
192 | } else if (arc_pos > 0) {
193 | arc_pos = 360 - arc_pos;
194 | }
195 |
196 | // 计算 hover 选中的 index
197 | int index = 0;
198 | for (const PieSlice &item : qAsConst(sliceList))
199 | {
200 | if (arc_pos >= item.startAngle && arc_pos <= (item.startAngle + item.angleSpan)) {
201 | if (index != hoveredIndex) {
202 | hoveredIndex = index;
203 | }
204 | break;
205 | }
206 | ++index;
207 | }
208 | // 因为由一个 tip 跟随鼠标移动,所以每次 move 都 update
209 | hoveredFlag = true;
210 | update();
211 | }
212 |
--------------------------------------------------------------------------------
/src/tabchart/PieView.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 |
4 | // 表示饼图的一个数据项
5 | struct PieSlice
6 | {
7 | QString name; // 名称
8 | double value; // 值
9 | double percentage; // 占总值的百分比
10 | double startAngle; // 起始角度 [0, 360)
11 | double angleSpan; // 占角度值 [0, 360),为 360 * percentage
12 | QColor color; // 颜色
13 |
14 | explicit PieSlice(const QString &name, double value = 0.0)
15 | : name(name), value(value) {
16 | }
17 | };
18 |
19 | // 饼图绘制
20 | class PieView : public QWidget
21 | {
22 | Q_OBJECT
23 | public:
24 | explicit PieView(QWidget *parent = nullptr);
25 | ~PieView();
26 |
27 | // 添加数据项
28 | void appendSlice(const PieSlice &slice);
29 | // 清除所有数据
30 | void clearSlice();
31 |
32 | protected:
33 | void paintEvent(QPaintEvent *event) override;
34 | void resizeEvent(QResizeEvent *event) override;
35 | void mouseMoveEvent(QMouseEvent *event) override;
36 |
37 | private:
38 | QList sliceList; // slice 数据容器
39 | double sliceValueCount{0.0}; // 所有 slice 值之和
40 |
41 | // hover 标志
42 | QPoint mousePos; // 记录鼠标 hover 轨迹
43 | bool hoveredFlag; // 是否 hover
44 | int hoveredIndex; // 当前选择的 index
45 |
46 | // 限定 hover 和绘制的范围
47 | int pieMargin{20};
48 | int pieRadius{20};
49 | QRectF pieRect; // 饼图绘制区域,正方形居中
50 | };
51 |
--------------------------------------------------------------------------------
/src/tabchart/TabChart.cpp:
--------------------------------------------------------------------------------
1 | #include "TabChart.h"
2 | #include "ui_TabChart.h"
3 |
4 | TabChart::TabChart(QWidget *parent)
5 | : QWidget{parent}
6 | , ui{new Ui::TabChart}
7 | {
8 | ui->setupUi(this);
9 | ui->tabWidget->setCurrentIndex(0);
10 | }
11 |
12 | TabChart::~TabChart()
13 | {
14 | delete ui;
15 | }
16 |
--------------------------------------------------------------------------------
/src/tabchart/TabChart.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 |
4 | namespace Ui {
5 | class TabChart;
6 | }
7 |
8 | // 绘制图表
9 | class TabChart : public QWidget
10 | {
11 | Q_OBJECT
12 | public:
13 | explicit TabChart(QWidget *parent = nullptr);
14 | ~TabChart();
15 |
16 | private:
17 | Ui::TabChart *ui;
18 | };
19 |
20 |
--------------------------------------------------------------------------------
/src/tabchart/TabChart.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | TabChart
4 |
5 |
6 |
7 | 0
8 | 0
9 | 515
10 | 386
11 |
12 |
13 |
14 | Form
15 |
16 |
17 | -
18 |
19 |
20 | 1
21 |
22 |
23 |
24 | XYView
25 |
26 |
27 |
28 |
29 | PieView
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | XYView
39 | QWidget
40 |
41 | 1
42 |
43 |
44 | PieView
45 | QWidget
46 |
47 | 1
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/tabchart/XYAxis.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | // 笛卡尔坐标系(直角坐标系)的坐标轴
6 | class XYAxis : public QObject
7 | {
8 | Q_OBJECT
9 | public:
10 | // 刻度线所在方位,上下左右
11 | enum AxisPosition
12 | {
13 | AtLeft,
14 | AtRight,
15 | AtTop,
16 | AtBottom
17 | };
18 | // 刻度线的间隔计算方式
19 | enum TickMode
20 | {
21 | // 固定值间隔
22 | FixedValue,
23 | // 根据参考像素间隔
24 | RefPixel
25 | };
26 |
27 | public:
28 | explicit XYAxis(QObject *parent = nullptr);
29 | // 初始化,构造后在渲染前调用
30 | void init(AxisPosition position, double minLimit, double maxLimit,
31 | double minRange, double minValue, double maxValue);
32 |
33 | // 刻度线所在方位,上下左右
34 | AxisPosition getAxisPosition() const;
35 | void setAxisPosition(AxisPosition position);
36 |
37 | // 刻度线的间隔计算方式
38 | TickMode getTickMode() const;
39 | void setTickMode(TickMode mode);
40 |
41 | // 坐标区域
42 | QRect getRect() const;
43 | void setRect(const QRect &rect);
44 |
45 | //小数精度
46 | int getDecimalPrecision() const;
47 | void setDecimalPrecision(int precison);
48 |
49 | // 固定值的间隔
50 | double getFixedValueSpace() const;
51 | void setFixedValueSpace(double value);
52 |
53 | // 参考像素范围的间隔
54 | int getRefPixelSpace() const;
55 | void setRefPixelSpace(int pixel);
56 |
57 | // 刻度位置
58 | QVector getTickPos() const;
59 | // 刻度值文本
60 | QVector getTickLabel() const;
61 |
62 | // 最小值限制
63 | double getMinLimit() const;
64 | // 最大值限制
65 | double getMaxLimit() const;
66 | // 最小范围限制
67 | double getMinRange() const;
68 | // 当前显示的最小刻度
69 | double getMinValue() const;
70 | // 当前显示的最大刻度
71 | double getMaxValue() const;
72 |
73 | // 像素与值的换算
74 | double getUnit1PxToValue() const;
75 | double getUnit1ValueToPx() const;
76 |
77 | /**
78 | * @brief 坐标轴像素值转数值
79 | * @details 暂时只有2方向,
80 | * Qt绘制起点为左上角,往右下角取正.
81 | * @param px 鼠标pos
82 | * 该函数只负责计算对应的刻度数值,横向时可能参数要减去left,
83 | * 竖向时可能参数先被bottom+1减一下
84 | * @return 对应的刻度数值
85 | */
86 | double pxToValue(double px) const;
87 |
88 | /**
89 | * @brief 数值转坐标轴像素值
90 | * @details 暂时只有2方向,
91 | * Qt绘制起点为左上角,往右下角取正.
92 | * @param value 对应的刻度数值
93 | * @return 鼠标所在point对应的px长度,
94 | * 该函数只负责计算距离,横向时可能要拿得到的px加上left,
95 | * 竖向时可能需要拿bottom+1来减去得到的px.
96 | */
97 | double valueToPx(double value) const;
98 |
99 | // 绘制
100 | void draw(QPainter *painter);
101 |
102 | private:
103 | // 坐标轴在上下左右不同位置时,绘制不同的效果,本 demo 只写部分
104 | void drawLeft(QPainter *painter);
105 | void drawBottom(QPainter *painter);
106 | // 大小或者范围等变动后重新计算刻度信息
107 | void calcAxis();
108 | // 计算间隔和起点
109 | void calcSpace(double axisLength);
110 | // 计算刻度像素间隔
111 | double calcPxSpace(double unitP2V, double valueSpace) const;
112 | // 计算刻度像素起点
113 | double calcPxStart(double unitP2V, double valueSpace, double valueMin, double valueMax) const;
114 | // 计算值间隔
115 | double calcValueSpace(double unitP2V, int pxRefSpace) const;
116 | // 辅助计算值间隔
117 | double calcValueSpaceHelper(double valueRefRange, int dividend) const;
118 | // 刻度值的小数位数
119 | int getTickPrecision() const;
120 | int getTickPrecisionHelper(double valueSpace, double compare, int precision) const;
121 | // 步进
122 | double valueCalcStep() const;
123 | double valueZoomInStep() const;
124 | double valueZoomOutStep() const;
125 | // 根据 pos 计算 zoom 的左右/上下百分比
126 | double calcZoomProportionWithPos(const QPoint &pos) const;
127 |
128 | signals:
129 | void axisChanged();
130 |
131 | public slots:
132 | // 移动
133 | void addMinValue();
134 | void subMinValue();
135 | void addMaxValue();
136 | void subMaxValue();
137 | bool moveValueWidthPx(int px);
138 | // 放大缩小
139 | void zoomValueIn();
140 | void zoomValueOut();
141 | void zoomValueInPos(const QPoint &pos);
142 | void zoomValueOutPos(const QPoint &pos);
143 | // 全览,value 设置为 limit
144 | void overallView();
145 | // 设置刻度 limit 范围
146 | void setLimitRange(double min, double max, double range);
147 | // 设置刻度当前 value 显示范围
148 | void setValueRange(double min, double max);
149 |
150 | private:
151 | // 刻度线所在方位,上下左右
152 | AxisPosition thePosition{AtLeft};
153 | // 刻度线的间隔计算方式
154 | TickMode theMode{RefPixel};
155 | // 坐标区域
156 | QRect theRect;
157 | // 显示的小数位数
158 | int decimalPrecision{3};
159 | // 刻度根据固定值间隔时的参考,一般用于等分
160 | double fixedValueSpace{100.0};
161 | // 刻度根据像素间隔的参考,一般用于自适应
162 | // 通过参考像素间隔计算得到值间隔,再取整后转换为像素间隔
163 | double refPixelSpace{35.0};
164 | // 刻度位置
165 | QVector tickPos;
166 | // 刻度值文本
167 | QVector tickLabel;
168 |
169 | // 刻度值限定范围
170 | double minLimit{0.0};
171 | double maxLimit{1000.0};
172 | // 最小缩放范围
173 | double minRange{10.0};
174 | // 当前显示范围
175 | double minValue{0.0};
176 | double maxValue{1000.0};
177 |
178 | // 1像素表示的值
179 | double unit1PxToValue{1.0};
180 | // 1单位值表示的像素
181 | double unit1ValueToPx{1.0};
182 | // 刻度绘制像素起点
183 | // 横向以左侧开始,竖向以底部开始
184 | double pxStart{0.0};
185 | // 刻度像素间隔
186 | double pxSpace{30.0};
187 | // 刻度值间隔
188 | double valueSpace{1.0};
189 | };
190 |
--------------------------------------------------------------------------------
/src/tabchart/XYView.cpp:
--------------------------------------------------------------------------------
1 | #include "XYView.h"
2 | #include "GlobalDef.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | XYView::XYView(QWidget *parent)
15 | : QWidget(parent)
16 | {
17 | setMouseTracking(true);
18 | xAxis = new XYAxis(this);
19 | xAxis->init(XYAxis::AtBottom, -1500, 1500, 100, -1000, 1000);
20 | yAxis = new XYAxis(this);
21 | yAxis->init(XYAxis::AtLeft, -1500, 1500, 100, -1000, 1000);
22 |
23 | connect(xAxis, &XYAxis::axisChanged, this, &XYView::refresh);
24 | connect(yAxis, &XYAxis::axisChanged, this, &XYView::refresh);
25 |
26 | // 初始化数据点
27 | for (int i = -500; i <= 500; i += 10)
28 | {
29 | seriesData.append(Node{i, int(500 * std::sin(qDegreesToRadians((double)i)))});
30 | }
31 | }
32 |
33 | void XYView::paintEvent(QPaintEvent *event)
34 | {
35 | event->accept();
36 | QPainter painter(this);
37 |
38 | // 绘制网格
39 | painter.setPen(QColor(0, 180, 200));
40 | auto x_tick = xAxis->getTickPos();
41 | auto y_tick = yAxis->getTickPos();
42 | for (int px : x_tick)
43 | {
44 | if (px >= plotArea.left() && px <= plotArea.right())
45 | {
46 | painter.drawLine(px, plotArea.top(), px, plotArea.bottom());
47 | }
48 | }
49 | for (int px : y_tick)
50 | {
51 | if (px >= plotArea.top() && px <= plotArea.bottom())
52 | {
53 | painter.drawLine(plotArea.left(), px, plotArea.right(), px);
54 | }
55 | }
56 |
57 | // 绘制坐标轴
58 | painter.setPen(QColor(255, 0, 0, 100));
59 | xAxis->draw(&painter);
60 | yAxis->draw(&painter);
61 |
62 | // 绘制曲线
63 | int mouse_index = -1;
64 | QPoint plot_pos = mousePos - plotArea.topLeft();
65 | if (!seriesData.isEmpty())
66 | {
67 | QPainterPath path;
68 | path.moveTo(plotArea.left() + xAxis->valueToPx(seriesData.first().x),
69 | plotArea.bottom() - yAxis->valueToPx(seriesData.first().y));
70 | for (const Node &item : qAsConst(seriesData))
71 | {
72 | path.lineTo(plotArea.left() + xAxis->valueToPx(item.x),
73 | plotArea.bottom() - yAxis->valueToPx(item.y));
74 | }
75 |
76 | painter.save();
77 | painter.setClipRect(plotArea);
78 | painter.setPen(QColor(0, 220, 0));
79 | painter.drawPath(path);
80 | // 鼠标 x 轴对应的数据点
81 | if (plotArea.isValid() && plotArea.contains(mousePos))
82 | {
83 | const double mouse_val = xAxis->pxToValue(plot_pos.x());
84 | mouse_index = searchDataIndex(0, seriesData.size(), mouse_val);
85 | painter.setPen(QPen(QColor(250, 0, 0, 100), 6));
86 | painter.drawPoint(plotArea.left() + xAxis->valueToPx(seriesData.at(mouse_index).x),
87 | plotArea.bottom() - yAxis->valueToPx(seriesData.at(mouse_index).y));
88 | }
89 | painter.restore();
90 | }
91 |
92 | painter.setPen(QColor(255, 0, 0, 100));
93 | // painter.drawRect(plotArea.adjusted(0,0,-1,-1));
94 | painter.drawRect(contentArea.adjusted(0, 0, -1, -1));
95 |
96 | // 绘制光标十字线
97 | if (plotArea.isValid() && plotArea.contains(mousePos))
98 | {
99 | painter.setPen(QColor(255, 0, 0));
100 | painter.drawLine(mousePos.x(), plotArea.top(), mousePos.x(), plotArea.bottom());
101 | painter.drawLine(plotArea.left(), mousePos.y(), plotArea.right(), mousePos.y());
102 | QString val = QString("(%1,%2)")
103 | .arg(QString::number(xAxis->pxToValue(plot_pos.x()), 'f', 2))
104 | .arg(QString::number(yAxis->pxToValue(plotArea.height() - 1 - plot_pos.y()), 'f', 2));
105 | if (mouse_index >= 0)
106 | {
107 | val = QString("(%1,%2)")
108 | .arg(QString::number(seriesData.at(mouse_index).x))
109 | .arg(QString::number(seriesData.at(mouse_index).y)) +
110 | val;
111 | }
112 | painter.drawText(mousePos + QPoint(10, -10), val);
113 | }
114 | }
115 |
116 | void XYView::resizeEvent(QResizeEvent *event)
117 | {
118 | QWidget::resizeEvent(event);
119 | // 边距
120 | int margin = 10;
121 | // series + axis 的区域
122 | // y 轴宽度 +1,x 轴高度 +1,这样 0 点就可以重合
123 | contentArea = rect().adjusted(margin, margin, -margin, -margin);
124 | xAxis->setRect(QRect(contentArea.left() + 50, contentArea.bottom() - 50,
125 | contentArea.width() - 50, 50 + 1));
126 | yAxis->setRect(QRect(contentArea.left(), contentArea.top(),
127 | 50 + 1, contentArea.height() - 50));
128 | // series 的区域
129 | plotArea = contentArea.adjusted(50, 0, 0, -50);
130 | }
131 |
132 | void XYView::mousePressEvent(QMouseEvent *event)
133 | {
134 | event->accept();
135 | mousePos = event->pos();
136 | prevPos = mousePos;
137 | if (event->button() == Qt::LeftButton)
138 | {
139 | pressFlag = true;
140 | }
141 | refresh();
142 | }
143 |
144 | void XYView::mouseMoveEvent(QMouseEvent *event)
145 | {
146 | event->accept();
147 | mousePos = event->pos();
148 | if (pressFlag)
149 | {
150 | xAxis->moveValueWidthPx(prevPos.x() - mousePos.x());
151 | yAxis->moveValueWidthPx(mousePos.y() - prevPos.y());
152 | }
153 | prevPos = mousePos;
154 | refresh();
155 | }
156 |
157 | void XYView::mouseReleaseEvent(QMouseEvent *event)
158 | {
159 | event->accept();
160 | mousePos = event->pos();
161 | pressFlag = false;
162 | refresh();
163 | }
164 |
165 | void XYView::leaveEvent(QEvent *event)
166 | {
167 | QWidget::leaveEvent(event);
168 | mousePos = QPoint(-1, -1);
169 | refresh();
170 | }
171 |
172 | void XYView::wheelEvent(QWheelEvent *event)
173 | {
174 | event->accept();
175 | const QPoint pos = GetMousePos(event);
176 | const int delta = GetMouseDelta(event);
177 | const Qt::KeyboardModifiers key_mod = QGuiApplication::keyboardModifiers();
178 | const bool delta_up = (delta > 0);
179 | // 按住ctrl滚动是Y轴缩放,否则是X轴缩放
180 | if (key_mod & Qt::ControlModifier)
181 | {
182 | if (delta_up)
183 | {
184 | yAxis->zoomValueInPos(pos);
185 | }
186 | else
187 | {
188 | yAxis->zoomValueOutPos(pos);
189 | }
190 | }
191 | else
192 | {
193 | if (delta_up)
194 | {
195 | xAxis->zoomValueInPos(pos);
196 | }
197 | else
198 | {
199 | xAxis->zoomValueOutPos(pos);
200 | }
201 | }
202 | }
203 |
204 | int XYView::searchDataIndex(int start, int end, double distinction) const
205 | {
206 | // 在[起止)范围内二分查找目标
207 | if (distinction >= seriesData.at(end - 1).x)
208 | return end - 1;
209 | if (distinction <= seriesData.at(start).x)
210 | return start;
211 | int mid;
212 | while (true)
213 | {
214 | mid = (start + end) / 2;
215 | if (mid > 0 && seriesData.at(mid - 1).x < distinction && seriesData.at(mid).x > distinction)
216 | {
217 | return ((seriesData.at(mid).x - distinction) >= (distinction - seriesData.at(mid - 1).x))
218 | ? mid - 1
219 | : mid;
220 | }
221 | if (distinction > seriesData.at(mid).x)
222 | {
223 | start = mid;
224 | }
225 | else if (distinction < seriesData.at(mid).x)
226 | {
227 | end = mid;
228 | }
229 | else
230 | {
231 | return mid;
232 | }
233 | }
234 | }
235 |
236 | void XYView::refresh()
237 | {
238 | update();
239 | }
240 |
--------------------------------------------------------------------------------
/src/tabchart/XYView.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include "XYAxis.h"
4 |
5 | // 笛卡尔坐标系(直角坐标系)图表的绘制
6 | class XYView : public QWidget
7 | {
8 | Q_OBJECT
9 | public:
10 | explicit XYView(QWidget *parent = nullptr);
11 |
12 | protected:
13 | // 绘制
14 | void paintEvent(QPaintEvent *event) override;
15 | // 组件尺寸变化后重新计算坐标轴
16 | void resizeEvent(QResizeEvent *event) override;
17 | // 鼠标操作
18 | void mousePressEvent(QMouseEvent *event) override;
19 | void mouseMoveEvent(QMouseEvent *event) override;
20 | void mouseReleaseEvent(QMouseEvent *event) override;
21 | // 离开组件区域时清除状态
22 | void leaveEvent(QEvent *event) override;
23 | // 滚轮滚动放大缩小
24 | void wheelEvent(QWheelEvent *event) override;
25 |
26 | private:
27 | // 查找对应值下标,在[起止)范围内二分查找目标
28 | int searchDataIndex(int start, int end, double distinction) const;
29 |
30 | public slots:
31 | void refresh();
32 |
33 | private:
34 | // 坐标轴
35 | XYAxis *xAxis;
36 | XYAxis *yAxis;
37 | // 绘图区域,坐标轴在其边上
38 | QRect contentArea;
39 | // 去掉坐标轴的图表区域
40 | QRect plotArea;
41 | // 鼠标位置
42 | QPoint mousePos; // 当前位置
43 | QPoint prevPos; // 上次位置
44 | // 鼠标按下
45 | bool pressFlag{false};
46 |
47 | // 曲线 xy 值
48 | struct Node
49 | {
50 | int x;
51 | int y;
52 | };
53 | QVector seriesData;
54 | };
55 |
--------------------------------------------------------------------------------
/src/tabchart/tabchart.pri:
--------------------------------------------------------------------------------
1 | FORMS += \
2 | $$PWD/TabChart.ui
3 |
4 | HEADERS += \
5 | $$PWD/PieView.h \
6 | $$PWD/TabChart.h \
7 | $$PWD/XYAxis.h \
8 | $$PWD/XYView.h
9 |
10 | SOURCES += \
11 | $$PWD/PieView.cpp \
12 | $$PWD/TabChart.cpp \
13 | $$PWD/XYAxis.cpp \
14 | $$PWD/XYView.cpp
15 |
--------------------------------------------------------------------------------
/src/tabdraw/CalcDegree.cpp:
--------------------------------------------------------------------------------
1 | #include "CalcDegree.h"
2 | #include "GlobalDef.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | CalcDegree::CalcDegree(QWidget *parent)
12 | : QWidget(parent)
13 | {
14 | setMouseTracking(true);
15 |
16 | // 定时旋转
17 | connect(&timer, &QTimer::timeout, this, [this]() {
18 | theRotate += 2;
19 | theRotate %= 360;
20 | update();
21 | });
22 | }
23 |
24 | void CalcDegree::showEvent(QShowEvent *event)
25 | {
26 | timer.start(100);
27 | QWidget::showEvent(event);
28 | }
29 |
30 | void CalcDegree::hideEvent(QHideEvent *event)
31 | {
32 | timer.stop();
33 | QWidget::hideEvent(event);
34 | }
35 |
36 | void CalcDegree::paintEvent(QPaintEvent *event)
37 | {
38 | event->accept();
39 | QPainter painter(this);
40 | // 先画一个白底黑框
41 | painter.fillRect(this->rect(), Qt::white);
42 | QPen pen(Qt::black);
43 | painter.setPen(pen);
44 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); // 右下角会超出范围
45 |
46 | // 抗锯齿
47 | painter.setRenderHint(QPainter::Antialiasing, true);
48 | painter.setRenderHint(QPainter::TextAntialiasing, true);
49 | painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
50 | // 移动坐标中心点到窗口中心,默认左上角为起点,往右下为正方向
51 | painter.translate(width() / 2, height() / 2);
52 |
53 | // 画圆盘
54 | drawCircle(&painter);
55 | // 画小球轨迹
56 | drawItemPath(&painter);
57 | // 画夹角
58 | drawPosDegree(&painter);
59 | }
60 |
61 | void CalcDegree::mouseMoveEvent(QMouseEvent *event)
62 | {
63 | event->accept();
64 | // 以中心点来计算
65 | mousePos = event->pos() - rect().center();
66 | update();
67 | }
68 |
69 | void CalcDegree::drawCircle(QPainter *painter)
70 | {
71 | painter->save();
72 |
73 | QFont font;
74 | font.setFamily("SimSun");
75 | font.setPixelSize(16);
76 | font.setBold(true);
77 | painter->setFont(font);
78 | painter->setPen(QPen(QColor(0, 0, 0), 2));
79 | painter->setBrush(QColor(10, 10, 10));
80 |
81 | const int radius = 220; // 最小半径
82 | painter->drawEllipse(QPoint(0, 0), radius, radius);
83 | const int step = 6; // 旋转步进
84 | const int text_height = GetTextHeight(painter->fontMetrics());
85 | // 可以看到旋转时文字会有抖动
86 | for (int i = 0; i < 360; i += step)
87 | {
88 | // 旋转后坐标计算公式
89 | // x'=x*cos(a)-y*sin(a)
90 | // y'=x*sin(a)+y*cos(a)
91 | // 角度转为弧度计算
92 | const double radians = qDegreesToRadians((double)(i));
93 | // 以顶部为起点顺时针的话 (x:0,y:radius) 带入公式
94 | const double x1 = -sin(radians) * (radius + 5);
95 | const double y1 = cos(radians) * (radius + 5);
96 | if (i % 30 == 0)
97 | {
98 | const double x2 = -sin(radians) * (radius + 15);
99 | const double y2 = cos(radians) * (radius + 15);
100 | // 默认屏幕坐标系上负下正
101 | painter->drawLine(-x1, -y1, -x2, -y2);
102 |
103 | // 文本的中心点也要计算
104 | const double x3 = -sin(radians) * (radius + 30);
105 | const double y3 = cos(radians) * (radius + 30);
106 | const QString text = QString::number(i);
107 | const int text_width = GetTextWidth(painter->fontMetrics(), text);
108 | // 文字的起点在左下角
109 | // 上减下加,左减右加,这样相当于往 x2 y3 左下角移动的,使文本中心点在计算的位置
110 | painter->drawText(-x3 - text_width / 2,
111 | -y3 + text_height / 2,
112 | text);
113 | }
114 | else
115 | {
116 | const double x2 = -sin(radians) * (radius + 10);
117 | const double y2 = cos(radians) * (radius + 10);
118 | // 默认屏幕坐标系上负下正
119 | painter->drawLine(-x1, -y1, -x2, -y2);
120 | }
121 | }
122 |
123 | painter->restore();
124 | }
125 |
126 | void CalcDegree::drawItemPath(QPainter *painter)
127 | {
128 | painter->save();
129 |
130 | // 红色虚线
131 | painter->setPen(QPen(QColor(200, 0, 0), 2, Qt::DotLine));
132 | const int a = 200; // 长轴
133 | const int b = 100; // 短轴
134 | const int rotation = 75; // 椭圆角度
135 | painter->rotate(-rotation); // 旋转后绘制椭圆
136 | painter->drawEllipse(QPoint(0, 0), a, b);
137 | painter->rotate(rotation);
138 |
139 | // 画物体小球
140 | painter->setPen(QPen(QColor(220, 0, 60), 2));
141 | painter->setBrush(QColor(220, 0, 60, 100));
142 | // 这里先计算了标准椭圆下位置,再用旋转矩阵进行计算偏移后的
143 | // 是按照角度旋转来的,如果需要根据轨迹匀速运动,需要计算,这里暂不需要
144 | // 椭圆上点公式,A 横长半轴,B 竖短半轴
145 | // x'=x0+A*B*cos(a)/sqrt(pow(A*sin(a),2)+pow(B*cos(a),2))
146 | // y'=y0+A*B*sin(a)/sqrt(pow(A*sin(a),2)+pow(B*cos(a),2))
147 | // 前面我们学过圆上一点坐标
148 | // x'=x0+R*cos(a)
149 | // y'=y0+R*sin(a)
150 | // 椭圆也可以化为相应的表达,不过半径需要计算
151 | // R=A*B/sqrt(pow(A*sin(a),2)+pow(B*cos(a),2))
152 | const double radians1 = qDegreesToRadians((double)(theRotate));
153 | const double r1 = a * b / sqrt(pow(a * sin(radians1), 2) + pow(b * cos(radians1), 2));
154 | const double x1 = r1 * cos(radians1);
155 | const double y1 = r1 * sin(radians1);
156 | // 旋转矩阵公式,旋转后坐标计算
157 | // x'=x*cos(a)-y*sin(a)
158 | // y'=x*sin(a)+y*cos(a)
159 | const double radians2 = qDegreesToRadians(-(double)rotation); // 注意这里取反了
160 | const double x2 = x1 * cos(radians2) - y1 * sin(radians2);
161 | const double y2 = x1 * sin(radians2) + y1 * cos(radians2);
162 | painter->drawLine(0, 0, x2, y2);
163 | painter->drawEllipse(QPointF(0, 0), 8, 8);
164 | painter->drawEllipse(QPointF(x2, y2), 10, 10);
165 |
166 | // 画夹角
167 | painter->drawLine(0, 0, 0, -220);
168 | // 画圆弧角度要乘上 16
169 | // 注意椭圆角度为逆时针
170 | painter->drawArc(QRect(-50, -50, 100, 100), 90 * 16, -(theRotate + (90 - rotation)) % 360 * 16);
171 |
172 | painter->restore();
173 | }
174 |
175 | void CalcDegree::drawPosDegree(QPainter *painter)
176 | {
177 | // 判断点是否在圆范围内
178 | // if(pow(mousePos.x(),2)+pow(mousePos.y(),2)>pow(250,2))
179 | // hypot 计算算术平方根
180 | if (std::hypot(mousePos.x(), mousePos.y()) > 250)
181 | return;
182 |
183 | painter->save();
184 |
185 | painter->setPen(QPen(QColor(255, 215, 0), 2));
186 | // painter->drawLine(QPoint(0,0),mousePos);
187 |
188 | // 从直角坐标转极坐标
189 | // r = 根号 (y^2+x^2)
190 | // 角度 = atan2(y,x) --atan2 式已将象限纳入考量的反正切函数
191 | // 或 atan 分段函数求角度
192 | // 角度是以右侧为 0 点,顺时针半圆为正,逆时针半圆为负
193 | // const double arc_tan=qAtan2(pos.y()-center_pos.y(),pos.x()-center_pos.x());
194 | const double arc_tan = qAtan2(mousePos.y(), mousePos.x());
195 | const double x1 = cos(arc_tan) * 220;
196 | const double y1 = sin(arc_tan) * 220;
197 | painter->drawLine(0, 0, x1, y1);
198 |
199 | // atan2 结果是以右侧为 0 点,顺时针半圆为正,逆时针半圆为负,结果单位是弧度
200 | // 此处需要转换为值正北为 0 点,顺时针增长,单位转为角度
201 | // 注意这个顺时针是因为 y 在屏幕坐标系反得
202 | double arc_degree = arc_tan * 180 / M_PI;
203 | if (arc_degree < 0)
204 | {
205 | arc_degree += 360;
206 | }
207 | arc_degree -= 270;
208 | if (arc_degree < 0)
209 | {
210 | arc_degree += 360;
211 | }
212 | // 画圆弧角度要乘上 16
213 | painter->drawArc(QRect(-60, -60, 120, 120), 90 * 16, -(arc_degree)*16);
214 | QFont font;
215 | font.setFamily("SimSun");
216 | font.setPixelSize(20);
217 | font.setBold(true);
218 | painter->setFont(font);
219 | painter->drawText(100, 100, QString::number(arc_degree, 'f', 2) + " °");
220 |
221 | painter->restore();
222 | }
223 |
--------------------------------------------------------------------------------
/src/tabdraw/CalcDegree.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | // calculate degree
6 | // 绘制圆盘,根据坐标位置计算角度
7 | class CalcDegree : public QWidget
8 | {
9 | Q_OBJECT
10 | public:
11 | explicit CalcDegree(QWidget *parent = nullptr);
12 |
13 | protected:
14 | // 显示时才启动定时动画
15 | void showEvent(QShowEvent *event) override;
16 | void hideEvent(QHideEvent *event) override;
17 | // 绘制
18 | void paintEvent(QPaintEvent *event) override;
19 | void mouseMoveEvent(QMouseEvent *event) override;
20 |
21 | private:
22 | // 画一个圆盘
23 | void drawCircle(QPainter *painter);
24 | // 绘制一个椭圆轨迹的 Item
25 | void drawItemPath(QPainter *painter);
26 | // 绘制鼠标点到竖直正方向夹角
27 | void drawPosDegree(QPainter *painter);
28 |
29 | private:
30 | // 定时动画
31 | QTimer timer;
32 | // 旋转的角度 [0-360]
33 | int theRotate{0};
34 | // 鼠标位置
35 | QPoint mousePos{-1000, -1000};
36 | };
37 |
--------------------------------------------------------------------------------
/src/tabdraw/CalcPos.cpp:
--------------------------------------------------------------------------------
1 | #include "CalcPos.h"
2 | #include "GlobalDef.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | //#include
9 |
10 | CalcPos::CalcPos(QWidget *parent)
11 | : QWidget(parent)
12 | {
13 | initImg_1();
14 | initImg_2();
15 | initImg_3();
16 |
17 | // 定时旋转
18 | // 减小刷新间隔和步进角度可以让旋转更自然,但是更耗费 cpu
19 | // 可以注释掉Timer来练习绘制静止状态下的圆和线
20 | connect(&timer, &QTimer::timeout, this, [this]() {
21 | theRotate += 1;
22 | theRotate %= 360;
23 | update();
24 | });
25 | }
26 |
27 | void CalcPos::showEvent(QShowEvent *event)
28 | {
29 | timer.start(100);
30 | QWidget::showEvent(event);
31 | }
32 |
33 | void CalcPos::hideEvent(QHideEvent *event)
34 | {
35 | timer.stop();
36 | QWidget::hideEvent(event);
37 | }
38 |
39 | void CalcPos::paintEvent(QPaintEvent *event)
40 | {
41 | Q_UNUSED(event)
42 | QPainter painter(this);
43 | // 先画一个白底黑框
44 | painter.fillRect(this->rect(), Qt::white);
45 | QPen pen(Qt::black);
46 | painter.setPen(pen);
47 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); // 右下角会超出范围
48 |
49 | // 抗锯齿
50 | painter.setRenderHint(QPainter::Antialiasing, true);
51 | painter.setRenderHint(QPainter::TextAntialiasing, true);
52 | painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
53 | // 移动坐标中心点到窗口中心,默认左上角为起点,往右下为正方向
54 | painter.translate(width() / 2, height() / 2);
55 | // 由内而外顺序计数,绘制时先画外面大的
56 | // 第5层
57 | {
58 | painter.save();
59 | // 自己计算,不用 rotate,这样文字也是正的
60 | // painter.rotate(theRotate);
61 | draw_5(&painter, theRotate);
62 | painter.restore();
63 | }
64 | // 第4层
65 | {
66 | painter.save();
67 | painter.rotate(-theRotate);
68 | draw_4(&painter);
69 | painter.restore();
70 | }
71 | // 第3层
72 | {
73 | painter.save();
74 | painter.rotate(theRotate);
75 | painter.drawImage(-theImg_3.width() / 2,
76 | -theImg_3.height() / 2,
77 | theImg_3);
78 | painter.restore();
79 | }
80 | // 第2层
81 | {
82 | painter.save();
83 | painter.rotate(-theRotate);
84 | painter.drawImage(-theImg_2.width() / 2,
85 | -theImg_2.height() / 2,
86 | theImg_2);
87 | painter.restore();
88 | }
89 | // 第1层
90 | {
91 | painter.save();
92 | painter.rotate(theRotate);
93 | painter.drawImage(-theImg_1.width() / 2,
94 | -theImg_1.height() / 2,
95 | theImg_1);
96 | painter.restore();
97 | }
98 | }
99 |
100 | void CalcPos::initImg_1()
101 | {
102 | theImg_1 = QImage(200, 200, QImage::Format_ARGB32);
103 | theImg_1.fill(Qt::transparent);
104 |
105 | // 画一个顺时针的圆盘
106 | QPainter painter(&theImg_1);
107 | painter.setRenderHint(QPainter::Antialiasing, true);
108 | painter.setRenderHint(QPainter::TextAntialiasing, true);
109 | painter.translate(theImg_1.width() / 2, theImg_1.height() / 2); // 移动到中心点
110 | QFont font;
111 | font.setFamily("SimSun");
112 | font.setPixelSize(16);
113 | font.setBold(true);
114 | painter.setFont(font);
115 |
116 | const int radius = 60; // 最小半径
117 | QPainterPath path1;
118 | path1.addEllipse(QPoint(0, 0), radius, radius);
119 | QPainterPath path2;
120 | const int text_height = GetTextHeight(painter.fontMetrics());
121 | path2.addEllipse(QPoint(0, 0), radius + text_height + 10, radius + text_height + 10);
122 | painter.fillPath(path2 - path1, QBrush(Qt::black));
123 | painter.fillPath(path1, QBrush(Qt::white));
124 | QList str_list{
125 | "乾", "坎", "艮", "震", "巽", "离", "坤", "兑"};
126 | QList str_list2{
127 | "☰", "☵", "☶", "☳", "☴", "☲", "☷", "☱"};
128 | const int step = 360 / str_list.count(); // 旋转步进
129 | for (int i = 0; i < str_list.count(); i++)
130 | {
131 | // 默认屏幕坐标系上负下正
132 | painter.setPen(QPen(Qt::black, 2));
133 | painter.drawLine(0, 0, 0, -radius / (i % 2 == 0 ? 2.0 : 3.0));
134 | // 画内圈字符
135 | const QString text2 = str_list2.at(i);
136 | painter.drawText(-GetTextWidth(painter.fontMetrics(), text2) / 2,
137 | (radius - 25 + text_height),
138 | text2);
139 |
140 | painter.setPen(QPen(Qt::white));
141 | // 画外圈文字
142 | const QString text = str_list.at(i);
143 | // 文字的起点在左下角
144 | painter.drawText(-GetTextWidth(painter.fontMetrics(), text) / 2,
145 | (radius + text_height),
146 | text);
147 |
148 | painter.rotate(step); // 每次叠加旋转 30 度
149 | }
150 | }
151 |
152 | void CalcPos::initImg_2()
153 | {
154 | theImg_2 = QImage(300, 300, QImage::Format_ARGB32);
155 | theImg_2.fill(Qt::transparent);
156 |
157 | // 画一个顺时针的圆盘
158 | QPainter painter(&theImg_2);
159 | painter.setRenderHint(QPainter::Antialiasing, true);
160 | painter.setRenderHint(QPainter::TextAntialiasing, true);
161 | painter.translate(theImg_2.width() / 2, theImg_2.height() / 2); // 移动到中心点
162 | QFont font;
163 | font.setFamily("SimSun");
164 | font.setPixelSize(16);
165 | font.setBold(true);
166 | painter.setFont(font);
167 |
168 | const int radius = 90; // 最小半径
169 | QPainterPath path1;
170 | path1.addEllipse(QPoint(0, 0), radius, radius);
171 | QPainterPath path2;
172 | const int text_height = GetTextHeight(painter.fontMetrics());
173 | path2.addEllipse(QPoint(0, 0), radius + text_height + 10, radius + text_height + 10);
174 | painter.fillPath(path2 - path1, QBrush(Qt::black));
175 | painter.setPen(QPen(Qt::white));
176 | QList str_list{
177 | "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"};
178 | const int angle = 360 / str_list.count();
179 | for (int i = 0; i < str_list.count(); i++)
180 | {
181 | // 默认屏幕坐标系上负下正,这里从原点下方开始绘制,这样文字的角度就反过来了
182 | const QString text = str_list.at(i);
183 | // 文字的起点在左下角
184 | painter.drawText(-GetTextWidth(painter.fontMetrics(), text) / 2,
185 | (radius + text_height),
186 | text);
187 | painter.rotate(angle);
188 | }
189 | }
190 |
191 | void CalcPos::initImg_3()
192 | {
193 | theImg_3 = QImage(300, 300, QImage::Format_ARGB32);
194 | theImg_3.fill(Qt::transparent);
195 |
196 | // 画一个顺时针的圆盘
197 | QPainter painter(&theImg_3);
198 | painter.setRenderHint(QPainter::Antialiasing, true);
199 | painter.setRenderHint(QPainter::TextAntialiasing, true);
200 | painter.translate(theImg_3.width() / 2, theImg_3.height() / 2); // 移动到中心点
201 | QFont font;
202 | font.setFamily("SimSun");
203 | font.setPixelSize(16);
204 | font.setBold(true);
205 | painter.setFont(font);
206 |
207 | const int radius = 120; // 最小半径
208 | QPainterPath path1;
209 | path1.addEllipse(QPoint(0, 0), radius, radius);
210 | QPainterPath path2;
211 | const int text_height = GetTextHeight(painter.fontMetrics());
212 | path2.addEllipse(QPoint(0, 0), radius + text_height + 10, radius + text_height + 10);
213 | painter.fillPath(path2 - path1, QBrush(Qt::black));
214 | painter.setPen(QPen(Qt::white));
215 | QList str_list{
216 | "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"};
217 | const int angle = 360 / str_list.count();
218 | for (int i = 0; i < str_list.count(); i++)
219 | {
220 | // 默认屏幕坐标系上负下正,这里从原点下方开始绘制,这样文字的角度就反过来了
221 | const QString text = str_list.at(i);
222 | // 文字的起点在左下角
223 | painter.drawText(-GetTextWidth(painter.fontMetrics(), text) / 2,
224 | (radius + text_height),
225 | text);
226 | painter.rotate(angle);
227 | }
228 | }
229 |
230 | void CalcPos::draw_4(QPainter *painter)
231 | {
232 | // 画一个逆时针的表盘
233 | QFont font;
234 | font.setFamily("SimSun");
235 | font.setPixelSize(16);
236 | font.setBold(true);
237 | painter->setFont(font);
238 | painter->setPen(QPen(Qt::black, 2));
239 |
240 | const int radius = 150; // 最小半径
241 | const int step = 6; // 旋转步进
242 | painter->drawEllipse(QPoint(0, 0), radius, radius);
243 | // 可以看到旋转时文字会有抖动
244 | for (int i = 0; i < 360; i += step)
245 | {
246 | if (i % 30 == 0)
247 | {
248 | // 默认屏幕坐标系上负下正,这里 y 取反过来
249 | painter->drawLine(0, -radius, 0, -(radius + 15));
250 | const QString text = QString::number(i);
251 | // 文字的起点在左下角
252 | painter->drawText(-GetTextWidth(painter->fontMetrics(), text) / 2,
253 | -(radius + 20),
254 | text);
255 | }
256 | else
257 | {
258 | painter->drawLine(0, -radius, 0, -(radius + 5));
259 | }
260 | painter->rotate(-step); // 逆时针旋转
261 | }
262 | }
263 |
264 | void CalcPos::draw_5(QPainter *painter, int rotate)
265 | {
266 | // 画一个顺时针的表盘
267 | // 用 qpainter 的 rotate,文字也是旋转的,可以想文字是固定方向
268 | // 要么绘制文字时反方向转一下,要么直接算坐标,不用rotate
269 | QFont font;
270 | font.setFamily("SimSun");
271 | font.setPixelSize(16);
272 | font.setBold(true);
273 | painter->setFont(font);
274 | painter->setPen(QPen(Qt::black, 2));
275 |
276 | const int radius = 200; // 最小半径
277 | const int step = 6; // 旋转步进
278 | const int text_height = GetTextHeight(painter->fontMetrics());
279 | painter->drawEllipse(QPoint(0, 0), radius, radius);
280 | // 可以看到旋转时文字会有抖动
281 | for (int i = 0; i <= 180; i += step)
282 | {
283 | // 旋转后坐标计算公式
284 | // x'=x*cos(a)-y*sin(a)
285 | // y'=x*sin(a)+y*cos(a)
286 | // 角度转为弧度计算
287 | const double radians = qDegreesToRadians((double)(i + rotate));
288 | // 以左侧为起点顺时针的话 (x:-radius,y:0) 带入公式
289 | // 即圆上一点坐标
290 | // x'=x0+R*cos(a)
291 | // y'=y0+R*sin(a)
292 | // 本来算出来是逆时针,不过因为屏幕坐标系 y 轴反的,就成了顺时针
293 | // x 或 y 取反都可以让表盘的数字顺序反一下,负负得正,但是左右上下颠倒了
294 | // 本来 xy 不取反,初始是 3 点方向顺时针往 9 点方向,现在为 9 点顺时到 3 点
295 | const double x1 = cos(radians) * radius;
296 | const double y1 = sin(radians) * radius;
297 | if (i % 30 == 0)
298 | {
299 | const double x2 = cos(radians) * (radius + 15);
300 | const double y2 = sin(radians) * (radius + 15);
301 | // 默认屏幕坐标系上负下正
302 | painter->drawLine(-x1, -y1, -x2, -y2);
303 |
304 | // 文本的中心点也要计算
305 | const double x3 = cos(radians) * (radius + 30);
306 | const double y3 = sin(radians) * (radius + 30);
307 | const QString text = QString::number(i);
308 | const int text_width = GetTextWidth(painter->fontMetrics(), text);
309 | // Qt 文字绘制的起点在左下角,所以得到文本中心后,往左下偏移宽高的一半
310 | // 上减下加,左减右加,这样相当于往 x2 y3 左下角移动的,使文本中心点在计算的位置
311 | painter->drawText(-x3 - text_width / 2,
312 | -y3 + text_height / 2,
313 | text);
314 | }
315 | else
316 | {
317 | const double x2 = cos(radians) * (radius + 5);
318 | const double y2 = sin(radians) * (radius + 5);
319 | // 默认屏幕坐标系上负下正
320 | painter->drawLine(-x1, -y1, -x2, -y2);
321 | }
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/src/tabdraw/CalcPos.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 | #include
5 |
6 | // 绘制圆盘,根据角度计算坐标位置
7 | class CalcPos : public QWidget
8 | {
9 | Q_OBJECT
10 | public:
11 | explicit CalcPos(QWidget *parent = nullptr);
12 |
13 | protected:
14 | // 显示时才启动定时动画
15 | void showEvent(QShowEvent *event) override;
16 | void hideEvent(QHideEvent *event) override;
17 | // 绘制
18 | void paintEvent(QPaintEvent *event) override;
19 |
20 | private:
21 | void initImg_1();
22 | void initImg_2();
23 | void initImg_3();
24 | void draw_4(QPainter *painter);
25 | void draw_5(QPainter *painter, int rotate);
26 |
27 | private:
28 | // 定时动画
29 | QTimer timer;
30 | // 旋转的角度 [0-360]
31 | int theRotate{0};
32 | // 第一个图,为了旋转时文字不抖动,先绘制到画布
33 | // 不过旋转时文字的抗锯齿效果很差
34 | QImage theImg_1;
35 | // 图2 天干
36 | QImage theImg_2;
37 | // 图3 地支
38 | QImage theImg_3;
39 | };
40 |
--------------------------------------------------------------------------------
/src/tabdraw/DrawAnimation.cpp:
--------------------------------------------------------------------------------
1 | #include "DrawAnimation.h"
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | DrawAnimation::DrawAnimation(QWidget *parent)
11 | : QWidget(parent)
12 | {
13 | // 1-1. 波纹
14 | connect(&waveTimer, &QTimer::timeout, this, [this] {
15 | waveOffset += 4;
16 | waveOffset %= waveLimit;
17 | update();
18 | });
19 | // 2-1. 色块
20 | // 先创建一个并行属性动画组
21 | blockAnimation = new QParallelAnimationGroup(this);
22 | // 添加色块颜色动画
23 | QPropertyAnimation *block_color = new QPropertyAnimation(this);
24 | block_color->setTargetObject(this);
25 | block_color->setPropertyName("blockColor");
26 | block_color->setStartValue(blockColor);
27 | block_color->setEndValue(QColor(255, 255, 0));
28 | block_color->setDuration(3000);
29 | blockAnimation->addAnimation(block_color);
30 | // 添加色块缩放动画
31 | QPropertyAnimation *block_scare = new QPropertyAnimation(this);
32 | block_scare->setTargetObject(this);
33 | block_scare->setPropertyName("blockScare");
34 | block_scare->setStartValue(blockScare);
35 | block_scare->setEndValue(0.2f);
36 | block_scare->setEasingCurve(QEasingCurve::InCubic); // 缓和曲线设置
37 | block_scare->setDuration(2000);
38 | blockAnimation->addAnimation(block_scare);
39 | // 动画组播放完了就倒着继续播放
40 | connect(blockAnimation, &QParallelAnimationGroup::finished, this, [this] {
41 | if (blockAnimation->direction() == QAbstractAnimation::Forward)
42 | {
43 | blockAnimation->setDirection(QAbstractAnimation::Backward);
44 | }
45 | else
46 | {
47 | blockAnimation->setDirection(QAbstractAnimation::Forward);
48 | }
49 | blockAnimation->start();
50 | });
51 | // 3-1. 圆角
52 | roundTimeline = new QTimeLine(5000, this);
53 | roundTimeline->setFrameRange(0, 100);
54 | connect(roundTimeline, &QTimeLine::frameChanged, this, [this](int frame) {
55 | roundRaiuds = frame;
56 | update();
57 | });
58 | connect(roundTimeline, &QTimeLine::finished, this, [this] {
59 | if (roundTimeline->direction() == QTimeLine::Forward)
60 | {
61 | roundTimeline->setDirection(QTimeLine::Backward);
62 | }
63 | else
64 | {
65 | roundTimeline->setDirection(QTimeLine::Forward);
66 | }
67 | roundTimeline->start();
68 | });
69 | }
70 |
71 | QColor DrawAnimation::getBlockColor() const
72 | {
73 | return blockColor;
74 | }
75 |
76 | void DrawAnimation::setBlockColor(const QColor &color)
77 | {
78 | blockColor = color;
79 | emit blockColorChanged();
80 | update();
81 | }
82 |
83 | float DrawAnimation::getBlockScare() const
84 | {
85 | return blockScare;
86 | }
87 |
88 | void DrawAnimation::setBlockScare(float scare)
89 | {
90 | blockScare = scare;
91 | emit blockScareChanged();
92 | update();
93 | }
94 |
95 | void DrawAnimation::showEvent(QShowEvent *event)
96 | {
97 | waveTimer.start(50);
98 | blockAnimation->start();
99 | roundTimeline->start();
100 | QWidget::showEvent(event);
101 | }
102 |
103 | void DrawAnimation::hideEvent(QHideEvent *event)
104 | {
105 | waveTimer.stop();
106 | // Qt6 提示 QAbstractAnimation::pause: Cannot pause a stopped animation
107 | if (blockAnimation->state() == QAbstractAnimation::Running)
108 | {
109 | blockAnimation->pause();
110 | }
111 | // Qt6 提示 QTimeLine::setPaused: Not running
112 | if (roundTimeline->state() == QTimeLine::Running)
113 | {
114 | roundTimeline->setPaused(true);
115 | }
116 | QWidget::hideEvent(event);
117 | }
118 |
119 | void DrawAnimation::paintEvent(QPaintEvent *event)
120 | {
121 | event->accept();
122 | QPainter painter(this);
123 | // 黑底白字
124 | painter.fillRect(this->rect(), Qt::black);
125 | // 1-1. 波纹
126 | drawWave(painter, QRect(20, 20, 200, 200));
127 | // 2-1. 色块
128 | drawColorBlock(painter, QRect(240, 20, 200, 200));
129 | // 3-1. 圆角
130 | drawRadiusBlock(painter, QRect(460, 20, 200, 200));
131 | }
132 |
133 | void DrawAnimation::drawWave(QPainter &painter, const QRectF &area)
134 | {
135 | painter.save();
136 | // 波纹
137 | QRadialGradient gradient(area.center(), 100);
138 | // 波纹间隔,总量为 1
139 | const double f_space = 0.18;
140 | // 一层波纹的内侧偏移,相对于 f_offset
141 | const double f_inner = 0.02;
142 | // 波峰偏移,相对于 f_offset
143 | const double f_crest = 0.03;
144 | // 波纹的外侧偏移,相对于 f_offset
145 | const double f_outer = 0.06;
146 | // 当前波纹偏移
147 | const double f_offset = waveOffset / double(waveLimit) * f_space;
148 | // 波纹个数
149 | const int f_count = std::floor(1 / f_space);
150 | // 波峰颜色
151 | QColor wave_color(255, 0, 0);
152 | gradient.setColorAt(0, Qt::transparent);
153 | for (int i = 0; i < f_count; i++)
154 | {
155 | if (f_offset + i * f_space > f_inner)
156 | {
157 | gradient.setColorAt(f_offset + i * f_space - f_inner, Qt::transparent);
158 | }
159 | // 最后一个波纹透明度逐渐为 0
160 | if (i == f_count - 1)
161 | {
162 | QColor temp_color = wave_color;
163 | temp_color.setAlpha((1.0 - f_offset / f_space) * 250);
164 | gradient.setColorAt(f_offset + i * f_space + f_crest, temp_color);
165 | }
166 | else
167 | {
168 | gradient.setColorAt(f_offset + i * f_space + f_crest, wave_color);
169 | }
170 | gradient.setColorAt(f_offset + i * f_space + f_outer, Qt::transparent);
171 | }
172 | // 波纹抗锯齿
173 | painter.setRenderHint(QPainter::Antialiasing, true);
174 | painter.fillRect(area, gradient);
175 | // 边框,画直线不用抗锯齿
176 | painter.setRenderHint(QPainter::Antialiasing, false);
177 | painter.setPen(Qt::white);
178 | painter.drawRect(area);
179 | painter.restore();
180 | }
181 |
182 | void DrawAnimation::drawColorBlock(QPainter &painter, const QRectF &area)
183 | {
184 | painter.save();
185 | // 根据当前属性颜色和缩放比例绘制色块
186 | QRectF block = area.adjusted(10, 10, -10, -10);
187 | block.setWidth(block.width() * getBlockScare());
188 | block.setHeight(block.height() * getBlockScare());
189 | block.moveCenter(area.center());
190 | painter.setRenderHint(QPainter::Antialiasing, true);
191 | painter.fillRect(block, getBlockColor());
192 | // 边框
193 | painter.setRenderHint(QPainter::Antialiasing, false);
194 | painter.setPen(Qt::white);
195 | painter.drawRect(area);
196 | painter.restore();
197 | }
198 |
199 | void DrawAnimation::drawRadiusBlock(QPainter &painter, const QRectF &area)
200 | {
201 | painter.save();
202 | // 根据当前属性颜色和缩放比例绘制色块
203 | QRectF block = area.adjusted(10, 10, -10, -10);
204 | painter.setRenderHint(QPainter::Antialiasing, true);
205 | painter.setPen(Qt::NoPen);
206 | painter.setBrush(Qt::cyan);
207 | QPainterPath round_path;
208 | round_path.addRoundedRect(block, roundRaiuds, roundRaiuds);
209 | painter.drawPath(round_path);
210 | // 边框
211 | painter.setRenderHint(QPainter::Antialiasing, false);
212 | painter.setPen(Qt::white);
213 | painter.setBrush(Qt::NoBrush);
214 | painter.drawRect(area);
215 | painter.restore();
216 | }
217 |
--------------------------------------------------------------------------------
/src/tabdraw/DrawAnimation.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | // 动画效果
11 | class DrawAnimation : public QWidget
12 | {
13 | Q_OBJECT
14 | Q_PROPERTY(QColor blockColor READ getBlockColor WRITE setBlockColor NOTIFY blockColorChanged)
15 | Q_PROPERTY(float blockScare READ getBlockScare WRITE setBlockScare NOTIFY blockScareChanged)
16 | public:
17 | explicit DrawAnimation(QWidget *parent = nullptr);
18 |
19 | QColor getBlockColor() const;
20 | void setBlockColor(const QColor &color);
21 |
22 | float getBlockScare() const;
23 | void setBlockScare(float scare);
24 |
25 | protected:
26 | // 显示时才启动定时动画
27 | void showEvent(QShowEvent *event) override;
28 | void hideEvent(QHideEvent *event) override;
29 | // 绘制
30 | void paintEvent(QPaintEvent *event) override;
31 |
32 | private:
33 | // 定时器波纹
34 | void drawWave(QPainter &painter, const QRectF &area);
35 | // 属性动画色块
36 | void drawColorBlock(QPainter &painter, const QRectF &area);
37 | // 时间轴圆角
38 | void drawRadiusBlock(QPainter &painter, const QRectF &area);
39 |
40 | signals:
41 | void blockColorChanged();
42 | void blockScareChanged();
43 |
44 | private:
45 | // 1. 定时器动画
46 | // 1-1. 波纹
47 | QTimer waveTimer;
48 | int waveOffset{0};
49 | const int waveLimit{100};
50 | // 2. 属性动画
51 | // 2-1. 色块
52 | // 一个并行执行的动画组,同时改变颜色和大小
53 | QParallelAnimationGroup *blockAnimation{nullptr};
54 | QColor blockColor{255, 0, 0};
55 | float blockScare{1.0};
56 | // 3. 时间轴
57 | // 3-1. 圆角
58 | QTimeLine *roundTimeline{nullptr};
59 | int roundRaiuds{0};
60 | };
61 |
--------------------------------------------------------------------------------
/src/tabdraw/DrawBezier.cpp:
--------------------------------------------------------------------------------
1 | #include "DrawBezier.h"
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | DrawBezier::DrawBezier(QWidget *parent)
9 | : QWidget(parent)
10 | {
11 | // 定时更新
12 | connect(&timer, &QTimer::timeout, this, [this]() {
13 | theProgress += 1;
14 | theProgress %= 100;
15 | update();
16 | });
17 | }
18 |
19 | void DrawBezier::showEvent(QShowEvent *event)
20 | {
21 | timer.start(50);
22 | QWidget::showEvent(event);
23 | }
24 |
25 | void DrawBezier::hideEvent(QHideEvent *event)
26 | {
27 | timer.stop();
28 | QWidget::hideEvent(event);
29 | }
30 |
31 | void DrawBezier::paintEvent(QPaintEvent *event)
32 | {
33 | Q_UNUSED(event)
34 | // 自适应半径
35 | QRect area = rect();
36 | int area_r = 200;
37 | int point_r = 4;
38 | // 绘制进度
39 | double progress = theProgress / 100.0;
40 | // 四个控制点绘制三阶贝塞尔
41 | QPoint p0(-area_r, area_r / 2);
42 | QPoint p1(-area_r, -area_r / 2);
43 | QPoint p2(area_r / 2, -area_r / 2);
44 | QPoint p3(area_r, area_r / 2);
45 |
46 | QPainter painter(this);
47 | painter.translate(area.center());
48 | painter.setRenderHint(QPainter::Antialiasing, true);
49 | // 连接控制点
50 | painter.setPen(QPen(Qt::black, 2));
51 | painter.drawLine(p0, p1);
52 | painter.drawLine(p1, p2);
53 | painter.drawLine(p2, p3);
54 | // 贝塞尔曲线
55 | QPainterPath bezier;
56 | bezier.moveTo(p0);
57 | bezier.cubicTo(p1, p2, p3);
58 | painter.setPen(QPen(Qt::red, 2));
59 | painter.drawPath(bezier);
60 | // 控制点
61 | painter.setPen(QPen(Qt::black, 2));
62 | painter.drawEllipse(p0, point_r, point_r);
63 | painter.drawEllipse(p1, point_r, point_r);
64 | painter.drawEllipse(p2, point_r, point_r);
65 | painter.drawEllipse(p3, point_r, point_r);
66 |
67 | // 每条线按相同的进度百分比取一个点,连接这些点
68 | QPoint p01 = (p1 - p0) * progress + p0;
69 | QPoint p12 = (p2 - p1) * progress + p1;
70 | QPoint p23 = (p3 - p2) * progress + p2;
71 | painter.setPen(QPen(Qt::green, 2));
72 | painter.drawLine(p01, p12);
73 | painter.drawLine(p12, p23);
74 | painter.drawEllipse(p01, point_r, point_r);
75 | painter.drawEllipse(p12, point_r, point_r);
76 | painter.drawEllipse(p23, point_r, point_r);
77 |
78 | //
79 | QPoint p012 = (p12 - p01) * progress + p01;
80 | QPoint p123 = (p23 - p12) * progress + p12;
81 | painter.setPen(QPen(Qt::blue, 2));
82 | painter.drawLine(p012, p123);
83 | painter.drawEllipse(p012, point_r, point_r);
84 | painter.drawEllipse(p123, point_r, point_r);
85 |
86 | // 直到只剩一个点,随着进度变化,这些点的集合构成的曲线就是贝塞尔曲线
87 | QPoint p0123 = (p123 - p012) * progress + p012;
88 | painter.setPen(QPen(Qt::red, 2));
89 | painter.drawEllipse(p0123, point_r, point_r);
90 | }
91 |
--------------------------------------------------------------------------------
/src/tabdraw/DrawBezier.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | // 画贝塞尔曲线
6 | class DrawBezier : public QWidget
7 | {
8 | Q_OBJECT
9 | public:
10 | explicit DrawBezier(QWidget *parent = nullptr);
11 |
12 | protected:
13 | // 显示时才启动定时动画
14 | void showEvent(QShowEvent *event) override;
15 | void hideEvent(QHideEvent *event) override;
16 | // 绘制
17 | void paintEvent(QPaintEvent *event) override;
18 |
19 | private:
20 | // 定时动画
21 | QTimer timer;
22 | // 进度 [0-100]
23 | int theProgress{0};
24 | };
25 |
--------------------------------------------------------------------------------
/src/tabdraw/DrawCircle.cpp:
--------------------------------------------------------------------------------
1 | #include "DrawCircle.h"
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | DrawCircle::DrawCircle(QWidget *parent)
8 | : QWidget(parent)
9 | {
10 | // 定时旋转
11 | connect(&timer, &QTimer::timeout, this, [this]() {
12 | theRotate += 1;
13 | theRotate %= 360;
14 | update();
15 | });
16 | }
17 |
18 | void DrawCircle::showEvent(QShowEvent *event)
19 | {
20 | timer.start(50);
21 | QWidget::showEvent(event);
22 | }
23 |
24 | void DrawCircle::hideEvent(QHideEvent *event)
25 | {
26 | timer.stop();
27 | QWidget::hideEvent(event);
28 | }
29 |
30 | void DrawCircle::paintEvent(QPaintEvent *event)
31 | {
32 | Q_UNUSED(event)
33 | // 自适应半径
34 | QRect area = rect();
35 | int radius = area.width() > area.height() ? area.height() : area.width();
36 | radius = radius / 2 - 10;
37 | if (radius < 20) {
38 | radius = 20;
39 | }
40 |
41 | QPainter painter(this);
42 | painter.translate(area.center());
43 | painter.drawLine(0, radius, 0, -radius);
44 | painter.drawLine(radius, 0, -radius, 0);
45 | painter.setRenderHint(QPainter::Antialiasing, true);
46 | painter.drawEllipse(QPoint(0, 0), radius / 2, radius / 2);
47 |
48 | // 这里是根据圆上点坐标倒推线条坐标
49 | // 根据直角三角形中线定理,三角形斜边上的中线等于斜边的一半
50 | // 所以线条坐标为点坐标数值的两倍
51 | const double a = qDegreesToRadians((double)theRotate);
52 | const int x = radius * cos(a);
53 | const int y = radius * sin(a);
54 |
55 | // 绘制线条和端点
56 | painter.setPen(QPen(Qt::red, 2));
57 | painter.drawLine(x, 0, 0, y);
58 | painter.drawEllipse(QPoint(x, 0), 5, 5);
59 | painter.drawEllipse(QPoint(0, y), 5, 5);
60 |
61 | // 绘制圆上点
62 | painter.setPen(QPen(Qt::green, 2));
63 | painter.drawEllipse(QPoint(x / 2, y / 2), 5, 5);
64 | }
65 |
--------------------------------------------------------------------------------
/src/tabdraw/DrawCircle.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | // 画圆
6 | class DrawCircle : public QWidget
7 | {
8 | Q_OBJECT
9 | public:
10 | explicit DrawCircle(QWidget *parent = nullptr);
11 |
12 | protected:
13 | // 显示时才启动定时动画
14 | void showEvent(QShowEvent *event) override;
15 | void hideEvent(QHideEvent *event) override;
16 | // 绘制
17 | void paintEvent(QPaintEvent *event) override;
18 |
19 | private:
20 | // 定时动画
21 | QTimer timer;
22 | // 旋转的角度 [0-360]
23 | int theRotate{0};
24 | };
25 |
--------------------------------------------------------------------------------
/src/tabdraw/LedLattice.cpp:
--------------------------------------------------------------------------------
1 | #include "LedLattice.h"
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include