├── .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 | ![2021-12-26](img/demo/1.png) 21 | 22 | ![2021-12-26](img/demo/2.png) 23 | 24 | ![2021-12-26](img/demo/3.png) 25 | 26 | ![2021-12-26](img/demo/4.png) 27 | 28 | ![2021-12-26](img/demo/5.png) 29 | 30 | ![2021-12-26](img/demo/6.png) 31 | 32 | ![2021-12-26](img/demo/7.png) 33 | 34 | ![2021-12-26](img/demo/8.png) 35 | 36 | ![2021-12-26](img/demo/9.png) 37 | 38 | ![2021-12-26](img/demo/10.png) 39 | 40 | ![2023-09-12](img/demo/11.png) 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 | 48 | 49 | 50 | 0 51 | 0 52 | 800 53 | 23 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | TabBasic 62 | QWidget 63 |
TabBasic.h
64 | 1 65 |
66 | 67 | TabDraw 68 | QWidget 69 |
TabDraw.h
70 | 1 71 |
72 | 73 | Tab3D 74 | QWidget 75 |
Tab3D.h
76 | 1 77 |
78 | 79 | TabChart 80 | QWidget 81 |
TabChart.h
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 |
Cube3D.h
41 | 1 42 |
43 | 44 | Windmill3D 45 | QWidget 46 |
Windmill3D.h
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 |
PenStyle.h
64 | 1 65 |
66 | 67 | PenColor 68 | QWidget 69 |
PenColor.h
70 | 1 71 |
72 | 73 | TextPath 74 | QWidget 75 |
TextPath.h
76 | 1 77 |
78 | 79 | BasicCurve 80 | QWidget 81 |
BasicCurve.h
82 | 1 83 |
84 | 85 | BasicImage 86 | QWidget 87 |
BasicImage.h
88 | 1 89 |
90 | 91 | BasicComposition 92 | QWidget 93 |
BasicComposition.h
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 |
XYView.h
41 | 1 42 |
43 | 44 | PieView 45 | QWidget 46 |
PieView.h
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 7 | #include 8 | 9 | // 爱心的字模 16*16 逐列,灯亮为 1,左上角为低位取行列 (0,0) 10 | unsigned short led_data[] = { 11 | 0b0000000000000000, 12 | 0b0000000011100000, 13 | 0b0000000111110000, 14 | 0b0000001111111000, 15 | 0b0000011111111000, 16 | 0b0000111111111000, 17 | 0b0001111111110000, 18 | 0b0011111111100000, 19 | 0b0011111111100000, 20 | 0b0001111111110000, 21 | 0b0000111111111000, 22 | 0b0000011111111000, 23 | 0b0000001111111000, 24 | 0b0000000111110000, 25 | 0b0000000011100000, 26 | 0b0000000000000000, 27 | }; 28 | 29 | LedLattice::LedLattice(QWidget *parent) 30 | : QWidget(parent) 31 | { 32 | connect(&timer, &QTimer::timeout, this, [this] { 33 | currentCol++; 34 | if(currentCol > colCount){ 35 | currentCol = 0; 36 | // 扫描完一轮后图形移动一次 37 | colOffset = (colOffset + 1) % colCount; 38 | } 39 | update(); 40 | }); 41 | } 42 | 43 | void LedLattice::showEvent(QShowEvent *event) 44 | { 45 | timer.start(100); 46 | QWidget::showEvent(event); 47 | } 48 | 49 | void LedLattice::hideEvent(QHideEvent *event) 50 | { 51 | timer.stop(); 52 | QWidget::hideEvent(event); 53 | } 54 | 55 | void LedLattice::paintEvent(QPaintEvent *event) 56 | { 57 | event->accept(); 58 | // 线高低电平的颜色 59 | const QColor high_color = QColor(255, 0, 0); 60 | const QColor low_color = QColor(0, 0, 255); 61 | const QColor led_color = QColor(0, 255, 0); 62 | const int line_width = 4; 63 | const int line_space = 16; 64 | const int led_width = 18; 65 | const int row_count = 16; 66 | const int col_count = 16; 67 | // 绘制范围 68 | const int area_width = col_count * line_width + (col_count + 1) * line_space; 69 | const int area_height = row_count * line_width + (row_count + 1) * line_space; 70 | QRect area = QRect(0, 0, area_width, area_height); 71 | area.moveCenter(rect().center()); 72 | QPainter painter(this); 73 | painter.fillRect(rect(), Qt::black); 74 | painter.translate(area.topLeft()); 75 | // 假设行为发光二极管正极,列为负极,列扫描 76 | // 默认列高电平行低电平,当前扫描到的列给低电平则该列高电平的行发光 77 | // 这里加 offset 用于实现移动效果 78 | unsigned short current_col_data = led_data[(currentCol + colOffset) % colCount]; 79 | unsigned short current_row_index = 0x01; 80 | for (int row = 0; row < row_count; row++) 81 | { 82 | int row_offset = row * (line_width + line_space) + line_space; 83 | painter.setPen(QPen(low_color, line_width)); 84 | painter.setOpacity(0.4); 85 | painter.drawLine(QPoint(0, row_offset), QPoint(area_width, row_offset)); 86 | // 标出高电平引脚 87 | if (current_col_data & current_row_index) 88 | { 89 | painter.setPen(QPen(high_color, line_width)); 90 | painter.setOpacity(1); 91 | painter.drawPoint(0, row_offset); 92 | } 93 | current_row_index <<= 1; 94 | } 95 | for (int col = 0; col < col_count; col++) 96 | { 97 | int col_offset = col * (line_width + line_space) + line_space; 98 | painter.setPen(QPen(high_color, line_width)); 99 | painter.setOpacity(0.4); 100 | painter.drawLine(QPoint(col_offset, 0), QPoint(col_offset, area_height)); 101 | // 当前扫描的列,标出低电平引脚 102 | if (col == currentCol) 103 | { 104 | painter.setPen(QPen(low_color, line_width)); 105 | painter.setOpacity(1); 106 | painter.drawPoint(col_offset, 0); 107 | } 108 | } 109 | painter.setPen(QPen(led_color, led_width)); 110 | for (int col = 0; col < col_count; col++) 111 | { 112 | int distance = 0; 113 | int offset = colOffset; 114 | if (currentCol >= col) 115 | { 116 | distance = currentCol - col; 117 | } 118 | else 119 | { 120 | // 后面的图形列保持之前的位置,不然会跳一格 121 | offset = (colOffset + colCount - 1) % colCount; 122 | distance = currentCol + colCount - col; 123 | } 124 | // 模拟荧光效果,当前扫描的列亮度为 1,逐渐变暗 125 | painter.setOpacity((colCount - distance) / (double)colCount); 126 | // 127 | int col_offset = col * (line_width + line_space) + line_space; 128 | unsigned short col_data = led_data[(col + offset) % colCount]; 129 | unsigned short row_index = 0x01; 130 | for (int row = 0; row < row_count; row++) 131 | { 132 | int row_offset = row * (line_width + line_space) + line_space; 133 | if (col_data & row_index) 134 | { 135 | painter.drawPoint(col_offset, row_offset); 136 | } 137 | row_index <<= 1; 138 | } 139 | } 140 | painter.setOpacity(1); 141 | } 142 | -------------------------------------------------------------------------------- /src/tabdraw/LedLattice.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // LED 点阵 6 | class LedLattice : public QWidget 7 | { 8 | Q_OBJECT 9 | public: 10 | explicit LedLattice(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 | // 总的列数 23 | static const int colCount{16}; 24 | // 当前扫描的列 25 | int currentCol{0}; 26 | // 横向移动偏移 27 | int colOffset{0}; 28 | }; 29 | -------------------------------------------------------------------------------- /src/tabdraw/PlanetSystem.cpp: -------------------------------------------------------------------------------- 1 | #include "PlanetSystem.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | PlanetSystem::PlanetSystem(QWidget *parent) 8 | : QWidget(parent) 9 | { 10 | initSystem(); 11 | 12 | connect(&timer, &QTimer::timeout, this, [this] { 13 | updatePlanet(rootPlanet); 14 | update(); 15 | }); 16 | } 17 | 18 | PlanetSystem::~PlanetSystem() 19 | { 20 | freeSystem(); 21 | } 22 | 23 | void PlanetSystem::showEvent(QShowEvent *event) 24 | { 25 | timer.start(50); 26 | QWidget::showEvent(event); 27 | } 28 | 29 | void PlanetSystem::hideEvent(QHideEvent *event) 30 | { 31 | timer.stop(); 32 | QWidget::hideEvent(event); 33 | } 34 | 35 | void PlanetSystem::paintEvent(QPaintEvent *event) 36 | { 37 | Q_UNUSED(event) 38 | QPainter painter(this); 39 | // 先画一个白底黑框 40 | painter.fillRect(this->rect(), Qt::white); 41 | QPen pen(Qt::black); 42 | painter.setPen(pen); 43 | painter.drawRect(this->rect().adjusted(0, 0, -1, -1)); // 右下角会超出范围 44 | 45 | // 移动到中心,开始绘制 46 | painter.translate(this->rect().center()); 47 | drawPlanet(&painter, rootPlanet); 48 | } 49 | 50 | void PlanetSystem::initSystem() 51 | { 52 | // PlanetNode (公转半径,公转速度,自身半径,自转速度,颜色) 53 | rootPlanet = new PlanetNode(0, 0.0f, 40, 1.5f, QColor(255, 0, 0)); 54 | 55 | PlanetNode *p0 = new PlanetNode(100, 1.5f, 15, 1.5f, QColor(255, 255, 0)); 56 | rootPlanet->subPlanet.push_back(p0); 57 | 58 | PlanetNode *p1 = new PlanetNode(160, 0.5f, 25, 1.5f, QColor(0, 255, 0)); 59 | rootPlanet->subPlanet.push_back(p1); 60 | 61 | PlanetNode *p1_1 = new PlanetNode(50, 1.0f, 15, 1.5f, QColor(0, 0, 255)); 62 | p1->subPlanet.push_back(p1_1); 63 | 64 | PlanetNode *p3 = new PlanetNode(260, 0.2f, 20, 1.5f, QColor(0, 255, 255)); 65 | rootPlanet->subPlanet.push_back(p3); 66 | } 67 | 68 | void PlanetSystem::freeSystem() 69 | { 70 | delete rootPlanet; 71 | } 72 | 73 | void PlanetSystem::drawPlanet(QPainter *painter, PlanetNode *planet) 74 | { 75 | painter->rotate(planet->curSelfRotate); 76 | // 画一个方框和一个圆,方框是为了看自转 77 | QRect planet_rect = QRect(QPoint(-planet->selfRadius, -planet->selfRadius), 78 | QPoint(planet->selfRadius, planet->selfRadius)); 79 | painter->setBrush(planet->color); 80 | painter->setPen(Qt::NoPen); 81 | painter->drawEllipse(planet_rect); 82 | painter->setBrush(Qt::NoBrush); 83 | painter->setPen(Qt::black); 84 | painter->drawRect(planet_rect); 85 | 86 | // 遍历子节点 87 | // 注意:子节点的旋转和位移是相对父节点的 88 | for (PlanetNode *sub_planet : qAsConst(planet->subPlanet)) 89 | { 90 | // 公转轨迹 91 | painter->drawEllipse(QPoint(0, 0), 92 | sub_planet->sysRadius, 93 | sub_planet->sysRadius); 94 | painter->save(); 95 | // 位置转移到子星体处绘制 96 | // 目前直接用的圆形轨道,可给每个星体一个轨道公式 97 | painter->rotate(sub_planet->curSysRotate); 98 | painter->translate(0, -sub_planet->sysRadius); 99 | drawPlanet(painter, sub_planet); 100 | painter->restore(); 101 | } 102 | } 103 | 104 | void PlanetSystem::updatePlanet(PlanetNode *planet) 105 | { 106 | // 公转 107 | planet->curSysRotate += planet->sysSpeed; 108 | // planet->curSysRotate%=360; 109 | // 自转 110 | planet->curSelfRotate += planet->selfSpeed; 111 | // planet->curSelfRotate%=360; 112 | // 遍历子节点 113 | for (PlanetNode *sub_planet : qAsConst(planet->subPlanet)) 114 | { 115 | updatePlanet(sub_planet); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/tabdraw/PlanetSystem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | // 单个星体 7 | class PlanetNode 8 | { 9 | public: 10 | // 构造 11 | PlanetNode(int sysradius, float sysspeed, 12 | int selfradius, float selfspeed, 13 | const QColor &color) 14 | : sysRadius(sysradius), sysSpeed(sysspeed), 15 | selfRadius(selfradius), selfSpeed(selfspeed), 16 | color(color) 17 | { 18 | } 19 | // 析构释放子节点 20 | ~PlanetNode() 21 | { 22 | for (PlanetNode *sub : subPlanet) 23 | { 24 | delete sub; 25 | } 26 | } 27 | 28 | private: 29 | // 公转半径 30 | const int sysRadius; 31 | // 公转速度,为了简化,这里表示每次刷新时围绕父节点旋转的角度(不是移动位置长度) 32 | const float sysSpeed; 33 | // 星体半径 34 | const int selfRadius; 35 | // 自转速度,为了简化,这里表示每次刷新时旋转的角度 36 | const float selfSpeed; 37 | // 当前公转的角度 38 | float curSysRotate{0.0f}; 39 | // 当前自转的角度 40 | float curSelfRotate{0.0f}; 41 | // 颜色 42 | const QColor color; 43 | // 围绕该星体旋转的子星体 44 | QList subPlanet; 45 | 46 | friend class PlanetSystem; 47 | }; 48 | 49 | // 简易的星体系统-演示 rotate 使用 50 | class PlanetSystem : public QWidget 51 | { 52 | Q_OBJECT 53 | public: 54 | explicit PlanetSystem(QWidget *parent = nullptr); 55 | ~PlanetSystem(); 56 | 57 | protected: 58 | // 显示时才启动定时动画 59 | void showEvent(QShowEvent *event) override; 60 | void hideEvent(QHideEvent *event) override; 61 | // 绘制 62 | void paintEvent(QPaintEvent *event) override; 63 | 64 | private: 65 | void initSystem(); 66 | void freeSystem(); 67 | // 递归绘制 68 | void drawPlanet(QPainter *painter, PlanetNode *planet); 69 | // 递归更新角度 70 | void updatePlanet(PlanetNode *planet); 71 | 72 | private: 73 | // 星体根节点 74 | PlanetNode *rootPlanet{nullptr}; 75 | // 旋转定时器 76 | QTimer timer; 77 | }; 78 | -------------------------------------------------------------------------------- /src/tabdraw/SimpleSelection.cpp: -------------------------------------------------------------------------------- 1 | #include "SimpleSelection.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | SimpleSelection::SimpleSelection(QWidget *parent) 10 | : QWidget(parent) 11 | { 12 | setMouseTracking(true); 13 | 14 | selection = QRect(50, 50, 200, 200); 15 | hasSelection = true; 16 | } 17 | 18 | void SimpleSelection::paintEvent(QPaintEvent *event) 19 | { 20 | event->accept(); 21 | QPainter painter(this); 22 | // 黑底 23 | painter.fillRect(this->rect(), Qt::black); 24 | 25 | if (!hasSelection) 26 | return; 27 | 28 | painter.save(); 29 | if (pressFlag && curPosition != AreaPosition::Outside) 30 | { // 点击样式,选用纯正的原谅绿主题 31 | painter.setPen(QColor(0, 255, 255)); 32 | painter.setBrush(QColor(0, 180, 0)); 33 | } 34 | else if (curPosition != AreaPosition::Outside) 35 | { // 悬停样式 36 | painter.setPen(QColor(0, 255, 255)); 37 | painter.setBrush(QColor(0, 160, 0)); 38 | } 39 | else 40 | { // 未选中样式 41 | painter.setPen(QColor(0, 150, 255)); 42 | painter.setBrush(QColor(0, 140, 0)); 43 | } 44 | // -1 是为了边界在 rect 范围内 45 | painter.drawRect(selection.adjusted(0, 0, -1, -1)); 46 | painter.restore(); 47 | } 48 | 49 | void SimpleSelection::mousePressEvent(QMouseEvent *event) 50 | { 51 | event->accept(); 52 | mousePos = event->pos(); 53 | if (event->button() == Qt::LeftButton) 54 | { 55 | // 鼠标左键进行编辑操作 56 | pressFlag = true; 57 | pressPos = event->pos(); 58 | if (curPosition == AreaPosition::Inside) 59 | { 60 | curEditType = PressInside; 61 | // 鼠标相对选区左上角的位置 62 | tempPos = mousePos - selection.topLeft(); 63 | } 64 | else if (curPosition != AreaPosition::Outside) 65 | { 66 | curEditType = EditSelection; 67 | } 68 | else 69 | { 70 | curEditType = PressOutside; 71 | } 72 | } 73 | else 74 | { 75 | // 非单独按左键时的操作 76 | } 77 | update(); 78 | } 79 | 80 | void SimpleSelection::mouseMoveEvent(QMouseEvent *event) 81 | { 82 | event->accept(); 83 | mousePos = event->pos(); 84 | if (pressFlag) 85 | { 86 | if (curEditType == PressInside) 87 | { 88 | // 在选区内点击且移动,则移动选区 89 | if (QPoint(pressPos - mousePos).manhattanLength() > 3) 90 | { 91 | curEditType = MoveSelection; 92 | } 93 | } 94 | else if (curEditType == PressOutside) 95 | { 96 | // 在选区外点击且移动,则绘制选区 97 | if (QPoint(pressPos - mousePos).manhattanLength() > 3) 98 | { 99 | hasSelection = true; 100 | curEditType = DrawSelection; 101 | } 102 | } 103 | 104 | QPoint mouse_p = mousePos; 105 | // 限制范围在可视区域 106 | if (mouse_p.x() < 0) 107 | { 108 | mouse_p.setX(0); 109 | } 110 | else if (mouse_p.x() > width() - 1) 111 | { 112 | mouse_p.setX(width() - 1); 113 | } 114 | if (mouse_p.y() < 0) 115 | { 116 | mouse_p.setY(0); 117 | } 118 | else if (mouse_p.y() > height() - 1) 119 | { 120 | mouse_p.setY(height() - 1); 121 | } 122 | 123 | if (curEditType == DrawSelection) 124 | { 125 | // 根据按下时位置和当前位置确定一个选区 126 | selection = QRect(pressPos, mouse_p); 127 | } 128 | else if (curEditType == MoveSelection) 129 | { 130 | // 移动选区 131 | selection.moveTopLeft(mousePos - tempPos); 132 | // 限制范围在可视区域 133 | if (selection.left() < 0) 134 | { 135 | selection.moveLeft(0); 136 | } 137 | else if (selection.right() > width() - 1) 138 | { 139 | selection.moveRight(width() - 1); 140 | } 141 | if (selection.top() < 0) 142 | { 143 | selection.moveTop(0); 144 | } 145 | else if (selection.bottom() > height() - 1) 146 | { 147 | selection.moveBottom(height() - 1); 148 | } 149 | } 150 | else if (curEditType == EditSelection) 151 | { 152 | // 拉伸选区边界 153 | int position = curPosition; 154 | if (position & AtLeft) 155 | { 156 | if (mouse_p.x() < selection.right()) 157 | { 158 | selection.setLeft(mouse_p.x()); 159 | } 160 | else 161 | { 162 | selection.setLeft(selection.right() - 1); 163 | } 164 | } 165 | else if (position & AtRight) 166 | { 167 | if (mouse_p.x() > selection.left()) 168 | { 169 | selection.setRight(mouse_p.x()); 170 | } 171 | else 172 | { 173 | selection.setRight(selection.left() + 1); 174 | } 175 | } 176 | if (position & AtTop) 177 | { 178 | if (mouse_p.y() < selection.bottom()) 179 | { 180 | selection.setTop(mouse_p.y()); 181 | } 182 | else 183 | { 184 | selection.setTop(selection.bottom() - 1); 185 | } 186 | } 187 | else if (position & AtBottom) 188 | { 189 | if (mouse_p.y() > selection.top()) 190 | { 191 | selection.setBottom(mouse_p.y()); 192 | } 193 | else 194 | { 195 | selection.setBottom(selection.top() + 1); 196 | } 197 | } 198 | } 199 | } 200 | else 201 | { 202 | setCurPosition(calcPosition(mousePos)); 203 | } 204 | update(); 205 | } 206 | 207 | void SimpleSelection::mouseReleaseEvent(QMouseEvent *event) 208 | { 209 | event->accept(); 210 | mousePos = event->pos(); 211 | pressFlag = false; 212 | if (curEditType != EditNone) 213 | { 214 | // 编辑结束后判断是否小于最小宽度,是则取消选区 215 | if (curEditType == DrawSelection) 216 | { 217 | selection = selection.normalized(); 218 | if (selection.width() < Min_Width || selection.height() < Min_Width) 219 | { 220 | hasSelection = false; 221 | } 222 | } 223 | else if (curEditType == MoveSelection) 224 | { 225 | } 226 | else if (curEditType == EditSelection) 227 | { 228 | if (selection.width() < Min_Width || selection.height() < Min_Width) 229 | { 230 | hasSelection = false; 231 | } 232 | } 233 | curEditType = EditNone; 234 | } 235 | setCurPosition(calcPosition(mousePos)); 236 | update(); 237 | } 238 | 239 | SimpleSelection::AreaPosition SimpleSelection::calcPosition(const QPoint &pos) 240 | { 241 | // 一条线太窄,不好触发,增加判断范围又会出现边界太近时交叠在一起 242 | // 目前的策略是从右下开始判断,左上的优先级更低一点 243 | static const int check_radius = 3; 244 | int position = AreaPosition::Outside; 245 | QRect check_rect = selection.adjusted(-check_radius, -check_radius, check_radius-1, check_radius-1); 246 | // 无选区,或者不在选区判定范围则返回 outside 247 | if (!hasSelection || !check_rect.contains(pos)) 248 | { 249 | return (SimpleSelection::AreaPosition)position; 250 | } 251 | // 判断是否在某个边界上 252 | if (std::abs(pos.x() - selection.right()) < check_radius) 253 | { 254 | position |= AreaPosition::AtRight; 255 | } 256 | else if (std::abs(pos.x() - selection.left()) < check_radius) 257 | { 258 | position |= AreaPosition::AtLeft; 259 | } 260 | if (std::abs(pos.y() - selection.bottom()) < check_radius) 261 | { 262 | position |= AreaPosition::AtBottom; 263 | } 264 | else if (std::abs(pos.y() - selection.top()) < check_radius) 265 | { 266 | position |= AreaPosition::AtTop; 267 | } 268 | // 没在边界上就判断是否在内部 269 | if (position == AreaPosition::Outside && selection.contains(pos)) 270 | { 271 | position = AreaPosition::Inside; 272 | } 273 | return (SimpleSelection::AreaPosition)position; 274 | } 275 | 276 | void SimpleSelection::setCurPosition(AreaPosition position) 277 | { 278 | if (position != curPosition) 279 | { 280 | curPosition = position; 281 | updateCursor(); 282 | } 283 | } 284 | 285 | void SimpleSelection::updateCursor() 286 | { 287 | switch (curPosition) 288 | { 289 | case AtLeft: 290 | case AtRight: 291 | setCursor(Qt::SizeHorCursor); 292 | break; 293 | case AtTop: 294 | case AtBottom: 295 | setCursor(Qt::SizeVerCursor); 296 | break; 297 | case AtTopLeft: 298 | case AtBottomRight: 299 | setCursor(Qt::SizeFDiagCursor); 300 | break; 301 | case AtTopRight: 302 | case AtBottomLeft: 303 | setCursor(Qt::SizeBDiagCursor); 304 | break; 305 | default: 306 | setCursor(Qt::ArrowCursor); 307 | break; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/tabdraw/SimpleSelection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // 选区绘制和交互 5 | class SimpleSelection : public QWidget 6 | { 7 | Q_OBJECT 8 | public: 9 | // 鼠标在区域的哪个位置 10 | enum AreaPosition : int 11 | { 12 | Outside = 0x00, 13 | Inside = 0xFF, // 任意值 14 | AtLeft = 0x01, 15 | AtRight = 0x02, 16 | AtTop = 0x10, 17 | AtBottom = 0x20, 18 | AtTopLeft = 0x11, // AtLeft | AtTop 19 | AtTopRight = 0x12, // AtRight | AtTop 20 | AtBottomLeft = 0x21, // AtLeft | AtBottom 21 | AtBottomRight = 0x22 // AtRight | AtBottom 22 | }; 23 | // 当前编辑类型 24 | enum EditType : int 25 | { 26 | EditNone, // 无操作 27 | PressInside, // 在选区范围内按下 28 | PressOutside, // 在选区范围外按下 29 | DrawSelection, // 绘制 30 | MoveSelection, // 拖动 31 | EditSelection // 拉伸编辑 32 | }; 33 | 34 | public: 35 | explicit SimpleSelection(QWidget *parent = nullptr); 36 | 37 | protected: 38 | void paintEvent(QPaintEvent *event) override; 39 | void mousePressEvent(QMouseEvent *event) override; 40 | void mouseMoveEvent(QMouseEvent *event) override; 41 | void mouseReleaseEvent(QMouseEvent *event) override; 42 | 43 | private: 44 | // 计算鼠标相对区域的位置 45 | AreaPosition calcPosition(const QPoint &pos); 46 | // 当前鼠标对应选区的位置 47 | void setCurPosition(AreaPosition position); 48 | // 根据鼠标当前位置更新鼠标样式 49 | void updateCursor(); 50 | 51 | private: 52 | // 当前选区 53 | // QRect 有四个成员变量,分别对应左上角和右下角点坐标 54 | // x1 - 左上角坐标 x 55 | // x2 - 等于 x1+width-1 56 | // y1 - 左上角坐标 y 57 | // y2 - 等于 y1+height-1 58 | // 即 QRect(50,50,200,200) 时,topLeft=(50,50)bottomRight=(249,249) 59 | // fillRect 会填充整个区域 60 | // drawRect 在画笔宽度奇数时,右下角会多 1px,绘制时整体宽度先减去 1px 61 | QRect selection; 62 | // 是否有选区 63 | bool hasSelection{false}; 64 | // 鼠标当前操作位置 65 | AreaPosition curPosition{AreaPosition::Outside}; 66 | // 当前操作类型 67 | EditType curEditType{EditType::EditNone}; 68 | // 鼠标按下标志 69 | bool pressFlag{false}; 70 | // 鼠标按下位置 71 | QPoint pressPos; 72 | // 目前用于记录 press 时鼠标与选区左上角的坐标差值 73 | QPoint tempPos; 74 | // 鼠标当前位置 75 | QPoint mousePos; 76 | 77 | // 最小宽度 78 | static const int Min_Width{5}; 79 | }; 80 | -------------------------------------------------------------------------------- /src/tabdraw/SineWave.cpp: -------------------------------------------------------------------------------- 1 | #include "SineWave.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | SineWave::SineWave(QWidget *parent) 9 | : QWidget(parent) 10 | { 11 | connect(&timer, &QTimer::timeout, this, [this]() { 12 | timeVal += 1; 13 | update(); 14 | }); 15 | } 16 | 17 | void SineWave::showEvent(QShowEvent *event) 18 | { 19 | timer.start(100); 20 | QWidget::showEvent(event); 21 | } 22 | 23 | void SineWave::hideEvent(QHideEvent *event) 24 | { 25 | timer.stop(); 26 | QWidget::hideEvent(event); 27 | } 28 | 29 | void SineWave::paintEvent(QPaintEvent *event) 30 | { 31 | Q_UNUSED(event) 32 | QPainter painter(this); 33 | painter.setRenderHint(QPainter::Antialiasing); 34 | // 波形曲线区域宽度 35 | const int series_width = 500; 36 | // 波形与圆间隔 37 | const int series_space = 120; 38 | // 波形幅度高度 39 | const int series_height = 100; 40 | QList r_list; 41 | // 拉伸后居中 42 | painter.translate((width() - (series_width + series_space * 2)) / 2, 43 | (height() - (series_height * 6)) / 2); 44 | // 前两个为普通的正弦波,最后一个为前面的幅度叠加 45 | { 46 | painter.save(); 47 | // 圆 48 | painter.translate(series_space, series_height); 49 | painter.setPen(QPen(Qt::black, 2)); 50 | const int the_r = 20; 51 | r_list.append(the_r); 52 | // 当前角度 53 | // timeVal 当作时间因子,每隔一段时间跑一段弧度距离,转换为角度 54 | const float angle = timeVal * 36.0 / (2 * the_r * M_PI); 55 | // painter.drawEllipse(QPoint(0,0),5,5); 56 | // 画圆 57 | painter.drawEllipse(QPointF(0, 0), the_r, the_r); 58 | painter.drawLine(-the_r, 0, the_r, 0); 59 | painter.drawLine(0, -the_r, 0, the_r); 60 | // 当前角度对应坐标点 61 | const float the_x = 0 + the_r * cos(-angle); 62 | const float the_y = 0 + the_r * sin(-angle); 63 | painter.setPen(QPen(Qt::red, 2)); 64 | painter.drawEllipse(QPointF(the_x, the_y), 5, 5); 65 | painter.drawLine(QPointF(the_x, the_y), QPointF(0, 0)); 66 | // 时域波形 67 | painter.translate(series_space, 0); 68 | painter.setPen(QPen(Qt::blue, 2)); 69 | QPainterPath the_series; 70 | the_series.moveTo(0, the_y); 71 | for (int i = 0; i < series_width; i++) 72 | { 73 | the_series.lineTo(i, the_r * sin(i * 3 / float(the_r) - angle)); 74 | } 75 | painter.drawPath(the_series); 76 | painter.setPen(QPen(Qt::black, 2)); 77 | painter.drawLine(0, 0, series_width, 0); 78 | painter.drawLine(0, -the_r, 0, the_r); 79 | painter.setPen(QPen(Qt::red, 2, Qt::DotLine)); 80 | painter.drawLine(QPointF(the_x - series_space, the_y), QPointF(0, the_y)); 81 | 82 | painter.restore(); 83 | } 84 | 85 | { 86 | painter.save(); 87 | // 圆 88 | painter.translate(series_space, series_height * 3); 89 | painter.setPen(QPen(Qt::black, 2)); 90 | const int the_r = 60; 91 | r_list.append(the_r); 92 | // 当前角度 93 | const float angle = timeVal * 36.0 / (2 * the_r * M_PI); 94 | // painter.drawEllipse(QPoint(0,0),5,5); 95 | painter.drawEllipse(QPointF(0, 0), the_r, the_r); 96 | painter.drawLine(-the_r, 0, the_r, 0); 97 | painter.drawLine(0, -the_r, 0, the_r); 98 | const float the_x = 0 + the_r * cos(-angle); 99 | const float the_y = 0 + the_r * sin(-angle); 100 | painter.setPen(QPen(Qt::red, 2)); 101 | painter.drawEllipse(QPointF(the_x, the_y), 5, 5); 102 | painter.drawLine(QPointF(the_x, the_y), QPointF(0, 0)); 103 | // 时域波形 104 | painter.translate(series_space, 0); 105 | painter.setPen(QPen(Qt::blue, 2)); 106 | QPainterPath the_series; 107 | the_series.moveTo(0, the_y); 108 | for (int i = 0; i < series_width; i++) 109 | { 110 | the_series.lineTo(i, the_r * sin(i * 3 / float(the_r) - angle)); 111 | } 112 | painter.drawPath(the_series); 113 | painter.setPen(QPen(Qt::black, 2)); 114 | painter.drawLine(0, 0, series_width, 0); 115 | painter.drawLine(0, -the_r, 0, the_r); 116 | painter.setPen(QPen(Qt::red, 2, Qt::DotLine)); 117 | painter.drawLine(QPointF(the_x - series_space, the_y), QPointF(0, the_y)); 118 | 119 | painter.restore(); 120 | } 121 | 122 | { 123 | painter.save(); 124 | // 圆 125 | painter.translate(series_space, series_height * 5); 126 | painter.save(); 127 | int r_temp = 0; 128 | float x_temp = 0; 129 | float y_temp = 0; 130 | for (int i = r_list.count() - 1; i >= 0; i--) 131 | { 132 | const int the_r = r_list.at(i); 133 | r_temp += the_r; 134 | // 当前角度 135 | const float angle = timeVal * 36.0 / (2 * the_r * M_PI); 136 | painter.setPen(QPen(Qt::black, 2)); 137 | painter.drawEllipse(QPointF(0, 0), the_r, the_r); 138 | if (i == r_list.count() - 1) 139 | { 140 | painter.drawLine(-the_r, 0, the_r, 0); 141 | painter.drawLine(0, -the_r, 0, the_r); 142 | } 143 | const float the_x = 0 + the_r * cos(-angle); 144 | const float the_y = 0 + the_r * sin(-angle); 145 | x_temp += the_x; 146 | y_temp += the_y; 147 | painter.setPen(QPen(Qt::red, 2)); 148 | painter.drawLine(QPointF(the_x, the_y), QPointF(0, 0)); 149 | painter.translate(the_x, the_y); 150 | } 151 | painter.restore(); 152 | 153 | // 时域波形 154 | painter.translate(series_space, 0); 155 | painter.setPen(QPen(Qt::blue, 2)); 156 | QPainterPath the_series; 157 | the_series.moveTo(0, y_temp); 158 | for (int i = 0; i < series_width; i++) 159 | { 160 | float y = 0; 161 | for (int j = r_list.count() - 1; j >= 0; j--) 162 | { 163 | int the_r = r_list.at(j); 164 | float angle = timeVal * 36.0 / (2 * the_r * M_PI); 165 | y += the_r * sin(i * 3 / float(the_r) - angle); 166 | } 167 | the_series.lineTo(i, y); 168 | } 169 | painter.drawPath(the_series); 170 | painter.setPen(QPen(Qt::black, 2)); 171 | painter.drawLine(0, 0, series_width, 0); 172 | painter.drawLine(0, -r_temp, 0, r_temp); 173 | painter.setPen(QPen(Qt::red, 2, Qt::DotLine)); 174 | painter.drawLine(QPointF(x_temp - series_space, y_temp), QPointF(0, y_temp)); 175 | 176 | painter.restore(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/tabdraw/SineWave.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // 绘制波形 6 | class SineWave : public QWidget 7 | { 8 | Q_OBJECT 9 | public: 10 | explicit SineWave(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 | // 根据时间步进 23 | int timeVal{0}; 24 | }; 25 | -------------------------------------------------------------------------------- /src/tabdraw/TabDraw.cpp: -------------------------------------------------------------------------------- 1 | #include "TabDraw.h" 2 | #include "ui_TabDraw.h" 3 | 4 | TabDraw::TabDraw(QWidget *parent) 5 | : QWidget{parent} 6 | , ui{new Ui::TabDraw} 7 | { 8 | ui->setupUi(this); 9 | ui->tabWidget->setCurrentIndex(0); 10 | } 11 | 12 | TabDraw::~TabDraw() 13 | { 14 | delete ui; 15 | } 16 | -------------------------------------------------------------------------------- /src/tabdraw/TabDraw.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace Ui { 5 | class TabDraw; 6 | } 7 | 8 | // 绘制自定义效果 9 | class TabDraw : public QWidget 10 | { 11 | Q_OBJECT 12 | public: 13 | explicit TabDraw(QWidget *parent = nullptr); 14 | ~TabDraw(); 15 | 16 | private: 17 | Ui::TabDraw *ui; 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /src/tabdraw/TabDraw.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | TabDraw 4 | 5 | 6 | 7 | 0 8 | 0 9 | 595 10 | 398 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 20 | 4 21 | 22 | 23 | 24 | TaiJi 25 | 26 | 27 | 28 | 29 | PlanetSystem 30 | 31 | 32 | 33 | 34 | SineWave 35 | 36 | 37 | 38 | 39 | Circle 40 | 41 | 42 | 43 | 44 | Bezier 45 | 46 | 47 | 48 | 49 | LedLattice 50 | 51 | 52 | 53 | 54 | CalcPos 55 | 56 | 57 | 58 | 59 | CalcDegree 60 | 61 | 62 | 63 | 64 | Selection 65 | 66 | 67 | 68 | 69 | Animation 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | PlanetSystem 79 | QWidget 80 |
PlanetSystem.h
81 | 1 82 |
83 | 84 | SineWave 85 | QWidget 86 |
SineWave.h
87 | 1 88 |
89 | 90 | CalcPos 91 | QWidget 92 |
CalcPos.h
93 | 1 94 |
95 | 96 | CalcDegree 97 | QWidget 98 |
CalcDegree.h
99 | 1 100 |
101 | 102 | SimpleSelection 103 | QWidget 104 |
SimpleSelection.h
105 | 1 106 |
107 | 108 | DrawAnimation 109 | QWidget 110 |
DrawAnimation.h
111 | 1 112 |
113 | 114 | TaiJi 115 | QWidget 116 |
TaiJi.h
117 | 1 118 |
119 | 120 | LedLattice 121 | QWidget 122 |
LedLattice.h
123 | 1 124 |
125 | 126 | DrawCircle 127 | QWidget 128 |
DrawCircle.h
129 | 1 130 |
131 | 132 | DrawBezier 133 | QWidget 134 |
DrawBezier.h
135 | 1 136 |
137 |
138 | 139 | 140 |
141 | -------------------------------------------------------------------------------- /src/tabdraw/TaiJi.cpp: -------------------------------------------------------------------------------- 1 | #include "TaiJi.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | TaiJi::TaiJi(QWidget *parent) 10 | : QWidget(parent) 11 | { 12 | connect(&timer, &QTimer::timeout, this, [this] { 13 | offset += 1; 14 | offset %= 360; 15 | update(); 16 | }); 17 | } 18 | 19 | void TaiJi::showEvent(QShowEvent *event) 20 | { 21 | timer.start(50); 22 | QWidget::showEvent(event); 23 | } 24 | 25 | void TaiJi::hideEvent(QHideEvent *event) 26 | { 27 | timer.stop(); 28 | QWidget::hideEvent(event); 29 | } 30 | 31 | void TaiJi::paintEvent(QPaintEvent *event) 32 | { 33 | event->accept(); 34 | { 35 | QPainter painter(this); 36 | // 黑色背景 37 | painter.fillRect(this->rect(), Qt::black); 38 | } 39 | taijiTest(); 40 | taijiPaint(); 41 | } 42 | 43 | void TaiJi::taijiTest() 44 | { 45 | // 此函数主要是验证旋转和图层遮挡的逻辑 46 | QPainter painter(this); 47 | // painter.fillRect(this->rect(), Qt::black); 48 | painter.setRenderHint(QPainter::Antialiasing); 49 | 50 | // 两像素的画笔避免抗锯齿或者变换后框线看不清 51 | QPen pen; 52 | pen.setWidth(2); 53 | 54 | int radius = 150; 55 | // 圆形,后面旋转 z 轴和 y 轴使两个平面相交 56 | QPainterPath ellipse_path; 57 | ellipse_path.addEllipse(QPointF(0, 0), radius, radius); 58 | 59 | // 在圆形基础上加了十字线,用于观察旋转方向 60 | QPainterPath line_path = ellipse_path; 61 | line_path.moveTo(0, radius); 62 | line_path.lineTo(0, -radius); 63 | line_path.closeSubpath(); 64 | line_path.moveTo(radius, 0); 65 | line_path.lineTo(-radius, 0); 66 | line_path.closeSubpath(); 67 | 68 | // QTransform 是一个二维变换类,可以和 QPainter 搭配使用 69 | QTransform trans; 70 | // 中心移动到窗口中心偏左 71 | trans.translate(width() / 2 - radius - 10, height() / 2); 72 | // z 轴旋转 15 度,效果是平面上右转了 15 度 73 | // 后面的变换也是在此基础上,所以两个平面相交的部分是右倾的 74 | trans.rotate(15, Qt::ZAxis); 75 | // 这里设置 trans 后,接下来的 clip 裁剪区域也是旋转了 15 度的 76 | painter.setTransform(trans); 77 | // 在初次变换的基础上,z 轴随时间偏移,产生旋转动画效果 78 | QTransform ztrans = trans; 79 | ztrans.rotate(offset, Qt::ZAxis); 80 | // 开始画第一个面 81 | pen.setColor(Qt::red); 82 | painter.setPen(pen); 83 | { 84 | // save 是为了 clip 不污染后面的操作 85 | painter.save(); 86 | // clip 顶部的矩形区域(底部是被另一个面遮盖的) 87 | painter.setClipRect(QRect(-radius, -radius, radius * 2, radius)); 88 | painter.setTransform(ztrans); 89 | // 填充这个区域会受 clip 的影响 90 | painter.fillPath(ellipse_path, QColor(255, 0, 0, 100)); 91 | painter.restore(); 92 | } 93 | // 在没有 clip 的情况下绘制框线,以进行观察 94 | painter.setTransform(ztrans); 95 | painter.drawPath(line_path); 96 | 97 | // 在 z 轴旋转 15 度的基础上,x 轴再旋转 50 度,即顶部往里翻转了 98 | trans.rotate(50, Qt::XAxis); 99 | painter.setTransform(trans); 100 | // 旋转的角度需要反过来 101 | QTransform xtrans = trans; 102 | xtrans.rotate(-offset, Qt::ZAxis); 103 | // 开始画第二个面,逻辑同第一个面 104 | pen.setColor(Qt::cyan); 105 | painter.setPen(pen); 106 | { 107 | painter.save(); 108 | painter.setClipRect(QRect(-radius, 0, radius * 2, radius)); 109 | painter.setTransform(xtrans); 110 | painter.fillPath(ellipse_path, QColor(0, 255, 0, 100)); 111 | painter.restore(); 112 | } 113 | painter.setTransform(xtrans); 114 | painter.drawPath(line_path); 115 | } 116 | 117 | void TaiJi::taijiPaint() 118 | { 119 | QPainter painter(this); 120 | // painter.fillRect(this->rect(), Qt::black); 121 | painter.setRenderHint(QPainter::Antialiasing); 122 | 123 | QPen pen; 124 | // 大圆半径 125 | int radius = 150; 126 | // 小孔半径 127 | int sub_radius = radius / 5; 128 | // 玉外面的球-圆心 129 | QPointF out_point(0, radius / 2); 130 | // 玉小孔的圆心 131 | QPointF in_point(0, -radius / 2); 132 | // z 轴旋转面的颜色 133 | QColor z_color(100, 100, 100); 134 | // x 轴旋转面的颜色 135 | QColor x_color(200, 200, 200); 136 | 137 | // 圆形,后面旋转 z 轴和 y 轴使两个平面相交 138 | QPainterPath ellipse_path; 139 | ellipse_path.addEllipse(QPointF(0, 0), radius, radius); 140 | 141 | // z 轴旋转面的玉路径 142 | QPainterPath z_path; 143 | // 奇偶填充,这样填充会把小孔留出空白 144 | z_path.setFillRule(Qt::OddEvenFill); 145 | z_path.moveTo(0, -radius); 146 | // 一个大圆弧 147 | z_path.arcTo(QRectF(-radius, -radius, radius * 2, radius * 2), 90, 180); 148 | // 两个小圆弧 149 | z_path.arcTo(QRectF(-radius / 2, 0, radius, radius), 270, -180); 150 | z_path.arcTo(QRectF(-radius / 2, -radius, radius, radius), 270, 180); 151 | // 小孔 152 | z_path.addEllipse(in_point, sub_radius, sub_radius); 153 | z_path.closeSubpath(); 154 | 155 | // x 轴旋转面的玉路径,做两个是因为旋转方向相反,绘制取反后绘制的效果不大好 156 | QPainterPath x_path; 157 | x_path.setFillRule(Qt::OddEvenFill); 158 | x_path.moveTo(0, radius); 159 | x_path.arcTo(QRectF(-radius, -radius, radius * 2, radius * 2), 270, 180); 160 | x_path.arcTo(QRectF(-radius / 2, -radius, radius, radius), 90, 180); 161 | x_path.arcTo(QRectF(-radius / 2, 0, radius, radius), 90, -180); 162 | x_path.addEllipse(in_point, sub_radius, sub_radius); 163 | x_path.closeSubpath(); 164 | 165 | // QTransform 是一个二维变换类,可以和 QPainter 搭配使用 166 | QTransform trans; 167 | // 中心移动到窗口中心偏右 168 | trans.translate(width() / 2 + radius + 10, height() / 2); 169 | // z 轴旋转 15 度,效果是平面上右转了 15 度 170 | // 后面的变换也是在此基础上,所以两个平面相交的部分是右倾的 171 | trans.rotate(15, Qt::ZAxis); 172 | // 在初次变换的基础上,z轴随时间偏移,产生旋转动画效果 173 | QTransform ztrans = trans; 174 | ztrans.rotate(offset, Qt::ZAxis); 175 | // 在 z 轴旋转 15 度的基础上,x 轴再旋转 50 度,即顶部往里翻转了 176 | trans.rotate(50, Qt::XAxis); 177 | QTransform xtrans = trans; 178 | xtrans.rotate(-offset, Qt::ZAxis); 179 | 180 | // 通过变换获取到小球和小孔圆心对应窗口实际的坐标 181 | QPointF z_out = ztrans.map(out_point); 182 | QPointF z_in = ztrans.map(in_point); 183 | QPainterPath z_ptpath; 184 | z_ptpath.addEllipse(z_out, sub_radius, sub_radius); 185 | QPointF x_out = xtrans.map(out_point); 186 | // QPointF x_in=xtrans.map(in_point); 187 | QPainterPath x_ptpath; 188 | x_ptpath.addEllipse(x_out, sub_radius, sub_radius); 189 | 190 | // 两个小球的渐变填充,是看起来有点立体感 191 | QRadialGradient x_gradient(x_out, sub_radius); 192 | x_gradient.setColorAt(0, QColor(250, 250, 250)); 193 | x_gradient.setColorAt(1, QColor(200, 200, 200)); 194 | QRadialGradient z_gradient(z_out, sub_radius); 195 | z_gradient.setColorAt(0, QColor(150, 150, 150)); 196 | z_gradient.setColorAt(1, QColor(100, 100, 100)); 197 | 198 | // 先绘制底层,即被遮盖的区域(相当于z值权重更低) 199 | { 200 | // 绘制 x 轴旋转的小球,y 小于另一个玉的小孔圆心 y,表示当前被遮挡 201 | if (x_out.y() < z_in.y()) 202 | { 203 | pen.setColor(Qt::red); 204 | painter.setPen(pen); 205 | painter.fillPath(x_ptpath, x_gradient); 206 | } 207 | 208 | painter.save(); 209 | QTransform trans; 210 | trans.translate(width() / 2 + radius + 10, height() / 2); 211 | // z 轴旋转 15 度,效果是平面上右转了 15 度 212 | trans.rotate(15, Qt::ZAxis); 213 | painter.setTransform(trans); 214 | { 215 | // save 是为了 clip 不污染后面的操作 216 | painter.save(); 217 | // clip 顶部的矩形区域(底部是被另一个面遮盖的) 218 | painter.setClipRect(QRect(-radius, 0, radius * 2, radius)); 219 | painter.setTransform(ztrans); 220 | painter.fillPath(z_path, z_color); 221 | painter.restore(); 222 | } 223 | // 在 z 轴旋转 15 度的基础上,x 轴再旋转 50 度,即顶部往里翻转了 224 | trans.rotate(50, Qt::XAxis); 225 | painter.setTransform(trans); 226 | { 227 | // save 是为了 clip 不污染后面的操作 228 | painter.save(); 229 | // clip 底部的矩形区域(顶部是被另一个面遮盖的) 230 | painter.setClipRect(QRect(-radius, -radius, radius * 2, radius)); 231 | painter.setTransform(xtrans); 232 | painter.fillPath(x_path, x_color); 233 | painter.restore(); 234 | } 235 | painter.restore(); 236 | 237 | // 绘制 z 轴旋转的小球 238 | pen.setColor(Qt::red); 239 | painter.setPen(pen); 240 | painter.fillPath(z_ptpath, z_gradient); 241 | } 242 | 243 | // 绘制表层,逻辑同绘制底层 244 | { 245 | painter.save(); 246 | QTransform trans; 247 | trans.translate(width() / 2 + radius + 10, height() / 2); 248 | trans.rotate(15, Qt::ZAxis); 249 | painter.setTransform(trans); 250 | { 251 | painter.save(); 252 | // 高度 +2 是为了遮盖两个平面相交部分 clip 加抗锯齿导致的虚线 253 | painter.setClipRect(QRect(-radius, -radius - 1, radius * 2, radius + 2)); 254 | painter.setTransform(ztrans); 255 | painter.fillPath(z_path, z_color); 256 | painter.restore(); 257 | } 258 | 259 | trans.rotate(50, Qt::XAxis); 260 | painter.setTransform(trans); 261 | { 262 | painter.save(); 263 | painter.setClipRect(QRect(-radius, -1, radius * 2, radius + 2)); 264 | painter.setTransform(xtrans); 265 | painter.fillPath(x_path, x_color); 266 | painter.restore(); 267 | } 268 | painter.restore(); 269 | 270 | // 绘制 x 轴旋转的小球,y 大于另一个玉的小孔圆心 y,表示在表层 271 | if (x_out.y() >= z_in.y()) 272 | { 273 | pen.setColor(Qt::red); 274 | painter.setPen(pen); 275 | painter.fillPath(x_ptpath, x_gradient); 276 | } 277 | } 278 | 279 | // 小球定位测试 280 | // painter.drawEllipse(z_in,10,10); 281 | // painter.drawEllipse(z_out,10,10); 282 | // painter.drawEllipse(x_in,10,10); 283 | // painter.drawEllipse(x_out,10,10); 284 | } 285 | -------------------------------------------------------------------------------- /src/tabdraw/TaiJi.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // 太极 6 | class TaiJi : public QWidget 7 | { 8 | Q_OBJECT 9 | public: 10 | explicit TaiJi(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 taijiTest(); 20 | // 太极绘制 21 | void taijiPaint(); 22 | 23 | private: 24 | // 定时动画 25 | QTimer timer; 26 | // 旋转角度 [0-360] 27 | int offset{0}; 28 | }; 29 | -------------------------------------------------------------------------------- /src/tabdraw/tabdraw.pri: -------------------------------------------------------------------------------- 1 | FORMS += \ 2 | $$PWD/TabDraw.ui 3 | 4 | HEADERS += \ 5 | $$PWD/CalcDegree.h \ 6 | $$PWD/CalcPos.h \ 7 | $$PWD/DrawAnimation.h \ 8 | $$PWD/DrawBezier.h \ 9 | $$PWD/DrawCircle.h \ 10 | $$PWD/LedLattice.h \ 11 | $$PWD/PlanetSystem.h \ 12 | $$PWD/SimpleSelection.h \ 13 | $$PWD/SineWave.h \ 14 | $$PWD/TabDraw.h \ 15 | $$PWD/TaiJi.h 16 | 17 | SOURCES += \ 18 | $$PWD/CalcDegree.cpp \ 19 | $$PWD/CalcPos.cpp \ 20 | $$PWD/DrawAnimation.cpp \ 21 | $$PWD/DrawBezier.cpp \ 22 | $$PWD/DrawCircle.cpp \ 23 | $$PWD/LedLattice.cpp \ 24 | $$PWD/PlanetSystem.cpp \ 25 | $$PWD/SimpleSelection.cpp \ 26 | $$PWD/SineWave.cpp \ 27 | $$PWD/TabDraw.cpp \ 28 | $$PWD/TaiJi.cpp 29 | 30 | --------------------------------------------------------------------------------