├── .gitignore ├── JellyButtonBox.pro ├── LICENSE ├── README.md ├── interactive_buttons ├── interactivebuttonbase.cpp ├── interactivebuttonbase.h ├── watercirclebutton.cpp └── watercirclebutton.h ├── jelly_button_box ├── jellybutton.cpp ├── jellybutton.h ├── jellybuttonbox.cpp └── jellybuttonbox.h ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui ├── pictures └── picture.gif ├── resources.qrc └── resources └── icons ├── icon1.png ├── icon2.png └── icon3.png /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | 41 | # xemacs temporary files 42 | *.flc 43 | 44 | # Vim temporary files 45 | .*.swp 46 | 47 | # Visual Studio generated files 48 | *.ib_pdb_index 49 | *.idb 50 | *.ilk 51 | *.pdb 52 | *.sln 53 | *.suo 54 | *.vcproj 55 | *vcproj.*.*.user 56 | *.ncb 57 | *.sdf 58 | *.opensdf 59 | *.vcxproj 60 | *vcxproj.* 61 | 62 | # MinGW generated files 63 | *.Debug 64 | *.Release 65 | 66 | # Python byte code 67 | *.pyc 68 | 69 | # Binaries 70 | # -------- 71 | *.dll 72 | *.exe 73 | 74 | -------------------------------------------------------------------------------- /JellyButtonBox.pro: -------------------------------------------------------------------------------- 1 | QT += core gui 2 | 3 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets 4 | 5 | CONFIG += c++11 6 | 7 | # The following define makes your compiler emit warnings if you use 8 | # any Qt feature that has been marked deprecated (the exact warnings 9 | # depend on your compiler). Please consult the documentation of the 10 | # deprecated API in order to know how to port your code away from it. 11 | DEFINES += QT_DEPRECATED_WARNINGS 12 | 13 | # You can also make your code fail to compile if it uses deprecated APIs. 14 | # In order to do so, uncomment the following line. 15 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 16 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 17 | 18 | INCLUDEPATH += jelly_button_box/ \ 19 | interactive_buttons/ 20 | 21 | SOURCES += \ 22 | interactive_buttons/interactivebuttonbase.cpp \ 23 | interactive_buttons/watercirclebutton.cpp \ 24 | jelly_button_box/jellybutton.cpp \ 25 | jelly_button_box/jellybuttonbox.cpp \ 26 | main.cpp \ 27 | mainwindow.cpp 28 | 29 | HEADERS += \ 30 | interactive_buttons/interactivebuttonbase.h \ 31 | interactive_buttons/watercirclebutton.h \ 32 | jelly_button_box/jellybutton.h \ 33 | jelly_button_box/jellybuttonbox.h \ 34 | mainwindow.h 35 | 36 | FORMS += \ 37 | mainwindow.ui 38 | 39 | # Default rules for deployment. 40 | qnx: target.path = /tmp/$${TARGET}/bin 41 | else: unix:!android: target.path = /opt/$${TARGET}/bin 42 | !isEmpty(target.path): INSTALLS += target 43 | 44 | RESOURCES += \ 45 | resources.qrc 46 | 47 | DISTFILES += \ 48 | README.md \ 49 | pictures/picture.gif 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 果冻按钮三剑客 2 | === 3 | 4 | 弹性拉出的三个按钮 5 | 6 | 使用贝塞尔曲线实现(计算得不是很好,但勉强看得出来) 7 | 8 | ![截图](pictures/picture.gif) -------------------------------------------------------------------------------- /interactive_buttons/interactivebuttonbase.cpp: -------------------------------------------------------------------------------- 1 | #include "interactivebuttonbase.h" 2 | 3 | /** 4 | * 所有内容的初始化 5 | * 如果要自定义,可以在这里调整所有的默认值 6 | */ 7 | InteractiveButtonBase::InteractiveButtonBase(QWidget *parent) 8 | : QPushButton(parent), icon(nullptr), text(""), paint_addin(), 9 | fore_paddings(4,4,4,4), 10 | self_enabled(true), parent_enabled(false), fore_enabled(true), 11 | show_animation(false), show_foreground(true), show_ani_appearing(false), show_ani_disappearing(false), 12 | show_duration(300), show_timestamp(0), hide_timestamp(0), show_ani_progress(0), show_ani_point(0,0), 13 | enter_pos(-1, -1), press_pos(-1, -1), release_pos(-1, -1), mouse_pos(-1, -1), anchor_pos(-1, -1), 14 | offset_pos(0, 0), effect_pos(-1, -1), release_offset(0, 0), 15 | hovering(false), pressing(false), 16 | hover_timestamp(0), leave_timestamp(0), press_timestamp(0), release_timestamp(0), 17 | hover_bg_duration(300), press_bg_duration(300), click_ani_duration(300), 18 | move_speed(5), 19 | icon_color(0, 0, 0), text_color(0,0,0), 20 | normal_bg(0xF2, 0xF2, 0xF2, 0), hover_bg(128, 128, 128, 32), press_bg(128, 128, 128, 64), border_bg(0,0,0,0), 21 | focus_bg(0,0,0,0), focus_border(0,0,0,0), 22 | hover_speed(5), press_start(40), press_speed(5), 23 | hover_progress(0), press_progress(0), icon_padding_proper(0.25), icon_text_padding(4), icon_text_size(16), 24 | border_width(1), radius_x(0), radius_y(0), 25 | font_size(0), fixed_fore_pos(false), fixed_fore_size(false), text_dynamic_size(false), auto_text_color(true), focusing(false), 26 | click_ani_appearing(false), click_ani_disappearing(false), click_ani_progress(0), 27 | mouse_press_event(nullptr), mouse_release_event(nullptr), 28 | unified_geometry(false), _l(0), _t(0), _w(32), _h(32), 29 | jitter_animation(true), elastic_coefficient(1.2), jitter_duration(300), 30 | water_animation(true), water_press_duration(800), water_release_duration(400), water_finish_duration(300), 31 | align(Qt::AlignCenter), _state(false), leave_after_clicked(false), _block_hover(false), 32 | double_clicked(false), double_timer(nullptr), double_prevent(false) 33 | { 34 | setMouseTracking(true); // 鼠标没有按下时也能捕获移动事件 35 | 36 | model = PaintModel::None; 37 | 38 | anchor_timer = new QTimer(this); 39 | anchor_timer->setInterval(10); 40 | connect(anchor_timer, SIGNAL(timeout()), this, SLOT(anchorTimeOut())); 41 | 42 | setWaterRipple(); 43 | 44 | connect(this, SIGNAL(clicked()), this, SLOT(slotClicked())); 45 | 46 | setFocusPolicy(Qt::NoFocus); // 避免一个按钮还获取Tab键焦点 47 | } 48 | 49 | /** 50 | * 文字类型的按钮 51 | */ 52 | InteractiveButtonBase::InteractiveButtonBase(QString text, QWidget *parent) 53 | : InteractiveButtonBase(parent) 54 | { 55 | setText(text); 56 | } 57 | 58 | /** 59 | * 图标类型的按钮 60 | */ 61 | InteractiveButtonBase::InteractiveButtonBase(QIcon icon, QWidget *parent) 62 | : InteractiveButtonBase(parent) 63 | { 64 | setIcon(icon); 65 | } 66 | 67 | /** 68 | * 变色图标类型的按钮 69 | */ 70 | InteractiveButtonBase::InteractiveButtonBase(QPixmap pixmap, QWidget *parent) 71 | : InteractiveButtonBase(parent) 72 | { 73 | setPixmap(pixmap); 74 | } 75 | 76 | InteractiveButtonBase::InteractiveButtonBase(QIcon icon, QString text, QWidget *parent) 77 | : InteractiveButtonBase(parent) 78 | { 79 | setIcon(icon); 80 | setText(text); 81 | } 82 | 83 | InteractiveButtonBase::InteractiveButtonBase(QPixmap pixmap, QString text, QWidget *parent) 84 | : InteractiveButtonBase(parent) 85 | { 86 | setPixmap(pixmap); 87 | setText(text); 88 | } 89 | 90 | /** 91 | * 设置按钮文字 92 | * @param text 按钮文字 93 | */ 94 | void InteractiveButtonBase::setText(QString text) 95 | { 96 | this->text = text; 97 | if (model == PaintModel::None) 98 | model = PaintModel::Text; 99 | else if (model == PaintModel::PixmapMask) 100 | { 101 | if (pixmap.isNull()) 102 | model = PaintModel::Text; 103 | else 104 | model = PaintModel::PixmapText; 105 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 106 | QFontMetrics fm(this->font()); 107 | icon_text_size = fm.lineSpacing(); 108 | } 109 | else if (model == PaintModel::Icon) 110 | { 111 | if (icon.isNull()) 112 | model = PaintModel::Text; 113 | else 114 | model = PaintModel::IconText; 115 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 116 | QFontMetrics fm(this->font()); 117 | icon_text_size = fm.lineSpacing(); 118 | } 119 | 120 | if (parent_enabled) 121 | QPushButton::setText(text); 122 | 123 | // 根据字体调整大小 124 | if (text_dynamic_size) 125 | { 126 | if (font_size <= 0) 127 | { 128 | QFontMetrics fm(font()); 129 | setMinimumSize(fm.horizontalAdvance(text)+fore_paddings.left+fore_paddings.right, fm.lineSpacing()+fore_paddings.top+fore_paddings.bottom); 130 | } 131 | else 132 | { 133 | QFont font; 134 | font.setPointSize(font_size); 135 | QFontMetrics fm(font); 136 | setMinimumSize(fm.horizontalAdvance(text)+fore_paddings.left+fore_paddings.right, fm.lineSpacing()+fore_paddings.top+fore_paddings.bottom); 137 | } 138 | } 139 | update(); 140 | } 141 | 142 | /** 143 | * 设置 icon 图标 144 | * @param path 图标路径文本 145 | */ 146 | void InteractiveButtonBase::setIconPath(QString path) 147 | { 148 | setIcon(QIcon(path)); 149 | } 150 | 151 | /** 152 | * 设置 pixmap 图标 153 | * @param path 图标路径文本 154 | */ 155 | void InteractiveButtonBase::setPixmapPath(QString path) 156 | { 157 | setPixmap(QPixmap(path)); 158 | } 159 | 160 | /** 161 | * 设置 icon 162 | * @param icon 图标 163 | */ 164 | void InteractiveButtonBase::setIcon(QIcon icon) 165 | { 166 | if (model == PaintModel::None) 167 | model = PaintModel::Icon; 168 | else if (model == PaintModel::Text) 169 | { 170 | if (text.isEmpty()) 171 | model = PaintModel::Icon; 172 | else 173 | model = PaintModel::IconText; 174 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 175 | QFontMetrics fm(this->font()); 176 | icon_text_size = fm.lineSpacing(); 177 | } 178 | else if (model == PaintModel::PixmapMask) 179 | { 180 | pixmap = QPixmap(); 181 | model = PaintModel::Icon; 182 | } 183 | else if (model == PaintModel::PixmapText) 184 | { 185 | pixmap = QPixmap(); 186 | if (text.isEmpty()) 187 | model = PaintModel::Icon; 188 | else 189 | model = PaintModel::IconText; 190 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 191 | QFontMetrics fm(this->font()); 192 | icon_text_size = fm.lineSpacing(); 193 | } 194 | this->icon = icon; 195 | if (parent_enabled) 196 | QPushButton::setIcon(icon); 197 | update(); 198 | } 199 | 200 | /** 201 | * 设置 Pixmap 202 | * @param pixmap [description] 203 | */ 204 | void InteractiveButtonBase::setPixmap(QPixmap pixmap) 205 | { 206 | if (model == PaintModel::None) 207 | model = PaintModel::PixmapMask; 208 | else if (model == PaintModel::Text) 209 | { 210 | if (text.isEmpty()) 211 | model = PaintModel::PixmapMask; 212 | else 213 | model = PaintModel::PixmapText; 214 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 215 | QFontMetrics fm(this->font()); 216 | icon_text_size = fm.lineSpacing(); 217 | } 218 | else if (model == PaintModel::Icon) 219 | { 220 | icon = QIcon(); 221 | model = PaintModel::PixmapMask; 222 | } 223 | else if (model == PaintModel::IconText) 224 | { 225 | icon = QIcon(); 226 | if (text.isEmpty()) 227 | model = PaintModel::PixmapMask; 228 | else 229 | model = PaintModel::PixmapText; 230 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 231 | QFontMetrics fm(this->font()); 232 | icon_text_size = fm.lineSpacing(); 233 | } 234 | this->pixmap = getMaskPixmap(pixmap, isEnabled()?icon_color:getOpacityColor(icon_color)); 235 | if (parent_enabled) 236 | QPushButton::setIcon(QIcon(pixmap)); 237 | update(); 238 | } 239 | 240 | /** 241 | * 设置额外的图标,例如角标 242 | * @param pixmap 图标 243 | * @param align 对齐方式 244 | * @param size 图标尺寸 245 | */ 246 | void InteractiveButtonBase::setPaintAddin(QPixmap pixmap, Qt::Alignment align, QSize size) 247 | { 248 | QBitmap mask = pixmap.mask(); 249 | pixmap.fill(icon_color); 250 | pixmap.setMask(mask); 251 | paint_addin = PaintAddin(pixmap, align, size); 252 | update(); 253 | } 254 | 255 | /** 256 | * 设置子类功能是否开启 257 | * 如果关闭,则相当于默认的 QPushButton 258 | * @param e 开关 259 | */ 260 | void InteractiveButtonBase::setSelfEnabled(bool e) 261 | { 262 | self_enabled = e; 263 | } 264 | 265 | /** 266 | * 设置父类(QPushButton)功能是否开启 267 | * 如果开启,则绘制父类背景、父类前景 268 | * @param e 开关 269 | */ 270 | void InteractiveButtonBase::setParentEnabled(bool e) 271 | { 272 | parent_enabled = e; 273 | 274 | // 传递子类内容到父类去,避免子类关掉后不显示 275 | if (model == PaintModel::Text || model == PaintModel::IconText || model == PaintModel::PixmapText) 276 | QPushButton::setText(text); 277 | if (model == PaintModel::Icon || model == PaintModel::IconText) 278 | QPushButton::setIcon(icon); 279 | if (model == PaintModel::PixmapMask || model == PaintModel::PixmapText) 280 | QPushButton::setIcon(QIcon(pixmap)); 281 | } 282 | 283 | /** 284 | * 设置是否绘制前景图标/文字 285 | * 关闭后则只绘制背景 286 | * @param e 开启 287 | */ 288 | void InteractiveButtonBase::setForeEnabled(bool e) 289 | { 290 | fore_enabled = e; 291 | } 292 | 293 | /** 294 | * 设置鼠标悬浮背景渐变的动画时长 295 | * @param d 动画时长(毫秒) 296 | */ 297 | void InteractiveButtonBase::setHoverAniDuration(int d) 298 | { 299 | this->hover_bg_duration = d; 300 | // hover_progress = 0; // 重置hover效果 301 | } 302 | 303 | /** 304 | * 设置鼠标按下渐变效果的动画时长 305 | * @param d 动画时长(毫秒) 306 | */ 307 | void InteractiveButtonBase::setPressAniDuration(int d) 308 | { 309 | this->press_bg_duration = d; 310 | } 311 | 312 | /** 313 | * 设置单击效果的动画时长 314 | * @param d 动画时长(毫秒) 315 | */ 316 | void InteractiveButtonBase::setClickAniDuration(int d) 317 | { 318 | this->click_ani_duration = d; 319 | } 320 | 321 | /** 322 | * 设置水波纹动画时长 323 | * @param press 按住时时长(时长毫秒) 324 | * @param release 松开后速度(时长毫秒) 325 | * @param finish 渐变消失速度(时长毫秒) 326 | */ 327 | void InteractiveButtonBase::setWaterAniDuration(int press, int release, int finish) 328 | { 329 | this->water_press_duration = press; 330 | this->water_release_duration = release; 331 | this->water_finish_duration = finish; 332 | } 333 | 334 | /** 335 | * 各种状态改变 336 | * 主要是监控 可用 状态,不可用时设置为半透明 337 | */ 338 | void InteractiveButtonBase::changeEvent(QEvent *event) 339 | { 340 | QPushButton::changeEvent(event); 341 | 342 | if (event->type() == QEvent::EnabledChange && model == PixmapMask) // 可用状态改变了 343 | { 344 | if (isEnabled()) // 恢复可用:透明度变回去 345 | { 346 | QColor color = icon_color; 347 | color.setAlpha(color.alpha() * 2); 348 | setIconColor(color); 349 | } 350 | else // 变成不可用:透明度减半 351 | { 352 | QColor color = icon_color; 353 | color.setAlpha(color.alpha() / 2); 354 | setIconColor(color); 355 | } 356 | } 357 | } 358 | 359 | /** 360 | * 设置水波纹动画是否开启 361 | * 关闭时,将使用渐变动画 362 | * @param enable 开关 363 | */ 364 | void InteractiveButtonBase::setWaterRipple(bool enable) 365 | { 366 | if (water_animation == enable) return ; 367 | water_animation = enable; 368 | } 369 | 370 | /** 371 | * 设置抖动效果是否开启 372 | * 鼠标拖拽移动的距离越长,抖动距离越长、次数越多 373 | * @param enable 开关 374 | */ 375 | void InteractiveButtonBase::setJitterAni(bool enable) 376 | { 377 | jitter_animation = enable; 378 | } 379 | 380 | /** 381 | * 设置是否使用统一图标绘制区域 382 | * 监听图标尺寸大小变化、中心点偏移,计算新的中心坐标位置 383 | * @param enable 开关 384 | */ 385 | void InteractiveButtonBase::setUnifyGeomerey(bool enable) 386 | { 387 | unified_geometry = enable; 388 | _l = _t = 0; _w = size().width(); _h = size().height(); 389 | } 390 | 391 | /** 392 | * 设置背景颜色 393 | * @param bg 背景颜色 394 | */ 395 | void InteractiveButtonBase::setBgColor(QColor bg) 396 | { 397 | setNormalColor(bg); 398 | update(); 399 | } 400 | 401 | /** 402 | * 设置事件背景颜色 403 | * @param hover 鼠标悬浮时的背景颜色 404 | * @param press 鼠标按下时的背景颜色 405 | */ 406 | void InteractiveButtonBase::setBgColor(QColor hover, QColor press) 407 | { 408 | if (hover != Qt::black) 409 | setHoverColor(hover); 410 | if (press != Qt::black) 411 | setPressColor(press); 412 | update(); 413 | } 414 | 415 | /** 416 | * 设置按钮背景颜色 417 | * @param color 背景颜色 418 | */ 419 | void InteractiveButtonBase::setNormalColor(QColor color) 420 | { 421 | normal_bg = color; 422 | } 423 | 424 | /** 425 | * 设置边框线条颜色 426 | * @param color 边框颜色 427 | */ 428 | void InteractiveButtonBase::setBorderColor(QColor color) 429 | { 430 | border_bg = color; 431 | } 432 | 433 | /** 434 | * 设置鼠标悬浮时的背景颜色 435 | * @param color 背景颜色 436 | */ 437 | void InteractiveButtonBase::setHoverColor(QColor color) 438 | { 439 | hover_bg = color; 440 | } 441 | 442 | /** 443 | * 设置鼠标按住时的背景颜色 444 | * @param color 背景颜色 445 | */ 446 | void InteractiveButtonBase::setPressColor(QColor color) 447 | { 448 | press_bg = color; 449 | } 450 | 451 | /** 452 | * 设置图标颜色(仅针对可变色的 pixmap 图标) 453 | * @param color 图标颜色 454 | */ 455 | void InteractiveButtonBase::setIconColor(QColor color) 456 | { 457 | icon_color = color; 458 | 459 | // 绘制图标(如果有) 460 | if (model == PaintModel::PixmapMask || model == PaintModel::PixmapText) 461 | { 462 | pixmap = getMaskPixmap(pixmap, isEnabled()?icon_color:getOpacityColor(icon_color)); 463 | } 464 | 465 | // 绘制额外角标(如果有的话) 466 | if (paint_addin.enable) 467 | { 468 | paint_addin.pixmap = getMaskPixmap(paint_addin.pixmap, isEnabled()?icon_color:getOpacityColor(icon_color)); 469 | } 470 | 471 | update(); 472 | } 473 | 474 | /** 475 | * 设置前景文字颜色 476 | * @param color 文字颜色 477 | */ 478 | void InteractiveButtonBase::setTextColor(QColor color) 479 | { 480 | text_color = color; 481 | update(); 482 | } 483 | 484 | /** 485 | * 设置获取焦点时的背景颜色(默认关闭) 486 | * @param color 背景颜色 487 | */ 488 | void InteractiveButtonBase::setFocusBg(QColor color) 489 | { 490 | setFocusPolicy(Qt::StrongFocus); 491 | focus_bg = color; 492 | } 493 | 494 | /** 495 | * 设置获取焦点时的边框颜色(默认关闭) 496 | * @param color 边框颜色 497 | */ 498 | void InteractiveButtonBase::setFocusBorder(QColor color) 499 | { 500 | setFocusPolicy(Qt::StrongFocus); 501 | focus_border = color; 502 | } 503 | 504 | /** 505 | * 设置文字大小(PointSize,覆盖 font() 字体大小) 506 | * @param f 文字大小 507 | */ 508 | void InteractiveButtonBase::setFontSize(int f) 509 | { 510 | if (!font_size) // 第一次设置字体大小,直接设置 511 | { 512 | font_size = f; 513 | QFont font(this->font()); 514 | font.setPointSize(f); 515 | setFont(font); 516 | update(); 517 | } 518 | else // 改变字体大小,使用字体缩放动画 519 | { 520 | QPropertyAnimation* ani = new QPropertyAnimation(this, "font_size"); 521 | ani->setStartValue(font_size); 522 | ani->setEndValue(f); 523 | ani->setDuration(click_ani_duration); 524 | connect(ani, &QPropertyAnimation::finished, [=]{ 525 | QFontMetrics fm(this->font()); 526 | icon_text_size = fm.lineSpacing(); 527 | ani->deleteLater(); 528 | }); 529 | ani->start(); 530 | } 531 | // 修改字体大小时调整按钮的最小尺寸,避免文字显示不全 532 | if (text_dynamic_size) 533 | { 534 | QFont font; 535 | font.setPointSize(f); 536 | QFontMetrics fms(font); 537 | setMinimumSize(fms.horizontalAdvance(text)+fore_paddings.left+fore_paddings.right, fms.lineSpacing()+fore_paddings.top+fore_paddings.bottom); 538 | } 539 | if (model != PaintModel::Text) 540 | { 541 | QFontMetrics fm(this->font()); 542 | icon_text_size = fm.lineSpacing(); 543 | } 544 | } 545 | 546 | /** 547 | * 获取字体大小 548 | * 用来作为字体动画的属性参数 549 | * @return 临时字体大小 550 | */ 551 | int InteractiveButtonBase::getFontSizeT() 552 | { 553 | return font_size; 554 | } 555 | 556 | /** 557 | * 设置动画中的临时字体大小 558 | * 用来作为字体动画的属性参数 559 | * @param f 临时字体大小 560 | */ 561 | void InteractiveButtonBase::setFontSizeT(int f) 562 | { 563 | this->font_size = f; 564 | QFont font(this->font()); 565 | font.setPointSize(f); 566 | setFont(font); 567 | update(); 568 | } 569 | 570 | /** 571 | * 如果点击失去焦点的话,即使鼠标移到上面,也不会出现背景 572 | * 可以用这个方法继续保持悬浮状态 573 | */ 574 | void InteractiveButtonBase::setHover() 575 | { 576 | if (!hovering && inArea(mapFromGlobal(QCursor::pos()))) 577 | InteractiveButtonBase::enterEvent(new QEvent(QEvent::Type::None)); 578 | } 579 | 580 | /** 581 | * 设置对齐方式 582 | * @param a 对齐方式 583 | */ 584 | void InteractiveButtonBase::setAlign(Qt::Alignment a) 585 | { 586 | align = a; 587 | update(); 588 | } 589 | 590 | /** 591 | * 设置四个角的半径 592 | * @param r 半径 593 | */ 594 | void InteractiveButtonBase::setRadius(int r) 595 | { 596 | radius_x = radius_y = r; 597 | } 598 | 599 | /** 600 | * 分开设置 X、Y 的半径 601 | * @param rx X半径 602 | * @param ry Y半径 603 | */ 604 | void InteractiveButtonBase::setRadius(int rx, int ry) 605 | { 606 | radius_x = rx; 607 | radius_y = ry; 608 | } 609 | 610 | /** 611 | * 设置边框线条的粗细 612 | * @param x 线条粗细 613 | */ 614 | void InteractiveButtonBase::setBorderWidth(int x) 615 | { 616 | border_width = x; 617 | } 618 | 619 | /** 620 | * 设置不可用情况(默认为假) 621 | * 区别于 setEnabled(bool),两个相反的,并不是覆盖方法 622 | * @param dis 不可用 623 | */ 624 | void InteractiveButtonBase::setDisabled(bool dis) 625 | { 626 | if (dis == !isEnabled()) // 相同的 627 | return ; 628 | 629 | setEnabled(!dis); 630 | 631 | if (parentWidget()!=nullptr) 632 | { 633 | setAttribute(Qt::WA_TransparentForMouseEvents, dis); // 点击穿透 634 | } 635 | 636 | if (model == PixmapMask || model == PixmapText) 637 | { 638 | pixmap = getMaskPixmap(pixmap, dis?getOpacityColor(icon_color):icon_color); 639 | } 640 | 641 | update(); // 修改透明度 642 | } 643 | 644 | /** 645 | * 设置前景和四条边的 paddings 646 | * @param l 左边空白 647 | * @param r 右边空白 648 | * @param t 顶边空白 649 | * @param b 底边空白 650 | */ 651 | void InteractiveButtonBase::setPaddings(int l, int r, int t, int b) 652 | { 653 | fore_paddings.left = l; 654 | fore_paddings.right = r; 655 | fore_paddings.top = t; 656 | fore_paddings.bottom = b; 657 | setFixedForeSize(); 658 | } 659 | 660 | /** 661 | * 统一设置方向的 paddings 662 | * @param h 横向 663 | * @param v 纵向 664 | */ 665 | void InteractiveButtonBase::setPaddings(int h, int v) 666 | { 667 | fore_paddings.left = fore_paddings.right = (h+1) / 2; 668 | fore_paddings.top = fore_paddings.bottom = (v+1) / 2; 669 | setFixedForeSize(); 670 | } 671 | 672 | /** 673 | * 统一设置前景和四条边的 paddings 674 | * @param x 一样大小的四边留白 675 | */ 676 | void InteractiveButtonBase::setPaddings(int x) 677 | { 678 | fore_paddings.left = x; 679 | fore_paddings.right = x; 680 | fore_paddings.top = x; 681 | fore_paddings.bottom = x; 682 | setFixedForeSize(); 683 | } 684 | 685 | void InteractiveButtonBase::setIconPaddingProper(double x) 686 | { 687 | icon_padding_proper = x; 688 | int short_side = min(geometry().width(), geometry().height()); // 短边 689 | // 非固定的情况,尺寸大小变了之后所有 padding 都要变 690 | int padding = short_side*icon_padding_proper; //static_cast(short_side * (1 - GOLDEN_RATIO) / 2); 691 | fore_paddings.left = fore_paddings.top = fore_paddings.right = fore_paddings.bottom = padding; 692 | update(); 693 | } 694 | 695 | /** 696 | * 设置字体大小时是否同步修改按钮的最小尺寸(避免按钮显示不全) 697 | * @param d 开关 698 | */ 699 | void InteractiveButtonBase::setTextDynamicSize(bool d) 700 | { 701 | text_dynamic_size = d; 702 | } 703 | 704 | /** 705 | * 见 setFixedForePos(bool f) 706 | */ 707 | void InteractiveButtonBase::setFixedTextPos(bool f) 708 | { 709 | fixed_fore_pos = f; 710 | } 711 | 712 | /** 713 | * 设置前景是否固定,而不移动 714 | * 将去除鼠标移入靠近、抖动效果,统一图标区域大小不变 715 | * 只包括:鼠标进入/点击,均表现为缩放效果(默认) 716 | * 不影响任何其他功能 717 | * @param f [description] 718 | */ 719 | void InteractiveButtonBase::setFixedForePos(bool f) 720 | { 721 | fixed_fore_pos = f; 722 | } 723 | 724 | /** 725 | * 固定按钮为适当尺寸,并且固定四周留白 726 | * 前景应为文字/图标对应尺寸的最小尺寸 727 | * @param f 是否固定前景 728 | * @param addin 留白的像素大小 729 | */ 730 | void InteractiveButtonBase::setFixedForeSize(bool f, int addin) 731 | { 732 | fixed_fore_size = f; 733 | 734 | if (!f) return ; 735 | if (model == PaintModel::Text || model == PaintModel::IconText || model == PaintModel::PixmapText) 736 | { 737 | QFont font = this->font(); 738 | if (font_size > 0) 739 | font.setPointSize(font_size); 740 | QFontMetrics fm(font); 741 | setMinimumSize( 742 | fm.horizontalAdvance(text)+fore_paddings.left+fore_paddings.right+addin, 743 | fm.lineSpacing()+fore_paddings.top+fore_paddings.bottom+addin 744 | ); 745 | } 746 | else if (model == PaintModel::Icon || model == PaintModel::PixmapMask) 747 | { 748 | int size = height(); 749 | setMinimumSize(size+addin, size+addin); 750 | } 751 | } 752 | 753 | void InteractiveButtonBase::setSquareSize() 754 | { 755 | setFixedWidth(height()); 756 | setMinimumWidth(height()); 757 | setMaximumWidth(height()); 758 | } 759 | 760 | /** 761 | * 设置鼠标单击松开后是否当做移开 762 | * 避免菜单、弹窗出现后,由于鼠标仍然留在按钮上面,导致依旧显示 hover 背景 763 | * @param l 开关 764 | */ 765 | void InteractiveButtonBase::setLeaveAfterClick(bool l) 766 | { 767 | leave_after_clicked = l; 768 | } 769 | 770 | /** 771 | * 响应双击事件 772 | * 注意:会先触发单击事件、再触发双击事件(其实就是懒得做) 773 | * 建议在 QListWidget 等地方使用! 774 | * @param e 开关 775 | */ 776 | void InteractiveButtonBase::setDoubleClicked(bool e) 777 | { 778 | double_clicked = e; 779 | 780 | if (double_timer == nullptr) 781 | { 782 | double_timer = new QTimer(this); 783 | double_timer->setInterval(DOUBLE_PRESS_INTERVAL); 784 | connect(double_timer, &QTimer::timeout, [=]{ 785 | double_timer->stop(); 786 | emit clicked(); // 手动触发单击事件 787 | }); 788 | } 789 | } 790 | 791 | /** 792 | * 动画时是否自动设置文字的颜色 793 | */ 794 | void InteractiveButtonBase::setAutoTextColor(bool a) 795 | { 796 | this->auto_text_color = a; 797 | } 798 | 799 | /** 800 | * 一开始没有聚焦时,假装获取焦点 801 | * 通过信号槽使其他控件(例如QLineEdit)按下enter键触发此按钮事件 802 | * 直到触发了焦点改变事件,此控件失去焦点(需要手动改变) 803 | */ 804 | void InteractiveButtonBase::setPretendFocus(bool f) 805 | { 806 | focusing = f; 807 | update(); 808 | } 809 | 810 | /** 811 | * 如果按钮被做成一个组合,在显示的时候开启动画 812 | * 一开始鼠标下的按钮一直在hover状态,移开也不会变 813 | * 开启后临时屏蔽,记得在动画结束后关闭 814 | */ 815 | void InteractiveButtonBase::setBlockHover(bool b) 816 | { 817 | _block_hover = b; 818 | if (b && hovering) 819 | leaveEvent(nullptr); 820 | } 821 | 822 | /** 823 | * 是否开启出现动画 824 | * 鼠标进入按钮区域,前景图标从对面方向缩放出现 825 | * @param enable 开关 826 | */ 827 | void InteractiveButtonBase::setShowAni(bool enable) 828 | { 829 | show_animation = enable; 830 | 831 | if (!show_animation) // 关闭隐藏前景 832 | { 833 | show_foreground = true; 834 | } 835 | else if (show_animation) // 开启隐藏前景 836 | { 837 | if (!hovering && !pressing) // 应该是隐藏状态 838 | { 839 | show_ani_appearing = show_ani_disappearing = show_foreground = false; 840 | show_ani_progress = 0; 841 | } 842 | else // 应该是显示状态 843 | { 844 | show_foreground = true; 845 | show_ani_appearing = show_ani_disappearing = false; 846 | show_ani_progress = 100; 847 | } 848 | } 849 | } 850 | 851 | /** 852 | * 按钮前景出现动画 853 | * 从中心点出现的缩放动画 854 | */ 855 | void InteractiveButtonBase::showForeground() 856 | { 857 | if (!show_animation) return ; 858 | waters.clear(); 859 | if (!anchor_timer->isActive()) 860 | anchor_timer->start(); 861 | if (show_ani_disappearing) 862 | show_ani_disappearing = false; 863 | show_ani_appearing = true; 864 | show_timestamp = getTimestamp(); 865 | show_foreground = true; 866 | show_ani_point = QPoint(0,0); 867 | } 868 | 869 | /** 870 | * 按钮前景出现动画2 871 | * 指定方向(笛卡尔坐标),从反方向至中心点 872 | * @param point 最开始出现的方向(大小不影响,只按 x、y 比例来) 873 | */ 874 | void InteractiveButtonBase::showForeground2(QPoint point) 875 | { 876 | showForeground(); 877 | if (point == QPoint(0,0)) 878 | point = mapFromGlobal(QCursor::pos()) - QPoint(size().width()/2, size().height()/2); // 相对于按钮中心 879 | show_ani_point = point; 880 | 881 | if (unified_geometry) // 统一出现动画 882 | updateUnifiedGeometry(); 883 | } 884 | 885 | /** 886 | * 隐藏前景 887 | * 为下一次的出现动画做准备 888 | */ 889 | void InteractiveButtonBase::hideForeground() 890 | { 891 | if (!show_animation) return ; 892 | if (!anchor_timer->isActive()) 893 | anchor_timer->start(); 894 | if (show_ani_appearing) 895 | show_ani_appearing = false; 896 | show_ani_disappearing = true; 897 | hide_timestamp = getTimestamp(); 898 | } 899 | 900 | /** 901 | * 延迟出现前景 902 | * 适用于多个按钮连续出现的一套效果 903 | * @param time 延迟时长(毫秒) 904 | * @param point 出现方向 905 | */ 906 | void InteractiveButtonBase::delayShowed(int time, QPoint point) 907 | { 908 | setShowAni(true); 909 | QTimer::singleShot(time, [=]{ 910 | showForeground2(point); 911 | connect(this, &InteractiveButtonBase::showAniFinished, [=]{ 912 | setShowAni(false); 913 | disconnect(this, SIGNAL(showAniFinished()), nullptr, nullptr); 914 | }); 915 | }); 916 | } 917 | 918 | /** 919 | * 获取文字 920 | */ 921 | QString InteractiveButtonBase::getText() 922 | { 923 | return text; 924 | } 925 | 926 | /** 927 | * 设置菜单 928 | * 并解决菜单无法监听到 release 的问题 929 | * @param menu 菜单对象 930 | */ 931 | void InteractiveButtonBase::setMenu(QMenu *menu) 932 | { 933 | // 默认设置了不获取焦点事件,所以如果设置了菜单的话,就不会有Release事件,水波纹动画会一直飘荡 934 | // 在 focusOut 事件中,模拟了 release 事件, 935 | this->setFocusPolicy(Qt::FocusPolicy::ClickFocus); 936 | 937 | QPushButton::setMenu(menu); 938 | } 939 | 940 | /** 941 | * 设置状态 942 | * 一个用来作为开关效果的属性 943 | * @param s 状态 944 | */ 945 | void InteractiveButtonBase::setState(bool s) 946 | { 947 | _state = s; 948 | update(); 949 | } 950 | 951 | /** 952 | * 获取状态 953 | * @return 状态 954 | */ 955 | bool InteractiveButtonBase::getState() 956 | { 957 | return _state; 958 | } 959 | 960 | /** 961 | * 模拟按下开关的效果,并改变状态 962 | * 如果不使用状态,则出现点击动画 963 | * @param s 目标状态(默认为false) 964 | * @param a 鼠标在区域内则点击无效(恐怕再次点击) 965 | */ 966 | void InteractiveButtonBase::simulateStatePress(bool s, bool a) 967 | { 968 | if (getState() == s) 969 | return ; 970 | 971 | // 鼠标悬浮在上方,有两种情况: 972 | // 1、点击按钮后触发,重复了 973 | // 2、需要假装触发,例如 Popup 类型,尽管悬浮在上面,但是无法点击到 974 | if (a && inArea(mapFromGlobal(QCursor::pos()))) // 点击当前按钮,不需要再模拟了 975 | return ; 976 | 977 | mousePressEvent(new QMouseEvent(QMouseEvent::Type::None, QPoint(size().width()/2,size().height()/2), Qt::LeftButton, Qt::NoButton, Qt::NoModifier)); 978 | 979 | mouseReleaseEvent(new QMouseEvent(QMouseEvent::Type::None, QPoint(size().width()/2,size().height()/2), Qt::LeftButton, Qt::NoButton, Qt::NoModifier)); 980 | 981 | // if (!inArea(mapFromGlobal(QCursor::pos()))) // 针对模拟release 后面 // 必定成立 982 | hovering = false; 983 | } 984 | 985 | /** 986 | * 模拟鼠标悬浮的效果 987 | * 适用于键盘操作时,模拟鼠标hover状态 988 | * 用 discardHoverPress 取消状态 989 | */ 990 | void InteractiveButtonBase::simulateHover() 991 | { 992 | if (!hovering) 993 | { 994 | if (_block_hover) 995 | setBlockHover(false); // 可能已经临时屏蔽掉鼠标 enter 事件,强制hover 996 | enterEvent(nullptr); 997 | } 998 | } 999 | 1000 | /** 1001 | * 强制丢弃hover、press状态 1002 | * 适用于悬浮/点击后,弹出模态浮窗 1003 | * 浮窗关闭后调用此方法 1004 | * @param force 如果鼠标仍在此按钮内,是否强制取消hover/press状态 1005 | */ 1006 | void InteractiveButtonBase::discardHoverPress(bool force) 1007 | { 1008 | if (!force && inArea(mapFromGlobal(QCursor::pos()))) // 鼠标还在这范围内 1009 | return ; 1010 | 1011 | if (hovering) 1012 | { 1013 | leaveEvent(nullptr); 1014 | } 1015 | 1016 | if (pressing) 1017 | { 1018 | mouseReleaseEvent(new QMouseEvent(QMouseEvent::Type::None, QPoint(size().width()/2,size().height()/2), Qt::LeftButton, Qt::NoButton, Qt::NoModifier)); 1019 | } 1020 | } 1021 | 1022 | /** 1023 | * 鼠标移入事件,触发 hover 时间戳 1024 | */ 1025 | void InteractiveButtonBase::enterEvent(QEvent *event) 1026 | { 1027 | if (_block_hover) // 临时屏蔽hover事件 1028 | { 1029 | if (event) 1030 | event->accept(); 1031 | return ; 1032 | } 1033 | 1034 | if (!anchor_timer->isActive()) 1035 | { 1036 | anchor_timer->start(); 1037 | } 1038 | hovering = true; 1039 | hover_timestamp = getTimestamp(); 1040 | leave_timestamp = 0; 1041 | if (mouse_pos == QPoint(-1,-1)) 1042 | mouse_pos = mapFromGlobal(QCursor::pos()); 1043 | emit signalMouseEnter(); 1044 | 1045 | return QPushButton::enterEvent(event); 1046 | } 1047 | 1048 | /** 1049 | * 鼠标移开事件,触发 leave 时间戳 1050 | */ 1051 | void InteractiveButtonBase::leaveEvent(QEvent *event) 1052 | { 1053 | hovering = false; 1054 | if (!pressing) 1055 | mouse_pos = QPoint(geometry().width()/2, geometry().height()/2); 1056 | emit signalMouseLeave(); 1057 | 1058 | return QPushButton::leaveEvent(event); 1059 | } 1060 | 1061 | /** 1062 | * 鼠标按下事件,触发 press 时间戳 1063 | * 添加水波纹动画 waters 队列 1064 | */ 1065 | void InteractiveButtonBase::mousePressEvent(QMouseEvent *event) 1066 | { 1067 | mouse_pos = event->pos(); 1068 | 1069 | if (event->button() == Qt::LeftButton) 1070 | { 1071 | if (!hovering) 1072 | InteractiveButtonBase::enterEvent(new QEvent(QEvent::Type::None)); 1073 | 1074 | pressing = true; 1075 | press_pos = mouse_pos; 1076 | // 判断双击事件 1077 | if (double_clicked) 1078 | { 1079 | qint64 last_press_timestamp = press_timestamp; 1080 | press_timestamp = getTimestamp(); 1081 | if (release_timestamp+DOUBLE_PRESS_INTERVAL>=press_timestamp 1082 | && last_press_timestamp+SINGLE_PRESS_INTERVAL>release_timestamp 1083 | && release_pos==press_pos) // 是双击(判断两次单击的间隔) 1084 | { 1085 | double_prevent = true; // 阻止本次的release识别为双击 1086 | double_timer->stop(); 1087 | emit doubleClicked(); 1088 | return ; 1089 | } 1090 | else 1091 | { 1092 | double_prevent = false; // 避免有额外的 bug 1093 | } 1094 | } 1095 | else 1096 | { 1097 | press_timestamp = getTimestamp(); 1098 | } 1099 | 1100 | if (water_animation) 1101 | { 1102 | if (waters.size() && waters.last().release_timestamp == 0) // 避免两个按键同时按下 1103 | waters.last().release_timestamp = getTimestamp(); 1104 | waters << Water(press_pos, press_timestamp); 1105 | } 1106 | else // 透明渐变 1107 | { 1108 | if (press_progress < press_start) 1109 | press_progress = press_start; // 直接设置为按下效果初始值(避免按下反应慢) 1110 | } 1111 | } 1112 | mouse_press_event = event; 1113 | emit signalMousePress(event); 1114 | 1115 | return QPushButton::mousePressEvent(event); 1116 | } 1117 | 1118 | /** 1119 | * 鼠标松开事件,触发 release 时间戳 1120 | * 添加抖动动画 jitters 队列 1121 | */ 1122 | void InteractiveButtonBase::mouseReleaseEvent(QMouseEvent* event) 1123 | { 1124 | if (pressing && event->button() == Qt::LeftButton) 1125 | { 1126 | if (!inArea(event->pos()) || leave_after_clicked) 1127 | { 1128 | hovering = false; 1129 | } 1130 | pressing = false; 1131 | release_pos = event->pos(); 1132 | release_timestamp = getTimestamp(); 1133 | 1134 | // 添加抖动效果 1135 | if (jitter_animation) 1136 | { 1137 | setJitter(); 1138 | } 1139 | 1140 | if (water_animation && waters.size()) 1141 | { 1142 | waters.last().release_timestamp = release_timestamp; 1143 | } 1144 | 1145 | if (double_clicked) 1146 | { 1147 | if (double_prevent) // 双击的当次release,不参与单击计算 1148 | { 1149 | double_prevent = false; 1150 | return ; 1151 | } 1152 | 1153 | // 应该不是双击的操作 1154 | if (release_pos != press_pos || release_timestamp - press_timestamp >= SINGLE_PRESS_INTERVAL) 1155 | { 1156 | 1157 | } 1158 | else // 可能是双击,准备 1159 | { 1160 | double_timer->start(); 1161 | return ; // 禁止单击事件 1162 | } 1163 | } 1164 | } 1165 | else if (leave_after_clicked && !pressing && double_clicked && double_prevent) // 双击,失去焦点了,pressing 丢失 1166 | { 1167 | return ; 1168 | } 1169 | mouse_release_event = event; 1170 | emit signalMouseRelease(event); 1171 | 1172 | return QPushButton::mouseReleaseEvent(event); 1173 | } 1174 | 1175 | /** 1176 | * 鼠标移动事件 1177 | */ 1178 | void InteractiveButtonBase::mouseMoveEvent(QMouseEvent *event) 1179 | { 1180 | if (_block_hover) // 临时屏蔽hover事件 1181 | { 1182 | if (event) 1183 | event->accept(); 1184 | return ; 1185 | } 1186 | if (hovering == false) // 失去焦点又回来了 1187 | { 1188 | enterEvent(nullptr); 1189 | } 1190 | mouse_pos = mapFromGlobal(QCursor::pos()); 1191 | 1192 | return QPushButton::mouseMoveEvent(event); 1193 | } 1194 | 1195 | /** 1196 | * 尺寸大小改变事件 1197 | * 同步调整和尺寸有关的所有属性 1198 | */ 1199 | void InteractiveButtonBase::resizeEvent(QResizeEvent *event) 1200 | { 1201 | if (!pressing && !hovering) 1202 | { 1203 | mouse_pos = QPoint(geometry().width()/2, geometry().height()/2); 1204 | anchor_pos = mouse_pos; 1205 | } 1206 | water_radius = static_cast(max(geometry().width(), geometry().height()) * 1.42); // 长边 1207 | // 非固定的情况,尺寸大小变了之后所有 padding 都要变 1208 | if (model == PaintModel::Icon || model == PaintModel::PixmapMask) 1209 | { 1210 | int short_side = min(geometry().width(), geometry().height()); // 短边 1211 | int padding = short_side*icon_padding_proper; //static_cast(short_side * (1 - GOLDEN_RATIO) / 2); 1212 | fore_paddings.left = fore_paddings.top = fore_paddings.right = fore_paddings.bottom = padding; 1213 | } 1214 | _l = _t = 0; _w = size().width(); _h = size().height(); 1215 | 1216 | return QPushButton::resizeEvent(event); 1217 | } 1218 | 1219 | /** 1220 | * 获得焦点事件 1221 | * 已经取消按钮获取焦点,focusIn和focusOut事件都不会触发 1222 | */ 1223 | void InteractiveButtonBase::focusInEvent(QFocusEvent *event) 1224 | { 1225 | if (!hovering && inArea(mapFromGlobal(QCursor::pos()))) 1226 | InteractiveButtonBase::enterEvent(new QEvent(QEvent::Type::None)); 1227 | 1228 | focusing = true; 1229 | emit signalFocusIn(); 1230 | 1231 | return QPushButton::focusInEvent(event); 1232 | } 1233 | 1234 | /** 1235 | * 失去焦点事件 1236 | * 兼容按住时突然失去焦点(例如弹出菜单、被其他窗口抢走了) 1237 | */ 1238 | void InteractiveButtonBase::focusOutEvent(QFocusEvent *event) 1239 | { 1240 | if (hovering) 1241 | { 1242 | hovering = false; 1243 | } 1244 | if (pressing) // 鼠标一直按住,可能在click事件中移动了焦点 1245 | { 1246 | pressing = false; 1247 | release_pos = mapFromGlobal(QCursor::pos()); 1248 | release_timestamp = getTimestamp(); 1249 | 1250 | if (water_animation && waters.size()) 1251 | { 1252 | waters.last().release_timestamp = release_timestamp; 1253 | } 1254 | } 1255 | 1256 | focusing = false; 1257 | emit signalFocusOut(); 1258 | 1259 | return QPushButton::focusOutEvent(event); 1260 | } 1261 | 1262 | /** 1263 | * 重绘事件 1264 | * 绘制所有内容:背景、动画、前景、角标 1265 | */ 1266 | void InteractiveButtonBase::paintEvent(QPaintEvent* event) 1267 | { 1268 | if (parent_enabled) // 绘制父类(以便使用父类的QSS和各项属性) 1269 | QPushButton::paintEvent(event); 1270 | if (!self_enabled) // 不绘制自己 1271 | return ; 1272 | QPainter painter(this); 1273 | 1274 | // ==== 绘制背景 ==== 1275 | QPainterPath path_back = getBgPainterPath(); 1276 | painter.setRenderHint(QPainter::Antialiasing,true); 1277 | 1278 | if (normal_bg.alpha() != 0) // 默认背景 1279 | { 1280 | painter.fillPath(path_back, isEnabled()?normal_bg:getOpacityColor(normal_bg)); 1281 | } 1282 | if (focusing && focus_bg.alpha() != 0) // 焦点背景 1283 | { 1284 | painter.fillPath(path_back, focus_bg); 1285 | } 1286 | 1287 | if ((border_bg.alpha() != 0 || (focusing && focus_border.alpha() != 0)) && border_width > 0) 1288 | { 1289 | painter.save(); 1290 | QPen pen; 1291 | pen.setColor((focusing && focus_border.alpha()) ? focus_border : border_bg); 1292 | pen.setWidth(border_width); 1293 | painter.setPen(pen); 1294 | painter.drawPath(path_back); 1295 | painter.restore(); 1296 | } 1297 | 1298 | if (hover_progress) // 悬浮背景 1299 | { 1300 | painter.fillPath(path_back, getOpacityColor(hover_bg, hover_progress / 100.0)); 1301 | } 1302 | 1303 | if (press_progress && !water_animation) // 按下渐变淡化消失 1304 | { 1305 | painter.fillPath(path_back, getOpacityColor(press_bg, press_progress/100.0)); 1306 | } 1307 | else if (water_animation && waters.size()) // 水波纹,且至少有一个水波纹 1308 | { 1309 | paintWaterRipple(painter); 1310 | } 1311 | 1312 | // ==== 绘制前景 ==== 1313 | if (fore_enabled/*针对按钮设置*/ && show_foreground/*针对动画设置*/) 1314 | { 1315 | painter.setPen(isEnabled()?icon_color:getOpacityColor(icon_color)); 1316 | 1317 | // 绘制额外内容(可能被前景覆盖) 1318 | if (paint_addin.enable) 1319 | { 1320 | int l = fore_paddings.left, t = fore_paddings.top, r = size().width()-fore_paddings.right, b = size().height()-fore_paddings.bottom; 1321 | int small_edge = min(size().height(), size().width()); 1322 | int pw = paint_addin.size.width() ? paint_addin.size.width() : small_edge-fore_paddings.left-fore_paddings.right; 1323 | int ph = paint_addin.size.height() ? paint_addin.size.height() : small_edge-fore_paddings.top-fore_paddings.bottom; 1324 | if (paint_addin.align & Qt::AlignLeft) 1325 | r = l + pw; 1326 | else if (paint_addin.align & Qt::AlignRight) 1327 | l = r - pw; 1328 | else if (paint_addin.align & Qt::AlignHCenter) 1329 | { 1330 | l = size().width()/2-pw/2; 1331 | r = l+pw; 1332 | } 1333 | if (paint_addin.align & Qt::AlignTop) 1334 | b = t + ph; 1335 | else if (paint_addin.align & Qt::AlignBottom) 1336 | t = b - ph; 1337 | else if (paint_addin.align & Qt::AlignVCenter) 1338 | { 1339 | t = size().height()/2-ph/2; 1340 | b = t+ph; 1341 | } 1342 | painter.drawPixmap(QRect(l,t,r-l,b-t), paint_addin.pixmap); 1343 | } 1344 | 1345 | QRect rect(fore_paddings.left+(fixed_fore_pos?0:offset_pos.x()), fore_paddings.top+(fixed_fore_pos?0:offset_pos.y()), // 原来的位置,不包含点击、出现效果 1346 | (size().width()-fore_paddings.left-fore_paddings.right), 1347 | size().height()-fore_paddings.top-fore_paddings.bottom); 1348 | 1349 | // 抖动出现动画 1350 | if ((show_ani_appearing || show_ani_disappearing) && show_ani_point != QPoint( 0, 0 ) && ! fixed_fore_pos) 1351 | { 1352 | //int w = size().width(), h = size().height(); 1353 | int pro = getSpringBackProgress(show_ani_progress, 50); 1354 | 1355 | // show_ani_point 是鼠标进入的点,那么起始方向应该是相反的 1356 | int x = show_ani_point.x(), y = show_ani_point.y(); 1357 | int gen = quick_sqrt(x*x + y*y); 1358 | x = water_radius * x / gen; // 动画起始中心点横坐标 反向 1359 | y = water_radius * y / gen; // 动画起始中心点纵坐标 反向 1360 | 1361 | rect = QRect( 1362 | rect.left() - x * (100-pro) / 100 + rect.width() * (100-pro) / 100, 1363 | rect.top() - y * (100-pro) / 100 + rect.height() * (100-pro) / 100, 1364 | rect.width() * pro / 100, 1365 | rect.height() * pro / 100 1366 | ); 1367 | 1368 | } 1369 | else if (align == Qt::AlignCenter && model != PaintModel::Text && !fixed_fore_size) // 默认的缩放动画 1370 | { 1371 | int delta_x = 0, delta_y = 0; 1372 | if (click_ani_progress != 0) // 图标缩放 1373 | { 1374 | delta_x = rect.width() * click_ani_progress / 400; 1375 | delta_y = rect.height() * click_ani_progress / 400; 1376 | } 1377 | else if (show_ani_appearing) 1378 | { 1379 | /*int pro; // 将动画进度转换为回弹动画进度 1380 | if (show_ani_progress <= 50) 1381 | pro = show_ani_progress * 2; 1382 | else if (show_ani_progress <= 75) 1383 | pro = (show_ani_progress-50)/2 + 100; 1384 | else 1385 | pro = 100 + (100-show_ani_progress)/2; 1386 | 1387 | delta_x = rect.width() * (100-pro) / 100; 1388 | delta_y = rect.height() * (100-pro) / 100;*/ 1389 | 1390 | double pro = getNolinearProg(show_ani_progress, SpringBack50); 1391 | delta_x = static_cast(rect.width() * (1-pro)); 1392 | delta_y = static_cast(rect.height() * (1-pro)); 1393 | } 1394 | else if (show_ani_disappearing) 1395 | { 1396 | double pro = 1 - getNolinearProg(show_ani_progress, SlowFaster); 1397 | delta_x = rect.width() * pro; // (100-show_ani_progress) / 100; 1398 | delta_y = rect.height() * pro; // (100-show_ani_progress) / 100; 1399 | } 1400 | if (delta_x || delta_y) 1401 | rect = QRect(rect.left()+delta_x, rect.top()+delta_y, 1402 | rect.width()-delta_x*2, rect.height()-delta_y*2); 1403 | } 1404 | 1405 | /*if (this->isEnabled()) 1406 | { 1407 | QColor color = icon_color; 1408 | color.setAlpha(color.alpha() / 2); 1409 | painter.setPen(color); 1410 | }*/ 1411 | 1412 | if (model == None) 1413 | { 1414 | // 子类自己的绘制内容 1415 | } 1416 | else if (model == Text) 1417 | { 1418 | // 绘制文字教程: https://blog.csdn.net/temetnosce/article/details/78068464 1419 | painter.setPen(isEnabled()?text_color:getOpacityColor(text_color)); 1420 | /*if (show_ani_appearing || show_ani_disappearing) 1421 | { 1422 | int pro = getSpringBackProgress(show_ani_progress, 50); 1423 | QFont font = painter.font(); 1424 | int ps = font.pointSize(); 1425 | ps = ps * show_ani_progress / 100; 1426 | font.setPointSize(ps); 1427 | painter.setFont(font); 1428 | }*/ 1429 | if (font_size > 0) 1430 | { 1431 | QFont font = painter.font(); 1432 | font.setPointSize(font_size); 1433 | painter.setFont(font); 1434 | } 1435 | painter.drawText(rect, static_cast(align), text); 1436 | } 1437 | else if (model == Icon) // 绘制图标 1438 | { 1439 | icon.paint(&painter, rect, align, getIconMode()); 1440 | } 1441 | else if (model == PixmapMask) 1442 | { 1443 | painter.setRenderHint(QPainter::SmoothPixmapTransform, true); // 可以让边缘看起来平滑一些 1444 | painter.drawPixmap(rect, pixmap); 1445 | } 1446 | else if (model == IconText || model == PixmapText) // 强制左对齐;左图标中文字 1447 | { 1448 | // 绘制图标 1449 | int& sz = icon_text_size; 1450 | QRect icon_rect(rect.left(), rect.top() + rect.height()/2 - sz / 2, sz, sz); 1451 | icon_rect.moveTo(icon_rect.left() - quick_sqrt(offset_pos.x()), icon_rect.top() - quick_sqrt(offset_pos.y())); 1452 | drawIconBeforeText(painter, icon_rect); 1453 | rect.setLeft(rect.left() + sz + icon_text_padding); 1454 | 1455 | // 绘制文字 1456 | // 扩展文字范围,确保文字可见 1457 | painter.setPen(isEnabled()?text_color:getOpacityColor(text_color)); 1458 | rect.setWidth(rect.width() + sz + icon_text_padding); 1459 | if (font_size > 0) 1460 | { 1461 | QFont font = painter.font(); 1462 | font.setPointSize(font_size); 1463 | painter.setFont(font); 1464 | } 1465 | painter.drawText(rect, Qt::AlignLeft | Qt::AlignVCenter, text); 1466 | } 1467 | } 1468 | 1469 | // ==== 绘制鼠标位置 ==== 1470 | // painter.drawEllipse(QRect(anchor_pos.x()-5, anchor_pos.y()-5, 10, 10)); // 移动锚点 1471 | // painter.drawEllipse(QRect(effect_pos.x()-2, effect_pos.y()-2, 4, 4)); // 影响位置锚点 1472 | 1473 | // return QPushButton::paintEvent(event); // 不绘制父类背景了 1474 | } 1475 | 1476 | /** 1477 | * IconText/PixmapText模式下,绘制图标 1478 | * 可扩展到绘制图标背景色(模仿menu选中、禁用情况)等 1479 | */ 1480 | void InteractiveButtonBase::drawIconBeforeText(QPainter& painter, QRect icon_rect) 1481 | { 1482 | if (model == IconText) 1483 | icon.paint(&painter, icon_rect, align, getIconMode()); 1484 | else if (model == PixmapText) 1485 | painter.drawPixmap(icon_rect, pixmap); 1486 | } 1487 | 1488 | /** 1489 | * 判断坐标是否在按钮区域内 1490 | * 避免失去了焦点,但是依旧需要 hover 效果(非菜单和弹窗抢走焦点) 1491 | * 为子类异形按钮区域判断提供支持 1492 | * @param point 当前鼠标 1493 | * @return 是否在区域内 1494 | */ 1495 | bool InteractiveButtonBase::inArea(QPoint point) 1496 | { 1497 | return !(point.x() < 0 || point.y() < 0 || point.x() > size().width() || point.y() > size().height()); 1498 | } 1499 | 1500 | /** 1501 | * 获取按钮背景的绘制区域 1502 | * 为子类异形按钮提供支持 1503 | * @return [description] 1504 | */ 1505 | QPainterPath InteractiveButtonBase::getBgPainterPath() 1506 | { 1507 | QPainterPath path; 1508 | if (radius_x || radius_y) 1509 | path.addRoundedRect(QRect(0,0,size().width(),size().height()), radius_x, radius_y); 1510 | else 1511 | path.addRect(QRect(0,0,size().width(),size().height())); 1512 | return path; 1513 | } 1514 | 1515 | /** 1516 | * 获取水波纹绘制区域(圆形,但不规则区域) 1517 | * 圆形水面 & 按钮区域 1518 | * @param water 一面水波纹动画对象 1519 | * @return 绘制路径 1520 | */ 1521 | QPainterPath InteractiveButtonBase::getWaterPainterPath(InteractiveButtonBase::Water water) 1522 | { 1523 | double prog = getNolinearProg(water.progress, FastSlower); 1524 | int ra = water_radius*prog; 1525 | QRect circle(water.point.x() - ra, 1526 | water.point.y() - ra, 1527 | ra*2, 1528 | ra*2); 1529 | /*QRect circle(water.point.x() - water_radius*water.progress/100, 1530 | water.point.y() - water_radius*water.progress/100, 1531 | water_radius*water.progress/50, 1532 | water_radius*water.progress/50);*/ 1533 | QPainterPath path; 1534 | path.addEllipse(circle); 1535 | if (radius_x || radius_y) 1536 | return path & getBgPainterPath(); 1537 | return path; 1538 | } 1539 | 1540 | /** 1541 | * 获取统一的尺寸大小(已废弃) 1542 | * 兼容圆形按钮出现动画,半径使用水波纹(对角线) 1543 | * 可直接使用 protected 对象 1544 | * @return 前景绘制区域 1545 | */ 1546 | QRect InteractiveButtonBase::getUnifiedGeometry() 1547 | { 1548 | // 将动画进度转换为回弹动画进度 1549 | int pro = show_ani_appearing ? getSpringBackProgress(show_ani_progress,50) : show_ani_progress; 1550 | int ul = 0, ut = 0, uw = size().width(), uh = size().height(); 1551 | 1552 | // show_ani_point 是鼠标进入的点,那么起始方向应该是相反的 1553 | int x = show_ani_point.x(), y = show_ani_point.y(); 1554 | int gen = quick_sqrt(x*x + y*y); 1555 | x = - water_radius * x / gen; // 动画起始中心点横坐标 反向 1556 | y = - water_radius * y / gen; // 动画起始中心点纵坐标 反向 1557 | 1558 | ul = ul + x * (100-pro) / 100 + uw * (100-pro) / 200; 1559 | ut = ut + y * (100-pro) / 100 + uh * (100-pro) / 200; 1560 | uw = uw * pro / 100; 1561 | uh = uh * pro / 100; 1562 | 1563 | return QRect(ul, ut, uw, uh); 1564 | } 1565 | 1566 | /** 1567 | * 更新统一绘制区域 1568 | * 内部的 _l, _t, _w, _h 可直接使用 1569 | */ 1570 | void InteractiveButtonBase::updateUnifiedGeometry() 1571 | { 1572 | _l = 0; _t = 0; _w = geometry().width(); _h = geometry().height(); 1573 | if ((show_ani_appearing || show_ani_disappearing) && show_ani_point != QPoint( 0, 0 )) 1574 | { 1575 | int pro; // 将动画进度转换为回弹动画进度 1576 | pro = show_ani_appearing ? getSpringBackProgress(show_ani_progress,50) : show_ani_progress; 1577 | 1578 | // show_ani_point 是鼠标进入的点,那么起始方向应该是相反的 1579 | int x = show_ani_point.x(), y = show_ani_point.y(); 1580 | int gen = quick_sqrt(x*x + y*y); 1581 | x = - water_radius * x / gen; // 动画起始中心点横坐标 反向 1582 | y = - water_radius * y / gen; // 动画起始中心点纵坐标 反向 1583 | 1584 | _l = _l + x * (100-pro) / 100 + _w * (100-pro) / 200; 1585 | _t = _t + y * (100-pro) / 100 + _h * (100-pro) / 200; 1586 | _w = _w * pro / 100; 1587 | _h = _h * pro / 100; 1588 | } 1589 | } 1590 | 1591 | /** 1592 | * 绘制一个水波纹动画 1593 | * @param painter 绘制对象(即painter(this)对象) 1594 | */ 1595 | void InteractiveButtonBase::paintWaterRipple(QPainter& painter) 1596 | { 1597 | QColor water_finished_color(press_bg); 1598 | 1599 | for (int i = 0; i < waters.size(); i++) 1600 | { 1601 | Water water = waters.at(i); 1602 | if (water.finished) // 渐变消失 1603 | { 1604 | water_finished_color.setAlpha(press_bg.alpha() * water.progress / 100); 1605 | QPainterPath path_back = getBgPainterPath(); 1606 | // painter.setPen(water_finished_color); 1607 | painter.fillPath(path_back, QBrush(water_finished_color)); 1608 | } 1609 | else // 圆形出现 1610 | { 1611 | QPainterPath path = getWaterPainterPath(water); 1612 | painter.fillPath(path, QBrush(press_bg)); 1613 | } 1614 | } 1615 | } 1616 | 1617 | /** 1618 | * 鼠标松开的时候,计算所有抖动效果的路径和事件 1619 | * 在每次重绘界面的时候,依次遍历所有的路径 1620 | */ 1621 | void InteractiveButtonBase::setJitter() 1622 | { 1623 | jitters.clear(); 1624 | QPoint center_pos = geometry().center()-geometry().topLeft(); 1625 | int full_manh = (anchor_pos-center_pos).manhattanLength(); // 距离 1626 | // 是否达到需要抖动的距离 1627 | if (full_manh > (geometry().topLeft() - geometry().bottomRight()).manhattanLength()) // 距离超过外接圆半径,开启抖动 1628 | { 1629 | QPoint jitter_pos(effect_pos); 1630 | full_manh = (jitter_pos-center_pos).manhattanLength(); 1631 | int manh = full_manh; 1632 | int duration = jitter_duration; 1633 | qint64 timestamp = release_timestamp; 1634 | while (manh > elastic_coefficient) 1635 | { 1636 | jitters << Jitter(jitter_pos, timestamp); 1637 | jitter_pos = center_pos - (jitter_pos - center_pos) / elastic_coefficient; 1638 | duration = jitter_duration * manh / full_manh; 1639 | timestamp += duration; 1640 | manh = static_cast(manh / elastic_coefficient); 1641 | } 1642 | jitters << Jitter(center_pos, timestamp); 1643 | anchor_pos = mouse_pos = center_pos; 1644 | } 1645 | else if (!hovering) // 悬浮的时候依旧有效 1646 | { 1647 | // 未达到抖动距离,直接恢复 1648 | mouse_pos = center_pos; 1649 | } 1650 | } 1651 | 1652 | /** 1653 | * 速度极快的开方算法,效率未知,原理未知 1654 | * @param X 待开方的数字 1655 | * @return 平方根 1656 | */ 1657 | int InteractiveButtonBase::quick_sqrt(long X) const 1658 | { 1659 | bool fu = false; 1660 | if (X < 0) 1661 | { 1662 | fu = true; 1663 | X = -X; 1664 | } 1665 | #if !defined(Q_OS_WIN) 1666 | X = qSqrt(X); 1667 | return fu ? -X : X; 1668 | #endif 1669 | unsigned long M = static_cast(X); 1670 | unsigned int N, i; 1671 | unsigned long tmp, ttp; // 结果、循环计数 1672 | if (M == 0) // 被开方数,开方结果也为0 1673 | return 0; 1674 | N = 0; 1675 | tmp = (M >> 30); // 获取最高位:B[m-1] 1676 | M <<= 2; 1677 | if (tmp > 1) // 最高位为1 1678 | { 1679 | N ++; // 结果当前位为1,否则为默认的0 1680 | tmp -= N; 1681 | } 1682 | for (i = 15; i > 0; i--) // 求剩余的15位 1683 | { 1684 | N <<= 1; // 左移一位 1685 | tmp <<= 2; 1686 | tmp += (M >> 30); // 假设 1687 | ttp = N; 1688 | ttp = (ttp << 1) + 1; 1689 | M <<= 2; 1690 | if (tmp >= ttp) // 假设成立 1691 | { 1692 | tmp -= ttp; 1693 | N ++; 1694 | } 1695 | } 1696 | return (fu ? -1 : 1) * static_cast(N); // 不知道为什么计算出来的结果是反过来的 1697 | } 1698 | 1699 | /** 1700 | * 最大值 1701 | */ 1702 | int InteractiveButtonBase::max(int a, int b) const { return a > b ? a : b; } 1703 | 1704 | /** 1705 | * 最小值 1706 | */ 1707 | int InteractiveButtonBase::min(int a, int b) const { return a < b ? a : b; } 1708 | 1709 | /** 1710 | * 获取现行时间戳,13位,精确到毫秒 1711 | * @return 时间戳 1712 | */ 1713 | qint64 InteractiveButtonBase::getTimestamp() const 1714 | { 1715 | return QDateTime::currentDateTime().toMSecsSinceEpoch(); 1716 | } 1717 | 1718 | /** 1719 | * 是否为亮色颜色 1720 | * @param color 颜色 1721 | * @return 是否为亮色 1722 | */ 1723 | bool InteractiveButtonBase::isLightColor(QColor color) 1724 | { 1725 | return color.red()*0.299 + color.green()*0.578 + color.blue()*0.114 >= 192; 1726 | } 1727 | 1728 | /** 1729 | * 获取非线性动画在某一时间比例的动画进度 1730 | * 仅适用于弹过头效果的动画 1731 | * @param x 实际相对完整100%的动画进度 1732 | * @param max 前半部分动画进度上限 1733 | * @return 应当显示的动画进度 1734 | */ 1735 | int InteractiveButtonBase::getSpringBackProgress(int x, int max) 1736 | { 1737 | if (x <= max) 1738 | return x * 100 / max; 1739 | if (x <= max + (100-max)/2) 1740 | return (x-max)/2+100; 1741 | return 100 + (100-x)/2; 1742 | } 1743 | 1744 | /** 1745 | * 获取透明的颜色 1746 | * @param color 颜色 1747 | * @param level 比例 1748 | * @return 透明颜色 1749 | */ 1750 | QColor InteractiveButtonBase::getOpacityColor(QColor color, double level) 1751 | { 1752 | color.setAlpha(static_cast(color.alpha() * level)); 1753 | return color; 1754 | } 1755 | 1756 | /** 1757 | * 获取对应颜色的图标 pixmap 1758 | * @param p 图标 1759 | * @param c 颜色 1760 | * @return 对应颜色的图标 1761 | */ 1762 | QPixmap InteractiveButtonBase::getMaskPixmap(QPixmap p, QColor c) 1763 | { 1764 | QBitmap mask = p.mask(); 1765 | p.fill(c); 1766 | p.setMask(mask); 1767 | return p; 1768 | } 1769 | 1770 | double InteractiveButtonBase::getNolinearProg(int p, InteractiveButtonBase::NolinearType type) 1771 | { 1772 | if (p <= 0) 1773 | return 0.0; 1774 | if (p >= 100) 1775 | return 1.0; 1776 | 1777 | switch (type) 1778 | { 1779 | case Linear: 1780 | return p / 100.0; 1781 | case SlowFaster: 1782 | return p * p / 10000.0; 1783 | case FastSlower : 1784 | return quick_sqrt(p*100) / 100.0; 1785 | case SlowFastSlower: 1786 | if (p <= 50) 1787 | return p * p / 50.0; 1788 | else 1789 | return 0.5 + quick_sqrt(50*(p-50))/100.0; 1790 | case SpringBack20: 1791 | case SpringBack50: 1792 | if (p <= 50) 1793 | return p / 50.0; 1794 | else if (p < 75) 1795 | return 1.0 + (p-50) / 200.0; 1796 | else 1797 | return 1.0 + (100-p) / 200.0; 1798 | } 1799 | } 1800 | 1801 | QIcon::Mode InteractiveButtonBase::getIconMode() 1802 | { 1803 | return isEnabled() ? (getState() ? QIcon::Selected : (hovering||pressing ? QIcon::Active : QIcon::Normal)) : QIcon::Disabled; 1804 | } 1805 | 1806 | /** 1807 | * 锚点变成到鼠标位置的定时时钟 1808 | * 同步计算所有和时间或者帧数有关的动画和属性 1809 | */ 1810 | void InteractiveButtonBase::anchorTimeOut() 1811 | { 1812 | qint64 timestamp = getTimestamp(); 1813 | // ==== 背景色 ==== 1814 | /*if (hovering) // 在框内:加深 1815 | { 1816 | if (hover_progress < 100) // 先判断,再计算,可节约运算资源 1817 | hover_progress = min((timestamp - hover_timestamp) * 100 / press_bg_duration, 100); 1818 | } 1819 | else // 在框外:变浅 1820 | { 1821 | if (hover_progress > 0) 1822 | hover_progress = max((timestamp - leave_timestamp) * 100 / press_bg_duration, 0); 1823 | } 1824 | 1825 | if (pressing) 1826 | { 1827 | if (press_progress < 100) 1828 | press_progress = min(press_start + (timestamp - press_timestamp) * 100 / press_bg_duration, 100); 1829 | } 1830 | else 1831 | { 1832 | if (press_progress > 0) // 如果按下的效果还在,变浅 1833 | press_progress = max((timestamp - release_timestamp) * 100 / press_bg_duration, 0); 1834 | }*/ 1835 | 1836 | if (pressing) // 鼠标按下 1837 | { 1838 | if (press_progress < 100) // 透明渐变,且没有完成 1839 | { 1840 | press_progress += press_speed; 1841 | if (press_progress >= 100) 1842 | { 1843 | press_progress = 100; 1844 | if (mouse_press_event) 1845 | { 1846 | emit signalMousePressLater(mouse_press_event); 1847 | mouse_press_event = nullptr; 1848 | } 1849 | } 1850 | } 1851 | if (hovering && hover_progress < 100) 1852 | { 1853 | hover_progress += hover_speed; 1854 | if (hover_progress >= 100) 1855 | { 1856 | hover_progress = 100; 1857 | emit signalMouseEnterLater(); 1858 | } 1859 | } 1860 | } 1861 | else // 鼠标悬浮 1862 | { 1863 | if (press_progress>0) // 如果按下的效果还在,变浅 1864 | { 1865 | press_progress -= press_speed; 1866 | if (press_progress <= 0) 1867 | { 1868 | press_progress = 0; 1869 | if (mouse_release_event) 1870 | { 1871 | emit signalMouseReleaseLater(mouse_release_event); 1872 | mouse_release_event = nullptr; 1873 | } 1874 | } 1875 | } 1876 | 1877 | if (hovering) // 在框内:加深 1878 | { 1879 | if (hover_progress < 100) 1880 | { 1881 | hover_progress += hover_speed; 1882 | if (hover_progress >= 100) 1883 | { 1884 | hover_progress = 100; 1885 | emit signalMouseEnterLater(); 1886 | } 1887 | } 1888 | } 1889 | else // 在框外:变浅 1890 | { 1891 | if (hover_progress > 0) 1892 | { 1893 | hover_progress -= hover_speed; 1894 | if (hover_progress <= 0) 1895 | { 1896 | hover_progress = 0; 1897 | emit signalMouseLeaveLater(); 1898 | } 1899 | } 1900 | } 1901 | } 1902 | 1903 | // ==== 按下背景水波纹动画 ==== 1904 | if (water_animation) 1905 | { 1906 | for (int i = 0; i < waters.size(); i++) 1907 | { 1908 | Water& water = waters[i]; 1909 | if (water.finished) // 结束状态 1910 | { 1911 | water.progress = static_cast(100 - 100 * (timestamp-water.finish_timestamp) / water_finish_duration); 1912 | if (water.progress <= 0) 1913 | { 1914 | waters.removeAt(i--); 1915 | if (mouse_release_event) // 还没有发送按下延迟信号 1916 | { 1917 | emit signalMouseReleaseLater(mouse_release_event); 1918 | mouse_release_event = nullptr; 1919 | } 1920 | } 1921 | } 1922 | else // 正在出现状态 1923 | { 1924 | if (water.progress >= 100) // 满了 1925 | { 1926 | water.progress = 100; 1927 | if (water.release_timestamp) // 鼠标已经松开了 1928 | { 1929 | water.finished = true; // 准备结束 1930 | water.finish_timestamp = timestamp; 1931 | } 1932 | } 1933 | else // 动画中的 1934 | { 1935 | if (water.release_timestamp) // 鼠标已经松开了 1936 | { 1937 | water.progress = static_cast(100 * (water.release_timestamp - water.press_timestamp) / water_press_duration 1938 | + 100 * (timestamp - water.release_timestamp) / water_release_duration); 1939 | } 1940 | else // 鼠标一直按下 1941 | { 1942 | water.progress = static_cast(100 * (timestamp - water.press_timestamp) / water_press_duration); 1943 | } 1944 | if (water.progress >= 100) 1945 | { 1946 | water.progress = 100; 1947 | if (mouse_press_event) // 还没有发送按下延迟信号 1948 | { 1949 | emit signalMousePressLater(mouse_press_event); 1950 | mouse_press_event = nullptr; 1951 | } 1952 | } 1953 | } 1954 | } 1955 | } 1956 | } 1957 | 1958 | // ==== 出现动画 ==== 1959 | if (show_animation) 1960 | { 1961 | if (show_ani_appearing) // 出现 1962 | { 1963 | qint64 delta = getTimestamp() - show_timestamp; 1964 | if (show_ani_progress >= 100) // 出现结束 1965 | { 1966 | show_ani_appearing = false; 1967 | emit showAniFinished(); 1968 | } 1969 | else 1970 | { 1971 | show_ani_progress = static_cast(100 * delta / show_duration); 1972 | if (show_ani_progress > 100) 1973 | show_ani_progress = 100; 1974 | } 1975 | } 1976 | if (show_ani_disappearing) // 消失 1977 | { 1978 | qint64 delta = getTimestamp() - hide_timestamp; 1979 | if (show_ani_progress <= 0) // 消失结束 1980 | { 1981 | show_ani_disappearing = false; 1982 | show_foreground = false; 1983 | show_ani_point = QPoint(0,0); 1984 | emit hideAniFinished(); 1985 | } 1986 | else 1987 | { 1988 | show_ani_progress = static_cast(100 - 100 * delta / show_duration); 1989 | if (show_ani_progress < 0) 1990 | show_ani_progress = 0; 1991 | } 1992 | } 1993 | } 1994 | 1995 | // ==== 按下动画 ==== 1996 | if (click_ani_disappearing) // 点击动画效果消失 1997 | { 1998 | qint64 delta = getTimestamp()-release_timestamp-click_ani_duration; 1999 | if (delta <= 0) click_ani_progress = 100; 2000 | else click_ani_progress = static_cast(100 - delta*100 / click_ani_duration); 2001 | if (click_ani_progress < 0) 2002 | { 2003 | click_ani_progress = 0; 2004 | click_ani_disappearing = false; 2005 | emit pressAppearAniFinished(); 2006 | } 2007 | } 2008 | if (click_ani_appearing) // 点击动画效果 2009 | { 2010 | qint64 delta = getTimestamp()-release_timestamp; 2011 | if (delta <= 0) click_ani_progress = 0; 2012 | else click_ani_progress = static_cast(delta * 100 / click_ani_duration); 2013 | if (click_ani_progress > 100) 2014 | { 2015 | click_ani_progress = 100; // 保持100的状态,下次点击时回到0 2016 | click_ani_appearing = false; 2017 | click_ani_disappearing = true; 2018 | emit pressDisappearAniFinished(); 2019 | } 2020 | } 2021 | 2022 | // ==== 锚点移动 ==== 2023 | if (jitters.size() > 0) // 松开时的抖动效果 2024 | { 2025 | // 当前应该是处在最后一个点 2026 | Jitter cur = jitters.first(); 2027 | Jitter aim = jitters.at(1); 2028 | int del = static_cast(getTimestamp()-cur.timestamp); 2029 | int dur = static_cast(aim.timestamp - cur.timestamp); 2030 | effect_pos = cur.point + (aim.point-cur.point)*del/dur; 2031 | offset_pos = effect_pos- (geometry().center() - geometry().topLeft()); 2032 | 2033 | if (del >= dur) 2034 | jitters.removeFirst(); 2035 | 2036 | // 抖动结束 2037 | if (jitters.size() == 1) 2038 | { 2039 | jitters.clear(); 2040 | emit jitterAniFinished(); 2041 | } 2042 | } 2043 | else if (anchor_pos != mouse_pos) // 移动效果 2044 | { 2045 | int delta_x = anchor_pos.x() - mouse_pos.x(), 2046 | delta_y = anchor_pos.y() - mouse_pos.y(); 2047 | 2048 | anchor_pos.setX( anchor_pos.x() - quick_sqrt(delta_x) ); 2049 | anchor_pos.setY( anchor_pos.y() - quick_sqrt(delta_y) ); 2050 | 2051 | offset_pos.setX(quick_sqrt(static_cast(anchor_pos.x()-(geometry().width()>>1)))); 2052 | offset_pos.setY(quick_sqrt(static_cast(anchor_pos.y()-(geometry().height()>>1)))); 2053 | effect_pos.setX( (geometry().width() >>1) + offset_pos.x()); 2054 | effect_pos.setY( (geometry().height()>>1) + offset_pos.y()); 2055 | } 2056 | else if (!pressing && !hovering && !hover_progress && !press_progress 2057 | && !click_ani_appearing && !click_ani_disappearing && !jitters.size() && !waters.size() 2058 | && !show_ani_appearing && !show_ani_disappearing) // 没有需要加载的项,暂停(节约资源) 2059 | { 2060 | anchor_timer->stop(); 2061 | } 2062 | 2063 | // ==== 统一坐标的出现动画 ==== 2064 | if (unified_geometry) 2065 | { 2066 | updateUnifiedGeometry(); 2067 | } 2068 | 2069 | update(); 2070 | } 2071 | 2072 | /** 2073 | * 鼠标单击事件 2074 | * 实测按下后,在按钮区域弹起,不管移动多少距离都算是 clicked 2075 | */ 2076 | void InteractiveButtonBase::slotClicked() 2077 | { 2078 | click_ani_appearing = true; 2079 | click_ani_disappearing = false; 2080 | click_ani_progress = 0; 2081 | release_offset = offset_pos; 2082 | 2083 | jitters.clear(); // 清除抖动 2084 | } 2085 | 2086 | /** 2087 | * 强行关闭状态 2088 | * 以槽的形式,便与利用 2089 | */ 2090 | void InteractiveButtonBase::slotCloseState() 2091 | { 2092 | setState(false); 2093 | } 2094 | -------------------------------------------------------------------------------- /interactive_buttons/interactivebuttonbase.h: -------------------------------------------------------------------------------- 1 | #ifndef INTERACTIVEBUTTONBASE_H 2 | #define INTERACTIVEBUTTONBASE_H 3 | 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 | #define PI 3.1415926 18 | #define GOLDEN_RATIO 0.618 19 | 20 | #define DOUBLE_PRESS_INTERVAL 300 // 松开和按下的间隔。相等为双击 21 | #define SINGLE_PRESS_INTERVAL 150 // 按下时间超过这个数就是单击。相等为单击 22 | 23 | /** 24 | * Copyright (c) 2019 命燃芯乂 All rights reserved. 25 | × 26 | * 邮箱:iwxyiii@gmail.com 27 | * QQ号:482582886 28 | * 时间:2020.04.20 29 | * 30 | * 说明:灵性的自定义按钮,简单又又去 31 | * 源码:https://github.com/MRXY001/Interactive-Windows-Buttons 32 | * 33 | * 本代码为本人编写方便自己使用,现在无私送给大家免费使用。 34 | * 程序版权归作者所有,只可使用不能出售,违反者本人有权追究责任。 35 | */ 36 | 37 | class InteractiveButtonBase : public QPushButton 38 | { 39 | Q_OBJECT 40 | Q_PROPERTY(bool self_enabled READ getSelfEnabled WRITE setSelfEnabled) // 是否启用自定义的按钮(true) 41 | Q_PROPERTY(bool parent_enabled READ getParentEnabled WRITE setParentEnabled) // 是否启用父类按钮(false) 42 | Q_PROPERTY(bool fore_enabled READ getForeEnabled WRITE setForeEnabled) // 是否绘制自定义按钮前景色(true) 43 | Q_PROPERTY(QString text READ getText WRITE setText) // 前景文字 44 | Q_PROPERTY(QString icon_path READ getIconPath WRITE setIconPath) // 前景图标 45 | Q_PROPERTY(QString pixmap_path READ getPixmapPath WRITE setPixmapPath) // 前景图标 46 | Q_PROPERTY(QColor icon_color READ getIconColor WRITE setIconColor) // 前景图标帅色 47 | Q_PROPERTY(QColor text_color READ getTextColor WRITE setTextColor) // 前景文字颜色 48 | Q_PROPERTY(QColor background_color READ getNormalColor WRITE setNormalColor) // 背景颜色 49 | Q_PROPERTY(QColor border_color READ getBorderColor WRITE setBorderColor) // 边界颜色 50 | Q_PROPERTY(QColor hover_color READ getHoverColor WRITE setHoverColor) // 鼠标悬浮背景颜色 51 | Q_PROPERTY(QColor press_color READ getPressColor WRITE setPressColor) // 鼠标按下背景颜色 52 | Q_PROPERTY(int hover_duration READ getHoverAniDuration WRITE setHoverAniDuration) // 鼠标悬浮动画周期 53 | Q_PROPERTY(int press_duration READ getPressAniDuration WRITE setPressAniDuration) // 鼠标按下动画周期 54 | Q_PROPERTY(int click_duration READ getClickAniDuration WRITE setClickAniDuration) // 鼠标点击动画周期 55 | Q_PROPERTY(double icon_padding_proper READ getIconPaddingProper WRITE setIconPaddingProper) // 图标四边空白处大小比例 56 | Q_PROPERTY(int radius READ getRadius WRITE setRadius) // 边框圆角半径 57 | Q_PROPERTY(int border_width READ getBorderWidth WRITE setBorderWidth) // 边框线条粗细 58 | Q_PROPERTY(bool fixed_fore_pos READ getFixedTextPos WRITE setFixedTextPos) // 是否固定前景位置(false) 59 | Q_PROPERTY(bool text_dynamic_size READ getTextDynamicSize WRITE setTextDynamicSize) // 修改字体大小时调整按钮最小尺寸(false) 60 | Q_PROPERTY(bool leave_after_clicked READ getLeaveAfterClick WRITE setLeaveAfterClick) // 鼠标单击松开后取消悬浮效果(针对菜单、弹窗) 61 | Q_PROPERTY(bool show_animation READ getShowAni WRITE setShowAni) // 是否启用出现动画(鼠标移开则消失)(false) 62 | Q_PROPERTY(bool water_animation READ getWaterRipple WRITE setWaterRipple) // 是否启用点击水波纹动画(否则使用渐变)(true) 63 | Q_PROPERTY(int font_size READ getFontSizeT WRITE setFontSizeT) // 动:按钮字体动画效果(自动,不应该设置) 64 | public: 65 | InteractiveButtonBase(QWidget *parent = nullptr); 66 | InteractiveButtonBase(QString text, QWidget *parent = nullptr); 67 | InteractiveButtonBase(QIcon icon, QWidget *parent = nullptr); 68 | InteractiveButtonBase(QPixmap pixmap, QWidget *parent = nullptr); 69 | InteractiveButtonBase(QIcon icon, QString text, QWidget *parent = nullptr); 70 | InteractiveButtonBase(QPixmap pixmap, QString text, QWidget *parent = nullptr); 71 | 72 | /** 73 | * 前景实体 74 | */ 75 | enum PaintModel 76 | { 77 | None, // 无前景,仅使用背景 78 | Text, // 纯文字(替代父类) 79 | Icon, // 纯图标 80 | PixmapMask, // 可变色图标(通过pixmap+遮罩实现),锯齿化明显 81 | IconText, // 图标+文字(强制左对齐) 82 | PixmapText // 变色图标+文字(强制左对齐) 83 | }; 84 | 85 | /** 86 | * 前景额外的图标(可以多个) 87 | * 可能是角标(比如展开箭头) 88 | * 可能时前缀(图例) 89 | */ 90 | struct PaintAddin 91 | { 92 | PaintAddin() : enable(false) {} 93 | PaintAddin(QPixmap p, Qt::Alignment a, QSize s) : enable(true), pixmap(p), align(a), size(s) {} 94 | bool enable; // 是否启用 95 | QPixmap pixmap; // 可变色图标 96 | Qt::Alignment align; // 对齐方式 97 | QSize size; // 固定大小 98 | }; 99 | 100 | /** 101 | * 鼠标松开时抖动动画 102 | * 松开的时候计算每一次抖动距离+时间,放入队列中 103 | * 定时调整抖动的队列实体索引 104 | */ 105 | struct Jitter 106 | { 107 | Jitter(QPoint p, qint64 t) : point(p), timestamp(t) {} 108 | QPoint point; // 要运动到的目标坐标 109 | qint64 timestamp; // 运动到目标坐标应该的时间戳,结束后删除本次抖动路径对象 110 | }; 111 | 112 | /** 113 | * 鼠标按下/弹起水波纹动画 114 | * 鼠标按下时动画速度慢(压住),松开后动画速度骤然加快 115 | * 同样用队列记录所有的水波纹动画实体 116 | */ 117 | struct Water 118 | { 119 | Water(QPoint p, qint64 t) : point(p), progress(0), press_timestamp(t), 120 | release_timestamp(0), finish_timestamp(0), finished(false) {} 121 | QPoint point; 122 | int progress; // 水波纹进度100%(已弃用,当前使用时间戳) 123 | qint64 press_timestamp; // 鼠标按下时间戳 124 | qint64 release_timestamp; // 鼠标松开时间戳。与按下时间戳、现行时间戳一起成为水波纹进度计算参数 125 | qint64 finish_timestamp; // 结束时间戳。与当前时间戳相减则为渐变消失经过的时间戳 126 | bool finished; // 是否结束。结束后改为渐变消失 127 | }; 128 | 129 | /** 130 | * 四周边界的padding 131 | * 调整按钮大小时:宽度+左右、高度+上下 132 | */ 133 | struct EdgeVal 134 | { 135 | EdgeVal() {} 136 | EdgeVal(int l, int t, int r, int b) : left(l), top(t), right(r), bottom(b) {} 137 | int left, top, right, bottom; // 四个边界的空白距离 138 | }; 139 | 140 | enum NolinearType 141 | { 142 | Linear, 143 | SlowFaster, 144 | FastSlower, 145 | SlowFastSlower, 146 | SpringBack20, 147 | SpringBack50 148 | }; 149 | 150 | virtual void setText(QString text); 151 | virtual void setIconPath(QString path); 152 | virtual void setIcon(QIcon icon); 153 | virtual void setPixmapPath(QString path); 154 | virtual void setPixmap(QPixmap pixmap); 155 | virtual void setPaintAddin(QPixmap pixmap, Qt::Alignment align = Qt::AlignRight, QSize size = QSize(0, 0)); 156 | 157 | void setSelfEnabled(bool e = true); 158 | void setParentEnabled(bool e = false); 159 | void setForeEnabled(bool e = true); 160 | 161 | void setHoverAniDuration(int d); 162 | void setPressAniDuration(int d); 163 | void setClickAniDuration(int d); 164 | void setWaterAniDuration(int press, int release, int finish); 165 | void setWaterRipple(bool enable = true); 166 | void setJitterAni(bool enable = true); 167 | void setUnifyGeomerey(bool enable = true); 168 | void setBgColor(QColor bg); 169 | void setBgColor(QColor hover, QColor press); 170 | void setNormalColor(QColor color); 171 | void setBorderColor(QColor color); 172 | void setHoverColor(QColor color); 173 | void setPressColor(QColor color); 174 | void setIconColor(QColor color = QColor(0, 0, 0)); 175 | void setTextColor(QColor color = QColor(0, 0, 0)); 176 | void setFocusBg(QColor color); 177 | void setFocusBorder(QColor color); 178 | void setFontSize(int f); 179 | void setHover(); 180 | void setAlign(Qt::Alignment a); 181 | void setRadius(int r); 182 | void setRadius(int rx, int ry); 183 | void setBorderWidth(int x); 184 | void setDisabled(bool dis = true); 185 | void setPaddings(int l, int r, int t, int b); 186 | void setPaddings(int h, int v); 187 | void setPaddings(int x); 188 | void setIconPaddingProper(double x); 189 | void setFixedForePos(bool f = true); 190 | void setFixedForeSize(bool f = true, int addin = 0); 191 | void setSquareSize(); 192 | void setTextDynamicSize(bool d = true); 193 | void setLeaveAfterClick(bool l = true); 194 | void setDoubleClicked(bool e = true); 195 | void setAutoTextColor(bool a = true); 196 | void setPretendFocus(bool f = true); 197 | void setBlockHover(bool b = true); 198 | 199 | void setShowAni(bool enable = true); 200 | void showForeground(); 201 | void showForeground2(QPoint point = QPoint(0, 0)); 202 | void hideForeground(); 203 | void delayShowed(int time, QPoint point = QPoint(0, 0)); 204 | 205 | QString getText(); 206 | void setMenu(QMenu *menu); 207 | void setState(bool s = true); 208 | bool getState(); 209 | virtual void simulateStatePress(bool s = true, bool a = false); 210 | bool isHovering() { return hovering; } 211 | bool isPressing() { return pressing; } 212 | void simulateHover(); 213 | void discardHoverPress(bool force = false); 214 | 215 | bool getSelfEnabled() { return self_enabled; } 216 | bool getParentEnabled() { return parent_enabled; } 217 | bool getForeEnabled() { return fore_enabled; } 218 | QColor getIconColor() { return icon_color; } 219 | QColor getTextColor() { return text_color; } 220 | QColor getNormalColor() { return normal_bg; } 221 | QColor getBorderColor() { return border_bg; } 222 | QColor getHoverColor() { return hover_bg; } 223 | QColor getPressColor() { return press_bg; } 224 | QString getIconPath() { return ""; } 225 | QString getPixmapPath() { return ""; } 226 | int getHoverAniDuration() { return hover_bg_duration; } 227 | int getPressAniDuration() { return press_bg_duration; } 228 | int getClickAniDuration() { return click_ani_duration; } 229 | double getIconPaddingProper() { return icon_padding_proper; } 230 | int getRadius() { return qMax(radius_x, radius_y); } 231 | int getBorderWidth() { return border_width; } 232 | bool getFixedTextPos() { return fixed_fore_pos; } 233 | bool getTextDynamicSize() { return text_dynamic_size; } 234 | bool getLeaveAfterClick() { return leave_after_clicked; } 235 | bool getShowAni() { return show_animation; } 236 | bool getWaterRipple() { return water_animation; } 237 | 238 | #if QT_DEPRECATED_SINCE(5, 11) 239 | QT_DEPRECATED_X("Use InteractiveButtonBase::setFixedForePos(bool fixed = true)") 240 | void setFixedTextPos(bool f = true); 241 | #endif 242 | 243 | protected: 244 | void enterEvent(QEvent *event) override; 245 | void leaveEvent(QEvent *event) override; 246 | void mousePressEvent(QMouseEvent *event) override; 247 | void mouseReleaseEvent(QMouseEvent *event) override; 248 | void mouseMoveEvent(QMouseEvent *event) override; 249 | void resizeEvent(QResizeEvent *event) override; 250 | void focusInEvent(QFocusEvent *event) override; 251 | void focusOutEvent(QFocusEvent *event) override; 252 | void changeEvent(QEvent *event) override; 253 | void paintEvent(QPaintEvent *event) override; 254 | 255 | virtual bool inArea(QPoint point); 256 | virtual QPainterPath getBgPainterPath(); 257 | virtual QPainterPath getWaterPainterPath(Water water); 258 | virtual void drawIconBeforeText(QPainter &painter, QRect icon_rect); 259 | 260 | QRect getUnifiedGeometry(); 261 | void updateUnifiedGeometry(); 262 | void paintWaterRipple(QPainter &painter); 263 | void setJitter(); 264 | 265 | int getFontSizeT(); 266 | void setFontSizeT(int f); 267 | 268 | int max(int a, int b) const; 269 | int min(int a, int b) const; 270 | int quick_sqrt(long X) const; 271 | qint64 getTimestamp() const; 272 | bool isLightColor(QColor color); 273 | int getSpringBackProgress(int x, int max); 274 | QColor getOpacityColor(QColor color, double level = 0.5); 275 | QPixmap getMaskPixmap(QPixmap p, QColor c); 276 | 277 | double getNolinearProg(int p, NolinearType type); 278 | QIcon::Mode getIconMode(); 279 | 280 | signals: 281 | void showAniFinished(); 282 | void hideAniFinished(); 283 | void pressAppearAniFinished(); 284 | void pressDisappearAniFinished(); 285 | void jitterAniFinished(); 286 | void doubleClicked(); 287 | void signalFocusIn(); 288 | void signalFocusOut(); 289 | 290 | void signalMouseEnter(); 291 | void signalMouseEnterLater(); // 进入后延迟信号(以渐变动画完成为准,相当于可手动设置) 292 | void signalMouseLeave(); 293 | void signalMouseLeaveLater(); // 离开后延迟的信号(直至渐变动画完成(要是划过一下子离开,这个也会变快)) 294 | void signalMousePress(QMouseEvent* event); 295 | void signalMousePressLater(QMouseEvent* event); 296 | void signalMouseRelease(QMouseEvent* event); 297 | void signalMouseReleaseLater(QMouseEvent* event); 298 | 299 | public slots: 300 | virtual void anchorTimeOut(); 301 | virtual void slotClicked(); 302 | void slotCloseState(); 303 | 304 | protected: 305 | PaintModel model; 306 | QIcon icon; 307 | QString text; 308 | QPixmap pixmap; 309 | PaintAddin paint_addin; 310 | EdgeVal fore_paddings; 311 | 312 | protected: 313 | // 总体开关 314 | bool self_enabled, parent_enabled, fore_enabled; // 是否启用子类、启动父类、绘制子类前景 315 | 316 | // 出现前景的动画 317 | bool show_animation, show_foreground; 318 | bool show_ani_appearing, show_ani_disappearing; 319 | int show_duration; 320 | qint64 show_timestamp, hide_timestamp; 321 | int show_ani_progress; 322 | QPoint show_ani_point; 323 | 324 | // 鼠标开始悬浮、按下、松开、离开的坐标和时间戳 325 | // 鼠标锚点、目标锚点、当前锚点的坐标;当前XY的偏移量 326 | QPoint enter_pos, press_pos, release_pos, mouse_pos, anchor_pos /*目标锚点渐渐靠近鼠标*/; 327 | QPoint offset_pos /*当前偏移量*/, effect_pos, release_offset; // 相对中心、相对左上角、弹起时的平方根偏移 328 | bool hovering, pressing; // 是否悬浮和按下的状态机 329 | qint64 hover_timestamp, leave_timestamp, press_timestamp, release_timestamp; // 各种事件的时间戳 330 | int hover_bg_duration, press_bg_duration, click_ani_duration; // 各种动画时长 331 | 332 | // 定时刷新界面(保证动画持续) 333 | QTimer *anchor_timer; 334 | int move_speed; 335 | 336 | // 背景与前景 337 | QColor icon_color, text_color; // 前景颜色 338 | QColor normal_bg, hover_bg, press_bg, border_bg; // 各种背景颜色 339 | QColor focus_bg, focus_border; // 有焦点的颜色 340 | int hover_speed, press_start, press_speed; // 颜色渐变速度 341 | int hover_progress, press_progress; // 颜色渐变进度 342 | double icon_padding_proper; // 图标的大小比例 343 | int icon_text_padding, icon_text_size; // 图标+文字模式共存时,两者间隔、图标大小 344 | int border_width; 345 | int radius_x, radius_y; 346 | int font_size; 347 | bool fixed_fore_pos; // 鼠标进入时是否固定文字位置 348 | bool fixed_fore_size; // 鼠标进入/点击时是否固定前景大小 349 | bool text_dynamic_size; // 设置字体时自动调整最小宽高 350 | bool auto_text_color; // 动画时是否自动调整文字颜色 351 | bool focusing; // 是否获得了焦点 352 | 353 | // 鼠标单击动画 354 | bool click_ani_appearing, click_ani_disappearing; // 是否正在按下的动画效果中 355 | int click_ani_progress; // 按下的进度(使用时间差计算) 356 | QMouseEvent *mouse_press_event, *mouse_release_event; 357 | 358 | // 统一绘制图标的区域(从整个按钮变为中心三分之二,并且根据偏移计算) 359 | bool unified_geometry; // 上面用不到的话,这个也用不到…… 360 | int _l, _t, _w, _h; 361 | 362 | // 鼠标拖拽弹起来回抖动效果 363 | bool jitter_animation; // 是否开启鼠标松开时的抖动效果 364 | double elastic_coefficient; // 弹性系数 365 | QList jitters; 366 | int jitter_duration; // 抖动一次,多次效果叠加 367 | 368 | // 鼠标按下水波纹动画效果 369 | bool water_animation; // 是否开启水波纹动画 370 | QList waters; 371 | int water_press_duration, water_release_duration, water_finish_duration; 372 | int water_radius; 373 | 374 | // 其他效果 375 | Qt::Alignment align; // 文字/图标对其方向 376 | bool _state; // 一个记录状态的变量,比如是否持续 377 | bool leave_after_clicked; // 鼠标单击松开后取消悬浮效果(针对菜单、弹窗),按钮必定失去焦点 378 | bool _block_hover; // 如果有出现动画,临时屏蔽hovering效果 379 | 380 | // 双击 381 | bool double_clicked; // 开启双击 382 | QTimer *double_timer; // 双击时钟 383 | bool double_prevent; // 双击阻止单击release的flag 384 | }; 385 | 386 | #endif // INTERACTIVEBUTTONBASE_H 387 | -------------------------------------------------------------------------------- /interactive_buttons/watercirclebutton.cpp: -------------------------------------------------------------------------------- 1 | #include "watercirclebutton.h" 2 | 3 | WaterCircleButton::WaterCircleButton(QWidget* parent) : InteractiveButtonBase (parent), in_circle(false), radius(16) 4 | { 5 | 6 | } 7 | 8 | WaterCircleButton::WaterCircleButton(QIcon icon, QWidget *parent) : InteractiveButtonBase (icon, parent), in_circle(false), radius(16) 9 | { 10 | 11 | } 12 | 13 | WaterCircleButton::WaterCircleButton(QPixmap pixmap, QWidget *parent) : InteractiveButtonBase (pixmap, parent), in_circle(false), radius(16) 14 | { 15 | 16 | } 17 | 18 | void WaterCircleButton::enterEvent(QEvent *event) 19 | { 20 | 21 | } 22 | 23 | void WaterCircleButton::leaveEvent(QEvent *event) 24 | { 25 | if (in_circle && !pressing && !inArea(mapFromGlobal(QCursor::pos()))) 26 | { 27 | in_circle = false; 28 | InteractiveButtonBase::leaveEvent(event); 29 | } 30 | } 31 | 32 | void WaterCircleButton::mousePressEvent(QMouseEvent *event) 33 | { 34 | if (in_circle || (!hovering && inArea(event->pos()))) 35 | return InteractiveButtonBase::mousePressEvent(event); 36 | } 37 | 38 | void WaterCircleButton::mouseReleaseEvent(QMouseEvent *event) 39 | { 40 | if (pressing) 41 | { 42 | InteractiveButtonBase::mouseReleaseEvent(event); 43 | 44 | if (leave_after_clicked || (!inArea(event->pos()) && !pressing)) // 鼠标移出 45 | { 46 | in_circle = false; 47 | InteractiveButtonBase::leaveEvent(nullptr); 48 | } 49 | } 50 | } 51 | 52 | void WaterCircleButton::mouseMoveEvent(QMouseEvent *event) 53 | { 54 | bool is_in = inArea(event->pos()); 55 | 56 | if (is_in && !in_circle)// 鼠标移入 57 | { 58 | in_circle = true; 59 | InteractiveButtonBase::enterEvent(nullptr); 60 | } 61 | else if (!is_in && in_circle && !pressing) // 鼠标移出 62 | { 63 | in_circle = false; 64 | InteractiveButtonBase::leaveEvent(nullptr); 65 | } 66 | 67 | if (in_circle) 68 | InteractiveButtonBase::mouseMoveEvent(event); 69 | } 70 | 71 | void WaterCircleButton::resizeEvent(QResizeEvent *event) 72 | { 73 | center_pos = geometry().center() - geometry().topLeft(); 74 | radius = min(size().width(), size().height())/ 2; 75 | 76 | return InteractiveButtonBase::resizeEvent(event); 77 | } 78 | 79 | QPainterPath WaterCircleButton::getBgPainterPath() 80 | { 81 | QPainterPath path; 82 | int w = size().width(), h = size().height(); 83 | QRect rect(w/2-radius, h/2-radius, radius*2, radius*2); 84 | path.addEllipse(rect); 85 | return path; 86 | } 87 | 88 | QPainterPath WaterCircleButton::getWaterPainterPath(InteractiveButtonBase::Water water) 89 | { 90 | QPainterPath path = InteractiveButtonBase::getWaterPainterPath(water) & getBgPainterPath(); 91 | return path; 92 | } 93 | 94 | void WaterCircleButton::simulateStatePress(bool s) 95 | { 96 | in_circle = true; 97 | InteractiveButtonBase::simulateStatePress(s); 98 | in_circle = false; 99 | } 100 | 101 | bool WaterCircleButton::inArea(QPoint point) 102 | { 103 | return (point - center_pos).manhattanLength() <= radius; 104 | } 105 | -------------------------------------------------------------------------------- /interactive_buttons/watercirclebutton.h: -------------------------------------------------------------------------------- 1 | #ifndef WATERCIRCLEBUTTON_H 2 | #define WATERCIRCLEBUTTON_H 3 | 4 | #include 5 | #include 6 | #include "interactivebuttonbase.h" 7 | 8 | class WaterCircleButton : public InteractiveButtonBase 9 | { 10 | public: 11 | WaterCircleButton(QWidget* parent = nullptr); 12 | WaterCircleButton(QIcon icon, QWidget* parent = nullptr); 13 | WaterCircleButton(QPixmap pixmap, QWidget* parent = nullptr); 14 | 15 | protected: 16 | void enterEvent(QEvent* event) override; 17 | void leaveEvent(QEvent* event) override; 18 | void mousePressEvent(QMouseEvent* event) override; 19 | void mouseReleaseEvent(QMouseEvent* event) override; 20 | void mouseMoveEvent(QMouseEvent* event) override; 21 | void resizeEvent(QResizeEvent* event) override; 22 | 23 | QPainterPath getBgPainterPath() override; 24 | QPainterPath getWaterPainterPath(Water water) override; 25 | 26 | void simulateStatePress(bool s = true); 27 | bool inArea(QPoint point) override; 28 | 29 | protected: 30 | QPoint center_pos; 31 | bool in_circle; 32 | int radius; 33 | }; 34 | 35 | #endif // WATERCIRCLEBUTTON_H 36 | -------------------------------------------------------------------------------- /jelly_button_box/jellybutton.cpp: -------------------------------------------------------------------------------- 1 | #include "jellybutton.h" 2 | 3 | JellyButton::JellyButton(QIcon ic, QWidget *parent) : WaterCircleButton(ic, parent) 4 | { 5 | 6 | } 7 | 8 | JellyButton::JellyButton(QPixmap ic, QWidget *parent) : WaterCircleButton(ic, parent) 9 | { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /jelly_button_box/jellybutton.h: -------------------------------------------------------------------------------- 1 | #ifndef JELLYBUTTON_H 2 | #define JELLYBUTTON_H 3 | 4 | #include "watercirclebutton.h" 5 | 6 | class JellyButton : public WaterCircleButton 7 | { 8 | public: 9 | JellyButton(QIcon ic, QWidget* parent = nullptr); 10 | JellyButton(QPixmap ic, QWidget* parent = nullptr); 11 | }; 12 | 13 | #endif // JELLYBUTTON_H 14 | -------------------------------------------------------------------------------- /jelly_button_box/jellybuttonbox.cpp: -------------------------------------------------------------------------------- 1 | #include "jellybuttonbox.h" 2 | 3 | JellyButtonBox::JellyButtonBox(QWidget *parent) : QWidget(parent) 4 | { 5 | setAttribute(Qt::WA_DeleteOnClose, true); 6 | setWindowFlag(Qt::FramelessWindowHint, true); 7 | setFocusPolicy(Qt::StrongFocus); 8 | 9 | effect = new QGraphicsDropShadowEffect(this); 10 | effect->setOffset(0,border_size*2); 11 | QColor blur = Qt::gray; 12 | blur.setAlpha(127); 13 | effect->setColor(blur); 14 | effect->setBlurRadius(btn_radius); 15 | setGraphicsEffect(effect); 16 | } 17 | 18 | void JellyButtonBox::setColors(QColor bg, QColor fg) 19 | { 20 | bg_color = bg; 21 | fg_color = fg; 22 | } 23 | 24 | void JellyButtonBox::setSize(int outer, int inner, int spacing) 25 | { 26 | outer_radius = outer; 27 | btn_radius = inner; 28 | btn_spacing = spacing; 29 | } 30 | 31 | void JellyButtonBox::setButtonIcons(QString icon1, QString icon2, QString icon3) 32 | { 33 | QList icons = {QIcon(icon1), QIcon(icon2), QIcon(icon3)}; 34 | setButtons(icons); 35 | } 36 | 37 | void JellyButtonBox::setButtonPixmaps(QString icon1, QString icon2, QString icon3) 38 | { 39 | QList icons = {QPixmap(icon1), QPixmap(icon2), QPixmap(icon3)}; 40 | setButtons(icons); 41 | } 42 | 43 | void JellyButtonBox::setButtons(QList icons, QList texts) 44 | { 45 | int size = icons.size(); 46 | if (size > 3) 47 | size = 3; 48 | for (int i = 0; i < size; i++) 49 | { 50 | auto btn = new JellyButton(icons.at(i), this); 51 | buttons.append(btn); 52 | btn->setFixedSize(btn_radius*2, btn_radius*2); 53 | btn->setIconColor(fg_color); 54 | connect(btn, &InteractiveButtonBase::clicked, this, [=]{ 55 | emit signalButtonClicked(i); 56 | if (hide_after_click) 57 | toHide(); 58 | }); 59 | if (texts.size() > i) 60 | btn->setToolTip(texts.at(i)); 61 | btn->hide(); 62 | } 63 | } 64 | 65 | void JellyButtonBox::setButtons(QList icons, QList texts) 66 | { 67 | int size = icons.size(); 68 | if (size > 3) 69 | size = 3; 70 | for (int i = 0; i < size; i++) 71 | { 72 | auto btn = new JellyButton(icons.at(i), this); 73 | buttons.append(btn); 74 | btn->setFixedSize(btn_radius*2, btn_radius*2); 75 | btn->setIconColor(fg_color); 76 | connect(btn, &InteractiveButtonBase::clicked, this, [=]{ 77 | emit signalButtonClicked(i); 78 | if (hide_after_click) 79 | toHide(); 80 | }); 81 | if (texts.size() > i) 82 | btn->setToolTip(texts.at(i)); 83 | btn->hide(); 84 | } 85 | } 86 | 87 | void JellyButtonBox::setHideAfterClick(bool h) 88 | { 89 | hide_after_click = h; 90 | } 91 | 92 | void JellyButtonBox::exec(QPoint start_pos, QPoint end_pos) 93 | { 94 | hiding = false; 95 | show(); 96 | startAnimation1(start_pos, end_pos); 97 | setFocus(); 98 | } 99 | 100 | void JellyButtonBox::toHide() 101 | { 102 | hiding = true; 103 | endAnimation3(); 104 | } 105 | 106 | /** 107 | * 按钮出现动画 108 | * @param start_pos 开始出现的位置 109 | * @param end_pos 结束出现的位置 110 | */ 111 | void JellyButtonBox::startAnimation1(QPoint start_pos, QPoint end_pos) 112 | { 113 | QRect rect(end_pos.x()-outer_radius-border_size, end_pos.y()-outer_radius-border_size, outer_radius*2+border_size*2, outer_radius*2+border_size*2); 114 | 115 | QPropertyAnimation* geo_ani = new QPropertyAnimation(this, "geometry"); 116 | geo_ani->setStartValue(QRect(start_pos, QSize(1, 1))); 117 | geo_ani->setEndValue(rect); 118 | geo_ani->setEasingCurve(QEasingCurve::OutBack); 119 | geo_ani->setDuration(350); 120 | connect(geo_ani, SIGNAL(finished()), geo_ani, SLOT(deleteLater())); 121 | connect(geo_ani, &QPropertyAnimation::finished, this, [=]{ 122 | total_width = (btn_radius*2 + btn_spacing + outer_radius + border_size) * 2; 123 | show_prop = 100; 124 | startAnimation2(); 125 | }); 126 | geo_ani->start(); 127 | } 128 | 129 | /** 130 | * 整体扩展、按钮背景撕开 动画(反弹) 131 | */ 132 | void JellyButtonBox::startAnimation2() 133 | { 134 | QRect rect(geometry().center().x() - total_width / 2, geometry().top(), total_width, height()); 135 | int dur = 500; 136 | 137 | QPropertyAnimation* geo_ani = new QPropertyAnimation(this, "geometry"); 138 | geo_ani->setStartValue(geometry()); 139 | geo_ani->setEndValue(rect); 140 | geo_ani->setEasingCurve(QEasingCurve::OutBack); 141 | geo_ani->setDuration(dur); 142 | connect(geo_ani, SIGNAL(finished()), geo_ani, SLOT(deleteLater())); 143 | geo_ani->start(); 144 | 145 | QPropertyAnimation* step2_ani = new QPropertyAnimation(this, "step2"); 146 | step2_ani->setStartValue(0); 147 | step2_ani->setEndValue(100); 148 | step2_ani->setDuration(dur); 149 | connect(step2_ani, SIGNAL(finished()), step2_ani, SLOT(deleteLater())); 150 | connect(step2_ani, &QPropertyAnimation::valueChanged, this, [=](const QVariant& value){ 151 | if (value > 64 && icon_prop == 0) 152 | { 153 | icon_prop = 1; 154 | startAnimation3(); 155 | } 156 | }); 157 | connect(step2_ani, &QPropertyAnimation::finished, this, [=]{ 158 | expd_prop = 100; 159 | }); 160 | step2_ani->start(); 161 | } 162 | 163 | void JellyButtonBox::startAnimation3() 164 | { 165 | QPropertyAnimation* step3_ani = new QPropertyAnimation(this, "step3"); 166 | step3_ani->setStartValue(1); 167 | step3_ani->setEndValue(100); 168 | step3_ani->setDuration(270); 169 | step3_ani->setEasingCurve(QEasingCurve::OutCirc); 170 | connect(step3_ani, SIGNAL(finished()), step3_ani, SLOT(deleteLater())); 171 | connect(step3_ani, &QPropertyAnimation::finished, this, [=]{ 172 | icon_prop = 100; 173 | }); 174 | step3_ani->start(); 175 | 176 | // 显示按钮控件 177 | for (int i = 0; i < buttons.size(); i++) 178 | { 179 | buttons.at(i)->show(); 180 | } 181 | } 182 | 183 | void JellyButtonBox::endAnimation3() 184 | { 185 | QPropertyAnimation* step3_ani = new QPropertyAnimation(this, "step3"); 186 | step3_ani->setStartValue(icon_prop); 187 | step3_ani->setEndValue(0); 188 | step3_ani->setDuration(270 * icon_prop / 100); 189 | step3_ani->setEasingCurve(QEasingCurve::OutCirc); 190 | connect(step3_ani, SIGNAL(finished()), step3_ani, SLOT(deleteLater())); 191 | connect(step3_ani, &QPropertyAnimation::finished, this, [=]{ 192 | icon_prop = 0; 193 | foreach (auto button, buttons) 194 | { 195 | button->hide(); 196 | } 197 | endAnimation2(); 198 | }); 199 | step3_ani->start(); 200 | } 201 | 202 | void JellyButtonBox::endAnimation2() 203 | { 204 | int rad = outer_radius+border_size; 205 | QRect rect(geometry().center()-QPoint(rad, rad), QSize(rad*2,rad*2)); 206 | int dur = 350; 207 | 208 | QPropertyAnimation* geo_ani = new QPropertyAnimation(this, "geometry"); 209 | geo_ani->setStartValue(geometry()); 210 | geo_ani->setEndValue(rect); 211 | geo_ani->setEasingCurve(QEasingCurve::InBack); 212 | geo_ani->setDuration(dur); 213 | connect(geo_ani, SIGNAL(finished()), geo_ani, SLOT(deleteLater())); 214 | geo_ani->start(); 215 | 216 | QPropertyAnimation* step2_ani = new QPropertyAnimation(this, "step2"); 217 | step2_ani->setStartValue(expd_prop); 218 | step2_ani->setEndValue(0); 219 | step2_ani->setDuration(dur * expd_prop / 100); 220 | connect(step2_ani, SIGNAL(finished()), step2_ani, SLOT(deleteLater())); 221 | connect(step2_ani, &QPropertyAnimation::finished, this, [=]{ 222 | expd_prop = 100; 223 | endAnimation1(); 224 | }); 225 | step2_ani->start(); 226 | } 227 | 228 | void JellyButtonBox::endAnimation1() 229 | { 230 | QRect rect(geometry().center(), QSize(1,1)); 231 | 232 | QPropertyAnimation* geo_ani = new QPropertyAnimation(this, "geometry"); 233 | geo_ani->setStartValue(geometry()); 234 | geo_ani->setEndValue(rect); 235 | geo_ani->setEasingCurve(QEasingCurve::OutBack); 236 | geo_ani->setDuration(270); 237 | connect(geo_ani, SIGNAL(finished()), geo_ani, SLOT(deleteLater())); 238 | geo_ani->start(); 239 | } 240 | 241 | void JellyButtonBox::paintEvent(QPaintEvent *) 242 | { 243 | QPainter painter(this); 244 | painter.setRenderHint(QPainter::Antialiasing, true); 245 | 246 | // 绘制背景 247 | if (show_prop < 100) // 画圆 248 | { 249 | QPainterPath bg_path; 250 | bg_path.addEllipse(border_size, border_size, width()-border_size*2, height() - border_size*2); 251 | painter.fillPath(bg_path, bg_color); 252 | } 253 | else // 圆角矩形 254 | { 255 | QPainterPath bg_path; 256 | bg_path.addRoundedRect(border_size, border_size, width()-border_size*2, height() - border_size*2, outer_radius, outer_radius); 257 | painter.fillPath(bg_path, bg_color); 258 | } 259 | 260 | // 绘制前景 261 | if (show_prop < 100) 262 | { 263 | QPainterPath fg_path; 264 | int real_radius = (width()-border_size*2) * btn_radius / outer_radius / 2; 265 | fg_path.addEllipse(width()/2-real_radius, height()/2-real_radius, real_radius*2, real_radius*2); 266 | painter.fillPath(fg_path, fg_color); 267 | } 268 | else if (expd_prop < 100) 269 | { 270 | QPainterPath fg_path; 271 | // fg_path.addRoundedRect(outer_radius-btn_radius+border_size, outer_radius-btn_radius+border_size, width()-border_size*2-(outer_radius-btn_radius)*2, height()-border_size*2-(outer_radius-btn_radius)*2, btn_radius, btn_radius); 272 | // 计算三个分开的曲线 273 | // 进度:expd_prop / 100 274 | // 总宽度:total_width 275 | // 最小宽度:(outer_radius + border_size)*2 276 | 277 | int ctry = height()/2; 278 | QPoint left(border_size + outer_radius, ctry), 279 | mid(width() / 2, ctry), 280 | right(width() - border_size - outer_radius, ctry); 281 | 282 | // 切线贝塞尔算法 283 | int delta_ol_om = mid.x()-left.x(); // 圆心之间的距离 284 | double prop = delta_ol_om/(double)(btn_spacing+btn_radius*2); 285 | if (prop > 1) // 反弹效果时会变成负的 286 | prop = 1; 287 | double angle = PI/2 - (PI/2) * prop * prop; // 切线角度 288 | int radius = btn_radius + (outer_radius-btn_radius)*prop/2; 289 | if (icon_prop) // step3已经开始了, 半径改变 290 | { 291 | radius = (btn_radius + (outer_radius-btn_radius)/2) * (100-icon_prop) / 100; 292 | } 293 | 294 | int qie_delta_x = radius * cos(angle); 295 | int qie_delta_y = radius * sin(angle); 296 | QPoint qie_ulm(left.x() + qie_delta_x,left.y() - qie_delta_y); 297 | QPoint qie_uml(mid.x() - qie_delta_x, mid.y() - qie_delta_y); 298 | QPoint qie_dlm(left.x() + qie_delta_x,left.y() + qie_delta_y); 299 | QPoint qie_dml(mid.x() - qie_delta_x, mid.y() + qie_delta_y); 300 | QPoint qie_umr(mid.x() + qie_delta_x, mid.y() - qie_delta_y); 301 | QPoint qie_urm(right.x() - qie_delta_x,right.y() - qie_delta_y); 302 | QPoint qie_dmr(mid.x() + qie_delta_x, mid.y() + qie_delta_y); 303 | QPoint qie_drm(right.x() - qie_delta_x,right.y() + qie_delta_y); 304 | 305 | int dis = qie_uml.x() - qie_ulm.x(); 306 | // int dis_calc = dis*sqrt(prop) * sin(angle) * cos(angle); 307 | int dis_calc = dis*prop*prop; 308 | int ctrl_delta_x = dis_calc * cos(angle); 309 | int ctrl_delta_y = dis_calc * sin(angle); 310 | QPoint ctrl_uml = qie_uml + QPoint(-ctrl_delta_x, ctrl_delta_y); 311 | QPoint ctrl_ulm = qie_ulm + QPoint(ctrl_delta_x, ctrl_delta_y); 312 | QPoint ctrl_dlm = qie_dlm + QPoint(ctrl_delta_x, -ctrl_delta_y); 313 | QPoint ctrl_dml = qie_dml + QPoint(-ctrl_delta_x, -ctrl_delta_y); 314 | QPoint ctrl_umr = qie_umr + QPoint(ctrl_delta_x, ctrl_delta_y); 315 | QPoint ctrl_urm = qie_urm + QPoint(-ctrl_delta_x, ctrl_delta_y); 316 | QPoint ctrl_drm = qie_drm + QPoint(-ctrl_delta_x, -ctrl_delta_y); 317 | QPoint ctrl_dmr = qie_dmr + QPoint(ctrl_delta_x, -ctrl_delta_y); 318 | 319 | angle = angle * 180 / PI; // 切线弧度制转角度制,用来 arcTo 320 | double degle = 90 - angle; // 90-切线角度 321 | 322 | fg_path.moveTo(mid.x(), mid.y()-radius); 323 | fg_path.arcTo(mid.x()-radius, mid.y()-radius, radius*2, radius*2, 90, degle); 324 | fg_path.cubicTo(ctrl_uml, ctrl_ulm, qie_ulm); 325 | fg_path.arcTo(QRect(left.x()-radius, left.y()-radius, radius*2, radius*2), angle, 180+degle*2); 326 | fg_path.cubicTo(ctrl_dlm, ctrl_dml, qie_dml); 327 | fg_path.arcTo(mid.x()-radius, mid.y()-radius, radius*2, radius*2, 180+angle, degle*2); 328 | fg_path.cubicTo(ctrl_dmr, ctrl_drm, qie_drm); 329 | fg_path.arcTo(right.x()-radius, right.y()-radius, radius*2, radius*2, 180+angle, 180+degle*2); 330 | fg_path.cubicTo(ctrl_urm, ctrl_umr, qie_umr); 331 | fg_path.arcTo(mid.x()-radius, mid.y()-radius, radius*2, radius*2, angle, degle); 332 | fg_path.lineTo(mid.x(), mid.y()-radius); 333 | 334 | QColor c = fg_color; 335 | c.setAlpha(qMax(c.alpha() * (100-icon_prop*4) / 100, 0)); 336 | painter.fillPath(fg_path, c); 337 | } 338 | else if (icon_prop < 100) // 背景收缩,图标出现 339 | { 340 | QPainterPath fg_path; 341 | int ctry = height()/2; 342 | QPoint left(border_size + outer_radius, ctry), 343 | mid(width() / 2, ctry), 344 | right(width() - border_size - outer_radius, ctry); 345 | int radius = (btn_radius + (outer_radius-btn_radius)/2) * (100-icon_prop) / 100; // 应该的半径 346 | fg_path.addEllipse(left.x()-radius, left.y()-radius, radius*2, radius*2); 347 | fg_path.addEllipse(mid.x()-radius, mid.y()-radius, radius*2, radius*2); 348 | fg_path.addEllipse(right.x()-radius, right.y()-radius, radius*2, radius*2); 349 | 350 | QColor c = fg_color; 351 | c.setAlpha(qMax(c.alpha() * (100-icon_prop*4) / 100, 0)); 352 | painter.fillPath(fg_path, c); 353 | } 354 | 355 | } 356 | 357 | void JellyButtonBox::focusOutEvent(QFocusEvent *event) 358 | { 359 | QWidget::focusOutEvent(event); 360 | 361 | if (!hiding) 362 | toHide(); 363 | } 364 | 365 | void JellyButtonBox::setStep1(int p) 366 | { 367 | show_prop = p; 368 | } 369 | 370 | void JellyButtonBox::setStep2(int p) 371 | { 372 | expd_prop = p; 373 | } 374 | 375 | void JellyButtonBox::setQieAngle(int a) 376 | { 377 | qie_angle = a; 378 | } 379 | 380 | void JellyButtonBox::setStep3(int p) 381 | { 382 | icon_prop = p; 383 | foreach (auto button, buttons) { 384 | button->setIconPaddingProper(p * 0.25 / 100); 385 | button->update(); 386 | } 387 | int width = this->width(); 388 | int height = this->height(); 389 | int ctry = height/2; 390 | QPoint left(border_size + outer_radius, ctry), 391 | mid(width / 2, ctry), 392 | right(width - border_size - outer_radius, ctry); 393 | if (buttons.size() > 0) 394 | buttons.at(0)->move(left.x()-btn_radius, left.y()-btn_radius); 395 | if (buttons.size() > 1) 396 | buttons.at(1)->move(mid.x()-btn_radius, mid.y()-btn_radius); 397 | if (buttons.size() > 2) 398 | buttons.at(2)->move(right.x()-btn_radius, right.y()-btn_radius); 399 | update(); 400 | } 401 | 402 | int JellyButtonBox::getStep1() 403 | { 404 | return show_prop; 405 | } 406 | 407 | int JellyButtonBox::getStep2() 408 | { 409 | return expd_prop; 410 | } 411 | 412 | int JellyButtonBox::getQieAngle() 413 | { 414 | return qie_angle; 415 | } 416 | 417 | int JellyButtonBox::getStep3() 418 | { 419 | return icon_prop; 420 | } 421 | -------------------------------------------------------------------------------- /jelly_button_box/jellybuttonbox.h: -------------------------------------------------------------------------------- 1 | #ifndef JELLYBUTTONBOX_H 2 | #define JELLYBUTTONBOX_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "jellybutton.h" 10 | 11 | class JellyButtonBox : public QWidget 12 | { 13 | Q_OBJECT 14 | Q_PROPERTY(int step1 READ getStep1 WRITE setStep1) 15 | Q_PROPERTY(int step2 READ getStep2 WRITE setStep2) 16 | Q_PROPERTY(int qie_angle READ getQieAngle WRITE setQieAngle) 17 | Q_PROPERTY(int step3 READ getStep3 WRITE setStep3) 18 | public: 19 | JellyButtonBox(QWidget *parent = nullptr); 20 | 21 | void setColors(QColor bg, QColor fg); 22 | void setSize(int outer, int inner, int spacing); 23 | void setButtonIcons(QString icon1, QString icon2, QString icon3); 24 | void setButtonPixmaps(QString icon1, QString icon2, QString icon3); 25 | void setButtons(QList icons, QList texts = {}); 26 | void setButtons(QList icons, QList texts = {}); 27 | void setHideAfterClick(bool h = true); 28 | void exec(QPoint start_pos, QPoint end_pos); 29 | void toHide(); 30 | 31 | private: 32 | void startAnimation1(QPoint start_pos, QPoint end_pos); 33 | void startAnimation2(); 34 | void startAnimation3(); 35 | void endAnimation3(); 36 | void endAnimation2(); 37 | void endAnimation1(); 38 | 39 | protected: 40 | void paintEvent(QPaintEvent *) override; 41 | void focusOutEvent(QFocusEvent *event) override; 42 | 43 | signals: 44 | void signalButtonClicked(int index); 45 | 46 | public slots: 47 | 48 | private: 49 | void setStep1(int p); 50 | void setStep2(int p); 51 | void setQieAngle(int a); 52 | void setStep3(int p); 53 | int getStep1(); 54 | int getStep2(); 55 | int getQieAngle(); 56 | int getStep3(); 57 | 58 | private: 59 | QList buttons; 60 | int btn_radius = 16; // 按钮最大半径 61 | int outer_radius = 24; // 大圆半径 62 | int btn_spacing = 32; // 两个按钮之间的间距 63 | QColor fg_color = Qt::white; 64 | QColor bg_color = QColor(58, 54, 94); 65 | int border_size = 4; // 边界阴影最大处宽度 66 | int total_width = -1; // 控件真正的总宽度 67 | int qie_angle = 90; // 切线 68 | QGraphicsDropShadowEffect* effect; 69 | 70 | int show_prop = 0; // 按钮出现 动画 71 | int expd_prop = 0; // 整体扩展、按钮背景撕开 动画(反弹) 72 | int icon_prop = 0; // 背景隐藏+按钮出现 动画 73 | bool hiding = false; // 是否正在隐藏(无法点击) 74 | 75 | bool hide_after_click = true; // 按钮点击后隐藏 76 | }; 77 | 78 | #endif // JELLYBUTTONBOX_H 79 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | 3 | #include 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | QApplication a(argc, argv); 8 | MainWindow w; 9 | w.show(); 10 | return a.exec(); 11 | } 12 | -------------------------------------------------------------------------------- /mainwindow.cpp: -------------------------------------------------------------------------------- 1 | #include "mainwindow.h" 2 | #include "ui_mainwindow.h" 3 | 4 | MainWindow::MainWindow(QWidget *parent) 5 | : QMainWindow(parent) 6 | , ui(new Ui::MainWindow) 7 | { 8 | ui->setupUi(this); 9 | } 10 | 11 | MainWindow::~MainWindow() 12 | { 13 | delete ui; 14 | } 15 | 16 | 17 | void MainWindow::on_pushButton_clicked() 18 | { 19 | // 要弹出的中心点 20 | QPoint center = ui->pushButton->geometry().center(); 21 | JellyButtonBox* jbb = new JellyButtonBox(this); 22 | QList icons = {QPixmap(":/icons/icon1"), QPixmap(":/icons/icon2"), QPixmap(":/icons/icon3")}; 23 | jbb->setButtons(icons); 24 | jbb->exec(center, QPoint(center.x(), center.y()-64)); 25 | } 26 | -------------------------------------------------------------------------------- /mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include "jellybuttonbox.h" 6 | 7 | QT_BEGIN_NAMESPACE 8 | namespace Ui { class MainWindow; } 9 | QT_END_NAMESPACE 10 | 11 | class MainWindow : public QMainWindow 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | MainWindow(QWidget *parent = nullptr); 17 | ~MainWindow(); 18 | 19 | private slots: 20 | void on_pushButton_clicked(); 21 | 22 | private: 23 | Ui::MainWindow *ui; 24 | }; 25 | #endif // MAINWINDOW_H 26 | -------------------------------------------------------------------------------- /mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 546 10 | 286 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 150 21 | 160 22 | 75 23 | 23 24 | 25 | 26 | 27 | 显示 28 | 29 | 30 | 31 | 32 | 33 | 240 34 | 160 35 | 75 36 | 23 37 | 38 | 39 | 40 | 焦点 41 | 42 | 43 | 44 | 45 | 46 | 47 | 0 48 | 0 49 | 546 50 | 23 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /pictures/picture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iwxyi/Qt-JellyButtonBox/07fc3b92f83ee429a59cad1047e69ed3bb2aa325/pictures/picture.gif -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | resources/icons/icon1.png 4 | resources/icons/icon2.png 5 | resources/icons/icon3.png 6 | 7 | 8 | -------------------------------------------------------------------------------- /resources/icons/icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iwxyi/Qt-JellyButtonBox/07fc3b92f83ee429a59cad1047e69ed3bb2aa325/resources/icons/icon1.png -------------------------------------------------------------------------------- /resources/icons/icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iwxyi/Qt-JellyButtonBox/07fc3b92f83ee429a59cad1047e69ed3bb2aa325/resources/icons/icon2.png -------------------------------------------------------------------------------- /resources/icons/icon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iwxyi/Qt-JellyButtonBox/07fc3b92f83ee429a59cad1047e69ed3bb2aa325/resources/icons/icon3.png --------------------------------------------------------------------------------